deckster/loupedeck_serial/src/device.rs
2023-12-29 16:57:53 +01:00

280 lines
9.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, ConnectError> {
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<LoupedeckEvent>,
commands_sender: crossbeam_channel::Sender<LoupedeckCommand>,
}
#[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<LoupedeckEvent> {
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 dont 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<Vec<AvailableLoupedeckDevice>, 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::<Vec<_>>())
}
pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result<LoupedeckDevice, ConnectError> {
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 dont 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::<LoupedeckEvent>();
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::<LoupedeckCommand>();
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,
})
}
}