Implemented:
- MMPT class for data store - SerialPort library for USB-CAN connection - Stream to Singnlak's DataBrowser 3
This commit is contained in:
362
package-lock.json
generated
Normal file
362
package-lock.json
generated
Normal file
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"name": "meb-solars",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meb-solars",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@serialport/parser-readline": "^12.0.0",
|
||||
"serialport": "^12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/binding-mock": {
|
||||
"version": "10.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
|
||||
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "^1.2.1",
|
||||
"debug": "^4.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz",
|
||||
"integrity": "sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"@serialport/parser-readline": "11.0.0",
|
||||
"debug": "4.3.4",
|
||||
"node-addon-api": "7.0.0",
|
||||
"node-gyp-build": "4.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz",
|
||||
"integrity": "sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-11.0.0.tgz",
|
||||
"integrity": "sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "11.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/bindings-cpp/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@serialport/bindings-interface": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
|
||||
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22 || ^14.13 || >=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-byte-length": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz",
|
||||
"integrity": "sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-cctalk": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz",
|
||||
"integrity": "sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-delimiter": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz",
|
||||
"integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-inter-byte-timeout": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz",
|
||||
"integrity": "sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-packet-length": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz",
|
||||
"integrity": "sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-readline": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz",
|
||||
"integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/parser-delimiter": "12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-ready": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-12.0.0.tgz",
|
||||
"integrity": "sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-regex": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-12.0.0.tgz",
|
||||
"integrity": "sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-slip-encoder": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz",
|
||||
"integrity": "sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/parser-spacepacket": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz",
|
||||
"integrity": "sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-12.0.0.tgz",
|
||||
"integrity": "sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/bindings-interface": "1.2.2",
|
||||
"debug": "4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@serialport/stream/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz",
|
||||
"integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz",
|
||||
"integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/serialport": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialport/-/serialport-12.0.0.tgz",
|
||||
"integrity": "sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@serialport/binding-mock": "10.2.2",
|
||||
"@serialport/bindings-cpp": "12.0.1",
|
||||
"@serialport/parser-byte-length": "12.0.0",
|
||||
"@serialport/parser-cctalk": "12.0.0",
|
||||
"@serialport/parser-delimiter": "12.0.0",
|
||||
"@serialport/parser-inter-byte-timeout": "12.0.0",
|
||||
"@serialport/parser-packet-length": "12.0.0",
|
||||
"@serialport/parser-readline": "12.0.0",
|
||||
"@serialport/parser-ready": "12.0.0",
|
||||
"@serialport/parser-regex": "12.0.0",
|
||||
"@serialport/parser-slip-encoder": "12.0.0",
|
||||
"@serialport/parser-spacepacket": "12.0.0",
|
||||
"@serialport/stream": "12.0.0",
|
||||
"debug": "4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/serialport/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/serialport/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/serialport/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/core/constants.js
Normal file
80
src/core/constants.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Costanti del protocollo Poweren MPPT Boost.
|
||||
Riferimento: Manuale Doc. 40.0000.206, Hw v1.2 Rev 01.
|
||||
|
||||
Il microcontrollore degli MPPT (PIC24/dsPIC33 a virgola fissa) trasmette interi a 16 bit.
|
||||
Per ottenere il valore fisico bisogna dividere il raw per il fattore di conversione.
|
||||
*/
|
||||
|
||||
// Fattori di conversione (valore_fisico = raw / factor)
|
||||
const conversionsFactors = {
|
||||
voltageInput: 10, // tensione ingresso [V] es: 296 -> 29.6 V
|
||||
currentInput: 100, // corrente ingresso [A] es: 1100 -> 11.00 A
|
||||
powerInput: 10, // potenza ingresso [W] es: 3260 -> 326.0 W
|
||||
voltageOutput: 10, // tensione uscita [V] es: 864 -> 86.4 V
|
||||
currentOutput: 100, // corrente uscita [A] es: 380 -> 3.80 A
|
||||
powerOutput: 10, // potenza uscita [W] es: 3110 -> 311.0 W
|
||||
temperaturePhase1: 10, // temperatura fase 1 [°C]
|
||||
temperaturePhase2: 10, // temperatura fase 2 [°C]
|
||||
chargeCapacity: 100, // capacita' caricata [Ah] es: 1234 -> 12.34 Ah
|
||||
};
|
||||
|
||||
// Flag del registro Status1 (registro 4): mappa bit -> codice operativo
|
||||
const status1Flags = {
|
||||
0: 'PwrEna', // Power stage abilitato via software
|
||||
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
|
||||
7: 'CCapRst', // Reset contatore Ah
|
||||
8: 'Ph1Ena', // Fase 1 abilitata
|
||||
9: 'Ph2Ena', // Fase 2 abilitata
|
||||
10: 'QREna', // Quasi-Resonant Mode
|
||||
11: 'DirMPPT', // Direzione algoritmo MPPT
|
||||
};
|
||||
|
||||
// Flag del registro Warning (registro 6): mappa bit -> codice di errore/warning
|
||||
const errorsFlags = {
|
||||
0: 'fault',
|
||||
1: 'vi_under_voltage_protection',
|
||||
2: 'vi_low_voltage', // MPPT in standby
|
||||
3: 'vi_over_voltage_protection',
|
||||
4: 'li_over_current_protection',
|
||||
5: 'under_minimum_battery_voltage',
|
||||
6: 'over_maximum_battery_voltage',
|
||||
7: 'current_to_battery_over_max', // protezione hw a 7A verso batteria
|
||||
8: 'internal_hardware_protection',
|
||||
9: 'over_temperature_protection', // sopra 120 °C
|
||||
10: 'memory_flash_read_error',
|
||||
};
|
||||
|
||||
// Indirizzi dei registri richiesti via polling attivo
|
||||
const registerAddresses = {
|
||||
powerInput: 20,
|
||||
powerOutput: 21,
|
||||
temperature1: 35,
|
||||
temperature2: 36,
|
||||
};
|
||||
|
||||
// Mapping dei bitrate CAN al comando slcan corrispondente (Lawicel CAN232/CANUSB)
|
||||
const slcanBitrateCommands = {
|
||||
10000: 'S0',
|
||||
20000: 'S1',
|
||||
50000: 'S2',
|
||||
100000: 'S3',
|
||||
125000: 'S4',
|
||||
250000: 'S5',
|
||||
500000: 'S6',
|
||||
800000: 'S7',
|
||||
1000000: 'S8',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
conversionsFactors,
|
||||
status1Flags,
|
||||
errorsFlags,
|
||||
registerAddresses,
|
||||
slcanBitrateCommands,
|
||||
};
|
||||
38
src/core/errors.js
Normal file
38
src/core/errors.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Utility centralizzate per la gestione degli errori del plugin.
|
||||
Gli errori vengono propagati al SignalK app tramite app.error / app.setPluginError.
|
||||
*/
|
||||
|
||||
// Errore specifico per il driver slcan (connessione USB-CAN)
|
||||
class SlcanDriverError extends Error {
|
||||
constructor(message, cause) {
|
||||
super(message);
|
||||
this.name = 'SlcanDriverError';
|
||||
if (cause) this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
// Errore di parsing dei frame CAN ricevuti
|
||||
class CanFrameParseError extends Error {
|
||||
constructor(message, rawLine) {
|
||||
super(message);
|
||||
this.name = 'CanFrameParseError';
|
||||
this.rawLine = rawLine;
|
||||
}
|
||||
}
|
||||
|
||||
// Errore di timeout su una richiesta di lettura registro
|
||||
class RegisterReadTimeoutError extends Error {
|
||||
constructor(rxId, regAddr) {
|
||||
super(`Timeout su lettura registro ${regAddr} dal nodo CAN 0x${rxId.toString(16)}`);
|
||||
this.name = 'RegisterReadTimeoutError';
|
||||
this.rxId = rxId;
|
||||
this.regAddr = regAddr;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SlcanDriverError,
|
||||
CanFrameParseError,
|
||||
RegisterReadTimeoutError,
|
||||
};
|
||||
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,
|
||||
};
|
||||
281
src/core/reader.js
Normal file
281
src/core/reader.js
Normal file
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
Driver per convertitore USB-CAN in modalita' slcan (Lawicel CAN232 / CANUSB).
|
||||
|
||||
Apre la porta seriale a 115200 bps, esegue la sequenza di init
|
||||
(C\r chiudi, S<n>\r bitrate, O\r apri) e parsea le righe in arrivo
|
||||
terminate da CR. Mantiene una collezione di istanze MPPT e instrada
|
||||
ad esse i frame ricevuti in base al CAN ID, esponendo gli update
|
||||
in formato SignalK delta.
|
||||
|
||||
Eventi emessi:
|
||||
'open' -> bus CAN aperto
|
||||
'frame' -> { id, data: Buffer } per ogni frame valido
|
||||
'updates' -> array di {path, value} pronti per la pubblicazione SignalK
|
||||
'error' -> Error
|
||||
'close' -> chiusura porta
|
||||
*/
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { SerialPort } = require('serialport');
|
||||
const { ReadlineParser } = require('@serialport/parser-readline');
|
||||
|
||||
const { MPPT } = require('./mpptscore');
|
||||
const { slcanBitrateCommands, registerAddresses } = require('./constants');
|
||||
const { SlcanDriverError, CanFrameParseError } = require('./errors');
|
||||
|
||||
// Riconnessione automatica con backoff esponenziale (clamp 30s)
|
||||
const RECONNECT_BASE_DELAY_MS = 1000;
|
||||
const RECONNECT_MAX_DELAY_MS = 30000;
|
||||
|
||||
class MPPTReader extends EventEmitter {
|
||||
constructor({
|
||||
device,
|
||||
canBitrate = 250000,
|
||||
mppts = [],
|
||||
log = () => {},
|
||||
} = {}) {
|
||||
super();
|
||||
this.device = device;
|
||||
this.canBitrate = canBitrate;
|
||||
this.log = log;
|
||||
|
||||
this.port = null;
|
||||
this.parser = null;
|
||||
this.isOpen = false;
|
||||
this.shouldReconnect = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.reconnectTimer = null;
|
||||
|
||||
// Istanzia gli MPPT e costruisce la routing table CAN_ID -> {mppt, type}
|
||||
this.mppts = new Map(); // name -> MPPT
|
||||
this.routing = new Map(); // canId -> { mppt, type: 'status'|'power'|'reply' }
|
||||
|
||||
for (const config of mppts) {
|
||||
const mppt = new MPPT({
|
||||
name: config.id,
|
||||
rxID: config.rxId,
|
||||
txIdBase: config.txIdBase,
|
||||
log: this.log,
|
||||
});
|
||||
this.mppts.set(config.id, mppt);
|
||||
// Broadcast MSG1 (status) e MSG2 (power)
|
||||
this.routing.set(config.txIdBase + 1, { mppt, type: 'status' });
|
||||
this.routing.set(config.txIdBase + 2, { mppt, type: 'power' });
|
||||
// Risposta a richiesta di lettura registro (txIdBase)
|
||||
this.routing.set(config.txIdBase, { mppt, type: 'reply' });
|
||||
}
|
||||
}
|
||||
|
||||
// Apre la porta seriale ed esegue la sequenza di init slcan
|
||||
async open() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.port = new SerialPort({
|
||||
path: this.device,
|
||||
baudRate: 115200,
|
||||
autoOpen: false,
|
||||
});
|
||||
|
||||
this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\r' }));
|
||||
this.parser.on('data', (line) => this._onLine(line));
|
||||
|
||||
this.port.on('error', (err) => {
|
||||
this.log(`[reader] errore porta seriale: ${err.message}`);
|
||||
this.emit('error', new SlcanDriverError(err.message, err));
|
||||
});
|
||||
|
||||
this.port.on('close', () => {
|
||||
this.isOpen = false;
|
||||
this.emit('close');
|
||||
if (this.shouldReconnect) this._scheduleReconnect();
|
||||
});
|
||||
|
||||
this.port.open(async (err) => {
|
||||
if (err) {
|
||||
reject(new SlcanDriverError(`Impossibile aprire ${this.device}: ${err.message}`, err));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this._initSlcan();
|
||||
this.isOpen = true;
|
||||
this.reconnectAttempts = 0;
|
||||
this.emit('open');
|
||||
resolve();
|
||||
} catch (initErr) {
|
||||
reject(initErr);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(new SlcanDriverError(err.message, err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sequenza di inizializzazione slcan: C (close), S<n> (bitrate), O (open)
|
||||
async _initSlcan() {
|
||||
const bitrateCommand = slcanBitrateCommands[this.canBitrate];
|
||||
if (!bitrateCommand) {
|
||||
throw new SlcanDriverError(`Bitrate CAN non supportato: ${this.canBitrate}`);
|
||||
}
|
||||
await this._writeRaw('C\r');
|
||||
await this._delay(50);
|
||||
await this._writeRaw(`${bitrateCommand}\r`);
|
||||
await this._delay(50);
|
||||
await this._writeRaw('O\r');
|
||||
await this._delay(50);
|
||||
}
|
||||
|
||||
// Chiude il canale e la porta
|
||||
async close() {
|
||||
this.shouldReconnect = false;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.port && this.port.isOpen) {
|
||||
try { await this._writeRaw('C\r'); } catch (_) { /* best effort */ }
|
||||
await new Promise((resolve) => this.port.close(() => resolve()));
|
||||
}
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
// Invia una richiesta di lettura registro: t<rxId 3 hex><len=1><regAddr 2 hex>\r
|
||||
async sendReadRequest(rxId, regAddr) {
|
||||
if (!this.isOpen) return;
|
||||
const idHex = rxId.toString(16).toUpperCase().padStart(3, '0');
|
||||
const addrHex = regAddr.toString(16).toUpperCase().padStart(2, '0');
|
||||
const command = `t${idHex}1${addrHex}\r`;
|
||||
await this._writeRaw(command);
|
||||
}
|
||||
|
||||
// Lista dei registri da interrogare in polling per ciascun MPPT
|
||||
getPollTargets() {
|
||||
const targets = [];
|
||||
for (const mppt of this.mppts.values()) {
|
||||
for (const regAddr of Object.values(registerAddresses)) {
|
||||
targets.push({ rxId: mppt.rxID, regAddr });
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
// Costruisce gli update aggregati (somma dei due lati Port + Starboard)
|
||||
buildAggregateUpdates() {
|
||||
let panelPowerSum = 0;
|
||||
let powerSum = 0;
|
||||
let currentSum = 0;
|
||||
let anyOnline = false;
|
||||
|
||||
for (const mppt of this.mppts.values()) {
|
||||
if (!mppt.isOnline()) continue;
|
||||
anyOnline = true;
|
||||
const s = mppt.state;
|
||||
if (s.voltageInput !== null && s.currentInput !== null) panelPowerSum += s.voltageInput * s.currentInput;
|
||||
if (s.voltageOutput !== null && s.currentOutput !== null) powerSum += s.voltageOutput * s.currentOutput;
|
||||
if (s.currentOutput !== null) currentSum += s.currentOutput;
|
||||
}
|
||||
|
||||
if (!anyOnline) return [];
|
||||
const roundedTo2 = (v) => Number(v.toFixed(2));
|
||||
return [
|
||||
{ path: 'meb.solar.total.panelPower', value: roundedTo2(panelPowerSum) },
|
||||
{ path: 'meb.solar.total.power', value: roundedTo2(powerSum) },
|
||||
{ path: 'meb.solar.total.current', value: roundedTo2(currentSum) },
|
||||
];
|
||||
}
|
||||
|
||||
// ---- gestione interna ----
|
||||
|
||||
_writeRaw(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.port.write(data, (err) => {
|
||||
if (err) reject(new SlcanDriverError(err.message, err));
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
_scheduleReconnect() {
|
||||
const delay = Math.min(RECONNECT_BASE_DELAY_MS * Math.pow(2, this.reconnectAttempts), RECONNECT_MAX_DELAY_MS);
|
||||
this.reconnectAttempts++;
|
||||
this.log(`[reader] riconnessione tra ${delay}ms (tentativo ${this.reconnectAttempts})`);
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.open().catch((err) => {
|
||||
this.emit('error', err);
|
||||
if (this.shouldReconnect) this._scheduleReconnect();
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Parsing di una riga slcan in arrivo
|
||||
_onLine(rawLine) {
|
||||
const line = rawLine.toString().trim();
|
||||
if (!line) return;
|
||||
// Risposte di ack/err: 'z', 'Z', BEL (0x07)
|
||||
if (line === 'z' || line === 'Z') return;
|
||||
|
||||
try {
|
||||
const frame = this._parseFrame(line);
|
||||
if (!frame) return;
|
||||
|
||||
this.emit('frame', frame);
|
||||
|
||||
const route = this.routing.get(frame.id);
|
||||
if (!route) return;
|
||||
|
||||
if (route.type === 'status' || route.type === 'power') {
|
||||
route.mppt.updateFromBroadcast(route.type, frame.data);
|
||||
} else if (route.type === 'reply') {
|
||||
route.mppt.updateFromRegisterReply(frame.data);
|
||||
}
|
||||
|
||||
// Emette gli update SignalK aggiornati per questo MPPT
|
||||
const updates = route.mppt.buildSignalKUpdates();
|
||||
if (updates.length) this.emit('updates', updates);
|
||||
} catch (err) {
|
||||
if (err instanceof CanFrameParseError) {
|
||||
this.log(`[reader] frame ignorato: ${err.message}`);
|
||||
} else {
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parser di un singolo frame slcan:
|
||||
// tIIILDD... standard (ID 3 hex)
|
||||
// TIIIIIIIILDD... extended (ID 8 hex)
|
||||
_parseFrame(line) {
|
||||
const type = line[0];
|
||||
if (type !== 't' && type !== 'T') return null;
|
||||
|
||||
const isExtended = type === 'T';
|
||||
const idLen = isExtended ? 8 : 3;
|
||||
if (line.length < 1 + idLen + 1) throw new CanFrameParseError('frame troppo corto', line);
|
||||
|
||||
const idHex = line.substr(1, idLen);
|
||||
const id = parseInt(idHex, 16);
|
||||
if (Number.isNaN(id)) throw new CanFrameParseError('CAN ID non valido', line);
|
||||
|
||||
const dlc = parseInt(line.substr(1 + idLen, 1), 10);
|
||||
if (Number.isNaN(dlc) || dlc < 0 || dlc > 8) throw new CanFrameParseError('DLC non valido', line);
|
||||
|
||||
const dataStart = 1 + idLen + 1;
|
||||
const dataHex = line.substr(dataStart, dlc * 2);
|
||||
if (dataHex.length < dlc * 2) throw new CanFrameParseError('payload incompleto', line);
|
||||
|
||||
const data = Buffer.alloc(dlc);
|
||||
for (let i = 0; i < dlc; i++) {
|
||||
const byte = parseInt(dataHex.substr(i * 2, 2), 16);
|
||||
if (Number.isNaN(byte)) throw new CanFrameParseError('byte payload non valido', line);
|
||||
data[i] = byte;
|
||||
}
|
||||
|
||||
return { id, data, extended: isExtended };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MPPTReader };
|
||||
Reference in New Issue
Block a user