This commit is contained in:
Moritz Ruth 2023-12-28 23:51:35 +01:00
parent 64574bd2fa
commit e1436733a5
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
11 changed files with 464 additions and 171 deletions

37
Cargo.lock generated
View file

@ -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"

View file

@ -6,3 +6,4 @@ edition = "2021"
[dependencies]
loupedeck_serial = { path = "../loupedeck_serial" }
color-eyre = "0.6.2"
rgb = "0.8.37"

View file

@ -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(())
}

View file

@ -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"

View file

@ -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
id: 0x4d,
width: 60,
height: 270,
offset: (0, 0),
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
},
LoupedeckDeviceDisplayConfiguration { // Center
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,
offset: (60, 0),
local_offset_x: 60,
local_offset_y: 0,
global_offset_x: 60,
global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
},
LoupedeckDeviceDisplayConfiguration { // Right
additional_displays: &[
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
name: "left",
width: 60,
height: 270,
offset: (420, 0),
local_offset_x: 0,
local_offset_y: 0,
global_offset_x: 0,
global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
},
LoupedeckDeviceDisplayConfiguration {
id: 0x4d,
name: "right",
width: 60,
height: 270,
local_offset_x: 420,
local_offset_y: 0,
global_offset_x: 420,
global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
},
]

View 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
}
}

View file

@ -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 dont 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
})
}
}

View file

@ -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,
})
}
}

View 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
}
}

View file

@ -1,3 +1,5 @@
pub mod characteristics;
pub mod device;
pub mod discovery;
pub mod events;
mod messages;
mod commands;

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