Bevy+Ratutui powered Monitoring of Pico-Strike devices

Signals display, timestamp handling

+134 -18
+52 -2
src/device.rs
··· 46 46 #[component(immutable)] 47 47 pub struct StormLevel(pub u16); 48 48 49 - #[derive(Debug, Component)] 49 + #[derive(Debug, Component, Deref)] 50 50 #[component(immutable)] 51 51 pub struct Timestamp(pub jiff::Timestamp); 52 52 53 + impl Timestamp { 54 + pub fn from_seconds(stamp: i64) -> Result<Self, jiff::Error> { 55 + jiff::Timestamp::from_second(stamp).map(Self) 56 + } 57 + 58 + pub fn from_microseconds(stamp: i64) -> Result<Self, jiff::Error> { 59 + jiff::Timestamp::from_microsecond(stamp).map(Self) 60 + } 61 + } 62 + 63 + #[derive(Debug, Component, Deref)] 64 + #[component(immutable)] 65 + pub struct StormSignal(Vec<i16>); 66 + 67 + impl StormSignal { 68 + pub fn new(value: Vec<i16>) -> Self { 69 + Self(value) 70 + } 71 + } 72 + 73 + #[derive(Debug, Component, Deref)] 74 + #[component(immutable)] 75 + pub struct SignalAverage(pub u16); 76 + 77 + impl SignalAverage { 78 + pub fn new(value: u16) -> Self { 79 + Self(value) 80 + } 81 + } 82 + 83 + #[derive(Debug, Component, Deref)] 84 + #[component(immutable)] 85 + pub struct SignalPeaks(Vec<u16>); 86 + 87 + impl SignalPeaks { 88 + pub fn new(value: Vec<u16>) -> Self { 89 + Self(value) 90 + } 91 + } 92 + 93 + #[derive(Debug, Component)] 94 + #[relationship(relationship_target = Signals)] 95 + pub struct SignalSource(pub Entity); 96 + 97 + #[derive(Component, Debug, Deref)] 98 + #[relationship_target(relationship = SignalSource, linked_spawn)] 99 + pub struct Signals { 100 + signals: Vec<Entity>, 101 + } 102 + 53 103 #[derive(Debug, Component)] 54 104 #[relationship(relationship_target = StormLevels)] 55 105 pub struct StormSource(pub Entity); 56 106 57 107 #[derive(Component, Debug, Deref)] 58 - #[relationship_target(relationship = StormSource)] 108 + #[relationship_target(relationship = StormSource, linked_spawn)] 59 109 pub struct StormLevels { 60 110 levels: Vec<Entity>, 61 111 }
+82 -16
src/views/monitoring.rs
··· 18 18 use jiff::SignedDuration; 19 19 use ratatui::{ 20 20 layout::{Constraint, HorizontalAlignment, Layout}, 21 - style::Color, 21 + style::{Color, Stylize}, 22 22 text::{Line, Span}, 23 - widgets::{Block, Padding, Paragraph}, 23 + widgets::{Axis, Block, Chart, Dataset, Padding, Paragraph}, 24 24 }; 25 25 use striker_proto::{StrikerResponse, Update}; 26 26 27 27 use crate::{ 28 28 device::{ 29 - ConnectedDevice, Device, DeviceSocket, StormLevel, StormLevels, StormSource, Timestamp, 29 + ConnectedDevice, Device, DeviceSocket, SignalAverage, SignalPeaks, SignalSource, Signals, 30 + StormLevel, StormLevels, StormSignal, StormSource, Timestamp, 30 31 }, 31 32 messages::StrikeMessage, 32 33 net::{StrikeConnect, StrikeConnection, StrikeUpdates}, ··· 111 112 ) -> Result { 112 113 let mut entity = commands.entity(connected.0); 113 114 114 - while let Ok(StrikerResponse::Update(Update::Warning { timestamp, level })) = 115 - updates.0.try_recv() 116 - { 117 - entity.with_related::<StormSource>(( 118 - Timestamp(jiff::Timestamp::from_second(timestamp)?), 119 - StormLevel(level), 120 - )); 115 + while let Ok(StrikerResponse::Update(update)) = updates.0.try_recv() { 116 + match update { 117 + Update::Warning { timestamp, level } => { 118 + entity.with_related::<StormSource>(( 119 + Timestamp::from_seconds(timestamp)?, 120 + StormLevel(level), 121 + )); 122 + } 123 + Update::Strike { 124 + timestamp, 125 + peaks, 126 + samples, 127 + average, 128 + } => { 129 + entity.with_related::<SignalSource>(( 130 + Timestamp::from_microseconds(timestamp)?, 131 + SignalPeaks::new(peaks), 132 + StormSignal::new(samples), 133 + SignalAverage::new(average), 134 + )); 135 + } 136 + } 121 137 } 122 138 123 139 Ok(()) ··· 126 142 pub fn monitoring_view( 127 143 mut context: ResMut<RatatuiContext>, 128 144 connected: Res<ConnectedDevice>, 129 - q_devices: Query<(&Name, Option<&StormLevels>), With<Device>>, 145 + q_devices: Query<(&Name, Option<&StormLevels>, Option<&Signals>), With<Device>>, 130 146 q_levels: Query<(&Timestamp, &StormLevel)>, 147 + q_signals: Query<(&Timestamp, &StormSignal)>, 131 148 ) -> Result { 132 149 context.draw(|frame| { 133 150 let [top, mid, bottom] = Layout::vertical([ ··· 144 161 ]) 145 162 .areas(top); 146 163 147 - let (device, children) = q_devices.get(connected.0).unwrap(); 164 + let (device, levels, signals) = q_devices.get(connected.0).unwrap(); 148 165 149 - let latest = children.and_then(|c| c.last().copied().and_then(|a| q_levels.get(a).ok())); 166 + let latest_level = levels.and_then(|c| { 167 + c.last() 168 + .copied() 169 + .and_then(|entity| q_levels.get(entity).ok()) 170 + }); 171 + let latest_signal = signals 172 + .and_then(|s| s.last().copied()) 173 + .and_then(|entity| q_signals.get(entity).ok()); 150 174 151 175 let name = Paragraph::new(device.as_str()).block( 152 176 Block::bordered() ··· 156 180 .border_style(Color::LightGreen), 157 181 ); 158 182 183 + let timestamp_signal = latest_signal.map(|(t, _)| t); 184 + let timestamp_level = latest_level.map(|(t, _)| t); 185 + 186 + let timestamp = match (timestamp_signal, timestamp_level) { 187 + (Some(signal), Some(level)) => signal 188 + .duration_since(**level) 189 + .is_positive() 190 + .then_some(signal) 191 + .or(Some(level)), 192 + (Some(time), None) | (None, Some(time)) => Some(time), 193 + _ => None, 194 + }; 195 + 159 196 let timestamp = Paragraph::new(Line::from_iter( 160 - latest.map(|(t, _)| Span::from(format!("{}", t.0))), 197 + timestamp.map(|t| Span::from(format!("{}", t.0))), 161 198 )) 162 199 .block( 163 200 Block::bordered() ··· 170 207 let warn_level = Paragraph::new(Line::from_iter( 171 208 Some(Span::raw("Level: ")) 172 209 .into_iter() 173 - .chain(latest.map(|(_, s)| Span::from(format!("{}", s.0)))), 210 + .chain(latest_level.map(|(_, s)| Span::from(format!("{}", s.0)))), 174 211 )) 175 212 .block( 176 213 Block::bordered() ··· 180 217 .border_style(Color::LightGreen), 181 218 ); 182 219 220 + let data = latest_signal 221 + .map(|(_, samples)| { 222 + samples 223 + .iter() 224 + .enumerate() 225 + .map(|(x, y)| (x as f64, *y as f64)) 226 + .collect() 227 + }) 228 + .unwrap_or_else(Vec::new); 229 + 230 + let y_bounds = data 231 + .iter() 232 + .map(|(_, y)| y.abs()) 233 + .reduce(f64::max) 234 + .unwrap_or(0.0); 235 + 236 + let datasets = vec![ 237 + Dataset::default() 238 + .graph_type(ratatui::widgets::GraphType::Line) 239 + .marker(ratatui::symbols::Marker::Braille) 240 + .red() 241 + .data(&data), 242 + ]; 243 + 183 244 let block = Block::bordered() 184 245 .title("Details") 185 246 .padding(Padding::new(2, 2, 1, 1)) 186 247 .border_style(Color::LightGreen); 187 248 249 + let chart = Chart::new(datasets) 250 + .x_axis(Axis::default().bounds([0.0, 512.0])) 251 + .y_axis(Axis::default().bounds([-y_bounds, y_bounds])) 252 + .block(block); 253 + 188 254 let help = Paragraph::new("Keys: 'q'/ESC Quit, BACKSPACE Return to Device select").block( 189 255 Block::bordered() 190 256 .padding(Padding::horizontal(2)) ··· 194 260 frame.render_widget(name, left); 195 261 frame.render_widget(timestamp, center); 196 262 frame.render_widget(warn_level, right); 197 - frame.render_widget(block, mid); 263 + frame.render_widget(chart, mid); 198 264 frame.render_widget(help, bottom); 199 265 })?; 200 266