This commit is contained in:
Moritz Ruth 2024-01-31 01:23:56 +01:00
parent 1904e3e96a
commit b5a7ab3c6b
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
71 changed files with 921 additions and 1297 deletions

View file

@ -0,0 +1,14 @@
[package]
name = "loupedeck_serial"
version = "0.1.0"
edition = "2021"
[dependencies]
serialport = "4.3.0"
enum-ordinalize = "4.3.0"
enumset = "1.1.3"
bytes = "1.5.0"
thiserror = "1.0.52"
rgb = "0.8.37"
flume = "0.11.0"
serde = { version = "1.0.195", features = ["derive"] }

View file

@ -0,0 +1,271 @@
use enum_ordinalize::Ordinalize;
use enumset::{enum_set, EnumSet, EnumSetType};
use crate::util::Endianness;
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u8)]
pub enum LoupedeckKnob {
LeftTop = 0x01,
LeftMiddle = 0x02,
LeftBottom = 0x03,
RightTop = 0x04,
RightMiddle = 0x05,
RightBottom = 0x06,
}
impl LoupedeckKnob {
fn is_left(&self) -> bool {
matches!(self, LoupedeckKnob::LeftTop | LoupedeckKnob::LeftMiddle | LoupedeckKnob::LeftBottom)
}
fn row(&self) -> u8 {
match self {
LoupedeckKnob::LeftTop | LoupedeckKnob::RightTop => 0,
LoupedeckKnob::LeftMiddle | LoupedeckKnob::RightMiddle => 1,
LoupedeckKnob::LeftBottom | LoupedeckKnob::RightBottom => 2,
}
}
}
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u8)]
pub enum LoupedeckButton {
N0 = 0x07,
N1 = 0x08,
N2 = 0x09,
N3 = 0x0a,
N4 = 0x0b,
N5 = 0x0c,
N6 = 0x0d,
N7 = 0x0e,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceDisplayConfiguration {
pub id: u8,
pub dpi: f32,
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: Endianness,
}
impl Eq for LoupedeckDeviceDisplayConfiguration {
fn assert_receiver_is_total_eq(&self) {}
}
impl PartialEq for LoupedeckDeviceDisplayConfiguration {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceKeyGridCharacteristics {
pub rows: u8,
pub columns: u8,
pub display: LoupedeckDeviceDisplayConfiguration,
}
#[derive(Debug)]
pub struct LoupedeckDisplayRect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl LoupedeckDeviceKeyGridCharacteristics {
pub fn key_size(&self) -> (u16, u16) {
// Assuming the sizes are integers
(self.display.width / self.columns as u16, self.display.height / self.rows as u16)
}
pub fn get_key_at_local_coordinates(&self, x: u16, y: u16) -> Option<u8> {
let (column_width, row_height) = self.key_size();
if x >= self.display.width || y >= self.display.height {
return None;
}
let column = (x / column_width) as u8;
let row = (y / row_height) as u8;
Some(row * self.columns + column)
}
pub fn get_key_at_global_coordinates(&self, x: u16, y: u16) -> Option<u8> {
if x < self.display.global_offset_x || y < self.display.global_offset_y {
return None;
}
let local_x = x - self.display.global_offset_x;
let local_y = y - self.display.global_offset_y;
self.get_key_at_local_coordinates(local_x, local_y)
}
pub fn get_local_key_rect(&self, key_index: u8) -> Option<LoupedeckDisplayRect> {
if key_index >= self.rows * self.columns {
return None;
}
let (column_width, row_height) = self.key_size();
let row = (key_index / self.columns) as u16;
let column = (key_index % self.columns) as u16;
Some(LoupedeckDisplayRect {
x: column * column_width,
y: row * row_height,
w: column_width,
h: row_height,
})
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceCharacteristics {
pub vendor_id: u16,
pub product_id: u16,
pub name: &'static str,
pub available_knobs: EnumSet<LoupedeckKnob>,
pub available_buttons: EnumSet<LoupedeckButton>,
pub key_grid: LoupedeckDeviceKeyGridCharacteristics,
pub knob_displays: Option<(LoupedeckDeviceDisplayConfiguration, LoupedeckDeviceDisplayConfiguration)>,
}
impl Eq for LoupedeckDeviceCharacteristics {
fn assert_receiver_is_total_eq(&self) {}
}
impl PartialEq for LoupedeckDeviceCharacteristics {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self, other)
}
}
impl LoupedeckDeviceCharacteristics {
pub fn knob_rows(&self) -> u8 {
self.available_knobs.iter().map(|k| k.row()).max().map(|v| v + 1).unwrap_or(0)
}
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 if let Some(knob_displays) = &self.knob_displays {
if check(&knob_displays.0) {
Some(&knob_displays.0)
} else if check(&knob_displays.1) {
Some(&knob_displays.1)
} else {
None
}
} else {
None
}
}
pub fn get_display_and_rect_for_knob(&self, knob: LoupedeckKnob) -> Option<(&LoupedeckDeviceDisplayConfiguration, LoupedeckDisplayRect)> {
if !self.available_knobs.contains(knob) {
return None;
}
if let Some(knob_displays) = &self.knob_displays {
let display = if knob.is_left() { &knob_displays.0 } else { &knob_displays.1 };
let row = knob.row() as u16;
let rows = self.knob_rows() as u16;
let row_height = display.height / rows;
let rect = LoupedeckDisplayRect {
x: 0,
y: row_height * row,
w: display.width,
h: row_height,
};
Some((display, rect))
} else {
None
}
}
}
static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics {
vendor_id: 0x2ec2,
product_id: 0x0004,
name: "Loupedeck Live",
available_knobs: enum_set!(
LoupedeckKnob::LeftTop
| LoupedeckKnob::LeftMiddle
| LoupedeckKnob::LeftBottom
| LoupedeckKnob::RightTop
| LoupedeckKnob::RightMiddle
| LoupedeckKnob::RightBottom
),
available_buttons: enum_set!(
LoupedeckButton::N0
| LoupedeckButton::N1
| LoupedeckButton::N2
| LoupedeckButton::N3
| LoupedeckButton::N4
| LoupedeckButton::N5
| LoupedeckButton::N6
| LoupedeckButton::N7
),
key_grid: LoupedeckDeviceKeyGridCharacteristics {
rows: 3,
columns: 4,
display: LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 360,
height: 270,
local_offset_x: 60,
local_offset_y: 0,
global_offset_x: 60,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
},
knob_displays: Some((
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 60,
height: 270,
local_offset_x: 0,
local_offset_y: 0,
global_offset_x: 0,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
dpi: 142.875,
width: 60,
height: 270,
local_offset_x: 420,
local_offset_y: 0,
global_offset_x: 420,
global_offset_y: 0,
endianness: Endianness::LittleEndian,
},
)),
};
pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [&LOUPEDECK_LIVE_CHARACTERISTIC];

