diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 61c84cb..9c3a7c7 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -4,6 +4,7 @@ use color_eyre::eyre::ContextCompat; use color_eyre::Result; use rgb::RGB8; use loupedeck_serial::characteristics::LoupedeckButton; +use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; fn main() -> Result<()> { let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; @@ -12,7 +13,7 @@ fn main() -> Result<()> { .wrap_err("at least one device should be connected")? .connect()?; - device.set_brightness(0.5); + device.set_brightness(1.0); let buttons = [ LoupedeckButton::N0, @@ -25,17 +26,49 @@ fn main() -> Result<()> { LoupedeckButton::N7, ]; + let mut value = 0u8; + let start = Instant::now(); loop { - sleep(Duration::from_millis(50)); + 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() { + let t = (ms + (index * 100) as u64) as f32; + 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, + (((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, )); } - } - Ok(()) + 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)); + } } diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index 85d1448..10462d7 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -1,5 +1,6 @@ use enum_ordinalize::Ordinalize; use enumset::{enum_set, EnumSet, EnumSetType}; +use crate::util::Endianness; #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u16)] @@ -32,12 +33,7 @@ pub enum LoupedeckButton { } #[derive(Debug)] -pub enum LoupedeckDeviceDisplayEndianness { - BigEndian, - LittleEndian -} - -#[derive(Debug)] +#[non_exhaustive] pub struct LoupedeckDeviceDisplayConfiguration { pub id: u8, pub name: &'static str, @@ -47,10 +43,11 @@ pub struct LoupedeckDeviceDisplayConfiguration { pub local_offset_y: u16, pub global_offset_x: u16, pub global_offset_y: u16, - pub endianness: LoupedeckDeviceDisplayEndianness, + pub endianness: Endianness, } #[derive(Debug)] +#[non_exhaustive] pub struct LoupedeckDeviceCharacteristics { pub vendor_id: u16, pub product_id: u16, @@ -60,14 +57,19 @@ pub struct LoupedeckDeviceCharacteristics { pub key_grid_rows: u8, pub key_grid_columns: u8, pub key_grid_display: LoupedeckDeviceDisplayConfiguration, - pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration] + pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration], } 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> { 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; + y >= display.global_offset_y && y <= display.global_offset_y + display.height; if check(&&self.key_grid_display) { Some(&self.key_grid_display) @@ -77,22 +79,21 @@ impl LoupedeckDeviceCharacteristics { } pub fn get_key_at_coordinates(&self, x: u16, y: u16) -> Option { - 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; + let (column_width, row_height) = self.key_size(); 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_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 + return None; } - let column = (local_x as f64 / column_width).floor() as u8; - let row = (local_y as f64 / row_height).floor() as u8; + let column = (local_x / column_width) as u8; + let row = (local_y / row_height) as u8; Some(row * self.key_grid_columns + column) } @@ -133,7 +134,7 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck local_offset_y: 0, global_offset_x: 60, global_offset_y: 0, - endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + endianness: Endianness::LittleEndian, }, additional_displays: &[ LoupedeckDeviceDisplayConfiguration { @@ -145,7 +146,7 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck local_offset_y: 0, global_offset_x: 0, global_offset_y: 0, - endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + endianness: Endianness::LittleEndian, }, LoupedeckDeviceDisplayConfiguration { id: 0x4d, @@ -156,9 +157,9 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck local_offset_y: 0, global_offset_x: 420, global_offset_y: 0, - endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + endianness: Endianness::LittleEndian, }, - ] + ], }; pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [ diff --git a/loupedeck_serial/src/commands.rs b/loupedeck_serial/src/commands.rs index 6bfda8c..3983913 100644 --- a/loupedeck_serial/src/commands.rs +++ b/loupedeck_serial/src/commands.rs @@ -1,3 +1,4 @@ +use bytes::Bytes; use rgb::RGB8; use crate::characteristics::LoupedeckButton; @@ -6,6 +7,17 @@ pub(crate) enum LoupedeckCommand { SetBrightness(f32), SetButtonColor { button: LoupedeckButton, - color: RGB8 + color: RGB8, + }, + ReplaceFramebufferArea { + display_id: u8, + x: u16, + y: u16, + width: u16, + height: u16, + buffer: Bytes, + }, + RefreshDisplay { + display_id: u8, } } \ No newline at end of file diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs index be87be6..cf5ae96 100644 --- a/loupedeck_serial/src/device.rs +++ b/loupedeck_serial/src/device.rs @@ -3,14 +3,16 @@ use std::io::{Read, Write}; use std::sync::mpsc::{channel, Sender}; use std::thread::{sleep, spawn}; use std::time::Duration; +use bytes::Bytes; use crossbeam_channel::{Receiver}; -use rgb::RGB8; +use rgb::{ComponentSlice, RGB8}; use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPortType, StopBits}; use thiserror::Error; -use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics}; +use crate::characteristics::{CHARACTERISTICS, LoupedeckButton, LoupedeckDeviceCharacteristics, LoupedeckDeviceDisplayConfiguration}; use crate::commands::LoupedeckCommand; use crate::events::LoupedeckEvent; 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)] pub struct AvailableLoupedeckDevice { @@ -28,7 +30,7 @@ impl AvailableLoupedeckDevice { } pub fn connect(&self) -> Result { - LoupedeckDevice::connect(&self) + LoupedeckDevice::connect(self) } } @@ -54,6 +56,27 @@ pub enum ConnectError { 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 { pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { self.characteristics @@ -71,6 +94,75 @@ impl LoupedeckDevice { 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 don’t 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, serialport::Error> { let ports = serialport::available_ports()?; @@ -128,7 +220,7 @@ impl LoupedeckDevice { Ok(LoupedeckDevice { characteristics, events_channel: events_receiver, - commands_sender + commands_sender, }) } } diff --git a/loupedeck_serial/src/lib.rs b/loupedeck_serial/src/lib.rs index 9c3ada9..229e249 100644 --- a/loupedeck_serial/src/lib.rs +++ b/loupedeck_serial/src/lib.rs @@ -2,4 +2,5 @@ pub mod characteristics; pub mod device; pub mod events; mod messages; -mod commands; \ No newline at end of file +mod commands; +mod util; \ No newline at end of file diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs index a8eca45..6c6dda9 100644 --- a/loupedeck_serial/src/messages.rs +++ b/loupedeck_serial/src/messages.rs @@ -116,7 +116,7 @@ fn parse_message(command: u8, mut message: Bytes) -> Option { }) } _ => { - println!("Unknown command: {}", command); + // println!("Unknown command: {}", command); None } } @@ -171,6 +171,28 @@ pub(crate) fn write_messages_worker(mut port: Box, receiver: Rec LoupedeckCommand::SetButtonColor { button, color } => { 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])); + } } } } \ No newline at end of file diff --git a/loupedeck_serial/src/util.rs b/loupedeck_serial/src/util.rs new file mode 100644 index 0000000..169f79b --- /dev/null +++ b/loupedeck_serial/src/util.rs @@ -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 +} \ No newline at end of file