///|
/// Stable output formats for trace artifacts.
pub enum TraceExportFormat {
TraceExportCanText
TraceExportCsv
TraceExportSummary
TraceExportReplayScript
}
///|
pub fn trace_export_format_variants() -> Array[TraceExportFormat] {
[
TraceExportCanText,
TraceExportCsv,
TraceExportSummary,
TraceExportReplayScript,
]
}
///|
/// Options controlling deterministic trace export.
pub struct TraceExportOptions {
format : TraceExportFormat
include_channel : Bool
include_sequence : Bool
include_header : Bool
normalize_timestamps : Bool
max_rows : Int
}
///|
pub fn trace_export_options(
format? : TraceExportFormat = TraceExportCanText,
include_channel? : Bool = true,
include_sequence? : Bool = true,
include_header? : Bool = true,
normalize_timestamps? : Bool = false,
max_rows? : Int = 0,
) -> TraceExportOptions {
{
format,
include_channel,
include_sequence,
include_header,
normalize_timestamps,
max_rows,
}
}
///|
pub fn TraceExportOptions::format(
self : TraceExportOptions,
) -> TraceExportFormat {
self.format
}
///|
pub fn TraceExportOptions::include_channel(self : TraceExportOptions) -> Bool {
self.include_channel
}
///|
pub fn TraceExportOptions::include_sequence(self : TraceExportOptions) -> Bool {
self.include_sequence
}
///|
pub fn TraceExportOptions::include_header(self : TraceExportOptions) -> Bool {
self.include_header
}
///|
pub fn TraceExportOptions::normalize_timestamps(
self : TraceExportOptions,
) -> Bool {
self.normalize_timestamps
}
///|
pub fn TraceExportOptions::max_rows(self : TraceExportOptions) -> Int {
self.max_rows
}
///|
/// A line-level export record used by CSV and replay tools.
pub struct TraceExportRecord {
timestamp_us : UInt64
sequence : Int
channel : String
frame : Frame
}
///|
pub fn trace_export_record(
timestamp_us : UInt64,
sequence : Int,
channel : String,
frame : Frame,
) -> TraceExportRecord {
{ timestamp_us, sequence, channel, frame }
}
///|
pub fn TraceExportRecord::timestamp_us(self : TraceExportRecord) -> UInt64 {
self.timestamp_us
}
///|
pub fn TraceExportRecord::sequence(self : TraceExportRecord) -> Int {
self.sequence
}
///|
pub fn TraceExportRecord::channel(self : TraceExportRecord) -> String {
self.channel
}
///|
pub fn TraceExportRecord::frame(self : TraceExportRecord) -> Frame {
self.frame
}
///|
pub fn TraceExportRecord::relative_to(
self : TraceExportRecord,
base : UInt64,
) -> UInt64 {
if self.timestamp_us >= base {
self.timestamp_us - base
} else {
0
}
}
///|
/// Convert a trace into ordered export records.
pub fn trace_export_records(
trace : Trace,
channel? : String = "can0",
normalize_timestamps? : Bool = false,
) -> Array[TraceExportRecord] {
let entries = trace.entries()
let base : UInt64 = if entries.is_empty() {
0
} else {
entries[0].timestamp()
}
let result : Array[TraceExportRecord] = []
for index, entry in entries {
let timestamp = if normalize_timestamps {
entry.timestamp() - base
} else {
entry.timestamp()
}
result.push(trace_export_record(timestamp, index, channel, entry.frame()))
}
result
}
///|
/// A stateful exporter that can be used incrementally by a logger.
pub struct TraceExporter {
options : TraceExportOptions
records : Array[TraceExportRecord]
mut rows : Int
mut bytes : Int
mut errors : Int
}
///|
pub fn new_trace_exporter(
options? : TraceExportOptions = trace_export_options(),
) -> TraceExporter {
{ options, records: [], rows: 0, bytes: 0, errors: 0 }
}
///|
pub fn TraceExporter::add(
self : TraceExporter,
record : TraceExportRecord,
) -> Bool {
if self.options.max_rows() > 0 &&
self.records.length() >= self.options.max_rows() {
self.errors += 1
false
} else {
self.records.push(record)
self.rows += 1
true
}
}
///|
pub fn TraceExporter::add_trace(
self : TraceExporter,
trace : Trace,
channel? : String = "can0",
) -> Int {
let records = trace_export_records(
trace,
channel~,
normalize_timestamps=self.options.normalize_timestamps(),
)
let mut added = 0
for record in records {
if self.add(record) {
added += 1
}
}
added
}
///|
pub fn TraceExporter::records(self : TraceExporter) -> Array[TraceExportRecord] {
self.records.copy()
}
///|
pub fn TraceExporter::rows(self : TraceExporter) -> Int {
self.rows
}
///|
pub fn TraceExporter::bytes(self : TraceExporter) -> Int {
self.bytes
}
///|
pub fn TraceExporter::errors(self : TraceExporter) -> Int {
self.errors
}
///|
pub fn TraceExporter::render(self : TraceExporter) -> String {
let output = match self.options.format() {
TraceExportCanText => trace_export_can_text(self.records, self.options)
TraceExportCsv => trace_export_csv(self.records, self.options)
TraceExportSummary => trace_export_summary(self.records)
TraceExportReplayScript => trace_export_replay_script(self.records)
}
self.bytes = output.length()
output
}
///|
pub fn trace_export_can_text(
records : Array[TraceExportRecord],
options : TraceExportOptions,
) -> String {
let lines : Array[String] = []
for record in records {
let prefix = record.timestamp_us().to_string() + " "
let channel = if options.include_channel() {
record.channel() + " "
} else {
""
}
lines.push(
prefix + channel + can_line(record.timestamp_us(), record.frame()),
)
}
lines.join("\n")
}
///|
pub fn trace_export_csv(
records : Array[TraceExportRecord],
options : TraceExportOptions,
) -> String {
let lines : Array[String] = []
if options.include_header() {
let header = if options.include_channel() {
"timestamp_us,sequence,channel,id,extended,data"
} else {
"timestamp_us,sequence,id,extended,data"
}
lines.push(header)
}
for record in records {
let data = frame_to_hex(record.frame())
let channel = if options.include_channel() {
"," + record.channel()
} else {
""
}
lines.push(
record.timestamp_us().to_string() +
"," +
record.sequence().to_string() +
channel +
"," +
record.frame().id().to_string() +
"," +
record.frame().is_extended().to_string() +
"," +
data,
)
}
lines.join("\n")
}
///|
pub fn trace_export_summary(records : Array[TraceExportRecord]) -> String {
let frames : Array[Frame] = []
for record in records {
frames.push(record.frame())
}
let metrics = frame_metrics(frames)
metrics.to_text()
}
///|
pub fn trace_export_replay_script(records : Array[TraceExportRecord]) -> String {
let lines : Array[String] = ["# moonbit-canbus replay script"]
for record in records {
lines.push(
"at " +
record.timestamp_us().to_string() +
" us: id=" +
record.frame().id().to_string() +
" data=" +
frame_to_hex(record.frame()),
)
}
lines.join("\n")
}
///|
/// Parse a compact CAN-text export generated by this package.
pub fn trace_import_can_text(text : String) -> Trace raise CanTextError {
let trace = new_trace()
for raw in text.split("\n") {
let line = raw.trim()
if line.is_empty() || line.has_prefix("#") {
continue
}
let parts : Array[String] = []
for part in line.split(" ") {
if !part.is_empty() {
parts.push(part.to_owned())
}
}
if parts.length() < 2 {
continue
}
let timestamp : UInt64 = @strconv.from_str(parts[0]) catch {
_ => raise CanTextError::InvalidTimestamp
}
let parsed = parse_can_line(parts[parts.length() - 1]) catch {
_ => raise CanTextError::InvalidPayload
}
trace.record(timestamp, parsed.1)
}
trace
}
///|
/// Return the earliest and latest timestamps in an export.
pub fn trace_export_time_range(
records : Array[TraceExportRecord],
) -> (UInt64, UInt64)? {
if records.is_empty() {
None
} else {
let mut start = records[0].timestamp_us()
let mut end = start
for record in records {
if record.timestamp_us() < start {
start = record.timestamp_us()
}
if record.timestamp_us() > end {
end = record.timestamp_us()
}
}
Some((start, end))
}
}
///|
/// Return records belonging to one identifier.
pub fn trace_export_filter_id(
records : Array[TraceExportRecord],
identifier : UInt,
) -> Array[TraceExportRecord] {
records.filter(record => record.frame().id() == identifier)
}
///|
/// Return records in a closed time interval.
pub fn trace_export_between(
records : Array[TraceExportRecord],
start_us : UInt64,
end_us : UInt64,
) -> Array[TraceExportRecord] {
records.filter(record => {
record.timestamp_us() >= start_us && record.timestamp_us() <= end_us
})
}
///|
/// Calculate inter-arrival times for one identifier.
pub fn trace_export_intervals(
records : Array[TraceExportRecord],
identifier : UInt,
) -> Array[UInt64] {
let selected = trace_export_filter_id(records, identifier)
let result : Array[UInt64] = []
for index in 1.. Array[TraceExportRecord] {
if stride <= 1 {
records.copy()
} else {
let result : Array[TraceExportRecord] = []
for index, record in records {
if index % stride == 0 {
result.push(record)
}
}
result
}
}
///|
/// Compare two export streams and return a compact diff count.
pub fn trace_export_difference_count(
left : Array[TraceExportRecord],
right : Array[TraceExportRecord],
) -> Int {
let total = if left.length() > right.length() {
left.length()
} else {
right.length()
}
let mut differences = 0
for index in 0..= left.length() || index >= right.length() {
differences += 1
} else if left[index].frame().id() != right[index].frame().id() ||
left[index].frame().data() != right[index].frame().data() {
differences += 1
}
}
differences
}