diff --git a/rust_native_module/Cargo.lock b/rust_native_module/Cargo.lock index e8de8d5..31e5d68 100644 --- a/rust_native_module/Cargo.lock +++ b/rust_native_module/Cargo.lock @@ -2,6 +2,66 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "CoreFoundation-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e9889e6db118d49d88d84728d0e964d973a5680befb5f85f55141beea5c20b" +dependencies = [ + "libc", + "mach 0.1.2", +] + +[[package]] +name = "IOKit-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99696c398cbaf669d2368076bdb3d627fb0ce51a26899d7c61228c5c0af3bf4a" +dependencies = [ + "CoreFoundation-sys", + "libc", + "mach 0.1.2", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -14,16 +74,66 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "697c714f50560202b1f4e2e09cd50a421881c83e9025db75d15f276616f04f40" +[[package]] +name = "libc" +version = "0.2.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" + [[package]] name = "libloading" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "winapi", ] +[[package]] +name = "libudev" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "mach" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd13ee2dd61cc82833ba05ade5a30bb3d63f7ced605ef827063c63078302de9" +dependencies = [ + "libc", +] + +[[package]] +name = "mach" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + [[package]] name = "neon" version = "0.9.1" @@ -60,11 +170,106 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02662cd2e62b131937bdef85d0918b05bc3c204daf4c64af62845403eccb60f3" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libloading", "smallvec", ] +[[package]] +name = "nix" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0eaf8df8bab402257e0a5c17a254e4cc1f72a93588a1ddfb5d356c801aa7cb" +dependencies = [ + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pkg-config" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" + [[package]] name = "proc-macro2" version = "1.0.32" @@ -83,11 +288,31 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "rust_native_module" version = "0.1.0" dependencies = [ + "anyhow", "neon", + "num", + "serialport", ] [[package]] @@ -105,6 +330,23 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "serialport" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cd7c0f22290ee2c01457009fa6fc1cae4153d5608a924e5dc423babc2c655" +dependencies = [ + "CoreFoundation-sys", + "IOKit-sys", + "bitflags", + "cfg-if 0.1.10", + "libudev", + "mach 0.2.3", + "nix", + "regex", + "winapi", +] + [[package]] name = "smallvec" version = "1.7.0" @@ -128,6 +370,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "winapi" version = "0.3.9" diff --git a/rust_native_module/Cargo.toml b/rust_native_module/Cargo.toml index da12d61..f22bd2a 100644 --- a/rust_native_module/Cargo.toml +++ b/rust_native_module/Cargo.toml @@ -12,6 +12,9 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +serialport = "4.0.1" +anyhow = "1.0.45" +num = "0.4.0" [dependencies.neon] version = "0.9" diff --git a/rust_native_module/index.d.ts b/rust_native_module/index.d.ts index ce28a3f..3dcc203 100644 --- a/rust_native_module/index.d.ts +++ b/rust_native_module/index.d.ts @@ -1,5 +1,31 @@ +type Result = + { type: "success" } & T + | { type: "error", message: string }; + +type Brightness = + { type: "off" } + | { type: "switch" } + | { type: "dimmer", value: number } + | { type: "strobe", value: number }; + +type MovingHeadState = { + start_address: number, // [0, 512] + + pan: number, // [-3pi/2, 3pi/2] + tilt: number, // [-pi/2, pi/2] + brightness: Brightness, + rgbw: [number, number, number, number], // RGBW, [0, 255] + speed: number, // [255, 0] +} + +type OutputHandle = { + set: (heads: [MovingHeadState, MovingHeadState, MovingHeadState, MovingHeadState]) => Result<{}>, + close: () => Result<{}>, +} + declare module rust_native_module { - function hello(): string; + function listPorts(): Array; + function openOutput(): Result; } export = rust_native_module; \ No newline at end of file diff --git a/rust_native_module/src/lib.rs b/rust_native_module/src/lib.rs index 69dac98..8f4a668 100644 --- a/rust_native_module/src/lib.rs +++ b/rust_native_module/src/lib.rs @@ -1,41 +1,199 @@ +pub mod output; +use std::cell::RefCell; + +use output::controller::Controller; +use output::fixtures::{Brightness, DMXFixture, MovingHead}; + +type BoxedController = JsBox>; +impl Finalize for Controller {} + use neon::prelude::*; -fn hello(mut cx: FunctionContext) -> JsResult { - Ok(cx.string("hello from rust 🦀")) -} +fn list_ports(mut cx: FunctionContext) -> JsResult { + 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 { + let path = cx.argument::(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 { + let arg = cx.argument::(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::(&mut cx)?; + + moving_heads.push(MovingHead { + start_address: head + .get(&mut cx, "startAddress")? + .downcast_or_throw::(&mut cx)? + .value(&mut cx) as usize, + + pan: head + .get(&mut cx, "pan")? + .downcast_or_throw::(&mut cx)? + .value(&mut cx), + + tilt: head + .get(&mut cx, "tilt")? + .downcast_or_throw::(&mut cx)? + .value(&mut cx), + + brightness: { + let brightness = head + .get(&mut cx, "brightness")? + .downcast_or_throw::(&mut cx)?; + + match brightness + .get(&mut cx, "type")? + .downcast_or_throw::(&mut cx)? + .value(&mut cx) + .as_str() + { + "off" => Brightness::Off, + "dimmer" => Brightness::Dimmer( + brightness + .get(&mut cx, "value")? + .downcast_or_throw::(&mut cx)? + .value(&mut cx), + ), + "strobe" => Brightness::Strobe( + brightness + .get(&mut cx, "value")? + .downcast_or_throw::(&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::(&mut cx)?; + + let mut vec = Vec::new(); + + for i in 0..4 { + let v = rgbw + .get(&mut cx, i)? + .downcast_or_throw::(&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::(&mut cx)? + .value(&mut cx) as u8, + }) } + + let this = cx.this(); + let boxed_controller = this + .get(&mut cx, "_rust_ptr")? + .downcast_or_throw::(&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> { - let my_struct = MyStruct::new("Test Struct ayayay".to_string()); +fn close_output(mut cx: FunctionContext) -> JsResult { + let this = cx.this(); - Ok(cx.boxed(my_struct)) + let boxed_controller = this + .get(&mut cx, "_rust_ptr")? + .downcast_or_throw::(&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(()) } diff --git a/rust_native_module/src/output/controller.rs b/rust_native_module/src/output/controller.rs new file mode 100644 index 0000000..b7f4868 --- /dev/null +++ b/rust_native_module/src/output/controller.rs @@ -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 { + 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>, + thread: Option>>, +} + +impl Controller { + pub fn new(path: String) -> Result { + 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, state: Arc>) -> 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!"); + } + } +} diff --git a/rust_native_module/src/output/fixtures.rs b/rust_native_module/src/output/fixtures.rs new file mode 100644 index 0000000..62463ba --- /dev/null +++ b/rust_native_module/src/output/fixtures.rs @@ -0,0 +1,58 @@ +use std::f64::consts::{FRAC_PI_2, PI}; + +use num::traits::float::Float; + +pub fn rescale(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); + } +} diff --git a/rust_native_module/src/output/mod.rs b/rust_native_module/src/output/mod.rs new file mode 100644 index 0000000..501ac5e --- /dev/null +++ b/rust_native_module/src/output/mod.rs @@ -0,0 +1,2 @@ +pub mod fixtures; +pub mod controller; \ No newline at end of file