rserial/src/app/serial_handler.rs
2026-02-09 04:53:44 -05:00

153 lines
5.7 KiB
Rust

use std::time::Instant;
use super::App;
use super::state::{MessageStore, SyncManager};
use crate::constants::{
SYNC_ACK, SYNC_BEGIN, SYNC_COMMAND, SYNC_END, SYNC_INITIAL_DELAY, SYNC_MAX_RETRIES,
SYNC_TIMEOUT,
};
use crate::serial::{SerialEvent, SerialHandle, SerialMessage};
use crate::sync::{SyncState, parse_command_line};
use crate::widgets::SyncDisplay;
/// Lines that are part of the sync protocol and should never appear in user-visible messages.
const SYNC_PROTOCOL_MARKERS: &[&str] = &[SYNC_BEGIN, SYNC_END, SYNC_ACK];
impl SyncManager {
/// Drive the sync state machine forward. Called each loop iteration.
pub(super) fn tick(&mut self, serial: &SerialHandle) {
match &self.sync_state {
SyncState::Idle => {
if self.started_at.elapsed() >= SYNC_INITIAL_DELAY {
serial.writeln(SYNC_COMMAND);
self.sync_state = SyncState::AwaitingBegin {
sent_at: Instant::now(),
attempts: 1,
};
}
}
SyncState::AwaitingBegin { sent_at, attempts } => {
if sent_at.elapsed() >= SYNC_TIMEOUT {
if *attempts >= SYNC_MAX_RETRIES {
self.sync_state = SyncState::Failed;
} else {
let next_attempt = *attempts + 1;
serial.writeln(SYNC_COMMAND);
self.sync_state = SyncState::AwaitingBegin {
sent_at: Instant::now(),
attempts: next_attempt,
};
}
}
}
SyncState::Receiving { .. } | SyncState::Synced | SyncState::Failed => {}
}
}
/// Process a line through the sync protocol. Returns `true` if consumed.
pub(super) fn handle_line(&mut self, line: &str, serial: &SerialHandle) -> bool {
// Always swallow lines containing sync protocol markers (e.g. the
// device echoing back "unknown: #acknowledge-sync") regardless of
// current state.
if !matches!(self.sync_state, SyncState::AwaitingBegin { .. } | SyncState::Receiving { .. })
&& SYNC_PROTOCOL_MARKERS.iter().any(|m| line.contains(m))
{
return true;
}
match &self.sync_state {
SyncState::AwaitingBegin { .. } => {
if line == SYNC_BEGIN {
self.sync_state = SyncState::Receiving {
commands: Vec::new(),
};
return true;
}
}
SyncState::Receiving { .. } => {
if line == SYNC_END {
if let SyncState::Receiving { commands } =
std::mem::replace(&mut self.sync_state, SyncState::Synced)
{
self.device_commands = commands;
}
serial.writeln(SYNC_ACK);
return true;
}
if let SyncState::Receiving { commands } = &mut self.sync_state {
commands.push(parse_command_line(line));
}
return true;
}
_ => {}
}
false
}
/// Get the current sync display state for the status bar.
pub(super) fn display(&self) -> SyncDisplay {
match &self.sync_state {
SyncState::Idle | SyncState::AwaitingBegin { .. } => SyncDisplay::Pending,
SyncState::Receiving { .. } => SyncDisplay::Receiving,
SyncState::Synced => SyncDisplay::Synced(self.device_commands.len()),
SyncState::Failed => SyncDisplay::Failed,
}
}
/// Reset sync state for a manual re-sync.
pub(super) fn start_resync(&mut self) {
self.device_commands.clear();
self.started_at = Instant::now();
self.sync_state = SyncState::Idle;
}
}
impl MessageStore {
/// Try to parse a graph data line. Returns `true` if consumed.
pub(super) fn handle_graph_line(&mut self, line: &str) -> bool {
if let Some(val_str) = line.strip_prefix("#graphf ")
&& let Ok(val) = val_str.trim().parse::<f64>()
{
self.push_graph_float(val);
return true;
} else if let Some(val_str) = line.strip_prefix("#graphi ")
&& let Ok(val) = val_str.trim().parse::<i64>()
{
self.push_graph_int(val);
return true;
}
false
}
}
impl App {
/// Poll for serial events. Returns `Some(reason)` if the connection was lost.
pub(super) fn receive_serial(&mut self) -> Option<String> {
if !self.view.receiving {
return None;
}
while let Some(event) = self.serial_connection.try_recv() {
match event {
SerialEvent::LineReceived(msg) => {
if self.sync.handle_line(&msg.message, &self.serial_connection) {
// consumed by sync protocol
} else if self.messages.handle_graph_line(&msg.message) {
// consumed by graph parser
} else {
let idx = self.messages.push_received(msg);
self.view.auto_scroll(idx);
}
}
SerialEvent::Error(e) => {
let idx = self.messages.push_received(SerialMessage::new(e));
self.view.auto_scroll(idx);
}
SerialEvent::Disconnected(reason) => {
return Some(reason);
}
}
}
None
}
}