1use crate::dto::{CanFrameDto, CaptureStatsDto, LiveCaptureUpdate, StatsHtml};
7use dbc_rs::FastDbc;
8use std::collections::{HashMap, HashSet, VecDeque};
9use std::time::Instant;
10
11const MAX_RECENT_FRAMES: usize = 100;
13
14const MAX_HISTORY_POINTS: usize = 50;
16
17const MAX_RECENT_ERRORS: usize = 100;
19
20struct ErrorEntry {
22 timestamp: f64,
23 channel: String,
24 error_type: String,
25 details: String,
26 count: u64,
27}
28
29struct MessageEntry {
31 can_id: u32,
32 message_name: String,
33 data: Vec<u8>,
34 dlc: u8,
35 count: u64,
36 last_update: f64,
37 last_count: u64,
39 last_rate_time: f64,
40 rate: f64,
41}
42
43type SignalKey = (u32, usize);
45
46struct SignalEntry {
48 signal_name: String,
49 message_name: String,
50 value: f64,
51 unit: String,
52 last_update: f64,
53 history: VecDeque<f64>,
55 min_value: f64,
56 max_value: f64,
57}
58
59pub struct LiveCaptureState {
64 capture_file: String,
66
67 messages: HashMap<u32, MessageEntry>,
69 signals: HashMap<SignalKey, SignalEntry>,
70
71 recent_frames: VecDeque<CanFrameDto>,
73
74 errors: HashMap<String, ErrorEntry>,
76 recent_errors: VecDeque<(f64, String, String)>, total_error_count: u64,
78
79 frame_count: u64,
81 start_time: Instant,
82
83 fast_dbc: Option<FastDbc>,
85
86 decode_buffer: Vec<f64>,
88
89 decode_blacklist: HashSet<u32>,
91
92 last_rate_update: Instant,
94 last_frame_count: u64,
95 frame_rate: f64,
96}
97
98impl LiveCaptureState {
99 pub fn new(capture_file: String, fast_dbc: Option<FastDbc>) -> Self {
104 let decode_buffer = fast_dbc
106 .as_ref()
107 .map(|f| vec![0.0f64; f.max_signals()])
108 .unwrap_or_default();
109
110 Self {
111 capture_file,
112 messages: HashMap::new(),
113 signals: HashMap::new(),
114 recent_frames: VecDeque::with_capacity(MAX_RECENT_FRAMES),
115 errors: HashMap::new(),
116 recent_errors: VecDeque::with_capacity(MAX_RECENT_ERRORS),
117 total_error_count: 0,
118 frame_count: 0,
119 start_time: Instant::now(),
120 fast_dbc,
121 decode_buffer,
122 decode_blacklist: HashSet::new(),
123 last_rate_update: Instant::now(),
124 last_frame_count: 0,
125 frame_rate: 0.0,
126 }
127 }
128
129 pub fn process_error(
131 &mut self,
132 timestamp: f64,
133 channel: &str,
134 error_type: &str,
135 details: &str,
136 ) {
137 self.total_error_count += 1;
138
139 let key = error_type.to_string();
141 if let Some(entry) = self.errors.get_mut(&key) {
142 entry.count += 1;
143 entry.timestamp = timestamp;
144 entry.channel = channel.to_string();
145 entry.details = details.to_string();
146 } else {
147 self.errors.insert(
148 key,
149 ErrorEntry {
150 timestamp,
151 channel: channel.to_string(),
152 error_type: error_type.to_string(),
153 details: details.to_string(),
154 count: 1,
155 },
156 );
157 }
158
159 if self.recent_errors.len() >= MAX_RECENT_ERRORS {
161 self.recent_errors.pop_front();
162 }
163 self.recent_errors
164 .push_back((timestamp, error_type.to_string(), details.to_string()));
165 }
166
167 pub fn process_frame(&mut self, frame: CanFrameDto) {
169 let timestamp = frame.timestamp;
170
171 self.frame_count += 1;
173
174 self.update_message_monitor(&frame, timestamp);
176
177 self.decode_and_update_signals(&frame, timestamp);
179
180 if self.recent_frames.len() >= MAX_RECENT_FRAMES {
182 self.recent_frames.pop_front();
183 }
184 self.recent_frames.push_back(frame);
185 }
186
187 fn update_message_monitor(&mut self, frame: &CanFrameDto, timestamp: f64) {
189 let message_name = self.get_message_name(frame.can_id);
190
191 if let Some(entry) = self.messages.get_mut(&frame.can_id) {
192 entry.data = frame.data.clone();
193 entry.dlc = frame.dlc;
194 entry.count += 1;
195 entry.last_update = timestamp;
196 } else {
197 self.messages.insert(
198 frame.can_id,
199 MessageEntry {
200 can_id: frame.can_id,
201 message_name,
202 data: frame.data.clone(),
203 dlc: frame.dlc,
204 count: 1,
205 last_update: timestamp,
206 last_count: 0,
207 last_rate_time: timestamp,
208 rate: 0.0,
209 },
210 );
211 }
212 }
213
214 fn decode_and_update_signals(&mut self, frame: &CanFrameDto, timestamp: f64) {
216 let Some(ref fast_dbc) = self.fast_dbc else {
217 return;
218 };
219
220 if self.decode_blacklist.contains(&frame.can_id) {
222 return;
223 }
224
225 let msg = if frame.is_extended {
227 fast_dbc.get_extended(frame.can_id)
228 } else {
229 fast_dbc.get(frame.can_id)
230 };
231
232 let Some(msg) = msg else {
233 self.decode_blacklist.insert(frame.can_id);
235 return;
236 };
237
238 let count = msg.decode_into(&frame.data, &mut self.decode_buffer);
240 if count == 0 {
241 return;
242 }
243
244 let can_id = frame.can_id;
245 let message_name = msg.name();
246
247 for (i, signal) in msg.signals().iter().enumerate().take(count) {
249 let value = self.decode_buffer[i];
250 let key: SignalKey = (can_id, i);
251
252 if let Some(entry) = self.signals.get_mut(&key) {
253 entry.value = value;
254 entry.last_update = timestamp;
255 if entry.history.len() >= MAX_HISTORY_POINTS {
257 entry.history.pop_front();
258 }
259 entry.history.push_back(value);
260 if value < entry.min_value {
262 entry.min_value = value;
263 }
264 if value > entry.max_value {
265 entry.max_value = value;
266 }
267 } else {
268 let mut history = VecDeque::with_capacity(MAX_HISTORY_POINTS);
270 history.push_back(value);
271 self.signals.insert(
272 key,
273 SignalEntry {
274 signal_name: signal.name().to_string(),
275 message_name: message_name.to_string(),
276 value,
277 unit: signal.unit().unwrap_or("").to_string(),
278 last_update: timestamp,
279 history,
280 min_value: value,
281 max_value: value,
282 },
283 );
284 }
285 }
286 }
287
288 fn get_message_name(&self, can_id: u32) -> String {
290 if let Some(ref fast_dbc) = self.fast_dbc {
291 if let Some(msg) = fast_dbc.get(can_id) {
293 return msg.name().to_string();
294 }
295 }
296 "-".to_string()
297 }
298
299 pub fn update_rates(&mut self) {
301 let now = Instant::now();
302 let elapsed = now.duration_since(self.last_rate_update).as_secs_f64();
303
304 if elapsed >= 0.5 {
305 let frame_delta = self.frame_count - self.last_frame_count;
307 self.frame_rate = frame_delta as f64 / elapsed;
308 self.last_frame_count = self.frame_count;
309
310 let current_time = self.start_time.elapsed().as_secs_f64();
312 for entry in self.messages.values_mut() {
313 let count_delta = entry.count - entry.last_count;
314 let time_delta = current_time - entry.last_rate_time;
315 if time_delta > 0.0 {
316 entry.rate = count_delta as f64 / time_delta;
317 }
318 entry.last_count = entry.count;
319 entry.last_rate_time = current_time;
320 }
321
322 self.last_rate_update = now;
323 }
324 }
325
326 pub fn generate_update(&self) -> LiveCaptureUpdate {
328 let elapsed = self.start_time.elapsed().as_secs_f64();
329
330 let stats = CaptureStatsDto {
331 frame_count: self.frame_count,
332 message_count: self.messages.len() as u32,
333 signal_count: self.signals.len() as u32,
334 frame_rate: self.frame_rate,
335 elapsed_secs: elapsed,
336 capture_file: Some(self.capture_file.clone()),
337 };
338
339 let mut messages: Vec<_> = self.messages.values().collect();
341 messages.sort_by_key(|e| e.can_id);
342 let messages_html = messages
343 .iter()
344 .map(|e| {
345 let id_hex = format!("{:03X}", e.can_id);
346 let data_hex = e
347 .data
348 .iter()
349 .map(|b| format!("{:02X}", b))
350 .collect::<Vec<_>>()
351 .join(" ");
352 format!(
353 "<tr><td class=\"cv-cell-id\">0x{}</td><td class=\"cv-cell-name\">{}</td><td class=\"cv-cell-data\">{}</td><td>{}</td><td>{:.1}/s</td></tr>",
354 id_hex, e.message_name, data_hex, e.count, e.rate
355 )
356 })
357 .collect::<Vec<_>>()
358 .join("");
359
360 let mut signals: Vec<_> = self.signals.values().collect();
362 signals.sort_by(|a, b| {
363 a.message_name
364 .cmp(&b.message_name)
365 .then_with(|| a.signal_name.cmp(&b.signal_name))
366 });
367
368 let mut signals_html = String::new();
369 let mut current_message: Option<&str> = None;
370 for e in &signals {
371 if current_message != Some(&e.message_name) {
373 signals_html.push_str(&format!(
374 "<div class=\"cv-signal-group-header\">{}</div>",
375 e.message_name
376 ));
377 current_message = Some(&e.message_name);
378 }
379
380 let value_str = if e.value.abs() >= 1000.0 {
382 format!("{:.0}", e.value)
383 } else if e.value.abs() >= 100.0 {
384 format!("{:.1}", e.value)
385 } else if e.value.fract() == 0.0 && e.value.abs() < 100.0 {
386 format!("{:.0}", e.value)
387 } else {
388 format!("{:.2}", e.value)
389 };
390
391 let sparkline = Self::render_sparkline(&e.history, e.min_value, e.max_value);
393
394 let min_str = if e.min_value.fract() == 0.0 {
396 format!("{}", e.min_value as i64)
397 } else {
398 format!("{:.2}", e.min_value)
399 };
400 let max_str = if e.max_value.fract() == 0.0 {
401 format!("{}", e.max_value as i64)
402 } else {
403 format!("{:.2}", e.max_value)
404 };
405
406 signals_html.push_str(&format!(
407 "<div class=\"cv-signal-row\">\
408 <div class=\"cv-signal-info\">\
409 <span class=\"cv-signal-name\">{}</span>\
410 <span class=\"cv-signal-value\">{} <span class=\"cv-signal-unit\">{}</span></span>\
411 </div>\
412 <div class=\"cv-signal-chart\">\
413 <span class=\"cv-signal-min\">{}</span>\
414 {}\
415 <span class=\"cv-signal-max\">{}</span>\
416 </div>\
417 </div>",
418 e.signal_name, value_str, e.unit, min_str, sparkline, max_str
419 ));
420 }
421
422 let frames_html = self
424 .recent_frames
425 .iter()
426 .take(MAX_RECENT_FRAMES) .map(|f| {
428 let id_hex = format!("{:03X}", f.can_id);
429 let data_hex = f
430 .data
431 .iter()
432 .map(|b| format!("{:02X}", b))
433 .collect::<Vec<_>>()
434 .join(" ");
435 let flags = Self::format_flags(f);
436 format!(
437 "<tr><td class=\"cv-cell-dim\">{:.6}</td><td class=\"cv-cell-id\">0x{}</td><td>{}</td><td class=\"cv-cell-data\">{}</td><td>{}</td></tr>",
438 f.timestamp, id_hex, f.dlc, data_hex, flags
439 )
440 })
441 .collect::<Vec<_>>()
442 .join("");
443
444 let mut errors_html = String::new();
446
447 let mut error_types: Vec<_> = self.errors.values().collect();
449 error_types.sort_by(|a, b| b.count.cmp(&a.count));
450
451 for e in error_types {
453 errors_html.push_str(&format!(
454 "<tr class=\"cv-error-summary-row\">\
455 <td class=\"cv-cell-dim\">{:.6}</td>\
456 <td>{}</td>\
457 <td class=\"cv-cell-error-type\">{}</td>\
458 <td>{}</td>\
459 <td class=\"cv-cell-value\">{}</td>\
460 </tr>",
461 e.timestamp, e.channel, e.error_type, e.details, e.count
462 ));
463 }
464
465 let secs = elapsed as u64;
467 let mins = secs / 60;
468 let remaining_secs = secs % 60;
469 let stats_html = StatsHtml {
470 message_count: self.messages.len().to_string(),
471 frame_count: self.frame_count.to_string(),
472 frame_rate: format!("{:.0}/s", self.frame_rate),
473 elapsed: format!("{}:{:02}", mins, remaining_secs),
474 };
475
476 LiveCaptureUpdate {
477 stats,
478 messages_html,
479 signals_html,
480 frames_html,
481 errors_html,
482 stats_html,
483 message_count: self.messages.len() as u32,
484 signal_count: self.signals.len() as u32,
485 frame_count: self.recent_frames.len(),
486 error_count: self.total_error_count as u32,
487 }
488 }
489
490 fn format_flags(frame: &CanFrameDto) -> &'static str {
492 match (frame.is_extended, frame.is_fd, frame.brs, frame.esi) {
493 (false, false, false, false) => "-",
494 (true, false, false, false) => "EXT",
495 (false, true, false, false) => "FD",
496 (false, true, true, false) => "FD, BRS",
497 (false, true, false, true) => "FD, ESI",
498 (false, true, true, true) => "FD, BRS, ESI",
499 (true, true, false, false) => "EXT, FD",
500 (true, true, true, false) => "EXT, FD, BRS",
501 (true, true, false, true) => "EXT, FD, ESI",
502 (true, true, true, true) => "EXT, FD, BRS, ESI",
503 _ => "-",
504 }
505 }
506
507 fn render_sparkline(history: &VecDeque<f64>, min: f64, max: f64) -> String {
509 const VB_WIDTH: f64 = 200.0;
511 const VB_HEIGHT: f64 = 32.0;
512 const PADDING: f64 = 2.0;
513
514 if history.is_empty() {
515 return "<div class=\"cv-sparkline\"></div>".to_string();
516 }
517
518 let range = max - min;
519 let effective_range = if range.abs() < f64::EPSILON {
520 1.0
521 } else {
522 range
523 };
524 let n = history.len();
525
526 let points: Vec<String> = history
528 .iter()
529 .enumerate()
530 .map(|(i, &v)| {
531 let x = PADDING + (i as f64 / (n.max(2) - 1) as f64) * (VB_WIDTH - 2.0 * PADDING);
532 let y = PADDING + (1.0 - (v - min) / effective_range) * (VB_HEIGHT - 2.0 * PADDING);
533 format!("{:.1},{:.1}", x, y)
534 })
535 .collect();
536
537 format!(
538 "<div class=\"cv-sparkline\">\
539 <svg viewBox=\"0 0 {} {}\" preserveAspectRatio=\"none\">\
540 <polyline points=\"{}\" fill=\"none\" stroke=\"var(--cv-accent)\" stroke-width=\"1.5\" vector-effect=\"non-scaling-stroke\"/>\
541 </svg>\
542 </div>",
543 VB_WIDTH,
544 VB_HEIGHT,
545 points.join(" ")
546 )
547 }
548}