Refactor MPPT CAN reader with options and logging

This commit is contained in:
Giuseppe Raffa
2026-06-06 14:11:45 +02:00
parent c893fa3edf
commit 2cc320b484
4 changed files with 199 additions and 38 deletions

View File

@@ -23,9 +23,9 @@ const status1Flags = {
1: 'ChgEna', // Ponte Pin1-Pin8 inserito (enable hardware) 1: 'ChgEna', // Ponte Pin1-Pin8 inserito (enable hardware)
2: 'ChgOk', // Condizioni di ricarica OK 2: 'ChgOk', // Condizioni di ricarica OK
3: 'PwrOn', // Stadio di potenza acceso (eroga corrente) 3: 'PwrOn', // Stadio di potenza acceso (eroga corrente)
4: 'StorMod', // Modalita' storage attiva 4: 'CtrlEna', // Algoritmo MPPT abilitato
5: 'FloatMod', // Modalita' float (mantenimento) 5: 'StorMod', // Modalita' storage attiva
6: 'CtrlEna', // Algoritmo MPPT abilitato 6: 'FloatMod', // Modalita' float (mantenimento)
7: 'CCapRst', // Reset contatore Ah 7: 'CCapRst', // Reset contatore Ah
8: 'Ph1Ena', // Fase 1 abilitata 8: 'Ph1Ena', // Fase 1 abilitata
9: 'Ph2Ena', // Fase 2 abilitata 9: 'Ph2Ena', // Fase 2 abilitata
@@ -61,14 +61,14 @@ const registerAddresses = {
deviceAddress: 1, // Addr - indirizzo del dispositivo (confermato: usato per discovery) deviceAddress: 1, // Addr - indirizzo del dispositivo (confermato: usato per discovery)
status1: 4, // St1 - flag di stato (confermato: "registro 4") status1: 4, // St1 - flag di stato (confermato: "registro 4")
warning: 6, // Warn - flag di warning/errore (confermato: "registro 6") warning: 6, // Warn - flag di warning/errore (confermato: "registro 6")
chargeCapacity: 7, // Chg_Cap - Ah caricati (da verificare) voltageInput: 16, // Vi - tensione ingresso (manuale: registro 16)
voltageInput: 16, // Vi - tensione ingresso (da verificare) currentInput: 18, // Ii - corrente ingresso (manuale: registro 18)
currentInput: 17, // Ii - corrente ingresso (da verificare) voltageOutput: 22, // Vo - tensione uscita (manuale: registro 22)
voltageOutput: 18, // Vo - tensione uscita (da verificare) currentOutput: 27, // Io - corrente uscita (manuale: registro 27)
currentOutput: 19, // Io - corrente uscita (da verificare)
powerInput: 20, // Pi - potenza ingresso (confermato) powerInput: 20, // Pi - potenza ingresso (confermato)
powerOutput: 21, // Po - potenza uscita (confermato) powerOutput: 31, // Po - potenza uscita (manuale: registro 31)
temperature1: 35, // T1 - temperatura fase 1 (confermato) chargeCapacity: null, // Chg_Cap - nel PDF hw1.2r01 collide con T1 sul registro 35: non pollare finche' non confermato
temperature1: 35, // T1 - temperatura fase 1 (manuale: registro 35)
temperature2: 36, // T2 - temperatura fase 2 (confermato) temperature2: 36, // T2 - temperatura fase 2 (confermato)
}; };
@@ -84,7 +84,6 @@ const pollRegisters = [
registerAddresses.currentInput, registerAddresses.currentInput,
registerAddresses.voltageOutput, registerAddresses.voltageOutput,
registerAddresses.currentOutput, registerAddresses.currentOutput,
registerAddresses.chargeCapacity,
registerAddresses.temperature1, registerAddresses.temperature1,
registerAddresses.temperature2, registerAddresses.temperature2,
]; ];

View File

