first commit

This commit is contained in:
lennlouisgeek
2026-03-30 02:59:56 +08:00
commit eec9927ae6
60 changed files with 15953 additions and 0 deletions

View File

@@ -0,0 +1,500 @@
use crate::serial_core::frame::TestFrame;
use std::collections::HashMap;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
const MAX_POINTS: usize = 28;
const MAX_SUMMARY_POINTS: usize = 42;
const PANEL_STALE_AFTER: Duration = Duration::from_millis(2400);
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HudPacket {
pub ts: u64,
pub panels: Vec<HudSignalPanel>,
pub summary: HudSummary,
pub pressure_matrix: Option<Vec<f32>>,
}
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HudSummary {
pub label: String,
pub points: Vec<f32>,
pub latest: Option<f32>,
pub min: Option<f32>,
pub max: Option<f32>,
}
#[derive(serde::Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum HudPanelSide {
Left,
Right,
}
#[derive(serde::Serialize, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum HudTone {
Cyan,
Lime,
Orange,
Violet,
Gold,
Rose,
}
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HudSignalPanel {
pub id: String,
pub code: String,
pub title: String,
pub side: HudPanelSide,
pub active: bool,
pub series: Vec<HudSignalSeries>,
pub icons: Vec<HudSignalIcon>,
pub latest: Option<f32>,
pub min: Option<f32>,
pub max: Option<f32>,
}
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HudSignalSeries {
pub id: String,
pub tone: HudTone,
pub points: Vec<f32>,
}
#[derive(serde::Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HudSignalIcon {
pub id: String,
pub label: String,
pub tone: HudTone,
}
struct HudPanelUpdate {
source_id: String,
values: Vec<f32>,
}
struct PanelEntry {
panel: HudSignalPanel,
last_seen: Instant,
}
pub struct HudChartState {
panels: HashMap<String, PanelEntry>,
order: Vec<String>,
summary_points: Vec<f32>,
pressure_matrix: Option<Vec<f32>>,
last_frame_seen: Option<Instant>,
}
impl HudChartState {
pub fn new() -> Self {
Self {
panels: HashMap::new(),
order: Vec::new(),
summary_points: Vec::new(),
pressure_matrix: None,
last_frame_seen: None,
}
}
pub fn record_summary(&mut self, value: f32) {
push_summary_point(&mut self.summary_points, value);
}
pub fn record_pressure_matrix(&mut self, values: &[i32]) {
if values.is_empty() {
return;
}
self.pressure_matrix = Some(values.iter().map(|value| *value as f32).collect());
}
pub fn apply_frame(&mut self, frame: &TestFrame, decoded_values: Option<&[i32]>) -> HudPacket {
let now = Instant::now();
self.last_frame_seen = Some(now);
for update in expand_frame_updates(frame, decoded_values) {
self.apply_update(update, now);
}
self.prune_stale_at(now);
self.snapshot()
}
pub fn prune_stale(&mut self) -> Option<HudPacket> {
let before = self.panels.len();
let summary_points_before = self.summary_points.len();
self.prune_stale_at(Instant::now());
if before == self.panels.len() && summary_points_before == self.summary_points.len() {
return None;
}
Some(self.snapshot())
}
fn apply_update(&mut self, update: HudPanelUpdate, now: Instant) {
if update.values.is_empty() {
return;
}
if !self.panels.contains_key(&update.source_id) {
let next_side = side_for_index(self.order.len());
self.order.push(update.source_id.clone());
self.panels.insert(
update.source_id.clone(),
PanelEntry {
panel: build_panel(&update.source_id, next_side, update.values.len()),
last_seen: now,
},
);
}
let entry = self
.panels
.get_mut(&update.source_id)
.expect("panel entry should exist after insertion");
entry.last_seen = now;
entry.panel.active = true;
ensure_panel_channels(&mut entry.panel, update.values.len());
for (index, value) in update.values.into_iter().enumerate() {
if let Some(series) = entry.panel.series.get_mut(index) {
push_point(&mut series.points, value);
}
}
refresh_panel_stats(&mut entry.panel);
}
fn prune_stale_at(&mut self, now: Instant) {
self.panels
.retain(|_, entry| now.duration_since(entry.last_seen) <= PANEL_STALE_AFTER);
self.order.retain(|id| self.panels.contains_key(id));
let summary_stale = self
.last_frame_seen
.map(|last_seen| now.duration_since(last_seen) > PANEL_STALE_AFTER)
.unwrap_or(false);
if summary_stale {
self.summary_points.clear();
self.pressure_matrix = None;
self.last_frame_seen = None;
}
}
fn snapshot(&mut self) -> HudPacket {
self.rebalance_sides();
let panels = self
.order
.iter()
.filter_map(|id| self.panels.get(id).map(|entry| entry.panel.clone()))
.collect();
HudPacket {
ts: now_millis(),
panels,
summary: build_summary(&self.summary_points),
pressure_matrix: self.pressure_matrix.clone(),
}
}
fn rebalance_sides(&mut self) {
for (index, id) in self.order.iter().enumerate() {
if let Some(entry) = self.panels.get_mut(id) {
entry.panel.side = side_for_index(index);
}
}
}
}
impl Default for HudChartState {
fn default() -> Self {
Self::new()
}
}
fn build_panel(source_id: &str, side: HudPanelSide, channel_count: usize) -> HudSignalPanel {
HudSignalPanel {
id: format!("panel-{source_id}"),
code: source_id.to_string(),
title: format!("Source {source_id}"),
side,
active: true,
series: build_panel_series(source_id, channel_count, &[]),
icons: build_panel_icons(source_id, channel_count),
latest: None,
min: None,
max: None,
}
}
fn expand_frame_updates(frame: &TestFrame, decoded_values: Option<&[i32]>) -> Vec<HudPanelUpdate> {
if let Some(values) = decoded_values {
if values.is_empty() {
return Vec::new();
}
return vec![HudPanelUpdate {
source_id: format_source_id(frame.cmd),
values: values.iter().map(|value| *value as f32).collect(),
}];
}
let chunks = frame.payload.chunks_exact(4);
if !frame.payload.is_empty() && chunks.remainder().is_empty() {
return chunks.map(build_update_from_chunk).collect();
}
vec![HudPanelUpdate {
source_id: format_source_id(frame.cmd),
values: fallback_values(frame),
}]
}
fn build_update_from_chunk(chunk: &[u8]) -> HudPanelUpdate {
HudPanelUpdate {
source_id: format_source_id(chunk[0]),
values: chunk[1..]
.iter()
.enumerate()
.map(|(index, byte)| normalize_value(*byte, tone_for_index(index)))
.collect(),
}
}
fn fallback_values(frame: &TestFrame) -> Vec<f32> {
let mut bytes = frame.payload.clone();
if bytes.is_empty() {
bytes.extend([
frame.cmd,
frame.length as u8,
frame.checksum,
frame.cmd.wrapping_add(frame.checksum),
]);
}
while bytes.len() < 3 {
let previous = *bytes.last().unwrap_or(&frame.cmd);
bytes.push(
previous
.wrapping_add(frame.cmd)
.wrapping_add(bytes.len() as u8),
);
}
bytes
.into_iter()
.enumerate()
.map(|(index, byte)| normalize_value(byte, tone_for_index(index)))
.collect()
}
fn normalize_value(byte: u8, tone: HudTone) -> f32 {
let base = (byte as f32 / 255.0) * 100.0;
let offset = match tone {
HudTone::Cyan => 6.0,
HudTone::Lime => 0.0,
HudTone::Orange => -6.0,
HudTone::Violet => 10.0,
HudTone::Gold => -10.0,
HudTone::Rose => 3.0,
};
(base + offset).clamp(0.0, 100.0)
}
fn format_source_id(byte: u8) -> String {
if byte.is_ascii_alphanumeric() {
(byte as char).to_ascii_uppercase().to_string()
} else {
format!("CH{:02X}", byte)
}
}
fn side_for_index(index: usize) -> HudPanelSide {
if index % 2 == 0 {
HudPanelSide::Left
} else {
HudPanelSide::Right
}
}
fn push_point(points: &mut Vec<f32>, value: f32) {
if points.len() >= MAX_POINTS {
points.remove(0);
}
points.push((value * 10.0).round() / 10.0);
}
fn build_panel_series(
source_id: &str,
channel_count: usize,
previous: &[HudSignalSeries],
) -> Vec<HudSignalSeries> {
(0..channel_count)
.map(|index| HudSignalSeries {
id: format!("{source_id}-series-{}", index + 1),
tone: tone_for_index(index),
points: previous
.get(index)
.map(|series| series.points.clone())
.unwrap_or_default(),
})
.collect()
}
fn build_panel_icons(source_id: &str, channel_count: usize) -> Vec<HudSignalIcon> {
(0..channel_count)
.map(|index| HudSignalIcon {
id: format!("{source_id}-icon-{}", index + 1),
label: if channel_count == 1 {
"TOTAL".to_string()
} else {
format!("{source_id}-{}", index + 1)
},
tone: tone_for_index(index),
})
.collect()
}
fn ensure_panel_channels(panel: &mut HudSignalPanel, channel_count: usize) {
if panel.series.len() == channel_count && panel.icons.len() == channel_count {
return;
}
panel.series = build_panel_series(&panel.code, channel_count, &panel.series);
panel.icons = build_panel_icons(&panel.code, channel_count);
}
fn refresh_panel_stats(panel: &mut HudSignalPanel) {
let latest_values: Vec<f32> = panel
.series
.iter()
.filter_map(|series| series.points.last().copied())
.collect();
panel.latest = if latest_values.is_empty() {
None
} else {
Some(latest_values.iter().sum::<f32>() / latest_values.len() as f32)
};
panel.min = panel
.series
.iter()
.flat_map(|series| series.points.iter().copied())
.reduce(f32::min);
panel.max = panel
.series
.iter()
.flat_map(|series| series.points.iter().copied())
.reduce(f32::max);
}
fn tone_for_index(index: usize) -> HudTone {
match index % 6 {
0 => HudTone::Cyan,
1 => HudTone::Lime,
2 => HudTone::Orange,
3 => HudTone::Violet,
4 => HudTone::Gold,
_ => HudTone::Rose,
}
}
fn push_summary_point(points: &mut Vec<f32>, value: f32) {
if points.len() >= MAX_SUMMARY_POINTS {
points.remove(0);
}
points.push((value * 10.0).round() / 10.0);
}
fn build_summary(points: &[f32]) -> HudSummary {
HudSummary {
label: "TOTAL".to_string(),
points: points.to_vec(),
latest: points.last().copied(),
min: points.iter().copied().reduce(f32::min),
max: points.iter().copied().reduce(f32::max),
}
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as u64)
.unwrap_or_default()
}
// #[cfg(test)]
// mod tests {
// use super::*;
//
// fn sample_frame() -> TestFrame {
// TestFrame {
// header: [0xAA, 0x55],
// cmd: 0x01,
// length: 4,
// payload: vec![0x00, 0x0A, 0x00, 0x14],
// checksum: 0,
//
// }
// }
//
// #[test]
// fn prune_stale_clears_panels_and_summary_after_timeout() {
// let mut state = HudChartState::new();
// let frame = sample_frame();
//
// state.record_summary(30.0);
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
//
// let stale_now = Instant::now();
// let stale_seen = stale_now - PANEL_STALE_AFTER - Duration::from_millis(1);
//
// state.last_frame_seen = Some(stale_seen);
//
// for entry in state.panels.values_mut() {
// entry.last_seen = stale_seen;
// }
//
// let packet = state
// .prune_stale()
// .expect("stale data should emit an update");
//
// assert!(packet.panels.is_empty());
// assert!(packet.summary.points.is_empty());
// assert!(state.panels.is_empty());
// assert!(state.summary_points.is_empty());
// }
//
// #[test]
// fn prune_stale_keeps_recent_summary_points() {
// let mut state = HudChartState::new();
// let frame = sample_frame();
//
// state.record_summary(30.0);
// let _ = state.apply_frame(&frame, Some(&[10, 20]));
//
// state.last_frame_seen = Some(Instant::now());
//
// assert!(state.prune_stale().is_none());
// assert_eq!(state.summary_points, vec![30.0]);
// assert_eq!(state.panels.len(), 1);
// }
// }