diff --git a/README.md b/README.md index 3651e7c..5558eb6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Important parts are based on the project [open3e](https://github.com/open3e). A python based implementation of a pure listening approach using MQTT messaging is also availabe, see [E3onCAN](https://github.com/MyHomeMyData/E3onCAN). -**Present implementation is restricted on listening and reading via UDSonCAN (ReadByDid).** One of next steps will be implemention of UDSonCAN service WriteByDid. +**Present implementation supports reading and writing of datapoints via UDSonCAN (ReadByDid and WriteByDid).** Writing is restricted to raw data and numeric datapoints w/o sub structure. During first start of adapter instance a device scan will be done providing a list of all available devices for configuration dialog. A scan for datapoints of each device is also available. @@ -84,8 +84,8 @@ You may use datapoints informations on tab "LIST OF DATAPOINTS" for reference (o ## What is different to open3e project? * Obviously, the main differece is the direct integration to ioBroker. Configuration can be done via dialogs, data get's directly listed in object tree. -* WriteByDid is not supported yet. To come soon. -* Devices specific datapoints are not supported yet. A scan for datapoints per device (as depict tool of open3e is doing) is under development and hopefully comming soon. +* WriteByDid is supported for raw data and for numeric datapoints without sub structure under tree-view of objects. Writing of data is triggered by storing the datapoint with ack=false. The datapoint will be read again two seconds after writing. +* A scan for datapoints per device (as depict tool of open3e is doing) is available now. After a successful scan, device specific datapoints are listed in object tree. * In addation to open3e real time collecting of data via listening is supported. ## May open3 be used in parallel? @@ -101,7 +101,7 @@ Yes, that is possible under certain conditions: ### **WORK IN PROGRESS** * This is beta stage! -* Implement WriteByDid +* Add writing for datapoints with complex structure. * Improve usability for tab "LIST OF DATAPOINTS" ## License diff --git a/io-package.json b/io-package.json index a317ec6..c105b3b 100644 --- a/io-package.json +++ b/io-package.json @@ -1,7 +1,7 @@ { "common": { "name": "e3oncan", - "version": "0.4.3", + "version": "0.5.0", "news": { "0.0.1": { "en": "initial release", diff --git a/lib/canCollect.js b/lib/canCollect.js index 9cbb64c..32650a7 100644 --- a/lib/canCollect.js +++ b/lib/canCollect.js @@ -27,13 +27,15 @@ class collect { cntCommTimeout : 0, // Number of timeouts cntCommBadProt : 0, // Number of bad communications cntTooBusy : 0, // Number of conflicting calls of msgCollect() + nextTs : 0, // Timestamp for next storage (earliest) + tsMinStep : 5000 // Minimum time step between storages }; } async initStates(ctx, opMode) { await this.storage.initStates(ctx, opMode); this.stat.state = 'standby'; - await this.storage.storeStatistics(ctx, this); + await this.storage.storeStatistics(ctx, this, true); } async onTimeout(ctxGlobal, ctxLocal) { @@ -44,7 +46,7 @@ class collect { async startup(ctx) { this.stat.state = 'active'; - await this.storage.storeStatistics(ctx, this); + await this.storage.storeStatistics(ctx, this, true); this.data.collecting = false; await this.storage.setOpMode('normal'); await ctx.log.info('Collect worker started on '+this.config.stateBase); @@ -59,7 +61,7 @@ class collect { this.timeoutHandle = null; this.stat.state = 'stopped'; - await this.storage.storeStatistics(ctx, this); + await this.storage.storeStatistics(ctx, this, true); // Stop worker: this.data.collecting = false; diff --git a/lib/canUds.js b/lib/canUds.js index b218417..3526bff 100644 --- a/lib/canUds.js +++ b/lib/canUds.js @@ -42,7 +42,7 @@ class uds { this.config.statId = 'statUDS'; this.config.worker = 'uds'; this.storage = new storage.storage(this.config); - this.states = ['standby','waitForFF','waitForCF']; + this.states = ['standby','waitForFFrbd','waitForCFrbd','waitForFFSFwbd','waitForFFMFwbd']; this.frameFC = [0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00]; this.readByDidProt = { 'idTx' : this.config.canID, @@ -53,6 +53,15 @@ class uds { '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 + 'SIDnr' : 0x7F, // SID negative response + 'FCrx' : 0x30, // Flow Control ID for MF transfer + }; this.data = { 'len' : 0, 'tsRequest' : 0, @@ -61,7 +70,8 @@ class uds { 'databytes' : [], 'did' : 0, 'state' : 0, - 'D0' : 0x21 + 'D0' : 0x21, + 'txPos' : 0 }; this.canIDhex = '0x'+Number(this.config.canID).toString(16); this.cmndsQueue = []; @@ -83,7 +93,9 @@ class uds { cntCommTimeoutPerDid: {}, // Number of communications ending in timeout for specific did cntCommBadProtocol : 0, // Number of bad communications, e.g. bad frame cntTooBusy : 0, // Number of conflicting calls of msgUds() - replyTime : {min:this.config.timeout,max:0,mean:0} + replyTime : {min:this.config.timeout,max:0,mean:0}, + nextTs : 0, // Timestamp for next storage (earliest) + tsMinStep : 5000 // Minimum time step between storages }; } @@ -110,51 +122,16 @@ class uds { native: {}, }); await ctx.setStateAsync(this.userReadByDidId, { val: JSON.stringify([]), ack: true }); - await this.storage.storeStatistics(ctx, this); + await this.storage.storeStatistics(ctx, this, true); } this.stat.state = 'standby'; } - async setWorkerOpMode(opMode) { - await this.storage.setOpMode(opMode); - } - - async getWorkerOpMode() { - return this.storage.getOpMode(); - } - - async getComState() { - return this.data.state; - } - - async setComState(comState) { - this.data.state = comState; - } - - async setDidDone(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 clearTimeout(this.timeoutHandle); - } - - async setDidStart(ctx, did) { - await this.setComState(1); // 'waitForFF' - const tsNow = new Date().getTime(); - const minWaiting = this.coolDownTs - tsNow; - if (minWaiting > 0) await this.sleep(minWaiting); - this.busy = true; - this.timeoutHandle = await setTimeout(this.onTimeout, this.config.timeout, ctx, this); - this.data.did = did; - this.data.tsRequest = tsNow; - } - async startup(ctx, opMode) { await this.setComState(0); await this.setWorkerOpMode(opMode); this.stat.state = 'active'; - await this.storage.storeStatistics(ctx, this); + 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 @@ -166,7 +143,6 @@ class uds { } else { await ctx.log.silly('UDS worker started in mode '+opMode+' on '+this.config.stateBase); } - if (['standby','normal'].includes(opMode)) await ctx.registerUdsOnStateChange(ctx, this, this.userReadByDidId, this.onUserReadDidsChange); this.cmndsHandle = setInterval(async () => { await this.cmndsLoop(ctx); }, this.cmndsUpdateTime); @@ -174,14 +150,12 @@ class uds { async stop(ctx) { try { + if (this.stat.state == 'stopped') return; + this.stat.state = 'stopped'; const opMode = await this.storage.getOpMode(); - if (['standby','normal'].includes(opMode)) { - await ctx.unRegisterUdsOnStateChange(this.userReadByDidId); - } - - await this.storage.storeStatistics(ctx, this); + await this.storage.storeStatistics(ctx, this, true); await this.storage.setOpMode('standby'); // Stop loops: @@ -206,6 +180,56 @@ class uds { } } + async setWorkerOpMode(opMode) { + await this.storage.setOpMode(opMode); + } + + async getWorkerOpMode() { + return this.storage.getOpMode(); + } + + async getComState() { + return this.data.state; + } + + async setComState(comState) { + this.data.state = comState; + } + + async setDidDone(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 clearTimeout(this.timeoutHandle); + } + + 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(minWaiting); + this.busy = true; + this.timeoutHandle = await setTimeout(this.onTimeout, this.config.timeout, ctx, this); + this.data.did = did; + this.data.tsRequest = tsNow; + } + async calcStat() { this.data.tsReply = new Date().getTime(); const rt = this.data.tsReply - this.data.tsRequest; @@ -283,20 +307,80 @@ class uds { await ctxLocal.setDidDone(0); } - async onUserReadDidsChange(ctxGlobal, ctxLocal, state) { - const dids = JSON.parse(state.val); - if (!state.ack) { - // Execute user command - await ctxGlobal.log.debug('UDS user command on device '+ctxLocal.config.stateBase+'. Dids='+JSON.stringify(dids)); - await ctxLocal.pushCmnd(ctxGlobal, 'read', dids); - await ctxGlobal.setStateAsync(ctxLocal.userReadByDidId, { val: JSON.stringify(dids), ack: true }); // Acknowlegde user command + async onUdsStateChange(ctx, id, state) { + if (id.includes(this.userReadByDidId)) { + // User requests ReadByDid + const dids = JSON.parse(state.val); + await ctx.log.debug('UDS user command 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 + } + if (id.includes('.json.')) { + // User requests WriteByDid for specific did + const i = id.indexOf('.json.')+6; // Index of start of did + const did = Number(id.slice(i,i+4)); + //const val = await this.storage.toByteArray(JSON.parse(state.val)); + await ctx.log.error('UDS user command WriteByDid on '+this.config.stateBase+'.'+String(did)+' - JSON format not supported yet.'); + } + if (id.includes('.raw.')) { + // User requests WriteByDid for specific did + const i = id.indexOf('.raw.')+5; // Index of start of did + const did = Number(id.slice(i,i+4)); + const val = await this.storage.toByteArray(JSON.parse(state.val)); + await ctx.log.debug('UDS user command WriteByDid on '+this.config.stateBase+'.'+String(did)+'='+this.storage.arr2Hex(val)); + await this.pushCmnd(ctx, 'write', [[did,val]]); + setTimeout(function(ctx,did){ctx.cmndsQueue.push({'mode':'read', 'did': did});},2500,this,did); // Read value after 2500 ms + } + if (id.includes('.tree.')) { + // User requests WriteByDid for specific did + const i = id.indexOf('.tree.')+6; // Index of start of did + const did = Number(id.slice(i,i+4)); + if (id.slice(i).includes('.')) { + // Datapoint has sub structure. Not supported yet + await ctx.log.error('UDS user command WriteByDid on '+this.config.stateBase+'.'+String(did)+': Did has sub structure. Not supported yet.'); + /* + //const idBase = id.slice(ctx.namespace.length+1,i+id.slice(i).indexOf('.')); + const idBase = id.slice(0,i+id.slice(i).indexOf('.')); + await ctx.log.debug(idBase); + await ctx.getStatesOf(function(err,obj) { + for (const state of Object.values(obj)) { + if (state._id.includes(idBase)) ctx.log.debug(state._id); + } + }); + //await ctx.getObject(idBase, function(err,obj) {ctx.log.debug(JSON.stringify(obj));}); + //await ctx.log.debug(await JSON.stringify((await ctx.getObject((idBase)).val)); + */ + return; + } + const val = await this.storage.encodeDataCAN(ctx, this, did, JSON.parse(state.val)); + await ctx.log.debug('UDS user command WriteByDid on '+this.config.stateBase+'.'+String(did)+'='+this.storage.arr2Hex(val)); + await this.pushCmnd(ctx, 'write', [[did,val]]); + setTimeout(function(ctx,did){ctx.cmndsQueue.push({'mode':'read', 'did': did});},2500,this,did); // Read value after 2500 ms } } - initialRequestSF(did) { + initialRequestReadSF(did) { return [this.readByDidProt.PCI, this.readByDidProt.SIDtx,((did >> 8) & 0xFF),(did & 0xFF),0x00,0x00,0x00,0x00]; } + initialRequestWrite(did, valRaw, len) { + let frame; + if (len <= 4) { + // Single frame communication + frame = [this.writeByDidProt.PCI+len+3, this.writeByDidProt.SIDtx,((did >> 8) & 0xFF),(did & 0xFF),0x00,0x00,0x00,0x00]; + for (let i=0; i> 8) & 0xFF),(did & 0xFF),0x00,0x00,0x00]; + for (let i=0; i<3; i++) { + frame[i+5] = valRaw[i]; + } + } + return(frame); + } + canMessage(canID, frame) { return { id: canID,ext: false, rtr: false,data: Buffer.from(frame) }; } @@ -317,13 +401,28 @@ class uds { return; } this.stat.cntCommTotal += 1; - await this.setDidStart(ctx, did); - await this.sendFrame(ctx, await this.initialRequestSF(did)); + 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)); } - async writeByDid(ctx, did) { - await ctx.log.error('UDS worker error on '+this.config.stateBase+': writeByDid() is not implemented yet. Did '+JSON.stringify(did)+' ignored.'); + async writeByDid(ctx, didArr) { + if (await this.storage.getOpMode() == 'standby') { + ctx.log.warn('UDS worker warning on '+this.config.stateBase+': Could not execute WriteByDid() for '+String(this.canIDhex)+'.'+JSON.stringify(didArr)+' due to opMode == standby.'); + return; + } + const did=didArr[0]; + const valRaw=didArr[1]; + const len=valRaw.length; + this.stat.cntCommTotal += 1; + this.data.len = len; + 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; + await this.setDidStart(ctx, did, 'write', len); + await this.sendFrame(ctx, await this.initialRequestWrite(did, valRaw, len)); + await ctx.log.silly('UDS worker on '+this.config.stateBase+': WriteByDid(): '+String(this.canIDhex)+'.'+String(did)+'='+this.storage.arr2Hex(valRaw)); } async msgUds(ctx, msg) { @@ -343,7 +442,7 @@ class uds { switch (await this.getComState()) { case 0: // standby break; - case 1: // waitForFF + case 1: // waitForFFrbd if ( (candata[0] == 0x03) && (candata[1] == 0x7F) && (candata[2] == this.readByDidProt.SIDtx) ) { // Negative response this.stat.cntCommNR += 1; @@ -390,11 +489,12 @@ class uds { this.data.databytes = candata.slice(5); this.data.D0 = 0x21; this.sendFrame(ctx, this.frameFC); // Send request for Consecutive Frames - await this.setComState(2); // 'waitForCF' + await this.setComState(2); // 'waitForCFrbd' break; } else { // Did does not match this.stat.cntCommBadProtocol += 1; + await this.calcStat(); if (this.callback) { this.callback(ctx, this, ['did mismatch MF', {'did':this.data.did,'didInfo':{'id':'','len':0},'val':''}]); } else { @@ -409,11 +509,12 @@ class uds { } else { ctx.log.error('UDS worker on '+this.config.stateBase+': Bad frame. candata: '+this.storage.arr2Hex(candata)); } + await this.calcStat(); this.stat.cntCommBadProtocol += 1; await this.setDidDone(2500); break; - case 2: // waitForCF + 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.arr2Hex(candata)); @@ -428,9 +529,7 @@ class uds { } else { // More data to come this.data.D0 += 1; - if (this.data.D0 > 0x2F) { - this.data.D0 = 0x20; - } + if (this.data.D0 > 0x2F) this.data.D0 = 0x20; } } else { // Bad CF @@ -444,6 +543,72 @@ class uds { } break; + case 3: // waitForFFSFwbd + if ( (candata[0] == 0x03) && (candata[1] == 0x7F) && (candata[2] == this.writeByDidProt.SIDtx) ) { + // Negative response + this.stat.cntCommNR += 1; + ctx.log.error('UDS worker error on '+this.config.stateBase+': Negative response on device '+this.canIDhex+'. Code=0x'+Number(candata[3]).toString(16)); + await this.setDidDone(0); + break; + } + if ( (candata.length == 8) && (candata[0] == 0x03) && (candata[1] == this.writeByDidProt.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+': writeByDid SF confirmation received.'); + await this.calcStat(); + this.storage.storeStatistics(ctx, this, false); + await this.setDidDone(0); + break; + } else { + // Did does not match + this.stat.cntCommBadProtocol += 1; + ctx.log.error('UDS worker on '+this.config.stateBase+': Did mismatch writeByDid SF. Expected='+String(this.data.did)+'; Received='+String(didRx)); + await this.calcStat(); + this.storage.storeStatistics(ctx, this, true); + await this.setDidDone(1000); + break; + } + } + ctx.log.error('UDS worker on '+this.config.stateBase+': Bad frame for writeByDid SF. candata: '+this.storage.arr2Hex(candata)); + this.stat.cntCommBadProtocol += 1; + await this.calcStat(); + await this.setDidDone(2500); + break; + + case 4: // waitForFFFwbd + if ( (candata[0] == 0x03) && (candata[1] == 0x7F) && (candata[2] == this.writeByDidProt.SIDtx) ) { + // Negative response + this.stat.cntCommNR += 1; + ctx.log.error('UDS worker error on '+this.config.stateBase+': Negative response on device '+this.canIDhex+'. Code=0x'+Number(candata[3]).toString(16)); + await this.setDidDone(0); + break; + } + if ( (candata.length == 8) && (candata[0] == 0x30) && (candata[1] == 0x00) ) { + // Multi-frame communication confirmed + // Send data in slices of 7 bytes + let ST = candata[2]; // Separation Time (ms) + if ((ST<20) || (ST>127)) ST=50; // Accept ST 20 .. 127 ms. Default to 50 ms. + while (this.data.txPos < this.data.len) { + // More data to send + await this.sleep(ST); + const frame = [this.data.D0].concat(this.data.databytes.slice(this.data.txPos,this.data.txPos+7)); + await this.sendFrame(ctx, frame); + this.data.txPos += 7; + this.data.D0 += 1; + if (this.data.D0 > 0x2f) this.data.D0 = 0x20; + } + await this.setComState(3); // waitForFFSFwbd (wait for confirmation) + break; + } + ctx.log.error('UDS worker on '+this.config.stateBase+': Bad frame for writeByDid MF. candata: '+this.storage.arr2Hex(candata)); + this.stat.cntCommBadProtocol += 1; + await this.calcStat(); + await this.setDidDone(2500); + break; + default: this.stat.cntCommBadProtocol += 1; if (this.callback) { diff --git a/lib/storage.js b/lib/storage.js index 1503bab..38f0c56 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -184,10 +184,41 @@ class storage { return hs; } - async storeStatistics(ctx, ctxWorker) { + toByteArray(hs) { + // Convert hex string, e.g. '21A8' to byte array: [33,168] + const ba = []; + for (let i=0; i