Fixed for initial handshake to fetch data from panels.

This commit is contained in:
Giuseppe Raffa
2026-06-06 14:01:47 +02:00
parent 1d5bb340d9
commit c893fa3edf
2 changed files with 76 additions and 7 deletions

View File

@@ -58,6 +58,7 @@ const errorsFlags = {
qui i numeri. I valori confermati dalla documentazione sono indicati come "(confermato)". qui i numeri. I valori confermati dalla documentazione sono indicati come "(confermato)".
*/ */
const registerAddresses = { const registerAddresses = {
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) chargeCapacity: 7, // Chg_Cap - Ah caricati (da verificare)
@@ -102,6 +103,7 @@ const serialProtocol = {
startOfFrame: 0x42, // SOF startOfFrame: 0x42, // SOF
sourceHost: 0xFF, // SRC host/GUI sourceHost: 0xFF, // SRC host/GUI
endOfFrame: 0x0D, // EOF endOfFrame: 0x0D, // EOF
addressAny: 0x00, // DST = qualsiasi dispositivo (usato per discovery)
headerLength: 5, // SOF + DST + SRC + 0x00 + DLEN headerLength: 5, // SOF + DST + SRC + 0x00 + DLEN
tailLength: 3, // CHK_HI + CHK_LO + EOF tailLength: 3, // CHK_HI + CHK_LO + EOF
// Comandi (scritti nel registro 2) // Comandi (scritti nel registro 2)
@@ -122,6 +124,12 @@ const serialDefaults = {
dataBits: 8, dataBits: 8,
parity: 'none', parity: 'none',
stopBits: 1, stopBits: 1,
// Linee di controllo: alcuni adapter (es. FTDI FT232R) resettano il target all'apertura.
// L'esempio di discovery Poweren funzionante le imposta entrambe a true.
dtr: true,
rts: true,
// Tempo di assestamento dopo l'apertura della porta prima di iniziare a trasmettere
openSettleMs: 250,
}; };
// Timeout e retry per le transazioni sincrone (richiesta -> risposta) // Timeout e retry per le transazioni sincrone (richiesta -> risposta)

View File

@@ -10,7 +10,7 @@
Formato pacchetto Poweren: Formato pacchetto Poweren:
[SOF=0x42][DST][SRC=0xFF][0x00][DLEN][DATA...][CHK_HI][CHK_LO][EOF=0x0D] [SOF=0x42][DST][SRC=0xFF][0x00][DLEN][DATA...][CHK_HI][CHK_LO][EOF=0x0D]
- Richiesta lettura registro: DATA = [reg] (DLEN=1) - Richiesta lettura registro: DATA = [reg] (DLEN=1)
- Risposta: DATA = [val_hi, val_lo] (valore a 16 bit big-endian) - Risposta: DATA = [val_hi, val_lo] oppure [reg, val_hi, val_lo]
- CHK = sum(DATA) & 0xFFFF in big-endian - CHK = sum(DATA) & 0xFFFF in big-endian
Eventi emessi: Eventi emessi:
@@ -26,6 +26,7 @@ const { SerialPort } = require('serialport');
const { MPPT } = require('./mpptscore'); const { MPPT } = require('./mpptscore');
const { const {
pollRegisters, pollRegisters,
registerAddresses,
serialProtocol, serialProtocol,
serialDefaults, serialDefaults,
transactionTiming, transactionTiming,
@@ -48,6 +49,8 @@ class MPPTReader extends EventEmitter {
pollIntervalMs = 1000, pollIntervalMs = 1000,
timeoutMs = transactionTiming.timeoutMs, timeoutMs = transactionTiming.timeoutMs,
retries = transactionTiming.retries, retries = transactionTiming.retries,
dtr = serialDefaults.dtr,
rts = serialDefaults.rts,
log = () => {}, log = () => {},
} = {}) { } = {}) {
super(); super();
@@ -56,6 +59,8 @@ class MPPTReader extends EventEmitter {
this.pollIntervalMs = pollIntervalMs; this.pollIntervalMs = pollIntervalMs;
this.timeoutMs = timeoutMs; this.timeoutMs = timeoutMs;
this.retries = retries; this.retries = retries;
this.dtr = dtr;
this.rts = rts;
this.log = log; this.log = log;
this.port = null; this.port = null;
@@ -116,10 +121,14 @@ class MPPTReader extends EventEmitter {
return; return;
} }
try { try {
// Disattiva DTR/RTS per non tenere l'MPPT in reset (alcuni adapter lo resettano) // Imposta le linee DTR/RTS come da protocollo Poweren e attende l'assestamento
await this._setControlLines(false, false); // dell'adapter (l'FT232R può resettare il target all'apertura della porta).
await this._setControlLines(this.dtr, this.rts);
await this._delay(serialDefaults.openSettleMs);
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.
await this._discoverDevices();
this.emit('open'); this.emit('open');
this._startPolling(); this._startPolling();
resolve(); resolve();
@@ -232,6 +241,34 @@ class MPPTReader extends EventEmitter {
// ---- transazione sincrona di lettura registro ---- // ---- transazione sincrona di lettura registro ----
/*
Esegue una lettura iniziale del registro ADDR con DST=0x00, equivalente al test Python:
42 00 ff 00 01 01 00 01 0d
La discovery non deve impedire il polling degli indirizzi configurati: su bus con piu'
MPPT, l'indirizzo broadcast puo' non essere utilizzabile per collisione delle risposte.
*/
async _discoverDevices() {
try {
const discoveredDeviceAddress = await this._readRegister(
serialProtocol.addressAny,
registerAddresses.deviceAddress,
);
this.log(`[reader] richiesta iniziale UART OK, indirizzo MPPT rilevato: ${discoveredDeviceAddress}`);
if (this.mppts.size === 1) {
const [configuredMppt] = this.mppts.values();
if (configuredMppt.address !== discoveredDeviceAddress) {
this.log(
`[reader] MPPT ${configuredMppt.name}: indirizzo configurato ${configuredMppt.address}, ` +
`indirizzo rilevato ${discoveredDeviceAddress}; continuo con la configurazione SignalK`,
);
}
}
} catch (err) {
this.log(`[reader] richiesta iniziale UART fallita: ${err.message}; continuo con il polling configurato`);
}
}
/* /*
Legge un singolo registro dall'MPPT indicato (DST=address). Legge un singolo registro dall'MPPT indicato (DST=address).
Ritorna il valore raw a 16 bit. Esegue retry in caso di timeout. Ritorna il valore raw a 16 bit. Esegue retry in caso di timeout.
@@ -241,10 +278,7 @@ class MPPTReader extends EventEmitter {
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 responseData = await this._transact(address, this._buildReadPacket(address, regAddr));
if (!responseData || responseData.length < 2) { return this._parseRegisterValue(responseData, regAddr);
throw new SerialFrameParseError('risposta troppo corta');
}
return (responseData[0] << 8) | responseData[1];
} catch (err) { } catch (err) {
lastError = err; lastError = err;
if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs); if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs);
@@ -253,6 +287,23 @@ class MPPTReader extends EventEmitter {
throw lastError || new RegisterReadTimeoutError(address, regAddr); throw lastError || new RegisterReadTimeoutError(address, regAddr);
} }
/*
Estrae il valore raw dalla risposta Poweren.
Alcuni firmware rispondono con DATA=[hi, lo], altri includono il registro letto:
DATA=[reg, hi, lo]. Supportare entrambi evita di pubblicare valori sfalsati.
*/
_parseRegisterValue(responseData, requestedRegisterAddress) {
if (!responseData || responseData.length < 2) {
throw new SerialFrameParseError('risposta troppo corta');
}
if (responseData.length >= 3 && responseData[0] === (requestedRegisterAddress & 0xFF)) {
return (responseData[1] << 8) | responseData[2];
}
return (responseData[0] << 8) | responseData[1];
}
// Invia un pacchetto e attende il prossimo frame valido (con timeout) // Invia un pacchetto e attende il prossimo frame valido (con timeout)
_transact(address, packet) { _transact(address, packet) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -277,6 +328,16 @@ class MPPTReader extends EventEmitter {
clearTimeout(timer); clearTimeout(timer);
this.pendingTransaction = null; this.pendingTransaction = null;
reject(new SerialDriverError(err.message, err)); reject(new SerialDriverError(err.message, err));
return;
}
if (typeof this.port.drain === 'function') {
this.port.drain((drainErr) => {
if (drainErr && this.pendingTransaction && this.pendingTransaction.timer === timer) {
clearTimeout(timer);
this.pendingTransaction = null;
reject(new SerialDriverError(drainErr.message, drainErr));
}
});
} }
}); });
}); });