Expose severity to display

See https://github.com/EFForg/rayhunter/issues/334

Severity levels low, medium, high are now exposed to the UI in form of
dotted, dashed and solid lines. The line on the UI represents the
highest-so-far severity seen.

Originally this was intended to be represented by Yellow/Orange/Red, but
this would mean yet another divergence for colorblind mode. This is
colorblind-friendly by default (I think...)

As part of this, simplify EventType so that it becomes a flat "level"
enum without nested variants.

There is also a new debug endpoint that allows one to overwrite the
display level directly for testing.
This commit is contained in:
Markus Unterwaditzer
2025-08-03 21:01:24 +02:00
committed by Cooper Quintin
parent 6927da49b4
commit 781d11ed72
24 changed files with 443 additions and 292 deletions

View File

@@ -5,6 +5,7 @@ use std::time::Duration;
use crate::config;
use crate::display::DisplayState;
use rayhunter::analysis::analyzer::EventType;
use log::{error, info};
use tokio::sync::mpsc::Receiver;
@@ -20,6 +21,13 @@ pub struct Dimensions {
pub width: u32,
}
#[derive(Copy, Clone)]
pub enum LinePattern {
Solid,
Dashed, // _ _ _ _
Dotted, // . . . .
}
#[allow(dead_code)]
#[derive(Copy, Clone)]
pub enum Color {
@@ -48,18 +56,36 @@ impl Color {
}
}
impl Color {
fn from_state(state: DisplayState, colorblind_mode: bool) -> Self {
match state {
DisplayState::Paused => Color::White,
DisplayState::Recording => {
if colorblind_mode {
Color::Blue
} else {
Color::Green
}
fn display_style_from_state(state: DisplayState, colorblind_mode: bool) -> (Color, LinePattern) {
match state {
DisplayState::Paused => (Color::White, LinePattern::Solid),
DisplayState::Recording => {
if colorblind_mode {
(Color::Blue, LinePattern::Solid)
} else {
(Color::Green, LinePattern::Solid)
}
DisplayState::WarningDetected => Color::Red,
}
DisplayState::WarningDetected { event_type } => {
let pattern = match event_type {
EventType::Informational => LinePattern::Solid,
EventType::Low => LinePattern::Dotted,
EventType::Medium => LinePattern::Dashed,
EventType::High => LinePattern::Solid,
};
let color = match event_type {
EventType::Informational => {
if colorblind_mode {
Color::Blue
} else {
Color::Green
}
}
_ => Color::Red,
};
(color, pattern)
}
}
}
@@ -120,11 +146,28 @@ pub trait GenericFramebuffer: Send + 'static {
}
async fn draw_line(&mut self, color: Color, height: u32) {
self.draw_patterned_line(color, height, LinePattern::Solid)
.await
}
async fn draw_patterned_line(&mut self, color: Color, height: u32, pattern: LinePattern) {
let width = self.dimensions().width;
let px_num = height * width;
let mut buffer = Vec::new();
for _ in 0..px_num {
buffer.push(color.rgb());
for _row in 0..height {
for col in 0..width {
let should_draw = match pattern {
LinePattern::Solid => true,
LinePattern::Dashed => (col / 4) % 2 == 0, // 4 pixels on, 4 pixels off
LinePattern::Dotted => col % 4 == 0, // 1 pixel on, 3 pixels off
};
if should_draw {
buffer.push(color.rgb());
} else {
buffer.push((0, 0, 0)); // Black background
}
}
}
self.write_buffer(buffer).await
@@ -145,7 +188,7 @@ pub fn update_ui(
}
let colorblind_mode = config.colorblind_mode;
let mut display_color = Color::from_state(DisplayState::Recording, colorblind_mode);
let mut display_style = display_style_from_state(DisplayState::Recording, colorblind_mode);
task_tracker.spawn(async move {
// this feels wrong, is there a more rusty way to do this?
@@ -176,7 +219,7 @@ pub fn update_ui(
}
match ui_update_rx.try_recv() {
Ok(state) => {
display_color = Color::from_state(state, colorblind_mode);
display_style = display_style_from_state(state, colorblind_mode);
}
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(e) => error!("error receiving framebuffer update message: {e}"),
@@ -196,7 +239,8 @@ pub fn update_ui(
// unknown value is used
_ => {}
};
fb.draw_line(display_color, 2).await;
let (color, pattern) = display_style;
fb.draw_patterned_line(color, 2, pattern).await;
tokio::time::sleep(Duration::from_millis(1000)).await;
}
});

View File

@@ -1,3 +1,6 @@
use rayhunter::analysis::analyzer::EventType;
use serde::{Deserialize, Serialize};
mod generic_framebuffer;
pub mod headless;
@@ -9,9 +12,15 @@ pub mod tplink_onebit;
pub mod uz801;
pub mod wingtech;
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum DisplayState {
/// We're recording but no warning has been found yet.
Recording,
/// We're not recording.
Paused,
WarningDetected,
/// A non-informational event has been detected.
///
/// Note that EventType::Informational is never sent through this. If it is, it's the same as
/// Recording
WarningDetected { event_type: EventType },
}

View File

@@ -1,7 +1,7 @@
/// Display module for Tmobile TMOHS1, blink LEDs on the front of the device.
/// DisplayState::Recording => Signal LED slowly blinks blue.
/// DisplayState::Paused => WiFi LED blinks white.
/// DisplayState::WarningDetected => Signal LED slowly blinks red.
/// DisplayState::WarningDetected { .. } => Signal LED slowly blinks red.
use log::{error, info};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
@@ -68,7 +68,7 @@ pub fn update_ui(
stop_blinking(led!("signal_red")).await;
start_blinking(led!("signal_blue")).await;
}
DisplayState::WarningDetected => {
DisplayState::WarningDetected { .. } => {
stop_blinking(led!("wlan_white")).await;
stop_blinking(led!("signal_blue")).await;
start_blinking(led!("signal_red")).await;

View File

@@ -136,7 +136,7 @@ pub fn update_ui(
match ui_update_rx.try_recv() {
Ok(DisplayState::Paused) => pixels = STATUS_PAUSED,
Ok(DisplayState::Recording) => pixels = STATUS_SMILING,
Ok(DisplayState::WarningDetected) => pixels = STATUS_WARNING,
Ok(DisplayState::WarningDetected { .. }) => pixels = STATUS_WARNING,
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
Err(e) => {
error!("error receiving framebuffer update message: {e}");

View File

@@ -73,7 +73,7 @@ pub fn update_ui(
led_off(led!("wifi")).await;
led_on(led!("green")).await;
}
DisplayState::WarningDetected => {
DisplayState::WarningDetected { .. } => {
led_off(led!("green")).await;
led_off(led!("wifi")).await;
led_on(led!("red")).await;