This commit is contained in:
Moritz Ruth 2023-12-29 16:19:10 +01:00
parent dbb492a72d
commit 482123638f
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
7 changed files with 229 additions and 83 deletions

View file

@ -3,8 +3,9 @@ use std::time::{Duration, Instant};
use color_eyre::eyre::ContextCompat; use color_eyre::eyre::ContextCompat;
use color_eyre::Result; use color_eyre::Result;
use rgb::RGB8; use rgb::RGB8;
use loupedeck_serial::characteristics::LoupedeckButton; use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::LoupedeckEvent;
fn main() -> Result<()> { fn main() -> Result<()> {
let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?;
@ -13,39 +14,23 @@ fn main() -> Result<()> {
.wrap_err("at least one device should be connected")? .wrap_err("at least one device should be connected")?
.connect()?; .connect()?;
println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number());
device.set_brightness(1.0); device.set_brightness(1.0);
let buttons = [ // run_vibrations(&device)?;
LoupedeckButton::N0, run_rainbow(&device)?;
LoupedeckButton::N1,
LoupedeckButton::N2,
LoupedeckButton::N3,
LoupedeckButton::N4,
LoupedeckButton::N5,
LoupedeckButton::N6,
LoupedeckButton::N7,
];
let mut value = 0u8; Ok(())
}
fn run_rainbow(device: &LoupedeckDevice) -> Result<()> {
let interval = Duration::from_millis(50);
let start = Instant::now(); let start = Instant::now();
let mut iteration = 0;
let buttons = device.characteristics().available_buttons.iter().filter(|b| b.supports_color()).collect::<Vec<_>>();
loop { loop {
while !device.events_channel().is_empty() {
let event = device.events_channel().recv().unwrap();
match event {
LoupedeckEvent::KnobRotate { direction: RotationDirection::Clockwise, .. } => {
value = value.wrapping_add(1);
println!("{}, {:#010b}, {:#018b}", value, value, ((value as u16) & 0b11111000) << 8);
}
LoupedeckEvent::KnobRotate { direction: RotationDirection::Counterclockwise, .. } => {
value = value.wrapping_sub(1);
println!("{}, {:#010b}, {:#018b}", value, value, ((value as u16) & 0b11111000) << 8);
}
_ => {}
};
}
let ms = start.elapsed().as_millis() as u64; let ms = start.elapsed().as_millis() as u64;
for (index, button) in buttons.iter().enumerate() { for (index, button) in buttons.iter().enumerate() {
@ -55,20 +40,18 @@ fn main() -> Result<()> {
(((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((t / 1000.0).sin() / 2.0 + 0.5) * 255.0) as u8,
(((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((t / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8,
(((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, (((t / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8,
)); ))?;
} }
device.replace_framebuffer_area( sleep((iteration + 1) * interval - start.elapsed());
&device.characteristics().key_grid_display, iteration += 1;
0,
0,
90,
90,
&[RGB8::new(value, 0, 0); 90 * 90]
)?;
device.refresh_display(&device.characteristics().key_grid_display)?;
sleep(Duration::from_millis(50));
} }
} }
fn run_vibrations(device: &LoupedeckDevice) -> Result<()> {
for event in device.events_channel() {
if let LoupedeckEvent::Touch { is_end: false, .. } = event { device.vibrate(VibrationPattern::Low) }
}
Ok(())
}

View file

@ -3,7 +3,7 @@ use enumset::{enum_set, EnumSet, EnumSetType};
use crate::util::Endianness; use crate::util::Endianness;
#[derive(Debug, Ordinalize, EnumSetType)] #[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u16)] #[repr(u8)]
pub enum LoupedeckKnob { pub enum LoupedeckKnob {
KnobTopLeft = 0x01, KnobTopLeft = 0x01,
KnobCenterLeft = 0x02, KnobCenterLeft = 0x02,
@ -14,7 +14,7 @@ pub enum LoupedeckKnob {
} }
#[derive(Debug, Ordinalize, EnumSetType)] #[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u16)] #[repr(u8)]
pub enum LoupedeckButton { pub enum LoupedeckButton {
KnobTopLeft = 0x01, KnobTopLeft = 0x01,
KnobCenterLeft = 0x02, KnobCenterLeft = 0x02,
@ -32,6 +32,12 @@ pub enum LoupedeckButton {
N7 = 0x0e, N7 = 0x0e,
} }
impl LoupedeckButton {
pub fn supports_color(&self) -> bool {
self.ordinal() >= LoupedeckButton::N0.ordinal() && self.ordinal() <= LoupedeckButton::N7.ordinal()
}
}
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]
pub struct LoupedeckDeviceDisplayConfiguration { pub struct LoupedeckDeviceDisplayConfiguration {

View file

@ -1,9 +1,45 @@
use bytes::Bytes; use bytes::Bytes;
use enum_ordinalize::Ordinalize;
use rgb::RGB8; use rgb::RGB8;
use crate::characteristics::LoupedeckButton; use crate::characteristics::LoupedeckButton;
#[derive(Debug, Ordinalize)]
#[repr(u8)]
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)] #[derive(Debug)]
pub(crate) enum LoupedeckCommand { pub(crate) enum LoupedeckCommand {
RequestSerialNumber,
RequestFirmwareVersion,
SetBrightness(f32), SetBrightness(f32),
SetButtonColor { SetButtonColor {
button: LoupedeckButton, button: LoupedeckButton,
@ -19,5 +55,8 @@ pub(crate) enum LoupedeckCommand {
}, },
RefreshDisplay { RefreshDisplay {
display_id: u8, display_id: u8,
} },
Vibrate {
pattern: VibrationPattern
},
} }

View file

@ -1,16 +1,15 @@
use std::io; use std::io;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc;
use std::thread::{sleep, spawn}; use std::thread::{sleep, spawn};
use std::time::Duration; use std::time::Duration;
use bytes::Bytes; use bytes::Bytes;
use crossbeam_channel::{Receiver};
use rgb::{ComponentSlice, RGB8}; use rgb::{ComponentSlice, RGB8};
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
use thiserror::Error; use thiserror::Error;
use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration}; use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration};
use crate::commands::LoupedeckCommand; use crate::commands::{LoupedeckCommand, VibrationPattern};
use crate::events::LoupedeckEvent; use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START}; use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START};
use crate::util::convert_rgb888_to_rgb565; use crate::util::convert_rgb888_to_rgb565;
@ -37,8 +36,10 @@ impl AvailableLoupedeckDevice {
#[derive(Debug)] #[derive(Debug)]
pub struct LoupedeckDevice { pub struct LoupedeckDevice {
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
pub(crate) events_channel: Receiver<LoupedeckEvent>, pub(crate) serial_number: String,
commands_sender: Sender<LoupedeckCommand>, pub(crate) firmware_version: String,
events_receiver: crossbeam_channel::Receiver<LoupedeckEvent>,
commands_sender: crossbeam_channel::Sender<LoupedeckCommand>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -49,8 +50,11 @@ pub enum ConnectError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
IO(#[from] io::Error), IO(#[from] io::Error),
#[error("The device did not respond with the expected handshake response.")] #[error("The device did not respond with the expected handshake response (early-stage).")]
WrongHandshakeResponse, WrongEarlyHandshakeResponse,
#[error("The device did not respond with the expected handshake response (late-stage).")]
WrongLateHandshakeResponse,
#[error("The device was already connected.")] #[error("The device was already connected.")]
AlreadyConnected, AlreadyConnected,
@ -62,6 +66,15 @@ pub enum RefreshDisplayError {
UnknownDisplay, UnknownDisplay,
} }
#[derive(Debug, Error)]
pub enum SetButtonColorError {
#[error("The specified button is not available for this device.")]
UnknownButton,
#[error("The button does not allow setting a color.")]
ColorNotSupported
}
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum ReplaceFramebufferAreaError { pub enum ReplaceFramebufferAreaError {
#[error("The specified display is not available for this device.")] #[error("The specified display is not available for this device.")]
@ -82,16 +95,34 @@ impl LoupedeckDevice {
self.characteristics self.characteristics
} }
pub fn events_channel(&self) -> Receiver<LoupedeckEvent> { pub fn serial_number(&self) -> &String {
self.events_channel.clone() &self.serial_number
}
pub fn firmware_version(&self) -> &String {
&self.firmware_version
}
pub fn events_channel(&self) -> crossbeam_channel::Receiver<LoupedeckEvent> {
self.events_receiver.clone()
} }
pub fn set_brightness(&self, value: f32) { pub fn set_brightness(&self, value: f32) {
self.commands_sender.send(LoupedeckCommand::SetBrightness(value)).unwrap(); self.commands_sender.send(LoupedeckCommand::SetBrightness(value)).unwrap();
} }
pub fn set_button_color(&self, button: LoupedeckButton, color: RGB8) { pub fn set_button_color(&self, button: LoupedeckButton, color: RGB8) -> Result<(), SetButtonColorError> {
if !self.characteristics.available_buttons.contains(button) {
return Err(SetButtonColorError::UnknownButton);
}
if !button.supports_color() {
return Err(SetButtonColorError::ColorNotSupported);
}
self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap(); self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap();
Ok(())
} }
/// Replaces the specified framebuffer area of the display with `buffer`. /// Replaces the specified framebuffer area of the display with `buffer`.
@ -163,6 +194,12 @@ impl LoupedeckDevice {
Ok(()) Ok(())
} }
pub fn vibrate(&self, pattern: VibrationPattern) {
self.commands_sender.send(LoupedeckCommand::Vibrate {
pattern
}).unwrap();
}
pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> { pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> {
let ports = serialport::available_ports()?; let ports = serialport::available_ports()?;
@ -199,7 +236,7 @@ impl LoupedeckDevice {
port.read_exact(&mut buf)?; port.read_exact(&mut buf)?;
if buf != WS_UPGRADE_RESPONSE_START.as_bytes() { if buf != WS_UPGRADE_RESPONSE_START.as_bytes() {
return Err(ConnectError::WrongHandshakeResponse); return Err(ConnectError::WrongEarlyHandshakeResponse);
} }
// I dont know why. There is garbage in the buffer without this. // I dont know why. There is garbage in the buffer without this.
@ -207,19 +244,36 @@ impl LoupedeckDevice {
port.clear(ClearBuffer::Input)?; port.clear(ClearBuffer::Input)?;
let cloned_port = port.try_clone().expect("port must be cloneable"); let cloned_port = port.try_clone().expect("port must be cloneable");
let (events_sender, events_receiver) = crossbeam_channel::unbounded::<LoupedeckEvent>(); let (public_events_sender, public_events_receiver) = crossbeam_channel::unbounded::<LoupedeckEvent>();
let (internal_events_sender, internal_events_receiver) = mpsc::sync_channel(2);
spawn(move || { spawn(move || {
read_messages_worker(port, events_sender); read_messages_worker(port, public_events_sender, internal_events_sender);
}); });
let (commands_sender, commands_receiver) = channel(); let (commands_sender, commands_receiver) = crossbeam_channel::unbounded::<LoupedeckCommand>();
spawn(move || { spawn(move || {
write_messages_worker(cloned_port, commands_receiver); 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(1)) {
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(1)) {
Ok(LoupedeckInternalEvent::GetFirmwareVersionResponse { firmware_version }) => Ok(firmware_version),
_ => Err(ConnectError::WrongLateHandshakeResponse)
}?;
drop(internal_events_receiver);
Ok(LoupedeckDevice { Ok(LoupedeckDevice {
characteristics, characteristics,
events_channel: events_receiver, serial_number,
firmware_version,
events_receiver: public_events_receiver,
commands_sender, commands_sender,
}) })
} }

View file

@ -6,8 +6,19 @@ pub enum RotationDirection {
Counterclockwise Counterclockwise
} }
#[derive(Debug)]
pub(crate) enum LoupedeckInternalEvent {
GetSerialNumberResponse {
serial_number: String
},
GetFirmwareVersionResponse {
firmware_version: String
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum LoupedeckEvent { pub enum LoupedeckEvent {
Disconnected,
ButtonDown { ButtonDown {
button: LoupedeckButton button: LoupedeckButton
}, },

View file

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

View file

@ -1,14 +1,13 @@
use std::cmp::min; use std::cmp::min;
use std::io::ErrorKind::TimedOut; use std::io::ErrorKind::TimedOut;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::sync::mpsc::Receiver; use std::sync::mpsc;
use bytes::{Buf, BufMut, Bytes, BytesMut}; use bytes::{Buf, BufMut, Bytes, BytesMut};
use crossbeam_channel::Sender;
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use serialport::SerialPort; use serialport::SerialPort;
use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
use crate::commands::LoupedeckCommand; use crate::commands::LoupedeckCommand;
use crate::events::LoupedeckEvent; use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
use crate::events::RotationDirection::{Clockwise, Counterclockwise}; use crate::events::RotationDirection::{Clockwise, Counterclockwise};
pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html pub(crate) const WS_UPGRADE_REQUEST: &str = r#"GET /index.html
@ -27,7 +26,31 @@ Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s=";
const MESSAGE_START_BYTE: u8 = 0x82; const MESSAGE_START_BYTE: u8 = 0x82;
const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize; const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize;
pub(crate) fn read_messages_worker(mut port: Box<dyn SerialPort>, sender: Sender<LoupedeckEvent>) { 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: crossbeam_channel::Sender<LoupedeckEvent>,
internal_sender: mpsc::SyncSender<LoupedeckInternalEvent>,
) {
let mut internal_sender = Some(internal_sender);
let mut buffer = BytesMut::new(); let mut buffer = BytesMut::new();
loop { loop {
@ -67,12 +90,24 @@ pub(crate) fn read_messages_worker(mut port: Box<dyn SerialPort>, sender: Sender
let mut message = buffer.split_to(length); let mut message = buffer.split_to(length);
let command = message[3]; let command = message[3];
let transaction_id = message[4]; // let transaction_id = message[4];
message.advance(5); message.advance(5);
let event = parse_message(command, message.freeze()); let result = parse_message(command, message.freeze());
if let Some(event) = event { match result {
sender.send(event).unwrap(); 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) => {
public_sender.send(event).unwrap()
}
ParseMessageResult::Nothing => {}
} }
} }
} else { } else {
@ -82,25 +117,35 @@ pub(crate) fn read_messages_worker(mut port: Box<dyn SerialPort>, sender: Sender
} }
} }
fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> { fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult {
match command { match command {
0x00 => { // Button 0x00 => { // Button
let button = LoupedeckButton::from_ordinal(message[0] as u16) let button = LoupedeckButton::from_ordinal(message[0])
.expect("Invalid button ID"); .expect("Invalid button ID");
Some(match message[1] { match message[1] {
0x00 => LoupedeckEvent::ButtonDown { button }, 0x00 => LoupedeckEvent::ButtonDown { button },
_ => LoupedeckEvent::ButtonUp { button }, _ => LoupedeckEvent::ButtonUp { button },
}) }.into()
} }
0x01 => { // Knob 0x01 => { // Knob
let knob = LoupedeckKnob::from_ordinal(message[0] as u16) let knob = LoupedeckKnob::from_ordinal(message[0])
.expect("Invalid button ID"); .expect("Invalid button ID");
Some(LoupedeckEvent::KnobRotate { LoupedeckEvent::KnobRotate {
knob, knob,
direction: if message[1] == 1 { Clockwise } else { Counterclockwise }, 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 => { // Touch 0x4d | 0x6d => { // Touch
message.advance(1); message.advance(1);
@ -108,21 +153,20 @@ fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> {
let y = message.get_u16(); let y = message.get_u16();
let touch_id = message.get_u8(); let touch_id = message.get_u8();
Some(LoupedeckEvent::Touch { LoupedeckEvent::Touch {
touch_id, touch_id,
x, x,
y, y,
is_end: command == 0x6d, is_end: command == 0x6d,
}) }.into()
} }
_ => { _ => {
// println!("Unknown command: {}", command); ParseMessageResult::Nothing
None
} }
} }
} }
pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Receiver<LoupedeckCommand>) { pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: crossbeam_channel::Receiver<LoupedeckCommand>) {
let mut next_transaction_id = 0; let mut next_transaction_id = 0;
let mut send = |command_id: u8, data: Bytes| { let mut send = |command_id: u8, data: Bytes| {
@ -164,12 +208,18 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Rec
for command in receiver { for command in receiver {
match command { match command {
LoupedeckCommand::RequestSerialNumber => {
send(0x03, Bytes::new());
}
LoupedeckCommand::RequestFirmwareVersion => {
send(0x07, Bytes::new());
}
LoupedeckCommand::SetBrightness(value) => { LoupedeckCommand::SetBrightness(value) => {
let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8; let raw_value = (value.clamp(0f32, 1f32) * 10.0) as u8;
send(0x09, Bytes::copy_from_slice(&[raw_value])); send(0x09, Bytes::copy_from_slice(&[raw_value]));
} }
LoupedeckCommand::SetButtonColor { button, color } => { LoupedeckCommand::SetButtonColor { button, color } => {
send(0x02, Bytes::copy_from_slice(&[button.ordinal() as u8, color.r, color.g, color.b])); send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b]));
} }
LoupedeckCommand::ReplaceFramebufferArea { LoupedeckCommand::ReplaceFramebufferArea {
display_id, display_id,
@ -193,6 +243,9 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Rec
LoupedeckCommand::RefreshDisplay { display_id } => { LoupedeckCommand::RefreshDisplay { display_id } => {
send(0x0f, Bytes::copy_from_slice(&[0, display_id])); send(0x0f, Bytes::copy_from_slice(&[0, display_id]));
} }
LoupedeckCommand::Vibrate { pattern } => {
send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()]));
}
} }
} }
} }