uz801: Add initial (experimental) support

This commit is contained in:
Andrej
2025-08-02 20:58:19 -04:00
parent fe6afac817
commit c697773244
7 changed files with 388 additions and 30 deletions
+1
View File
@@ -6,6 +6,7 @@ pub mod tmobile;
pub mod tplink;
pub mod tplink_framebuffer;
pub mod tplink_onebit;
pub mod uz801;
pub mod wingtech;
#[derive(Clone, Copy, PartialEq)]
+81
View File
@@ -0,0 +1,81 @@
/// Display module for Uz801, light LEDs on the front of the device.
/// DisplayState::Recording => Signal LED is solid blue (wifi LED).
/// DisplayState::Paused => Green LED is solid.
/// DisplayState::WarningDetected => Signal LED is solid red.
use log::{error, info};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::task::TaskTracker;
use std::time::Duration;
use crate::config;
use crate::display::DisplayState;
macro_rules! led {
($l:expr) => {{ format!("/sys/class/leds/{}/brightness", $l) }};
}
async fn led_on(path: String) {
tokio::fs::write(&path, "1").await.ok();
}
async fn led_off(path: String) {
tokio::fs::write(&path, "0").await.ok();
}
pub fn update_ui(
task_tracker: &TaskTracker,
config: &config::Config,
mut ui_shutdown_rx: oneshot::Receiver<()>,
mut ui_update_rx: mpsc::Receiver<DisplayState>,
) {
let mut invisible: bool = false;
if config.ui_level == 0 {
info!("Invisible mode, not spawning UI.");
invisible = true;
}
task_tracker.spawn(async move {
let mut state = DisplayState::Recording;
let mut last_state = DisplayState::Paused;
loop {
match ui_shutdown_rx.try_recv() {
Ok(_) => {
info!("received UI shutdown");
break;
}
Err(oneshot::error::TryRecvError::Empty) => {}
Err(e) => panic!("error receiving shutdown message: {e}"),
}
match ui_update_rx.try_recv() {
Ok(new_state) => state = new_state,
Err(mpsc::error::TryRecvError::Empty) => {}
Err(e) => error!("error receiving ui update message: {e}"),
};
if invisible || state == last_state {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
match state {
DisplayState::Paused => {
led_off(led!("red")).await;
led_off(led!("green")).await;
led_on(led!("wifi")).await;
}
DisplayState::Recording => {
led_off(led!("red")).await;
led_off(led!("wifi")).await;
led_on(led!("green")).await;
}
DisplayState::WarningDetected => {
led_off(led!("green")).await;
led_off(led!("wifi")).await;
led_on(led!("red")).await;
}
}
last_state = state;
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
}
+1
View File
@@ -243,6 +243,7 @@ async fn run_with_config(
Device::Tmobile => display::tmobile::update_ui,
Device::Wingtech => display::wingtech::update_ui,
Device::Pinephone => display::headless::update_ui,
Device::Uz801 => display::uz801::update_ui
};
update_ui(&task_tracker, &config, ui_shutdown_rx, ui_update_rx);
+89 -30
View File
@@ -7,7 +7,7 @@ use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use log::error;
use rayhunter::util::RuntimeMetadata;
use rayhunter::{Device, util::RuntimeMetadata};
use serde::Serialize;
use tokio::process::Command;
@@ -19,10 +19,10 @@ pub struct SystemStats {
}
impl SystemStats {
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
Ok(Self {
disk_stats: DiskStats::new(qmdl_path).await?,
memory_stats: MemoryStats::new().await?,
disk_stats: DiskStats::new(qmdl_path, device).await?,
memory_stats: MemoryStats::new(device).await?,
runtime_metadata: RuntimeMetadata::new(),
})
}
@@ -40,21 +40,46 @@ pub struct DiskStats {
impl DiskStats {
// runs "df -h <qmdl_path>" to get storage statistics for the partition containing
// the QMDL file
pub async fn new(qmdl_path: &str) -> Result<Self, String> {
// the QMDL file. The Uz801 device doesn't support the -h flag, so we skip it for that device.
pub async fn new(qmdl_path: &str, device: &Device) -> Result<Self, String> {
let mut df_cmd = Command::new("df");
df_cmd.arg("-h");
// Only add -h flag for devices other than Uz801, as Uz801's df doesn't support it
if !matches!(device, Device::Uz801) {
df_cmd.arg("-h");
}
df_cmd.arg(qmdl_path);
let stdout = get_cmd_output(df_cmd).await?;
let mut parts = stdout.split_whitespace().skip(7).to_owned();
Ok(Self {
partition: parts.next().ok_or("error parsing df output")?.to_string(),
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
})
if matches!(device, Device::Uz801) {
// Handle Uz801 format:
// Filesystem Size Used Free Blksize
// /data/rayhunter/ 774.9M 68.0M 706.9M 4096
let lines: Vec<&str> = stdout.lines().collect();
if lines.len() < 2 {
return Err("error parsing df output: insufficient lines".to_string());
}
let data_line = lines[1];
let mut parts = data_line.split_whitespace();
Ok(Self {
partition: parts.next().ok_or("error parsing df output: missing filesystem")?.to_string(),
total_size: parts.next().ok_or("error parsing df output: missing size")?.to_string(),
used_size: parts.next().ok_or("error parsing df output: missing used")?.to_string(),
available_size: parts.next().ok_or("error parsing df output: missing free")?.to_string(),
used_percent: "N/A".to_string(), // Uz801 df doesn't provide percentage
mounted_on: qmdl_path.to_string(), // Use the path we queried
})
} else {
// Handle standard df -h format
let mut parts = stdout.split_whitespace().skip(7);
Ok(Self {
partition: parts.next().ok_or("error parsing df output")?.to_string(),
total_size: parts.next().ok_or("error parsing df output")?.to_string(),
used_size: parts.next().ok_or("error parsing df output")?.to_string(),
available_size: parts.next().ok_or("error parsing df output")?.to_string(),
used_percent: parts.next().ok_or("error parsing df output")?.to_string(),
mounted_on: parts.next().ok_or("error parsing df output")?.to_string(),
})
}
}
}
@@ -83,19 +108,53 @@ async fn get_cmd_output(mut cmd: Command) -> Result<String, String> {
}
impl MemoryStats {
// runs "free -k" and parses the output to retrieve memory stats
pub async fn new() -> Result<Self, String> {
let mut free_cmd = Command::new("free");
free_cmd.arg("-k");
let stdout = get_cmd_output(free_cmd).await?;
let mut numbers = stdout
.split_whitespace()
.flat_map(|part| part.parse::<usize>());
Ok(Self {
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
used: humanize_kb(numbers.next().ok_or("error parsing free output")?),
free: humanize_kb(numbers.next().ok_or("error parsing free output")?),
})
// runs "free -k" and parses the output to retrieve memory stats for most devices,
// or reads /proc/meminfo for Uz801 which doesn't have the free command
pub async fn new(device: &Device) -> Result<Self, String> {
if matches!(device, Device::Uz801) {
// Read /proc/meminfo for Uz801
let meminfo_content = tokio::fs::read_to_string("/proc/meminfo")
.await
.map_err(|e| format!("error reading /proc/meminfo: {}", e))?;
let mut mem_total_kb = None;
let mut mem_free_kb = None;
for line in meminfo_content.lines() {
if let Some(value_str) = line.strip_prefix("MemTotal:") {
if let Some(kb_str) = value_str.trim().strip_suffix(" kB") {
mem_total_kb = kb_str.trim().parse::<usize>().ok();
}
} else if let Some(value_str) = line.strip_prefix("MemFree:") {
if let Some(kb_str) = value_str.trim().strip_suffix(" kB") {
mem_free_kb = kb_str.trim().parse::<usize>().ok();
}
}
}
let total = mem_total_kb.ok_or("error parsing MemTotal from /proc/meminfo")?;
let free = mem_free_kb.ok_or("error parsing MemFree from /proc/meminfo")?;
let used = total - free;
Ok(Self {
total: humanize_kb(total),
used: humanize_kb(used),
free: humanize_kb(free),
})
} else {
// Use free command for other devices
let mut free_cmd = Command::new("free");
free_cmd.arg("-k");
let stdout = get_cmd_output(free_cmd).await?;
let mut numbers = stdout
.split_whitespace()
.flat_map(|part| part.parse::<usize>());
Ok(Self {
total: humanize_kb(numbers.next().ok_or("error parsing free output")?),
used: humanize_kb(numbers.next().ok_or("error parsing free output")?),
free: humanize_kb(numbers.next().ok_or("error parsing free output")?),
})
}
}
}
@@ -111,7 +170,7 @@ pub async fn get_system_stats(
State(state): State<Arc<ServerState>>,
) -> Result<Json<SystemStats>, (StatusCode, String)> {
let qmdl_store = state.qmdl_store_lock.read().await;
match SystemStats::new(qmdl_store.path.to_str().unwrap()).await {
match SystemStats::new(qmdl_store.path.to_str().unwrap(), &state.config.device).await {
Ok(stats) => Ok(Json(stats)),
Err(err) => {
error!("error getting system stats: {err}");