Implement output
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
138
rust_native_module/src/output/controller.rs
Normal file
138
rust_native_module/src/output/controller.rs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
58
rust_native_module/src/output/fixtures.rs
Normal file
58
rust_native_module/src/output/fixtures.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
rust_native_module/src/output/mod.rs
Normal file
2
rust_native_module/src/output/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod fixtures;
|
||||
pub mod controller;
|
||||
Reference in New Issue
Block a user