288 lines
10 KiB
Rust
288 lines
10 KiB
Rust
use std::io::{Read, Write};
|
||
use std::sync::mpsc;
|
||
use std::thread::sleep;
|
||
use std::time::Duration;
|
||
use std::{io, thread};
|
||
|
||
use bytes::Bytes;
|
||
use rgb::{ComponentSlice, RGB8};
|
||
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
|
||
use thiserror::Error;
|
||
|
||
use crate::characteristics::{LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration, CHARACTERISTICS};
|
||
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: flume::Receiver<LoupedeckEvent>,
|
||
commands_sender: flume::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,
|
||
}
|
||
|
||
#[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(&self) -> flume::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);
|
||
}
|
||
|
||
// The write worker thread not running means the device was disconnected.
|
||
// In that case, the read worker thread sends a LoupedeckEvent::Disconnected.
|
||
self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).ok();
|
||
|
||
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<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 = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?;
|
||
|
||
let mut buf = [0; WS_UPGRADE_RESPONSE_START.len()];
|
||
|
||
if port.read_exact(&mut buf).is_err() {
|
||
drop(port);
|
||
port = LoupedeckDevice::create_port_and_send_ws_upgrade_request(port_name)?;
|
||
port.read_exact(&mut buf).unwrap();
|
||
}
|
||
|
||
if buf != WS_UPGRADE_RESPONSE_START.as_bytes() {
|
||
return Err(ConnectError::WrongEarlyHandshakeResponse);
|
||
}
|
||
|
||
// I don’t know why, but 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 thread_name_base = format!("loupedeck_serial ({})", port.name().unwrap_or("<unnamed>".to_owned()));
|
||
|
||
let (public_events_sender, public_events_receiver) = flume::unbounded::<LoupedeckEvent>();
|
||
let (internal_events_sender, internal_events_receiver) = mpsc::sync_channel(2);
|
||
thread::Builder::new().name(thread_name_base.to_owned() + " read worker").spawn(move || {
|
||
read_messages_worker(port, public_events_sender, internal_events_sender);
|
||
})?;
|
||
|
||
let (commands_sender, commands_receiver) = flume::unbounded::<LoupedeckCommand>();
|
||
thread::Builder::new().name(thread_name_base.to_owned() + " write worker").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(10)) {
|
||
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(10)) {
|
||
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,
|
||
})
|
||
}
|
||
|
||
fn create_port_and_send_ws_upgrade_request(port_name: &str) -> serialport::Result<Box<dyn serialport::SerialPort>> {
|
||
let mut port = serialport::new(port_name, 256000)
|
||
.data_bits(DataBits::Eight)
|
||
.stop_bits(StopBits::One)
|
||
.parity(Parity::None)
|
||
.flow_control(FlowControl::Software)
|
||
.timeout(Duration::from_secs(1))
|
||
.open()?;
|
||
|
||
port.clear(ClearBuffer::All).unwrap();
|
||
port.write_all(WS_UPGRADE_REQUEST.as_bytes()).unwrap();
|
||
port.flush().unwrap();
|
||
|
||
Ok(port)
|
||
}
|
||
}
|