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
JavaScript
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}`,
);