Skip to main content

nucleus_trace/
translate.rs

1//! Turning raw ITM [`Packet`]s into named, typed [`TraceEvent`]s.
2//!
3//! This is the only place trace *meaning* is assigned: stimulus port 0 is the
4//! UTF-8 log stream (it replaces `printf`), and ports 1–7 carry typed variables
5//! named by the project's `[[trace.variables]]` table. The mapping is pure and
6//! unit-tested; the async plumbing in [`crate::server`] just forwards results.
7
8use std::collections::{BTreeMap, VecDeque};
9
10use nucleus_itm::Packet;
11use serde::Serialize;
12
13/// The DWT hardware-source discriminator for periodic PC-sample packets.
14const DWT_PC_SAMPLE: u8 = 2;
15
16/// How many PC samples the rolling CPU-load estimate averages over.
17const CPU_WINDOW: usize = 32;
18
19/// The numeric type a traced variable's bytes decode to.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum VarType {
22    F32,
23    U16,
24    U32,
25    I32,
26}
27
28impl VarType {
29    /// Parse the `type = "…"` field of a `[[trace.variables]]` entry.
30    pub fn parse(s: &str) -> Option<VarType> {
31        match s {
32            "f32" => Some(VarType::F32),
33            "u16" => Some(VarType::U16),
34            "u32" => Some(VarType::U32),
35            "i32" => Some(VarType::I32),
36            _ => None,
37        }
38    }
39
40    fn name(self) -> &'static str {
41        match self {
42            VarType::F32 => "f32",
43            VarType::U16 => "u16",
44            VarType::U32 => "u32",
45            VarType::I32 => "i32",
46        }
47    }
48
49    /// Decode little-endian `data` to a JSON number, zero-padding or truncating
50    /// to the type's width so malformed widths never panic.
51    fn decode(self, data: &[u8]) -> serde_json::Value {
52        let mut buf4 = [0u8; 4];
53        let n = data.len().min(4);
54        buf4[..n].copy_from_slice(&data[..n]);
55        match self {
56            VarType::F32 => json_f64(f64::from(f32::from_le_bytes(buf4))),
57            VarType::U32 => serde_json::Value::from(u32::from_le_bytes(buf4)),
58            VarType::I32 => serde_json::Value::from(i32::from_le_bytes(buf4)),
59            VarType::U16 => {
60                let mut buf2 = [0u8; 2];
61                let m = data.len().min(2);
62                buf2[..m].copy_from_slice(&data[..m]);
63                serde_json::Value::from(u16::from_le_bytes(buf2))
64            }
65        }
66    }
67}
68
69fn json_f64(v: f64) -> serde_json::Value {
70    serde_json::Number::from_f64(v)
71        .map(serde_json::Value::Number)
72        .unwrap_or(serde_json::Value::Null)
73}
74
75/// One traced variable: its name and decode type.
76#[derive(Debug, Clone)]
77pub struct Variable {
78    pub name: String,
79    pub ty: VarType,
80}
81
82/// Port → variable mapping, built from `[[trace.variables]]`.
83#[derive(Debug, Clone, Default)]
84pub struct VariableMap(BTreeMap<u8, Variable>);
85
86impl VariableMap {
87    pub fn new() -> VariableMap {
88        VariableMap(BTreeMap::new())
89    }
90
91    /// Register `name`/`ty` on stimulus `port`.
92    pub fn insert(&mut self, port: u8, name: impl Into<String>, ty: VarType) {
93        self.0.insert(
94            port,
95            Variable {
96                name: name.into(),
97                ty,
98            },
99        );
100    }
101
102    /// Build a map from a parsed `[trace]` config, ignoring entries with an
103    /// unknown type (port 0 is always the log stream and needs no entry).
104    pub fn from_config(trace: &nucleus_compiler::config::Trace) -> VariableMap {
105        let mut map = VariableMap::new();
106        for v in &trace.variables {
107            if let Some(ty) = VarType::parse(&v.ty) {
108                map.insert(v.port, v.name.clone(), ty);
109            }
110        }
111        map
112    }
113
114    fn get(&self, port: u8) -> Option<&Variable> {
115        self.0.get(&port)
116    }
117}
118
119/// A structured trace event, serialized to JSON for WebSocket clients.
120#[derive(Debug, Clone, PartialEq, Serialize)]
121#[serde(tag = "kind", rename_all = "lowercase")]
122pub enum TraceEvent {
123    /// A decoded line from the port-0 log stream.
124    Log { message: String },
125    /// A typed variable update from ports 1–7.
126    Variable {
127        port: u8,
128        name: String,
129        #[serde(rename = "type")]
130        ty: &'static str,
131        value: serde_json::Value,
132    },
133    /// The device reported a trace FIFO overflow.
134    Overflow,
135    /// Estimated CPU utilization in `[0, 1]`, derived from DWT PC sampling.
136    CpuLoad { load: f64 },
137}
138
139/// Stateful translator: maps a stream of [`Packet`]s to [`TraceEvent`]s,
140/// reassembling port-0 bytes into whole log lines.
141#[derive(Debug)]
142pub struct Translator {
143    vars: VariableMap,
144    /// Bytes received on port 0 since the last newline.
145    log_line: Vec<u8>,
146    /// Recent DWT PC samples (`true` = running, `false` = asleep) for CPU load.
147    cpu_samples: VecDeque<bool>,
148}
149
150impl Translator {
151    pub fn new(vars: VariableMap) -> Translator {
152        Translator {
153            vars,
154            log_line: Vec::new(),
155            cpu_samples: VecDeque::with_capacity(CPU_WINDOW),
156        }
157    }
158
159    /// Translate one packet into zero or more events.
160    pub fn translate(&mut self, packet: &Packet) -> Vec<TraceEvent> {
161        match packet {
162            Packet::Instrumentation { port: 0, data } => self.push_log_bytes(data),
163            Packet::Instrumentation { port, data } => match self.vars.get(*port) {
164                Some(var) => vec![TraceEvent::Variable {
165                    port: *port,
166                    name: var.name.clone(),
167                    ty: var.ty.name(),
168                    value: var.ty.decode(data),
169                }],
170                None => Vec::new(), // unmapped port: nothing to report
171            },
172            Packet::Overflow => vec![TraceEvent::Overflow],
173            // DWT periodic PC sampling → rolling CPU-load estimate.
174            Packet::Hardware {
175                discriminator: DWT_PC_SAMPLE,
176                data,
177            } => self.push_pc_sample(data),
178            // Timestamps, sync, other hardware, extension carry no value yet.
179            _ => Vec::new(),
180        }
181    }
182
183    /// Record one DWT PC sample and emit the updated rolling CPU load.
184    ///
185    /// A periodic PC-sample packet is a single `0x00` byte when the core was
186    /// asleep (WFI/WFE) at the sample instant, or a 4-byte program counter when
187    /// it was executing. Load is the fraction of recent samples that were
188    /// running.
189    fn push_pc_sample(&mut self, data: &[u8]) -> Vec<TraceEvent> {
190        let running = !(data.len() == 1 && data[0] == 0x00);
191        if self.cpu_samples.len() == CPU_WINDOW {
192            self.cpu_samples.pop_front();
193        }
194        self.cpu_samples.push_back(running);
195        let active = self.cpu_samples.iter().filter(|&&r| r).count();
196        let load = active as f64 / self.cpu_samples.len() as f64;
197        vec![TraceEvent::CpuLoad { load }]
198    }
199
200    /// Flush any buffered partial log line (e.g. on shutdown).
201    pub fn flush(&mut self) -> Vec<TraceEvent> {
202        if self.log_line.is_empty() {
203            return Vec::new();
204        }
205        let message = take_utf8(&mut self.log_line);
206        vec![TraceEvent::Log { message }]
207    }
208
209    fn push_log_bytes(&mut self, data: &[u8]) -> Vec<TraceEvent> {
210        let mut events = Vec::new();
211        for &b in data {
212            if b == b'\n' {
213                let message = take_utf8(&mut self.log_line);
214                events.push(TraceEvent::Log { message });
215            } else if b != b'\r' {
216                self.log_line.push(b);
217            }
218        }
219        events
220    }
221}
222
223/// Drain `buf` into a lossy-UTF-8 string.
224fn take_utf8(buf: &mut Vec<u8>) -> String {
225    let s = String::from_utf8_lossy(buf).into_owned();
226    buf.clear();
227    s
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    fn map() -> VariableMap {
235        let mut m = VariableMap::new();
236        m.insert(1, "temperature", VarType::F32);
237        m.insert(2, "duty", VarType::U16);
238        m.insert(3, "loop_us", VarType::U32);
239        m
240    }
241
242    #[test]
243    fn port0_bytes_become_log_lines() {
244        let mut t = Translator::new(map());
245        let mut events = Vec::new();
246        for ch in b"hi\nbye\n" {
247            events.extend(t.translate(&Packet::Instrumentation {
248                port: 0,
249                data: vec![*ch],
250            }));
251        }
252        assert_eq!(
253            events,
254            vec![
255                TraceEvent::Log {
256                    message: "hi".into()
257                },
258                TraceEvent::Log {
259                    message: "bye".into()
260                },
261            ]
262        );
263    }
264
265    #[test]
266    fn partial_log_line_waits_for_newline() {
267        let mut t = Translator::new(map());
268        assert!(t
269            .translate(&Packet::Instrumentation {
270                port: 0,
271                data: b"par".to_vec()
272            })
273            .is_empty());
274        let done = t.translate(&Packet::Instrumentation {
275            port: 0,
276            data: b"tial\n".to_vec(),
277        });
278        assert_eq!(
279            done,
280            vec![TraceEvent::Log {
281                message: "partial".into()
282            }]
283        );
284    }
285
286    #[test]
287    fn f32_variable_decodes_to_a_number() {
288        let mut t = Translator::new(map());
289        let events = t.translate(&Packet::Instrumentation {
290            port: 1,
291            data: 3.5f32.to_le_bytes().to_vec(),
292        });
293        assert_eq!(
294            events,
295            vec![TraceEvent::Variable {
296                port: 1,
297                name: "temperature".into(),
298                ty: "f32",
299                value: serde_json::json!(3.5),
300            }]
301        );
302    }
303
304    #[test]
305    fn u16_and_u32_variables_decode() {
306        let mut t = Translator::new(map());
307        let duty = t.translate(&Packet::Instrumentation {
308            port: 2,
309            data: 1000u16.to_le_bytes().to_vec(),
310        });
311        assert_eq!(
312            duty[0],
313            TraceEvent::Variable {
314                port: 2,
315                name: "duty".into(),
316                ty: "u16",
317                value: serde_json::json!(1000),
318            }
319        );
320        let lt = t.translate(&Packet::Instrumentation {
321            port: 3,
322            data: 70_000u32.to_le_bytes().to_vec(),
323        });
324        assert_eq!(
325            lt[0],
326            TraceEvent::Variable {
327                port: 3,
328                name: "loop_us".into(),
329                ty: "u32",
330                value: serde_json::json!(70_000),
331            }
332        );
333    }
334
335    #[test]
336    fn unmapped_port_is_ignored() {
337        let mut t = Translator::new(map());
338        assert!(t
339            .translate(&Packet::Instrumentation {
340                port: 5,
341                data: vec![1, 2, 3, 4]
342            })
343            .is_empty());
344    }
345
346    #[test]
347    fn dwt_pc_samples_estimate_cpu_load() {
348        let mut t = Translator::new(map());
349        // Discriminator 2 = PC sample. Sleeping sample = single 0x00 byte;
350        // running sample = a 4-byte PC value.
351        let sleep = Packet::Hardware {
352            discriminator: 2,
353            data: vec![0x00],
354        };
355        let run = Packet::Hardware {
356            discriminator: 2,
357            data: vec![0x00, 0x10, 0x00, 0x08],
358        };
359
360        // First sample running -> load 1.0.
361        let e = t.translate(&run);
362        assert_eq!(e, vec![TraceEvent::CpuLoad { load: 1.0 }]);
363        // Then one sleeping -> 1 of 2 running -> 0.5.
364        let e = t.translate(&sleep);
365        assert_eq!(e, vec![TraceEvent::CpuLoad { load: 0.5 }]);
366    }
367
368    #[test]
369    fn cpu_load_event_json_shape() {
370        let json = serde_json::to_value(TraceEvent::CpuLoad { load: 0.25 }).unwrap();
371        assert_eq!(json, serde_json::json!({"kind": "cpuload", "load": 0.25}));
372    }
373
374    #[test]
375    fn non_pc_hardware_packets_are_ignored() {
376        let mut t = Translator::new(map());
377        // Discriminator 1 = exception trace, not PC sampling.
378        assert!(t
379            .translate(&Packet::Hardware {
380                discriminator: 1,
381                data: vec![0x01, 0x02]
382            })
383            .is_empty());
384    }
385
386    #[test]
387    fn overflow_is_reported_and_serializes() {
388        let mut t = Translator::new(map());
389        let events = t.translate(&Packet::Overflow);
390        assert_eq!(events, vec![TraceEvent::Overflow]);
391        let json = serde_json::to_string(&events[0]).unwrap();
392        assert_eq!(json, r#"{"kind":"overflow"}"#);
393    }
394
395    #[test]
396    fn variable_event_json_shape() {
397        let ev = TraceEvent::Variable {
398            port: 1,
399            name: "temperature".into(),
400            ty: "f32",
401            value: serde_json::json!(3.5),
402        };
403        let json = serde_json::to_value(&ev).unwrap();
404        assert_eq!(
405            json,
406            serde_json::json!({
407                "kind": "variable",
408                "port": 1,
409                "name": "temperature",
410                "type": "f32",
411                "value": 3.5
412            })
413        );
414    }
415}