View file

@ -0,0 +1,65 @@
use bytes::Bytes;
use enum_ordinalize::Ordinalize;
use rgb::RGB8;
use serde::{Deserialize, Serialize};
use crate::characteristics::LoupedeckButton;
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Ordinalize)]
#[repr(u8)]
#[serde(rename_all = "kebab-case")]
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,
color: RGB8,
},
ReplaceFramebufferArea {
display_id: u8,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: Bytes,
},
RefreshDisplay {
display_id: u8,
},
Vibrate {
pattern: VibrationPattern,
},
}

View file

@ -0,0 +1,345 @@
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> {
self.check_replace_framebuffer_area_parameters(display, x, y, width, height)?;
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(());
}
// 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(Bytes::copy_from_slice(buffer.as_slice()), display.endianness);
self.unchecked_replace_framebuffer_area_raw(display, x, y, width, height, converted_buffer.freeze());
Ok(())
}
/// Replaces the specified framebuffer area of the display with `buffer`.
///
/// `buffer` must contain exactly (`width * height`) RGB565 (16 bit) values, LE or BE depending on `display.endianness`.
///
/// If you have a buffer of `rgb::RGB8` values, you should use [replace_framebuffer_area](LoupedeckDevice::replace_framebuffer_area) instead.
pub fn replace_framebuffer_area_raw(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: Bytes,
) -> Result<(), ReplaceFramebufferAreaError> {
self.check_replace_framebuffer_area_parameters(display, x, y, width, height)?;
let expected_buffer_size = (height * width * 2) 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(());
}
self.unchecked_replace_framebuffer_area_raw(display, x, y, width, height, buffer);
Ok(())
}
fn check_replace_framebuffer_area_parameters(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
) -> Result<(), ReplaceFramebufferAreaError> {
if !(display.id == self.characteristics.key_grid.display.id
|| self
.characteristics
.knob_displays
.as_ref()
.is_some_and(|d| d.0.id == display.id || d.1.id == display.id))
{
return Err(ReplaceFramebufferAreaError::UnknownDisplay);
}
if x + width > display.width || y + height > display.height {
return Err(ReplaceFramebufferAreaError::OutOfBounds);
}
Ok(())
}
fn unchecked_replace_framebuffer_area_raw(&self, display: &LoupedeckDeviceDisplayConfiguration, x: u16, y: u16, width: u16, height: u16, buffer: Bytes) {
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,
})
.unwrap();
}
pub fn refresh_display(&self, display: &LoupedeckDeviceDisplayConfiguration) -> Result<(), RefreshDisplayError> {
if !(display.id == self.characteristics.key_grid.display.id
|| self
.characteristics
.knob_displays
.as_ref()
.is_some_and(|d| d.0.id == display.id || d.1.id == display.id))
{
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 dont 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)
}
}

