///|
/// A compact CAN frame with optional candump timestamp and interface metadata.
pub(all) struct CanLogRecord {
timestamp : String?
interface_name : String?
/// `None` denotes Classical CAN; `Some(flags)` denotes CAN FD.
fd_flags : Byte?
frame : CanFrame
} derive(Eq, Debug)
///|
pub(all) enum CanLogTextError {
CanLogInvalidFieldCount(Int)
CanLogInvalidTimestamp(String)
CanLogInvalidInterface(String)
CanLogMissingFdFlags
CanLogInvalidFdFlags(String)
CanLogFdFlagsOutOfRange(Int)
CanLogTimestampWithoutInterface
CanLogFrameTextError(CanFrameTextError)
} derive(Eq, Debug)
///|
fn can_log_fields(text : StringView) -> Array[String] {
let fields : Array[String] = []
let current : Array[Char] = []
for char in text.iter() {
if char == ' ' || char == '\t' || char == '\r' || char == '\n' {
if !current.is_empty() {
fields.push(String::from_array(current))
current.clear()
}
} else {
current.push(char)
}
}
if !current.is_empty() {
fields.push(String::from_array(current))
}
fields
}
///|
fn can_log_timestamp_is_valid(timestamp : StringView) -> Bool {
if timestamp.is_empty() {
return false
}
match parse_double(timestamp) {
Some(value) => !value.is_nan() && !value.is_inf()
None => false
}
}
///|
fn parse_can_log_timestamp(text : StringView) -> String? {
if text.length() < 3 || !text.has_prefix("(") || !text.has_suffix(")") {
return None
}
let timestamp = text[1:text.length() - 1]
if can_log_timestamp_is_valid(timestamp) {
Some(timestamp.to_owned())
} else {
None
}
}
///|
fn can_log_interface_is_valid(interface_name : StringView) -> Bool {
if interface_name.is_empty() {
return false
}
for char in interface_name.iter() {
let valid = (char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '_' ||
char == '-' ||
char == '.' ||
char == ':' ||
char == '@'
if !valid {
return false
}
}
true
}
///|
fn parse_can_log_frame(
text : StringView,
) -> Result[(CanFrame, Byte?), CanLogTextError] {
match text.find("##") {
None =>
match parse_can_frame(text) {
Ok(frame) => Ok((frame, None))
Err(error) => Err(CanLogFrameTextError(error))
}
Some(separator) => {
let flags_start = separator + 2
if flags_start >= text.length() {
return Err(CanLogMissingFdFlags)
}
let flags_text = text[flags_start:flags_start + 1]
let mut flags : Int? = None
for char in flags_text.iter() {
flags = can_hex_nibble(char)
}
let flags = match flags {
Some(value) => value.to_byte()
None => return Err(CanLogInvalidFdFlags(flags_text.to_owned()))
}
let classical_text = "\{text[:separator]}#\{text[flags_start + 1:]}"
match parse_can_frame(classical_text) {
Ok(frame) => Ok((frame, Some(flags)))
Err(error) => Err(CanLogFrameTextError(error))
}
}
}
}
///|
fn can_log_frame_to_text(
frame : CanFrame,
fd_flags : Byte?,
) -> Result[String, CanLogTextError] {
let frame_text = match frame.to_text() {
Ok(value) => value
Err(error) => return Err(CanLogFrameTextError(error))
}
match fd_flags {
None => Ok(frame_text)
Some(flags) => {
let value = flags.to_int()
if value > 0xF {
return Err(CanLogFdFlagsOutOfRange(value))
}
let separator = frame_text.find("#").unwrap()
Ok(
"\{frame_text[:separator]}##\{can_hex_digit(value)}\{frame_text[separator + 1:]}",
)
}
}
}
///|
/// Parse compact SocketCAN text, `INTERFACE ID#DATA`, or the common candump
/// form `(TIMESTAMP) INTERFACE ID#DATA`. CAN FD uses `ID##FLAGS+DATA` with one
/// hexadecimal flags digit. Timestamp text is preserved without losing precision.
pub fn parse_can_log_record(
text : StringView,
) -> Result[CanLogRecord, CanLogTextError] {
let fields = can_log_fields(text)
match fields {
[frame_text] => {
let (frame, fd_flags) = match parse_can_log_frame(frame_text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({ timestamp: None, interface_name: None, fd_flags, frame, })
}
[interface_name, frame_text] => {
if parse_can_log_timestamp(interface_name) is Some(_) {
return Err(CanLogTimestampWithoutInterface)
}
if !can_log_interface_is_valid(interface_name) {
return Err(CanLogInvalidInterface(interface_name))
}
let (frame, fd_flags) = match parse_can_log_frame(frame_text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({
timestamp: None,
interface_name: Some(interface_name),
fd_flags,
frame,
})
}
[timestamp_text, interface_name, frame_text] => {
let timestamp = match parse_can_log_timestamp(timestamp_text) {
Some(value) => value
None => return Err(CanLogInvalidTimestamp(timestamp_text))
}
if !can_log_interface_is_valid(interface_name) {
return Err(CanLogInvalidInterface(interface_name))
}
let (frame, fd_flags) = match parse_can_log_frame(frame_text) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok({
timestamp: Some(timestamp),
interface_name: Some(interface_name),
fd_flags,
frame,
})
}
_ => Err(CanLogInvalidFieldCount(fields.length()))
}
}
///|
/// Render a log record using the shortest candump form that preserves its
/// timestamp and interface metadata.
pub fn CanLogRecord::to_text(
self : CanLogRecord,
) -> Result[String, CanLogTextError] {
let frame_text = match can_log_frame_to_text(self.frame, self.fd_flags) {
Ok(value) => value
Err(error) => return Err(error)
}
match (self.timestamp, self.interface_name) {
(None, None) => Ok(frame_text)
(None, Some(interface_name)) =>
if can_log_interface_is_valid(interface_name) {
Ok("\{interface_name} \{frame_text}")
} else {
Err(CanLogInvalidInterface(interface_name))
}
(Some(_), None) => Err(CanLogTimestampWithoutInterface)
(Some(timestamp), Some(interface_name)) => {
if !can_log_timestamp_is_valid(timestamp) {
return Err(CanLogInvalidTimestamp(timestamp))
}
if !can_log_interface_is_valid(interface_name) {
return Err(CanLogInvalidInterface(interface_name))
}
Ok("(\{timestamp}) \{interface_name} \{frame_text}")
}
}
}