diff --git a/src/core/constants.js b/src/core/constants.js index 5877bff..948a4fa 100644 --- a/src/core/constants.js +++ b/src/core/constants.js @@ -23,9 +23,9 @@ const status1Flags = { 1: 'ChgEna', // Ponte Pin1-Pin8 inserito (enable hardware) 2: 'ChgOk', // Condizioni di ricarica OK 3: 'PwrOn', // Stadio di potenza acceso (eroga corrente) - 4: 'StorMod', // Modalita' storage attiva - 5: 'FloatMod', // Modalita' float (mantenimento) - 6: 'CtrlEna', // Algoritmo MPPT abilitato + 4: 'CtrlEna', // Algoritmo MPPT abilitato + 5: 'StorMod', // Modalita' storage attiva + 6: 'FloatMod', // Modalita' float (mantenimento) 7: 'CCapRst', // Reset contatore Ah 8: 'Ph1Ena', // Fase 1 abilitata 9: 'Ph2Ena', // Fase 2 abilitata @@ -61,14 +61,14 @@ 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) - voltageInput: 16, // Vi - tensione ingresso (da verificare) - currentInput: 17, // Ii - corrente ingresso (da verificare) - voltageOutput: 18, // Vo - tensione uscita (da verificare) - currentOutput: 19, // Io - corrente uscita (da verificare) + voltageInput: 16, // Vi - tensione ingresso (manuale: registro 16) + currentInput: 18, // Ii - corrente ingresso (manuale: registro 18) + voltageOutput: 22, // Vo - tensione uscita (manuale: registro 22) + currentOutput: 27, // Io - corrente uscita (manuale: registro 27) powerInput: 20, // Pi - potenza ingresso (confermato) - powerOutput: 21, // Po - potenza uscita (confermato) - temperature1: 35, // T1 - temperatura fase 1 (confermato) + powerOutput: 31, // Po - potenza uscita (manuale: registro 31) + chargeCapacity: null, // Chg_Cap - nel PDF hw1.2r01 collide con T1 sul registro 35: non pollare finche' non confermato + temperature1: 35, // T1 - temperatura fase 1 (manuale: registro 35) temperature2: 36, // T2 - temperatura fase 2 (confermato) }; @@ -84,7 +84,6 @@ const pollRegisters = [ registerAddresses.currentInput, registerAddresses.voltageOutput, registerAddresses.currentOutput, - registerAddresses.chargeCapacity, registerAddresses.temperature1, registerAddresses.temperature2, ]; diff --git a/src/core/mpptscore.js b/src/core/mpptscore.js index 107110a..26c6d11 100644 --- a/src/core/mpptscore.js +++ b/src/core/mpptscore.js @@ -26,11 +26,18 @@ const numericRegisterMap = { [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 }, }; +if (Number.isInteger(registerAddresses.chargeCapacity)) { + numericRegisterMap[registerAddresses.chargeCapacity] = { + field: 'chargeCapacity', + factor: conversionsFactors.chargeCapacity, + signed: false, + }; +} + // Arrotondamenti const roundedTo1 = (v) => Number(v.toFixed(1)); const roundedTo2 = (v) => Number(v.toFixed(2)); @@ -104,10 +111,12 @@ class MPPT { rawValue = valore raw a 16 bit ricevuto nella risposta (big-endian, gia' decodificato) */ updateRegister(regAddr, rawValue) { + this.log(`[${this.name}] update registro ${regAddr}: raw=${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); + this.log(`[${this.name}] St1 flags=${JSON.stringify(this.state.flags)}`); } else if (regAddr === registerAddresses.warning) { // Registro Warning: decodifica i bit in codici di errore/warning this.state.warningRaw = rawValue; @@ -122,6 +131,7 @@ class MPPT { let value = rawValue; if (target.signed && value >= 0x8000) value -= 0x10000; // intero con segno this.state[target.field] = value / target.factor; + this.log(`[${this.name}] ${target.field}=${this.state[target.field]} (raw=${rawValue}, conv=${target.factor})`); } this.state.lastUpdate = Date.now(); } @@ -162,6 +172,7 @@ class MPPT { updates.push({ path: `${base}.flags`, value: s.flags }); updates.push({ path: `${base}.warnings`, value: s.warnings }); + this.log(`[${this.name}] update SignalK costruiti=${updates.length} snapshot=${JSON.stringify(this.getSnapshot())}`); return updates; } diff --git a/src/core/reader.js b/src/core/reader.js index 4df2288..990e8a2 100644 --- a/src/core/reader.js +++ b/src/core/reader.js @@ -85,10 +85,19 @@ class MPPTReader extends EventEmitter { }); this.mppts.set(config.id, mppt); } + this.log( + `[reader] configurazione: device=${this.device}, baudRate=${this.baudRate}, ` + + `pollIntervalMs=${this.pollIntervalMs}, timeoutMs=${this.timeoutMs}, retries=${this.retries}, ` + + `dtr=${this.dtr}, rts=${this.rts}, mppts=${JSON.stringify(Array.from(this.mppts.values()).map((mppt) => ({ + name: mppt.name, + address: mppt.address, + })))}` + ); } // Apre la porta seriale e avvia il loop di polling async open() { + this.log(`[reader] open(): preparo apertura seriale ${this.device}`); return new Promise((resolve, reject) => { try { this.port = new SerialPort({ @@ -101,7 +110,10 @@ class MPPTReader extends EventEmitter { }); // Protocollo binario: si leggono i byte grezzi (l'EOF 0x0D puo' comparire nei dati) - this.port.on('data', (chunk) => this._onData(chunk)); + this.port.on('data', (chunk) => { + this.log(`[reader] RX chunk len=${chunk.length} hex=${chunk.toString('hex')}`); + this._onData(chunk); + }); this.port.on('error', (err) => { this.log(`[reader] errore porta seriale: ${err.message}`); @@ -109,6 +121,7 @@ class MPPTReader extends EventEmitter { }); this.port.on('close', () => { + this.log('[reader] porta seriale chiusa'); this.isOpen = false; this._stopPolling(); this.emit('close'); @@ -117,26 +130,33 @@ class MPPTReader extends EventEmitter { this.port.open(async (err) => { if (err) { + this.log(`[reader] apertura seriale fallita: ${err.message}`); reject(new SerialDriverError(`Impossibile aprire ${this.device}: ${err.message}`, err)); return; } try { + this.log(`[reader] porta aperta: ${this.device}`); // 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); + this.log(`[reader] linee controllo impostate: dtr=${this.dtr}, rts=${this.rts}`); await this._delay(serialDefaults.openSettleMs); + this.log(`[reader] attesa assestamento completata: ${serialDefaults.openSettleMs}ms`); this.isOpen = true; this.reconnectAttempts = 0; // Invia la prima richiesta UART: l'MPPT Poweren risponde solo dopo una lettura. await this._discoverDevices(); + this.log('[reader] discovery iniziale completata, avvio polling'); this.emit('open'); this._startPolling(); resolve(); } catch (initErr) { + this.log(`[reader] inizializzazione fallita: ${initErr.stack || initErr.message}`); reject(initErr); } }); } catch (err) { + this.log(`[reader] errore creazione SerialPort: ${err.stack || err.message}`); reject(new SerialDriverError(err.message, err)); } }); @@ -144,6 +164,7 @@ class MPPTReader extends EventEmitter { // Chiude la porta e ferma il polling async close() { + this.log('[reader] close(): arresto richiesto'); this.shouldReconnect = false; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); @@ -159,6 +180,7 @@ class MPPTReader extends EventEmitter { await new Promise((resolve) => this.port.close(() => resolve())); } this.isOpen = false; + this.log('[reader] close(): completato'); } // Costruisce gli update aggregati (somma dei lati attivi) @@ -179,23 +201,30 @@ class MPPTReader extends EventEmitter { if (!anyOnline) return []; const roundedTo2 = (v) => Number(v.toFixed(2)); - return [ + const aggregateUpdates = [ { path: 'meb.solar.total.panelPower', value: roundedTo2(panelPowerSum) }, { path: 'meb.solar.total.power', value: roundedTo2(powerSum) }, { path: 'meb.solar.total.current', value: roundedTo2(currentSum) }, ]; + this.log(`[reader] aggregati calcolati: ${JSON.stringify(aggregateUpdates)}`); + return aggregateUpdates; } // ---- loop di polling ---- _startPolling() { if (this.pollTimer) return; + this.log('[reader] startPolling(): timer polling attivato'); // setTimeout ricorsivo: garantisce che un ciclo finisca prima del successivo const tick = async () => { - if (!this.isOpen) return; + if (!this.isOpen) { + this.log('[reader] tick polling ignorato: porta non aperta'); + return; + } try { await this._pollCycle(); } catch (err) { + this.log(`[reader] errore ciclo polling: ${err.stack || err.message}`); this.emit('error', err); } if (this.isOpen) { @@ -207,6 +236,7 @@ class MPPTReader extends EventEmitter { _stopPolling() { if (this.pollTimer) { + this.log('[reader] stopPolling(): timer polling fermato'); clearTimeout(this.pollTimer); this.pollTimer = null; } @@ -214,14 +244,22 @@ class MPPTReader extends EventEmitter { // Un ciclo di polling: per ogni MPPT legge tutti i registri e pubblica gli update async _pollCycle() { - if (this.isPolling) return; + if (this.isPolling) { + this.log('[reader] ciclo polling saltato: ciclo precedente ancora attivo'); + return; + } this.isPolling = true; + const cycleStartedAt = Date.now(); + this.log(`[reader] ciclo polling avviato: mppts=${this.mppts.size}, registri=${pollRegisters.join(',')}`); try { for (const mppt of this.mppts.values()) { let updated = false; + this.log(`[reader] polling MPPT ${mppt.name} address=${mppt.address}`); for (const regAddr of pollRegisters) { try { + this.log(`[reader] leggo MPPT ${mppt.name} address=${mppt.address} registro=${regAddr}`); const rawValue = await this._readRegister(mppt.address, regAddr); + this.log(`[reader] letto MPPT ${mppt.name} address=${mppt.address} registro=${regAddr} raw=${rawValue}`); mppt.updateRegister(regAddr, rawValue); updated = true; } catch (err) { @@ -231,11 +269,15 @@ class MPPTReader extends EventEmitter { } if (updated) { const updates = mppt.buildSignalKUpdates(); + this.log(`[reader] MPPT ${mppt.name}: updated=true, updateSignalK=${updates.length}`); if (updates.length) this.emit('updates', updates); + } else { + this.log(`[reader] MPPT ${mppt.name}: nessun registro aggiornato in questo ciclo`); } } } finally { this.isPolling = false; + this.log(`[reader] ciclo polling completato in ${Date.now() - cycleStartedAt}ms`); } } @@ -277,10 +319,24 @@ class MPPTReader extends EventEmitter { let lastError = null; for (let attempt = 0; attempt <= this.retries; attempt++) { try { - const responseData = await this._transact(address, this._buildReadPacket(address, regAddr)); - return this._parseRegisterValue(responseData, regAddr); + const packet = this._buildReadPacket(address, regAddr); + this.log( + `[reader] richiesta registro: address=${address}, reg=${regAddr}, ` + + `tentativo=${attempt + 1}/${this.retries + 1}, packet=${packet.toString('hex')}` + ); + const responseData = await this._transact(address, packet); + const rawValue = this._parseRegisterValue(responseData, regAddr); + this.log( + `[reader] risposta registro: address=${address}, reg=${regAddr}, ` + + `data=${responseData.toString('hex')}, raw=${rawValue}` + ); + return rawValue; } catch (err) { lastError = err; + this.log( + `[reader] tentativo fallito: address=${address}, reg=${regAddr}, ` + + `tentativo=${attempt + 1}/${this.retries + 1}, errore=${err.message}` + ); if (attempt < this.retries) await this._delay(transactionTiming.retryDelayMs); } } @@ -308,6 +364,7 @@ class MPPTReader extends EventEmitter { _transact(address, packet) { return new Promise((resolve, reject) => { if (!this.isOpen) { + this.log(`[reader] transact rifiutata: porta non aperta, address=${address}, packet=${packet.toString('hex')}`); reject(new SerialDriverError('porta non aperta')); return; } @@ -317,26 +374,33 @@ class MPPTReader extends EventEmitter { const timer = setTimeout(() => { if (this.pendingTransaction && this.pendingTransaction.timer === timer) { this.pendingTransaction = null; + this.log(`[reader] timeout transazione: address=${address}, reg=${packet[5]}, packet=${packet.toString('hex')}`); reject(new RegisterReadTimeoutError(address, packet[5])); } }, this.timeoutMs); - this.pendingTransaction = { resolve, reject, timer }; + this.pendingTransaction = { resolve, reject, timer, address, regAddr: packet[5], packet }; + this.log(`[reader] TX write: address=${address}, reg=${packet[5]}, hex=${packet.toString('hex')}`); this.port.write(packet, (err) => { if (err) { clearTimeout(timer); this.pendingTransaction = null; + this.log(`[reader] errore TX write: ${err.message}`); reject(new SerialDriverError(err.message, err)); return; } + this.log(`[reader] TX write callback OK: address=${address}, reg=${packet[5]}`); if (typeof this.port.drain === 'function') { this.port.drain((drainErr) => { if (drainErr && this.pendingTransaction && this.pendingTransaction.timer === timer) { clearTimeout(timer); this.pendingTransaction = null; + this.log(`[reader] errore TX drain: ${drainErr.message}`); reject(new SerialDriverError(drainErr.message, drainErr)); + return; } + this.log(`[reader] TX drain OK: address=${address}, reg=${packet[5]}`); }); } }); @@ -363,13 +427,18 @@ class MPPTReader extends EventEmitter { _onData(chunk) { this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]); + this.log(`[reader] rxBuffer len=${this.rxBuffer.length} hex=${this.rxBuffer.toString('hex')}`); let frame; while ((frame = this._extractFrame()) !== null) { + this.log(`[reader] frame valido: dst=${frame.dst}, src=${frame.src}, data=${frame.data.toString('hex')}`); if (this.pendingTransaction) { - const { resolve, timer } = this.pendingTransaction; + const { resolve, timer, address, regAddr } = this.pendingTransaction; clearTimeout(timer); this.pendingTransaction = null; + this.log(`[reader] transazione risolta: address=${address}, reg=${regAddr}, data=${frame.data.toString('hex')}`); resolve(frame.data); + } else { + this.log(`[reader] frame ricevuto senza transazione pendente: data=${frame.data.toString('hex')}`); } } } @@ -385,16 +454,23 @@ class MPPTReader extends EventEmitter { // Allinea il buffer al primo SOF disponibile let sofIndex = this.rxBuffer.indexOf(startOfFrame); if (sofIndex < 0) { + this.log(`[reader] nessun SOF nel buffer, scarto ${this.rxBuffer.length} byte: ${this.rxBuffer.toString('hex')}`); this.rxBuffer = Buffer.alloc(0); return null; } if (sofIndex > 0) { + this.log(`[reader] scarto ${sofIndex} byte prima del SOF: ${this.rxBuffer.slice(0, sofIndex).toString('hex')}`); this.rxBuffer = this.rxBuffer.slice(sofIndex); } if (this.rxBuffer.length < headerLength) return null; const dlen = (this.rxBuffer[3] << 8) | this.rxBuffer[4]; + if (dlen > 512) { + this.log(`[reader] frame scartato: DLEN non plausibile (${dlen}), buffer=${this.rxBuffer.toString('hex')}`); + this.rxBuffer = this.rxBuffer.slice(1); + return null; + } const totalLength = headerLength + dlen + tailLength; if (this.rxBuffer.length < totalLength) return null; @@ -408,13 +484,15 @@ class MPPTReader extends EventEmitter { this.rxBuffer = this.rxBuffer.slice(totalLength); if (eof !== endOfFrame) { - this.log('[reader] frame scartato: EOF mancante'); + this.log(`[reader] frame scartato: EOF mancante, frame=${this.rxBuffer.toString('hex')}`); return null; } let sum = 0; for (const b of data) sum += b; if ((sum & 0xFFFF) !== checksum) { - this.log('[reader] frame scartato: checksum errato'); + this.log( + `[reader] frame scartato: checksum errato, atteso=${sum & 0xFFFF}, ricevuto=${checksum}, data=${data.toString('hex')}` + ); return null; } @@ -426,10 +504,14 @@ class MPPTReader extends EventEmitter { _setControlLines(dtr, rts) { return new Promise((resolve) => { if (!this.port || typeof this.port.set !== 'function') { + this.log('[reader] setControlLines ignorato: port.set non disponibile'); resolve(); return; } - this.port.set({ dtr, rts }, () => resolve()); + this.port.set({ dtr, rts }, (err) => { + if (err) this.log(`[reader] errore setControlLines: ${err.message}`); + resolve(); + }); }); } diff --git a/src/index.js b/src/index.js index dc69a4a..ab6be2a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,17 @@ const { MPPTReader } = require('./core/reader'); module.exports = function (app) { + const defaultPluginOptions = { + device: '/dev/ttyUSB0', + baudRate: 115200, + publishIntervalMs: 1000, + pollIntervalMs: 1000, + mppts: [ + { id: 'port', address: 50 }, + { id: 'starboard', address: 60 }, + ], + }; + const plugin = { id: 'meb-solars', name: 'MEB Solar Panels', @@ -53,67 +64,84 @@ module.exports = function (app) { }, default: [ { id: 'port', address: 50 }, + { id: 'starboard', address: 60 }, ], }, }, }; plugin.start = function (options) { - app.debug('Avvio plugin meb-solars con opzioni:', options); + const resolvedOptions = normalizeOptions(options); + const pluginLogger = createPluginLogger(app); + pluginLogger(`Avvio plugin con opzioni risolte: ${JSON.stringify(resolvedOptions)}`); reader = new MPPTReader({ - device: options.device, - baudRate: options.baudRate, - mppts: options.mppts, - pollIntervalMs: options.pollIntervalMs, - log: app.debug ? app.debug.bind(app) : console.log, + device: resolvedOptions.device, + baudRate: resolvedOptions.baudRate, + mppts: resolvedOptions.mppts, + pollIntervalMs: resolvedOptions.pollIntervalMs, + log: pluginLogger, }); // Bufferizza gli update in arrivo dal driver: pubblicazione periodica reader.on('updates', (updates) => { + pluginLogger(`Ricevuti ${updates.length} update dal reader`); + pluginLogger(`Primi update reader: ${JSON.stringify(updates.slice(0, 8))}`); for (const update of updates) { pendingUpdatesByPath.set(update.path, update.value); } + pluginLogger(`Buffer SignalK aggiornato: ${pendingUpdatesByPath.size} path in attesa`); }); reader.on('open', () => { - app.setPluginStatus(`Connesso alla seriale (${options.device} @ ${options.baudRate} bps)`); + pluginLogger(`Evento open: seriale connessa (${resolvedOptions.device} @ ${resolvedOptions.baudRate} bps)`); + app.setPluginStatus(`Connesso alla seriale (${resolvedOptions.device} @ ${resolvedOptions.baudRate} bps)`); }); reader.on('error', (err) => { + pluginLogger(`Evento error dal reader: ${err.stack || err.message}`); app.error(`Errore driver UART: ${err.message}`); app.setPluginError(err.message); }); reader.on('close', () => { + pluginLogger('Evento close: seriale disconnessa'); app.setPluginStatus('Seriale disconnessa, tentativo di riconnessione in corso'); }); reader.open() .then(() => { + pluginLogger('Reader aperto: pubblico metadata e avvio intervallo SignalK'); publishMetadata(); // Pubblicazione periodica degli update bufferizzati + aggregati. // Il polling dei registri UART e' gestito internamente dal driver. publishInterval = setInterval(() => { const aggregateUpdates = reader.buildAggregateUpdates(); + pluginLogger(`Tick publish: aggregati=${aggregateUpdates.length}, pendingPrima=${pendingUpdatesByPath.size}`); for (const update of aggregateUpdates) { pendingUpdatesByPath.set(update.path, update.value); } - if (pendingUpdatesByPath.size === 0) return; + if (pendingUpdatesByPath.size === 0) { + pluginLogger('Tick publish: nessun dato da pubblicare'); + return; + } const values = Array.from(pendingUpdatesByPath.entries()).map(([path, value]) => ({ path, value })); pendingUpdatesByPath.clear(); - publishUpdates(values); - }, options.publishIntervalMs); + pluginLogger(`Tick publish: pubblico ${values.length} path, esempi=${JSON.stringify(values.slice(0, 10))}`); + publishUpdates(values, pluginLogger); + }, resolvedOptions.publishIntervalMs); }) .catch((err) => { + pluginLogger(`Impossibile aprire il reader UART: ${err.stack || err.message}`); app.error(`Impossibile aprire il driver UART: ${err.message}`); app.setPluginError(err.message); }); }; plugin.stop = function () { - app.debug('Arresto plugin meb-solars'); + const pluginLogger = createPluginLogger(app); + pluginLogger('Arresto plugin'); if (publishInterval) { clearInterval(publishInterval); publishInterval = null; } if (reader) { reader.close().catch((err) => app.error(`Errore in chiusura: ${err.message}`)); @@ -124,7 +152,7 @@ module.exports = function (app) { }; // Pubblica un set di update come delta SignalK - function publishUpdates(values) { + function publishUpdates(values, pluginLogger = createPluginLogger(app)) { const delta = { updates: [ { @@ -134,12 +162,21 @@ module.exports = function (app) { }, ], }; - app.handleMessage(plugin.id, delta); + try { + pluginLogger(`handleMessage values: ${JSON.stringify(delta)}`); + app.handleMessage(plugin.id, delta); + pluginLogger(`handleMessage completato: ${values.length} path pubblicati`); + } catch (err) { + pluginLogger(`Errore app.handleMessage(values): ${err.stack || err.message}`); + app.error(`Errore pubblicazione SignalK: ${err.message}`); + app.setPluginError(err.message); + } } // Pubblica i metadati (unita' di misura, displayName) per il data browser function publishMetadata() { if (!reader) return; + const pluginLogger = createPluginLogger(app); const meta = []; for (const mpptName of reader.mppts.keys()) { const base = `meb.solar.${mpptName}`; @@ -175,9 +212,41 @@ module.exports = function (app) { { path: 'meb.solar.total.current', value: { units: 'A', displayName: 'Total Battery Current' } }, ); - app.handleMessage(plugin.id, { + const metaDelta = { updates: [{ meta, timestamp: new Date().toISOString() }], - }); + }; + try { + pluginLogger(`Pubblico metadata SignalK: ${meta.length} path`); + pluginLogger(`Metadata delta: ${JSON.stringify(metaDelta)}`); + app.handleMessage(plugin.id, metaDelta); + pluginLogger('Metadata SignalK pubblicati'); + } catch (err) { + pluginLogger(`Errore app.handleMessage(metadata): ${err.stack || err.message}`); + app.error(`Errore metadata SignalK: ${err.message}`); + app.setPluginError(err.message); + } + } + + function normalizeOptions(options = {}) { + return { + device: options.device || defaultPluginOptions.device, + baudRate: Number(options.baudRate || defaultPluginOptions.baudRate), + publishIntervalMs: Number(options.publishIntervalMs || defaultPluginOptions.publishIntervalMs), + pollIntervalMs: Number(options.pollIntervalMs || defaultPluginOptions.pollIntervalMs), + mppts: Array.isArray(options.mppts) && options.mppts.length > 0 + ? options.mppts + : defaultPluginOptions.mppts, + }; + } + + function createPluginLogger(appInstance) { + return (message) => { + const logMessage = `[meb-solars] ${new Date().toISOString()} ${message}`; + console.log(logMessage); + if (appInstance && typeof appInstance.debug === 'function') { + appInstance.debug(logMessage); + } + }; } return plugin;