191 lines
8.0 KiB
JavaScript
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,
|
|
};
|