Files
meb-solars/src/core/mpptscore.js

191 lines
8.0 KiB
JavaScript

/*
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,
};