UNPKG

bt-sensors-plugin-sk

Version:

Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors

501 lines (442 loc) 14 kB
const BTSensor = require("../BTSensor"); let FakeDevice,FakeGATTService,FakeGATTCharacteristic; // Dynamically import FakeBTDevice.js for node<= 20 import('../development/FakeBTDevice.js') .then(module => { FakeDevice = module.FakeDevice; FakeGATTService= module.FakeGATTService FakeGATTCharacteristic=module.FakeGATTCharacteristic }) .catch(error => { console.error('Error loading FakeBTDevice:', error); }); function sumByteArray(byteArray) { let sum = 0; for (let i = 0; i < byteArray.length; i++) { sum += byteArray[i]; } return sum; } const countSetBits=(n)=> {return (n == 0)?0:(n & 1) + countSetBits(n >> 1)}; class JikongBMS extends BTSensor { static Domain = BTSensor.SensorDomains.electrical; static RX_SERVICE = "0000ffe0-0000-1000-8000-00805f9b34fb"; static RX_CHAR_UUID = "0000ffe1-0000-1000-8000-00805f9b34fb"; static validResponseHeader = 0x55aaeb90; static validAcknowledgeHeader = 0xaa5590eb; static commandResponse = { 0x96: 0x02, 0x97: 0x03, }; static async test(datafile) { const data = require(datafile); const device = new FakeDevice([ new FakeGATTService(this.RX_SERVICE, [ new FakeGATTCharacteristic( this.RX_CHAR_UUID, data.data["0x96"], data.delay ), ]), ]); const obj = new JikongBMS(device, { offset: 16, dischargeFloor: 0.1, numberOfCells:4 }); obj.currentProperties = { Name: "Fake JKBMS", Address: "<mac>" }; obj.debug = (m) => { console.log(m); }; obj.deviceConnect = () => {}; await obj.initSchema(); await obj.initGATTConnection() await obj.getAndEmitBatteryInfo(); for (const [tag, path] of Object.entries( obj._schema.properties.paths.properties )) { obj.on(tag, (val) => { console.log(`${tag} => ${val} `); }); } } static identify(device) { return null; } static ImageFile = "JikongBMS.jpg"; connections = 0; pollFreq = 30; jikongCommand(command) { var result = [ 0xaa, 0x55, 0x90, 0xeb, command, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; result.push(Buffer.from([sumByteArray(result)])[0]); return result; } async sendReadFunctionRequest(command) { this.debug(`sending ${command} for ${this.getName()}`); try { return await this.rxChar.writeValueWithoutResponse( Buffer.from(this.jikongCommand(command)) ); } catch (e) { this.debug( `Error rec'd writing data: ${e.message} for ${this.getName()}` ); } } async initSchema() { super.initSchema(); this.addDefaultParam("batteryID"); this.addParameter("offset", { description: "Data offset", type: "number", isRequired: true, default: 16, }); this.addParameter("dischargeFloor", { description: "Discharge floor ratio ", isRequired: true, type: "number", default: 0.1, minimum: 0, max: 0.99, }); if (this.numberOfCells == undefined) { try { this.debug("Getting number of cells...") await this.initGATTConnection(); this.numberOfCells = await this.getNumberOfCells() } catch(e){ this.numberOfCells = 4; } } this.addParameter("numberOfCells", { description: "Number of cells", type: "number", isRequired: true, default: this.numberOfCells, }); for (let i = 0; i < this?.numberOfCells ?? 4; i++) { this.addMetadatum( `cell${i}Voltage`, "V", `Cell ${i + 1} voltage`, (buffer) => { if (i == 0) { this.currentProperties._totalCellVoltage = 0; this.currentProperties._maxCellVoltage = 0; this.currentProperties._minCellVoltage = 0; } const v = buffer.readUInt16LE(6 + i * 2) / 1000; this.currentProperties._totalCellVoltage += v; if (v > this.currentProperties._maxCellVoltage) this.currentProperties._maxCellVoltage = v; if ( this.currentProperties._minCellVoltage == 0 || v < this.currentProperties._minCellVoltage ) this.currentProperties._minCellVoltage = v; return v; } ).default = `electrical.batteries.{batteryID}.cell${i}.voltage`; this.addMetadatum( `cell${i}Resistance`, "ohm", `Cell ${i + 1} resistance in ohms`, (buffer) => { return buffer.readUInt16LE(i * 2 + 64 + this.offset) / 1000; } ).default = `electrical.batteries.{batteryID}.cell${i}.resistance`; } this.addMetadatum( "avgCellVoltage", "number", "Average Cell Voltage", () => { return this.currentProperties._totalCellVoltage / this.numberOfCells; } ).default = "electrical.batteries.{batteryID}.avgCellVoltage"; this.addMetadatum( "deltaCellVoltage", "number", "Delta Cell Voltage", () => { return ( this.currentProperties._maxCellVoltage - this.currentProperties._minCellVoltage ); } ).default = "electrical.batteries.{batteryID}.deltaCellVoltage"; this.addDefaultPath("voltage", "electrical.batteries.voltage").read = ( buffer ) => { return buffer.readUInt16LE(118 + this.offset * 2) / 1000; }; this.addDefaultPath("power", "electrical.batteries.power", (buffer) => { return buffer.readInt16LE(122 + this.offset * 2) / 1000; }); this.addDefaultPath("current", "electrical.batteries.current").read = ( buffer ) => { this.currentProperties._current = buffer.readInt16LE(126 + this.offset * 2) / 1000; return this.currentProperties._current; }; this.addDefaultPath( "remainingCapacity", "electrical.batteries.capacity.remaining" ).read = (buffer) => { this.currentProperties._capacityRemaining = (buffer.readUInt32LE(142 + this.offset * 2) / 1000) * 3600; return this.currentProperties._capacityRemaining; }; this.addDefaultPath( "capacity", "electrical.batteries.capacity.actual" ).read = (buffer) => { this.currentProperties._capacityActual = (buffer.readUInt32LE(146 + this.offset * 2) / 1000) * 3600; return this.currentProperties._capacityActual; }; this.addDefaultPath( "timeRemaining", "electrical.batteries.capacity.timeRemaining" ).read = (buffer) => { return Math.abs( (this.currentProperties._capacityActual * this.dischargeFloor) / this.currentProperties._current ); }; this.addDefaultPath("cycles", "electrical.batteries.cycles").read = ( buffer ) => { return buffer.readUInt32LE(150 + this.offset * 2); }; this.addMetadatum("cycleCapacity", "number", "Cycle capacity", (buffer) => { return buffer.readUInt32LE(154 + this.offset * 2) / 1000; }).default = "electrical.batteries.{batteryID}.cycleCapacity"; this.addMetadatum( "balanceAction", "", "Balancing action (0=off 1=Charging Balance, 2=Discharging Balance", (buffer) => { return buffer[140 + this.offset * 2]; } ).default = "electrical.batteries.{batteryID}.balanceAction"; this.addMetadatum("balancingCurrent", "", "Balancing current", (buffer) => { return buffer.readUInt16LE(138 + this.offset * 2) / 1000; }).default = "electrical.batteries.{batteryID}.balance"; this.addDefaultPath( "SOC", "electrical.batteries.capacity.stateOfCharge" ).read = (buffer) => { return buffer[141 + this.offset * 2] / 100; }; this.addDefaultPath( "SOH", "electrical.batteries.capacity.stateOfHealth" ).read = (buffer) => { return buffer[158 + this.offset * 2] / 100; }; this.addMetadatum("runtime", "s", "Total runtime in seconds", (buffer) => { return buffer.readUInt32LE(162 + this.offset * 2); }).default = "electrical.batteries.{batteryID}.runtime"; this.addMetadatum( "timeEmergency", "s", "Time emergency in seconds", (buffer) => { return buffer.readUInt16LE(186 + this.offset * 2); } ).default = "electrical.batteries.{batteryID}.timeEmergency"; this.addMetadatum( "charging", "bool", "MOSFET Charging enable", (buffer) => { return buffer[166 + this.offset * 2] == 1; } ).default = "electrical.batteries.{batteryID}.charging"; this.addMetadatum( "discharging", "bool", "MOSFET Disharging enable", (buffer) => { return buffer[167 + this.offset * 2] == 1; } ).default = "electrical.batteries.{batteryID}.discharging"; this.addMetadatum("temp1", "K", "Temperature in K", (buffer) => { return 273.15 + buffer.readInt16LE(130 + this.offset * 2) / 10; }).default = "electrical.batteries.{batteryID}.temperature"; this.addMetadatum("temp2", "K", "Temperature 2 in K", (buffer) => { return 273.15 + buffer.readInt16LE(132 + this.offset * 2) / 10; }).default = "electrical.batteries.{batteryID}.temperature2"; this.addMetadatum("mosTemp", "K", "MOS Temperature in K", (buffer) => { return 273.15 + buffer.readInt16LE(112 + this.offset * 2) / 10; }).default = "electrical.batteries.{batteryID}.mosTemperature"; } hasGATT() { return true; } async initGATTNotifications() { this.debug(`${this.getName()}::initGATTNotifications`); } async emitGATT() { await this.getAndEmitBatteryInfo(); } async getNumberOfCells() { this.debug(`${this.getName()}::getNumberOfCells`); const b = await this.getBuffer(0x96); return countSetBits(b.readUInt32BE(70)); } getBuffer(command) { return new Promise(async (resolve, reject) => { const r = await this.sendReadFunctionRequest(command); let datasize = 300; let result = Buffer.alloc(datasize); let offset = 0; const timer = setTimeout(() => { this.rxChar.removeAllListeners(); clearTimeout(timer); reject( new Error( `Response timed out (+30s) getting results for command ${command} from JikongBMS device ${this.getName()}.` ) ); }, 30000); const valChanged = async (buffer) => { if ( offset == 0 && //first packet (buffer.length < 5 || buffer.readUInt32BE(0) !== this.constructor.validResponseHeader) ) { if ( buffer.readUInt32BE(0) !== this.constructor.validAcknowledgeHeader ) this.debug( `Invalid buffer ${JSON.stringify( buffer )}from ${this.getName()}, not processing.` ); } else { buffer.copy(result, offset); if (offset + buffer.length == datasize) { if (result[4] == this.constructor.commandResponse[command]) { this.rxChar.removeAllListeners(); clearTimeout(timer); this.debug( `Rec'd command in buffer ${JSON.stringify( result )} for ${this.getName()}` ); resolve(result); } else { offset = 0; result = Buffer.alloc(datasize); } } else { offset += buffer.length; } } }; this.rxChar.on("valuechanged", valChanged); }); } usingGATT() { return true; } async getAndEmitBatteryInfo() { const buffer = await this.getBuffer(0x96); [ "current", "voltage", "remainingCapacity", "capacity", "cycles", "charging", "discharging", "balanceAction", "timeRemaining", "balancingCurrent", "cycleCapacity", "timeEmergency", "SOC", "SOH", "runtime", "temp1", "temp2", "mosTemp", ].forEach((tag) => this.emitData(tag, buffer)); for (let i = 0; i < this.numberOfCells; i++) { this.emitData(`cell${i}Voltage`, buffer); this.emitData(`cell${i}Resistance`, buffer); } ["deltaCellVoltage", "avgCellVoltage"].forEach((tag) => this.emitData(tag)); } async deactivateGATT() { await this.stopGATTNotifications(this.rxChar) await super.deactivateGATT(); } async initGATTConnection(isReconnecting = false) { this.debug(`${this.getName()}::initGATTConnection`); if (this.rxChar) try { this.rxChar.removeAllListeners() await this.rxChar.stopNotifications() } catch(e){ this.debug(`error while stopping notifications`) this.debug(e) } try { await super.initGATTConnection(isReconnecting); const gattServer = await this.getGATTServer(); this.rxService = await gattServer.getPrimaryService( this.constructor.RX_SERVICE ); this.rxChar = await this.rxService.getCharacteristic( this.constructor.RX_CHAR_UUID ); await this.rxChar.startNotifications(); } catch (e) { this.setError(e.message) } try { await this.getBuffer(0x97) } catch(e){ this.debug(`Error encountered calling getBuffer(0x97)`) } //this.debug(`(${this.getName()}) Connections: ${this.connections++}`) } async initGATTInterval(){ this.intervalID = setInterval(async () => { this._error = false if (!(await this.device.isConnected())) { await this.initGATTConnection(true); } await this.getAndEmitBatteryInfo(); }, (this?.pollFreq??40) * 1000); try { await this.getAndEmitBatteryInfo(); } catch(e) { this.setError(e.message) } } } module.exports = JikongBMS;