use core::net::SocketAddr; use bevy::{ ecs::{ entity::Entity, error::Result, message::MessageReader, name::Name, query::With, resource::Resource, system::{Commands, Query, Res, ResMut}, world::World, }, state::state::NextState, time::{Real, Time, Timer}, }; use bevy_ratatui::RatatuiContext; use jiff::SignedDuration; use ratatui::{ layout::{Constraint, HorizontalAlignment, Layout}, style::{Color, Stylize}, text::{Line, Span, Text, ToSpan}, widgets::{Axis, Block, Chart, Dataset, Padding, Paragraph, Widget}, }; use striker_proto::{Response, StrikerRequest, StrikerResponse, Update}; use crate::{ device::{ ConnectedDevice, ConnectionState, Device, DeviceDetector, DeviceSocket, SignalAverage, SignalPeaks, SignalSource, Signals, StormLevel, StormLevels, StormSignal, StormSource, Timestamp, }, messages::StrikeMessage, net::{StrikeAction, StrikeActions, StrikeRequests, StrikeUpdateState, StrikeUpdates}, state::AppState, }; #[derive(Debug, Resource)] pub struct DataClear(pub Timer); pub fn monitoring_message_handler( mut strike_reader: MessageReader, mut commands: Commands, ) { for message in strike_reader.read() { if let StrikeMessage::StopMonitoring = message { commands.queue(|world: &mut World| { let mut next = world.resource_mut::>(); next.set(AppState::Home); }); } } } pub fn enter_monitoring( connected: Res, signal: Res, request: Res, q_devices: Query<&DeviceSocket, With>, mut commands: Commands, ) -> Result { let addr = q_devices.get(connected.device)?; signal .0 .try_send(StrikeAction::Connect(SocketAddr::new(addr.ip, addr.port)))?; request.0.force_send(StrikerRequest { request: striker_proto::Request::DetectorInfo, })?; commands.insert_resource(DataClear(Timer::from_seconds( 60.0 * 1.0, bevy::time::TimerMode::Repeating, ))); Ok(()) } pub fn exit_monitoring(signal: Res, mut commands: Commands) -> Result { signal.0.try_send(StrikeAction::Disconnect)?; commands.remove_resource::(); Ok(()) } pub fn clear_device_data( mut timer: ResMut, time: Res>, mut commands: Commands, ) { timer.0.tick(time.delta()); if timer.0.just_finished() { commands.queue(|world: &mut World| { let now = jiff::Timestamp::now(); let to_remove: Vec<_> = world .query_filtered::<(Entity, &Timestamp), With>() .iter(world) .filter_map(|(entity, time)| { (time.0.duration_since(now) > SignedDuration::from_mins(5)).then_some(entity) }) .collect(); for remove in to_remove { world.despawn(remove); } }); } } pub fn update_device_data( updates: Res, mut connected: ResMut, mut commands: Commands, ) -> Result { let mut entity = commands.entity(connected.device); while let Ok(update) = updates.0.try_recv() { match update { StrikeUpdateState::Connected => { connected.connection_state = ConnectionState::Connected; } StrikeUpdateState::Connecting => { connected.connection_state = ConnectionState::Connecting; } StrikeUpdateState::Updating(response) => match response { StrikerResponse::Response(Response::DetectorInfo { blip_threshold, blip_size, }) => { entity.insert(DeviceDetector { blip_threshold, blip_size, }); } StrikerResponse::Update(Update::Warning { timestamp, level }) => { entity.with_related::(( Timestamp::from_microseconds(timestamp)?, StormLevel(level), )); } StrikerResponse::Update(Update::Strike { timestamp, peaks, samples, average, }) => { entity.with_related::(( Timestamp::from_microseconds(timestamp)?, SignalPeaks::new(peaks), StormSignal::new(samples), SignalAverage::new(average), )); } _ => {} }, StrikeUpdateState::Disconnected => { connected.connection_state = ConnectionState::Disconnected; } } } Ok(()) } pub fn monitoring_view( mut context: ResMut, connected: Res, q_devices: Query< ( &Name, Option<&DeviceDetector>, Option<&StormLevels>, Option<&Signals>, ), With, >, q_levels: Query<&StormLevel>, q_signals: Query<(&Timestamp, &StormSignal, &SignalPeaks)>, ) -> Result { context.draw(|frame| { let [mid, bottom] = Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]).areas(frame.area()); let [chart_block, side_block] = Layout::horizontal([Constraint::Fill(3), Constraint::Fill(1)]).areas(mid); let [device_block, detector_block] = Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(side_block); let (device, detector, levels, signals) = q_devices.get(connected.device).unwrap(); let latest_level = levels.and_then(|c| { c.last() .copied() .and_then(|entity| q_levels.get(entity).ok()) }); let latest_signal = signals .and_then(|s| s.last().copied()) .and_then(|entity| q_signals.get(entity).ok()); let timestamp = latest_signal.map(|(t, _, _)| t); let info = DeviceInfo { name: device, connected: &connected, timestamp, level: latest_level, }; let signal_chart = SignalChart { signal: latest_signal, }; let help = Paragraph::new("Keys: 'q'/ESC Quit, BACKSPACE Return to Device select") .block( Block::bordered() .padding(Padding::horizontal(2)) .border_style(Color::LightGreen), ) .white(); let detector = detector.copied().unwrap_or_default(); frame.render_widget(info, device_block); frame.render_widget(signal_chart, chart_block); frame.render_widget(detector, detector_block); frame.render_widget(help, bottom); })?; Ok(()) } struct DeviceInfo<'a> { name: &'a Name, connected: &'a ConnectedDevice, timestamp: Option<&'a Timestamp>, level: Option<&'a StormLevel>, } impl Widget for DeviceInfo<'_> { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let lines = [ Line::from(self.name.to_span()), Line::from_iter([ Span::raw("Status: "), self.connected.connection_state.to_span(), ]), ] .into_iter() .chain(self.timestamp.map(|t| Line::from(t.0.to_span()))) .chain( self.level .map(|level| Line::from_iter([Span::raw("Level: "), level.0.to_span()])), ); let info = Paragraph::new(Text::from_iter(lines)) .block( Block::bordered() .padding(Padding::horizontal(1)) .title("Device") .title_alignment(HorizontalAlignment::Center) .border_style(Color::LightGreen), ) .white(); info.render(area, buf); } } struct SignalChart<'a> { signal: Option<(&'a Timestamp, &'a StormSignal, &'a SignalPeaks)>, } impl Widget for SignalChart<'_> { fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) where Self: Sized, { let (data, detected) = self .signal .map(|(_, samples, signals)| { ( samples .iter() .enumerate() .map(|(x, y)| (x as f64, *y as f64)) .collect(), signals.iter().fold(None, |acc, el| { Some(acc.map_or_else( || (el.0, el.0 + 32), |prev: (usize, usize)| (prev.0, prev.1.max(el.0 + 32)), )) }), ) }) .unwrap_or_else(|| (Vec::new(), None)); let y_bounds = data .iter() .map(|(_, y)| y.abs()) .reduce(f64::max) .unwrap_or(0.0); let detected = detected.map_or_else(Vec::new, |signal| { (signal.0..=signal.1) .map(|i| (i as f64, y_bounds)) .collect() }); let datasets = vec![ Dataset::default() .graph_type(ratatui::widgets::GraphType::Line) .marker(ratatui::symbols::Marker::Braille) .red() .data(&data), ]; let block = Block::bordered() .title("Details") .border_style(Color::LightGreen); let chart_labels = [ (-y_bounds).to_string(), "0".to_string(), y_bounds.to_string(), ]; let chart2 = Chart::new(vec![ Dataset::default() .graph_type(ratatui::widgets::GraphType::Bar) .marker(ratatui::symbols::Marker::Dot) .style(Color::Indexed(22)) .data(&detected), ]) .x_axis(Axis::default().bounds([0.0, 512.0])) .y_axis( Axis::default() .bounds([0.0, 1.0]) .labels(chart_labels.clone()), ) .block(block.clone()); let chart = Chart::new(datasets) .x_axis(Axis::default().bounds([0.0, 512.0])) .y_axis( Axis::default() .bounds([-y_bounds, y_bounds]) .labels(chart_labels), ) .block(block); chart2.render(area, buf); chart.render(area, buf); } }