diff --git a/Cargo.lock b/Cargo.lock index f8f6ceb..23ed517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "bytes" version = "1.5.0" @@ -107,6 +113,25 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "crossbeam-channel" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" +dependencies = [ + "cfg-if", +] + [[package]] name = "darling" version = "0.20.3" @@ -147,6 +172,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "loupedeck_serial", + "rgb", ] [[package]] @@ -271,8 +297,10 @@ name = "loupedeck_serial" version = "0.1.0" dependencies = [ "bytes", + "crossbeam-channel", "enum-ordinalize", "enumset", + "rgb", "serialport", "thiserror", ] @@ -392,6 +420,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index ef61dcd..2251420 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -5,4 +5,5 @@ edition = "2021" [dependencies] loupedeck_serial = { path = "../loupedeck_serial" } -color-eyre = "0.6.2" \ No newline at end of file +color-eyre = "0.6.2" +rgb = "0.8.37" \ No newline at end of file diff --git a/deckster/src/main.rs b/deckster/src/main.rs index bf80c76..61c84cb 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -1,14 +1,41 @@ +use std::thread::sleep; +use std::time::{Duration, Instant}; use color_eyre::eyre::ContextCompat; use color_eyre::Result; +use rgb::RGB8; +use loupedeck_serial::characteristics::LoupedeckButton; fn main() -> Result<()> { let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; - let mut device = available_devices.first() + let device = available_devices.first() .wrap_err("at least one device should be connected")? - .open()?; + .connect()?; - device.connect()?; + device.set_brightness(0.5); + + let buttons = [ + LoupedeckButton::N0, + LoupedeckButton::N1, + LoupedeckButton::N2, + LoupedeckButton::N3, + LoupedeckButton::N4, + LoupedeckButton::N5, + LoupedeckButton::N6, + LoupedeckButton::N7, + ]; + + let start = Instant::now(); + loop { + sleep(Duration::from_millis(50)); + for (index, button) in buttons.iter().enumerate() { + device.set_button_color(*button, RGB8::new( + ((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, + ((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, + ((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, + )); + } + } Ok(()) } diff --git a/loupedeck_serial/Cargo.toml b/loupedeck_serial/Cargo.toml index 2659a7a..83c7e5f 100644 --- a/loupedeck_serial/Cargo.toml +++ b/loupedeck_serial/Cargo.toml @@ -8,4 +8,6 @@ serialport = "4.3.0" enum-ordinalize = "4.3.0" enumset = "1.1.3" bytes = "1.5.0" -thiserror = "1.0.52" \ No newline at end of file +thiserror = "1.0.52" +crossbeam-channel = "0.5.10" +rgb = "0.8.37" \ No newline at end of file diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index 70fd207..85d1448 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -3,7 +3,7 @@ use enumset::{enum_set, EnumSet, EnumSetType}; #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u16)] -pub enum LoupedeckDeviceKnob { +pub enum LoupedeckKnob { KnobTopLeft = 0x01, KnobCenterLeft = 0x02, KnobBottomLeft = 0x03, @@ -14,7 +14,7 @@ pub enum LoupedeckDeviceKnob { #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u16)] -pub enum LoupedeckDeviceButton { +pub enum LoupedeckButton { KnobTopLeft = 0x01, KnobCenterLeft = 0x02, KnobBottomLeft = 0x03, @@ -40,10 +40,14 @@ pub enum LoupedeckDeviceDisplayEndianness { #[derive(Debug)] pub struct LoupedeckDeviceDisplayConfiguration { pub id: u8, - pub width: usize, - pub height: usize, - pub offset: (usize, usize), - pub endianness: LoupedeckDeviceDisplayEndianness + pub name: &'static str, + pub width: u16, + pub height: u16, + pub local_offset_x: u16, + pub local_offset_y: u16, + pub global_offset_x: u16, + pub global_offset_y: u16, + pub endianness: LoupedeckDeviceDisplayEndianness, } #[derive(Debug)] @@ -51,57 +55,107 @@ pub struct LoupedeckDeviceCharacteristics { pub vendor_id: u16, pub product_id: u16, pub name: &'static str, - pub available_knobs: EnumSet, - pub available_buttons: EnumSet, - pub key_grid_dimensions: (usize, usize), - pub displays: &'static [LoupedeckDeviceDisplayConfiguration] + pub available_knobs: EnumSet, + pub available_buttons: EnumSet, + pub key_grid_rows: u8, + pub key_grid_columns: u8, + pub key_grid_display: LoupedeckDeviceDisplayConfiguration, + pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration] +} + +impl LoupedeckDeviceCharacteristics { + pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> { + let check = |display: &&LoupedeckDeviceDisplayConfiguration| + x >= display.global_offset_x && x <= display.global_offset_x + display.width && + y >= display.global_offset_y && y <= display.global_offset_y + display.height; + + if check(&&self.key_grid_display) { + Some(&self.key_grid_display) + } else { + self.additional_displays.iter().find(check) + } + } + + pub fn get_key_at_coordinates(&self, x: u16, y: u16) -> Option { + let column_width = self.key_grid_display.width as f64 / self.key_grid_columns as f64; + let row_height = self.key_grid_display.height as f64 / self.key_grid_rows as f64; + + if x < self.key_grid_display.global_offset_x || y < self.key_grid_display.global_offset_y { + return None + } + + let local_x = x - self.key_grid_display.global_offset_x; + let local_y = y - self.key_grid_display.global_offset_y; + + if local_x >= self.key_grid_display.width || local_y >= self.key_grid_display.height { + return None + } + + let column = (local_x as f64 / column_width).floor() as u8; + let row = (local_y as f64 / row_height).floor() as u8; + + Some(row * self.key_grid_columns + column) + } } static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics { vendor_id: 0x2ec2, product_id: 0x0004, name: "Loupedeck Live", - available_knobs: enum_set!(LoupedeckDeviceKnob::KnobTopLeft - | LoupedeckDeviceKnob::KnobCenterLeft - | LoupedeckDeviceKnob::KnobBottomLeft - | LoupedeckDeviceKnob::KnobTopRight - | LoupedeckDeviceKnob::KnobCenterRight - | LoupedeckDeviceKnob::KnobBottomRight), - available_buttons: enum_set!(LoupedeckDeviceButton::KnobTopLeft - | LoupedeckDeviceButton::KnobCenterLeft - | LoupedeckDeviceButton::KnobBottomLeft - | LoupedeckDeviceButton::KnobTopRight - | LoupedeckDeviceButton::KnobCenterRight - | LoupedeckDeviceButton::KnobBottomRight - | LoupedeckDeviceButton::N0 - | LoupedeckDeviceButton::N1 - | LoupedeckDeviceButton::N2 - | LoupedeckDeviceButton::N3 - | LoupedeckDeviceButton::N4 - | LoupedeckDeviceButton::N5 - | LoupedeckDeviceButton::N6 - | LoupedeckDeviceButton::N7), - key_grid_dimensions: (4, 3), - displays: &[ - LoupedeckDeviceDisplayConfiguration { // Left + available_knobs: enum_set!(LoupedeckKnob::KnobTopLeft + | LoupedeckKnob::KnobCenterLeft + | LoupedeckKnob::KnobBottomLeft + | LoupedeckKnob::KnobTopRight + | LoupedeckKnob::KnobCenterRight + | LoupedeckKnob::KnobBottomRight), + available_buttons: enum_set!(LoupedeckButton::KnobTopLeft + | LoupedeckButton::KnobCenterLeft + | LoupedeckButton::KnobBottomLeft + | LoupedeckButton::KnobTopRight + | LoupedeckButton::KnobCenterRight + | LoupedeckButton::KnobBottomRight + | LoupedeckButton::N0 + | LoupedeckButton::N1 + | LoupedeckButton::N2 + | LoupedeckButton::N3 + | LoupedeckButton::N4 + | LoupedeckButton::N5 + | LoupedeckButton::N6 + | LoupedeckButton::N7), + key_grid_rows: 3, + key_grid_columns: 4, + key_grid_display: LoupedeckDeviceDisplayConfiguration { + id: 0x4d, + name: "center", + width: 360, + height: 270, + local_offset_x: 60, + local_offset_y: 0, + global_offset_x: 60, + global_offset_y: 0, + endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + }, + additional_displays: &[ + LoupedeckDeviceDisplayConfiguration { id: 0x4d, + name: "left", width: 60, height: 270, - offset: (0, 0), + local_offset_x: 0, + local_offset_y: 0, + global_offset_x: 0, + global_offset_y: 0, endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, }, - LoupedeckDeviceDisplayConfiguration { // Center - id: 0x4d, - width: 360, - height: 270, - offset: (60, 0), - endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, - }, - LoupedeckDeviceDisplayConfiguration { // Right + LoupedeckDeviceDisplayConfiguration { id: 0x4d, + name: "right", width: 60, height: 270, - offset: (420, 0), + local_offset_x: 420, + local_offset_y: 0, + global_offset_x: 420, + global_offset_y: 0, endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, }, ] diff --git a/loupedeck_serial/src/commands.rs b/loupedeck_serial/src/commands.rs new file mode 100644 index 0000000..6bfda8c --- /dev/null +++ b/loupedeck_serial/src/commands.rs @@ -0,0 +1,11 @@ +use rgb::RGB8; +use crate::characteristics::LoupedeckButton; + +#[derive(Debug)] +pub(crate) enum LoupedeckCommand { + SetBrightness(f32), + SetButtonColor { + button: LoupedeckButton, + color: RGB8 + } +} \ No newline at end of file diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs index 6e548d0..be87be6 100644 --- a/loupedeck_serial/src/device.rs +++ b/loupedeck_serial/src/device.rs @@ -1,52 +1,74 @@ use std::io; -use std::io::{BufReader, Read, Write}; -use std::io::ErrorKind::TimedOut; -use std::sync::Arc; -use std::sync::mpsc::{Sender, sync_channel, SyncSender}; -use std::thread::spawn; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use serialport::{SerialPort, SerialPortType}; +use std::io::{Read, Write}; +use std::sync::mpsc::{channel, Sender}; +use std::thread::{sleep, spawn}; +use std::time::Duration; +use crossbeam_channel::{Receiver}; +use rgb::RGB8; +use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use thiserror::Error; -use crate::characteristics::{CHARACTERISTICS, LoupedeckDeviceCharacteristics}; -use crate::discovery::AvailableLoupedeckDevice; +use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics}; +use crate::commands::LoupedeckCommand; +use crate::events::LoupedeckEvent; +use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START}; -const WS_UPGRADE_REQUEST: &str = r#"GET /index.html -HTTP/1.1 -Connection: Upgrade -Upgrade: websocket -Sec-WebSocket-Key: 123abc +#[derive(Debug)] +pub struct AvailableLoupedeckDevice { + pub(crate) port_name: String, + pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, +} -"#; +impl AvailableLoupedeckDevice { + pub fn port_name(&self) -> &str { + &self.port_name + } -const WS_UPGRADE_RESPONSE_START: &str = "HTTP/1.1 101 Switching Protocols\r\n\ -Upgrade: websocket\r\n\ -Connection: Upgrade\r\n\ -Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s="; + pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { + self.characteristics + } -const MESSAGE_START_BYTE: u8 = 0x82; -const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize; + pub fn connect(&self) -> Result { + LoupedeckDevice::connect(&self) + } +} #[derive(Debug)] pub struct LoupedeckDevice { - pub(crate) port: Option>, - pub characteristics: &'static LoupedeckDeviceCharacteristics, + pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, + pub(crate) events_channel: Receiver, + commands_sender: Sender, } #[derive(Debug, Error)] pub enum ConnectError { - #[error("IO Error: {0}")] + #[error("Serial port error: {0}")] + SerialPort(#[from] serialport::Error), + + #[error("IO error: {0}")] IO(#[from] io::Error), #[error("The device did not respond with the expected handshake response.")] WrongHandshakeResponse, + + #[error("The device was already connected.")] + AlreadyConnected, } impl LoupedeckDevice { - pub fn new_unchecked(port: Box, characteristics: &'static LoupedeckDeviceCharacteristics) -> LoupedeckDevice { - LoupedeckDevice { - port: Some(port), - characteristics, - } + pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { + self.characteristics + } + + pub fn events_channel(&self) -> Receiver { + self.events_channel.clone() + } + + pub fn set_brightness(&self, value: f32) { + self.commands_sender.send(LoupedeckCommand::SetBrightness(value)).unwrap(); + } + + pub fn set_button_color(&self, button: LoupedeckButton, color: RGB8) { + self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap(); } pub fn discover() -> Result, serialport::Error> { @@ -69,8 +91,15 @@ impl LoupedeckDevice { }).collect::>()) } - pub fn connect(&mut self) -> Result<(), ConnectError> { - let mut port = self.port.take().expect("already connected"); + pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result { + let mut port = serialport::new(port_name, 256000) + .data_bits(DataBits::Eight) + .stop_bits(StopBits::One) + .parity(Parity::None) + .flow_control(FlowControl::None) + .timeout(Duration::from_secs(10)) + .open()?; + port.write_all(WS_UPGRADE_REQUEST.as_bytes())?; port.flush()?; @@ -81,65 +110,25 @@ impl LoupedeckDevice { return Err(ConnectError::WrongHandshakeResponse); } - let (sender, receiver) = sync_channel::(8); - let handle = spawn(move || { - LoupedeckDevice::handle_messages(port, sender); + // I don’t know why. There is garbage in the buffer without this. + sleep(Duration::from_secs(1)); + port.clear(ClearBuffer::Input)?; + let cloned_port = port.try_clone().expect("port must be cloneable"); + + let (events_sender, events_receiver) = crossbeam_channel::unbounded::(); + spawn(move || { + read_messages_worker(port, events_sender); }); - for x in receiver { - dbg!(&x[1]); - } + let (commands_sender, commands_receiver) = channel(); + spawn(move || { + write_messages_worker(cloned_port, commands_receiver); + }); - handle.join().unwrap(); - - Ok(()) + Ok(LoupedeckDevice { + characteristics, + events_channel: events_receiver, + commands_sender + }) } - - fn handle_messages(mut port: Box, sender: SyncSender) { - let mut buffer = BytesMut::new(); - - loop { - let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH); - let read_result = port.read(&mut chunk); - - if let Err(err) = &read_result { - if err.kind() == TimedOut { - continue - } - } - - let len = read_result.unwrap(); - if len == 0 { - panic!("read 0 bytes"); - } - - chunk.truncate(len); - buffer.put(chunk); - - loop { - let start_index = buffer.iter().position(|b| *b == MESSAGE_START_BYTE); - - if let Some(start_index) = start_index { - if start_index > 0 { - buffer.advance(start_index); - - if buffer.remaining() == 0 { - break; - } - } - - let length = buffer[1] as usize + 2; - if length > buffer.remaining() { - break; - } else { - let mut message = buffer.split_to(length); - message.advance(2); - sender.send(message.freeze()).unwrap(); - } - } else { - break; - } - } - } - } -} \ No newline at end of file +} diff --git a/loupedeck_serial/src/discovery.rs b/loupedeck_serial/src/discovery.rs deleted file mode 100644 index 629ff82..0000000 --- a/loupedeck_serial/src/discovery.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::time::Duration; -use serialport::{DataBits, FlowControl, Parity, StopBits}; -use crate::characteristics::LoupedeckDeviceCharacteristics; -use crate::device::LoupedeckDevice; - -#[derive(Debug)] -pub struct AvailableLoupedeckDevice { - pub(crate) port_name: String, - pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, -} - -impl AvailableLoupedeckDevice { - pub fn port_name(&self) -> &str { - &self.port_name - } - - pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { - self.characteristics - } - - pub fn open(&self) -> Result { - Ok(LoupedeckDevice { - port: Some(serialport::new(&self.port_name, 256000) - .data_bits(DataBits::Eight) - .stop_bits(StopBits::One) - .parity(Parity::None) - .flow_control(FlowControl::None) - .timeout(Duration::from_secs(10)) - .open()?), - characteristics: self.characteristics, - }) - } -} \ No newline at end of file diff --git a/loupedeck_serial/src/events.rs b/loupedeck_serial/src/events.rs new file mode 100644 index 0000000..629b0f5 --- /dev/null +++ b/loupedeck_serial/src/events.rs @@ -0,0 +1,27 @@ +use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; + +#[derive(Debug)] +pub enum RotationDirection { + Clockwise, + Counterclockwise +} + +#[derive(Debug)] +pub enum LoupedeckEvent { + ButtonDown { + button: LoupedeckButton + }, + ButtonUp { + button: LoupedeckButton + }, + KnobRotate { + knob: LoupedeckKnob, + direction: RotationDirection + }, + Touch { + touch_id: u8, + x: u16, + y: u16, + is_end: bool + } +} \ No newline at end of file diff --git a/loupedeck_serial/src/lib.rs b/loupedeck_serial/src/lib.rs index 9347fc6..9c3ada9 100644 --- a/loupedeck_serial/src/lib.rs +++ b/loupedeck_serial/src/lib.rs @@ -1,3 +1,5 @@ pub mod characteristics; pub mod device; -pub mod discovery; \ No newline at end of file +pub mod events; +mod messages; +mod commands; \ No newline at end of file diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs new file mode 100644 index 0000000..a8eca45 --- /dev/null +++ b/loupedeck_serial/src/messages.rs @@ -0,0 +1,176 @@ +use std::cmp::min; +use std::io::ErrorKind::TimedOut; +use std::io::{Read, Write}; +use std::sync::mpsc::Receiver; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use crossbeam_channel::Sender; +use enum_ordinalize::Ordinalize; +use serialport::SerialPort; +use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; +use crate::commands::LoupedeckCommand; +use crate::events::LoupedeckEvent; +use crate::events::RotationDirection::{Clockwise, Counterclockwise}; + +pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html +HTTP/1.1 +Connection: Upgrade +Upgrade: websocket +Sec-WebSocket-Key: 123abc + +"#; + +pub(crate) const WS_UPGRADE_RESPONSE_START: &str = "HTTP/1.1 101 Switching Protocols\r\n\ +Upgrade: websocket\r\n\ +Connection: Upgrade\r\n\ +Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s="; + +const MESSAGE_START_BYTE: u8 = 0x82; +const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize; + +pub(crate) fn read_messages_worker(mut port: Box, sender: Sender) { + let mut buffer = BytesMut::new(); + + loop { + let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH); + let read_result = port.read(&mut chunk); + + if let Err(err) = &read_result { + if err.kind() == TimedOut { + continue; + } + } + + let len = read_result.unwrap(); + if len == 0 { + panic!("read 0 bytes"); + } + + chunk.truncate(len); + buffer.put(chunk); + + loop { + let start_index = buffer.iter().position(|b| *b == MESSAGE_START_BYTE); + + if let Some(start_index) = start_index { + if start_index > 0 { + buffer.advance(start_index); + + if buffer.remaining() == 0 { + break; + } + } + + let length = buffer[1] as usize + 2; + if length > buffer.remaining() { + break; + } else { + let mut message = buffer.split_to(length); + + let command = message[3]; + let transaction_id = message[4]; + message.advance(5); + + let event = parse_message(command, message.freeze()); + if let Some(event) = event { + sender.send(event).unwrap(); + } + } + } else { + break; + } + } + } +} + +fn parse_message(command: u8, mut message: Bytes) -> Option { + match command { + 0x00 => { // Button + let button = LoupedeckButton::from_ordinal(message[0] as u16) + .expect("Invalid button ID"); + + Some(match message[1] { + 0x00 => LoupedeckEvent::ButtonDown { button }, + _ => LoupedeckEvent::ButtonUp { button }, + }) + } + 0x01 => { // Knob + let knob = LoupedeckKnob::from_ordinal(message[0] as u16) + .expect("Invalid button ID"); + + Some(LoupedeckEvent::KnobRotate { + knob, + direction: if message[1] == 1 { Clockwise } else { Counterclockwise }, + }) + } + 0x4d | 0x6d => { // Touch + message.advance(1); + let x = message.get_u16(); + let y = message.get_u16(); + let touch_id = message.get_u8(); + + Some(LoupedeckEvent::Touch { + touch_id, + x, + y, + is_end: command == 0x6d, + }) + } + _ => { + println!("Unknown command: {}", command); + None + } + } +} + +pub(crate) fn write_messages_worker(mut port: Box, receiver: Receiver) { + let mut next_transaction_id = 0; + + let mut send = |command_id: u8, data: Bytes| { + if next_transaction_id == 0 { + next_transaction_id += 1; + } + + let mut data_with_header = BytesMut::with_capacity(data.len() + 3); + data_with_header.put_u8(min(u8::MAX as usize, data.len() + 3) as u8); + data_with_header.put_u8(command_id); + data_with_header.put_u8(next_transaction_id); + data_with_header.put(data); + + let length = data_with_header.len(); + if length > u8::MAX as usize { + let mut prep = BytesMut::with_capacity(14); + prep.put_u8(0x82); + prep.put_u8(0xff); + prep.put_bytes(0x00, 4); + prep.put_u32(length as u32); + prep.put_bytes(0x00, 4); + + port.write_all(&prep).unwrap(); + port.flush().unwrap(); + } else { + let mut prep = BytesMut::zeroed(6); + prep[0] = 0x82; + prep[1] = (0x80 + length) as u8; + + port.write_all(&prep).unwrap(); + port.flush().unwrap(); + } + + port.write_all(&data_with_header).unwrap(); + port.flush().unwrap(); + + next_transaction_id = next_transaction_id.wrapping_add(1); + }; + + for command in receiver { + match command { + LoupedeckCommand::SetBrightness(value) => { + let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8; + send(0x09, Bytes::copy_from_slice(&[raw_value])); + } + LoupedeckCommand::SetButtonColor { button, color } => { + send(0x02, Bytes::copy_from_slice(&[button.ordinal() as u8, color.r, color.g, color.b])); + } + } + } +} \ No newline at end of file