embedded_runner/
defmt.rs

1use std::{
2    io::Read,
3    path::Path,
4    sync::{atomic::AtomicBool, Arc},
5    time::Duration,
6};
7
8use defmt_decoder::{DecodeError, Frame, Locations, Table};
9use defmt_json_schema::v1::{JsonFrame, Location as JsonLocation, ModulePath};
10
11#[derive(Debug, thiserror::Error)]
12pub enum DefmtError {
13    #[error("Received a malformend frame.")]
14    MalformedFrame,
15    #[error("No frames received.")]
16    NoFramesReceived,
17    #[error("TCP error: {}", .0)]
18    TcpError(String),
19    #[error("TCP connection error: {}", .0)]
20    TcpConnect(String),
21    #[error("Failed reading binary. Cause: {}", .0)]
22    ReadBinary(std::io::Error),
23    #[error("Missing defmt data in given binary.")]
24    MissingDefmt,
25}
26
27pub fn read_defmt_frames(
28    binary: &Path,
29    workspace_root: &Path,
30    mut stream: std::net::TcpStream,
31    end_signal: Arc<AtomicBool>,
32) -> Result<Vec<JsonFrame>, DefmtError> {
33    let bytes = std::fs::read(binary).map_err(DefmtError::ReadBinary)?;
34    let table = Table::parse(&bytes)
35        .map_err(|_| DefmtError::MissingDefmt)?
36        .ok_or(DefmtError::MissingDefmt)?;
37    let locs = table
38        .get_locations(&bytes)
39        .map_err(|_| DefmtError::MissingDefmt)?;
40
41    // check if the locations info contains all the indicies
42    let locs = if table.indices().all(|idx| locs.contains_key(&(idx as u64))) {
43        Some(locs)
44    } else {
45        log::warn!("(BUG) location info is incomplete; it will be omitted from the output");
46        None
47    };
48
49    const READ_BUFFER_SIZE: usize = 1024;
50    let mut buf = [0; READ_BUFFER_SIZE];
51    let mut decoder = table.new_stream_decoder();
52    let mut stream_decoder = Box::pin(&mut decoder);
53
54    let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
55    let mut json_frames = Vec::new();
56
57    loop {
58        // read from tcpstream and push it to the decoder
59        if end_signal.load(std::sync::atomic::Ordering::Relaxed) {
60            return Ok(json_frames);
61        }
62
63        let n = match stream.read(&mut buf) {
64            Ok(len) => {
65                if len == 0 {
66                    continue;
67                } else {
68                    len
69                }
70            }
71            Err(err) => {
72                if err.kind() == std::io::ErrorKind::TimedOut {
73                    continue;
74                } else if matches!(
75                    err.kind(),
76                    std::io::ErrorKind::ConnectionAborted | std::io::ErrorKind::ConnectionReset
77                ) {
78                    return Ok(json_frames);
79                } else {
80                    return Err(DefmtError::TcpError(err.to_string()));
81                }
82            }
83        };
84
85        stream_decoder.received(&buf[..n]);
86
87        // decode the received data
88        loop {
89            match stream_decoder.decode() {
90                Ok(frame) => {
91                    let json_frame = create_json_frame(workspace_root, &frame, &locs);
92
93                    let mod_path = if let Some(mod_path) = &json_frame.location.module_path {
94                        if mod_path.modules.is_empty() {
95                            Some(format!("{}::{}", mod_path.crate_name, mod_path.function))
96                        } else {
97                            Some(format!(
98                                "{}::{}::{}",
99                                mod_path.crate_name,
100                                mod_path.modules.join("::"),
101                                mod_path.function
102                            ))
103                        }
104                    } else {
105                        None
106                    };
107
108                    // use kv feature due to lifetime problems with arg
109                    let val = Some([("msg", log::kv::Value::from_display(&json_frame.data))]);
110
111                    match json_frame.level {
112                        Some(level) => {
113                            let log_record = log::RecordBuilder::new()
114                                .level(level)
115                                .file(json_frame.location.file.as_deref())
116                                .line(json_frame.location.line)
117                                .module_path(mod_path.as_deref())
118                                .target("embedded")
119                                .key_values(&val)
120                                .build();
121                            log::logger().log(&log_record);
122                        }
123                        None => {
124                            // mantra coverage logs not printed to remove clutter
125                            if mantra_rust_macros::extract::extract_first_coverage(&json_frame.data)
126                                .is_none()
127                            {
128                                println!("TARGET-PRINT | {}", json_frame.data);
129
130                                if log::Level::Trace <= log::STATIC_MAX_LEVEL
131                                    && log::Level::Trace <= log::max_level()
132                                {
133                                    let location = if json_frame.location.file.is_some()
134                                        && json_frame.location.line.is_some()
135                                        && mod_path.is_some()
136                                    {
137                                        format!(
138                                            "{} in {}:{}",
139                                            mod_path.unwrap(),
140                                            json_frame.location.file.as_ref().unwrap(),
141                                            json_frame.location.line.unwrap(),
142                                        )
143                                    } else {
144                                        "no-location info available".to_string()
145                                    };
146
147                                    println!("             | => {location}");
148                                }
149                            }
150                        }
151                    }
152
153                    json_frames.push(json_frame);
154                }
155                Err(DecodeError::UnexpectedEof) => break,
156                Err(DecodeError::Malformed) => match table.encoding().can_recover() {
157                    // if recovery is impossible, abort
158                    false => return Err(DefmtError::MalformedFrame),
159                    // if recovery is possible, skip the current frame and continue with new data
160                    true => {
161                        log::warn!("Malformed defmt frame skipped!");
162                        continue;
163                    }
164                },
165            }
166        }
167    }
168}
169
170pub type LocationInfo = (Option<String>, Option<u32>, Option<String>);
171
172pub fn location_info(
173    workspace_root: &Path,
174    frame: &Frame,
175    locs: &Option<Locations>,
176) -> LocationInfo {
177    let (mut file, mut line, mut mod_path) = (None, None, None);
178
179    let loc = locs.as_ref().map(|locs| locs.get(&frame.index()));
180
181    if let Some(Some(loc)) = loc {
182        // try to get the relative path from workspace root, else the full one
183        let path = mantra_lang_tracing::path::make_relative(&loc.file, workspace_root)
184            .unwrap_or(loc.file.to_path_buf());
185
186        file = Some(path.display().to_string());
187        line = Some(loc.line as u32);
188        mod_path = Some(loc.module.clone());
189    }
190
191    (file, line, mod_path)
192}
193
194/// Create a new [JsonFrame] from a log-frame from the target
195pub fn create_json_frame(
196    workspace_root: &Path,
197    frame: &Frame,
198    locs: &Option<Locations>,
199) -> JsonFrame {
200    let (file, line, mod_path) = location_info(workspace_root, frame, locs);
201    let host_timestamp = time::OffsetDateTime::now_utc()
202        .unix_timestamp_nanos()
203        .min(i64::MAX as i128) as i64;
204
205    JsonFrame {
206        data: frame.display_message().to_string(),
207        host_timestamp,
208        level: frame.level().map(to_json_level),
209        location: JsonLocation {
210            file,
211            line,
212            module_path: create_module_path(mod_path.as_deref()),
213        },
214        target_timestamp: frame
215            .display_timestamp()
216            .map(|ts| ts.to_string())
217            .unwrap_or_default(),
218    }
219}
220
221fn create_module_path(module_path: Option<&str>) -> Option<ModulePath> {
222    let mut path = module_path?.split("::").collect::<Vec<_>>();
223
224    // there need to be at least two elements, the crate and the function
225    if path.len() < 2 {
226        return None;
227    };
228
229    // the last element is the function
230    let function = path.pop()?.to_string();
231    // the first element is the crate_name
232    let crate_name = path.remove(0).to_string();
233
234    Some(ModulePath {
235        crate_name,
236        modules: path.into_iter().map(|a| a.to_string()).collect(),
237        function,
238    })
239}
240
241pub fn to_json_level(level: defmt_parser::Level) -> log::Level {
242    match level {
243        defmt_parser::Level::Trace => log::Level::Trace,
244        defmt_parser::Level::Debug => log::Level::Debug,
245        defmt_parser::Level::Info => log::Level::Info,
246        defmt_parser::Level::Warn => log::Level::Warn,
247        defmt_parser::Level::Error => log::Level::Error,
248    }
249}