deckster/crates/loupedeck_serial/src/messages.rs
2024-01-31 01:23:56 +01:00

303 lines
11 KiB
Rust

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);
}
}
}
}
}