Una repositoy che integra un plugin custom su SignalK per ottenere tutti i dati del BMS della batteria
- Introdotta l'implementazione JavaScript per la comunicazione BMS in bmscore.js, inclusi i metodi per il recupero dati e la gestione degli errori. - Creato errors.js per mappare i codici di errore dal formato Python a quello JavaScript.
This commit is contained in:
262
src/bmscore.js
Normal file
262
src/bmscore.js
Normal file
@@ -0,0 +1,262 @@
|
||||
const { SerialPort } = require('serialport');
|
||||
const errors = require('./errors');
|
||||
|
||||
const address = 0x04;
|
||||
const frameLength = 13;
|
||||
const timeoutMS = 700;
|
||||
const retryDelayMS = 200;
|
||||
|
||||
//Arrotondamenti
|
||||
const r1 = v => Math.round(v * 10) / 10; // 0.1 — tensione totale, corrente, SOC%
|
||||
const r3 = v => Math.round(v * 1000) / 1000; // 0.001 — tensioni cella, capacità Ah
|
||||
const r2 = v => Math.round(v * 100) / 100; // 0.01 — temperature in kelvin (SignalK)
|
||||
|
||||
class BMS {
|
||||
constructor({ device, retries = 5, log = () => {} } = {}) {
|
||||
this.device = device;
|
||||
this.retries = retries;
|
||||
this.log = log;
|
||||
this.port = null;
|
||||
this.status = null; // ultimo getStatus (n. celle, sensori temp)
|
||||
this._queue = Promise.resolve(); // serializza le richieste sulla seriale
|
||||
}
|
||||
|
||||
open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.port = new SerialPort({
|
||||
path: this.device,
|
||||
baudRate: 9600,
|
||||
dataBits: 8,
|
||||
parity: 'none',
|
||||
stopBits: 1,
|
||||
autoOpen: false
|
||||
});
|
||||
this.port.open(err => err ? reject(err) : this.port.flush(() => resolve()));
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.port && this.port.isOpen) {
|
||||
await new Promise(r => this.port.close(() => r()));
|
||||
}
|
||||
this.port = null;
|
||||
}
|
||||
|
||||
static _crc(buf) {
|
||||
let s = 0;
|
||||
for (let i = 0; i < buf.length; i++) s = (s + buf[i]) & 0xFF;
|
||||
return s;
|
||||
}
|
||||
|
||||
_buildFrame(cmd, extraHex = '') {
|
||||
const hex = `a5${address.toString(16).padStart(2, '0')}${cmd}08${extraHex}`.padEnd(24, '0');
|
||||
const buf = Buffer.from(hex, 'hex');
|
||||
return Buffer.concat([buf, Buffer.from([BMS._crc(buf)])]);
|
||||
}
|
||||
|
||||
//Invii multipli
|
||||
_sendBatch(cmd, opts = {}) {
|
||||
const run = async () => {
|
||||
let lastErr;
|
||||
for (let i = 0; i < this.retries; i++) {
|
||||
try {
|
||||
const r = await this._send(cmd, opts);
|
||||
if (opts.returnList ? r && r.length : r) return r;
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
this.log(`cmd ${cmd} try ${i + 1} failed: ${e.message}`);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, retryDelayMS));
|
||||
}
|
||||
throw lastErr || new Error(`cmd ${cmd} failed after ${this.retries} retries`);
|
||||
};
|
||||
const next = this._queue.then(run, run);
|
||||
this._queue = next.catch(() => {}); // non rompere la coda al primo errore
|
||||
return next;
|
||||
}
|
||||
|
||||
_send(cmd, { extraHex = '', maxResponses = 1, returnList = false } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.port || !this.port.isOpen) return reject(new Error('serial not open'));
|
||||
const tx = this._buildFrame(cmd, extraHex);
|
||||
const frames = [];
|
||||
let buf = Buffer.alloc(0);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
this.port.off('data', onData);
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
cleanup();
|
||||
if (returnList || frames.length > 1) return resolve(frames);
|
||||
return resolve(frames[0] || null);
|
||||
};
|
||||
|
||||
const onData = chunk => {
|
||||
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
|
||||
while (buf.length >= frameLength) {
|
||||
const frame = buf.subarray(0, frameLength);
|
||||
buf = buf.subarray(frameLength);
|
||||
if (BMS._crc(frame.subarray(0, 12)) !== frame[12]) {
|
||||
this.log(`crc mismatch: ${frame.toString('hex')}`);
|
||||
continue;
|
||||
}
|
||||
if (frame[2].toString(16).padStart(2, '0') !== cmd) {
|
||||
this.log(`unexpected cmd: ${frame.toString('hex')}`);
|
||||
continue;
|
||||
}
|
||||
frames.push(frame.subarray(4, 12));
|
||||
if (frames.length >= maxResponses) return finish();
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (frames.length === 0) { cleanup(); return reject(new Error('serial timeout')); }
|
||||
finish();
|
||||
}, timeoutMS);
|
||||
|
||||
this.port.flush(() => {
|
||||
this.port.on('data', onData);
|
||||
this.port.write(tx, err => { if (err) { cleanup(); reject(err); } });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 0x90 — voltage / current / SOC
|
||||
async getSoc() {
|
||||
const d = await this._sendBatch('90');
|
||||
if (!d) return null;
|
||||
return {
|
||||
total_voltage: r1(d.readInt16BE(0) / 10),
|
||||
current: r1((d.readInt16BE(4) - 30000) / 10), // < 0 charging, > 0 discharging
|
||||
soc_percent: r1(d.readInt16BE(6) / 10)
|
||||
};
|
||||
}
|
||||
|
||||
// 0x91 — range tensioni cella
|
||||
async getCellVoltageRange() {
|
||||
const d = await this._sendBatch('91');
|
||||
if (!d) return null;
|
||||
return {
|
||||
highest_voltage: r3(d.readInt16BE(0) / 1000),
|
||||
highest_cell: d.readInt8(2),
|
||||
lowest_voltage: r3(d.readInt16BE(3) / 1000),
|
||||
lowest_cell: d.readInt8(5)
|
||||
};
|
||||
}
|
||||
|
||||
// 0x92 — range temperature
|
||||
async getTemperatureRange() {
|
||||
const d = await this._sendBatch('92');
|
||||
if (!d) return null;
|
||||
return {
|
||||
highest_temperature: d.readInt8(0) - 40,
|
||||
highest_sensor: d.readInt8(1),
|
||||
lowest_temperature: d.readInt8(2) - 40,
|
||||
lowest_sensor: d.readInt8(3)
|
||||
};
|
||||
}
|
||||
|
||||
// 0x93 — MOSFET + capacità residua
|
||||
async getMosfetStatus() {
|
||||
const d = await this._sendBatch('93');
|
||||
if (!d) return null;
|
||||
const m = d.readInt8(0);
|
||||
return {
|
||||
mode: m === 0 ? 'stationary' : m === 1 ? 'charging' : 'discharging',
|
||||
charging_mosfet: !!d[1],
|
||||
discharging_mosfet: !!d[2],
|
||||
capacity_ah: r3(d.readInt32BE(4) / 1000)
|
||||
};
|
||||
}
|
||||
|
||||
// 0x94 — status (n. celle, sensori, cicli, DI/DO)
|
||||
async getStatus() {
|
||||
const d = await this._sendBatch('94');
|
||||
if (!d) return null;
|
||||
const stateByte = d.readInt8(4);
|
||||
const stateNames = ['DI1','DI2','DI3','DI4','DO1','DO2','DO3','DO4'];
|
||||
const states = {};
|
||||
for (let i = 0; i < 8; i++) states[stateNames[i]] = !!((stateByte >> i) & 1);
|
||||
const data = {
|
||||
cells: d.readInt8(0),
|
||||
temperature_sensors: d.readInt8(1),
|
||||
charger_running: !!d[2],
|
||||
load_running: !!d[3],
|
||||
states,
|
||||
cycles: d.readInt16BE(5)
|
||||
};
|
||||
this.status = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
async _ensureStatus() {
|
||||
if (!this.status) await this.getStatus();
|
||||
if (!this.status) throw new Error('cannot read BMS status');
|
||||
return this.status;
|
||||
}
|
||||
|
||||
_splitFrames(frames, total, perFrame, parser) {
|
||||
const out = {};
|
||||
let expected = 1;
|
||||
for (const f of frames) {
|
||||
const { idx, vals } = parser(f);
|
||||
if (idx !== expected) { this.log(`frame out of order: exp ${expected} got ${idx}`); continue; }
|
||||
for (let i = 0; i < perFrame; i++) {
|
||||
const k = (expected - 1) * perFrame + i + 1;
|
||||
if (k > total) return out;
|
||||
out[k] = vals[i];
|
||||
}
|
||||
expected++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 0x95 — tensioni cella (3/frame)
|
||||
async getCellVoltages() {
|
||||
const st = await this._ensureStatus();
|
||||
const max = Math.ceil(st.cells / 3);
|
||||
const frames = await this._sendBatch('95', { maxResponses: max, returnList: true });
|
||||
if (!frames) return null;
|
||||
const cells = this._splitFrames(frames, st.cells, 3, f => ({
|
||||
idx: f.readInt8(0),
|
||||
vals: [f.readInt16BE(1), f.readInt16BE(3), f.readInt16BE(5)]
|
||||
}));
|
||||
for (const k of Object.keys(cells)) cells[k] = cells[k] / 1000;
|
||||
return cells;
|
||||
}
|
||||
|
||||
// 0x96 — temperature (7/frame, raw - 40 = °C)
|
||||
async getTemperatures() {
|
||||
const st = await this._ensureStatus();
|
||||
const max = Math.ceil(st.temperature_sensors / 7);
|
||||
const frames = await this._sendBatch('96', { maxResponses: max, returnList: true });
|
||||
if (!frames) return null;
|
||||
const t = this._splitFrames(frames, st.temperature_sensors, 7, f => ({
|
||||
idx: f.readInt8(0),
|
||||
vals: [f.readInt8(1), f.readInt8(2), f.readInt8(3), f.readInt8(4), f.readInt8(5), f.readInt8(6), f.readInt8(7)]
|
||||
}));
|
||||
for (const k of Object.keys(t)) t[k] = t[k] - 40;
|
||||
return t;
|
||||
}
|
||||
|
||||
// 0x98 — errori (bitmap)
|
||||
async getErrors() {
|
||||
const d = await this._sendBatch('98');
|
||||
if (!d) return [];
|
||||
const errors = [];
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
const b = d[i];
|
||||
if (!b) continue;
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
if ((b >> bit) & 1) {
|
||||
errors.push((errors[i] && errors[i][bit]) || `unknown(${i}.${bit})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BMS;
|
||||
Reference in New Issue
Block a user