Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 44 additions & 12 deletions src/main/transport/asc.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
import { CanMessage } from '../share/can'
import { CAN_ID_TYPE, CanMessage } from '../share/can'
import winston, { format } from 'winston'
import Transport from 'winston-transport'
import { getCheckSum, LinChecksumType, LinDirection, LinError, LinMsg } from '../share/lin'

/** SocketCAN / Vector BLF extended-frame flag in a 32-bit CAN ID */
const CAN_EFF_FLAG = 0x80000000
/** 29-bit CAN identifier mask */
const CAN_EFF_MASK = 0x1fffffff

/**
* Whether a CAN frame should be logged as an extended identifier in Vector ASC.
*
* Vector's CAN_LOG_TRIGGER_ASC_Format marks 29-bit IDs with a trailing `x`
* (e.g. `54C5638x`). An 11-bit standard ID cannot exceed 0x7FF, so IDs above
* that range are treated as extended even if `idType` was not set correctly.
*/
export function isExtendedCanId(msg: CanMessage): boolean {
const idType = msg.msgType?.idType
if (idType === CAN_ID_TYPE.EXTENDED) {
return true
}
const rawId = Number(msg.id) || 0
if ((rawId & CAN_EFF_FLAG) !== 0) {
return true
}
return (rawId & CAN_EFF_MASK) > 0x7ff
}

/**
* Format a CAN arbitration ID for Vector ASC numeric logging.
*
* @returns hex ID, with a trailing `x` for extended frames (CANoe/CANalyzer requirement)
*/
export function formatAscArbitrationId(msg: CanMessage): string {
const canId = (Number(msg.id) || 0) & CAN_EFF_MASK
const hex = canId.toString(16).toUpperCase()
return isExtendedCanId(msg) ? `${hex}x` : hex
}

// LogData interface matching the one from trace.vue

// Extended winston info interface
Expand Down Expand Up @@ -166,20 +201,14 @@ function formatLinMessage(msg: LinMsg, channel: number, timestamp: number): stri
return `${timestamp.toFixed(6)} ${channelStr} ${idHex.padStart(2)} ${dir} ${dlc} ${dataHex.padEnd(24)} checksum = ${checksum} CSM = ${csm}`
}

