use std::cell::RefCell; use std::path::Path; use std::sync::Arc; use std::thread; use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; use enum_ordinalize::Ordinalize; use log::{error, info, trace}; use rgb::RGB8; use tiny_skia::IntSize; use deckster_shared::handler_communication::KnobEvent; use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind}; use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; use deckster_shared::state::{Key, Knob}; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use crate::handler_host; use crate::handler_host::KeyOrKnobConfig; use crate::icons::IconManager; use crate::model::config::Config; use crate::model::position::ButtonPosition; use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::{render_key, render_knob, GraphicsContext}; use crate::runner::state::State; mod graphics; pub mod state; pub async fn start(config_directory: &Path, config: Config) -> Result<()> { let config = Arc::new(config); info!("Discovering devices…"); let available_devices = LoupedeckDevice::discover()?; let available_device = available_devices.first().wrap_err("No device connected.")?; info!("Found {} device(s).", available_devices.len()); let (commands_sender, commands_receiver) = flume::bounded::(5); let (events_sender, events_receiver) = flume::bounded::(5); commands_sender .send(HandlerCommand::SetActivePages { knob_page_id: config.initial.knob_page.clone(), key_page_id: config.initial.key_page.clone(), }) .unwrap(); let key_configs = config .key_pages_by_id .iter() .flat_map(|(page_id, p)| { p.keys.iter().map(|(position, k)| { let (handler_name, mode_string) = k .handler .split_once(' ') .map(|(a, b)| (a.into(), b.into())) .unwrap_or_else(|| (k.handler.as_str().into(), "".into())); ( KeyPath { page_id: page_id.clone(), position: *position, }, KeyOrKnobConfig { handler_name, mode_string, handler_config: Arc::clone(&k.config), }, ) }) }) .collect(); let knob_configs = config .knob_pages_by_id .iter() .flat_map(|(page_id, p)| { p.knobs.iter().filter_map(|(position, k)| { if k.handler.is_empty() { return None; } let (handler_name, mode_string) = k .handler .split_once(' ') .map(|(a, b)| (a.into(), b.into())) .unwrap_or_else(|| (k.handler.as_str().into(), "".into())); Some(( KnobPath { page_id: page_id.clone(), position, }, KeyOrKnobConfig { handler_name, mode_string, handler_config: Arc::clone(&k.config), }, )) }) }) .collect(); info!("Initializing handler processes…"); handler_host::start( &config_directory.join("handlers"), key_configs, knob_configs, commands_sender.clone(), events_receiver, ) .await?; info!("Connecting to the device…"); let device = available_device.connect().wrap_err("Connecting to the device failed.")?; info!("Connected."); device.set_brightness(0.1); device.vibrate(VibrationPattern::RiseFall); let io_worker_context = IoWorkerContext::create(config_directory, Arc::clone(&config), device, commands_sender.clone(), events_sender); let io_worker_thread = thread::Builder::new() .name("deckster IO worker".to_owned()) .spawn(move || { do_io_work(io_worker_context, commands_receiver); }) .wrap_err("Could not spawn the worker thread")?; info!("Ready."); io_worker_thread.join().unwrap(); Ok(()) } enum IoWork { DeviceEvent(LoupedeckEvent), Command(HandlerCommand), } struct IoWorkerContext { config: Arc, device: LoupedeckDevice, commands_sender: flume::Sender, events_sender: flume::Sender, graphics: GraphicsContext, } impl IoWorkerContext { pub fn create( config_directory: &Path, config: Arc, device: LoupedeckDevice, commands_sender: flume::Sender, events_sender: flume::Sender, ) -> Self { let buffer_endianness = device.characteristics().key_grid.display.endianness; let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref())); let dpi = device.characteristics().key_grid.display.dpi; let icon_packs = Arc::clone(&config.icon_packs); IoWorkerContext { config, device, commands_sender, events_sender, graphics: GraphicsContext { buffer_endianness, label_renderer, icon_manager: IconManager::new(config_directory.to_path_buf(), icon_packs, dpi), }, } } } fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver) { let mut state = State::create(&context.config); let device_events_receiver = context.device.events(); loop { let a = flume::Selector::new() .recv(&device_events_receiver, |e| IoWork::DeviceEvent(e.unwrap())) .recv(&commands_receiver, |c| IoWork::Command(c.unwrap())) .wait(); match a { IoWork::DeviceEvent(event) => { if !handle_event(&context, &mut state, event) { break; } } IoWork::Command(command) => handle_command(&context, &mut state, command), } } } fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool { trace!("Handling event: {:?}", &event); let send_key_event = |path: KeyPath, event: KeyEvent| { trace!("Sending key event ({}): {:?}", &path, &event); context.events_sender.send(HandlerEvent::Key { path, event }).unwrap(); }; let send_knob_event = |path: KnobPath, event: KnobEvent| { trace!("Sending knob event ({:?}): {:?}", &path, &event); context.events_sender.send(HandlerEvent::Knob { path, event }).unwrap(); }; match event { LoupedeckEvent::Disconnected => return false, LoupedeckEvent::ButtonDown { button } => { let position = ButtonPosition::of(&button); let button_config = &context.config.buttons[position]; context .commands_sender .send(HandlerCommand::SetActivePages { key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(), knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(), }) .unwrap() } LoupedeckEvent::Touch { x, y, is_end, touch_id } => { let characteristics = context.device.characteristics(); let display = characteristics.get_display_at_coordinates(x, y); if let Some(display) = display { if display == &characteristics.key_grid.display { let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y); if let Some(key_index) = key_index { let position = KeyPosition { x: (key_index % characteristics.key_grid.columns) as u16 + 1, y: (key_index / characteristics.key_grid.columns) as u16 + 1, }; let path = KeyPath { page_id: state.active_key_page_id.clone(), position, }; let LoupedeckDisplayRect { x: top_left_x, y: top_left_y, .. } = characteristics.key_grid.get_local_key_rect(key_index).unwrap(); let kind = if is_end { state.active_touch_ids.remove(&touch_id); KeyTouchEventKind::End } else { let is_new = state.active_touch_ids.insert(touch_id); if is_new { KeyTouchEventKind::Start } else { KeyTouchEventKind::Move } }; send_key_event( path.clone(), KeyEvent::Touch { touch_id, x: x - top_left_x, y: y - top_left_y, kind, }, ); if kind == KeyTouchEventKind::Start { send_key_event(path.clone(), KeyEvent::Press); } } } } } LoupedeckEvent::KnobRotate { knob, direction } => { let position: KnobPosition = get_position_of_loupedeck_knob(knob); send_knob_event( KnobPath { page_id: state.active_knob_page_id.clone(), position, }, KnobEvent::Rotate { direction: match direction { RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise, RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise, }, }, ) } LoupedeckEvent::KnobDown { knob } => { let position: KnobPosition = get_position_of_loupedeck_knob(knob); send_knob_event( KnobPath { page_id: state.active_knob_page_id.clone(), position, }, KnobEvent::Press, ) } _ => {} } true } fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) { trace!("Handling command: {:?}", &command); match command { HandlerCommand::SetActivePages { key_page_id, knob_page_id } => { state.active_key_page_id = key_page_id; state.active_knob_page_id = knob_page_id; for button in LoupedeckButton::VARIANTS { let position = ButtonPosition::of(button); context .device .set_button_color(*button, get_correct_button_color(context, state, position)) .unwrap(); } let key_grid = &context.device.characteristics().key_grid; for index in 0..(key_grid.rows * key_grid.columns) { draw_key_at_index(context, state, index); } for position in KnobPosition::VARIANTS { draw_knob_at_position(context, state, *position); } context.device.refresh_display(&key_grid.display).unwrap(); } HandlerCommand::SetKeyStyle { path, value } => { state.mutate_key_for_command("SetKeyStyle", &path, |k| { k.style = value; }); draw_key_at_path_if_visible(context, state, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); } HandlerCommand::SetKnobStyle { path, value } => { state.mutate_knob_for_command("SetKnobStyle", &path, |k| { k.style = value; }); draw_knob_at_path_if_visible(context, state, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); } HandlerCommand::SetKnobValue { path, value } => { if let Some(v) = value { if !(0.0..=1.0).contains(&v) { error!("Received SetKnobValue with an out-of-range value: {}", v); return; } } state.mutate_knob_for_command("SetKnobValue", &path, |k| { k.value = value; }); draw_knob_at_path_if_visible(context, state, path); } } } // active -> config.active_button_color // no actions defined -> #000000 // inactive -> config.inactive_button_color fn get_correct_button_color(context: &IoWorkerContext, state: &State, button_position: ButtonPosition) -> RGB8 { let button_config = &context.config.buttons[button_position]; if let Some(key_page) = &button_config.key_page { if key_page == &state.active_key_page_id { if let Some(knob_page) = &button_config.knob_page { if knob_page == &state.active_knob_page_id { return context.config.active_button_color.into(); } } } } else if button_config.knob_page.is_none() { return RGB8::new(0, 0, 0); } context.config.inactive_button_color.into() } fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option { if (position.x - 1) >= key_grid.columns as u16 || (position.y - 1) >= key_grid.rows as u16 { None } else { let x = (position.x - 1) as u8; let y = (position.y - 1) as u8; Some(y * key_grid.columns + x) } } fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics, index: u8) -> KeyPosition { let x = index % key_grid.columns; let y = index / key_grid.columns; KeyPosition { x: (x + 1) as u16, y: (y + 1) as u16, } } fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) { let key_grid = &context.device.characteristics().key_grid; let rect = key_grid.get_local_key_rect(index).unwrap(); let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key); context .device .replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer) .unwrap(); } fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) { let position = get_key_position_for_index(&context.device.characteristics().key_grid, index); draw_key(context, index, state.active_key_page().keys_by_position.get(&position)); } fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) { let index = get_key_index_for_position(&context.device.characteristics().key_grid, position); if let Some(index) = index { draw_key(context, index, state.active_key_page().keys_by_position.get(&position)); } } fn draw_key_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KeyPath) { if state.active_key_page_id == path.page_id { draw_key_at_position_if_visible(context, state, path.position); } } fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Knob>) { if let Some((display, rect)) = context.device.characteristics().get_display_and_rect_for_knob(match position { KnobPosition::LeftTop => LoupedeckKnob::LeftTop, KnobPosition::LeftMiddle => LoupedeckKnob::LeftMiddle, KnobPosition::LeftBottom => LoupedeckKnob::LeftBottom, KnobPosition::RightTop => LoupedeckKnob::RightTop, KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle, KnobPosition::RightBottom => LoupedeckKnob::RightBottom, }) { let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob); context .device .replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer) .unwrap(); } } fn draw_knob_at_position(context: &IoWorkerContext, state: &State, position: KnobPosition) { draw_knob(context, position, Some(&state.active_knob_page().knobs_by_position[position])); } fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KnobPath) { if state.active_knob_page_id == path.page_id { draw_knob_at_position(context, state, path.position); } } fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition { match value { LoupedeckKnob::LeftTop => KnobPosition::LeftTop, LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, LoupedeckKnob::RightTop => KnobPosition::RightTop, LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, LoupedeckKnob::RightBottom => KnobPosition::RightBottom, } }