Implemented:
- MMPT class for data store - SerialPort library for USB-CAN connection - Stream to Singnlak's DataBrowser 3
This commit is contained in:
205
src/core/mpptscore.js
Normal file
205
src/core/mpptscore.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
Modello di un singolo controller MPPT Poweren Boost.
|
||||
|
||||
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');
|
||||
|
||||
// Arrotondamenti convenzionali per esporre dati leggibili nei getter
|
||||
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;
|
||||
};
|
||||
|
||||
// Parsing del payload broadcast: 8 byte -> 4 uint16 big-endian
|
||||
function parsePayload(data) {
|
||||
if (!data || data.length < 8) return null;
|
||||
return [
|
||||
(data[0] << 8) | data[1],
|
||||
(data[2] << 8) | data[3],
|
||||
(data[4] << 8) | data[5],
|
||||
(data[6] << 8) | data[7],
|
||||
];
|
||||
}
|
||||
|
||||
class MPPT {
|
||||
constructor({
|
||||
name,
|
||||
rxID,
|
||||
txIdBase,
|
||||
log = () => {},
|
||||
} = {}) {
|
||||
this.name = name; // identificativo logico (es. 'port', 'starboard')
|
||||
this.rxID = rxID; // indirizzo CAN del dispositivo (registro Addr)
|
||||
this.txIdBase = txIdBase; // ID base di trasmissione (broadcast su +1 e +2)
|
||||
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 da un frame broadcast (MSG1=status, MSG2=power)
|
||||
updateFromBroadcast(type, data) {
|
||||
const regs = parsePayload(data);
|
||||
if (!regs) return;
|
||||
|
||||
if (type === 'status') {
|
||||
const [st1, , warn, chgCap] = regs;
|
||||
this.state.status1Raw = st1;
|
||||
this.state.warningRaw = warn;
|
||||
this.state.chargeCapacity = chgCap / conversionsFactors.chargeCapacity;
|
||||
this.state.flags = decodeFlags(st1, status1Flags);
|
||||
this.state.warnings = decodeFlags(warn, errorsFlags);
|
||||
if (this.state.warnings.length) {
|
||||
this.log(`[${this.name}] WARN: ${this.state.warnings.join(',')}`);
|
||||
}
|
||||
} else if (type === 'power') {
|
||||
const [vi, ii, vo, io] = regs;
|
||||
this.state.voltageInput = vi / conversionsFactors.voltageInput;
|
||||
this.state.currentInput = ii / conversionsFactors.currentInput;
|
||||
this.state.voltageOutput = vo / conversionsFactors.voltageOutput;
|
||||
this.state.currentOutput = io / conversionsFactors.currentOutput;
|
||||
}
|
||||
this.state.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
// Aggiorna lo stato a partire da una risposta a richiesta di registro
|
||||
// Payload reply: [reg_addr, value_high, value_low]
|
||||
updateFromRegisterReply(data) {
|
||||
if (!data || data.length < 3) return;
|
||||
const regAddr = data[0];
|
||||
const rawValue = (data[1] << 8) | data[2];
|
||||
|
||||
const mapping = {
|
||||
[registerAddresses.powerInput]: { field: 'powerInput', factor: conversionsFactors.powerInput },
|
||||
[registerAddresses.powerOutput]: { field: 'powerOutput', factor: conversionsFactors.powerOutput },
|
||||
[registerAddresses.temperature1]: { field: 'temperature1', factor: conversionsFactors.temperaturePhase1 },
|
||||
[registerAddresses.temperature2]: { field: 'temperature2', factor: conversionsFactors.temperaturePhase2 },
|
||||
};
|
||||
|
||||
const target = mapping[regAddr];
|
||||
if (!target) return;
|
||||
this.state[target.field] = rawValue / 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,
|
||||
parsePayload,
|
||||
decodeFlags,
|
||||
};
|
||||
Reference in New Issue
Block a user