Implement output

This commit is contained in:
2021-11-08 18:14:03 +01:00
parent 200946c642
commit c956ff08e5
7 changed files with 658 additions and 25 deletions

View File

@@ -1,41 +1,199 @@
pub mod output;
use std::cell::RefCell;
use output::controller::Controller;
use output::fixtures::{Brightness, DMXFixture, MovingHead};
type BoxedController = JsBox<RefCell<Controller>>;
impl Finalize for Controller {}
use neon::prelude::*;
fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
Ok(cx.string("hello from rust 🦀"))
}
fn list_ports(mut cx: FunctionContext) -> JsResult<JsArray> {
let array = cx.empty_array();
struct MyStruct {
name: String,
}
impl MyStruct {
fn new(name: String) -> Self {
println!("NEW {}", &name);
Self { name }
if let Ok(ports) = serialport::available_ports() {
for (i, port) in ports.iter().enumerate() {
let port_name = cx.string(&port.port_name);
array.set(&mut cx, i as u32, port_name)?;
}
}
Ok(array)
}
impl Drop for MyStruct {
fn drop(&mut self) {
println!("DROP {}", self.name);
fn open_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let path = cx.argument::<JsString>(0)?;
let path = path.value(&mut cx);
let obj = cx.empty_object();
let success_string;
match Controller::new(path) {
Ok(controller) => {
success_string = cx.string("success");
let boxed_controller = cx.boxed(RefCell::new(controller));
obj.set(&mut cx, "_rust_ptr", boxed_controller)?;
let set_output_function = JsFunction::new(&mut cx, set_output)?;
obj.set(&mut cx, "set", set_output_function)?;
let close_function = JsFunction::new(&mut cx, close_output)?;
obj.set(&mut cx, "close", close_function)?;
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}
impl Finalize for MyStruct {
fn finalize<'a, C: Context<'a>>(self, _: &mut C) {
println!("FINALIZE {}", self.name);
fn set_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let arg = cx.argument::<JsArray>(0)?;
let mut moving_heads = Vec::with_capacity(4);
for i in 0..4 {
// why isn't neon-serde maintained anymore? T_T
let head = arg
.get(&mut cx, i)?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
moving_heads.push(MovingHead {
start_address: head
.get(&mut cx, "startAddress")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as usize,
pan: head
.get(&mut cx, "pan")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
tilt: head
.get(&mut cx, "tilt")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
brightness: {
let brightness = head
.get(&mut cx, "brightness")?
.downcast_or_throw::<JsObject, _>(&mut cx)?;
match brightness
.get(&mut cx, "type")?
.downcast_or_throw::<JsString, _>(&mut cx)?
.value(&mut cx)
.as_str()
{
"off" => Brightness::Off,
"dimmer" => Brightness::Dimmer(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"strobe" => Brightness::Strobe(
brightness
.get(&mut cx, "value")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx),
),
"switch" => Brightness::Switch,
s => {
let err = cx.string(format!("Invalid brightness type: {}", s));
return cx.throw(err);
}
}
},
rgbw: {
let rgbw = head
.get(&mut cx, "rgbw")?
.downcast_or_throw::<JsArray, _>(&mut cx)?;
let mut vec = Vec::new();
for i in 0..4 {
let v = rgbw
.get(&mut cx, i)?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8;
vec.push(v);
}
(vec[0], vec[1], vec[2], vec[3])
},
speed: head
.get(&mut cx, "speed")?
.downcast_or_throw::<JsNumber, _>(&mut cx)?
.value(&mut cx) as u8,
})
}
let this = cx.this();
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let boxed_controller = boxed_controller.borrow_mut();
{
let mut state = boxed_controller.state.lock().unwrap();
for head in moving_heads {
head.render(&mut state.data);
}
}
let obj = cx.empty_object();
let success_text = cx.string("success".to_string());
obj.set(&mut cx, "type", success_text)?;
Ok(obj)
}
fn test_box(mut cx: FunctionContext) -> JsResult<JsBox<MyStruct>> {
let my_struct = MyStruct::new("Test Struct ayayay".to_string());
fn close_output(mut cx: FunctionContext) -> JsResult<JsObject> {
let this = cx.this();
Ok(cx.boxed(my_struct))
let boxed_controller = this
.get(&mut cx, "_rust_ptr")?
.downcast_or_throw::<BoxedController, _>(&mut cx)?;
let obj = cx.empty_object();
let success_string;
match boxed_controller.borrow_mut().stop() {
Ok(()) => {
success_string = cx.string("success");
}
Err(e) => {
success_string = cx.string("error");
let error_message = cx.string(e.to_string());
obj.set(&mut cx, "message", error_message)?;
}
}
obj.set(&mut cx, "type", success_string)?;
Ok(obj)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("hello", hello)?;
cx.export_function("gib_box", test_box)?;
cx.export_function("listPorts", list_ports)?;
cx.export_function("openOutput", open_output)?;
Ok(())
}

View File

@@ -0,0 +1,138 @@
use std::{
io,
sync::{Arc, Mutex},
thread::{self, JoinHandle},
time::{Duration, Instant},
};
use anyhow::{anyhow, Error, Result};
use serialport::SerialPort;
enum MCUResponse {
Sync,
Ack,
}
const FPS: u32 = 50;
fn poll_response(ser: &mut dyn SerialPort) -> Result<MCUResponse> {
let mut read_buffer = vec![0u8; 32];
let bytes_read;
loop {
match ser.read(read_buffer.as_mut_slice()) {
Ok(t) => {
bytes_read = t;
break;
}
Err(ref e) if e.kind() == io::ErrorKind::TimedOut => continue,
Err(e) => Err(e),
}?
}
let response = std::str::from_utf8(&read_buffer[..bytes_read])?;
match response.trim() {
"Sync." => Ok(MCUResponse::Sync),
"Ack." => Ok(MCUResponse::Ack),
s => Err(anyhow!("Unknown response: \"{}\"", s)),
}
}
pub struct Controller {
pub state: Arc<Mutex<SharedState>>,
thread: Option<JoinHandle<Result<(), Error>>>,
}
impl Controller {
pub fn new(path: String) -> Result<Self> {
let state = Arc::new(Mutex::new(SharedState {
running: true,
data: [0; 512],
}));
let ser = serialport::new(path, 500_000)
.timeout(Duration::from_millis(10))
.open()?;
let handle = {
let state = state.clone();
thread::spawn(move || controller_thread(ser, state))
};
Ok(Self {
state,
thread: Some(handle),
})
}
pub fn stop(&mut self) -> Result<()> {
{
let mut state = self.state.lock().unwrap();
state.running = false;
}
if self.thread.is_some() {
self.thread.take().unwrap().join().unwrap()
} else {
Err(anyhow!("thread was already closed"))
}
}
}
pub struct SharedState {
running: bool,
pub data: [u8; 512],
}
fn controller_thread(mut ser: Box<dyn SerialPort>, state: Arc<Mutex<SharedState>>) -> Result<()> {
let frame_time = Duration::from_secs_f64(1.0 / FPS as f64);
// wait for initial sync
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => break,
_ => continue,
}
}
'main: loop {
{
let running = state.lock().unwrap().running;
if !running {
break Ok(());
}
}
let loop_start = Instant::now();
let write_result = {
let dmx_buffer = state.lock().unwrap().data;
ser.write(&dmx_buffer)
};
if write_result.is_err() {
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Sync) => continue 'main,
_ => continue,
}
}
}
loop {
match poll_response(&mut *ser) {
Ok(MCUResponse::Ack) => break,
Ok(MCUResponse::Sync) => continue 'main,
Err(_) => continue, // Eventually the MCU will send a "Sync" again
}
}
let loop_time = loop_start.elapsed();
if loop_time < frame_time {
thread::sleep(frame_time - loop_time);
} else {
eprintln!("[DMX] loop took too long!");
}
}
}

View File

@@ -0,0 +1,58 @@
use std::f64::consts::{FRAC_PI_2, PI};
use num::traits::float::Float;
pub fn rescale<T: Float>(x: T, from: (T, T), to: (T, T)) -> T {
let (a, b) = from;
let (c, d) = to;
c + (d - c) * (x - a) / (b - a)
}
pub enum Brightness {
Off,
Dimmer(f64), // 0 to 1
Strobe(f64), // 0 to 1
Switch, // No idea what this does
}
pub struct MovingHead {
pub start_address: usize,
pub pan: f64, // -3pi/2 to 3pi/2
pub tilt: f64, // -pi/2 to pi/2
pub brightness: Brightness,
pub rgbw: (u8, u8, u8, u8), // RGBW
pub speed: u8, // reversed
}
pub trait DMXFixture {
fn render(&self, dst: &mut [u8]);
}
impl DMXFixture for MovingHead {
fn render(&self, dst: &mut [u8]) {
let pan = rescale(self.pan, (-1.5 * PI, 1.5 * PI), (255.0, 0.0)) as u8;
let pan_fine = 0;
let tilt = rescale(self.tilt, (-1.0 * FRAC_PI_2, FRAC_PI_2), (0.0, 255.0)) as u8;
let tilt_fine = 0;
let dimmer = match self.brightness {
Brightness::Off => 0,
Brightness::Dimmer(dimmer) => rescale(dimmer, (0.0, 1.0), (8.0, 134.0)) as u8,
Brightness::Strobe(strobe) => rescale(strobe, (0.0, 1.0), (135.0, 239.0)) as u8,
Brightness::Switch => 255,
};
let (r, g, b, w) = self.rgbw;
let offset = self.start_address - 1;
let channels = [
pan, pan_fine, tilt, tilt_fine, self.speed, dimmer, r, g, b, w, 0, 0, 0, 0,
];
dst[offset..offset + channels.len()].copy_from_slice(&channels);
}
}

View File

@@ -0,0 +1,2 @@
pub mod fixtures;
pub mod controller;