function formatCanMessage(msg: CanMessage, channel: number, timestamp: number): string {
export function formatCanMessage(msg: CanMessage, channel: number, timestamp: number): string {
const dir = msg.dir === 'OUT' ? 'Tx' : 'Rx'

// Format arbitration ID with extended ID handling
let arbId = msg.id.toString(16).toUpperCase()
if (msg.msgType.idType === 'EXTENDED') {
arbId += 'x'
}
// Vector ASC: extended IDs are hex plus a trailing `x` (CAN_LOG_TRIGGER_ASC_Format)
const arbId = formatAscArbitrationId(msg)

if (msg.msgType.canfd) {
// CAN FD format
return formatCanFdMessage(msg, channel, timestamp, dir, arbId)
} else {
// Classic CAN format
return formatClassicCanMessage(msg, channel, timestamp, dir, arbId)
}
}
Expand All @@ -193,15 +222,18 @@ function formatClassicCanMessage(
): string {
const dlc = msg.data.length

// Vector ASC numeric ID field is 15 characters (14 + trailing `x` for extended)
const idField = arbId.padEnd(15)

if (msg.msgType.remote) {
// Remote frame format: <Time> <Channel> <ID> <Dir> r <DLC>
return `${timestamp.toFixed(6)} ${channel} ${arbId} ${dir} r ${dlc.toString(16)}`
return `${timestamp.toFixed(6)} ${channel} ${idField} ${dir.padEnd(4)} r ${dlc.toString(16)}`
} else {
// Data frame format: <Time> <Channel> <ID> <Dir> d <DLC> <D0> <D1>...<D7>
const dataHex = Array.from(msg.data)
.map((byte) => byte.toString(16).toUpperCase().padStart(2, '0'))
.join(' ')
return `${timestamp.toFixed(6)} ${channel} ${arbId} ${dir} d ${dlc.toString(16)} ${dataHex}`
return `${timestamp.toFixed(6)} ${channel} ${idField} ${dir.padEnd(4)} d ${dlc.toString(16)} ${dataHex}`
}
}

Expand Down
125 changes: 125 additions & 0 deletions test/transport/asc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { describe, expect, it } from 'vitest'
import { CAN_ID_TYPE, type CanMessage } from '../../src/main/share/can'
import { AscReader } from '../../src/main/replay/ascReader'
import {
formatAscArbitrationId,
formatCanMessage,
isExtendedCanId
} from '../../src/main/transport/asc'

function createCanMessage(overrides: Partial<CanMessage> & Pick<CanMessage, 'id'>): CanMessage {
return {
data: Buffer.from([0x03, 0x22, 0x56, 0x78, 0xcc, 0xcc, 0xcc, 0xcc]),
dir: 'IN',
ts: 1_000_000,
...overrides,
msgType: {
idType: CAN_ID_TYPE.STANDARD,
canfd: false,
brs: false,
remote: false,
...overrides.msgType
}
}
}

describe('ASC extended ID formatting', () => {
it('appends x for explicit extended frames, including 11-bit values', () => {
const msg = createCanMessage({
id: 0x123,
msgType: { idType: CAN_ID_TYPE.EXTENDED, canfd: false, brs: false, remote: false }
})
expect(isExtendedCanId(msg)).toBe(true)
expect(formatAscArbitrationId(msg)).toBe('123x')
})

it('appends x when a 29-bit ID cannot fit in an 11-bit identifier', () => {
// Hardware/logger may omit idType; Vector ASC still requires the trailing x.
const msg = createCanMessage({
id: 0x18daf110,
msgType: { idType: CAN_ID_TYPE.STANDARD, canfd: false, brs: false, remote: false }
})
expect(isExtendedCanId(msg)).toBe(true)
expect(formatAscArbitrationId(msg)).toBe('18DAF110x')
})

it('does not append x for standard 11-bit IDs', () => {
const msg = createCanMessage({ id: 0x7ff })
expect(isExtendedCanId(msg)).toBe(false)
expect(formatAscArbitrationId(msg)).toBe('7FF')
})

it('writes classic CAN extended frames in Vector ASC form', () => {
const msg = createCanMessage({
id: 0x18daf110,
msgType: { idType: CAN_ID_TYPE.EXTENDED, canfd: false, brs: false, remote: false }
})
const line = formatCanMessage(msg, 1, 4.87687)
expect(line).toMatch(/1\s+18DAF110x\s+Rx\s+d 8 03 22 56 78 CC CC CC CC/)
expect(line).not.toMatch(/18DAF110[^x]/)
})

it('writes CAN FD extended frames with a trailing x on the ID', () => {
const msg = createCanMessage({
id: 0x10001,
data: Buffer.from([0x01]),
msgType: { idType: CAN_ID_TYPE.EXTENDED, canfd: true, brs: false, remote: false }
})
const line = formatCanMessage(msg, 2, 0.100995)
expect(line).toMatch(/^0\.100995 CANFD\s+2 Rx\s+10001x\s/)
})
})

describe('ASC reader round-trip', () => {
it('parses trailing x as an extended frame and keeps standard IDs standard', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ecubus-asc-ext-'))
const filePath = path.join(tempDir, 'device-log.asc')

try {
const extended = createCanMessage({
id: 0x18daf110,
msgType: { idType: CAN_ID_TYPE.EXTENDED, canfd: false, brs: false, remote: false }
})
const standard = createCanMessage({
id: 0x123,
data: Buffer.from([0x00, 0x00]),
msgType: { idType: CAN_ID_TYPE.STANDARD, canfd: false, brs: false, remote: false }
})

const content = [
'date Tue Aug 25 03:55:52.711 2026',
'base hex timestamps absolute',
'internal events logged',
'Begin Triggerblock Tue Aug 25 03:55:52.711 2026',
formatCanMessage(extended, 1, 4.87687),
formatCanMessage(standard, 1, 5.0),
'End TriggerBlock',
''
].join('\n')

await fs.writeFile(filePath, content, 'utf8')

const reader = new AscReader(filePath, 0)
reader.init()
const frames = []
let frame = await reader.readFrame()
while (frame) {
frames.push(frame)
frame = await reader.readFrame()
}
reader.close()

expect(content).toMatch(/18DAF110x/)
expect(content).not.toMatch(/\b18DAF110\s/)
expect(frames.map((item) => [item.id, item.msgType.idType])).toEqual([
[0x18daf110, CAN_ID_TYPE.EXTENDED],
[0x123, CAN_ID_TYPE.STANDARD]
])
} finally {
await fs.rm(tempDir, { recursive: true, force: true })
}
})
})
Loading