View file

@ -0,0 +1,24 @@
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RotationDirection {
Clockwise,
Counterclockwise,
}
#[derive(Debug)]
pub(crate) enum LoupedeckInternalEvent {
GetSerialNumberResponse { serial_number: String },
GetFirmwareVersionResponse { firmware_version: String },
}
#[derive(Debug)]
pub enum LoupedeckEvent {
Disconnected,
ButtonDown { button: LoupedeckButton },
ButtonUp { button: LoupedeckButton },
KnobDown { knob: LoupedeckKnob },
KnobUp { knob: LoupedeckKnob },
KnobRotate { knob: LoupedeckKnob, direction: RotationDirection },
Touch { touch_id: u8, x: u16, y: u16, is_end: bool },
}

View file

@ -0,0 +1,6 @@
pub mod characteristics;
pub mod commands;
pub mod device;
pub mod events;
mod messages;
pub mod util;

View file

@ -0,0 +1,303 @@
use std::cmp::min;
use std::io;
use std::io::{ErrorKind, Read, Write};
use std::sync::mpsc;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use enum_ordinalize::Ordinalize;
use serialport::SerialPort;
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
use crate::commands::LoupedeckCommand;
use crate::events::RotationDirection::{Clockwise, Counterclockwise};
use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
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;
enum ParseMessageResult {
InternalEvent(LoupedeckInternalEvent),
PublicEvent(LoupedeckEvent),
Nothing,
}
impl From<LoupedeckInternalEvent> for ParseMessageResult {
fn from(value: LoupedeckInternalEvent) -> Self {
ParseMessageResult::InternalEvent(value)
}
}
impl From<LoupedeckEvent> for ParseMessageResult {
fn from(value: LoupedeckEvent) -> Self {
ParseMessageResult::PublicEvent(value)
}
}
pub(crate) fn read_messages_worker(
mut port: Box<dyn SerialPort>,
public_sender: flume::Sender<LoupedeckEvent>,
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
) {
let mut internal_sender = Some(internal_sender);
let mut should_stop = false;
let mut buffer = BytesMut::new();
while !should_stop {
let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH);
let read_result = port.read(&mut chunk);
let read_length = match read_result {
Ok(length) => length,
Err(err) => {
match err.kind() {
ErrorKind::BrokenPipe => {
// This fails only if the other side is disconnected.
// In that case, this thread should terminate anyway and we can ignore the error.
public_sender.send(LoupedeckEvent::Disconnected).ok();
break;
}
ErrorKind::TimedOut => continue,
_ => panic!("{}", err),
}
}
};
chunk.truncate(read_length);
buffer.put(chunk);
while !should_stop {
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 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) => {
if public_sender.send(event).is_err() {
should_stop = false
}
}
ParseMessageResult::Nothing => {}
}
}
} else {
break;
}
}
}
}
fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
match command {
0x00 => match message[1] {
0x00 => match message[0] {
0x01 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::LeftTop },
0x02 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::LeftMiddle,
},
0x03 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::LeftBottom,
},
0x04 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::RightTop },
0x05 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::RightMiddle,
},
0x06 => LoupedeckEvent::KnobDown {
knob: LoupedeckKnob::RightBottom,
},
0x07 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N0 },
0x08 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N1 },
0x09 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N2 },
0x0a => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N3 },
0x0b => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N4 },
0x0c => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N5 },
0x0d => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N6 },
0x0e => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N7 },
_ => panic!("Illegal button id: {}", message[1]),
},
_ => match message[0] {
0x01 => LoupedeckEvent::KnobUp { knob: LoupedeckKnob::LeftTop },
0x02 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::LeftMiddle,
},
0x03 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::LeftBottom,
},
0x04 => LoupedeckEvent::KnobUp { knob: LoupedeckKnob::RightTop },
0x05 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::RightMiddle,
},
0x06 => LoupedeckEvent::KnobUp {
knob: LoupedeckKnob::RightBottom,
},
0x07 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N0 },
0x08 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N1 },
0x09 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N2 },
0x0a => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N3 },
0x0b => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N4 },
0x0c => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N5 },
0x0d => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N6 },
0x0e => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N7 },
_ => panic!("Illegal button id: {}", message[1]),
},
}
.into(),
0x01 => {
let knob = LoupedeckKnob::from_ordinal(message[0]).expect("Invalid button ID");
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 => {
message.advance(1);
let x = message.get_u16();
let y = message.get_u16();
let touch_id = message.get_u8();
LoupedeckEvent::Touch {
touch_id,
x,
y,
is_end: command == 0x6d,
}
.into()
}
_ => ParseMessageResult::Nothing,
}
}
pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: flume::Receiver<LoupedeckCommand>) {
let mut next_transaction_id = 0;
let mut send = |command_id: u8, data: Bytes| -> Result<(), io::Error> {
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)?;
port.flush()?;
} else {
let mut prep = BytesMut::zeroed(6);
prep[0] = 0x82;
prep[1] = (0x80 + length) as u8;
port.write_all(&prep)?;
port.flush()?;
}
port.write_all(&data_with_header)?;
port.flush()?;
next_transaction_id = next_transaction_id.wrapping_add(1);
Ok(())
};
for command in receiver {
let result = 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(), color.r, color.g, color.b])),
LoupedeckCommand::ReplaceFramebufferArea {
display_id,
x,
y,
width,
height,
buffer,
} => {
let mut data = BytesMut::with_capacity(10 + buffer.len());
data.put_u8(0);
data.put_u8(display_id);
data.put_u16(x);
data.put_u16(y);
data.put_u16(width);
data.put_u16(height);
data.put(buffer);
send(0x10, data.freeze())
}
LoupedeckCommand::RefreshDisplay { display_id } => send(0x0f, Bytes::copy_from_slice(&[0, display_id])),
LoupedeckCommand::Vibrate { pattern } => send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()])),
};
if let Err(error) = result {
match error.kind() {
ErrorKind::TimedOut | ErrorKind::BrokenPipe => break,
_ => {
panic!("IO error during write: {}", error);
}
}
}
}
}

View file

@ -0,0 +1,32 @@
use bytes::{BufMut, Bytes, BytesMut};
pub fn convert_rgb888_to_rgb565(original: Bytes, endianness: Endianness) -> BytesMut {
let pixel_count = original.len() / 3;
let excess_bytes = original.len() % 3;
if excess_bytes != 0 {
panic!("Found {} excess bytes at the end of the original buffer", excess_bytes);
}
let mut result = BytesMut::with_capacity(pixel_count * 2);
for index in 0..pixel_count {
let red = original[index * 3] as u16;
let green = original[index * 3 + 1] as u16;
let blue = original[index * 3 + 2] as u16;
let color = ((red & 0b11111000) << 8) | ((green & 0b11111100) << 3) | (blue.wrapping_shr(3));
match endianness {
Endianness::LittleEndian => result.put_u16_le(color),
Endianness::BigEndian => result.put_u16(color),
}
}
result
}
#[derive(Debug, Copy, Clone)]
pub enum Endianness {
BigEndian,
LittleEndian,
}