Skip to content

Commit

Permalink
Add a gui implementation for led control (#7)
Browse files Browse the repository at this point in the history
* Add a gui implementation for led control (and maybe more)

* Fix slider clamping

* Feature parity with all commands, collapsing

* Minor typo fix
  • Loading branch information
efyang authored Aug 11, 2021
1 parent 47f7a1e commit 121f82b
Show file tree
Hide file tree
Showing 4 changed files with 398 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ serial-core = "0.4"
serial-unix = "0.4"
failure = "0.1"
ron = "0.6"
eframe = "0.13"
anyhow = "1.0"
277 changes: 277 additions & 0 deletions examples/gui/app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
use std::{
collections::VecDeque,
num::NonZeroU16,
sync::mpsc::{channel, Receiver, Sender},
time::Duration,
};

use eframe::{
egui::{self, FontDefinitions, FontFamily, ScrollArea, Vec2},
epi::{self, Storage},
};
use panel_protocol::{Command, PulseMode, Report};

const SHOW_LAST_COMMAND_NUM: usize = 15;

#[derive(Clone, Copy, PartialEq)]
struct LedState {
r: u8,
g: u8,
b: u8,
if_breathing_interval_ms: u16,
pulse_mode: PulseMode,
}

impl Default for LedState {
fn default() -> Self {
Self {
r: 255,
g: 255,
b: 255,
if_breathing_interval_ms: 4000,
pulse_mode: PulseMode::Solid,
}
}
}

impl From<LedState> for Command {
fn from(led_state: LedState) -> Command {
Command::Led {
r: led_state.r,
g: led_state.g,
b: led_state.b,
pulse_mode: led_state.pulse_mode,
}
}
}

#[derive(Default, Clone, Copy, PartialEq)]
struct LightState {
brightness: u16,
temperature: u16,
}

pub struct App {
report_rx: Receiver<Report>,
command_tx: Sender<Command>,
led_state: LedState,
light_state: [LightState; 2],
last_recv_reports: VecDeque<Report>,
kill_updater: Option<Sender<()>>,
}

impl App {
pub fn new(report_rx: Receiver<Report>, command_tx: Sender<Command>) -> Self {
Self {
report_rx,
command_tx,
led_state: Default::default(),
light_state: Default::default(),
last_recv_reports: VecDeque::new(),
kill_updater: None,
}
}

fn led_configuration_section(&mut self, ui: &mut eframe::egui::Ui) {
ui.add(
egui::Slider::new(&mut self.led_state.r, 0..=255).text("LED Red").clamp_to_range(true),
);
ui.add(
egui::Slider::new(&mut self.led_state.g, 0..=255)
.text("LED Green")
.clamp_to_range(true),
);
ui.add(
egui::Slider::new(&mut self.led_state.b, 0..=255).text("LED Blue").clamp_to_range(true),
);

// Pulse mode
egui::ComboBox::from_label("Pulse Mode")
.selected_text(format!("{:?}", self.led_state.pulse_mode))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.led_state.pulse_mode, PulseMode::Solid, "Solid");
ui.selectable_value(
&mut self.led_state.pulse_mode,
PulseMode::DialTurn,
"DialTurn",
);
ui.selectable_value(
&mut self.led_state.pulse_mode,
PulseMode::Breathing {
interval_ms: NonZeroU16::new(self.led_state.if_breathing_interval_ms)
.unwrap(),
},
"Breathing",
);
});

// Duration for pulse mode if breathing
ui.scope(|ui| {
ui.set_visible(matches!(self.led_state.pulse_mode, PulseMode::Breathing { .. }));

let response = ui.add(
egui::Slider::new(&mut self.led_state.if_breathing_interval_ms, 1..=u16::MAX)
.text("Breathing (half) interval (ms)")
.clamp_to_range(true),
);
if response.changed() {
self.led_state.pulse_mode = PulseMode::Breathing {
interval_ms: NonZeroU16::new(self.led_state.if_breathing_interval_ms).unwrap(),
};
}
});
}

fn lighting_configuration_section(&mut self, ui: &mut eframe::egui::Ui) {
ui.label("Front Lights");
ui.group(|ui| {
ui.add(
egui::Slider::new(&mut self.light_state[0].brightness, 0..=u16::MAX)
.text("Brightness")
.clamp_to_range(true),
);
ui.add(
egui::Slider::new(&mut self.light_state[0].temperature, 0..=u16::MAX)
.text("Temperature")
.clamp_to_range(true),
);
});
ui.label("Back Lights");
ui.group(|ui| {
ui.add(
egui::Slider::new(&mut self.light_state[1].brightness, 0..=u16::MAX)
.text("Brightness")
.clamp_to_range(true),
);
ui.add(
egui::Slider::new(&mut self.light_state[1].temperature, 0..=u16::MAX)
.text("Temperature")
.clamp_to_range(true),
);
});
}

fn other_commands_section(&mut self, ui: &mut eframe::egui::Ui) {
if ui.button(format!("Send {:?} command", Command::Bootload)).clicked() {
self.command_tx.send(Command::Bootload).unwrap();
}
}