@@ -26,11 +26,18 @@ const numericRegisterMap = {
[registerAddresses.currentOutput]: { field: 'currentOutput', factor: conversionsFactors.currentOutput, signed: false }, [registerAddresses.currentOutput]: { field: 'currentOutput', factor: conversionsFactors.currentOutput, signed: false },
[registerAddresses.powerInput]: { field: 'powerInput', factor: conversionsFactors.powerInput, signed: false }, [registerAddresses.powerInput]: { field: 'powerInput', factor: conversionsFactors.powerInput, signed: false },
[registerAddresses.powerOutput]: { field: 'powerOutput', factor: conversionsFactors.powerOutput, signed: false }, [registerAddresses.powerOutput]: { field: 'powerOutput', factor: conversionsFactors.powerOutput, signed: false },
[registerAddresses.chargeCapacity]: { field: 'chargeCapacity', factor: conversionsFactors.chargeCapacity, signed: false },
[registerAddresses.temperature1]: { field: 'temperature1', factor: conversionsFactors.temperaturePhase1, signed: true }, [registerAddresses.temperature1]: { field: 'temperature1', factor: conversionsFactors.temperaturePhase1, signed: true },
[registerAddresses.temperature2]: { field: 'temperature2', factor: conversionsFactors.temperaturePhase2, signed: true }, [registerAddresses.temperature2]: { field: 'temperature2', factor: conversionsFactors.temperaturePhase2, signed: true },
}; };
if (Number.isInteger(registerAddresses.chargeCapacity)) {
numericRegisterMap[registerAddresses.chargeCapacity] = {
field: 'chargeCapacity',
factor: conversionsFactors.chargeCapacity,
signed: false,
};
}
// Arrotondamenti // Arrotondamenti
const roundedTo1 = (v) => Number(v.toFixed(1)); const roundedTo1 = (v) => Number(v.toFixed(1));
const roundedTo2 = (v) => Number(v.toFixed(2)); const roundedTo2 = (v) => Number(v.toFixed(2));
@@ -104,10 +111,12 @@ class MPPT {
rawValue = valore raw a 16 bit ricevuto nella risposta (big-endian, gia' decodificato) rawValue = valore raw a 16 bit ricevuto nella risposta (big-endian, gia' decodificato)
*/ */
updateRegister(regAddr, rawValue) { updateRegister(regAddr, rawValue) {
this.log(`[${this.name}] update registro ${regAddr}: raw=${rawValue}`);
if (regAddr === registerAddresses.status1) { if (regAddr === registerAddresses.status1) {
// Registro di stato St1: decodifica i bit in flag operativi // Registro di stato St1: decodifica i bit in flag operativi
this.state.status1Raw = rawValue; this.state.status1Raw = rawValue;
this.state.flags = decodeFlags(rawValue, status1Flags); this.state.flags = decodeFlags(rawValue, status1Flags);
this.log(`[${this.name}] St1 flags=${JSON.stringify(this.state.flags)}`);
} else if (regAddr === registerAddresses.warning) { } else if (regAddr === registerAddresses.warning) {
// Registro Warning: decodifica i bit in codici di errore/warning // Registro Warning: decodifica i bit in codici di errore/warning
this.state.warningRaw = rawValue; this.state.warningRaw = rawValue;
@@ -122,6 +131,7 @@ class MPPT {
let value = rawValue; let value = rawValue;
if (target.signed && value >= 0x8000) value -= 0x10000; // intero con segno if (target.signed && value >= 0x8000) value -= 0x10000; // intero con segno
this.state[target.field] = value / target.factor; this.state[target.field] = value / target.factor;
this.log(`[${this.name}] ${target.field}=${this.state[target.field]} (raw=${rawValue}, conv=${target.factor})`);
} }
this.state.lastUpdate = Date.now(); this.state.lastUpdate = Date.now();
} }
@@ -162,6 +172,7 @@ class MPPT {
updates.push({ path: `${base}.flags`, value: s.flags }); updates.push({ path: `${base}.flags`, value: s.flags });
updates.push({ path: `${base}.warnings`, value: s.warnings }); updates.push({ path: `${base}.warnings`, value: s.warnings });
this.log(`[${this.name}] update SignalK costruiti=${updates.length} snapshot=${JSON.stringify(this.getSnapshot())}`);
return updates; return updates;
} }

View File

@@ -85,10 +85,19 @@ class MPPTReader extends EventEmitter {
}); });
this.mppts.set(config.id, mppt); this.mppts.set(config.id, mppt);
} }
this.log(
`[reader] configurazione: device=${this.device}, baudRate=${this.baudRate}, ` +
`pollIntervalMs=${this.pollIntervalMs}, timeoutMs=${this.timeoutMs}, retries=${this.retries}, ` +
`dtr=${this.dtr}, rts=${this.rts}, mppts=${JSON.stringify(Array.from(this.mppts.values()).map((mppt) => ({
name: mppt.name,
address: mppt.address,
})))}`
);
} }
// Apre la porta seriale e avvia il loop di polling // Apre la porta seriale e avvia il loop di polling
async open() { async open() {
this.log(`[reader] open(): preparo apertura seriale ${this.device}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
this.port = new SerialPort({ this.port = new SerialPort({
@@ -101,7 +110,10 @@ class MPPTReader extends EventEmitter {
}); });
// Protocollo binario: si leggono i byte grezzi (l'EOF 0x0D puo' comparire nei dati) // Protocollo binario: si leggono i byte grezzi (l'EOF 0x0D puo' comparire nei dati)
this.port.on('data', (chunk) => this._onData(chunk)); this.port.on('data', (chunk) => {
this.log(`[reader] RX chunk len=${chunk.length} hex=${chunk.toString('hex')}`);
this._onData(chunk);
});
this.port.on('error', (err) => { this.port.on('error', (err) => {
this.log(`[reader] errore porta seriale: ${err.message}`); this.log(`[reader] errore porta seriale: ${err.message}`);
@@ -109,6 +121,7 @@ class MPPTReader extends EventEmitter {
}); });
this.port.on('close', () => { this.port.on('close', () => {
this.log('[reader] porta seriale chiusa');
this.isOpen = false; this.isOpen = false;
this._stopPolling(); this._stopPolling();
this.emit('close'); this.emit('close');
@@ -117,26 +130,33 @@ class MPPTReader extends EventEmitter {
this.port.open(async (err) => { this.port.open(async (err) => {
if (err) { if (err) {
this.log(`[reader] apertura seriale fallita: ${err.message}`);
reject(new SerialDriverError(`Impossibile aprire ${this.device}: ${err.message}`, err)); reject(new SerialDriverError(`Impossibile aprire ${this.device}: ${err.message}`, err));
return; return;
} }
try { try {
this.log(`[reader] porta aperta: ${this.device}`);
// Imposta le linee DTR/RTS come da protocollo Poweren e attende l'assestamento // Imposta le linee DTR/RTS come da protocollo Poweren e attende l'assestamento
// dell'adapter (l'FT232R può resettare il target all'apertura della porta). // dell'adapter (l'FT232R può resettare il target all'apertura della porta).
await this._setControlLines(this.dtr, this.rts); await this._setControlLines(this.dtr, this.rts);
this.log(`[reader] linee controllo impostate: dtr=${this.dtr}, rts=${this.rts}`);
await this._delay(serialDefaults.openSettleMs); await this._delay(serialDefaults.openSettleMs);
this.log(`[reader] attesa assestamento completata: ${serialDefaults.openSettleMs}ms`);
this.isOpen = true; this.isOpen = true;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
// Invia la prima richiesta UART: l'MPPT Poweren risponde solo dopo una lettura. // Invia la prima richiesta UART: l'MPPT Poweren risponde solo dopo una lettura.
await this._discoverDevices(); await this._discoverDevices();
this.log('[reader] discovery iniziale completata, avvio polling');
this.emit('open'); this.emit('open');
this._startPolling(); this._startPolling();
resolve(); resolve();
} catch (initErr) { } catch (initErr) {
this.log(`[reader] inizializzazione fallita: ${initErr.stack || initErr.message}`);
reject(initErr); reject(initErr);
} }
}); });
} catch (err) { } catch (err) {
this.log(`[reader] errore creazione SerialPort: ${err.stack || err.message}`);
reject(new SerialDriverError(err.message, err)); reject(new SerialDriverError(err.message, err));
} }
}); });
@@ -144,6 +164,7 @@ class MPPTReader extends EventEmitter {
// Chiude la porta e ferma il polling // Chiude la porta e ferma il polling
async close() { async close() {
this.log('[reader] close(): arresto richiesto');
this.shouldReconnect = false; this.shouldReconnect = false;
if (this.reconnectTimer) { if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer); clearTimeout(this.reconnectTimer);
@@ -159,6 +180,7 @@ class MPPTReader extends EventEmitter {
await new Promise((resolve) => this.port.close(() => resolve())); await new Promise((resolve) => this.port.close(() => resolve()));
} }
this.isOpen = false; this.isOpen = false;
this.log('[reader] close(): completato');
} }
// Costruisce gli update aggregati (somma dei lati attivi) // Costruisce gli update aggregati (somma dei lati attivi)
@@ -179,23 +201,30 @@ class MPPTReader extends EventEmitter {
if (!anyOnline) return []; if (!anyOnline) return [];
const roundedTo2 = (v) => Number(v.toFixed(2)); const roundedTo2 = (v) => Number(v.toFixed(2));
return [ const aggregateUpdates = [
{ path: 'meb.solar.total.panelPower', value: roundedTo2(panelPowerSum) }, { path: 'meb.solar.total.panelPower', value: roundedTo2(panelPowerSum) },
{ path: 'meb.solar.total.power', value: roundedTo2(powerSum) }, { path: 'meb.solar.total.power', value: roundedTo2(powerSum) },
{ path: 'meb.solar.total.current', value: roundedTo2(currentSum) }, { path: 'meb.solar.total.current', value: roundedTo2(currentSum) },
]; ];
this.log(`[reader] aggregati calcolati: ${JSON.stringify(aggregateUpdates)}`);
return aggregateUpdates;
} }
// ---- loop di polling ---- // ---- loop di polling ----
_startPolling() { _startPolling() {
if (this.pollTimer) return; if (this.pollTimer) return;
this.log('[reader] startPolling(): timer polling attivato');
// setTimeout ricorsivo: garantisce che un ciclo finisca prima del successivo // setTimeout ricorsivo: garantisce che un ciclo finisca prima del successivo
const tick = async () => { const tick = async () => {
if (!this.isOpen) return; if (!this.isOpen) {
this.log('[reader] tick polling ignorato: porta non aperta');
return;
}
try { try {
await this._pollCycle(); await this._pollCycle();
} catch (err) { } catch (err) {
this.log(`[reader] errore ciclo polling: ${err.stack || err.message}`);
this.emit('error', err); this.emit('error', err);
} }
if (this.isOpen) { if (this.isOpen) {
@@ -207,6 +236,7 @@ class MPPTReader extends EventEmitter {
_stopPolling() { _stopPolling() {
if (this.pollTimer) { if (this.pollTimer) {
this.log('[reader] stopPolling(): timer polling fermato');
clearTimeout(this.pollTimer); clearTimeout(this.pollTimer);
this.pollTimer = null; this.pollTimer = null;
} }
@@ -214,14 +244,22 @@ class MPPTReader extends EventEmitter {
// Un ciclo di polling: per ogni MPPT legge tutti i registri e pubblica gli update // Un ciclo di polling: per ogni MPPT legge tutti i registri e pubblica gli update
async _pollCycle() { async _pollCycle() {
if (this.isPolling) return; if (this.isPolling) {
this.log('[reader] ciclo polling saltato: ciclo precedente ancora attivo');
return;
}
this.isPolling = true; this.isPolling = true;
const cycleStartedAt = Date.now();
this.log(`[reader] ciclo polling avviato: mppts=${this.mppts.size}, registri=${pollRegisters.join(',')}`);
try { try {
for (const mppt of this.mppts.values()) { for (const mppt of this.mppts.values()) {
let updated = false; let updated = false;
this.log(`[reader] polling MPPT ${mppt.name} address=${mppt.address}`);
for (const regAddr of pollRegisters) { for (const regAddr of pollRegisters) {
try { try {
this.log(`[reader] leggo MPPT ${mppt.name} address=${mppt.address} registro=${regAddr}`);
const rawValue = await this._readRegister(mppt.address, regAddr); const rawValue = await this._readRegister(mppt.address, regAddr);
this.log(`[reader] letto MPPT ${mppt.name} address=${mppt.address} registro=${regAddr} raw=${rawValue}`);
mppt.updateRegister(regAddr, rawValue); mppt.updateRegister(regAddr, rawValue);
updated = true; updated = true;
} catch (err) { } catch (err) {
@@ -231,11 +269,15 @@ class MPPTReader extends EventEmitter {
} }
if (updated) { if (updated) {
const updates = mppt.buildSignalKUpdates(); const updates = mppt.buildSignalKUpdates();
this.log(`[reader] MPPT ${mppt.name}: updated=true, updateSignalK=${updates.length}`);
if (updates.length) this.emit('updates', updates); if (updates.length) this.emit('updates', updates);
} else {
this.log(`[reader] MPPT ${mppt.name}: nessun registro aggiornato in questo ciclo`);
} }
} }
} finally { } finally {
this.isPolling = false; this.isPolling = false;
this.log(`[reader] ciclo polling completato in ${Date.now() - cycleStartedAt}ms`);
} }
} }
@@ -277,10 +319,24 @@ class MPPTReader extends EventEmitter {
let lastError = null; let lastError = null;
for (let attempt = 0; attempt <= this.retries; attempt++) { for (let attempt = 0; attempt <= this.retries; attempt++) {
try { try {
const responseData = await this._transact(address, this._buildReadPacket(address, regAddr)); const packet = this._buildReadPacket(address, regAddr);
return this._parseRegisterValue(responseData, regAddr); this.log(
`[reader] richiesta registro: address=${address}, reg=${regAddr}, ` +
`tentativo=${attempt + 1}/${this.retries + 1}, packet=${packet.toString('hex')}`
);
const responseData = await this._transact(address, packet);
const rawValue = this._parseRegisterValue(responseData, regAddr);
this.log(
`[reader] risposta registro: address=${address}, reg=${regAddr}, ` +
`data=${responseData.toString('hex')}, raw=${rawValue}`
);
return rawValue;
} catch (err) { } catch (err) {
lastError = err; lastError = err;
this.log(
`[reader] tentativo fallito: address=${address}, reg=${regAddr}, ` +
`tentativo=${attempt + 1}/${this.retries + 1}, errore=${err.message}`
);
if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs); if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs);
} }
} }
@@ -308,6 +364,7 @@ class MPPTReader extends EventEmitter {
_transact(address, packet) { _transact(address, packet) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!this.isOpen) { if (!this.isOpen) {
this.log(`[reader] transact rifiutata: porta non aperta, address=${address}, packet=${packet.toString('hex')}`);
reject(new SerialDriverError('porta non aperta')); reject(new SerialDriverError('porta non aperta'));
return; return;
} }
@@ -317,26 +374,33 @@ class MPPTReader extends EventEmitter {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (this.pendingTransaction && this.pendingTransaction.timer === timer) { if (this.pendingTransaction && this.pendingTransaction.timer === timer) {
this.pendingTransaction = null; this.pendingTransaction = null;
this.log(`[reader] timeout transazione: address=${address}, reg=${packet[5]}, packet=${packet.toString('hex')}`);
reject(new RegisterReadTimeoutError(address, packet[5])); reject(new RegisterReadTimeoutError(address, packet[5]));
} }
}, this.timeoutMs); }, this.timeoutMs);
this.pendingTransaction = { resolve, reject, timer }; this.pendingTransaction = { resolve, reject, timer, address, regAddr: packet[5], packet };
this.log(`[reader] TX write: address=${address}, reg=${packet[5]}, hex=${packet.toString('hex')}`);
this.port.write(packet, (err) => { this.port.write(packet, (err) => {
if (err) { if (err) {
clearTimeout(timer); clearTimeout(timer);
this.pendingTransaction = null; this.pendingTransaction = null;
this.log(`[reader] errore TX write: ${err.message}`);
reject(new SerialDriverError(err.message, err)); reject(new SerialDriverError(err.message, err));
return; return;
} }
this.log(`[reader] TX write callback OK: address=${address}, reg=${packet[5]}`);
if (typeof this.port.drain === 'function') { if (typeof this.port.drain === 'function') {
this.port.drain((drainErr) => { this.port.drain((drainErr) => {
if (drainErr && this.pendingTransaction && this.pendingTransaction.timer === timer) { if (drainErr && this.pendingTransaction && this.pendingTransaction.timer === timer) {
clearTimeout(timer); clearTimeout(timer);
this.pendingTransaction = null; this.pendingTransaction = null;
this.log(`[reader] errore TX drain: ${drainErr.message}`);
reject(new SerialDriverError(drainErr.message, drainErr)); reject(new SerialDriverError(drainErr.message, drainErr));
return;
} }
this.log(`[reader] TX drain OK: address=${address}, reg=${packet[5]}`);
}); });
} }
}); });
@@ -363,13 +427,18 @@ class MPPTReader extends EventEmitter {
_onData(chunk) { _onData(chunk) {
this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]); this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]);
this.log(`[reader] rxBuffer len=${this.rxBuffer.length} hex=${this.rxBuffer.toString('hex')}`);
let frame; let frame;
while ((frame = this._extractFrame()) !== null) { while ((frame = this._extractFrame()) !== null) {
this.log(`[reader] frame valido: dst=${frame.dst}, src=${frame.src}, data=${frame.data.toString('hex')}`);
if (this.pendingTransaction) { if (this.pendingTransaction) {
const { resolve, timer } = this.pendingTransaction; const { resolve, timer, address, regAddr } = this.pendingTransaction;
clearTimeout(timer); clearTimeout(timer);
this.pendingTransaction = null; this.pendingTransaction = null;
this.log(`[reader] transazione risolta: address=${address}, reg=${regAddr}, data=${frame.data.toString('hex')}`);
resolve(frame.data); resolve(frame.data);
} else {
this.log(`[reader] frame ricevuto senza transazione pendente: data=${frame.data.toString('hex')}`);
} }
} }
} }
@@ -385,16 +454,23 @@ class MPPTReader extends EventEmitter {
// Allinea il buffer al primo SOF disponibile // Allinea il buffer al primo SOF disponibile
let sofIndex = this.rxBuffer.indexOf(startOfFrame); let sofIndex = this.rxBuffer.indexOf(startOfFrame);
if (sofIndex < 0) { if (sofIndex < 0) {
this.log(`[reader] nessun SOF nel buffer, scarto ${this.rxBuffer.length} byte: ${this.rxBuffer.toString('hex')}`);
this.rxBuffer = Buffer.alloc(0); this.rxBuffer = Buffer.alloc(0);
return null; return null;
} }
if (sofIndex > 0) { if (sofIndex > 0) {
this.log(`[reader] scarto ${sofIndex} byte prima del SOF: ${this.rxBuffer.slice(0, sofIndex).toString('hex')}`);
this.rxBuffer = this.rxBuffer.slice(sofIndex); this.rxBuffer = this.rxBuffer.slice(sofIndex);
} }
if (this.rxBuffer.length < headerLength) return null; if (this.rxBuffer.length < headerLength) return null;
const dlen = (this.rxBuffer[3] << 8) | this.rxBuffer[4]; const dlen = (this.rxBuffer[3] << 8) | this.rxBuffer[4];
if (dlen > 512) {
this.log(`[reader] frame scartato: DLEN non plausibile (${dlen}), buffer=${this.rxBuffer.toString('hex')}`);
this.rxBuffer = this.rxBuffer.slice(1);
return null;
}
const totalLength = headerLength + dlen + tailLength; const totalLength = headerLength + dlen + tailLength;
if (this.rxBuffer.length < totalLength) return null; if (this.rxBuffer.length < totalLength) return null;
@@ -408,13 +484,15 @@ class MPPTReader extends EventEmitter {
this.rxBuffer = this.rxBuffer.slice(totalLength); this.rxBuffer = this.rxBuffer.slice(totalLength);
if (eof !== endOfFrame) { if (eof !== endOfFrame) {
this.log('[reader] frame scartato: EOF mancante'); this.log(`[reader] frame scartato: EOF mancante, frame=${this.rxBuffer.toString('hex')}`);
return null; return null;
} }
let sum = 0; let sum = 0;
for (const b of data) sum += b; for (const b of data) sum += b;
if ((sum & 0xFFFF) !== checksum) { if ((sum & 0xFFFF) !== checksum) {
this.log('[reader] frame scartato: checksum errato'); this.log(
`[reader] frame scartato: checksum errato, atteso=${sum & 0xFFFF}, ricevuto=${checksum}, data=${data.toString('hex')}`
);
return null; return null;
} }
@@ -426,10 +504,14 @@ class MPPTReader extends EventEmitter {
_setControlLines(dtr, rts) { _setControlLines(dtr, rts) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (!this.port || typeof this.port.set !== 'function') { if (!this.port || typeof this.port.set !== 'function') {
this.log('[reader] setControlLines ignorato: port.set non disponibile');
resolve(); resolve();
return; return;
} }
this.port.set({ dtr, rts }, () => resolve()); this.port.set({ dtr, rts }, (err) => {
if (err) this.log(`[reader] errore setControlLines: ${err.message}`);
resolve();
});
}); });
} }

View File

@@ -1,6 +1,17 @@
const { MPPTReader } = require('./core/reader'); const { MPPTReader } = require('./core/reader');
module.exports = function (app) { module.exports = function (app) {
const defaultPluginOptions = {
device: '/dev/ttyUSB0',
baudRate: 115200,
publishIntervalMs: 1000,
pollIntervalMs: 1000,
mppts: [
{ id: 'port', address: 50 },
{ id: 'starboard', address: 60 },
],
};
const plugin = { const plugin = {
id: 'meb-solars', id: 'meb-solars',
name: 'MEB Solar Panels', name: 'MEB Solar Panels',
@@ -53,67 +64,84 @@ module.exports = function (app) {
}, },
default: [ default: [
{ id: 'port', address: 50 }, { id: 'port', address: 50 },
{ id: 'starboard', address: 60 },
], ],
}, },
}, },
}; };
plugin.start = function (options) { plugin.start = function (options) {
app.debug('Avvio plugin meb-solars con opzioni:', options); const resolvedOptions = normalizeOptions(options);
const pluginLogger = createPluginLogger(app);
pluginLogger(`Avvio plugin con opzioni risolte: ${JSON.stringify(resolvedOptions)}`);
reader = new MPPTReader({ reader = new MPPTReader({
device: options.device, device: resolvedOptions.device,
baudRate: options.baudRate, baudRate: resolvedOptions.baudRate,
mppts: options.mppts, mppts: resolvedOptions.mppts,
pollIntervalMs: options.pollIntervalMs, pollIntervalMs: resolvedOptions.pollIntervalMs,
log: app.debug ? app.debug.bind(app) : console.log, log: pluginLogger,
}); });
// Bufferizza gli update in arrivo dal driver: pubblicazione periodica // Bufferizza gli update in arrivo dal driver: pubblicazione periodica
reader.on('updates', (updates) => { reader.on('updates', (updates) => {
pluginLogger(`Ricevuti ${updates.length} update dal reader`);
pluginLogger(`Primi update reader: ${JSON.stringify(updates.slice(0, 8))}`);
for (const update of updates) { for (const update of updates) {
pendingUpdatesByPath.set(update.path, update.value); pendingUpdatesByPath.set(update.path, update.value);
} }
pluginLogger(`Buffer SignalK aggiornato: ${pendingUpdatesByPath.size} path in attesa`);
}); });
reader.on('open', () => { reader.on('open', () => {
app.setPluginStatus(`Connesso alla seriale (${options.device} @ ${options.baudRate} bps)`); pluginLogger(`Evento open: seriale connessa (${resolvedOptions.device} @ ${resolvedOptions.baudRate} bps)`);
app.setPluginStatus(`Connesso alla seriale (${resolvedOptions.device} @ ${resolvedOptions.baudRate} bps)`);
}); });
reader.on('error', (err) => { reader.on('error', (err) => {
pluginLogger(`Evento error dal reader: ${err.stack || err.message}`);
app.error(`Errore driver UART: ${err.message}`); app.error(`Errore driver UART: ${err.message}`);
app.setPluginError(err.message); app.setPluginError(err.message);
}); });
reader.on('close', () => { reader.on('close', () => {
pluginLogger('Evento close: seriale disconnessa');
app.setPluginStatus('Seriale disconnessa, tentativo di riconnessione in corso'); app.setPluginStatus('Seriale disconnessa, tentativo di riconnessione in corso');
}); });
reader.open() reader.open()
.then(() => { .then(() => {
pluginLogger('Reader aperto: pubblico metadata e avvio intervallo SignalK');
publishMetadata(); publishMetadata();
// Pubblicazione periodica degli update bufferizzati + aggregati. // Pubblicazione periodica degli update bufferizzati + aggregati.
// Il polling dei registri UART e' gestito internamente dal driver. // Il polling dei registri UART e' gestito internamente dal driver.
publishInterval = setInterval(() => { publishInterval = setInterval(() => {
const aggregateUpdates = reader.buildAggregateUpdates(); const aggregateUpdates = reader.buildAggregateUpdates();
pluginLogger(`Tick publish: aggregati=${aggregateUpdates.length}, pendingPrima=${pendingUpdatesByPath.size}`);
for (const update of aggregateUpdates) { for (const update of aggregateUpdates) {
pendingUpdatesByPath.set(update.path, update.value); pendingUpdatesByPath.set(update.path, update.value);
} }
if (pendingUpdatesByPath.size === 0) return; if (pendingUpdatesByPath.size === 0) {
pluginLogger('Tick publish: nessun dato da pubblicare');
return;
}
const values = Array.from(pendingUpdatesByPath.entries()).map(([path, value]) => ({ path, value })); const values = Array.from(pendingUpdatesByPath.entries()).map(([path, value]) => ({ path, value }));
pendingUpdatesByPath.clear(); pendingUpdatesByPath.clear();
publishUpdates(values); pluginLogger(`Tick publish: pubblico ${values.length} path, esempi=${JSON.stringify(values.slice(0, 10))}`);
}, options.publishIntervalMs); publishUpdates(values, pluginLogger);
}, resolvedOptions.publishIntervalMs);
}) })
.catch((err) => { .catch((err) => {
pluginLogger(`Impossibile aprire il reader UART: ${err.stack || err.message}`);
app.error(`Impossibile aprire il driver UART: ${err.message}`); app.error(`Impossibile aprire il driver UART: ${err.message}`);
app.setPluginError(err.message); app.setPluginError(err.message);
}); });
}; };
plugin.stop = function () { plugin.stop = function () {
app.debug('Arresto plugin meb-solars'); const pluginLogger = createPluginLogger(app);
pluginLogger('Arresto plugin');
if (publishInterval) { clearInterval(publishInterval); publishInterval = null; } if (publishInterval) { clearInterval(publishInterval); publishInterval = null; }
if (reader) { if (reader) {
reader.close().catch((err) => app.error(`Errore in chiusura: ${err.message}`)); reader.close().catch((err) => app.error(`Errore in chiusura: ${err.message}`));
@@ -124,7 +152,7 @@ module.exports = function (app) {
}; };
// Pubblica un set di update come delta SignalK // Pubblica un set di update come delta SignalK
function publishUpdates(values) { function publishUpdates(values, pluginLogger = createPluginLogger(app)) {
const delta = { const delta = {
updates: [ updates: [
{ {
@@ -134,12 +162,21 @@ module.exports = function (app) {
}, },
], ],
}; };
try {
pluginLogger(`handleMessage values: ${JSON.stringify(delta)}`);
app.handleMessage(plugin.id, delta); app.handleMessage(plugin.id, delta);
pluginLogger(`handleMessage completato: ${values.length} path pubblicati`);
} catch (err) {
pluginLogger(`Errore app.handleMessage(values): ${err.stack || err.message}`);
app.error(`Errore pubblicazione SignalK: ${err.message}`);
app.setPluginError(err.message);
}
} }
// Pubblica i metadati (unita' di misura, displayName) per il data browser // Pubblica i metadati (unita' di misura, displayName) per il data browser
function publishMetadata() { function publishMetadata() {
if (!reader) return; if (!reader) return;
const pluginLogger = createPluginLogger(app);
const meta = []; const meta = [];
for (const mpptName of reader.mppts.keys()) { for (const mpptName of reader.mppts.keys()) {
const base = `meb.solar.${mpptName}`; const base = `meb.solar.${mpptName}`;
@@ -175,9 +212,41 @@ module.exports = function (app) {
{ path: 'meb.solar.total.current', value: { units: 'A', displayName: 'Total Battery Current' } }, { path: 'meb.solar.total.current', value: { units: 'A', displayName: 'Total Battery Current' } },
); );
app.handleMessage(plugin.id, { const metaDelta = {
updates: [{ meta, timestamp: new Date().toISOString() }], updates: [{ meta, timestamp: new Date().toISOString() }],
}); };
try {
pluginLogger(`Pubblico metadata SignalK: ${meta.length} path`);
pluginLogger(`Metadata delta: ${JSON.stringify(metaDelta)}`);
app.handleMessage(plugin.id, metaDelta);
pluginLogger('Metadata SignalK pubblicati');
} catch (err) {
pluginLogger(`Errore app.handleMessage(metadata): ${err.stack || err.message}`);
app.error(`Errore metadata SignalK: ${err.message}`);
app.setPluginError(err.message);
}
}
function normalizeOptions(options = {}) {
return {
device: options.device || defaultPluginOptions.device,
baudRate: Number(options.baudRate || defaultPluginOptions.baudRate),
publishIntervalMs: Number(options.publishIntervalMs || defaultPluginOptions.publishIntervalMs),
pollIntervalMs: Number(options.pollIntervalMs || defaultPluginOptions.pollIntervalMs),
mppts: Array.isArray(options.mppts) && options.mppts.length > 0
? options.mppts
: defaultPluginOptions.mppts,
};
}
function createPluginLogger(appInstance) {
return (message) => {
const logMessage = `[meb-solars] ${new Date().toISOString()} ${message}`;
console.log(logMessage);
if (appInstance && typeof appInstance.debug === 'function') {
appInstance.debug(logMessage);
}
};
} }
return plugin; return plugin;