commit
This commit is contained in:
parent
e1436733a5
commit
dbb492a72d
7 changed files with 226 additions and 33 deletions
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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;
|
||||
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] = [
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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, ConnectError> {
|
||||
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<Vec<AvailableLoupedeckDevice>, serialport::Error> {
|
||||
let ports = serialport::available_ports()?;
|
||||
|
||||
|
@ -128,7 +220,7 @@ impl LoupedeckDevice {
|
|||
Ok(LoupedeckDevice {
|
||||
characteristics,
|
||||
events_channel: events_receiver,
|
||||
commands_sender
|
||||
commands_sender,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,5 @@ pub mod characteristics;
|
|||
pub mod device;
|
||||
pub mod events;
|
||||
mod messages;
|
||||
mod commands;
|
||||
mod commands;
|
||||
mod util;
|
|
@ -116,7 +116,7 @@ fn parse_message(command: u8, mut message: Bytes) -> Option<LoupedeckEvent> {
|
|||
})
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown command: {}", command);
|
||||
// println!("Unknown command: {}", command);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -171,6 +171,28 @@ pub(crate) fn write_messages_worker(mut port: Box<dyn SerialPort>, 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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
loupedeck_serial/src/util.rs
Normal file
32
loupedeck_serial/src/util.rs
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue