UNPKG

iobroker.e3oncan

Version:

Collect data on CAN bus for Viessmann E3 devices, e.g. Vitocal, Vitocharge, Energy Meters E380CA and E3100CB

1,230 lines (1,174 loc) 57 kB
const storage = require('./storage'); class scheduleLoop { constructor(ctxGlobal, ctxLocal, schedule) { this.ctxGlobal = ctxGlobal; this.ctxLocal = ctxLocal; this.schedule = schedule; this.dids = []; this.schedHandle = null; } async startSchedule(ctx) { if (this.schedule == 0) { await this.ctxGlobal.log.silly( `UDS schedule one time: ${this.ctxLocal.canIDhex}.${JSON.stringify(this.dids)}`, ); await this.ctxLocal.pushCmnd(this.ctxGlobal, 'read', this.dids); this.schedHandle = null; } else { this.schedHandle = ctx.setInterval(async () => { await this.loop(); }, this.schedule * 1000); } } async stopSchedule(ctx) { try { if (this.schedHandle) { await ctx.clearInterval(this.schedHandle); } await this.ctxGlobal.log.silly( `UDS schedule stopped: ${String(this.schedule)} ${this.ctxLocal.canIDhex}.${JSON.stringify(this.dids)}`, ); } catch (e) { await this.ctxGlobal.log.warn( `Exception while stopping UDS schedule: ${String(this.schedule)} ${this.ctxLocal.canIDhex}.${JSON.stringify(this.dids)}; msg: ${e}`, ); } } async addDids(dids) { const didsArr = dids.replace(' ', '').split(','); this.dids = this.dids.concat( didsArr.map(function (str) { return parseInt(str); }), ); } async loop() { await this.ctxGlobal.log.silly( `UDS schedule: ${String(this.schedule)} ${this.ctxLocal.canIDhex}.${JSON.stringify(this.dids)}`, ); await this.ctxLocal.pushCmnd(this.ctxGlobal, 'read', this.dids); } } /** * Implement relevant set of communication routines according to UDSonCAN protocol */ class uds { /** * @param {object} config Device UDS worker configuration */ constructor(config) { this.config = config; this.config.statId = 'statUDS'; this.config.worker = 'uds'; this.storage = new storage.storage(this.config); this.states = ['standby', 'waitForFFrbd', 'waitForCFrbd', 'waitForFFSFwbd', 'waitForFFMFwbd']; this.readByDidProt = { idTx: this.config.canID, idRx: Number(this.config.canID) + 0x10, PCI: 0x03, // Protocol Control Information SIDtx: 0x22, // Service ID transmit SIDrx: 0x62, // Service ID receive SIDcf: 0x03, // Service ID confirmation byte SIDnr: 0x7f, // SID negative response FC: [0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], // Flow Control frame }; this.writeByDidProt = { idTx: this.config.canID, idRx: Number(this.config.canID) + 0x10, PCI: 0x00, // Protocol Control Information = length of data +3 SIDtx: 0x2e, // Service ID transmit SIDrx: 0x6e, // Service ID receive SIDcf: 0x03, // Service ID confirmation byte SIDnr: 0x7f, // SID negative response FCrx: 0x30, // Flow Control ID for MF transfer }; this.writeByDidSID77Prot = { idTx: this.config.canID, idRx: Number(this.config.canID) + 0x10, PCI: 0x00, // Protocol Control Information = length of data +3 SIDtx: 0x77, // Service ID transmit SIDrx: 0x77, // Service ID receive SIDcf: 0x04, // Service ID confirmation byte SIDnr: 0x7f, // SID negative response FCrx: 0x30, // Flow Control ID for MF transfer }; this.writeProt = this.writeByDidProt; // Default: Use standard protocol for writeDataByIdentifier this.data = { len: 0, tsRequest: 0, tsReply: 0, tsTotal: 0, valRaw: Array(0), databytes: Array(0), did: 0, state: 0, D0: 0x21, txPos: 0, cntFCrx: -1, // Counter till next 'Frame Control frame': 0 ==> FCf expected as next frame }; this.canIDhex = `0x${Number(this.config.canID).toString(16)}`; this.SID77addrOffset = 0x02; this.cmndsQueue = []; this.cmndsHandle = null; this.cmndsUpdateTime = 40; // Check for new commands (ms) this.busy = false; // Worker is busy this.commBusy = false; // Communication routine running this.schedules = {}; this.userReadByDidId = `${this.config.stateBase}.cmnd.udsReadByDid`; this.timeoutHandle = null; this.callback = null; this.coolDownTs = 0; // Earliest time for next communication this.stat = { state: 'standby', CANdevAddr: '', // CAN device address (hex) cntCommTotal: 0, // Number of startes communications cntCommOk: 0, // Number of succesfull communications cntCommNR: 0, // Number of communications ending in negative response cntCommZL: 0, // Number of dids received having a length of zero cntCommTimeout: 0, // Number of communications ending in timeout cntCommBadProtocol: 0, // Number of bad communications, e.g. bad frame cntCommFailedPerDid: {}, // Number of communications failed (timeout or bad protocol) for specific did cntTooBusy: 0, // Number of conflicting calls of msgUds() replyTime: { min: this.config.timeout, max: 0, mean: 0 }, nextTs: 0, // Timestamp for next storage (earliest) tsMinStep: 5000, // Minimum time step between storages }; } /** * Setup uds worker * * @param {object} ctx Caller context * @param {string} opMode Initial mode of operation */ async initStates(ctx, opMode) { await this.storage.initStates(ctx, opMode); if (['standby', 'normal', 'udsDidScan'].includes(opMode)) { await ctx.setObjectNotExistsAsync(`${this.config.stateBase}.cmnd`, { type: 'channel', common: { name: `${this.config.stateBase} commands`, }, native: {}, }); await ctx.setObjectNotExistsAsync(this.userReadByDidId, { type: 'state', common: { name: 'List of dids to be read via UDS ReadByDid. Place command with ack=false.', type: 'json', role: 'state', read: true, write: true, }, native: {}, }); await ctx.setStateAsync(this.userReadByDidId, { val: JSON.stringify([]), ack: true }); await this.storage.storeStatistics(ctx, this, true); } this.stat.state = 'standby'; this.stat.CANdevAddr = this.canIDhex; } /** * Start up uds worker * * @param {object} ctx Caller context * @param {string} opMode Initial mode of operation */ async startup(ctx, opMode) { await this.setComState(0); await this.setWorkerOpMode(opMode); this.stat.state = 'active'; await this.storage.storeStatistics(ctx, this, true); if (opMode == 'normal') { for (const sched of Object.values(this.schedules)) { // Start schedules on startup and do one-time schedules await sched.startSchedule(ctx); } ctx.subscribeStates(`${ctx.namespace}.${this.config.stateBase}.*`); } if (opMode == 'normal') { await ctx.log.info( `UDS worker started on ${this.config.stateBase} (${this.canIDhex}) with ${String( Object.keys(this.schedules).length, )} active schedule(s).`, ); } else { await ctx.log.silly( `UDS worker started in mode ${opMode} on ${this.config.stateBase} (${this.canIDhex}) with ${String( Object.keys(this.schedules).length, )} active schedule(s).`, ); } if (opMode != 'service77') { this.cmndsHandle = ctx.setInterval(async () => { await this.cmndsLoop(ctx); }, this.cmndsUpdateTime); } ctx.cntWorkersActive += 1; } /** * Stop uds worker * * @param {object} ctx Caller context */ async stop(ctx) { try { if (this.stat.state == 'stopped') { return; } this.stat.state = 'stopped'; const opMode = await this.getWorkerOpMode(); await this.storage.storeStatistics(ctx, this, true); await this.storage.setOpMode('standby'); // Stop loops: for (const sched of Object.values(this.schedules)) { await sched.stopSchedule(ctx); } if (this.cmndsHandle) { await ctx.clearInterval(this.cmndsHandle); } // Stop Timeout: if (this.timeoutHandle) { await ctx.clearTimeout(this.timeoutHandle); } this.timeoutHandle = null; // Stop worker: this.callback = null; if (opMode == 'normal') { ctx.unsubscribeStates(`${ctx.namespace}.${this.config.stateBase}.*`); ctx.log.info(`UDS worker stopped on ${this.config.stateBase}`); } else { ctx.log.silly(`UDS worker stopped in mode ${opMode} on ${this.config.stateBase} (${this.canIDhex})`); } } catch (e) { ctx.log.error(`UDS worker on ${this.config.stateBase} could not be stopped. err=${e.message}`); } ctx.cntWorkersActive -= 1; } /** * @param {string} opMode Workes operation mode */ async setWorkerOpMode(opMode) { await this.storage.setOpMode(opMode); } /** * Returns actual operation mode of uds worker */ async getWorkerOpMode() { return this.storage.getOpMode(); } /** * Returns actual communication state of uds worker */ async getComState() { return this.data.state; } /** * @param {number} comState Workes comunication state */ async setComState(comState) { this.data.state = comState; } /** * Mark communication as finalized * * @param {object} ctx Caller context * @param {number} coolDownTime Minumum delay till start of next communicaion (ms) */ async setDidDone(ctx, coolDownTime) { // Finalize communication for recent did this.coolDownTs = new Date().getTime() + coolDownTime; if (this.cmndsQueue.length == 0) { this.busy = false; } await this.setComState(0); if (this.timeoutHandle) { await ctx.clearTimeout(this.timeoutHandle); } } /** * Start communication for did * * @param {object} ctx Caller context * @param {number} did Requested DID * @param {string} mode Comm. mode (read or write) * @param {number} len Requested DID */ async setDidStart(ctx, did, mode, len) { switch (mode) { case 'read': await this.setComState(1); // 'waitForFFrbd' break; case 'write': if (len <= 4) { // Single frame communication await this.setComState(3); // 'waitForFFSFwbd' } else { // Multi frame communication await this.setComState(4); // 'waitForFFMFwbd' } break; default: ctx.log.warn(`UDS worker started on ${this.config.stateBase}: mode ${mode} not implemented.`); } const tsNow = new Date().getTime(); const minWaiting = this.coolDownTs - tsNow; if (minWaiting > 0) { await this.sleep(ctx, minWaiting); } this.busy = true; this.timeoutHandle = await ctx.setTimeout(this.onTimeout, this.config.timeout, ctx, this); this.data.did = did; this.data.tsRequest = tsNow; } /** * Returns statistical data of recent communication */ async calcStat() { this.data.tsReply = new Date().getTime(); const rt = this.data.tsReply - this.data.tsRequest; this.data.tsTotal += rt; if (rt < this.stat.replyTime.min) { this.stat.replyTime.min = rt; } if (rt > this.stat.replyTime.max) { this.stat.replyTime.max = rt; } this.stat.replyTime.mean = Math.round(this.data.tsTotal / this.stat.cntCommOk); } /** * Set callback * * @param {object} callback Callback funtion */ async setCallback(callback) { this.callback = callback; } /** * Add schedule for regular comuunication * * @param {object} ctx Caller context * @param {number} schedule Schedule time (s) * @param {Array} dids List if DIDs */ async addSchedule(ctx, schedule, dids) { if (!Object.keys(this.schedules).includes(String(schedule))) { // New schedule this.schedules[schedule] = new scheduleLoop(ctx, this, schedule); await ctx.log.silly(`UDS worker on ${this.config.stateBase}: Added schedule ${String(schedule)}s.`); } await this.schedules[schedule].addDids(dids); await ctx.log.silly( `UDS worker on ${this.config.stateBase}: Added dids to schedule ${String(schedule)}s ${JSON.stringify( this.schedules[schedule].dids, )}`, ); } /** * Push commuincation command to queue * * @param {object} ctx Caller context * @param {string} mode Comm. mode (read or write) * @param {Array} dids List if DIDs */ async pushCmnd(ctx, mode, dids) { await ctx.log.silly( `UDS worker on ${this.config.stateBase}: pushCmnd(): ${mode} ${String(this.canIDhex)}.${String( JSON.stringify(dids), )}`, ); if (Array.isArray(dids)) { for (const did of Object.values(dids)) { await this.cmndsQueue.push({ mode: mode, did: did }); } } else { await ctx.log.warn( `UDS worker warning on ${ this.config.stateBase }: Wrong format for command. dids have to be array. Got dids=${JSON.stringify(dids)}`, ); } } /** * Wait for a specified time * * @param {object} ctx Caller context * @param {number} milliseconds Waiting time (ms) */ sleep(ctx, milliseconds) { return new Promise(resolve => ctx.setTimeout(resolve, milliseconds)); } /** * Start up uds worker for service 77 * * @param {object} ctx Caller context * @param {number} addr Device address */ async startupUdsWorkerService77(ctx, addr) { const udsWorker = new uds({ canID: Number(addr), stateBase: `${this.config.stateBase}_service77`, device: 'common', delay: 0, active: true, channel: ctx.channelExt, timeout: this.config.timeout, }); await udsWorker.initStates(ctx, 'service77'); await udsWorker.startup(ctx, 'service77'); return udsWorker; } /** * Regulary called loop to execute next command on queue (if any) * * @param {object} ctx Caller context */ async cmndsLoop(ctx) { if ( (await this.getWorkerOpMode()) != 'standby' && this.cmndsQueue.length > 0 && (await this.getComState()) == 0 ) { const cmnd = await this.cmndsQueue.shift(); switch (cmnd.mode) { case 'read': { // ReadByDid await this.readByDid(ctx, cmnd.did); await ctx.log.silly( `UDS worker on ${this.config.stateBase}: cmndLoop()->readByDid(): ${String(cmnd.did)}`, ); break; } case 'write': case 'write77': { // WriteByDid if (cmnd.mode == 'write77') { await ctx.log.silly( `UDS worker on ${this.config.stateBase}: cmndLoop()->writeByDid77(): ${String(cmnd.did)}`, ); // Startup UDS worker for service 77 if not already available: const txAddr = Number(this.config.canID) + this.SID77addrOffset; const rxAddr = txAddr + 0x10; if (!ctx.E3UdsSID77Workers[rxAddr]) { ctx.E3UdsSID77Workers[rxAddr] = await this.startupUdsWorkerService77(ctx, txAddr); } await ctx.E3UdsSID77Workers[rxAddr].writeByDid77(ctx, cmnd.did); } else { await this.writeByDid2E(ctx, cmnd.did); } await ctx.log.silly( `UDS worker on ${this.config.stateBase}: cmndLoop()->writeByDid(): ${String(cmnd.did)}`, ); break; } default: { await ctx.log.error( `UDS worker on ${this.config.stateBase}: Received unknown command ${cmnd.mode}`, ); } } } } /** * Create or increase error counter for did * * @param {object} ctxLocal Worker context * @param {number} did Affected DID */ statCommFailed(ctxLocal, did) { const didNo = Number(did); if (didNo in ctxLocal.stat.cntCommFailedPerDid) { ctxLocal.stat.cntCommFailedPerDid[didNo] += 1; } else { ctxLocal.stat.cntCommFailedPerDid[didNo] = 1; } } /** * Handle timeout error * * @param {object} ctxGlobal Adapter context * @param {object} ctxLocal Worker context */ async onTimeout(ctxGlobal, ctxLocal) { const opMode = await ctxLocal.getWorkerOpMode(); if (['standby', 'normal', 'service77'].includes(opMode)) { await ctxGlobal.log.warn(`UDS timeout on ${ctxLocal.canIDhex}.${String(ctxLocal.data.did)}`); if (opMode == 'service77') { await ctxGlobal.log.error( 'Write access using SID 0x77 failed. Hint: This service is available on internal and master bus only!', ); } } ctxLocal.stat.cntCommTimeout += 1; ctxLocal.statCommFailed(ctxLocal, ctxLocal.data.did); if (ctxLocal.callback) { await ctxLocal.callback(ctxGlobal, ctxLocal, [ 'timeout', { did: ctxLocal.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } // Store statistics to update timeout count (but do not include the event in the time values): ctxLocal.storage.storeStatistics(ctxGlobal, ctxLocal, false); await ctxLocal.setDidDone(ctxGlobal, 0); } /** * Handle user request * * @param {object} ctx Adapter context * @param {object} ctxWorker Worker context * @param {Array} id Chaged id * @param {object} state State reference */ async onUdsStateChange(ctx, ctxWorker, id, state) { // Change of UDS Writables // ======================= if (id.includes(this.storage.storageDids.didsWritablesId)) { // User requests change of UDS writables await ctx.log.info(`User requested change of UDS dids writable on ${this.config.stateBase}`); await this.storage.storageDids.readKnownDids(ctx, await this.getWorkerOpMode()); await ctx.setStateAsync(id, { val: state.val, ack: true }); // Acknowlegde user command return; } // Change of UDS device specific datapoint definition // ================================================== if (id.includes(this.storage.storageDids.didsSpecId)) { // User requests change of UDS device specific datapoint definition await ctx.log.info( `User requested change of UDS device specific datapoint definition on ${this.config.stateBase}`, ); await this.storage.storageDids.readKnownDids(ctx, await this.getWorkerOpMode()); await ctx.setStateAsync(id, { val: state.val, ack: true }); // Acknowlegde user command return; } // User command ReadByDid // ====================== if (id.includes(this.userReadByDidId)) { // User requests ReadByDid try { const dids = JSON.parse(state.val); await ctx.log.debug( `User command UDS ReadByDid on ${this.config.stateBase}. Dids=${JSON.stringify(dids)}`, ); await this.pushCmnd(ctx, 'read', dids); await ctx.setStateAsync(id, { val: JSON.stringify(dids), ack: true }); // Acknowlegde user command } catch (e) { ctx.log.error( `ReadByDid(): Parsing of list of DIDs failed on ${this.config.stateBase}; err=${JSON.stringify( e, )} - You have to provide a list of numerical values.`, ); } return; } // User command WriteByDid // ======================= const dcs = await ctx.idToDCS(id); // Get device, channel and state id if (!['json', 'raw', 'tree'].includes(dcs.channel)) { // State other than datapoint was stored => no action return; } if (dcs.state.length < 6) { // Implausible state id ctx.log.warn( `User command UDS WriteByDid on ${this.config.stateBase}: Could not evaluate state change on id ${id}`, ); return; } const did = Number(dcs.state.slice(0, 4)); if (!(did in this.storage.storageDids.didsWritable)) { ctx.log.error( `User command UDS WriteByDid on ${this.config.stateBase}.${String( did, )}: Writing not allowed on this did. Pls. refer to README for further informations.`, ); return; } await ctx.log.debug(`User command UDS WriteByDid on ${this.config.stateBase}.${String(did)}`); //await ctx.log.debug(JSON.stringify(dcs)+' did='+String(did)+' id='+id+' state='+JSON.stringify(state)); let byteArr = null; // Encoded data let lenBaseId; // Index of start of did state id (did_name ...) in full state id (e3oncan ...) switch (dcs.channel) { case 'json': // Change in json data try { byteArr = await this.storage.encodeDataCAN(ctx, this, String(did), await JSON.parse(state.val)); if (byteArr) { await this.pushCmnd(ctx, 'write', [[did, byteArr]]); ctx.setTimeout( function (ctxWorker, did) { ctxWorker.cmndsQueue.push({ mode: 'read', did: did }); }, 2500, this, did, ); // Read value after 2500 ms } else { ctx.log.error( `User command UDS WriteByDid on ${this.config.stateBase}: Encoding of data failed.`, ); } } catch (e) { ctx.log.error( `WriteByDid(): Encoding of data failed on ${this.config.stateBase}.${String( did, )}; err=${JSON.stringify(e)}`, ); } break; case 'raw': // Change in raw data try { byteArr = this.storage.storageDids.toByteArray(await JSON.parse(state.val)); if (byteArr) { await this.pushCmnd(ctx, 'write', [[did, byteArr]]); ctx.setTimeout( function (ctxWorker, did) { ctxWorker.cmndsQueue.push({ mode: 'read', did: did }); }, 2500, this, did, ); // Read value after 2500 ms } else { ctx.log.error( `User command UDS WriteByDid on ${this.config.stateBase}: Encoding of data failed.`, ); } } catch (e) { ctx.log.error( `WriteByDid(): Encoding of data failed on ${this.config.stateBase}.${String( did, )}; err=${JSON.stringify(e)} - You have to provide JSON formatted data.`, ); } break; case 'tree': // Change in tree data lenBaseId = ctx.namespace.length + dcs.device.length + dcs.channel.length + dcs.state.length + 3; if (id.length == lenBaseId) { // Scalar value w/o sub structure try { byteArr = await this.storage.encodeDataCAN(ctx, this, String(did), await JSON.parse(state.val)); if (byteArr) { await this.pushCmnd(ctx, 'write', [[did, byteArr]]); ctx.setTimeout( function (ctxWorker, did) { ctxWorker.cmndsQueue.push({ mode: 'read', did: did }); }, 2500, this, did, ); // Read value after 2500 ms } else { ctx.log.error( `User command UDS WriteByDid on ${this.config.stateBase}: Encoding of data failed.`, ); } } catch (e) { ctx.log.error( `WriteByDid(): Encoding of data failed on ${this.config.stateBase}.${String( did, )}; err=${JSON.stringify(e)}`, ); } break; } // Build json object for complete object tree of changed did: await ctx.getStatesOf(dcs.device, `${dcs.device}.${dcs.channel}`, async function (err, obj) { // Get all states for changed device.channel function insertDictSubVal(dict, keyArr, val) { const listLabels = ['ListEntries', 'Schedules', 'TopologyElement']; if (keyArr.length == 1) { const key = keyArr[0]; if (listLabels.includes(key)) { dict[key].push(val); } else { dict[key] = val; } } else { const key = keyArr.shift(); if (!(key in dict)) { if (listLabels.includes(key)) { dict[key] = []; } else { dict[key] = {}; } } insertDictSubVal(dict[key], keyArr, val); } } const treeDict = {}; for (const st of Object.values(obj)) { if (st._id.includes(dcs.state)) { const label = st._id.slice(lenBaseId + 1); const val = await JSON.parse( (await ctx.getStateAsync(st._id.slice(ctx.namespace.length + 1))).val, ); insertDictSubVal(treeDict, label.split('.'), val); } } try { byteArr = await ctxWorker.storage.encodeDataCAN(ctx, ctxWorker, did, treeDict); if (byteArr) { await ctxWorker.pushCmnd(ctx, 'write', [[did, byteArr]]); ctx.setTimeout( function (ctxWorker, did) { ctxWorker.cmndsQueue.push({ mode: 'read', did: did }); }, 2500, ctxWorker, did, ); // Read value after 2500 ms } else { ctx.log.error( `User command UDS WriteByDid on ${ ctxWorker.config.stateBase }: Encoding of data failed.`, ); } } catch (e) { ctx.log.error( `WriteByDid(): Encoding of data failed on ${ctxWorker.config.stateBase}.${String( did, )}; err=${JSON.stringify(e)}`, ); } }); break; default: ctx.log.warn( `User command UDS WriteByDid on ${this.config.stateBase}: Could not evaluate state change on id ${ id }`, ); } } /** * Return CAN frame for initialRequestReadSF * * @param {number} did Requested DID */ initialRequestReadSF(did) { return [ this.readByDidProt.PCI, this.readByDidProt.SIDtx, (did >> 8) & 0xff, did & 0xff, 0x00, 0x00, 0x00, 0x00, ]; } /** * Return CAN frame for initialRequestWrite * * @param {number} did Requested DID * @param {Array} valRaw Raw data of DID (array of bytes) * @param {number} len Length of DID * @param {object} prot Protocol bytes */ initialRequestWrite(did, valRaw, len, prot) { let frame; if (len <= 4) { // Single frame communication frame = [prot.PCI + len + 3, prot.SIDtx, (did >> 8) & 0xff, did & 0xff, 0x00, 0x00, 0x00, 0x00]; for (let i = 0; i < len; i++) { frame[i + 4] = valRaw[i]; } } else { // Multi frame communication frame = [prot.PCI + 0x10, len + 3, prot.SIDtx, (did >> 8) & 0xff, did & 0xff, 0x00, 0x00, 0x00]; for (let i = 0; i < 3; i++) { frame[i + 5] = valRaw[i]; } } return frame; } /** * Return CAN frame * * @param {number} canID CAN id * @param {Array} frame Data frame */ canMessage(canID, frame) { return { id: canID, ext: false, rtr: false, data: Buffer.from(frame) }; } /** * Send CAN frame * * @param {object} ctx Adapter context * @param {object} frame CAN frame */ async sendFrame(ctx, frame) { await this.config.channel.send(this.canMessage(this.config.canID, frame)); } /** * Read DID from device * * @param {object} ctx Adapter context * @param {number} did Requested DID */ async readByDid(ctx, did) { if ((await this.getWorkerOpMode()) == 'standby') { ctx.log.warn( `UDS worker warning on ${this.config.stateBase}: Could not execute ReadByDid() for ${String( this.canIDhex, )}.${String(did)} due to opMode == standby.`, ); return; } const state = await this.getComState(); if (state != 0) { await ctx.log.warn( `UDS worker warning on ${this.config.stateBase}: ReadByDid(): state ${ this.states[state] } != standby when called! Did ${String(this.canIDhex)}.${String(did)}; Retry issued.`, ); await this.pushCmnd(ctx, 'read', [did]); return; } this.stat.cntCommTotal += 1; await this.setDidStart(ctx, did, 'read', 0); await this.sendFrame(ctx, await this.initialRequestReadSF(did)); await ctx.log.silly( `UDS worker on ${this.config.stateBase}: ReadByDid(): ${String(this.canIDhex)}.${String(did)}`, ); } /** * Write DID to device using standard service 2E * * @param {object} ctx Adapter context * @param {Array} didArr Requested DID and value */ async writeByDid2E(ctx, didArr) { if (this.stat.state != 'active') { ctx.log.warn( `UDS worker warning on ${this.config.stateBase}: Could not execute WriteByDid() for ${String( this.canIDhex, )}.${JSON.stringify(didArr)} due to state != active.`, ); return; } const did = didArr[0]; const valRaw = didArr[1]; const len = valRaw.length; this.stat.cntCommTotal += 1; this.data.len = len; this.data.valRaw = valRaw; this.data.databytes = valRaw.concat(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); // Add padding this.data.did = did; this.data.txPos = 3; this.data.D0 = 0x21; this.writeProt = this.writeByDidProt; await this.setDidStart(ctx, did, 'write', len); await this.sendFrame(ctx, await this.initialRequestWrite(did, valRaw, len, this.writeByDidProt)); await ctx.log.silly( `UDS worker on ${this.config.stateBase}: WriteByDid(): ${String(this.canIDhex)}.${String( did, )}=${this.storage.storageDids.arr2Hex(valRaw)}`, ); } /** * Write DID to device using Viessmann specific service 77 * * @param {object} ctx Adapter context * @param {Array} didArr Requested DID and value */ async writeByDid77(ctx, didArr) { if (this.stat.state != 'active') { ctx.log.warn( `UDS worker warning on ${this.config.stateBase}: Could not execute WriteByDid() for ${String( this.canIDhex, )}.${JSON.stringify(didArr)} due to state != active.`, ); return; } await ctx.log.debug('User command UDS writeByDid is using SID 0x77'); const did = didArr[0]; const valRaw = didArr[1]; const len = valRaw.length + 6; const len_code = 0xb0 + valRaw.length; // encode length: 0xb0 + data length const prefix77 = [0x43, 0x01, 0x82, did & 0xff, (did >> 8) & 0xff, len_code]; this.stat.cntCommTotal += 1; this.data.len = len; this.data.valRaw = valRaw; this.data.databytes = prefix77.concat(valRaw.concat(0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55)); // Add padding this.data.did = did; this.data.txPos = 3; this.data.D0 = 0x21; this.writeProt = this.writeByDidSID77Prot; await this.setDidStart(ctx, did, 'write', len); await this.sendFrame( ctx, await this.initialRequestWrite(did, this.data.databytes, len, this.writeByDidSID77Prot), ); await ctx.log.silly( `UDS worker on ${this.config.stateBase}: WriteByDid(): ${String(this.canIDhex)}.${String( did, )}=${this.storage.storageDids.arr2Hex(valRaw)}`, ); } /** * Evaluate received CAN message and perform UDS communication * * @param {object} ctx Adapter context * @param {object} msg CAN frame received */ async msgUds(ctx, msg) { // Typical communication patterns // ReadDataByIdentifier SF: // vcan0 680 [8] 03 22 01 8C 00 00 00 00 // vcan0 690 [8] 05 62 01 8C C2 01 55 55 // ReadDataByIdentifier MF: // vcan0 680 [8] 03 22 01 00 00 00 00 00 // vcan0 690 [8] 10 27 62 01 00 01 02 1F // vcan0 680 [8] 30 00 00 00 00 00 00 00 // vcan0 690 [8] 21 09 14 00 FD 01 01 09 // vcan0 690 [8] 22 C0 00 02 00 64 02 65 // vcan0 690 [8] 23 00 04 00 37 34 37 30 // vcan0 690 [8] 24 36 32 38 32 30 33 33 // vcan0 690 [8] 25 30 37 31 32 38 55 55 // WriteDataByIdentifier SF: // vcan0 680 [8] 05 2E 01 8C C2 01 00 00 // vcan0 690 [8] 03 6E 01 8C 55 55 55 55 // WriteDataByIdentifier MF: // vcan0 680 [8] 10 0C 2E 01 A8 E6 00 D2 // vcan0 690 [8] 30 00 50 55 55 55 55 55 // vcan0 680 [8] 21 00 96 00 00 00 00 00 // vcan0 690 [8] 03 6E 01 A8 55 55 55 55 // WriteDataByIdentifier MF using service 77: // vcan0 682 [8] 10 17 77 01 F8 43 01 82 // vcan0 692 [8] 30 00 05 00 00 00 00 00 // vcan0 682 [8] 21 F8 01 BE 64 00 64 00 // vcan0 682 [8] 22 F4 01 58 02 B6 03 00 // vcan0 682 [8] 23 00 26 02 55 55 55 55 // vcan0 692 [8] 04 77 01 F8 44 55 55 55 if (this.stat.state != 'active') { return; } // Communication allowed only in state 'active' if (this.commBusy) { this.stat.cntTooBusy += 1; if (this.stat.cntTooBusy == 1) { ctx.log.warn( `UDS worker warning on ${this.config.stateBase} (0x${Number(msg.id).toString( 16, )}): Evaluation of messages overloaded.`, ); } if (this.stat.cntTooBusy % 100 == 0) { ctx.log.debug( `UDS worker warning on ${this.config.stateBase} (0x${Number(msg.id).toString( 16, )}): Evaluation of messages overloaded. Counter=${String(this.stat.cntTooBusy)}`, ); } return; } this.commBusy = true; const candata = msg.data.toJSON().data; //ctx.log.debug('UDS worker on '+this.config.stateBase+' ('+this.canIDhex+'): candata: '+this.storage.storageDids.arr2Hex(candata)); switch (await this.getComState()) { case 0: // standby break; case 1: // waitForFFrbd if ( candata[0] == this.readByDidProt.SIDcf && candata[1] == 0x7f && candata[2] == this.readByDidProt.SIDtx ) { // Negative response this.stat.cntCommNR += 1; if (this.callback) { this.callback(ctx, this, [ 'negative response', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker error on ${this.config.stateBase}: Negative response reading did ${String( this.data.did, )}. Code=0x${Number(candata[3]).toString(16)}`, ); } await this.setDidDone(ctx, 0); break; } if (candata.length == 8 && candata[0] >> 4 == 0 && candata[1] == this.readByDidProt.SIDrx) { // Single-frame communication const didRx = candata[3] + 256 * candata[2]; if (didRx == this.data.did) { // Did does match this.stat.cntCommOk += 1; ctx.log.silly( `UDS worker on ${ this.config.stateBase }: SF received. candata: ${this.storage.storageDids.arr2Hex(candata)}`, ); await this.calcStat(); this.data.len = candata[0] - 3; if (this.data.len > 0) { // Only non-zero length is valid response this.data.databytes = candata.slice(4, 4 + this.data.len); this.storage.decodeDataCAN( ctx, this, String(this.data.did), this.data.databytes.slice(0, this.data.len), ); } else { // Treat zero length did as negative response this.stat.cntCommZL += 1; if (this.callback) { this.callback(ctx, this, [ 'negative response', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker error on ${this.config.stateBase}: Got did with a length of zero. Ignoring. Did=${String( this.data.did, )}`, ); } } await this.setDidDone(ctx, 0); break; } else { // Did does not match this.stat.cntCommBadProtocol += 1; this.statCommFailed(this, this.data.did); if (this.callback) { this.callback(ctx, this, [ 'did mismatch SF', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker on ${this.config.stateBase}: Did mismatch MF. Expected=${String( this.data.did, )}; Received=${String(didRx)}`, ); } await this.setDidDone(ctx, 1000); break; } } if (candata.length == 8 && candata[0] >> 4 == 1 && candata[2] == this.readByDidProt.SIDrx) { // Multiframe communication const didRx = candata[4] + 256 * candata[3]; if (didRx == this.data.did) { // Did does match this.data.len = (candata[0] & 0x0f) * 256 + candata[1] - 3; ctx.log.silly( `UDS worker on ${ this.config.stateBase }: FF received. candata: ${this.storage.storageDids.arr2Hex(candata)}`, ); this.data.databytes = candata.slice(5); this.data.D0 = 0x21; this.sendFrame(ctx, this.readByDidProt.FC); // Send request for Consecutive Frames await this.setComState(2); // 'waitForCFrbd' break; } else { // Did does not match this.stat.cntCommBadProtocol += 1; this.statCommFailed(this, this.data.did); await this.calcStat(); if (this.callback) { this.callback(ctx, this, [ 'did mismatch MF', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker on ${this.config.stateBase}: Did mismatch MF. Expected=${String( this.data.did, )}; Received=${String(didRx)}`, ); } await this.setDidDone(ctx, 1000); break; } } if (this.callback) { this.callback(ctx, this, [ 'bad MF frame', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker on ${this.config.stateBase}: Bad frame readByDid. candata: ${this.storage.storageDids.arr2Hex( candata, )}`, ); } await this.calcStat(); this.stat.cntCommBadProtocol += 1; this.statCommFailed(this, this.data.did); await this.setDidDone(ctx, 2500); break; case 2: // waitForCFrbd if (candata.length == 8 && candata[0] == this.data.D0) { // Correct code for Consecutive Frame ctx.log.silly( `UDS worker on ${ this.config.stateBase }: CF received. candata: ${this.storage.storageDids.arr2Hex(candata)}`, ); this.data.databytes = this.data.databytes.concat(candata.slice(1)); if (this.data.databytes.length >= this.data.len) { // All data received this.stat.cntCommOk += 1; await this.calcStat(); ctx.log.silly( `UDS worker on ${ this.config.stateBase }: MF completed. candata: ${this.storage.storageDids.arr2Hex(candata)}`, ); this.storage.decodeDataCAN( ctx, this, String(this.data.did), this.data.databytes.slice(0, this.data.len), ); await this.setDidDone(ctx, 0); } else { // More data to come this.data.D0 += 1; if (this.data.D0 > 0x2f) { this.data.D0 = 0x20; } } } else { // Bad CF if (this.callback) { this.callback(ctx, this, [ 'bad CF frame', { did: this.data.did, didInfo: { id: '', len: 0 }, val: '' }, ]); } else { ctx.log.warn( `UDS worker on ${ this.config.stateBase }: Bad frame readByDid. candata: ${this.storage.storageDids.arr2Hex(candata)}`, ); } this.stat.cntCommBadProtocol += 1; this.statCommFailed(this, this.data.did); await this.setDidDone(ctx, 2500); } break; case 3: // waitForFFSFwbd (wait for confirmation) if (candata[0] == this.writeProt.SIDcf && candata[1] == 0x7f && candata[2] == this.writeProt.SIDtx) { // Negative response this.stat.cntCommNR += 1; ctx.log.warn( `UDS worker error on ${this.config.stateBase}: Negative response writing did ${String( this.data.did, )}. Code=0x${Number(candata[3]).toString(16)}`, ); if ((await this.getWorkerOpMode()) == 'normal') { // Give it one more try using service 77 ctx.log.info( `Going to try again using SID 0x77 to write data point on ${this.config.stateBase}`, );