/* Ogni istanza mantiene lo stato corrente del dispositivo (tensioni, correnti, temperature, flag operativi, warnings) aggiornato dai frame CAN broadcast (MSG1 status, MSG2 power) e dalle risposte alle richieste di lettura registro. I valori esposti sono gia' convertiti in unita' fisiche (V, A, W, °C, Ah). */ const { conversionsFactors, status1Flags, errorsFlags, registerAddresses, } = require('./constants'); /* Mappa indirizzo registro -> campo di stato e fattore di conversione. Usata da updateRegister per convertire il valore raw a 16 bit nel valore fisico. I registri di flag (status1, warning) sono gestiti a parte perche' vanno decodificati a bit. signed=true indica un intero a 16 bit con segno (complemento a 2), usato per le temperature. */ const numericRegisterMap = { [registerAddresses.voltageInput]: { field: 'voltageInput', factor: conversionsFactors.voltageInput, signed: false }, [registerAddresses.currentInput]: { field: 'currentInput', factor: conversionsFactors.currentInput, signed: false }, [registerAddresses.voltageOutput]: { field: 'voltageOutput', factor: conversionsFactors.voltageOutput, signed: false }, [registerAddresses.currentOutput]: { field: 'currentOutput', factor: conversionsFactors.currentOutput, signed: false }, [registerAddresses.powerInput]: { field: 'powerInput', factor: conversionsFactors.powerInput, 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.temperature2]: { field: 'temperature2', factor: conversionsFactors.temperaturePhase2, signed: true }, }; // Arrotondamenti const roundedTo1 = (v) => Number(v.toFixed(1)); const roundedTo2 = (v) => Number(v.toFixed(2)); const roundedTo3 = (v) => Number(v.toFixed(3)); // Decodifica un valore numerico in array di stringhe usando una mappa bit -> nome const decodeFlags = (value, map) => { const active = []; for (const [bit, name] of Object.entries(map)) { if (value & (1 << parseInt(bit, 10))) { // Nel caso di status1Flags i valori sono stringhe; nel caso di errorsFlags idem active.push(typeof name === 'string' ? name : name.code); } } return active; }; class MPPT { constructor({ name, address, log = () => {}, } = {}) { this.name = name; // identificativo logico (es. 'port', 'starboard') this.address = address; // indirizzo UART/DST del dispositivo (registro Addr) this.log = log; this.state = { voltageInput: null, currentInput: null, voltageOutput: null, currentOutput: null, powerInput: null, // letto via polling registro 20 powerOutput: null, // letto via polling registro 21 temperature1: null, // [°C] temperature2: null, // [°C] chargeCapacity: null, // [Ah] status1Raw: 0, warningRaw: 0, flags: [], warnings: [], lastUpdate: 0, }; } // Restituisce true se l'MPPT ha trasmesso dati recentemente isOnline(thresholdMs = 5000) { return this.state.lastUpdate > 0 && (Date.now() - this.state.lastUpdate) < thresholdMs; } // Calcola l'efficienza istantanea (output/input) come ratio 0..1 calculateEfficiency() { const { voltageInput, currentInput, voltageOutput, currentOutput } = this.state; if (voltageInput === null || currentInput === null || voltageOutput === null || currentOutput === null) return null; const inputPower = voltageInput * currentInput; if (inputPower < 1) return null; return roundedTo3((voltageOutput * currentOutput) / inputPower); } // Deriva la modalita' di carica corrente a partire dai flag attivi deriveChargingMode() { if (!this.state.flags.includes('PwrOn')) return 'off'; if (this.state.flags.includes('FloatMod')) return 'float'; if (this.state.flags.includes('StorMod')) return 'storage'; return 'bulk'; } /* Aggiorna lo stato a partire dalla lettura UART di un singolo registro. regAddr = indirizzo del registro letto rawValue = valore raw a 16 bit ricevuto nella risposta (big-endian, gia' decodificato) */ updateRegister(regAddr, rawValue) { if (regAddr === registerAddresses.status1) { // Registro di stato St1: decodifica i bit in flag operativi this.state.status1Raw = rawValue; this.state.flags = decodeFlags(rawValue, status1Flags); } else if (regAddr === registerAddresses.warning) { // Registro Warning: decodifica i bit in codici di errore/warning this.state.warningRaw = rawValue; this.state.warnings = decodeFlags(rawValue, errorsFlags); if (this.state.warnings.length) { this.log(`[${this.name}] WARN: ${this.state.warnings.join(',')}`); } } else { // Registri numerici (tensioni, correnti, temperature, capacita') const target = numericRegisterMap[regAddr]; if (!target) return; let value = rawValue; if (target.signed && value >= 0x8000) value -= 0x10000; // intero con segno this.state[target.field] = value / target.factor; } this.state.lastUpdate = Date.now(); } // Costruisce gli update SignalK (path + value) a partire dallo stato corrente // Tutti i valori sono in unita' SI (V, A, W, K, ratio, Ah) buildSignalKUpdates() { const base = `meb.solar.${this.name}`; const s = this.state; const updates = []; if (s.voltageInput !== null) updates.push({ path: `${base}.panelVoltage`, value: roundedTo1(s.voltageInput) }); if (s.currentInput !== null) updates.push({ path: `${base}.panelCurrent`, value: roundedTo2(s.currentInput) }); if (s.voltageInput !== null && s.currentInput !== null) { updates.push({ path: `${base}.panelPower`, value: roundedTo2(s.voltageInput * s.currentInput) }); } if (s.voltageOutput !== null) updates.push({ path: `${base}.voltage`, value: roundedTo1(s.voltageOutput) }); if (s.currentOutput !== null) updates.push({ path: `${base}.current`, value: roundedTo2(s.currentOutput) }); if (s.voltageOutput !== null && s.currentOutput !== null) { updates.push({ path: `${base}.power`, value: roundedTo2(s.voltageOutput * s.currentOutput) }); } const efficiency = this.calculateEfficiency(); if (efficiency !== null) updates.push({ path: `${base}.efficiency`, value: efficiency }); if (s.temperature1 !== null) updates.push({ path: `${base}.controllerTemperature.phase1`, value: roundedTo2(s.temperature1 + 273.15) }); if (s.temperature2 !== null) updates.push({ path: `${base}.controllerTemperature.phase2`, value: roundedTo2(s.temperature2 + 273.15) }); if (s.chargeCapacity !== null) { updates.push({ path: `${base}.chargedCapacity`, value: roundedTo2(s.chargeCapacity) }); if (s.voltageOutput !== null) { // Energia in Joule: Ah * V * 3600 updates.push({ path: `${base}.chargedEnergy`, value: roundedTo2(s.chargeCapacity * s.voltageOutput * 3600) }); } } updates.push({ path: `${base}.chargingMode`, value: this.deriveChargingMode() }); updates.push({ path: `${base}.flags`, value: s.flags }); updates.push({ path: `${base}.warnings`, value: s.warnings }); return updates; } // Snapshot operativo per logging / debug getSnapshot() { return { online: this.isOnline(), voltageInput: this.state.voltageInput, currentInput: this.state.currentInput, voltageOutput: this.state.voltageOutput, currentOutput: this.state.currentOutput, temperature1: this.state.temperature1, temperature2: this.state.temperature2, chargeCapacity: this.state.chargeCapacity, flags: this.state.flags, warnings: this.state.warnings, chargingMode: this.deriveChargingMode(), efficiency: this.calculateEfficiency(), }; } } module.exports = { MPPT, decodeFlags, };