- 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.
151 lines
5.3 KiB
JavaScript
151 lines
5.3 KiB
JavaScript
// SignalK plugin — Daly BMS via USB/RS485 (solo USB).
|
|
// Polling periodico → delta SignalK. POST read-only on-demand.
|
|
'use strict';
|
|
|
|
const DalyProtocol = require('./lib/daly-protocol');
|
|
|
|
const DEFAULTS = Object.freeze({
|
|
device: '/dev/ttyUSB0',
|
|
batteryId: 'house',
|
|
pollSocMs: 2000,
|
|
pollStatusMs: 30000,
|
|
pollCellsMs: 10000,
|
|
retries: 5
|
|
});
|
|
|
|
module.exports = function (app) {
|
|
const plugin = {
|
|
id: 'signalk-daly-bms',
|
|
name: 'Daly BMS (USB)',
|
|
description: 'Legge un BMS Daly via seriale USB/RS485 e pubblica su SignalK'
|
|
};
|
|
|
|
plugin.schema = {
|
|
type: 'object',
|
|
properties: {
|
|
device: { type: 'string', default: DEFAULTS.device, title: 'Serial device (USB)' },
|
|
batteryId: { type: 'string', default: DEFAULTS.batteryId, title: 'SignalK battery id' },
|
|
pollSocMs: { type: 'integer', default: DEFAULTS.pollSocMs, title: 'Polling SOC/V/A (ms)' },
|
|
pollStatusMs: { type: 'integer', default: DEFAULTS.pollStatusMs, title: 'Polling status (ms)' },
|
|
pollCellsMs: { type: 'integer', default: DEFAULTS.pollCellsMs, title: 'Polling celle/temperature (ms)' },
|
|
retries: { type: 'integer', default: DEFAULTS.retries, title: 'Retry per richiesta' }
|
|
}
|
|
};
|
|
|
|
let bms = null;
|
|
let opts = null;
|
|
const timers = new Set();
|
|
|
|
const sk = (path, value) => ({ path: `electrical.batteries.${opts.batteryId}.${path}`, value });
|
|
const sendDelta = values => values && values.length && app.handleMessage(plugin.id, { updates: [{ values }] });
|
|
|
|
const safe = async (label, fn) => {
|
|
try { return await fn(); }
|
|
catch (e) { app.error(`${label}: ${e.message}`); return null; }
|
|
};
|
|
|
|
// --- pollers ---
|
|
async function pollSoc() {
|
|
const s = await safe('pollSoc', () => bms.getSoc());
|
|
if (!s) return;
|
|
sendDelta([
|
|
sk('voltage', s.total_voltage),
|
|
sk('current', s.current), // SignalK: + = uscita (scarica), come Daly
|
|
sk('capacity.stateOfCharge', s.soc_percent / 100)
|
|
]);
|
|
}
|
|
|
|
async function pollStatus() {
|
|
const s = await safe('pollStatus', () => bms.getStatus());
|
|
if (!s) return;
|
|
sendDelta([
|
|
sk('cycles', s.cycles),
|
|
sk('cellsCount', s.cells),
|
|
sk('chargerRunning', s.charger_running),
|
|
sk('loadRunning', s.load_running)
|
|
]);
|
|
}
|
|
|
|
async function pollCells() {
|
|
const [cells, temps, errs] = await Promise.all([
|
|
safe('getCellVoltages', () => bms.getCellVoltages()),
|
|
safe('getTemperatures', () => bms.getTemperatures()),
|
|
safe('getErrors', () => bms.getErrors())
|
|
]);
|
|
const values = [];
|
|
if (cells) for (const [k, v] of Object.entries(cells)) values.push(sk(`cells.${k}.voltage`, v));
|
|
if (temps) {
|
|
const list = Object.values(temps);
|
|
const toK = c => Math.round((c + 273.15) * 100) / 100; // °C → K, 2 decimali
|
|
if (list.length) values.push(sk('temperature', toK(list[0])));
|
|
for (const [k, v] of Object.entries(temps)) values.push(sk(`temperatures.${k}`, toK(v)));
|
|
}
|
|
if (errs) values.push(sk('errors', errs));
|
|
sendDelta(values);
|
|
}
|
|
|
|
const startTimer = (fn, ms) => {
|
|
const t = setInterval(() => { fn(); }, ms);
|
|
timers.add(t);
|
|
};
|
|
|
|
plugin.start = async function (options = {}) {
|
|
opts = { ...DEFAULTS, ...options };
|
|
app.setPluginStatus(`Apertura ${opts.device}…`);
|
|
|
|
bms = new DalyProtocol({
|
|
device: opts.device,
|
|
retries: opts.retries,
|
|
log: msg => app.debug(msg)
|
|
});
|
|
|
|
try {
|
|
await bms.open();
|
|
await bms.getStatus(); // necessario per cell/temp
|
|
app.setPluginStatus(`Connesso a ${opts.device}`);
|
|
} catch (e) {
|
|
app.setPluginError(`init failed: ${e.message}`);
|
|
return;
|
|
}
|
|
|
|
// primo giro immediato + polling periodico
|
|
await Promise.all([pollStatus(), pollSoc(), pollCells()]);
|
|
startTimer(pollSoc, opts.pollSocMs);
|
|
startTimer(pollStatus, opts.pollStatusMs);
|
|
startTimer(pollCells, opts.pollCellsMs);
|
|
};
|
|
|
|
plugin.stop = async function () {
|
|
for (const t of timers) clearInterval(t);
|
|
timers.clear();
|
|
if (bms) { await bms.close().catch(() => {}); bms = null; }
|
|
app.setPluginStatus('stopped');
|
|
};
|
|
|
|
// --- API REST: solo letture on-demand ---
|
|
plugin.registerWithRouter = function (router) {
|
|
const wrap = fn => async (_req, res) => {
|
|
if (!bms) return res.status(503).json({ error: 'plugin not started' });
|
|
try { res.json(await fn()); }
|
|
catch (e) { res.status(500).json({ error: e.message }); }
|
|
};
|
|
router.post('/soc', wrap(() => bms.getSoc()));
|
|
router.post('/status', wrap(() => bms.getStatus()));
|
|
router.post('/temperatures', wrap(() => bms.getTemperatures()));
|
|
router.post('/cell-voltages', wrap(() => bms.getCellVoltages()));
|
|
router.post('/cell-range', wrap(() => bms.getCellVoltageRange()));
|
|
router.post('/temp-range', wrap(() => bms.getTemperatureRange()));
|
|
router.post('/mosfet', wrap(() => bms.getMosfetStatus()));
|
|
router.post('/errors', wrap(() => bms.getErrors()));
|
|
router.post('/all', wrap(async () => {
|
|
const [soc, status, cell_voltages, temperatures, mosfet, errors] = await Promise.all([
|
|
bms.getSoc(), bms.getStatus(), bms.getCellVoltages(),
|
|
bms.getTemperatures(), bms.getMosfetStatus(), bms.getErrors()
|
|
]);
|
|
return { soc, status, cell_voltages, temperatures, mosfet, errors };
|
|
}));
|
|
};
|
|
|
|
return plugin;
|
|
};
|