This commit is contained in:
Moritz Ruth 2023-12-29 15:04:56 +01:00
parent e1436733a5
commit dbb492a72d
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
7 changed files with 226 additions and 33 deletions

View file

@ -4,6 +4,7 @@ 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::characteristics::LoupedeckButton;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
fn main() -> Result<()> { fn main() -> Result<()> {
let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?;
@ -12,7 +13,7 @@ fn main() -> Result<()> {
.wrap_err("at least one device should be connected")? .wrap_err("at least one device should be connected")?
.connect()?; .connect()?;
device.set_brightness(0.5); device.set_brightness(1.0);
let buttons = [ let buttons = [
LoupedeckButton::N0, LoupedeckButton::N0,
@ -25,17 +26,49 @@ fn main() -> Result<()> {
LoupedeckButton::N7, LoupedeckButton::N7,
]; ];
let mut value = 0u8;
let start = Instant::now(); let start = Instant::now();
loop { loop {
sleep(Duration::from_millis(50)); while !device.events_channel().is_empty() {
for (index, button) in buttons.iter().enumerate() { let event = device.events_channel().recv().unwrap();
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, match event {
((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 250.0).sin() / 2.0 + 0.5) * 255.0) as u8, LoupedeckEvent::KnobRotate { direction: RotationDirection::Clockwise, .. } => {
((((start.elapsed().as_millis() as f32 + index as f32 * 100.0) / 500.0).sin() / 2.0 + 0.5) * 255.0) as u8, 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);
}
_ => {}
};
} }
Ok(()) let ms = start.elapsed().as_millis() as u64;
for (index, button) in buttons.iter().enumerate() {
let t = (ms + (index * 100) as u64) as f32;
device.set_button_color(*button, RGB8::new(
(((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));
}
} }

View file

@ -1,5 +1,6 @@
use enum_ordinalize::Ordinalize; use enum_ordinalize::Ordinalize;
use enumset::{enum_set, EnumSet, EnumSetType}; use enumset::{enum_set, EnumSet, EnumSetType};
use crate::util::Endianness;
#[derive(Debug, Ordinalize, EnumSetType)] #[derive(Debug, Ordinalize, EnumSetType)]
#[repr(u16)] #[repr(u16)]
@ -32,12 +33,7 @@ pub enum LoupedeckButton {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum LoupedeckDeviceDisplayEndianness { #[non_exhaustive]
BigEndian,
LittleEndian
}
#[derive(Debug)]
pub struct LoupedeckDeviceDisplayConfiguration { pub struct LoupedeckDeviceDisplayConfiguration {
pub id: u8, pub id: u8,
pub name: &'static str, pub name: &'static str,
@ -47,10 +43,11 @@ pub struct LoupedeckDeviceDisplayConfiguration {
pub local_offset_y: u16, pub local_offset_y: u16,
pub global_offset_x: u16, pub global_offset_x: u16,
pub global_offset_y: u16, pub global_offset_y: u16,
pub endianness: LoupedeckDeviceDisplayEndianness, pub endianness: Endianness,
} }
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive]
pub struct LoupedeckDeviceCharacteristics { pub struct LoupedeckDeviceCharacteristics {
pub vendor_id: u16, pub vendor_id: u16,
pub product_id: u16, pub product_id: u16,
@ -60,10 +57,15 @@ pub struct LoupedeckDeviceCharacteristics {
pub key_grid_rows: u8, pub key_grid_rows: u8,
pub key_grid_columns: u8, pub key_grid_columns: u8,
pub key_grid_display: LoupedeckDeviceDisplayConfiguration, pub key_grid_display: LoupedeckDeviceDisplayConfiguration,
pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration] pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration],
} }
impl LoupedeckDeviceCharacteristics { impl LoupedeckDeviceCharacteristics {
pub fn key_size(&self) -> (u16, u16) {
// Assuming the sizes are integers
(self.key_grid_display.width / self.key_grid_columns as u16, self.key_grid_display.height / self.key_grid_rows as u16)
}
pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> { pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> {
let check = |display: &&LoupedeckDeviceDisplayConfiguration| let check = |display: &&LoupedeckDeviceDisplayConfiguration|
x >= display.global_offset_x && x <= display.global_offset_x + display.width && x >= display.global_offset_x && x <= display.global_offset_x + display.width &&
@ -77,22 +79,21 @@ impl LoupedeckDeviceCharacteristics {
} }
pub fn get_key_at_coordinates(&self, x: u16, y: u16) -> Option<u8> { 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 (column_width, row_height) = self.key_size();
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 { if x < self.key_grid_display.global_offset_x || y < self.key_grid_display.global_offset_y {
return None return None;
} }
let local_x = x - self.key_grid_display.global_offset_x; let local_x = x - self.key_grid_display.global_offset_x;
let local_y = y - self.key_grid_display.global_offset_y; 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 { if local_x >= self.key_grid_display.width || local_y >= self.key_grid_display.height {
return None return None;
} }
let column = (local_x as f64 / column_width).floor() as u8; let column = (local_x / column_width) as u8;
let row = (local_y as f64 / row_height).floor() as u8; let row = (local_y / row_height) as u8;
Some(row * self.key_grid_columns + column) Some(row * self.key_grid_columns + column)
} }
@ -133,7 +134,7 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
local_offset_y: 0, local_offset_y: 0,
global_offset_x: 60, global_offset_x: 60,
global_offset_y: 0, global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, endianness: Endianness::LittleEndian,
}, },
additional_displays: &[ additional_displays: &[
LoupedeckDeviceDisplayConfiguration { LoupedeckDeviceDisplayConfiguration {
@ -145,7 +146,7 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
local_offset_y: 0, local_offset_y: 0,
global_offset_x: 0, global_offset_x: 0,
global_offset_y: 0, global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, endianness: Endianness::LittleEndian,
}, },
LoupedeckDeviceDisplayConfiguration { LoupedeckDeviceDisplayConfiguration {
id: 0x4d, id: 0x4d,
@ -156,9 +157,9 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck
local_offset_y: 0, local_offset_y: 0,
global_offset_x: 420, global_offset_x: 420,
global_offset_y: 0, global_offset_y: 0,
endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, endianness: Endianness::LittleEndian,
}, },
] ],
}; };
pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [ pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [

View file

@ -1,3 +1,4 @@
use bytes::Bytes;
use rgb::RGB8; use rgb::RGB8;
use crate::characteristics::LoupedeckButton; use crate::characteristics::LoupedeckButton;
@ -6,6 +7,17 @@ pub(crate) enum LoupedeckCommand {
SetBrightness(f32), SetBrightness(f32),
SetButtonColor { SetButtonColor {
button: LoupedeckButton, button: LoupedeckButton,
color: RGB8 color: RGB8,
},
ReplaceFramebufferArea {
display_id: u8,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: Bytes,
},
RefreshDisplay {
display_id: u8,
} }
} }

View file

@ -3,14 +3,16 @@ use std::io::{Read, Write};
use std::sync::mpsc::{channel, Sender}; use std::sync::mpsc::{channel, Sender};
use std::thread::{sleep, spawn}; use std::thread::{sleep, spawn};
use std::time::Duration; use std::time::Duration;
use bytes::Bytes;
use crossbeam_channel::{Receiver}; use crossbeam_channel::{Receiver};
use rgb::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}; use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration};
use crate::commands::LoupedeckCommand; use crate::commands::LoupedeckCommand;
use crate::events::LoupedeckEvent; use crate::events::LoupedeckEvent;
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;
#[derive(Debug)] #[derive(Debug)]
pub struct AvailableLoupedeckDevice { pub struct AvailableLoupedeckDevice {
@ -28,7 +30,7 @@ impl AvailableLoupedeckDevice {
} }
pub fn connect(&self) -> Result<LoupedeckDevice, ConnectError> { pub fn connect(&self) -> Result<LoupedeckDevice, ConnectError> {
LoupedeckDevice::connect(&self) LoupedeckDevice::connect(self)
} }
} }
@ -54,6 +56,27 @@ pub enum ConnectError {
AlreadyConnected, AlreadyConnected,
} }
#[derive(Debug, Error)]
pub enum RefreshDisplayError {
#[error("The specified display is not available for this device.")]
UnknownDisplay,
}
#[derive(Debug, Error)]
pub enum ReplaceFramebufferAreaError {
#[error("The specified display is not available for this device.")]
UnknownDisplay,
#[error("The area is not (fully) within the bounds of the display.")]
OutOfBounds,
#[error("Given the specified dimensions, the buffer size must be {expected} but it was {actual}.")]
WrongBufferSize {
expected: usize,
actual: usize,
},
}
impl LoupedeckDevice { impl LoupedeckDevice {
pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics {
self.characteristics self.characteristics
@ -71,6 +94,75 @@ impl LoupedeckDevice {
self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap(); self.commands_sender.send(LoupedeckCommand::SetButtonColor { button, color }).unwrap();
} }
/// Replaces the specified framebuffer area of the display with `buffer`.
///
/// `buffer` must contain exactly as many pixels as required.
///
/// Please note that the internal color format of all currently known devices is RGB565 (16 bits in total).
pub fn replace_framebuffer_area(
&self,
display: &LoupedeckDeviceDisplayConfiguration,
x: u16,
y: u16,
width: u16,
height: u16,
buffer: &[RGB8]
) -> Result<(), ReplaceFramebufferAreaError> {
if !std::ptr::eq(display, &self.characteristics.key_grid_display) &&
!self.characteristics.additional_displays.iter().any(|d| std::ptr::eq(display, d)) {
return Err(ReplaceFramebufferAreaError::UnknownDisplay);
}
if x + width > display.width || y + height > display.height {
return Err(ReplaceFramebufferAreaError::OutOfBounds);
}
let expected_buffer_size = (height * width) as usize;
if buffer.len() != expected_buffer_size {
return Err(ReplaceFramebufferAreaError::WrongBufferSize {
expected: expected_buffer_size,
actual: buffer.len(),
});
}
if width == 0 || height == 0 {
return Ok(());
}
let buffer = Bytes::copy_from_slice(buffer.as_slice());
// For some color values x, the pixel brightness is lower than for x - 1.
// I dont understand why and what is the pattern of these values.
let converted_buffer = convert_rgb888_to_rgb565(buffer, display.endianness);
let local_x = display.local_offset_x + x;
let local_y = display.local_offset_y + y;
self.commands_sender.send(LoupedeckCommand::ReplaceFramebufferArea {
display_id: display.id,
x: local_x,
y: local_y,
width,
height,
buffer: converted_buffer.freeze(),
}).unwrap();
Ok(())
}
pub fn refresh_display(&self, display: &LoupedeckDeviceDisplayConfiguration) -> Result<(), RefreshDisplayError>{
if !std::ptr::eq(display, &self.characteristics.key_grid_display) &&
!self.characteristics.additional_displays.iter().any(|d| std::ptr::eq(display, d)) {
return Err(RefreshDisplayError::UnknownDisplay);
}
self.commands_sender.send(LoupedeckCommand::RefreshDisplay {
display_id: display.id
}).unwrap();
Ok(())
}
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()?;
@ -128,7 +220,7 @@ impl LoupedeckDevice {
Ok(LoupedeckDevice { Ok(LoupedeckDevice {
characteristics, characteristics,
events_channel: events_receiver, events_channel: events_receiver,
commands_sender commands_sender,
}) })
} }
} }

