From 482123638f27127ff1d509ca0e3931fb7ab66c5f Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Fri, 29 Dec 2023 16:19:10 +0100 Subject: [PATCH] commit --- deckster/src/main.rs | 67 +++++++---------- loupedeck_serial/src/characteristics.rs | 10 ++- loupedeck_serial/src/commands.rs | 41 ++++++++++- loupedeck_serial/src/device.rs | 86 +++++++++++++++++----- loupedeck_serial/src/events.rs | 11 +++ loupedeck_serial/src/lib.rs | 2 +- loupedeck_serial/src/messages.rs | 95 +++++++++++++++++++------ 7 files changed, 229 insertions(+), 83 deletions(-) diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 9c3a7c7..46cba02 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -3,8 +3,9 @@ use std::time::{Duration, Instant}; use color_eyre::eyre::ContextCompat; use color_eyre::Result; use rgb::RGB8; -use loupedeck_serial::characteristics::LoupedeckButton; -use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; +use loupedeck_serial::commands::VibrationPattern; +use loupedeck_serial::device::LoupedeckDevice; +use loupedeck_serial::events::LoupedeckEvent; fn main() -> Result<()> { let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; @@ -13,39 +14,23 @@ fn main() -> Result<()> { .wrap_err("at least one device should be connected")? .connect()?; + println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number()); device.set_brightness(1.0); - let buttons = [ - LoupedeckButton::N0, - LoupedeckButton::N1, - LoupedeckButton::N2, - LoupedeckButton::N3, - LoupedeckButton::N4, - LoupedeckButton::N5, - LoupedeckButton::N6, - LoupedeckButton::N7, - ]; + // run_vibrations(&device)?; + run_rainbow(&device)?; - let mut value = 0u8; + Ok(()) +} +fn run_rainbow(device: &LoupedeckDevice) -> Result<()> { + let interval = Duration::from_millis(50); let start = Instant::now(); + let mut iteration = 0; + + let buttons = device.characteristics().available_buttons.iter().filter(|b| b.supports_color()).collect::>(); + loop { - while !device.events_channel().is_empty() { - let event = device.events_channel().recv().unwrap(); - - match event { - LoupedeckEvent::KnobRotate { direction: RotationDirection::Clockwise, .. } => { - value = value.wrapping_add(1); - println!("{}, {:#010b}, {:#018b}", value, value, ((value as u16) & 0b11111000) << 8); - } - LoupedeckEvent::KnobRotate { direction: RotationDirection::Counterclockwise, .. } => { - value = value.wrapping_sub(1); - println!("{}, {:#010b}, {:#018b}", value, value, ((value as u16) & 0b11111000) << 8); - } - _ => {} - }; - } - let ms = start.elapsed().as_millis() as u64; for (index, button) in buttons.iter().enumerate() { @@ -55,20 +40,18 @@ fn main() -> Result<()> { (((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, - )); + ))?; } - device.replace_framebuffer_area( - &device.characteristics().key_grid_display, - 0, - 0, - 90, - 90, - &[RGB8::new(value, 0, 0); 90 * 90] - )?; - - device.refresh_display(&device.characteristics().key_grid_display)?; - - sleep(Duration::from_millis(50)); + sleep((iteration + 1) * interval - start.elapsed()); + iteration += 1; } } + +fn run_vibrations(device: &LoupedeckDevice) -> Result<()> { + for event in device.events_channel() { + if let LoupedeckEvent::Touch { is_end: false, .. } = event { device.vibrate(VibrationPattern::Low) } + } + + Ok(()) +} \ No newline at end of file diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index 10462d7..5912729 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -3,7 +3,7 @@ use enumset::{enum_set, EnumSet, EnumSetType}; use crate::util::Endianness; #[derive(Debug, Ordinalize, EnumSetType)] -#[repr(u16)] +#[repr(u8)] pub enum LoupedeckKnob { KnobTopLeft = 0x01, KnobCenterLeft = 0x02, @@ -14,7 +14,7 @@ pub enum LoupedeckKnob { } #[derive(Debug, Ordinalize, EnumSetType)] -#[repr(u16)] +#[repr(u8)] pub enum LoupedeckButton { KnobTopLeft = 0x01, KnobCenterLeft = 0x02, @@ -32,6 +32,12 @@ pub enum LoupedeckButton { N7 = 0x0e, } +impl LoupedeckButton { + pub fn supports_color(&self) -> bool { + self.ordinal() >= LoupedeckButton::N0.ordinal() && self.ordinal() <= LoupedeckButton::N7.ordinal() + } +} + #[derive(Debug)] #[non_exhaustive] pub struct LoupedeckDeviceDisplayConfiguration { diff --git a/loupedeck_serial/src/commands.rs b/loupedeck_serial/src/commands.rs index 3983913..02ffd89 100644 --- a/loupedeck_serial/src/commands.rs +++ b/loupedeck_serial/src/commands.rs @@ -1,9 +1,45 @@ use bytes::Bytes; +use enum_ordinalize::Ordinalize; use rgb::RGB8; use crate::characteristics::LoupedeckButton; +#[derive(Debug, Ordinalize)] +#[repr(u8)] +pub enum VibrationPattern { + Short = 0x01, + Medium = 0x0a, + Long = 0x0f, + Low = 0x31, + ShortLow = 0x32, + ShortLower = 0x33, + Lower = 0x40, + Lowest = 0x41, + DescendSlow = 0x46, + DescendMed = 0x47, + DescendFast = 0x48, + AscendSlow = 0x52, + AscendMed = 0x53, + AscendFast = 0x58, + RevSlowest = 0x5e, + RevSlow = 0x5f, + RevMed = 0x60, + RevFast = 0x61, + RevFaster = 0x62, + RevFastest = 0x63, + RiseFall = 0x6a, + Buzz = 0x70, + Rumble5 = 0x77, + Rumble4 = 0x78, + Rumble3 = 0x79, + Rumble2 = 0x7a, + Rumble1 = 0x7b, + VeryLong = 0x76, +} + #[derive(Debug)] pub(crate) enum LoupedeckCommand { + RequestSerialNumber, + RequestFirmwareVersion, SetBrightness(f32), SetButtonColor { button: LoupedeckButton, @@ -19,5 +55,8 @@ pub(crate) enum LoupedeckCommand { }, RefreshDisplay { display_id: u8, - } + }, + Vibrate { + pattern: VibrationPattern + }, } \ No newline at end of file diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs index cf5ae96..f564df7 100644 --- a/loupedeck_serial/src/device.rs +++ b/loupedeck_serial/src/device.rs @@ -1,16 +1,15 @@ use std::io; use std::io::{Read, Write}; -use std::sync::mpsc::{channel, Sender}; +use std::sync::mpsc; use std::thread::{sleep, spawn}; use std::time::Duration; use bytes::Bytes; -use crossbeam_channel::{Receiver}; use rgb::{ComponentSlice, RGB8}; use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use thiserror::Error; use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration}; -use crate::commands::LoupedeckCommand; -use crate::events::LoupedeckEvent; +use crate::commands::{LoupedeckCommand, VibrationPattern}; +use crate::events::{LoupedeckEvent, LoupedeckInternalEvent}; use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START}; use crate::util::convert_rgb888_to_rgb565; @@ -37,8 +36,10 @@ impl AvailableLoupedeckDevice { #[derive(Debug)] pub struct LoupedeckDevice { pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, - pub(crate) events_channel: Receiver, - commands_sender: Sender, + pub(crate) serial_number: String, + pub(crate) firmware_version: String, + events_receiver: crossbeam_channel::Receiver, + commands_sender: crossbeam_channel::Sender, } #[derive(Debug, Error)] @@ -49,8 +50,11 @@ pub enum ConnectError { #[error("IO error: {0}")] IO(#[from] io::Error), - #[error("The device did not respond with the expected handshake response.")] - WrongHandshakeResponse, + #[error("The device did not respond with the expected handshake response (early-stage).")] + WrongEarlyHandshakeResponse, + + #[error("The device did not respond with the expected handshake response (late-stage).")] + WrongLateHandshakeResponse, #[error("The device was already connected.")] AlreadyConnected, @@ -62,6 +66,15 @@ pub enum RefreshDisplayError { UnknownDisplay, } +#[derive(Debug, Error)] +pub enum SetButtonColorError { + #[error("The specified button is not available for this device.")] + UnknownButton, + + #[error("The button does not allow setting a color.")] + ColorNotSupported +} + #[derive(Debug, Error)] pub enum ReplaceFramebufferAreaError { #[error("The specified display is not available for this device.")] @@ -82,16 +95,34 @@ impl LoupedeckDevice { self.characteristics } - pub fn events_channel(&self) -> Receiver { - self.events_channel.clone() + pub fn serial_number(&self) -> &String { + &self.serial_number + } + + pub fn firmware_version(&self) -> &String { + &self.firmware_version + } + + pub fn events_channel(&self) -> crossbeam_channel::Receiver { + self.events_receiver.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) { + pub fn set_button_color(&self, button: LoupedeckButton, color: RGB8) -> Result<(), SetButtonColorError> { + if !self.characteristics.available_buttons.contains(button) { + return Err(SetButtonColorError::UnknownButton); + } + + if !button.supports_color() { + return Err(SetButtonColorError::ColorNotSupported); + } + self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap(); + + Ok(()) } /// Replaces the specified framebuffer area of the display with `buffer`. @@ -163,6 +194,12 @@ impl LoupedeckDevice { Ok(()) } + pub fn vibrate(&self, pattern: VibrationPattern) { + self.commands_sender.send(LoupedeckCommand::Vibrate { + pattern + }).unwrap(); + } + pub fn discover() -> Result, serialport::Error> { let ports = serialport::available_ports()?; @@ -199,7 +236,7 @@ impl LoupedeckDevice { port.read_exact(&mut buf)?; if buf != WS_UPGRADE_RESPONSE_START.as_bytes() { - return Err(ConnectError::WrongHandshakeResponse); + return Err(ConnectError::WrongEarlyHandshakeResponse); } // I don’t know why. There is garbage in the buffer without this. @@ -207,19 +244,36 @@ impl LoupedeckDevice { port.clear(ClearBuffer::Input)?; let cloned_port = port.try_clone().expect("port must be cloneable"); - let (events_sender, events_receiver) = crossbeam_channel::unbounded::(); + let (public_events_sender, public_events_receiver) = crossbeam_channel::unbounded::(); + let (internal_events_sender, internal_events_receiver) = mpsc::sync_channel(2); spawn(move || { - read_messages_worker(port, events_sender); + read_messages_worker(port, public_events_sender, internal_events_sender); }); - let (commands_sender, commands_receiver) = channel(); + let (commands_sender, commands_receiver) = crossbeam_channel::unbounded::(); spawn(move || { write_messages_worker(cloned_port, commands_receiver); }); + commands_sender.send(LoupedeckCommand::RequestSerialNumber).unwrap(); + let serial_number = match internal_events_receiver.recv_timeout(Duration::from_secs(1)) { + Ok(LoupedeckInternalEvent::GetSerialNumberResponse { serial_number }) => Ok(serial_number), + _ => Err(ConnectError::WrongLateHandshakeResponse) + }?; + + commands_sender.send(LoupedeckCommand::RequestFirmwareVersion).unwrap(); + let firmware_version = match internal_events_receiver.recv_timeout(Duration::from_secs(1)) { + Ok(LoupedeckInternalEvent::GetFirmwareVersionResponse { firmware_version }) => Ok(firmware_version), + _ => Err(ConnectError::WrongLateHandshakeResponse) + }?; + + drop(internal_events_receiver); + Ok(LoupedeckDevice { characteristics, - events_channel: events_receiver, + serial_number, + firmware_version, + events_receiver: public_events_receiver, commands_sender, }) } diff --git a/loupedeck_serial/src/events.rs b/loupedeck_serial/src/events.rs index 629b0f5..59aa1e8 100644 --- a/loupedeck_serial/src/events.rs +++ b/loupedeck_serial/src/events.rs @@ -6,8 +6,19 @@ pub enum RotationDirection { Counterclockwise } +#[derive(Debug)] +pub(crate) enum LoupedeckInternalEvent { + GetSerialNumberResponse { + serial_number: String + }, + GetFirmwareVersionResponse { + firmware_version: String + } +} + #[derive(Debug)] pub enum LoupedeckEvent { + Disconnected, ButtonDown { button: LoupedeckButton }, diff --git a/loupedeck_serial/src/lib.rs b/loupedeck_serial/src/lib.rs index 229e249..38c4027 100644 --- a/loupedeck_serial/src/lib.rs +++ b/loupedeck_serial/src/lib.rs @@ -1,6 +1,6 @@ pub mod characteristics; pub mod device; pub mod events; +pub mod commands; mod messages; -mod commands; mod util; \ No newline at end of file diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs index 6c6dda9..203f7ed 100644 --- a/loupedeck_serial/src/messages.rs +++ b/loupedeck_serial/src/messages.rs @@ -1,14 +1,13 @@ use std::cmp::min; use std::io::ErrorKind::TimedOut; use std::io::{Read, Write}; -use std::sync::mpsc::Receiver; +use std::sync::mpsc; 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::{LoupedeckEvent, LoupedeckInternalEvent}; use crate::events::RotationDirection::{Clockwise, Counterclockwise}; pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html @@ -27,7 +26,31 @@ 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) { +enum ParseMessageResult { + InternalEvent(LoupedeckInternalEvent), + PublicEvent(LoupedeckEvent), + Nothing +} + +impl From for ParseMessageResult { + fn from(value: LoupedeckInternalEvent) -> Self { + ParseMessageResult::InternalEvent(value) + } +} + +impl From for ParseMessageResult { + fn from(value: LoupedeckEvent) -> Self { + ParseMessageResult::PublicEvent(value) + } +} + +pub(crate) fn read_messages_worker( + mut port: Box, + public_sender: crossbeam_channel::Sender, + internal_sender: mpsc::SyncSender, +) { + let mut internal_sender = Some(internal_sender); + let mut buffer = BytesMut::new(); loop { @@ -67,12 +90,24 @@ pub(crate) fn read_messages_worker(mut port: Box, sender: Sender let mut message = buffer.split_to(length); let command = message[3]; - let transaction_id = message[4]; + // let transaction_id = message[4]; message.advance(5); - let event = parse_message(command, message.freeze()); - if let Some(event) = event { - sender.send(event).unwrap(); + let result = parse_message(command, message.freeze()); + match result { + ParseMessageResult::InternalEvent(event) => { + // Does nothing after the receiving side has been closed + if let Some(sender) = internal_sender.take() { + let is_open = sender.send(event).is_ok(); + if is_open { + internal_sender = Some(sender); + } + } + } + ParseMessageResult::PublicEvent(event) => { + public_sender.send(event).unwrap() + } + ParseMessageResult::Nothing => {} } } } else { @@ -82,25 +117,35 @@ pub(crate) fn read_messages_worker(mut port: Box, sender: Sender } } -fn parse_message(command: u8, mut message: Bytes) -> Option { +fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { match command { 0x00 => { // Button - let button = LoupedeckButton::from_ordinal(message[0] as u16) + let button = LoupedeckButton::from_ordinal(message[0]) .expect("Invalid button ID"); - Some(match message[1] { + match message[1] { 0x00 => LoupedeckEvent::ButtonDown { button }, _ => LoupedeckEvent::ButtonUp { button }, - }) + }.into() } 0x01 => { // Knob - let knob = LoupedeckKnob::from_ordinal(message[0] as u16) + let knob = LoupedeckKnob::from_ordinal(message[0]) .expect("Invalid button ID"); - Some(LoupedeckEvent::KnobRotate { + LoupedeckEvent::KnobRotate { knob, direction: if message[1] == 1 { Clockwise } else { Counterclockwise }, - }) + }.into() + } + 0x03 => { + LoupedeckInternalEvent::GetSerialNumberResponse { + serial_number: String::from_utf8_lossy(&message).into_owned() + }.into() + } + 0x07 => { + LoupedeckInternalEvent::GetFirmwareVersionResponse { + firmware_version: format!("{}.{}.{}", message[0], message[1], message[2]) + }.into() } 0x4d | 0x6d => { // Touch message.advance(1); @@ -108,21 +153,20 @@ fn parse_message(command: u8, mut message: Bytes) -> Option { let y = message.get_u16(); let touch_id = message.get_u8(); - Some(LoupedeckEvent::Touch { + LoupedeckEvent::Touch { touch_id, x, y, is_end: command == 0x6d, - }) + }.into() } _ => { - // println!("Unknown command: {}", command); - None + ParseMessageResult::Nothing } } } -pub(crate) fn write_messages_worker(mut port: Box, receiver: Receiver) { +pub(crate) fn write_messages_worker(mut port: Box, receiver: crossbeam_channel::Receiver) { let mut next_transaction_id = 0; let mut send = |command_id: u8, data: Bytes| { @@ -164,12 +208,18 @@ pub(crate) fn write_messages_worker(mut port: Box, receiver: Rec for command in receiver { match command { + LoupedeckCommand::RequestSerialNumber => { + send(0x03, Bytes::new()); + } + LoupedeckCommand::RequestFirmwareVersion => { + send(0x07, Bytes::new()); + } 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])); + send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b])); } LoupedeckCommand::ReplaceFramebufferArea { display_id, @@ -193,6 +243,9 @@ pub(crate) fn write_messages_worker(mut port: Box, receiver: Rec LoupedeckCommand::RefreshDisplay { display_id } => { send(0x0f, Bytes::copy_from_slice(&[0, display_id])); } + LoupedeckCommand::Vibrate { pattern } => { + send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])); + } } } } \ No newline at end of file