commit
This commit is contained in:
parent
64574bd2fa
commit
e1436733a5
11 changed files with 464 additions and 171 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -53,6 +53,12 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.5.0"
|
||||
|
@ -107,6 +113,25 @@ version = "0.8.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.3"
|
||||
|
@ -147,6 +172,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"color-eyre",
|
||||
"loupedeck_serial",
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -271,8 +297,10 @@ name = "loupedeck_serial"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"crossbeam-channel",
|
||||
"enum-ordinalize",
|
||||
"enumset",
|
||||
"rgb",
|
||||
"serialport",
|
||||
"thiserror",
|
||||
]
|
||||
|
@ -392,6 +420,15 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
|
|
@ -6,3 +6,4 @@ edition = "2021"
|
|||
[dependencies]
|
||||
loupedeck_serial = { path = "../loupedeck_serial" }
|
||||
color-eyre = "0.6.2"
|
||||
rgb = "0.8.37"
|
|
@ -1,14 +1,41 @@
|
|||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use rgb::RGB8;
|
||||
use loupedeck_serial::characteristics::LoupedeckButton;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?;
|
||||
|
||||
let mut device = available_devices.first()
|
||||
let device = available_devices.first()
|
||||
.wrap_err("at least one device should be connected")?
|
||||
.open()?;
|
||||
.connect()?;
|
||||
|
||||
device.connect()?;
|
||||
device.set_brightness(0.5);
|
||||
|
||||
let buttons = [
|
||||
LoupedeckButton::N0,
|
||||
LoupedeckButton::N1,
|
||||
LoupedeckButton::N2,
|
||||
LoupedeckButton::N3,
|
||||
LoupedeckButton::N4,
|
||||
LoupedeckButton::N5,
|
||||
LoupedeckButton::N6,
|
||||
LoupedeckButton::N7,
|
||||
];
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
sleep(Duration::from_millis(50));
|
||||
for (index, button) in buttons.iter().enumerate() {
|
||||
device.set_button_color(*button, RGB8::new(
|
||||
((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8,
|
||||
((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8,
|
||||
((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,3 +9,5 @@ enum-ordinalize = "4.3.0"
|
|||
enumset = "1.1.3"
|
||||
bytes = "1.5.0"
|
||||
thiserror = "1.0.52"
|
||||
crossbeam-channel = "0.5.10"
|
||||
rgb = "0.8.37"
|
|
@ -3,7 +3,7 @@ use enumset::{enum_set, EnumSet, EnumSetType};
|
|||
|
||||
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||
#[repr(u16)]
|
||||
pub enum LoupedeckDeviceKnob {
|
||||
pub enum LoupedeckKnob {
|
||||
KnobTopLeft = 0x01,
|
||||
KnobCenterLeft = 0x02,
|
||||
KnobBottomLeft = 0x03,
|
||||
|
@ -14,7 +14,7 @@ pub enum LoupedeckDeviceKnob {
|
|||
|
||||
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||
#[repr(u16)]
|
||||
pub enum LoupedeckDeviceButton {
|
||||
pub enum LoupedeckButton {
|
||||
KnobTopLeft = 0x01,
|
||||
KnobCenterLeft = 0x02,
|
||||
KnobBottomLeft = 0x03,
|
||||
|
@ -40,10 +40,14 @@ pub enum LoupedeckDeviceDisplayEndianness {
|
|||
#[derive(Debug)]
|
||||
pub struct LoupedeckDeviceDisplayConfiguration {
|
||||
pub id: u8,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub offset: (usize, usize),
|
||||
pub endianness: LoupedeckDeviceDisplayEndianness
|
||||
pub name: &'static str,
|
||||
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: LoupedeckDeviceDisplayEndianness,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -51,57 +55,107 @@ pub struct LoupedeckDeviceCharacteristics {
|
|||
pub vendor_id: u16,
|
||||
pub product_id: u16,
|
||||
pub name: &'static str,
|
||||
pub available_knobs: EnumSet<LoupedeckDeviceKnob>,
|
||||
pub available_buttons: EnumSet<LoupedeckDeviceButton>,
|
||||
pub key_grid_dimensions: (usize, usize),
|
||||
pub displays: &'static [LoupedeckDeviceDisplayConfiguration]
|
||||
pub available_knobs: EnumSet<LoupedeckKnob>,
|
||||
pub available_buttons: EnumSet<LoupedeckButton>,
|
||||
pub key_grid_rows: u8,
|
||||
pub key_grid_columns: u8,
|
||||
pub key_grid_display: LoupedeckDeviceDisplayConfiguration,
|
||||
pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration]
|
||||
}
|
||||
|
||||
impl LoupedeckDeviceCharacteristics {
|
||||
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 {
|
||||
self.additional_displays.iter().find(check)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_key_at_coordinates(&self, x: u16, y: u16) -> Option<u8> {
|
||||
let column_width = self.key_grid_display.width as f64 / self.key_grid_columns as f64;
|
||||
let row_height = self.key_grid_display.height as f64 / self.key_grid_rows as f64;
|
||||
|
||||
if x < self.key_grid_display.global_offset_x || y < self.key_grid_display.global_offset_y {
|
||||
return None
|
||||
}
|
||||
|
||||
let local_x = x - self.key_grid_display.global_offset_x;
|
||||
let local_y = y - self.key_grid_display.global_offset_y;
|
||||
|
||||
if local_x >= self.key_grid_display.width || local_y >= self.key_grid_display.height {
|
||||
return None
|
||||
}
|
||||
|
||||
let column = (local_x as f64 / column_width).floor() as u8;
|
||||
let row = (local_y as f64 / row_height).floor() as u8;
|
||||
|
||||
Some(row * self.key_grid_columns + column)
|
||||
}
|
||||
}
|
||||
|
||||
static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics {
|
||||
vendor_id: 0x2ec2,
|
||||
product_id: 0x0004,
|
||||
name: "Loupedeck Live",
|
||||
available_knobs: enum_set!(LoupedeckDeviceKnob::KnobTopLeft
|
||||
| LoupedeckDeviceKnob::KnobCenterLeft
|
||||
| LoupedeckDeviceKnob::KnobBottomLeft
|
||||
| LoupedeckDeviceKnob::KnobTopRight
|
||||
| LoupedeckDeviceKnob::KnobCenterRight
|
||||
| LoupedeckDeviceKnob::KnobBottomRight),
|
||||
available_buttons: enum_set!(LoupedeckDeviceButton::KnobTopLeft
|
||||
| LoupedeckDeviceButton::KnobCenterLeft
|
||||
| LoupedeckDeviceButton::KnobBottomLeft
|
||||
| LoupedeckDeviceButton::KnobTopRight
|
||||
| LoupedeckDeviceButton::KnobCenterRight
|
||||
| LoupedeckDeviceButton::KnobBottomRight
|
||||
| LoupedeckDeviceButton::N0
|
||||
| LoupedeckDeviceButton::N1
|
||||
| LoupedeckDeviceButton::N2
|
||||
| LoupedeckDeviceButton::N3
|
||||
| LoupedeckDeviceButton::N4
|
||||
| LoupedeckDeviceButton::N5
|
||||
| LoupedeckDeviceButton::N6
|
||||
| LoupedeckDeviceButton::N7),
|
||||
key_grid_dimensions: (4, 3),
|
||||
displays: &[
|
||||
LoupedeckDeviceDisplayConfiguration { // Left
|
||||
available_knobs: enum_set!(LoupedeckKnob::KnobTopLeft
|
||||
| LoupedeckKnob::KnobCenterLeft
|
||||
| LoupedeckKnob::KnobBottomLeft
|
||||
| LoupedeckKnob::KnobTopRight
|
||||
| LoupedeckKnob::KnobCenterRight
|
||||
| LoupedeckKnob::KnobBottomRight),
|
||||
available_buttons: enum_set!(LoupedeckButton::KnobTopLeft
|
||||
| LoupedeckButton::KnobCenterLeft
|
||||
| LoupedeckButton::KnobBottomLeft
|
||||
| LoupedeckButton::KnobTopRight
|
||||
| LoupedeckButton::KnobCenterRight
|
||||
| LoupedeckButton::KnobBottomRight
|
||||
| LoupedeckButton::N0
|
||||
| LoupedeckButton::N1
|
||||
| LoupedeckButton::N2
|
||||
| LoupedeckButton::N3
|
||||
| LoupedeckButton::N4
|
||||
| LoupedeckButton::N5
|
||||
| LoupedeckButton::N6
|
||||
| LoupedeckButton::N7),
|
||||
key_grid_rows: 3,
|
||||
key_grid_columns: 4,
|
||||
key_grid_display: LoupedeckDeviceDisplayConfiguration {
|
||||
id: 0x4d,
|
||||
name: "center",
|
||||
width: 360,
|
||||
height: 270,
|
||||
local_offset_x: 60,
|
||||
local_offset_y: 0,
|
||||
global_offset_x: 60,
|
||||
global_offset_y: 0,
|
||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
||||
},
|
||||
additional_displays: &[
|
||||
LoupedeckDeviceDisplayConfiguration {
|
||||
id: 0x4d,
|
||||
name: "left",
|
||||
width: 60,
|
||||
height: 270,
|
||||
offset: (0, 0),
|
||||
local_offset_x: 0,
|
||||
local_offset_y: 0,
|
||||
global_offset_x: 0,
|
||||
global_offset_y: 0,
|
||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
||||
},
|
||||
LoupedeckDeviceDisplayConfiguration { // Center
|
||||
id: 0x4d,
|
||||
width: 360,
|
||||
height: 270,
|
||||
offset: (60, 0),
|
||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
||||
},
|
||||
LoupedeckDeviceDisplayConfiguration { // Right
|
||||
LoupedeckDeviceDisplayConfiguration {
|
||||
id: 0x4d,
|
||||
name: "right",
|
||||
width: 60,
|
||||
height: 270,
|
||||
offset: (420, 0),
|
||||
local_offset_x: 420,
|
||||
local_offset_y: 0,
|
||||
global_offset_x: 420,
|
||||
global_offset_y: 0,
|
||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
||||
},
|
||||
]
|
||||
|
|
11
loupedeck_serial/src/commands.rs
Normal file
11
loupedeck_serial/src/commands.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use rgb::RGB8;
|
||||
use crate::characteristics::LoupedeckButton;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum LoupedeckCommand {
|
||||
SetBrightness(f32),
|
||||
SetButtonColor {
|
||||
button: LoupedeckButton,
|
||||
color: RGB8
|
||||
}
|
||||
}
|
|
@ -1,52 +1,74 @@
|
|||
use std::io;
|
||||
use std::io::{BufReader, Read, Write};
|
||||
use std::io::ErrorKind::TimedOut;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Sender, sync_channel, SyncSender};
|
||||
use std::thread::spawn;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use serialport::{SerialPort, SerialPortType};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread::{sleep, spawn};
|
||||
use std::time::Duration;
|
||||
use crossbeam_channel::{Receiver};
|
||||
use rgb::RGB8;
|
||||
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
|
||||
use thiserror::Error;
|
||||
use crate::characteristics::{CHARACTERISTICS, LoupedeckDeviceCharacteristics};
|
||||
use crate::discovery::AvailableLoupedeckDevice;
|
||||
use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics};
|
||||
use crate::commands::LoupedeckCommand;
|
||||
use crate::events::LoupedeckEvent;
|
||||
use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START};
|
||||
|
||||
const WS_UPGRADE_REQUEST: &str = r#"GET /index.html
|
||||
HTTP/1.1
|
||||
Connection: Upgrade
|
||||
Upgrade: websocket
|
||||
Sec-WebSocket-Key: 123abc
|
||||
#[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
|
||||
}
|
||||
|
||||
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=";
|
||||
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
|
||||
self.characteristics
|
||||
}
|
||||
|
||||
const MESSAGE_START_BYTE: u8 = 0x82;
|
||||
const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize;
|
||||
pub fn connect(&self) -> Result<LoupedeckDevice, ConnectError> {
|
||||
LoupedeckDevice::connect(&self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoupedeckDevice {
|
||||
pub(crate) port: Option<Box<dyn SerialPort>>,
|
||||
pub characteristics: &'static LoupedeckDeviceCharacteristics,
|
||||
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
|
||||
pub(crate) events_channel: Receiver<LoupedeckEvent>,
|
||||
commands_sender: Sender<LoupedeckCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConnectError {
|
||||
#[error("IO Error: {0}")]
|
||||
#[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.")]
|
||||
WrongHandshakeResponse,
|
||||
|
||||
#[error("The device was already connected.")]
|
||||
AlreadyConnected,
|
||||
}
|
||||
|
||||
impl LoupedeckDevice {
|
||||
pub fn new_unchecked(port: Box<dyn SerialPort>, characteristics: &'static LoupedeckDeviceCharacteristics) -> LoupedeckDevice {
|
||||
LoupedeckDevice {
|
||||
port: Some(port),
|
||||
characteristics,
|
||||
}
|
||||
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
|
||||
self.characteristics
|
||||
}
|
||||
|
||||
pub fn events_channel(&self) -> Receiver<LoupedeckEvent> {
|
||||
self.events_channel.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) {
|
||||
self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap();
|
||||
}
|
||||
|
||||
pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> {
|
||||
|
@ -69,8 +91,15 @@ impl LoupedeckDevice {
|
|||
}).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn connect(&mut self) -> Result<(), ConnectError> {
|
||||
let mut port = self.port.take().expect("already connected");
|
||||
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()?;
|
||||
|
||||
|
@ -81,65 +110,25 @@ impl LoupedeckDevice {
|
|||
return Err(ConnectError::WrongHandshakeResponse);
|
||||
}
|
||||
|
||||
let (sender, receiver) = sync_channel::<Bytes>(8);
|
||||
let handle = spawn(move || {
|
||||
LoupedeckDevice::handle_messages(port, sender);
|
||||
// I don’t 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 (events_sender, events_receiver) = crossbeam_channel::unbounded::<LoupedeckEvent>();
|
||||
spawn(move || {
|
||||
read_messages_worker(port, events_sender);
|
||||
});
|
||||
|
||||
for x in receiver {
|
||||
dbg!(&x[1]);
|
||||
}
|
||||
let (commands_sender, commands_receiver) = channel();
|
||||
spawn(move || {
|
||||
write_messages_worker(cloned_port, commands_receiver);
|
||||
});
|
||||
|
||||
handle.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_messages(mut port: Box<dyn SerialPort>, sender: SyncSender<Bytes>) {
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
loop {
|
||||
let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH);
|
||||
let read_result = port.read(&mut chunk);
|
||||
|
||||
if let Err(err) = &read_result {
|
||||
if err.kind() == TimedOut {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
let len = read_result.unwrap();
|
||||
if len == 0 {
|
||||
panic!("read 0 bytes");
|
||||
}
|
||||
|
||||
chunk.truncate(len);
|
||||
buffer.put(chunk);
|
||||
|
||||
loop {
|
||||
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);
|
||||
message.advance(2);
|
||||
sender.send(message.freeze()).unwrap();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(LoupedeckDevice {
|
||||
characteristics,
|
||||
events_channel: events_receiver,
|
||||
commands_sender
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
use std::time::Duration;
|
||||
use serialport::{DataBits, FlowControl, Parity, StopBits};
|
||||
use crate::characteristics::LoupedeckDeviceCharacteristics;
|
||||
use crate::device::LoupedeckDevice;
|
||||
|
||||
#[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 open(&self) -> Result<LoupedeckDevice, serialport::Error> {
|
||||
Ok(LoupedeckDevice {
|
||||
port: Some(serialport::new(&self.port_name, 256000)
|
||||
.data_bits(DataBits::Eight)
|
||||
.stop_bits(StopBits::One)
|
||||
.parity(Parity::None)
|
||||
.flow_control(FlowControl::None)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.open()?),
|
||||
characteristics: self.characteristics,
|
||||
})
|
||||
}
|
||||
}
|
27
loupedeck_serial/src/events.rs
Normal file
27
loupedeck_serial/src/events.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RotationDirection {
|
||||
Clockwise,
|
||||
Counterclockwise
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoupedeckEvent {
|
||||
ButtonDown {
|
||||
button: LoupedeckButton
|
||||
},
|
||||
ButtonUp {
|
||||
button: LoupedeckButton
|
||||
},
|
||||
KnobRotate {
|
||||
knob: LoupedeckKnob,
|
||||
direction: RotationDirection
|
||||
},
|
||||
Touch {
|
||||
touch_id: u8,
|
||||
x: u16,
|
||||
y: u16,
|
||||
is_end: bool
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
pub mod characteristics;
|
||||
pub mod device;
|
||||
pub mod discovery;
|
||||
pub mod events;
|
||||
mod messages;
|
||||
mod commands;
|
176
loupedeck_serial/src/messages.rs
Normal file
176
loupedeck_serial/src/messages.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
use std::cmp::min;
|
||||
use std::io::ErrorKind::TimedOut;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use crossbeam_channel::Sender;
|
||||
use enum_ordinalize::Ordinalize;
|
||||
use serialport::SerialPort;
|
||||
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
|
||||
use crate::commands::LoupedeckCommand;
|
||||
use crate::events::LoupedeckEvent;
|
||||
use crate::events::RotationDirection::{Clockwise, Counterclockwise};
|
||||
|
||||
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;
|
||||
|
||||
pub(crate) fn read_messages_worker(mut port: Box<dyn SerialPort>, sender: Sender<LoupedeckEvent>) {
|
||||
let mut buffer = BytesMut::new();
|
||||
|
||||
loop {
|
||||
let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH);
|
||||
let read_result = port.read(&mut chunk);
|
||||
|
||||
if let Err(err) = &read_result {
|
||||
if err.kind() == TimedOut {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let len = read_result.unwrap();
|
||||
if len == 0 {
|
||||
panic!("read 0 bytes");
|
||||
}
|
||||
|
||||
chunk.truncate(len);
|
||||
buffer.put(chunk);
|
||||
|
||||
loop {
|
||||
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 event = parse_message(command, message.freeze());
|
||||
if let Some(event) = event {
|
||||
sender.send(event).unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> {
|
||||
match command {
|
||||
0x00 => { // Button
|
||||
let button = LoupedeckButton::from_ordinal(message[0] as u16)
|
||||
.expect("Invalid button ID");
|
||||
|
||||
Some(match message[1] {
|
||||
0x00 => LoupedeckEvent::ButtonDown { button },
|
||||
_ => LoupedeckEvent::ButtonUp { button },
|
||||
})
|
||||
}
|
||||
0x01 => { // Knob
|
||||
let knob = LoupedeckKnob::from_ordinal(message[0] as u16)
|
||||
.expect("Invalid button ID");
|
||||
|
||||
Some(LoupedeckEvent::KnobRotate {
|
||||
knob,
|
||||
direction: if message[1] == 1 { Clockwise } else { Counterclockwise },
|
||||
})
|
||||
}
|
||||
0x4d | 0x6d => { // Touch
|
||||
message.advance(1);
|
||||
let x = message.get_u16();
|
||||
let y = message.get_u16();
|
||||
let touch_id = message.get_u8();
|
||||
|
||||
Some(LoupedeckEvent::Touch {
|
||||
touch_id,
|
||||
x,
|
||||
y,
|
||||
is_end: command == 0x6d,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown command: {}", command);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Receiver<LoupedeckCommand>) {
|
||||
let mut next_transaction_id = 0;
|
||||
|
||||
let mut send = |command_id: u8, data: Bytes| {
|
||||
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).unwrap();
|
||||
port.flush().unwrap();
|
||||
} else {
|
||||
let mut prep = BytesMut::zeroed(6);
|
||||
prep[0] = 0x82;
|
||||
prep[1] = (0x80 + length) as u8;
|
||||
|
||||
port.write_all(&prep).unwrap();
|
||||
port.flush().unwrap();
|
||||
}
|
||||
|
||||
port.write_all(&data_with_header).unwrap();
|
||||
port.flush().unwrap();
|
||||
|
||||
next_transaction_id = next_transaction_id.wrapping_add(1);
|
||||
};
|
||||
|
||||
for command in receiver {
|
||||
match command {
|
||||
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() as u8, color.r, color.g, color.b]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue