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
269 lines (227 loc) • 8.24 kB
JavaScript
const BTSensor = require("../BTSensor");
const EventEmitter = require('node:events');
const testData=[
['5e 43',
'34',
'33 33 30 30 30 30 44 32 46 37 46 46 46 46 34 38',
'45 38 30 31 30 30 30 45 30 30 34 41',
'30 30 39 30 30 42 30 30 38 30 30 37 42 34 45 44',
'30 43 46 30 30 43 46 32 30 43 46 35 30 43 30 30',
'30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30',
'30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30',
'30 30 30 30 30 30 30 30 30 30 30 30',
'30 30 30 43 31 31 00 00 00 00 00 00 00 00'],
["5e 35 34 33 37 30 30 30 30 30 30 30 30 30 30 30 30",
"34 38 45 38 30 31 30 30 31 30 30 30 36 33 30 30",
"43 30 30 42 30 30 38 30 38 37 46 34",
"33 41 30 45 30 35 30 45 30 33 30 45 30 46 30 44",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 35 37 44 00 00 00 00 00 00 00 00"
],
//bad data (!=0x21)
["5e 35 34 33 37 30 30 30 30 30 30 30 30 30 30 30 30",
"34 38 45 38 30 21 30 30 31 30 30 30 36 33 30 30",
"43 30 30 42 30 30 38 30 38 37 46 34",
"33 41 30 45 30 35 30 45 30 33 30 45 30 46 30 44",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30",
"30 35 37 44 00 00 00 00 00 00 00 00"
]
]
function delimitedAt(data){
const delimiters = [0x5e,0xaf]
for (const d of delimiters){
if (data.includes(d)) return data.indexOf(d)
}
return -1
}
class EctiveDataManager extends EventEmitter{
static expectedLength = 120
buffer = Buffer.from([])
add(buff) {
const delimiterIndex = delimitedAt(buff)
if (delimiterIndex == 0)
this.buffer= buff.subarray(1)
else {
if (delimiterIndex==-1)
this.buffer=Buffer.concat([this.buffer, buff])
else {
this.buffer=Buffer.concat([this.buffer,buff.subarray(0, delimiterIndex)])
}
}
if ( this.buffer.length==this.constructor.expectedLength) {
const _data = new EctiveData (this.buffer)
if (_data.error) {
this.buffer=Buffer.from([])
throw new Error (`malformed packet: ${JSON.parse(JSON.stringify(_data)).ascii}`)
}
else {
this.emit("valuechanged", _data.hex )
this.buffer=Buffer.from([])
}
} else {
if (delimiterIndex>0) {
this.buffer=buff.subarray(delimiterIndex+1)
}
}
}
}
class EctiveData{
constructor(data){
this.ascii = data.toString("utf8").slice(0,data.indexOf(0))
this.hex = Buffer.from(this.ascii,"hex")
this.error = !this.verify()
}
verify(){
return (this.hex.length==this.ascii.toString("utf8").length/2)
}
}
class EctiveBMS extends BTSensor {
static Domain = BTSensor.SensorDomains.electrical
static ImageFile = "TopbandBattery.webp"
static RX_SERVICE = "0000ffe0-0000-1000-8000-00805f9b34fb"
static NOTIFY_CHAR_UUID = "0000ffe4-0000-1000-8000-00805f9b34fb"
static test(data, numCells=4){
const obj = new EctiveBMS()
obj.currentProperties={Name:"Topbrand BMS", Address:"<mac>"}
obj.numberOfCells=numCells
obj.initSchema()
obj.debug=(m)=>{console.log(m)}
for (const [tag,path] of Object.entries(obj._schema.properties.paths.properties)) {
obj.on(tag, val=>{console.log(`${tag} => ${val} `)})
}
obj._dataManager.on("valuechanged", (b)=>{
obj.emitValuesFrom(b)
})
data.forEach(d => {
const _d=d.replaceAll(" ","").toLowerCase()
const b=Buffer.from(_d,"hex")
try {
obj._dataManager.add(b)
} catch (e){
obj.debug(`(${obj.getName()}): ${e.message}`)
}
});
}
static testStream(filename, numCells=4){
const obj = new EctiveBMS()
obj.currentProperties={Name:"Topbrand BMS", Address:"<mac>"}
obj.numberOfCells=numCells
obj.initSchema()
obj.debug=(m)=>{console.log(m)}
for (const [tag,path] of Object.entries(obj._schema.properties.paths.properties)) {
obj.on(tag, val=>{console.log(`${tag} => ${val} `)})
}
obj._dataManager.on("valuechanged", (b)=>{
obj.emitValuesFrom(b)
})
const lineReader = require('readline').createInterface({ input: require('fs').createReadStream(filename)});
// Each line in input.txt will be successively available here as `line`.
(async ()=>{for await (const line of lineReader) {
const _d = line.slice(0,50).replaceAll(" ","").toLowerCase()
const b=Buffer.from(_d,"hex")
try {
obj._dataManager.add(b)
} catch (e){
obj.debug(`(${obj.getName()}): ${e.message}`)
}
}})()
}
static DisplayName(){
return "EctiveBMS: Topbrand, Skanbatt and other LiFePo4 batteries"
}
static identify(device){
return null
}
_dataManager = new EctiveDataManager()
initSchema(){
super.initSchema()
this.addDefaultParam("batteryID")
this.addParameter(
"numberOfCells",
{
title:'number of cells in battery',
type: 'integer',
default: 4,
isRequired: true
}
)
this.addDefaultPath('voltage','electrical.batteries.voltage')
.read=
(buffer)=>{return buffer.readUInt32LE(0) / 1000}
this.addDefaultPath('current','electrical.batteries.current')
.read=
(buffer)=>{return buffer.readInt32LE(4) / 1000}
this.addMetadatum(`cycleCharge`, 'C', `cycle charge`,
(buffer)=>{
return 3600*(buffer.readUInt32LE(8)/1000)
})
.default='electrical.batteries.{batteryID}.cycleCharge'
this.addMetadatum('cycleCount', '', 'number of cycles',
(buffer)=>{
return buffer.readUInt16LE(12)
})
.default='electrical.batteries.{batteryID}.cycles'
this.addDefaultPath('SOC','electrical.batteries.capacity.stateOfCharge')
.read=(buffer)=>{return buffer.readUInt16LE(14)/100}
this.addMetadatum('temp', 'K', 'Temperature reading',
(buffer)=>{
return buffer.readUInt16LE(16)/10
})
.default='electrical.batteries.{batteryID}.temperature'
this.addMetadatum('flags', '', 'Problem flags',
(buffer)=>{
return buffer.readUInt16LE(18).toString('2').padStart(16,'0')
})
.default='electrical.batteries.{batteryID}.problemFlags'
for (let i=0; i<this?.numberOfCells??4; i++){
this.addMetadatum(`cell${i+1}Voltage`, 'V', `Cell ${i+1} voltage`,
(buffer)=>{return buffer.readUInt16LE((22+(i*2)))/1000} )
.default=`electrical.batteries.{batteryID}.cell${i+1}.voltage`
}
}
hasGATT(){
return true
}
usingGATT(){
return true
}
emitGATT(){
//do nothing
}
verifyData(d1,d2){
if (d1.length!==d2.toString("utf8").length/2) {
this._dataError=true
this.debug(`(${this.getName()}) malformed packet received: ${d2.toString()}`)
}
return !this._dataError
}
async initGATTNotifications(){
await this.rxChar.startNotifications()
this.debug(`(${this.getName()}) Notifications started`)
this.rxChar.on("valuechanged", (buffer)=>{
try {
this._dataManager.add(buffer)
} catch (e){
this.debug(`(${this.getName()}): ${e.message}`)
}
})
this._dataManager.on("valuechanged", (b)=>{
this.emitValuesFrom(b)
} )
}
async initGATTConnection(isReconnecting) {
await super.initGATTConnection(isReconnecting)
const gattServer = await this.getGATTServer()
const rxService= await gattServer.getPrimaryService(this.constructor.RX_SERVICE)
this.rxChar = await rxService.getCharacteristic(this.constructor.NOTIFY_CHAR_UUID)
}
async deactivateGATT(){
await this.stopGATTNotifications(this.rxChar)
await super.deactivateGATT()
}
}
module.exports = EctiveBMS;