trans to tokio

This commit is contained in:
lenn
2026-04-23 23:54:33 +08:00
parent 05accd3690
commit 8182e67152
9 changed files with 1671 additions and 550 deletions

View File

@@ -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(&current_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(&current_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
View 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| {})
}
}

View File

@@ -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!");
}

View File

@@ -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)
}
}

View File

@@ -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);
// }
// }

View File

@@ -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,