use std::io; use std::io::{Read, Write}; use std::sync::mpsc; use std::thread::{sleep, spawn}; use std::time::Duration; use bytes::Bytes; 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, 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; #[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 connect(&self) -> Result { LoupedeckDevice::connect(self) } } #[derive(Debug)] pub struct LoupedeckDevice { pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, pub(crate) serial_number: String, pub(crate) firmware_version: String, events_receiver: crossbeam_channel::Receiver, commands_sender: crossbeam_channel::Sender, } #[derive(Debug, Error)] pub enum ConnectError { #[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 (early-stage).")] WrongEarlyHandshakeResponse, #[error("The device did not respond with the expected handshake response (late-stage).")] WrongLateHandshakeResponse, #[error("The device was already connected.")] AlreadyConnected, } #[derive(Debug, Error)] pub enum RefreshDisplayError { #[error("The specified display is not available for this device.")] 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.")] UnknownDisplay, #[error("The area is not (fully) within the bounds of the display.")] OutOfBounds, #[error("Given the specified dimensions, the buffer size must be {expected} but it was {actual}.")] WrongBufferSize { expected: usize, actual: usize, }, } impl LoupedeckDevice { pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { self.characteristics } 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) -> 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`. /// /// `buffer` must contain exactly as many pixels as required. /// /// Please note that the internal color format of all currently known devices is RGB565 (16 bits in total). pub fn replace_framebuffer_area( &self, display: &LoupedeckDeviceDisplayConfiguration, x: u16, y: u16, width: u16, height: u16, buffer: &[RGB8] ) -> Result<(), ReplaceFramebufferAreaError> { if !std::ptr::eq(display, &self.characteristics.key_grid_display) && !self.characteristics.additional_displays.iter().any(|d| std::ptr::eq(display, d)) { return Err(ReplaceFramebufferAreaError::UnknownDisplay); } if x + width > display.width || y + height > display.height { return Err(ReplaceFramebufferAreaError::OutOfBounds); } let expected_buffer_size = (height * width) as usize; if buffer.len() != expected_buffer_size { return Err(ReplaceFramebufferAreaError::WrongBufferSize { expected: expected_buffer_size, actual: buffer.len(), }); } if width == 0 || height == 0 { return Ok(()); } let buffer = Bytes::copy_from_slice(buffer.as_slice()); // For some color values x, the pixel brightness is lower than for x - 1. // I don’t understand why and what is the pattern of these values. let converted_buffer = convert_rgb888_to_rgb565(buffer, display.endianness); let local_x = display.local_offset_x + x; let local_y = display.local_offset_y + y; self.commands_sender.send(LoupedeckCommand::ReplaceFramebufferArea { display_id: display.id, x: local_x, y: local_y, width, height, buffer: converted_buffer.freeze(), }).unwrap(); Ok(()) } pub fn refresh_display(&self, display: &LoupedeckDeviceDisplayConfiguration) -> Result<(), RefreshDisplayError>{ if !std::ptr::eq(display, &self.characteristics.key_grid_display) && !self.characteristics.additional_displays.iter().any(|d| std::ptr::eq(display, d)) { return Err(RefreshDisplayError::UnknownDisplay); } self.commands_sender.send(LoupedeckCommand::RefreshDisplay { display_id: display.id }).unwrap(); 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()?; Ok(ports.iter().filter_map(|port| { if let SerialPortType::UsbPort(info) = &port.port_type { let characteristics = CHARACTERISTICS.iter() .find(|c| c.vendor_id == info.vid && c.product_id == info.pid); if let Some(characteristics) = characteristics { return Some(AvailableLoupedeckDevice { port_name: port.port_name.clone(), characteristics, }); } } None }).collect::>()) } 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()?; let mut buf = [0; WS_UPGRADE_RESPONSE_START.len()]; port.read_exact(&mut buf)?; if buf != WS_UPGRADE_RESPONSE_START.as_bytes() { return Err(ConnectError::WrongEarlyHandshakeResponse); } // 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 (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, public_events_sender, internal_events_sender); }); 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, serial_number, firmware_version, events_receiver: public_events_receiver, commands_sender, }) } }