///|
pub(all) enum TraceError {
TraceFrameTextError(CanFrameTextError)
TraceCanLogTextError(CanLogTextError)
TraceFrameDecodeError(FrameDecodeError)
} derive(Eq, Debug)
///|
pub(all) struct TraceDiagnostic {
line : Int
text : String
error : TraceError
} derive(Eq, Debug)
///|
pub(all) struct DecodedTraceFrame {
line : Int
timestamp : String?
interface_name : String?
fd_flags : Byte?
frame : CanFrame
decoded : DecodedMessage
} derive(Eq, Debug)
///|
pub(all) struct TraceDecodeResult {
frames : Array[DecodedTraceFrame]
diagnostics : Array[TraceDiagnostic]
} derive(Eq, Debug)
///|
/// Counts accepted records and the reasons other records could not be decoded.
/// Unknown identifiers are kept separate because partial DBC coverage is normal.
pub(all) struct TraceSummary {
records : Int
decoded_frames : Int
decoded_signals : Int
unknown_frames : Int
malformed_records : Int
short_payload_frames : Int
failed_frames : Int
} derive(Eq, Debug)
///|
pub fn TraceDecodeResult::summary(self : TraceDecodeResult) -> TraceSummary {
let mut decoded_signals = 0
for frame in self.frames {
decoded_signals += frame.decoded.signals.length()
}
let mut unknown_frames = 0
let mut malformed_records = 0
let mut short_payload_frames = 0
let mut failed_frames = 0
for diagnostic in self.diagnostics {
match diagnostic.error {
TraceFrameTextError(_) | TraceCanLogTextError(_) => malformed_records += 1
TraceFrameDecodeError(UnknownCanMessage(_, _) | UnknownMessage(_)) =>
unknown_frames += 1
TraceFrameDecodeError(PayloadTooShort(_, _)) => short_payload_frames += 1
TraceFrameDecodeError(_) => failed_frames += 1
}
}
{
records: self.frames.length() + self.diagnostics.length(),
decoded_frames: self.frames.length(),
decoded_signals,
unknown_frames,
malformed_records,
short_payload_frames,
failed_frames,
}
}
///|
pub fn TraceDecodeResult::is_valid(self : TraceDecodeResult) -> Bool {
self.diagnostics.is_empty()
}
///|
/// Decode compact SocketCAN frames or candump records line by line. Blank lines
/// and lines starting with `//` are ignored. Invalid or undecodable lines are
/// retained as diagnostics without preventing later records from being processed.
pub fn Database::decode_trace(
self : Database,
source : StringView,
) -> TraceDecodeResult {
let frames : Array[DecodedTraceFrame] = []
let diagnostics : Array[TraceDiagnostic] = []
let mut line_number = 0
for raw_line in source.split("\n") {
line_number += 1
let line = raw_line.trim(chars=" \t\r")
if line.is_empty() || line.has_prefix("//") {
continue
}
match parse_can_log_record(line) {
Err(CanLogFrameTextError(error)) =>
diagnostics.push({
line: line_number,
text: line.to_owned(),
error: TraceFrameTextError(error),
})
Err(error) =>
diagnostics.push({
line: line_number,
text: line.to_owned(),
error: TraceCanLogTextError(error),
})
Ok(record) =>
match self.decode_can_frame(record.frame) {
Err(error) =>
diagnostics.push({
line: line_number,
text: line.to_owned(),
error: TraceFrameDecodeError(error),
})
Ok(decoded) =>
frames.push({
line: line_number,
timestamp: record.timestamp,
interface_name: record.interface_name,
fd_flags: record.fd_flags,
frame: record.frame,
decoded,
})
}
}
}
{ frames, diagnostics, }
}
///|
fn trace_csv_text(value : StringView) -> String {
let chars : Array[Char] = ['"']
for char in value.iter() {
if char == '"' {
chars.push('"')
}
chars.push(char)
}
chars.push('"')
String::from_array(chars)
}
///|
fn trace_raw_text(raw : RawValue) -> String {
match raw {
UnsignedRaw(value) => value.to_string()
SignedRaw(value) => value.to_string()
FloatRaw(value) => value.to_string()
}
}
///|
fn trace_label_text(label : String?) -> String {
match label {
Some(value) => value
None => ""
}
}
///|
fn trace_optional_text(value : String?) -> String {
match value {
Some(text) => text
None => ""
}
}
///|
fn trace_fd_flags_text(value : Byte?) -> String {
match value {
Some(flags) => flags.to_int().to_string(radix=16).to_upper()
None => ""
}
}
///|
/// Export successfully decoded trace signals as RFC 4180-style CSV rows.
/// Text fields are always quoted and embedded quotes are doubled.
pub fn TraceDecodeResult::to_csv(
self : TraceDecodeResult,
) -> Result[String, CanLogTextError] {
let lines = [
"line,timestamp,interface,fd_flags,frame,message,signal,raw,physical,unit,label",
]
for record in self.frames {
let frame_text = match
can_log_frame_to_text(record.frame, record.fd_flags) {
Ok(value) => value
Err(error) => return Err(error)
}
for signal in record.decoded.signals {
lines.push(
"\{record.line},\{trace_csv_text(trace_optional_text(record.timestamp))},\{trace_csv_text(trace_optional_text(record.interface_name))},\{trace_fd_flags_text(record.fd_flags)},\{frame_text},\{trace_csv_text(record.decoded.name)},\{trace_csv_text(signal.name)},\{trace_raw_text(signal.raw)},\{signal.physical},\{trace_csv_text(signal.unit)},\{trace_csv_text(trace_label_text(signal.label))}",
)
}
}
Ok(lines.join("\r\n"))
}