trans to tokio
This commit is contained in:
190
src/app.rs
190
src/app.rs
@@ -1,24 +1,86 @@
|
||||
use std::{sync::{Arc, Mutex}, thread::JoinHandle};
|
||||
|
||||
use log::error;
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_serial::SerialPortBuilderExt;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::serial_core::{TactileARecording, error::SerialError};
|
||||
use crate::serial_core::{
|
||||
TactileARecording,
|
||||
codecs::tactile_a::{TactileACodec, TactileAHandler},
|
||||
error::SerialError,
|
||||
frame::TactileAFrame,
|
||||
serial::{self, PollMode, SerialFrame, TactileAPollRequester},
|
||||
};
|
||||
|
||||
struct SerialSession {
|
||||
pub struct SerialSession {
|
||||
port: String,
|
||||
current_record: Arc<Mutex<TactileARecording>>,
|
||||
cancel: CancellationToken,
|
||||
task: JoinHandle<()>,
|
||||
current_record: Arc<Mutex<TactileARecording>>
|
||||
task: Mutex<Option<JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl SerialSession {
|
||||
fn new(
|
||||
port: String,
|
||||
current_record: Arc<Mutex<TactileARecording>>,
|
||||
cancel: CancellationToken,
|
||||
) -> Self {
|
||||
Self {
|
||||
port,
|
||||
current_record,
|
||||
cancel,
|
||||
task: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SerialConnectionState {
|
||||
session: Mutex<Option<SerialSession>>,
|
||||
last_record: Mutex<Option<Arc<Mutex<TactileARecording>>>>
|
||||
session: Mutex<Option<Arc<SerialSession>>>,
|
||||
last_record: Mutex<Option<Arc<Mutex<TactileARecording>>>>,
|
||||
}
|
||||
|
||||
impl SerialConnectionState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
session: Mutex::new(None),
|
||||
last_record: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_port(&self) -> Result<Option<String>, SerialError> {
|
||||
let session = self.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
Ok(session.as_ref().map(|session| session.port.clone()))
|
||||
}
|
||||
|
||||
pub fn current_record(&self) -> Result<Option<Arc<Mutex<TactileARecording>>>, SerialError> {
|
||||
let session = self.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
Ok(session
|
||||
.as_ref()
|
||||
.map(|session| Arc::clone(&session.current_record)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SerialConnectionState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SerialFrame for TactileAFrame {
|
||||
fn dts_ms(&self) -> u64 {
|
||||
match self {
|
||||
TactileAFrame::Req(_) => 0,
|
||||
TactileAFrame::Rep(frame) => frame.dts_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serial_connect(
|
||||
port: String,
|
||||
state: Arc<SerialConnectionState>
|
||||
state: Arc<SerialConnectionState>,
|
||||
) -> Result<(), SerialError> {
|
||||
let port_name = port.trim().to_string();
|
||||
if port_name.is_empty() {
|
||||
@@ -32,4 +94,112 @@ pub async fn serial_connect(
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
let serial_port = tokio_serial::new(&port_name, 921500)
|
||||
.open_native_async()
|
||||
.map_err(|_| SerialError::OpenError)?;
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let current_record = Arc::new(Mutex::new(TactileARecording::new()));
|
||||
let session_started_at = Instant::now();
|
||||
let session = Arc::new(SerialSession::new(
|
||||
port_name,
|
||||
Arc::clone(¤t_record),
|
||||
cancel.clone(),
|
||||
));
|
||||
|
||||
{
|
||||
let mut active_session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
*active_session = Some(Arc::clone(&session));
|
||||
}
|
||||
{
|
||||
let mut last_record = state
|
||||
.last_record
|
||||
.lock()
|
||||
.map_err(|_| SerialError::StateError)?;
|
||||
*last_record = None;
|
||||
}
|
||||
|
||||
let task_state = Arc::clone(&state);
|
||||
let task_session = Arc::clone(&session);
|
||||
let task_record = Arc::clone(¤t_record);
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let codec = TactileACodec::new(7, 12);
|
||||
let handler = TactileAHandler;
|
||||
let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
|
||||
Duration::from_millis(10),
|
||||
7,
|
||||
12,
|
||||
Duration::from_millis(450),
|
||||
)));
|
||||
|
||||
if let Err(err) = serial::run_serial_with_poll(
|
||||
serial_port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
Arc::clone(&task_record),
|
||||
cancel,
|
||||
poll_mode,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("serial task exited with error: {err}");
|
||||
}
|
||||
|
||||
if let Ok(mut last_record) = task_state.last_record.lock() {
|
||||
*last_record = Some(Arc::clone(&task_record));
|
||||
}
|
||||
if let Ok(mut active_session) = task_state.session.lock() {
|
||||
let should_clear = active_session
|
||||
.as_ref()
|
||||
.map(|session| Arc::ptr_eq(session, &task_session))
|
||||
.unwrap_or(false);
|
||||
if should_clear {
|
||||
*active_session = None;
|
||||
}
|
||||
}
|
||||
if let Ok(mut task_slot) = task_session.task.lock() {
|
||||
*task_slot = None;
|
||||
}
|
||||
});
|
||||
|
||||
let mut task_slot = session.task.lock().map_err(|_| SerialError::StateError)?;
|
||||
*task_slot = Some(task);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn serial_disconnect(state: Arc<SerialConnectionState>) -> Result<(), SerialError> {
|
||||
shutdown_active_session(&state).await
|
||||
}
|
||||
|
||||
pub async fn shutdown_active_session(state: &SerialConnectionState) -> Result<(), SerialError> {
|
||||
let session = {
|
||||
let mut active_session = state.session.lock().map_err(|_| SerialError::StateError)?;
|
||||
active_session.take()
|
||||
};
|
||||
|
||||
let Some(session) = session else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
session.cancel.cancel();
|
||||
|
||||
let task = {
|
||||
let mut task_slot = session.task.lock().map_err(|_| SerialError::StateError)?;
|
||||
task_slot.take()
|
||||
};
|
||||
|
||||
if let Some(task) = task {
|
||||
task.await.map_err(|_| SerialError::CloseError)?;
|
||||
}
|
||||
|
||||
let mut last_record = state
|
||||
.last_record
|
||||
.lock()
|
||||
.map_err(|_| SerialError::StateError)?;
|
||||
*last_record = Some(Arc::clone(&session.current_record));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
115
src/cmd.rs
Normal file
115
src/cmd.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::io::{self, Stdout, stdout};
|
||||
|
||||
use anyhow::Ok;
|
||||
use crossterm::{
|
||||
event::KeyCode,
|
||||
execute,
|
||||
terminal::{
|
||||
self, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::{self, CrosstermBackend},
|
||||
layout::{Constraint, Direction, Layout, Margin},
|
||||
style::{Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph, Wrap},
|
||||
};
|
||||
use ratatui_textarea::TextArea;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
messages: Vec<String>,
|
||||
should_quit: bool,
|
||||
input: TextArea<'static>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut input = TextArea::default();
|
||||
input.set_block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
input.set_cursor_line_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
Self {
|
||||
messages: vec!["Welcome to JE-Skin-Cli".to_string()],
|
||||
should_quit: false,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_key(&mut self, code: KeyCode) {
|
||||
match code {
|
||||
KeyCode::Esc => self.should_quit = true,
|
||||
KeyCode::Enter => {
|
||||
let text = self.input.lines().join("\n");
|
||||
if !text.trim().is_empty() {
|
||||
self.messages.push(format!("You send: {}", text.trim()));
|
||||
}
|
||||
self.input = {
|
||||
let mut ta = TextArea::default();
|
||||
ta.set_block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
ta.set_cursor_line_style(Style::default().add_modifier(Modifier::REVERSED));
|
||||
ta
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
self.input.input(match code {
|
||||
KeyCode::Char(c) => c.into(),
|
||||
KeyCode::Backspace => ratatui_textarea::Input {
|
||||
key: ratatui_textarea::Key::Backspace,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
},
|
||||
KeyCode::Left => ratatui_textarea::Input {
|
||||
key: ratatui_textarea::Key::Left,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
},
|
||||
KeyCode::Right => ratatui_textarea::Input {
|
||||
key: ratatui_textarea::Key::Right,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
},
|
||||
KeyCode::Up => ratatui_textarea::Input {
|
||||
key: ratatui_textarea::Key::Up,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
},
|
||||
KeyCode::Down => ratatui_textarea::Input {
|
||||
key: ratatui_textarea::Key::Down,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
},
|
||||
_ => return,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
pub fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
let mut app = App::new();
|
||||
while !app.should_quit {
|
||||
terminal.draw(|f| {})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod serial_core;
|
||||
pub mod flog;
|
||||
pub mod app;
|
||||
pub mod cmd;
|
||||
pub mod flog;
|
||||
pub mod serial_core;
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use tokio_serial::available_ports;
|
||||
|
||||
use crate::serial_core::{
|
||||
error::SerialError, frame::{TactileAFrame, TestFrame}, record::Recording
|
||||
error::SerialError,
|
||||
frame::{TactileAFrame, TestFrame},
|
||||
record::Recording,
|
||||
};
|
||||
|
||||
pub mod codec;
|
||||
pub mod codecs;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod model;
|
||||
pub mod serial;
|
||||
pub mod record;
|
||||
pub mod serial;
|
||||
pub mod utils;
|
||||
|
||||
pub type TestRecording = Recording<TestFrame>;
|
||||
@@ -40,4 +41,5 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
||||
.collect();
|
||||
|
||||
Ok(ports)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,500 +0,0 @@
|
||||
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);
|
||||
// }
|
||||
// }
|
||||
@@ -1,34 +1,26 @@
|
||||
use crate::serial_core::codec::Codec;
|
||||
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
||||
use crate::serial_core::model::{HudChartState, HudPacket};
|
||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame};
|
||||
use crate::serial_core::record::Recording;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
use anyhow::Result;
|
||||
use std::future::pending;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||
use tokio_serial::SerialStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use std::future::pending;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||
|
||||
pub enum PollMode<F> {
|
||||
Disable,
|
||||
Enabled(Box<dyn PollRequester<F>>)
|
||||
Enabled(Box<dyn PollRequester<F>>),
|
||||
}
|
||||
|
||||
pub trait SerialFrame: Clone + Send + 'static {
|
||||
fn dts_ms(&self) -> u64;
|
||||
|
||||
fn to_hud_packet(
|
||||
&self,
|
||||
chart_state: &mut HudChartState,
|
||||
display_values: Option<&[i32]>,
|
||||
) -> Option<HudPacket>;
|
||||
}
|
||||
|
||||
|
||||
pub trait PollRequester<F>: Send {
|
||||
fn poll_interval(&self) -> Option<Duration> {
|
||||
None
|
||||
@@ -122,11 +114,18 @@ where
|
||||
F: SerialFrame,
|
||||
C: Codec<F> + Send + 'static,
|
||||
H: FrameHandler<F, T> + Send + 'static,
|
||||
T: Into<i32>
|
||||
T: Into<i32>,
|
||||
{
|
||||
run_serial_with_poll(
|
||||
port, codec, handler, session_started_at, recording, cancel, PollMode::Disable
|
||||
).await
|
||||
port,
|
||||
codec,
|
||||
handler,
|
||||
session_started_at,
|
||||
recording,
|
||||
cancel,
|
||||
PollMode::Disable,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_serial_with_poll<C, H, T, F>(
|
||||
@@ -136,7 +135,7 @@ pub async fn run_serial_with_poll<C, H, T, F>(
|
||||
session_started_at: Instant,
|
||||
recording: Arc<Mutex<Recording<F>>>,
|
||||
cancel: CancellationToken,
|
||||
poll_mode: PollMode<F>
|
||||
poll_mode: PollMode<F>,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: SerialFrame,
|
||||
@@ -149,21 +148,16 @@ where
|
||||
PollMode::Enabled(r) => Some(r),
|
||||
};
|
||||
|
||||
let mut poll_interval = requester
|
||||
.as_ref()
|
||||
.and_then(|r| r.poll_interval())
|
||||
.map(|d| {
|
||||
let mut poll_interval = requester.as_ref().and_then(|r| r.poll_interval()).map(|d| {
|
||||
let mut it = time::interval(d);
|
||||
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
it
|
||||
});
|
||||
|
||||
let mut chart_state = HudChartState::new();
|
||||
let mut buffer = [0u8; 1024];
|
||||
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => break,
|
||||
|
||||
Reference in New Issue
Block a user