trans to tokio
This commit is contained in:
1356
Cargo.lock
generated
1356
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,3 +18,6 @@ crc = "3.4.0"
|
|||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
serde = { version = "1.0.228", features=["derive"]}
|
serde = { version = "1.0.228", features=["derive"]}
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
crossterm = "0.29.0"
|
||||||
|
ratatui = "0.30.0"
|
||||||
|
ratatui-textarea = "0.9.1"
|
||||||
|
|||||||
188
src/app.rs
188
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 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,
|
port: String,
|
||||||
|
current_record: Arc<Mutex<TactileARecording>>,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
task: JoinHandle<()>,
|
task: Mutex<Option<JoinHandle<()>>>,
|
||||||
current_record: Arc<Mutex<TactileARecording>>
|
}
|
||||||
|
|
||||||
|
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 {
|
pub struct SerialConnectionState {
|
||||||
session: Mutex<Option<SerialSession>>,
|
session: Mutex<Option<Arc<SerialSession>>>,
|
||||||
last_record: Mutex<Option<Arc<Mutex<TactileARecording>>>>
|
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(
|
pub async fn serial_connect(
|
||||||
port: String,
|
port: String,
|
||||||
state: Arc<SerialConnectionState>
|
state: Arc<SerialConnectionState>,
|
||||||
) -> Result<(), SerialError> {
|
) -> Result<(), SerialError> {
|
||||||
let port_name = port.trim().to_string();
|
let port_name = port.trim().to_string();
|
||||||
if port_name.is_empty() {
|
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 app;
|
||||||
|
pub mod cmd;
|
||||||
|
pub mod flog;
|
||||||
|
pub mod serial_core;
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
println!("Hello, world!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use tokio_serial::available_ports;
|
use tokio_serial::available_ports;
|
||||||
|
|
||||||
use crate::serial_core::{
|
use crate::serial_core::{
|
||||||
error::SerialError, frame::{TactileAFrame, TestFrame}, record::Recording
|
error::SerialError,
|
||||||
|
frame::{TactileAFrame, TestFrame},
|
||||||
|
record::Recording,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
pub mod codecs;
|
pub mod codecs;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
pub mod model;
|
|
||||||
pub mod serial;
|
|
||||||
pub mod record;
|
pub mod record;
|
||||||
|
pub mod serial;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub type TestRecording = Recording<TestFrame>;
|
pub type TestRecording = Recording<TestFrame>;
|
||||||
@@ -41,3 +42,4 @@ pub fn serial_enum() -> Result<Vec<String>, SerialError> {
|
|||||||
|
|
||||||
Ok(ports)
|
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::codec::Codec;
|
||||||
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
use crate::serial_core::codecs::tactile_a::TactileACodec;
|
||||||
use crate::serial_core::frame::{FrameHandler, TactileAFrame, TestFrame};
|
use crate::serial_core::frame::{FrameHandler, TactileAFrame};
|
||||||
use crate::serial_core::model::{HudChartState, HudPacket};
|
|
||||||
use crate::serial_core::record::Recording;
|
use crate::serial_core::record::Recording;
|
||||||
|
use crate::serial_core::record::{FrameTiming, RecordedFrame};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use std::future::pending;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::time::{self, Duration, MissedTickBehavior};
|
use tokio::time::{self, Duration, MissedTickBehavior};
|
||||||
use tokio_serial::SerialStream;
|
use tokio_serial::SerialStream;
|
||||||
use tokio_util::sync::CancellationToken;
|
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> {
|
pub enum PollMode<F> {
|
||||||
Disable,
|
Disable,
|
||||||
Enabled(Box<dyn PollRequester<F>>)
|
Enabled(Box<dyn PollRequester<F>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SerialFrame: Clone + Send + 'static {
|
pub trait SerialFrame: Clone + Send + 'static {
|
||||||
fn dts_ms(&self) -> u64;
|
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 {
|
pub trait PollRequester<F>: Send {
|
||||||
fn poll_interval(&self) -> Option<Duration> {
|
fn poll_interval(&self) -> Option<Duration> {
|
||||||
None
|
None
|
||||||
@@ -122,11 +114,18 @@ where
|
|||||||
F: SerialFrame,
|
F: SerialFrame,
|
||||||
C: Codec<F> + Send + 'static,
|
C: Codec<F> + Send + 'static,
|
||||||
H: FrameHandler<F, T> + Send + 'static,
|
H: FrameHandler<F, T> + Send + 'static,
|
||||||
T: Into<i32>
|
T: Into<i32>,
|
||||||
{
|
{
|
||||||
run_serial_with_poll(
|
run_serial_with_poll(
|
||||||
port, codec, handler, session_started_at, recording, cancel, PollMode::Disable
|
port,
|
||||||
).await
|
codec,
|
||||||
|
handler,
|
||||||
|
session_started_at,
|
||||||
|
recording,
|
||||||
|
cancel,
|
||||||
|
PollMode::Disable,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_serial_with_poll<C, H, T, F>(
|
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,
|
session_started_at: Instant,
|
||||||
recording: Arc<Mutex<Recording<F>>>,
|
recording: Arc<Mutex<Recording<F>>>,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
poll_mode: PollMode<F>
|
poll_mode: PollMode<F>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
F: SerialFrame,
|
F: SerialFrame,
|
||||||
@@ -149,21 +148,16 @@ where
|
|||||||
PollMode::Enabled(r) => Some(r),
|
PollMode::Enabled(r) => Some(r),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut poll_interval = requester
|
let mut poll_interval = requester.as_ref().and_then(|r| r.poll_interval()).map(|d| {
|
||||||
.as_ref()
|
|
||||||
.and_then(|r| r.poll_interval())
|
|
||||||
.map(|d| {
|
|
||||||
let mut it = time::interval(d);
|
let mut it = time::interval(d);
|
||||||
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
it.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||||
it
|
it
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut chart_state = HudChartState::new();
|
|
||||||
let mut buffer = [0u8; 1024];
|
let mut buffer = [0u8; 1024];
|
||||||
let mut prune_interval = time::interval(Duration::from_millis(450));
|
let mut prune_interval = time::interval(Duration::from_millis(450));
|
||||||
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
prune_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||||
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel.cancelled() => break,
|
_ = cancel.cancelled() => break,
|
||||||
|
|||||||
Reference in New Issue
Block a user