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::Result;
use rgb::RGB8;
use loupedeck_serial::characteristics::LoupedeckButton;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::LoupedeckEvent;
fn main() -> Result<()> {
let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?;
@ -13,39 +14,23 @@ fn main() -> Result<()> {
.wrap_err("at least one device should be connected")?
.connect()?;
println!("Version: {}\nSerial number: {}", device.firmware_version(), device.serial_number());
device.set_brightness(1.0);
let buttons = [
LoupedeckButton::N0,
LoupedeckButton::N1,
LoupedeckButton::N2,
LoupedeckButton::N3,
LoupedeckButton::N4,
LoupedeckButton::N5,
LoupedeckButton::N6,
LoupedeckButton::N7,
];
// run_vibrations(&device)?;
run_rainbow(&device)?;
let mut value = 0u8;
Ok(())
}
fn run_rainbow(device: &LoupedeckDevice) -> Result<()> {
let interval = Duration::from_millis(50);
let start = Instant::now();
let mut iteration = 0;
let buttons = device.characteristics().available_buttons.iter().filter(|b| b.supports_color()).collect::<Vec<_>>();
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;
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 / 500.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(
&device.characteristics().key_grid_display,
0,
0,
90,
90,
&[RGB8::new(value, 0, 0); 90 * 90]
)?;
device.refresh_display(&device.characteristics().key_grid_display)?;
sleep(Duration::from_millis(50));
sleep((iteration + 1) * interval - start.elapsed());
iteration += 1;
}
}
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;
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u16)]
#[repr(u8)]
pub enum LoupedeckKnob {
KnobTopLeft = 0x01,
KnobCenterLeft = 0x02,
@ -14,7 +14,7 @@ pub enum LoupedeckKnob {
}
#[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u16)]
#[repr(u8)]
pub enum LoupedeckButton {
KnobTopLeft = 0x01,
KnobCenterLeft = 0x02,
@ -32,6 +32,12 @@ pub enum LoupedeckButton {
N7 = 0x0e,
}
impl LoupedeckButton {
pub fn supports_color(&self) -> bool {
self.ordinal() >= LoupedeckButton::N0.ordinal() && self.ordinal() <= LoupedeckButton::N7.ordinal()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceDisplayConfiguration {

View file

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

View file

@ -1,16 +1,15 @@
use std::io;
use std::io::{Read, Write};
use std::sync::mpsc::{channel, Sender};
use std::sync::mpsc;
use std::thread::{sleep, spawn};
use std::time::Duration;
use bytes::Bytes;
use crossbeam_channel::{Receiver};
use rgb::{ComponentSlice, RGB8};
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
use thiserror::Error;
use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration};
use crate::commands::LoupedeckCommand;
use crate::events::LoupedeckEvent;
use crate::commands::{LoupedeckCommand, VibrationPattern};
use crate::events::{LoupedeckEvent, LoupedeckInternalEvent};
use crate::messages::{read_messages_worker, write_messages_worker, WS_UPGRADE_REQUEST, WS_UPGRADE_RESPONSE_START};
use crate::util::convert_rgb888_to_rgb565;
@ -37,8 +36,10 @@ impl AvailableLoupedeckDevice {
#[derive(Debug)]
pub struct LoupedeckDevice {
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
pub(crate) events_channel: Receiver<LoupedeckEvent>,
commands_sender: Sender<LoupedeckCommand>,
pub(crate) serial_number: String,
pub(crate) firmware_version: String,
events_receiver: crossbeam_channel::Receiver<LoupedeckEvent>,
commands_sender: crossbeam_channel::Sender<LoupedeckCommand>,
}
#[derive(Debug, Error)]
@ -49,8 +50,11 @@ pub enum ConnectError {
#[error("IO error: {0}")]
IO(#[from] io::Error),
#[error("The device did not respond with the expected handshake response.")]
WrongHandshakeResponse,
#[error("The device did not respond with the expected handshake response (early-stage).")]
WrongEarlyHandshakeResponse,
#[error("The device did not respond with the expected handshake response (late-stage).")]
WrongLateHandshakeResponse,
#[error("The device was already connected.")]
AlreadyConnected,
@ -62,6 +66,15 @@ pub enum RefreshDisplayError {
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)]
pub enum ReplaceFramebufferAreaError {
#[error("The specified display is not available for this device.")]
@ -82,16 +95,34 @@ impl LoupedeckDevice {
self.characteristics
}
pub fn events_channel(&self) -> Receiver<LoupedeckEvent> {
self.events_channel.clone()
pub fn serial_number(&self) -> &String {
&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) {
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();
Ok(())
}
/// Replaces the specified framebuffer area of the display with `buffer`.
@ -163,6 +194,12 @@ impl LoupedeckDevice {
Ok(())
}
pub fn vibrate(&self, pattern: VibrationPattern) {
self.commands_sender.send(LoupedeckCommand::Vibrate {
pattern
}).unwrap();
}
pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> {
let ports = serialport::available_ports()?;
@ -199,7 +236,7 @@ impl LoupedeckDevice {
port.read_exact(&mut buf)?;
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.
@ -207,19 +244,36 @@ impl LoupedeckDevice {
port.clear(ClearBuffer::Input)?;
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 || {
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 || {
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 {
characteristics,
events_channel: events_receiver,
serial_number,
firmware_version,
events_receiver: public_events_receiver,
commands_sender,
})
}

View file

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

View file

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

View file

@ -1,14 +1,13 @@
use std::cmp::min;
use std::io::ErrorKind::TimedOut;
use std::io::{Read, Write};
use std::sync::mpsc::Receiver;
use std::sync::mpsc;
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::{LoupedeckEvent, LoupedeckInternalEvent};
use crate::events::RotationDirection::{Clockwise, Counterclockwise};
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 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();
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 command = message[3];
let transaction_id = message[4];
// let transaction_id = message[4];
message.advance(5);
let event = parse_message(command, message.freeze());
if let Some(event) = event {
sender.send(event).unwrap();
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) => {
public_sender.send(event).unwrap()
}
ParseMessageResult::Nothing => {}
}
}
} 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 {
0x00 => { // Button
let button = LoupedeckButton::from_ordinal(message[0] as u16)
let button = LoupedeckButton::from_ordinal(message[0])
.expect("Invalid button ID");
Some(match message[1] {
match message[1] {
0x00 => LoupedeckEvent::ButtonDown { button },
_ => LoupedeckEvent::ButtonUp { button },
})
}.into()
}
0x01 => { // Knob
let knob = LoupedeckKnob::from_ordinal(message[0] as u16)
let knob = LoupedeckKnob::from_ordinal(message[0])
.expect("Invalid button ID");
Some(LoupedeckEvent::KnobRotate {
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 => { // Touch
message.advance(1);
@ -108,21 +153,20 @@ fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> {
let y = message.get_u16();
let touch_id = message.get_u8();
Some(LoupedeckEvent::Touch {
LoupedeckEvent::Touch {
touch_id,
x,
y,
is_end: command == 0x6d,
})
}.into()
}
_ => {
// println!("Unknown command: {}", command);
None
ParseMessageResult::Nothing
}
}
}
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 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 {
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() as u8, color.r, color.g, color.b]));
send(0x02, Bytes::copy_from_slice(&[button.ordinal(), color.r, color.g, color.b]));
}
LoupedeckCommand::ReplaceFramebufferArea {
display_id,
@ -193,6 +243,9 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Rec
LoupedeckCommand::RefreshDisplay { display_id } => {
send(0x0f, Bytes::copy_from_slice(&[0, display_id]));
}
LoupedeckCommand::Vibrate { pattern } => {
send(0x1b, Bytes::copy_from_slice(&[pattern.ordinal()]));
}
}
}
}