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)".
*/
const registerAddresses = {
deviceAddress: 1, // Addr - indirizzo del dispositivo (confermato: usato per discovery)
status1: 4, // St1 - flag di stato (confermato: "registro 4")
warning: 6, // Warn - flag di warning/errore (confermato: "registro 6")
chargeCapacity: 7, // Chg_Cap - Ah caricati (da verificare)
@@ -102,6 +103,7 @@ const serialProtocol = {
startOfFrame: 0x42, // SOF
sourceHost: 0xFF, // SRC host/GUI
endOfFrame: 0x0D, // EOF
addressAny: 0x00, // DST = qualsiasi dispositivo (usato per discovery)
headerLength: 5, // SOF + DST + SRC + 0x00 + DLEN
tailLength: 3, // CHK_HI + CHK_LO + EOF
// Comandi (scritti nel registro 2)
@@ -122,6 +124,12 @@ const serialDefaults = {
dataBits: 8,
parity: 'none',
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)

View File

@@ -10,7 +10,7 @@
Formato pacchetto Poweren:
[SOF=0x42][DST][SRC=0xFF][0x00][DLEN][DATA...][CHK_HI][CHK_LO][EOF=0x0D]
- 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
Eventi emessi:
@@ -26,6 +26,7 @@ const { SerialPort } = require('serialport');
const { MPPT } = require('./mpptscore');
const {
pollRegisters,
registerAddresses,
serialProtocol,
serialDefaults,
transactionTiming,
@@ -48,6 +49,8 @@ class MPPTReader extends EventEmitter {
pollIntervalMs = 1000,
timeoutMs = transactionTiming.timeoutMs,
retries = transactionTiming.retries,
dtr = serialDefaults.dtr,
rts = serialDefaults.rts,
log = () => {},
} = {}) {
super();
@@ -56,6 +59,8 @@ class MPPTReader extends EventEmitter {
this.pollIntervalMs = pollIntervalMs;
this.timeoutMs = timeoutMs;
this.retries = retries;
this.dtr = dtr;
this.rts = rts;
this.log = log;
this.port = null;
@@ -116,10 +121,14 @@ class MPPTReader extends EventEmitter {
return;
}
try {
// Disattiva DTR/RTS per non tenere l'MPPT in reset (alcuni adapter lo resettano)
await this._setControlLines(false, false);
// 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).
await this._setControlLines(this.dtr, this.rts);
await this._delay(serialDefaults.openSettleMs);
this.isOpen = true;
this.reconnectAttempts = 0;
// Invia la prima richiesta UART: l'MPPT Poweren risponde solo dopo una lettura.
await this._discoverDevices();
this.emit('open');
this._startPolling();
resolve();
@@ -232,6 +241,34 @@ class MPPTReader extends EventEmitter {
// ---- 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).
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++) {
try {
const responseData = await this._transact(address, this._buildReadPacket(address, regAddr));
if (!responseData || responseData.length < 2) {
throw new SerialFrameParseError('risposta troppo corta');
}
return (responseData[0] << 8) | responseData[1];
return this._parseRegisterValue(responseData, regAddr);
} catch (err) {
lastError = err;
if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs);
@@ -253,6 +287,23 @@ class MPPTReader extends EventEmitter {
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)
_transact(address, packet) {
return new Promise((resolve, reject) => {
@@ -277,6 +328,16 @@ class MPPTReader extends EventEmitter {
clearTimeout(timer);
this.pendingTransaction = null;
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));
}
});
}
});
});