// Session report: candump text -> ISO-TP -> UDS/OBD decoding.
pub(all) enum DiagEvent {
UdsEvent(Int, UdsMessage)
ObdEvent(Int, ObdMessage)
RawEvent(Int, Array[Int])
}
pub(all) struct DiagReport {
frame_count : Int
events : Array[DiagEvent]
errors : Array[String]
incomplete_ids : Array[Int]
negative_count : Int
dtc_count : Int
}
fn slice(bytes : Array[Int], start : Int, end : Int) -> Array[Int] {
let out : Array[Int] = Array::new()
for i in start.. String {
let out = StringBuilder()
for b in bytes {
if b >= 32 && b <= 126 {
out.write_char(Char::from_int(b))
} else {
out.write_char('.')
}
}
out.to_string()
}
fn describe_isotp_error(err : IsotpError) -> String {
match err {
EmptyFrame => "empty frame"
EmptyPayload => "empty payload"
InvalidFrameSize(n) => "invalid frame size " + n.to_string()
InvalidLength(n) => "invalid length " + n.to_string()
TooLarge(n) => "payload too large " + n.to_string()
UnexpectedFirstFrame => "unexpected first frame"
UnexpectedConsecutiveFrame => "unexpected consecutive frame"
UnexpectedSingleFrame => "unexpected single frame"
BadSequence(got, want) =>
"bad sequence " + got.to_string() + ", expected " + want.to_string()
Incomplete(got, want) =>
"incomplete " + got.to_string() + " of " + want.to_string()
}
}
fn classify(can_id : Int, payload : Array[Int]) -> DiagEvent {
if payload.length() == 0 {
return RawEvent(can_id, payload)
}
let sid = payload[0]
if sid == 0x41 || sid == 0x43 || sid == 0x47 || sid == 0x4a {
ObdEvent(can_id, parse_obd_message(payload))
} else if sid == 0x7f || (sid >= 0x40 && sid <= 0x7e) {
UdsEvent(can_id, parse_uds_message(payload))
} else {
RawEvent(can_id, payload)
}
}
pub fn analyze_log(text : String) -> DiagReport {
let frames = parse_candump(text)
let events : Array[DiagEvent] = Array::new()
let errors : Array[String] = Array::new()
let incomplete : Array[Int] = Array::new()
let ids : Array[Int] = Array::new()
for frame in frames {
let mut known = false
for id in ids {
if id == frame.id {
known = true
break
}
}
if !known {
ids.push(frame.id)
}
}
for id in ids {
let mut reasm = IsotpReassembler::new()
for frame in frames {
if frame.id == id {
let result = try {
reasm.push(frame.data)
} catch {
err => {
errors.push(
"CAN 0x" + hex_can_id(id) + ": " + describe_isotp_error(err),
)
reasm = IsotpReassembler::new()
None
}
}
match result {
Some(payload) => events.push(classify(id, payload))
None => ()
}
}
}
if reasm.is_active() {
incomplete.push(id)
}
}
let mut negative = 0
let mut dtcs = 0
for event in events {
match event {
UdsEvent(_, message) =>
match message {
Negative(_, _, _) => negative = negative + 1
Positive(sid, data) =>
if sid == 0x59 && data.length() >= 2 {
let full : Array[Int] = [0x59]
for b in data {
full.push(b)
}
match parse_dtc_response(full) {
Some(list) => dtcs = dtcs + list.length()
None => ()
}
}
Malformed(_) => ()
}
ObdEvent(_, message) =>
match message {
DtcResponse(_, list) => dtcs = dtcs + list.length()
_ => ()
}
RawEvent(_, _) => ()
}
}
{
frame_count: frames.length(),
events,
errors,
incomplete_ids: incomplete,
negative_count: negative,
dtc_count: dtcs,
}
}
fn describe_uds(can_id : Int, message : UdsMessage) -> String {
let head = "CAN 0x" + hex_can_id(can_id) + " UDS "
match message {
Positive(sid, data) => {
let service = sid - 0x40
let name = uds_service_name(service)
if service == 0x22 && data.length() >= 2 {
let full : Array[Int] = [sid]
for b in data {
full.push(b)
}
match parse_did_response(full) {
Some((did, value)) =>
head + name + " DID 0x" + hex_int(did, 4) + " = \"" + ascii(value) +
"\" [" + hex_encode(value) + "]"
None => head + name
}
} else if service == 0x19 {
let full : Array[Int] = [sid]
for b in data {
full.push(b)
}
match parse_dtc_response(full) {
Some(list) => {
let out = StringBuilder()
out.write_string(head + name + " DTCs:")
for d in list {
out.write_string(" " + d.code)
}
out.to_string()
}
None => head + name + " (" + data.length().to_string() + " bytes)"
}
} else {
head + name + " [" + hex_encode(data) + "]"
}
}
Negative(service, nrc, reason) =>
head + "Negative 0x" + hex_int(service, 2) + " NRC 0x" + hex_int(nrc, 2) +
" " + reason
Malformed(reason) => head + "Malformed: " + reason
}
}
fn describe_obd(can_id : Int, message : ObdMessage) -> String {
let head = "CAN 0x" + hex_can_id(can_id) + " OBD "
match message {
Mode01(pid, bytes) =>
match decode_obd_pid(pid, slice(bytes, 2, bytes.length())) {
Some(r) =>
head + "PID 0x" + hex_int(pid, 2) + " " + r.label + " = " + r.value +
(if r.unit.length() > 0 { " " + r.unit } else { "" })
None => head + "PID 0x" + hex_int(pid, 2) + " unsupported"
}
DtcResponse(mode, dtcs) => {
let out = StringBuilder()
out.write_string(head + "mode 0x" + hex_int(mode, 2) + " DTCs:")
for d in dtcs {
out.write_string(" " + d.code)
}
out.to_string()
}
Unknown(bytes) => head + "unrecognized [" + hex_encode(bytes) + "]"
}
}
pub fn render_report(report : DiagReport) -> String {
let out = StringBuilder()
out.write_string("frames: " + report.frame_count.to_string() + "\n")
out.write_string("messages: " + report.events.length().to_string() + "\n")
out.write_string("negative responses: " + report.negative_count.to_string() + "\n")
out.write_string("DTCs: " + report.dtc_count.to_string() + "\n")
if report.incomplete_ids.length() > 0 {
out.write_string("incomplete ISO-TP ids:")
for id in report.incomplete_ids {
out.write_string(" 0x" + hex_can_id(id))
}
out.write_string("\n")
}
for event in report.events {
match event {
UdsEvent(id, message) => out.write_string(describe_uds(id, message) + "\n")
ObdEvent(id, message) => out.write_string(describe_obd(id, message) + "\n")
RawEvent(id, payload) =>
out.write_string(
"CAN 0x" + hex_can_id(id) + " raw [" + hex_encode(payload) + "]\n",
)
}
}
for error in report.errors {
out.write_string("error: " + error + "\n")
}
out.to_string()
}
fn json_escape(text : String) -> String {
let out = StringBuilder()
for i in 0..> 4))
out.write_char(hex_nibble_char(c))
} else {
out.write_char(Char::from_int(c))
}
}
out.to_string()
}
fn json_string(text : String) -> String {
"\"" + json_escape(text) + "\""
}
fn quoted_array(items : Array[String]) -> String {
"[" + items.join(",") + "]"
}
fn dtc_json(list : Array[Dtc]) -> String {
let items : Array[String] = Array::new()
for d in list {
items.push(
"{\"code\":" + json_string(d.code) + ",\"failureType\":" +
json_string("0x" + hex_int(d.failure_type, 2)) + ",\"status\":" +
json_string("0x" + hex_int(d.status, 2)) + "}",
)
}
quoted_array(items)
}
fn uds_event_json(can_id : Int, message : UdsMessage) -> String {
let can = json_string("0x" + hex_can_id(can_id))
match message {
Positive(sid, data) => {
let service = sid - 0x40
let fields : Array[String] = [
"\"canId\":" + can,
"\"kind\":\"uds\"",
"\"positive\":true",
"\"service\":" + json_string("0x" + hex_int(service, 2)),
"\"serviceName\":" + json_string(uds_service_name(service)),
"\"data\":" + json_string(hex_encode(data)),
]
if service == 0x22 && data.length() >= 2 {
let full : Array[Int] = [sid]
for b in data {
full.push(b)
}
match parse_did_response(full) {
Some((did, value)) => {
fields.push("\"did\":" + json_string("0x" + hex_int(did, 4)))
fields.push("\"value\":" + json_string(hex_encode(value)))
fields.push("\"ascii\":" + json_string(ascii(value)))
}
None => ()
}
} else if service == 0x19 {
let full : Array[Int] = [sid]
for b in data {
full.push(b)
}
match parse_dtc_response(full) {
Some(list) => fields.push("\"dtcs\":" + dtc_json(list))
None => ()
}
}
"{" + fields.join(",") + "}"
}
Negative(service, nrc, reason) => {
let fields : Array[String] = [
"\"canId\":" + can,
"\"kind\":\"uds\"",
"\"positive\":false",
"\"service\":" + json_string("0x" + hex_int(service, 2)),
"\"nrc\":" + json_string("0x" + hex_int(nrc, 2)),
"\"reason\":" + json_string(reason),
]
"{" + fields.join(",") + "}"
}
Malformed(reason) =>
"{\"canId\":" + can + ",\"kind\":\"uds\",\"malformed\":" +
json_string(reason) + "}"
}
}
fn obd_event_json(can_id : Int, message : ObdMessage) -> String {
let can = json_string("0x" + hex_can_id(can_id))
match message {
Mode01(pid, bytes) => {
let data = slice(bytes, 2, bytes.length())
match decode_obd_pid(pid, data) {
Some(r) =>
"{\"canId\":" + can + ",\"kind\":\"obd\",\"mode\":\"0x41\"," +
"\"pid\":" + json_string("0x" + hex_int(pid, 2)) + ",\"label\":" +
json_string(r.label) + ",\"value\":" + json_string(r.value) +
",\"unit\":" + json_string(r.unit) + "}"
None =>
"{\"canId\":" + can + ",\"kind\":\"obd\",\"mode\":\"0x41\"," +
"\"pid\":" + json_string("0x" + hex_int(pid, 2)) +
",\"supported\":false}"
}
}
DtcResponse(mode, dtcs) =>
"{\"canId\":" + can + ",\"kind\":\"obd\",\"mode\":" +
json_string("0x" + hex_int(mode, 2)) + ",\"dtcs\":" + dtc_json(dtcs) +
"}"
Unknown(bytes) =>
"{\"canId\":" + can + ",\"kind\":\"obd\",\"unknown\":" +
json_string(hex_encode(bytes)) + "}"
}
}
pub fn render_json(report : DiagReport) -> String {
let events : Array[String] = Array::new()
for event in report.events {
match event {
UdsEvent(id, message) => events.push(uds_event_json(id, message))
ObdEvent(id, message) => events.push(obd_event_json(id, message))
RawEvent(id, payload) =>
events.push(
"{\"canId\":" + json_string("0x" + hex_can_id(id)) +
",\"kind\":\"raw\",\"payload\":" + json_string(hex_encode(payload)) +
"}",
)
}
}
let incomplete : Array[String] = Array::new()
for id in report.incomplete_ids {
incomplete.push(json_string("0x" + hex_can_id(id)))
}
let errors : Array[String] = Array::new()
for error in report.errors {
errors.push(json_string(error))
}
"{\"frames\":" + report.frame_count.to_string() + ",\"messages\":" +
report.events.length().to_string() + ",\"negativeResponses\":" +
report.negative_count.to_string() + ",\"dtcs\":" +
report.dtc_count.to_string() + ",\"incompleteIsoTpIds\":" +
quoted_array(incomplete) + ",\"events\":[" + events.join(",") +
"],\"errors\":" + quoted_array(errors) + "}"
}
fn csv_field(text : String) -> String {
let out = StringBuilder()
out.write_char('"')
for i in 0.. String {
let out = StringBuilder()
out.write_string("can_id,kind,detail\n")
for event in report.events {
match event {
UdsEvent(id, message) =>
out.write_string(
"0x" + hex_can_id(id) + ",uds," +
csv_field(describe_uds(id, message)) + "\n",
)
ObdEvent(id, message) =>
out.write_string(
"0x" + hex_can_id(id) + ",obd," +
csv_field(describe_obd(id, message)) + "\n",
)
RawEvent(id, payload) =>
out.write_string(
"0x" + hex_can_id(id) + ",raw," + csv_field(hex_encode(payload)) +
"\n",
)
}
}
out.to_string()
}
pub(all) struct IdSummary {
can_id : Int
messages : Int
negatives : Int
}
pub fn summarize_by_id(report : DiagReport) -> Array[IdSummary] {
let ids : Array[Int] = Array::new()
let counts : Array[Int] = Array::new()
let negatives : Array[Int] = Array::new()
for event in report.events {
let id = match event {
UdsEvent(id, _) => id
ObdEvent(id, _) => id
RawEvent(id, _) => id
}
let mut slot = -1
for i in 0..
negatives[slot] = negatives[slot] + 1
_ => ()
}
}
let out : Array[IdSummary] = Array::new()
for i in 0..