303 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|