Skip to content

Commit 7136be1

Browse files
committed
feat(xcp): add XCP master implementation with script API
Add an XCP (ASAM MCD-1 XCP) master, mirroring the CAN-TP architecture: - src/main/xcp/xcpProtocol.ts: transport-agnostic command/response codec covering STD, CAL/PAG, DAQ and PGM commands, with Intel/Motorola byte-order handling. Byte layout validated against the pyXCP master test vectors. - src/main/xcp/xcpMaster.ts: high-level XcpMaster over a pluggable transport, negotiating slave byte order at CONNECT. - src/main/xcp/xcpCan.ts: XCP-on-CAN transport binding on the existing CAN layer. - src/main/worker/xcp.ts: worker script API (XcpCreateConnection, XcpConnect, XcpShortUpload, DAQ/PGM helpers, ...) bridged via the 'xcpApi' RPC. - nodeItem.ts: xcpApi handler dispatching whitelisted master methods. Tests (test/xcp): 79 codec/master vectors ported from pyXCP plus 5 end-to-end XCP-on-CAN tests over the simulate backend. Also add ambient *?asset / *?asset&asarUnpack module declarations so the worker webpack bundle builds (pre-existing gap that blocked npm run worker:js).
1 parent df6f42c commit 7136be1

11 files changed

Lines changed: 2600 additions & 0 deletions

File tree