View file

@ -3,3 +3,4 @@ pub mod device;
pub mod events; pub mod events;
mod messages; mod messages;
mod commands; mod commands;
mod util;

View file

@ -116,7 +116,7 @@ fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> {
}) })
} }
_ => { _ => {
println!("Unknown command: {}", command); // println!("Unknown command: {}", command);
None None
} }
} }
@ -171,6 +171,28 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, receiver: Rec
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() as u8, color.r, color.g, color.b]));
} }
LoupedeckCommand::ReplaceFramebufferArea {
display_id,
x,
y,
width,
height,
buffer
} => {
let mut data = BytesMut::with_capacity(10 + buffer.len());
data.put_u8(0);
data.put_u8(display_id);
data.put_u16(x);
data.put_u16(y);
data.put_u16(width);
data.put_u16(height);
data.put(buffer);
send(0x10, data.freeze());
}
LoupedeckCommand::RefreshDisplay { display_id } => {
send(0x0f, Bytes::copy_from_slice(&[0, display_id]));
}
} }
} }
} }

View file

@ -0,0 +1,32 @@
use bytes::{BufMut, Bytes, BytesMut};
pub(crate) fn convert_rgb888_to_rgb565(original: Bytes, endianness: Endianness) -> BytesMut {
let pixel_count = original.len() / 3;
let excess_bytes = original.len() % 3;
if excess_bytes != 0 {
panic!("Found {} excess bytes at the end of the original buffer", excess_bytes);
}
let mut result = BytesMut::with_capacity(pixel_count * 2);
for index in 0..pixel_count {
let red = original[index * 3] as u16;
let green = original[index * 3 + 1] as u16;
let blue = original[index * 3 + 2] as u16;
let color = ((red & 0b11111000) << 8) | ((green & 0b11111100) << 3) | (blue.wrapping_shr(3));
match endianness {
Endianness::LittleEndian => result.put_u16_le(color),
Endianness::BigEndian => result.put_u16(color),
}
}
result
}
#[derive(Debug, Copy, Clone)]
pub enum Endianness {
BigEndian,
LittleEndian
}