This commit is contained in:
Moritz Ruth 2024-03-01 01:19:28 +01:00
parent 1ced4381b8
commit f44283160a
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
10 changed files with 181 additions and 138 deletions

View file

@ -50,7 +50,6 @@ pub enum HandlerEvent {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "kebab-case")]
pub enum HandlerCommand {
SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
SetKnobValue { path: KnobPath, value: Option<f32> },

View file

@ -6,6 +6,7 @@ use crate::style::{KeyStyle, KnobStyle};
#[derive(Debug)]
pub struct Key {
pub path: KeyPath,
pub host_id: Box<str>,
pub base_style: KeyStyle,
pub style: Option<KeyStyle>,
}
@ -13,6 +14,7 @@ pub struct Key {
#[derive(Debug)]
pub struct Knob {
pub path: KnobPath,
pub host_id: Box<str>,
pub base_style: KnobStyle,
pub style: Option<KnobStyle>,
pub value: Option<f32>,

View file

@ -1,4 +1,5 @@
use std::cell::RefCell;
use std::collections::HashSet;
use bytes::{BufMut, Bytes, BytesMut};
use resvg::usvg::tiny_skia_path::PathBuilder;
@ -18,10 +19,11 @@ pub struct GraphicsContext {
pub icon_manager: IconManager,
}
pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes {
pub fn render_key(context: &GraphicsContext, key_size: IntSize, active_remote_handler_host_ids: &HashSet<Box<str>>, state: Option<&Key>) -> Bytes {
let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).expect("constraints were already asserted by IntSize");
if let Some(state) = state {
if active_remote_handler_host_ids.contains(&state.host_id) {
let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style));
let style = style.as_ref().unwrap_or(&state.base_style);
@ -38,8 +40,9 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K
}
if let Some(color) = style.border {
let path =
PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).expect("width and height are not negative"));
let path = PathBuilder::from_rect(
Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).expect("width and height are not negative"),
);
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)),
@ -57,14 +60,16 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K
pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None);
}
}
}
convert_pixels_to_rgb565(pixmap.pixels(), context.buffer_endianness).freeze()
}
pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Option<&Knob>) -> Bytes {
pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, active_remote_handler_host_ids: &HashSet<Box<str>>, state: Option<&Knob>) -> Bytes {
let mut pixmap = Pixmap::new(screen_size.width(), screen_size.height()).expect("constraints were already asserted by IntSize");
if let Some(state) = state {
if active_remote_handler_host_ids.contains(&state.host_id) {
let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style));
let style = style.as_ref().unwrap_or(&state.base_style);
@ -115,6 +120,7 @@ pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Optio
}
}
}
}
convert_pixels_to_rgb565(pixmap.pixels(), context.buffer_endianness).freeze()
}

View file