src/main/nodeItem.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import UdsTester, {
1212
SomeipApiCall
1313
} from './workerClient'
1414
import { CAN_TP, CAN_TP_SOCKET, TpError as CanTpError } from './docan/cantp'
15+
import { XcpMaster, XcpCanTransport } from './xcp'
1516
import { UdsLOG, VarLOG } from './log'
1617
import { applyBuffer, getRxPdu, getTxPdu, PwmBaseInfo, ServiceItem, UdsDevice } from './share/uds'
1718
import { findService, UDSTesterMain } from './docan/uds'
@@ -77,11 +78,58 @@ class TestTransport extends Transport {
7778
callback()
7879
}
7980
}
81+
/** Whitelist of {@link XcpMaster} methods callable through the worker `xcpApi` bridge. */
82+
const XCP_ALLOWED_METHODS = new Set<string>([
83+
'connect',
84+
'disconnect',
85+
'getStatus',
86+
'synch',
87+
'getCommModeInfo',
88+
'getVersion',
89+
'getId',
90+
'setRequest',
91+
'getSeed',
92+
'unlock',
93+
'setMta',
94+
'upload',
95+
'shortUpload',
96+
'buildChecksum',
97+
'userCmd',
98+
'transportLayerCmd',
99+
'download',
100+
'downloadNext',
101+
'downloadMax',
102+
'shortDownload',
103+
'modifyBits',
104+
'setCalPage',
105+
'getCalPage',
106+
'copyCalPage',
107+
'setDaqPtr',
108+
'writeDaq',
109+
'setDaqListMode',
110+
'startStopDaqList',
111+
'startStopSynch',
112+
'getDaqClock',
113+
'freeDaq',
114+
'allocDaq',
115+
'allocOdt',
116+
'allocOdtEntry',
117+
'clearDaqList',
118+
'programStart',
119+
'programClear',
120+
'program',
121+
'programReset',
122+
'programNext',
123+
'programMax',
124+
'programVerify'
125+
])
126+
80127
export class NodeClass {
81128
pool?: UdsTester
82129
private cantp: CAN_TP[] = []
83130
private lintp: LIN_TP[] = []
84131
private cantpSocketMap: Map<string, { tp: CAN_TP; socket: CAN_TP_SOCKET }> = new Map()
132+
private xcpMasterMap: Map<string, { master: XcpMaster; transport: XcpCanTransport }> = new Map()
85133
private linBaseId: string[] = []
86134
private canBaseId: string[] = []
87135
private ethBaseId: string[] = []
@@ -247,6 +295,7 @@ export class NodeClass {
247295
this.pool.registerHandler('runUdsSeq', this.runUdsSeq.bind(this))
248296
this.pool.registerHandler('linApi', this.linApi.bind(this))
249297
this.pool.registerHandler('canApi', this.canApi.bind(this))
298+
this.pool.registerHandler('xcpApi', this.xcpApi.bind(this))
250299
this.pool.registerHandler('stopUdsSeq', this.stopUdsSeq.bind(this))
251300
this.pool.registerHandler('pwmApi', this.pwmApi.bind(this))
252301
this.pool.registerHandler('serialApi', this.serialApi.bind(this))
@@ -1030,6 +1079,65 @@ export class NodeClass {
10301079
throw new Error(`unknown canApi op: ${op}`)
10311080
}
10321081

1082+
/**
1083+
* Worker RPC: XCP-on-CAN master operations.
1084+
*
1085+
* Ops:
1086+
* - `createConnection` — open an {@link XcpMaster} bound to an {@link XcpCanTransport}.
1087+
* - `closeConnection` — dispose a previously opened connection.
1088+
* - `command` — invoke a whitelisted {@link XcpMaster} method by name with args.
1089+
*/
1090+
async xcpApi(data: any): Promise<any> {
1091+
const { op } = data
1092+
1093+
const findCanBase = (device?: string) => {
1094+
if (device != undefined) {
1095+
for (const channelId of this.canBaseId) {
1096+
const item = this.canBaseMap.get(channelId)
1097+
if (item && item.info.name == device) return item
1098+
}
1099+
throw new Error(`CAN device '${device}' not found`)
1100+
}
1101+
if (this.canBaseId.length > 0) {
1102+
const item = this.canBaseMap.get(this.canBaseId[0])
1103+
if (item) return item
1104+
}
1105+
throw new Error('no CAN device attached to this node')
1106+
}
1107+
1108+
if (op === 'createConnection') {
1109+
const base = findCanBase(data.device)
1110+
const transport = new XcpCanTransport(base, data.addr)
1111+
const master = new XcpMaster(transport)
1112+
const handle = `xcp-${Date.now()}-${Math.random().toString(36).slice(2)}`
1113+
this.xcpMasterMap.set(handle, { master, transport })
1114+
return handle
1115+
}
1116+
1117+
if (op === 'closeConnection') {
1118+
const entry = this.xcpMasterMap.get(data.handle)
1119+
if (!entry) throw new Error(`XCP handle '${data.handle}' not found`)
1120+
entry.master.close()
1121+
this.xcpMasterMap.delete(data.handle)
1122+
return
1123+
}
1124+
1125+
if (op === 'command') {
1126+
const entry = this.xcpMasterMap.get(data.handle)
1127+
if (!entry) throw new Error(`XCP handle '${data.handle}' not found`)
1128+
const method = data.method as string
1129+
if (!XCP_ALLOWED_METHODS.has(method)) {
1130+
throw new Error(`unknown or forbidden xcp command: ${method}`)
1131+
}
1132+
const fn = (entry.master as any)[method]
1133+
const result = await fn.apply(entry.master, data.args ?? [])
1134+
// Normalize Buffers to plain number arrays so they survive worker RPC cleanly.
1135+
return Buffer.isBuffer(result) ? Array.from(result) : result
1136+
}
1137+
1138+
throw new Error(`unknown xcpApi op: ${op}`)
1139+
}
1140+
10331141
private resolveSomeipClient(channel?: string): VSomeIP_Client {
10341142
if (channel) {
10351143
const c = this.someipMap.get(channel)
@@ -1543,6 +1651,10 @@ export class NodeClass {
15431651
tp.close(false)
15441652
}
15451653
this.cantpSocketMap.clear()
1654+
for (const { master } of this.xcpMasterMap.values()) {
1655+
master.close()
1656+
}
1657+
this.xcpMasterMap.clear()
15461658
this.lintp.forEach((tp) => {
15471659
tp.close(false)
15481660
})

src/main/worker/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
export * from './uds'
3838
export * from './someip'
3939
export * from './cantp'
40+
export * from './xcp'
4041
export * from './secureAccess'
4142
export * from './crc'
4243
export * from './cryptoExt'

src/main/worker/node.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,15 @@ declare module '*.html?raw' {
2020
const content: string
2121
export default content
2222
}
23+
24+
/** Asset path import (electron-vite `?asset` query) — resolves to a file path string. */
25+
declare module '*?asset' {
26+
const path: string
27+
export default path
28+
}
29+
30+
/** Asset path import unpacked from the asar archive (`?asset&asarUnpack`) — resolves to a file path string. */
31+
declare module '*?asset&asarUnpack' {
32+
const path: string
33+
export default path
34+
}

0 commit comments

Comments
 (0)