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;