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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
@ -107,6 +113,25 @@ version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
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]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.3"
|
version = "0.20.3"
|
||||||
|
@ -147,6 +172,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"loupedeck_serial",
|
"loupedeck_serial",
|
||||||
|
"rgb",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -271,8 +297,10 @@ name = "loupedeck_serial"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"crossbeam-channel",
|
||||||
"enum-ordinalize",
|
"enum-ordinalize",
|
||||||
"enumset",
|
"enumset",
|
||||||
|
"rgb",
|
||||||
"serialport",
|
"serialport",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
@ -392,6 +420,15 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rgb"
|
||||||
|
version = "0.8.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8"
|
||||||
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
|
|
@ -5,4 +5,5 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
loupedeck_serial = { path = "../loupedeck_serial" }
|
loupedeck_serial = { path = "../loupedeck_serial" }
|
||||||
color-eyre = "0.6.2"
|
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::eyre::ContextCompat;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
|
use rgb::RGB8;
|
||||||
|
use loupedeck_serial::characteristics::LoupedeckButton;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?;
|
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")?
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,6 @@ serialport = "4.3.0"
|
||||||
enum-ordinalize = "4.3.0"
|
enum-ordinalize = "4.3.0"
|
||||||
enumset = "1.1.3"
|
enumset = "1.1.3"
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
thiserror = "1.0.52"
|
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)]
|
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum LoupedeckDeviceKnob {
|
pub enum LoupedeckKnob {
|
||||||
KnobTopLeft = 0x01,
|
KnobTopLeft = 0x01,
|
||||||
KnobCenterLeft = 0x02,
|
KnobCenterLeft = 0x02,
|
||||||
KnobBottomLeft = 0x03,
|
KnobBottomLeft = 0x03,
|
||||||
|
@ -14,7 +14,7 @@ pub enum LoupedeckDeviceKnob {
|
||||||
|
|
||||||
#[derive(Debug, Ordinalize, EnumSetType)]
|
#[derive(Debug, Ordinalize, EnumSetType)]
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
pub enum LoupedeckDeviceButton {
|
pub enum LoupedeckButton {
|
||||||
KnobTopLeft = 0x01,
|
KnobTopLeft = 0x01,
|
||||||
KnobCenterLeft = 0x02,
|
KnobCenterLeft = 0x02,
|
||||||
KnobBottomLeft = 0x03,
|
KnobBottomLeft = 0x03,
|
||||||
|
@ -40,10 +40,14 @@ pub enum LoupedeckDeviceDisplayEndianness {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LoupedeckDeviceDisplayConfiguration {
|
pub struct LoupedeckDeviceDisplayConfiguration {
|
||||||
pub id: u8,
|
pub id: u8,
|
||||||
pub width: usize,
|
pub name: &'static str,
|
||||||
pub height: usize,
|
pub width: u16,
|
||||||
pub offset: (usize, usize),
|
pub height: u16,
|
||||||
pub endianness: LoupedeckDeviceDisplayEndianness
|
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)]
|
#[derive(Debug)]
|
||||||
|
@ -51,57 +55,107 @@ pub struct LoupedeckDeviceCharacteristics {
|
||||||
pub vendor_id: u16,
|
pub vendor_id: u16,
|
||||||
pub product_id: u16,
|
pub product_id: u16,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub available_knobs: EnumSet<LoupedeckDeviceKnob>,
|
pub available_knobs: EnumSet<LoupedeckKnob>,
|
||||||
pub available_buttons: EnumSet<LoupedeckDeviceButton>,
|
pub available_buttons: EnumSet<LoupedeckButton>,
|
||||||
pub key_grid_dimensions: (usize, usize),
|
pub key_grid_rows: u8,
|
||||||
pub displays: &'static [LoupedeckDeviceDisplayConfiguration]
|
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 {
|
static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics {
|
||||||
vendor_id: 0x2ec2,
|
vendor_id: 0x2ec2,
|
||||||
product_id: 0x0004,
|
product_id: 0x0004,
|
||||||
name: "Loupedeck Live",
|
name: "Loupedeck Live",
|
||||||
available_knobs: enum_set!(LoupedeckDeviceKnob::KnobTopLeft
|
available_knobs: enum_set!(LoupedeckKnob::KnobTopLeft
|
||||||
| LoupedeckDeviceKnob::KnobCenterLeft
|
| LoupedeckKnob::KnobCenterLeft
|
||||||
| LoupedeckDeviceKnob::KnobBottomLeft
|
| LoupedeckKnob::KnobBottomLeft
|
||||||
| LoupedeckDeviceKnob::KnobTopRight
|
| LoupedeckKnob::KnobTopRight
|
||||||
| LoupedeckDeviceKnob::KnobCenterRight
|
| LoupedeckKnob::KnobCenterRight
|
||||||
| LoupedeckDeviceKnob::KnobBottomRight),
|
| LoupedeckKnob::KnobBottomRight),
|
||||||
available_buttons: enum_set!(LoupedeckDeviceButton::KnobTopLeft
|
available_buttons: enum_set!(LoupedeckButton::KnobTopLeft
|
||||||
| LoupedeckDeviceButton::KnobCenterLeft
|
| LoupedeckButton::KnobCenterLeft
|
||||||
| LoupedeckDeviceButton::KnobBottomLeft
|
| LoupedeckButton::KnobBottomLeft
|
||||||
| LoupedeckDeviceButton::KnobTopRight
|
| LoupedeckButton::KnobTopRight
|
||||||
| LoupedeckDeviceButton::KnobCenterRight
|
| LoupedeckButton::KnobCenterRight
|
||||||
| LoupedeckDeviceButton::KnobBottomRight
|
| LoupedeckButton::KnobBottomRight
|
||||||
| LoupedeckDeviceButton::N0
|
| LoupedeckButton::N0
|
||||||
| LoupedeckDeviceButton::N1
|
| LoupedeckButton::N1
|
||||||
| LoupedeckDeviceButton::N2
|
| LoupedeckButton::N2
|
||||||
| LoupedeckDeviceButton::N3
|
| LoupedeckButton::N3
|
||||||
| LoupedeckDeviceButton::N4
|
| LoupedeckButton::N4
|
||||||
| LoupedeckDeviceButton::N5
|
| LoupedeckButton::N5
|
||||||
| LoupedeckDeviceButton::N6
|
| LoupedeckButton::N6
|
||||||
| LoupedeckDeviceButton::N7),
|
| LoupedeckButton::N7),
|
||||||
key_grid_dimensions: (4, 3),
|
key_grid_rows: 3,
|
||||||
displays: &[
|
key_grid_columns: 4,
|
||||||
LoupedeckDeviceDisplayConfiguration { // Left
|
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,
|
id: 0x4d,
|
||||||
|
name: "left",
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 270,
|
height: 270,
|
||||||
offset: (0, 0),
|
local_offset_x: 0,
|
||||||
|
local_offset_y: 0,
|
||||||
|
global_offset_x: 0,
|
||||||
|
global_offset_y: 0,
|
||||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
||||||
},
|
},
|
||||||
LoupedeckDeviceDisplayConfiguration { // Center
|
LoupedeckDeviceDisplayConfiguration {
|
||||||
id: 0x4d,
|
|
||||||
width: 360,
|
|
||||||
height: 270,
|
|
||||||
offset: (60, 0),
|
|
||||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
|
||||||
},
|
|
||||||
LoupedeckDeviceDisplayConfiguration { // Right
|
|
||||||
id: 0x4d,
|
id: 0x4d,
|
||||||
|
name: "right",
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 270,
|
height: 270,
|
||||||
offset: (420, 0),
|
local_offset_x: 420,
|
||||||
|
local_offset_y: 0,
|
||||||
|
global_offset_x: 420,
|
||||||
|
global_offset_y: 0,
|
||||||
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian,
|
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;
|
||||||
use std::io::{BufReader, Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::io::ErrorKind::TimedOut;
|
use std::sync::mpsc::{channel, Sender};
|
||||||
use std::sync::Arc;
|
use std::thread::{sleep, spawn};
|
||||||
use std::sync::mpsc::{Sender, sync_channel, SyncSender};
|
use std::time::Duration;
|
||||||
use std::thread::spawn;
|
use crossbeam_channel::{Receiver};
|
||||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
use rgb::RGB8;
|
||||||
use serialport::{SerialPort, SerialPortType};
|
use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use crate::characteristics::{CHARACTERISTICS, LoupedeckDeviceCharacteristics};
|
use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics};
|
||||||
use crate::discovery::AvailableLoupedeckDevice;
|
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
|
#[derive(Debug)]
|
||||||
HTTP/1.1
|
pub struct AvailableLoupedeckDevice {
|
||||||
Connection: Upgrade
|
pub(crate) port_name: String,
|
||||||
Upgrade: websocket
|
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
|
||||||
Sec-WebSocket-Key: 123abc
|
}
|
||||||
|
|
||||||
"#;
|
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\
|
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
|
||||||
Upgrade: websocket\r\n\
|
self.characteristics
|
||||||
Connection: Upgrade\r\n\
|
}
|
||||||
Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s=";
|
|
||||||
|
|
||||||
const MESSAGE_START_BYTE: u8 = 0x82;
|
pub fn connect(&self) -> Result<LoupedeckDevice, ConnectError> {
|
||||||
const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize;
|
LoupedeckDevice::connect(&self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LoupedeckDevice {
|
pub struct LoupedeckDevice {
|
||||||
pub(crate) port: Option<Box<dyn SerialPort>>,
|
pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics,
|
||||||
pub characteristics: &'static LoupedeckDeviceCharacteristics,
|
pub(crate) events_channel: Receiver<LoupedeckEvent>,
|
||||||
|
commands_sender: Sender<LoupedeckCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ConnectError {
|
pub enum ConnectError {
|
||||||
#[error("IO Error: {0}")]
|
#[error("Serial port error: {0}")]
|
||||||
|
SerialPort(#[from] serialport::Error),
|
||||||
|
|
||||||
|
#[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.")]
|
||||||
WrongHandshakeResponse,
|
WrongHandshakeResponse,
|
||||||
|
|
||||||
|
#[error("The device was already connected.")]
|
||||||
|
AlreadyConnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoupedeckDevice {
|
impl LoupedeckDevice {
|
||||||
pub fn new_unchecked(port: Box<dyn SerialPort>, characteristics: &'static LoupedeckDeviceCharacteristics) -> LoupedeckDevice {
|
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
|
||||||
LoupedeckDevice {
|
self.characteristics
|
||||||
port: Some(port),
|
}
|
||||||
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> {
|
pub fn discover() -> Result<Vec<AvailableLoupedeckDevice>, serialport::Error> {
|
||||||
|
@ -69,8 +91,15 @@ impl LoupedeckDevice {
|
||||||
}).collect::<Vec<_>>())
|
}).collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect(&mut self) -> Result<(), ConnectError> {
|
pub(crate) fn connect(AvailableLoupedeckDevice { port_name, characteristics }: &AvailableLoupedeckDevice) -> Result<LoupedeckDevice, ConnectError> {
|
||||||
let mut port = self.port.take().expect("already connected");
|
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.write_all(WS_UPGRADE_REQUEST.as_bytes())?;
|
||||||
port.flush()?;
|
port.flush()?;
|
||||||
|
|
||||||
|
@ -81,65 +110,25 @@ impl LoupedeckDevice {
|
||||||
return Err(ConnectError::WrongHandshakeResponse);
|
return Err(ConnectError::WrongHandshakeResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (sender, receiver) = sync_channel::<Bytes>(8);
|
// I don’t know why. There is garbage in the buffer without this.
|
||||||
let handle = spawn(move || {
|
sleep(Duration::from_secs(1));
|
||||||
LoupedeckDevice::handle_messages(port, sender);
|
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 {
|
let (commands_sender, commands_receiver) = channel();
|
||||||
dbg!(&x[1]);
|
spawn(move || {
|
||||||
}
|
write_messages_worker(cloned_port, commands_receiver);
|
||||||
|
});
|
||||||
|
|
||||||
handle.join().unwrap();
|
Ok(LoupedeckDevice {
|
||||||
|
characteristics,
|
||||||
Ok(())
|
events_channel: events_receiver,
|
||||||
|
commands_sender
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 characteristics;
|
||||||
pub mod device;
|
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