UNPKG

iobroker.e3oncan

Version:

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

358 lines (328 loc) 16 kB
const uds = require('./canUds'); const E3DidsDict = require('./didsE3.json'); const E3DidsWritable = require('./didsE3Writables.json'); class udsScan { constructor() { this.callbackBusy = false; this.udsMaxTrialsDevScan = 2; // Number of trials during UDS device scan this.udsMaxTrialsDidScan = 4; // Number of trials during UDS dids scan this.udsTimeoutDevScan = 1500; // Timeout (ms) for UDS devive scan this.udsTimeoutDidScan = 2500; // Timeout (ms) for UDS dids scan this.udsDevName2CanId = { 'HPMUMASTER': '0x693', // available only on internal bus (?) 'EMCUMASTER': '0x451' }; this.workers = {}; this.udsCntNewDevs = 0; // New devices found during scan this.cntUdsScansActive = 0; this.udsScanAddrSpan = 0x10; this.udsScanAddrRange = [0x680, 0x6a0, 0x6c0, 0x6e0]; this.cntUdsScansActive = 0; this.udsScanDids = {}; this.udsScanDidsCntSuccess = 0; this.udsScanDidsCntTotal = 0; this.udsScanDidsCntDone = 0; this.udsScanDidsCntRetries = 0; } async scanDevCallback(ctx, ctxWorker, _args) { async function mergeDev(dev) { let newDev = true; await ctx.log.silly('UDS device scan found device: '+String(dev.devStateName)); for (const d of Object(ctx.udsDevices).values()) { if ( (d.devName == dev.devStateName.slice(0,-6)) && (d.devAddr == dev.devAddr) ) { await ctx.log.silly('Device is already known. No change applied.'); newDev = false; dev.devStateName = d.devStateName; dev.collectCanId = d.collectCanId; break; } } if (newDev) { await ctx.log.info('UDS device scan found NEW device: '+String(dev.devStateName)); await ctx.udsDevices.push(dev); ctx.udsScanWorker.udsCntNewDevs += 1; } } const response = _args[0]; const result = _args[1]; switch (response) { case 'ok': if (result) { const devName = result.val.DeviceProperty.Text; const busAddress = '00'+String(result.val.BusAddress); await mergeDev ({ 'devName': devName, 'devStateName': devName+'_'+String(ctxWorker.canIDhex), 'devAddr': String(ctxWorker.canIDhex), 'collectCanId': (devName in ctx.udsScanWorker.udsDevName2CanId ? ctx.udsScanWorker.udsDevName2CanId[devName] : ''), 'devTopName': devName.replace('MASTER','')+'_CAN'+busAddress.slice(-2) }); } else { await ctx.log.error('UDS Scan: '+String(ctxWorker.canIDhex)+' got ok, but empty response!'); } break; case 'timeout': if (ctxWorker.stat.cntCommTimeout < ctx.udsScanWorker.udsMaxTrialsDevScan) { await ctxWorker.pushCmnd(ctx, 'read', [ctx.udsDidForScan]); ctx.udsScanWorker.udsScanDidsCntRetries += 1; } await ctx.log.silly('UDS Scan: '+String(ctxWorker.canIDhex)+' '+response); break; default: await ctx.log.silly('UDS Scan: '+String(ctxWorker.canIDhex)+' '+response); } if (ctxWorker.cmndsQueue.length == 0) { await ctxWorker.setCallback(null); // Scan worker completed. Reset callback. ctx.udsScanWorker.cntUdsScansActive -= 1; } } async scanDidsCallback(ctx, ctxWorker, _args) { if (ctx.udsScanWorker.callbackBusy) { ctx.log.error('UDS dids scan: '+ctxWorker.config.stateBase+' - callback busy!'); return; } ctx.udsScanWorker.callbackBusy = true; const response = _args[0]; const result = _args[1]; if (result) { const did = await Number(result.did); switch (response) { case 'ok': case 'negative response': if (response == 'ok') { if (result.common) { ctxWorker.storage.storageDids.didsDictDevCom[did] = result.didInfo; } else { // Device specific did. Do not override previous data if (!(did in ctxWorker.storage.storageDids.didsDictDevSpec)) { ctxWorker.storage.storageDids.didsDictDevSpec[did] = result.didInfo; } } if (did in E3DidsWritable) { // Did is writable. Add it to device specific list of writable dids: ctxWorker.storage.storageDids.didsWritable[did] = E3DidsWritable[did]; } ctx.udsScanWorker.udsScanDidsCntSuccess += 1; } ctx.udsScanWorker.udsScanDidsCntDone += 1; ctx.udsScanWorker.udsScanDids[ctxWorker.canIDhex] -= 1; await ctx.log.silly('UDS dids scan: '+ctxWorker.config.stateBase+'.'+String(ctxWorker.data.did)+': '+response); break; case 'timeout': case 'did mismatch SF': case 'did mismatch MF': case 'bad MF frame': case 'bad CF frame': if (response == 'timeout') { ctx.log.silly('UDS dids scan: Timeout on '+ctxWorker.config.stateBase+'.'+String(ctxWorker.data.did)+' cnt: '+String(ctxWorker.stat.cntCommFailedPerDid[ctxWorker.data.did])); ctx.log.silly('UDS dids scan: '+ctxWorker.config.stateBase+' '+response); } else { ctx.log.debug('UDS dids scan: '+ctxWorker.config.stateBase+' - communication failed: '+response); } if (ctxWorker.stat.cntCommFailedPerDid[ctxWorker.data.did] < ctx.udsScanWorker.udsMaxTrialsDidScan) { ctxWorker.pushCmnd(ctx, 'read', [ctxWorker.data.did]); ctx.udsScanWorker.udsScanDidsCntRetries += 1; } break; default: await ctx.log.warn('UDS dids scan: '+ctxWorker.config.stateBase+' - callback received unknown status: '+String(response)); } if (ctx.udsScanWorker.udsScanDids[ctxWorker.canIDhex] == 0) ctx.udsScanWorker.cntUdsScansActive -= 1; if (ctx.udsScanWorker.cntUdsScansActive == -1) { await ctx.log.error('UDS dids scan: '+ctxWorker.config.stateBase+' - number of active dids got negative - this should not happen.'); } if (ctx.udsScanWorker.udsScanDids[ctxWorker.canIDhex] == -1) { await ctx.log.error('UDS dids scan: '+ctxWorker.config.stateBase+' - number of remaining dids got negative - this should not happen.'); } } else { await ctx.log.error('UDS dids scan: '+ctxWorker.config.stateBase+' - callback received empty result. response='+String(response)); } ctx.udsScanWorker.callbackBusy = false; } async startupUdsWorker(ctx, worker, opMode) { const rxAddr = Number(worker.config.canID) + Number(0x10); ctx.udsScanWorker.workers[rxAddr] = worker; await worker.initStates(ctx, opMode); await worker.startup(ctx, opMode); //await ctx.log.debug(ctx.udsScanWorker.workers[rxAddr].canIDhex+': '+await this.workers[rxAddr].storage.getOpMode()); } async startupScanUdsDevice(ctx, addr) { const udsWorker = new uds.uds( { 'canID' : Number(addr), 'stateBase': 'udsScanAddr', 'device' : 'common', 'delay' : 0, 'active' : true, 'channel' : ctx.channelExt, 'timeout' : this.udsTimeoutDevScan }); await udsWorker.initStates(ctx, 'udsDevScan'); await udsWorker.setCallback(this.scanDevCallback); await this.startupUdsWorker(ctx, udsWorker, 'udsDevScan'); await udsWorker.pushCmnd(ctx, 'read', [ctx.udsDidForScan]); this.cntUdsScansActive += 1; } async scanUdsDevices(ctx) { function range(size, startAt = 0) { return [...Array(size).keys()].map(i => i + startAt); } await ctx.log.info('UDS device scan - start'); this.workers = {}; this.udsCntNewDevs = 0; ctx.udsDevices = ctx.config.tableUdsDevices; // Stop all running workers to avoid communication conflicts: for (const worker of Object.values(ctx.E3UdsWorkers)) { await worker.stop(ctx); } // @ts-ignore const canExtActivated = ctx.config.canExtActivated; // @ts-ignore const canExtName = ctx.config.canExtName; // Startup CAN: if (canExtActivated) { if ((ctx.channelExt) && (ctx.channelExtName != canExtName) ) { // CAN is different from running CAN. Stop actual CAN first. [ctx.channelExt, ctx.channelExtName] = await ctx.disconnectFromCan(ctx.channelExt, ctx.channelExtName); } [ctx.channelExt, ctx.channelExtName] = await ctx.connectToCan(ctx.channelExt, canExtName, ctx.onCanMsgExt); if (!ctx.channelExt) { await ctx.log.error('UDS device scan: Could not connect to CAN Adapter '+canExtName+'. Aborting.'); return(false); } } else { await ctx.log.error('UDS device scan: External CAN not activated! Aborting.'); return(false); } this.udsScanDidsCntRetries = 0; this.cntUdsScansActive = 0; for (const baseAddr of Object(this.udsScanAddrRange).values()) { for (const addr of Object(range(Number(this.udsScanAddrSpan), Number(baseAddr))).values()) { await this.startupScanUdsDevice(ctx, addr); await this.sleep(ctx, ctx.udsTimeDelta); } } const tsAbort = new Date().getTime() + this.udsMaxTrialsDevScan*this.udsTimeoutDevScan+250; await ctx.log.info('UDS device scan: Waiting for scans to complete.'); while ( (this.cntUdsScansActive > 0) && (new Date().getTime() < tsAbort) ) { await this.sleep(ctx, 100); } // Stop all scan workers: for (const worker of Object.values(this.workers)) { await worker.stop(ctx); } this.workers = {}; // Restart all previously running workers: for (const worker of Object.values(ctx.E3UdsWorkers)) { await worker.startup(ctx,'normal'); await this.sleep(ctx, ctx.udsTimeDelta); } if (this.cntUdsScansActive < 0) await ctx.log.warn('UDS scan finished. Number of retries / active UDS scans (should be 0): '+String(this.udsScanDidsCntRetries)+' / '+String(this.cntUdsScansActive)); await ctx.log.info('UDS device scan found '+ String(this.udsCntNewDevs)+ ' new of total '+ String(ctx.udsDevices.length)+ ' devices: '+ JSON.stringify(ctx.udsDevices) ); await ctx.log.info('UDS device scan - done'); return(true); } async startupScanUdsDids(ctx, addr, dids) { const hexAddr = '0x'+Number(addr).toString(16); // @ts-ignore const devInfo = ctx.config.tableUdsDevices.filter(item => item.devAddr == hexAddr); let devName = ''; if (devInfo.length > 0) { devName = devInfo[0].devStateName; } else { devName = hexAddr; } const udsWorker = await new uds.uds( { 'canID' : Number(addr), 'stateBase': devName, 'device' : 'common', 'delay' : 0, 'active' : true, 'channel' : ctx.channelExt, 'timeout' : this.udsTimeoutDidScan }); await udsWorker.setCallback(this.scanDidsCallback); this.udsScanDids[udsWorker.canIDhex] = dids.length; this.udsScanDidsCntTotal += dids.length; await this.startupUdsWorker(ctx, udsWorker, 'udsDidScan'); this.cntUdsScansActive += 1; await udsWorker.pushCmnd(ctx, 'read', dids); } async scanUdsDids(ctx, udsAddrs, udsMaxCntDids) { function range(size, startAt = 0) { return [...Array(size).keys()].map(i => i + startAt); } async function logStatus(ctx, ctxScan) { await ctx.log.info('UDS dids scan status (retries/found/done/total): ('+ String(ctxScan.udsScanDidsCntRetries)+'/'+ String(ctxScan.udsScanDidsCntSuccess)+'/'+ String(ctxScan.udsScanDidsCntDone)+'/'+ String(ctxScan.udsScanDidsCntTotal)+ '), remaining: '+ JSON.stringify(ctxScan.udsScanDids) ); } await ctx.log.info('UDS dids scan - start'); this.workers = {}; this.udsScanDids = {}; // Stop all running workers to avoid communication conflicts: for (const worker of Object.values(ctx.E3UdsWorkers)) { await worker.stop(ctx); } this.cntUdsScansActive = 0; this.udsScanDidsCntTotal = 0; this.udsScanDidsCntRetries = 0; this.udsScanDidsCntSuccess = 0; this.udsScanDidsCntDone = 0; const dids = range(udsMaxCntDids, 256); for (const addr of Object(udsAddrs).values()) { await this.startupScanUdsDids(ctx, addr, dids); await this.sleep(ctx, 50); } let cntDoneLast = -1; await logStatus(ctx, this); let ts = new Date().getTime(); const tsAbort = ts + this.udsScanDidsCntTotal*500; while ( (this.cntUdsScansActive > 0) && (new Date().getTime() < tsAbort) ) { await this.sleep(ctx, 100); if ((new Date().getTime() - ts) >= 10000) { ts += 10000; await logStatus(ctx, this); const cnt = this.udsScanDidsCntDone+this.udsScanDidsCntRetries; if (cntDoneLast == cnt) { // No progress in last 10 seconds await ctx.log.warn('UDS dids scan stalled. Aborting'); break; } cntDoneLast = cnt; } } await logStatus(ctx, this); // Store dids found and stop all scan workers: for (const worker of Object.values(this.workers)) { worker.storage.storageDids.didsDictDevCom['Version'] = E3DidsDict.Version; await worker.storage.storageDids.storeKnownDids(ctx); await worker.stop(ctx); } this.workers = {}; if (this.cntUdsScansActive < 0) await ctx.log.warn('UDS dids scan finished. Number of active UDS scans (should be 0): '+String(this.cntUdsScansActive)); await ctx.log.info('UDS dids scan found '+String(this.udsScanDidsCntSuccess)+' dids. See channel "info" at device objects for details.'); // Restart all previously running workers: for (const worker of Object.values(ctx.E3UdsWorkers)) { await worker.startup(ctx,'normal'); await this.sleep(ctx, ctx.udsTimeDelta); } } sleep(ctx, milliseconds) { return new Promise(resolve => ctx.setTimeout(resolve, milliseconds)); } } module.exports = { udsScan };