@ -21,17 +21,25 @@ use crate::icons::IconManager;
use crate::model::coordinator_config::Config;
use crate::model::position::ButtonPosition;
#[derive(Debug, PartialEq, Clone)]
pub enum CoordinatorCommand {
SetActivePages { key_page_id: String, knob_page_id: String },
SetRemoteHostIsActive { host_id: Box<str>, is_active: bool },
}
enum IoWork {
DeviceEvent(LoupedeckEvent),
Command(HandlerCommand),
HandlerCommand(HandlerCommand),
CoordinatorCommand(CoordinatorCommand),
}
pub struct IoWorkerContext {
config: Arc<Config>,
device: LoupedeckDevice,
commands_sender: flume::Sender<HandlerCommand>,
coordinator_commands_sender: flume::Sender<CoordinatorCommand>,
events_sender: broadcast::Sender<HandlerEvent>,
graphics: GraphicsContext,
state: State,
}
impl IoWorkerContext {
@ -39,52 +47,59 @@ impl IoWorkerContext {
config_directory: &Path,
config: Arc<Config>,
device: LoupedeckDevice,
commands_sender: flume::Sender<HandlerCommand>,
coordinator_commands_sender: flume::Sender<CoordinatorCommand>,
events_sender: broadcast::Sender<HandlerEvent>,
) -> 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);
let state = State::create(&config);
IoWorkerContext {
config,
device,
commands_sender,
coordinator_commands_sender,
events_sender,
graphics: GraphicsContext {
buffer_endianness,
label_renderer,
icon_manager: IconManager::new(config_directory.to_path_buf(), icon_packs, dpi),
},
state,
}
}
}
pub fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
let mut state = State::create(&context.config);
pub fn do_io_work(
mut context: IoWorkerContext,
coordinator_commands_receiver: &flume::Receiver<CoordinatorCommand>,
handler_commands_receiver: &flume::Receiver<HandlerCommand>,
) {
let device_events_receiver = context.device.events();
loop {
let a = flume::Selector::new()
let work = flume::Selector::new()
.recv(&device_events_receiver, |e| {
IoWork::DeviceEvent(e.expect("the device events channel is not closed by the other side"))
})
.recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
.recv(coordinator_commands_receiver, |c| IoWork::CoordinatorCommand(c.unwrap()))
.recv(handler_commands_receiver, |c| IoWork::HandlerCommand(c.unwrap()))
.wait();
match a {
match work {
IoWork::DeviceEvent(event) => {
if !handle_event(&context, &mut state, event) {
if !handle_event(&mut context, event) {
break;
}
}
IoWork::Command(command) => handle_command(&context, &mut state, command),
IoWork::CoordinatorCommand(command) => handle_coordinator_command(&mut context, command),
IoWork::HandlerCommand(command) => handle_handler_command(&mut context, command),
}
}
}
fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool {
fn handle_event(context: &mut IoWorkerContext, event: LoupedeckEvent) -> bool {
log::trace!("Handling event: {:?}", &event);
let send_key_event = |path: KeyPath, event: KeyEvent| {
@ -104,10 +119,10 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
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(),
.coordinator_commands_sender
.send(CoordinatorCommand::SetActivePages {
key_page_id: button_config.key_page.as_ref().unwrap_or(&context.state.active_key_page_id).clone(),
knob_page_id: button_config.knob_page.as_ref().unwrap_or(&context.state.active_knob_page_id).clone(),
})
.unwrap()
}
@ -125,7 +140,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
};
let path = KeyPath {
page_id: state.active_key_page_id.clone(),
page_id: context.state.active_key_page_id.clone(),
position,
};
@ -137,10 +152,10 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
.expect("the key index is valid because is was returned by get_key_at_global_coordinates");
let kind = if is_end {
state.active_touch_ids.remove(&touch_id);
context.state.active_touch_ids.remove(&touch_id);
KeyTouchEventKind::End
} else {
let is_new = state.active_touch_ids.insert(touch_id);
let is_new = context.state.active_touch_ids.insert(touch_id);
if is_new {
KeyTouchEventKind::Start
} else {
@ -170,7 +185,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
send_knob_event(
KnobPath {
page_id: state.active_knob_page_id.clone(),
page_id: context.state.active_knob_page_id.clone(),
position,
},
KnobEvent::Rotate {
@ -186,7 +201,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
send_knob_event(
KnobPath {
page_id: state.active_knob_page_id.clone(),
page_id: context.state.active_knob_page_id.clone(),
position,
},
KnobEvent::Press,
@ -198,46 +213,53 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
true
}
fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) {
log::trace!("Handling command: {:?}", &command);
fn handle_coordinator_command(context: &mut IoWorkerContext, command: CoordinatorCommand) {
log::trace!("Handling coordinator 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;
CoordinatorCommand::SetActivePages { key_page_id, knob_page_id } => {
context.state.active_key_page_id = key_page_id;
context.state.active_knob_page_id = knob_page_id;
for button in context.device.characteristics().available_buttons {
context
.device
.set_button_color(button, get_correct_button_color(context, state, ButtonPosition::of(&button)))
.set_button_color(button, get_correct_button_color(context, &context.state, ButtonPosition::of(&button)))
.expect("the button is available for this device because that is literally what we are iterating over");
}
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);
redraw_visible_page(&context);
}
CoordinatorCommand::SetRemoteHostIsActive { host_id, is_active } => {
if is_active {
context.state.active_remote_handler_host_ids.insert(host_id);
} else {
context.state.active_remote_handler_host_ids.remove(&host_id);
}
for position in KnobPosition::VARIANTS {
draw_knob_at_position(context, state, *position);
redraw_visible_page(&context);
}
}
}
context.device.refresh_display(&key_grid.display).unwrap();
}
fn handle_handler_command(context: &mut IoWorkerContext, command: HandlerCommand) {
log::trace!("Handling handler command: {:?}", &command);
match command {
HandlerCommand::SetKeyStyle { path, value } => {
state.mutate_key_for_command("SetKeyStyle", &path, |k| {
context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
k.style = value;
});
draw_key_at_path_if_visible(context, state, path);
draw_key_at_path_if_visible(context, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
}
HandlerCommand::SetKnobStyle { path, value } => {
state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
context.state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
k.style = value;
});
draw_knob_at_path_if_visible(context, state, path);
draw_knob_at_path_if_visible(context, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
}
HandlerCommand::SetKnobValue { path, value } => {
@ -248,15 +270,28 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: Handler
}
}
state.mutate_knob_for_command("SetKnobValue", &path, |k| {
context.state.mutate_knob_for_command("SetKnobValue", &path, |k| {
k.value = value;
});
draw_knob_at_path_if_visible(context, state, path);
draw_knob_at_path_if_visible(context, path);
}
}
}
fn redraw_visible_page(context: &IoWorkerContext) {
let key_grid = &context.device.characteristics().key_grid;
for index in 0..(key_grid.rows * key_grid.columns) {
draw_key_at_index(context, index);
}
for position in KnobPosition::VARIANTS {
draw_knob_at_position(context, *position);
}
context.device.refresh_display(&key_grid.display).unwrap();
}
// active -> config.active_button_color
// no actions defined -> #000000
// inactive -> config.inactive_button_color
@ -306,6 +341,7 @@ fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
let buffer = render_key(
&context.graphics,
IntSize::from_wh(rect.w as u32, rect.h as u32).expect("rect.w and rect.h are not zero"),
&context.state.active_remote_handler_host_ids,
key,
);
context
@ -314,23 +350,23 @@ fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
.unwrap();
}
fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) {
fn draw_key_at_index(context: &IoWorkerContext, 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));
draw_key(context, index, context.state.active_key_page().keys_by_position.get(&position));
}
fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) {
fn draw_key_at_position_if_visible(context: &IoWorkerContext, 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));
draw_key(context, index, context.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_key_at_path_if_visible(context: &IoWorkerContext, path: KeyPath) {
if context.state.active_key_page_id == path.page_id {
draw_key_at_position_if_visible(context, path.position);
}
}
@ -346,6 +382,7 @@ fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Kn
let buffer = render_knob(
&context.graphics,
IntSize::from_wh(rect.w as u32, rect.h as u32).expect("rect.w and rect.h are not zero."),
&context.state.active_remote_handler_host_ids,
knob,
);
context
@ -355,13 +392,13 @@ fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Kn
}
}
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_position(context: &IoWorkerContext, position: KnobPosition) {
draw_knob(context, position, Some(&context.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 draw_knob_at_path_if_visible(context: &IoWorkerContext, path: KnobPath) {
if context.state.active_knob_page_id == path.page_id {
draw_knob_at_position(context, path.position);
}
}

View file

@ -12,12 +12,11 @@ use deckster_shared::path::{KeyPath, KnobPath};
use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice;
use crate::coordinator::io_worker::{do_io_work, IoWorkerContext};
use crate::coordinator::io_worker::{do_io_work, CoordinatorCommand, IoWorkerContext};
use crate::coordinator::mqtt::start_mqtt_client;
use crate::handler_runner;
use crate::handler_runner::KeyOrKnobHandlerConfig;
use crate::model::coordinator_config::Config;
use crate::model::get_default_host_id;
use crate::model::mqtt::HandlerHostsConfig;
mod graphics;
@ -33,11 +32,12 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
let available_device = available_devices.first().wrap_err("No device connected.")?;
log::info!("Found {} device(s).", available_devices.len());
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
let (coordinator_commands_sender, coordinator_commands_receiver) = flume::bounded::<CoordinatorCommand>(5);
let (handler_commands_sender, handler_commands_receiver) = flume::bounded::<HandlerCommand>(5);
let events_sender = broadcast::Sender::<HandlerEvent>::new(5);
commands_sender
.send(HandlerCommand::SetActivePages {
coordinator_commands_sender
.send(CoordinatorCommand::SetActivePages {
knob_page_id: config.initial.knob_page.clone(),
key_page_id: config.initial.key_page.clone(),
})
@ -91,16 +91,16 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
if let Some(mqtt_config) = &config.mqtt {
log::info!("Initializing MQTT client…");
start_mqtt_client(mqtt_config, &handler_hosts_config, commands_sender.clone(), events_sender.subscribe()).await;
start_mqtt_client(mqtt_config, &handler_hosts_config, handler_commands_sender.clone(), events_sender.subscribe()).await;
}
log::info!("Initializing handler processes…");
handler_runner::start(
get_default_host_id(),
String::default().into_boxed_str(),
&config_directory.join("handlers"),
handler_hosts_config,
commands_sender.clone(),
handler_commands_sender.clone(),
events_sender.subscribe(),
)
.await?;
@ -112,12 +112,12 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
device.set_brightness(0.5);
device.vibrate(VibrationPattern::RiseFall);
let io_worker_context = IoWorkerContext::create(config_directory, Arc::clone(&config), device, commands_sender.clone(), events_sender);
let io_worker_context = IoWorkerContext::create(config_directory, Arc::clone(&config), device, coordinator_commands_sender, 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);
do_io_work(io_worker_context, &coordinator_commands_receiver, &handler_commands_receiver);
})
.wrap_err("Could not spawn the worker thread")?;

View file

@ -13,6 +13,7 @@ pub struct State {
pub active_key_page_id: String,
pub active_knob_page_id: String,
pub active_touch_ids: HashSet<u8>,
pub active_remote_handler_host_ids: HashSet<Box<str>>,
pub key_pages_by_id: HashMap<String, KeyPage>,
pub knob_pages_by_id: HashMap<String, KnobPage>,
}
@ -32,6 +33,7 @@ impl State {
page_id: p.id.clone(),
position: *position,
},
host_id: k.host.clone(),
base_style: k.base_style.clone(),
style: None,
})
@ -54,6 +56,7 @@ impl State {
page_id: p.id.clone(),
position,
},
host_id: knob_config.host.clone(),
base_style: knob_config.base_style.clone(),
style: None,
value: None,
@ -67,6 +70,7 @@ impl State {
active_key_page_id: config.initial.key_page.clone(),
active_knob_page_id: config.initial.knob_page.clone(),
active_touch_ids: HashSet::new(),
active_remote_handler_host_ids: HashSet::new(),
key_pages_by_id,
knob_pages_by_id,
}

View file

@ -135,7 +135,6 @@ pub async fn start_mqtt_client(
tokio::spawn(async move {
while let Ok(command) = commands_receiver.recv_async().await {
match command {
HandlerCommand::SetActivePages { .. } => log::warn!("HandlerCommand::SetActivePages is not supported for remote handlers."),
HandlerCommand::SetKeyStyle { path, value } => client
.publish(
format!("{topic_prefix}/keys/{path}/style"),

View file

@ -50,7 +50,7 @@ pub struct Key {
#[serde(default, flatten)]
pub base_style: KeyStyle,
#[serde(default = "super::get_default_host_id")]
#[serde(default)]
pub host: Box<str>,
pub handler: Box<str>,
pub config: Arc<toml::Table>,

View file

@ -24,7 +24,7 @@ pub struct Knob {
#[serde(default, flatten)]
pub base_style: KnobStyle,
#[serde(default = "super::get_default_host_id")]
#[serde(default)]
pub host: Box<str>,
pub handler: Box<str>,
pub config: Arc<toml::Table>,

View file

@ -5,7 +5,3 @@ pub mod key_page;
pub mod knob_page;
pub mod mqtt;
pub mod position;
pub fn get_default_host_id() -> Box<str> {
"local".to_owned().into_boxed_str()
}