fn serial_monitor_section(&mut self, ui: &mut eframe::egui::Ui) {
ui.group(|ui| {
let commands_strings = self
.last_recv_reports
.iter()
.map(|report| format!("New serial message received: {:?}", report))
.collect::<Vec<_>>();
ui.add(egui::Label::new(commands_strings.join("\n")).code())
});
}
}

impl epi::App for App {
fn setup(
&mut self,
_ctx: &eframe::egui::CtxRef,
_frame: &mut epi::Frame<'_>,
_: Option<&dyn Storage>,
) {
// Add another thread to force a repaint on new reports being received, forwards those reports
let (report_tx, mut report_rx) = channel();
let (kill_updater_tx, kill_updater_rx) = channel();
std::mem::swap(&mut self.report_rx, &mut report_rx);
self.kill_updater = Some(kill_updater_tx);
let repaint_signal = _frame.repaint_signal().clone();
std::thread::spawn(move || loop {
if kill_updater_rx.try_recv().is_ok() {
println!("Killed updater thread.");
break;
}
while let Ok(report) = report_rx.try_recv() {
report_tx.send(report).unwrap();
repaint_signal.request_repaint();
}
std::thread::sleep(Duration::from_millis(1));
});

// Update the led on startup
self.command_tx.send(self.led_state.into()).unwrap();

// Setup some fonts
let mut fonts = FontDefinitions::default();
fonts.family_and_size.insert(egui::TextStyle::Body, (FontFamily::Proportional, 18.0));
fonts.family_and_size.insert(egui::TextStyle::Button, (FontFamily::Proportional, 18.0));
fonts.family_and_size.insert(egui::TextStyle::Monospace, (FontFamily::Monospace, 18.0));
fonts.family_and_size.insert(egui::TextStyle::Heading, (FontFamily::Proportional, 24.0));
_ctx.set_fonts(fonts);
}

fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) {
let current_led_state = self.led_state.clone();
let current_light_state = self.light_state.clone();
egui::CentralPanel::default().show(ctx, |ui| {
if let Ok(report) = self.report_rx.try_recv() {
self.last_recv_reports.push_back(report);
while self.last_recv_reports.len() > SHOW_LAST_COMMAND_NUM {
self.last_recv_reports.pop_front();
}
}

ScrollArea::auto_sized().show(ui, |ui| {
ui.spacing_mut().slider_width = ui.available_width() - 300.0;
ui.spacing_mut().item_spacing = Vec2::new(10.0, 10.0);
ui.spacing_mut().button_padding = Vec2::new(10.0, 10.0);
ui.vertical_centered_justified(|ui| {
ui.heading("Panel Configurator");
// RGB sliders
ui.separator();
ui.collapsing("RGB LED Configuration", |ui| self.led_configuration_section(ui));

// Lighting
ui.separator();
ui.collapsing("Lighting", |ui| self.lighting_configuration_section(ui));

// Bootloader command
ui.separator();
ui.collapsing("Other Commands", |ui| self.other_commands_section(ui));

// Show last few commands
ui.separator();
ui.collapsing(
format!("Serial Monitor (last {} messages)", SHOW_LAST_COMMAND_NUM),
|ui| self.serial_monitor_section(ui),
);

// Warn if debug build
egui::warn_if_debug_build(ui);
});
});
});

if self.led_state != current_led_state {
self.command_tx.send(self.led_state.into()).unwrap();
}

if self.light_state != current_light_state {
for (target, state) in self.light_state.iter().enumerate() {
let target = target as u8;
self.command_tx
.send(Command::Brightness { target, value: state.brightness })
.unwrap();
self.command_tx
.send(Command::Temperature { target, value: state.temperature })
.unwrap();
}
}
}

fn name(&self) -> &str {
"Panel Configurator"
}

fn on_exit(&mut self) {
if let Some(kill_updater) = &self.kill_updater {
kill_updater.send(()).unwrap();
}
}
}
63 changes: 63 additions & 0 deletions examples/gui/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use anyhow::Result;
use eframe::run_native;
use std::{
env,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
mod app;
mod panel;

fn print_usage(args: &[String]) {
println!("Usage: {} <tty_port>", args[0]);
println!("");
println!("The program initiates a serial connection with the device specified by the ");
println!("tty_port, and prints every Report that comes in");
println!("");
}

fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
print_usage(&args);
return Ok(());
}

let port = &args[1];
let (report_tx, report_rx) = std::sync::mpsc::channel();
let (command_tx, command_rx) = std::sync::mpsc::channel();

let should_exit = Arc::new(AtomicBool::new(false));
thread::spawn({
let mut panel = panel::Panel::new(port)?;
let should_exit = should_exit.clone();
move || loop {
match panel.poll() {
Ok(reports) => {
for report in reports {
println!("New serial message: {:?}", &report);
report_tx.send(report).unwrap();
}
},
Err(e) => {
eprintln!("Failed to poll reports: {}", e);
should_exit.store(true, Ordering::SeqCst);
return;
},
}

while let Ok(command) = command_rx.try_recv() {
panel.send(&command).unwrap();
}
thread::sleep(Duration::from_micros(50));
}
});

let app = app::App::new(report_rx, command_tx);

run_native(Box::new(app), Default::default());
}
Loading

0 comments on commit 121f82b

Please sign in to comment.