diff --git a/src/core/constants.js b/src/core/constants.js index 4d148a4..5877bff 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -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) diff --git a/src/core/reader.js b/src/core/reader.js index 5dfe4bf..4df2288 100644 --- a/src/core/reader.js +++ b/src/core/reader.js @@ -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)); + } + }); } }); });