Le bridage a un coupable : cinq cellules du module d'adresse 3
Les exports BatteryView contiennent la sortie brute de la console du BMS : 1798 enregistrements sur 37 jours, avec les 15 tensions de cellule a chaque fois. La preuve etait deja ecrite, il n'y avait rien a attendre de l'apres-midi. Les cellules 2, 6, 10, 12 et 15 montent a 3554-3649 mV quand les dix autres plafonnent a 3440, et descendent a 3128-3236 quand les autres s'arretent a 3252. Monter plus haut ET descendre plus bas est la signature d'une capacite moindre. Le BMS leur attribue d'ailleurs un pourcentage de charge distinct dans chaque enregistrement, et note le module 0 sur 100 contre 100 pour un module sain du meme parc. L'ecart est de 12 mV a 60 % de charge et de 168 mV a 85 % : c'est pourquoi un releve du matin ne peut rien montrer. Deux outils rejouables : la console serie sans dependance, dont le lecteur de table a ete eprouve sur la sortie reelle avant tout branchement, et l'analyse d'historique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
110
HANDOFF.md
@@ -3742,3 +3742,113 @@ un seul module. **A instruire avant de commander quoi que ce soit.**
|
|||||||
|
|
||||||
---
|
---
|
||||||
*Document mis a jour le : 05/09/2026 — bridage identifie : une cellule, des 75 % de SOC, prouve sans cable serie*
|
*Document mis a jour le : 05/09/2026 — bridage identifie : une cellule, des 75 % de SOC, prouve sans cable serie*
|
||||||
|
|
||||||
|
## 49. Session du 05/09/2026 — Le bridage a un coupable nomme : le module d'adresse 3
|
||||||
|
|
||||||
|
Le proprietaire a exporte, depuis BatteryView (machine Windows 10.10.10.93),
|
||||||
|
l'historique et les evenements de deux modules. Ces exports sont la sortie
|
||||||
|
BRUTE de la console du BMS : trente-sept jours d'enregistrements, un toutes les
|
||||||
|
trente minutes, **avec les quinze tensions de cellule a chaque fois**. Il n'y
|
||||||
|
avait donc rien a attendre de l'apres-midi — la preuve etait deja ecrite.
|
||||||
|
|
||||||
|
### Le protocole console, releve et non devine
|
||||||
|
Le module declare lui-meme sa configuration dans la reponse a `info` :
|
||||||
|
|
||||||
|
```
|
||||||
|
Device name : US2000C Cell Number : 15
|
||||||
|
Board : NF4.E2 Specification : 48V/50AH
|
||||||
|
Main Soft version : B69.13.0.0
|
||||||
|
EPONPort rate : 1200 Console Port rate : 115200
|
||||||
|
```
|
||||||
|
|
||||||
|
Invite `pylon_debug>`, fin de reponse `$$`, pagination par
|
||||||
|
`Press [Enter] to be continued`. Commandes utilisees par BatteryView :
|
||||||
|
`info`, `stat`, `data history`, **`datalist history dtls <n>`** (quinze
|
||||||
|
enregistrements par page). Tout est reproductible depuis Linux.
|
||||||
|
|
||||||
|
### Cinq cellules sur quinze, et le BMS le dit lui-meme
|
||||||
|
Sur 1798 enregistrements du module **K220803C30160319, adresse 3** :
|
||||||
|
|
||||||
|
| cellule | tension max | tension min |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 1, 3, 4, 5, 7, 8, 9, 11, 13, 14 | 3436 a 3445 mV | 3251 a 3256 mV |
|
||||||
|
| **2, 6, 10, 12, 15** | **3554 a 3649 mV** | **3128 a 3236 mV** |
|
||||||
|
|
||||||
|
Ces cinq-la **montent plus haut en charge ET descendent plus bas en decharge** :
|
||||||
|
signature d'une capacite moindre, elles se remplissent et se vident avant les
|
||||||
|
autres. Ce n'est pas une erreur de mesure — le BMS leur attribue d'ailleurs un
|
||||||
|
pourcentage de charge different des dix autres (100 % contre 96 %) dans chaque
|
||||||
|
enregistrement.
|
||||||
|
|
||||||
|
Deux sources independantes concordent exactement : le groupe deduit des
|
||||||
|
trente-sept jours d'historique et les cinq cellules les plus basses du releve
|
||||||
|
instantane du 05/09 08:35 sont **les memes cinq**.
|
||||||
|
|
||||||
|
### L'ecart n'apparait qu'en haut, ce qui explique tout
|
||||||
|
Ecart entre cellules, en charge :
|
||||||
|
|
||||||
|
| SOC | ecart median | cellule la plus haute |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 60-69 % | **12 mV** | 3416 mV |
|
||||||
|
| 70-79 % | 27 mV | **3621 mV** |
|
||||||
|
| 80-89 % | **168 mV** | **3649 mV** |
|
||||||
|
|
||||||
|
A 60-69 % le module est irreprochable. C'est pourquoi une capture du matin ne
|
||||||
|
peut rien montrer, et pourquoi le proprietaire « ne voyait rien » dans ses logs
|
||||||
|
precedents. 3649 mV place la cellule dans la zone de protection : le BMS reduit
|
||||||
|
alors le courant de charge autorise pour tout le parc.
|
||||||
|
|
||||||
|
### Le contraste avec un module sain est brutal
|
||||||
|
Compteurs `stat`, deux modules du meme parc :
|
||||||
|
|
||||||
|
| | K220803C30160322 (adresse 1) | K220803C30160319 (adresse 3) |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Bat HV Times | **7** | **416** |
|
||||||
|
| Bat OV Times | 8 | 179 |
|
||||||
|
| CYCLE Times | 0 | 24 |
|
||||||
|
| **SOH** | **100** | **0** |
|
||||||
|
|
||||||
|
⚠️ **Le BMS note lui-meme ce module 0 sur 100.** Et c'est celui que le
|
||||||
|
proprietaire croyait « le moins merdique » (sa BAT4). Le module qu'il juge le
|
||||||
|
plus abime, sa BAT3, est deja hors du parc — il n'est pour rien dans le bridage
|
||||||
|
quotidien. **Le bridage vient d'un module encore en service.**
|
||||||
|
|
||||||
|
### C'est chronique, pas en fuite
|
||||||
|
Depassements de 3550 mV : 1 a 4 releves par jour, **tous les jours depuis le
|
||||||
|
30/07**, sans tendance. Le desequilibre est installe et stable. C'est une
|
||||||
|
bonne nouvelle pour la reparation : rien ne s'emballe.
|
||||||
|
|
||||||
|
### L'horloge du module avance d'environ huit heures
|
||||||
|
Le dernier enregistrement porte `26-09-05 16:42:48` alors que l'export a ete
|
||||||
|
fait a 08:35. Correlation du SOC du module contre le SOC du parc mesure dans
|
||||||
|
InfluxDB : minimum net a **7 h 45 - 8 h 15 d'avance** (ecart moyen 9,7 points
|
||||||
|
contre 30 points sans decalage). La correlation sur le courant donne 6 h,
|
||||||
|
mais elle est ecartee : un enregistrement ne peut pas etre posterieur a
|
||||||
|
l'export qui le lit.
|
||||||
|
|
||||||
|
⚠️ **Toute date lue dans l'historique du BMS doit etre corrigee d'environ
|
||||||
|
-8 h** avant d'etre comparee a InfluxDB, aux journaux n8n ou a Home Assistant.
|
||||||
|
A remettre a l'heure depuis la console.
|
||||||
|
|
||||||
|
### Outils ajoutes
|
||||||
|
- **`tools/pylontech-console.mjs`** — console serie, sans aucune dependance
|
||||||
|
(stty pour la ligne, fs pour les octets). `--detecter` · `--commande help` ·
|
||||||
|
`--historique` (extraction paginee complete) · **`--suivi 30`** (releve
|
||||||
|
cellule par cellule toutes les 30 s, horodate a l'heure REELLE de la machine
|
||||||
|
et non a celle du module, en CSV). Le lecteur de table a ete eprouve sur la
|
||||||
|
sortie reelle de BatteryView avant tout branchement.
|
||||||
|
⚠️ `data` numerote les cellules de 0 a 14, `datalist history dtls` de 1 a 15.
|
||||||
|
L'outil ramene tout sur 1..15.
|
||||||
|
- **`tools/analyse-historique-pylontech.py`** — rejoue l'analyse ci-dessus sur
|
||||||
|
n'importe quel export.
|
||||||
|
|
||||||
|
### Cote materiel
|
||||||
|
Le cable console mesure 30 cm, ce qui oblige a tenir le portable devant
|
||||||
|
l'armoire. La rallonge doit se faire **cote USB**, pas cote RS232 : une
|
||||||
|
rallonge USB active de 5 m coute une dizaine d'euros et supprime le besoin de
|
||||||
|
support. Les quatre pilotes USB-serie usuels (ftdi_sio, ch341, cp210x, pl2303)
|
||||||
|
sont presents sur le poste Ubuntu, et brltty — qui capture souvent les
|
||||||
|
adaptateurs CH340 — y est desactive.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Document mis a jour le : 05/09/2026 (~11:40) — cellules 2, 6, 10, 12, 15 du module adresse 3 identifiees comme cause du bridage ; SOH 0 contre 100 sur un module sain*
|
||||||
|
|||||||
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083417.png
Executable file
|
After Width: | Height: | Size: 361 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083518.png
Executable file
|
After Width: | Height: | Size: 493 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083623.png
Executable file
|
After Width: | Height: | Size: 392 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083654.png
Executable file
|
After Width: | Height: | Size: 425 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083729.png
Executable file
|
After Width: | Height: | Size: 452 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 083804.png
Executable file
|
After Width: | Height: | Size: 397 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 084531.png
Executable file
|
After Width: | Height: | Size: 275 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 084549.png
Executable file
|
After Width: | Height: | Size: 308 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 084623.png
Executable file
|
After Width: | Height: | Size: 290 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 084642.png
Executable file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/mesures/batteries/Capture d'écran 2026-09-05 084656.png
Executable file
|
After Width: | Height: | Size: 493 KiB |
BIN
docs/mesures/batteries/K220803C30160319_event_20260905083554.txt
Executable file
BIN
docs/mesures/batteries/K220803C30160319_event_20260905084321.txt
Executable file
BIN
docs/mesures/batteries/K220803C30160319_history_20260905083542.txt
Executable file
BIN
docs/mesures/batteries/K220803C30160319_history_20260905083818.txt
Executable file
BIN
docs/mesures/batteries/K220803C30160322_event_20260724100819.txt
Executable file
BIN
docs/mesures/batteries/K220803C30160322_history_20260724100806.txt
Executable file
BIN
docs/mesures/batteries/batteryview_run_log_0724100701.txt
Executable file
BIN
docs/mesures/batteries/batteryview_run_log_0905082536.txt
Executable file
BIN
docs/mesures/batteries/batteryview_run_log_0905083309.txt
Executable file
BIN
docs/mesures/batteries/scanlog_0724100702.xlsx
Executable file
BIN
docs/mesures/batteries/scanlog_0905083309.xlsx
Executable file
113
tools/analyse-historique-pylontech.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Analyse les exports d'historique BatteryView d'un module Pylontech.
|
||||||
|
|
||||||
|
BatteryView enregistre la sortie brute de la console du module. Chaque ligne
|
||||||
|
de `datalist history dtls <n>` porte l'etat du module ET les quinze cellules,
|
||||||
|
toutes les trente minutes. Le module en garde 1798, soit trente-sept jours.
|
||||||
|
|
||||||
|
python3 tools/analyse-historique-pylontech.py docs/mesures/batteries/*_history_*.txt
|
||||||
|
|
||||||
|
Ce que l'outil cherche : quelles cellules divergent, de combien, a quel niveau
|
||||||
|
de charge, et si cela s'aggrave. Les tensions sont en millivolts.
|
||||||
|
"""
|
||||||
|
import collections
|
||||||
|
import glob
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Au-dela, le BMS commence a reduire le courant de charge autorise.
|
||||||
|
SEUIL_HAUT_MV = 3550
|
||||||
|
|
||||||
|
|
||||||
|
def lire(chemin):
|
||||||
|
"""Retourne les enregistrements, dedoublonnes par identifiant.
|
||||||
|
|
||||||
|
Les pages de `datalist history dtls` se chevauchent : le meme
|
||||||
|
enregistrement apparait dans plusieurs exports. L'identifiant tranche.
|
||||||
|
"""
|
||||||
|
recs = {}
|
||||||
|
brut = open(chemin, "rb").read()
|
||||||
|
txt = brut.decode("utf-16" if brut[:2] in (b"\xff\xfe", b"\xfe\xff") else "utf-8",
|
||||||
|
errors="replace").replace("\r", "")
|
||||||
|
for ligne in txt.split("\n"):
|
||||||
|
ch = ligne.split()
|
||||||
|
if len(ch) < 18 + 15 * 6:
|
||||||
|
continue
|
||||||
|
if not re.fullmatch(r"\d+", ch[0]) or not re.fullmatch(r"\d\d-\d\d-\d\d", ch[1]):
|
||||||
|
continue
|
||||||
|
recs[int(ch[0])] = dict(
|
||||||
|
id=int(ch[0]), jour=ch[1], heure=ch[2],
|
||||||
|
volt=int(ch[3]), curr=int(ch[4]),
|
||||||
|
etat=ch[10], soc=int(ch[16].rstrip("%")),
|
||||||
|
cellules=[int(ch[18 + k * 6 + 1]) for k in range(15)],
|
||||||
|
)
|
||||||
|
return recs
|
||||||
|
|
||||||
|
|
||||||
|
def main(motifs):
|
||||||
|
recs = {}
|
||||||
|
for m in motifs:
|
||||||
|
for f in sorted(glob.glob(m)):
|
||||||
|
recs.update(lire(f))
|
||||||
|
r = [recs[k] for k in sorted(recs)]
|
||||||
|
if not r:
|
||||||
|
sys.exit("Aucun enregistrement lisible.")
|
||||||
|
|
||||||
|
print(f"{len(r)} enregistrements, du {r[0]['jour']} {r[0]['heure']} "
|
||||||
|
f"au {r[-1]['jour']} {r[-1]['heure']} (heure INTERNE du module)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("TENSION EXTREME ATTEINTE PAR CHAQUE CELLULE")
|
||||||
|
hauts = []
|
||||||
|
for i in range(15):
|
||||||
|
vs = [x["cellules"][i] for x in r]
|
||||||
|
marque = ""
|
||||||
|
if max(vs) > SEUIL_HAUT_MV:
|
||||||
|
marque = " <-- monte trop haut ET descend trop bas"
|
||||||
|
hauts.append(i + 1)
|
||||||
|
print(f" cellule {i+1:>2} : max {max(vs)} mV min {min(vs)} mV{marque}")
|
||||||
|
print()
|
||||||
|
if hauts:
|
||||||
|
print(f" Les cellules {', '.join(map(str, hauts))} se distinguent des "
|
||||||
|
f"{15 - len(hauts)} autres.")
|
||||||
|
print(" Monter plus haut en charge ET descendre plus bas en decharge est la")
|
||||||
|
print(" signature d'une capacite moindre : elles se remplissent et se vident")
|
||||||
|
print(" avant les autres. Ce n'est pas un defaut de mesure.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("ECART ENTRE CELLULES, EN CHARGE, SELON LE NIVEAU DE CHARGE")
|
||||||
|
print(f" {'SOC':>9} {'releves':>8} {'ecart median':>14} {'ecart max':>11} {'cellule la plus haute':>22}")
|
||||||
|
tr = collections.defaultdict(list)
|
||||||
|
for x in r:
|
||||||
|
if x["etat"] == "Charge":
|
||||||
|
tr[min(90, x["soc"] // 10 * 10)].append(x)
|
||||||
|
for k in sorted(tr):
|
||||||
|
e = sorted(max(x["cellules"]) - min(x["cellules"]) for x in tr[k])
|
||||||
|
vm = max(max(x["cellules"]) for x in tr[k])
|
||||||
|
print(f" {k:>4}-{k+9:<4} {len(e):>8} {e[len(e)//2]:>11} mV {max(e):>8} mV {vm:>19} mV")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"DEPASSEMENTS DE {SEUIL_HAUT_MV} mV, PAR JOUR")
|
||||||
|
j = collections.Counter(x["jour"] for x in r if max(x["cellules"]) > SEUIL_HAUT_MV)
|
||||||
|
if not j:
|
||||||
|
print(" aucun")
|
||||||
|
else:
|
||||||
|
for k in sorted(j):
|
||||||
|
print(f" {k} : {j[k]:>2} releve(s) sur 48")
|
||||||
|
n = sorted(j.values())
|
||||||
|
print(f" total {sum(j.values())} releves sur {len(r)}, repartis sur {len(j)} jours ; "
|
||||||
|
f"median {n[len(n)//2]} par jour")
|
||||||
|
print(" Un nombre stable jour apres jour decrit un desequilibre chronique,")
|
||||||
|
print(" pas une degradation en cours.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("LES DIX PIRES MOMENTS")
|
||||||
|
for x in sorted(r, key=lambda z: max(z["cellules"]) - min(z["cellules"]), reverse=True)[:10]:
|
||||||
|
hi, lo = max(x["cellules"]), min(x["cellules"])
|
||||||
|
print(f" {x['jour']} {x['heure']} SOC {x['soc']:>3}% {x['etat']:<7} "
|
||||||
|
f"parc {x['volt']/1000:5.2f} V {x['curr']/1000:>6.2f} A "
|
||||||
|
f"ecart {hi-lo:>3} mV haute = cellule {x['cellules'].index(hi)+1} ({hi} mV)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(sys.argv[1:] or ["docs/mesures/batteries/*_history_*.txt"])
|
||||||
280
tools/pylontech-console.mjs
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Console serie des modules Pylontech US2000C.
|
||||||
|
//
|
||||||
|
// Le protocole n'est pas devine : il est repris tel quel des exports
|
||||||
|
// BatteryView du 05/09/2026 (docs/mesures/batteries/). Le module declare
|
||||||
|
// lui-meme sa vitesse dans la reponse a `info` :
|
||||||
|
// EPONPort rate : 1200
|
||||||
|
// Console Port rate : 115200
|
||||||
|
// L'invite est `pylon_debug>` et la fin de reponse est marquee par `$$`.
|
||||||
|
//
|
||||||
|
// Aucune dependance : stty pour regler la ligne, fs pour lire et ecrire.
|
||||||
|
//
|
||||||
|
// node tools/pylontech-console.mjs --detecter
|
||||||
|
// node tools/pylontech-console.mjs --port /dev/ttyUSB0
|
||||||
|
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --commande help
|
||||||
|
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --historique
|
||||||
|
// node tools/pylontech-console.mjs --port /dev/ttyUSB0 --suivi 30
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
|
||||||
|
const arg = (nom, defaut = null) => {
|
||||||
|
const i = process.argv.indexOf('--' + nom);
|
||||||
|
if (i < 0) return defaut;
|
||||||
|
const v = process.argv[i + 1];
|
||||||
|
return v && !v.startsWith('--') ? v : true;
|
||||||
|
};
|
||||||
|
const DOSSIER = 'docs/mesures/batteries';
|
||||||
|
const VITESSE = arg('vitesse', '115200');
|
||||||
|
const BAVARD = !!arg('bavard');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- detection
|
||||||
|
function portsCandidats() {
|
||||||
|
const base = '/dev/serial/by-id';
|
||||||
|
const out = [];
|
||||||
|
if (fs.existsSync(base)) {
|
||||||
|
for (const n of fs.readdirSync(base)) {
|
||||||
|
out.push({ lien: path.join(base, n), dev: fs.realpathSync(path.join(base, n)), nom: n });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const n of fs.readdirSync('/dev')) {
|
||||||
|
if (/^(ttyUSB|ttyACM)\d+$/.test(n) && !out.some((p) => p.dev === '/dev/' + n)) {
|
||||||
|
out.push({ lien: '/dev/' + n, dev: '/dev/' + n, nom: n });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------- ligne
|
||||||
|
class Ligne {
|
||||||
|
constructor(dev, vitesse) {
|
||||||
|
this.dev = dev;
|
||||||
|
// clocal : ne pas attendre la porteuse (DCD), sinon l'ouverture bloque
|
||||||
|
// sur un cable qui ne cable pas ce signal - le cas de la plupart des
|
||||||
|
// adaptateurs console.
|
||||||
|
const reglage = [
|
||||||
|
'-F', dev, vitesse, 'cs8', '-cstopb', '-parenb', '-crtscts',
|
||||||
|
'clocal', 'raw', '-echo', '-echoe', '-echok', '-ixon', '-ixoff',
|
||||||
|
'min', '0', 'time', '0',
|
||||||
|
];
|
||||||
|
execFileSync('stty', reglage);
|
||||||
|
this.fd = fs.openSync(dev, fs.constants.O_RDWR | fs.constants.O_NOCTTY | fs.constants.O_NONBLOCK);
|
||||||
|
execFileSync('stty', reglage); // l'ouverture peut remettre des defauts
|
||||||
|
this.tampon = '';
|
||||||
|
}
|
||||||
|
ecrire(txt) {
|
||||||
|
if (BAVARD) process.stderr.write(` -> ${JSON.stringify(txt)}\n`);
|
||||||
|
fs.writeSync(this.fd, Buffer.from(txt, 'ascii'));
|
||||||
|
}
|
||||||
|
vider() { this.tampon = ''; }
|
||||||
|
// Lit jusqu'a voir l'un des motifs, ou jusqu'au silence.
|
||||||
|
async lireJusqua(motifs, msMax = 8000, msSilence = 700) {
|
||||||
|
const buf = Buffer.alloc(4096);
|
||||||
|
const debut = Date.now();
|
||||||
|
let dernier = Date.now();
|
||||||
|
for (;;) {
|
||||||
|
let n = 0;
|
||||||
|
try { n = fs.readSync(this.fd, buf, 0, buf.length, null); }
|
||||||
|
catch (e) { if (e.code !== 'EAGAIN') throw e; }
|
||||||
|
if (n > 0) {
|
||||||
|
this.tampon += buf.slice(0, n).toString('ascii');
|
||||||
|
dernier = Date.now();
|
||||||
|
for (const m of motifs) if (this.tampon.includes(m)) return { texte: this.tampon, motif: m };
|
||||||
|
} else {
|
||||||
|
if (this.tampon && Date.now() - dernier > msSilence) return { texte: this.tampon, motif: null };
|
||||||
|
if (Date.now() - debut > msMax) return { texte: this.tampon, motif: null };
|
||||||
|
await new Promise((r) => setTimeout(r, 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fermer() { try { fs.closeSync(this.fd); } catch {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
const FINS = ['pylon_debug>', '$$', 'Press [Enter] to be continued'];
|
||||||
|
|
||||||
|
async function commande(l, cmd, msMax = 12000) {
|
||||||
|
l.vider();
|
||||||
|
l.ecrire(cmd + '\r');
|
||||||
|
let tout = '';
|
||||||
|
for (let page = 0; page < 200; page++) {
|
||||||
|
const { texte, motif } = await l.lireJusqua(FINS, msMax);
|
||||||
|
tout += texte;
|
||||||
|
l.vider();
|
||||||
|
if (motif !== 'Press [Enter] to be continued') break;
|
||||||
|
l.ecrire('\r'); // page suivante
|
||||||
|
}
|
||||||
|
return tout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ lecture
|
||||||
|
function cellules(txt) {
|
||||||
|
// Table produite par `data` / `data history` :
|
||||||
|
// Battery Volt Curr Tempr Base State ...
|
||||||
|
// 0 3303 -7623 23300 Dischg ...
|
||||||
|
const out = [];
|
||||||
|
for (const ligne of txt.split('\n')) {
|
||||||
|
const m = ligne.trim().match(/^(\d{1,2})\s+(\d{4})\s+(-?\d+)\s+(-?\d+)\s+(\w+)/);
|
||||||
|
// `data` numerote les cellules a partir de 0, `datalist history dtls` a
|
||||||
|
// partir de 1. On ramene tout sur 1..15, la numerotation de l'historique,
|
||||||
|
// pour que les deux sources se comparent sans piege.
|
||||||
|
if (m) out.push({ n: +m[1] + 1, mV: +m[2], mA: +m[3], mC: +m[4], etat: m[5] });
|
||||||
|
}
|
||||||
|
return out.length >= 10 ? out : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableau(cel) {
|
||||||
|
const v = cel.map((c) => c.mV);
|
||||||
|
const hi = Math.max(...v), lo = Math.min(...v);
|
||||||
|
const nHi = cel.find((c) => c.mV === hi).n, nLo = cel.find((c) => c.mV === lo).n;
|
||||||
|
let s = '';
|
||||||
|
for (const c of cel) {
|
||||||
|
const marque = c.mV === hi ? ' <-- la plus haute' : c.mV === lo ? ' <-- la plus basse' : '';
|
||||||
|
const barre = '#'.repeat(Math.max(0, Math.round((c.mV - lo) / Math.max(1, hi - lo) * 30)));
|
||||||
|
s += ` cellule ${String(c.n).padStart(2)} : ${c.mV} mV ${barre.padEnd(30)}${marque}\n`;
|
||||||
|
}
|
||||||
|
s += `\n ecart ${hi - lo} mV entre la cellule ${nHi} (${hi} mV) et la cellule ${nLo} (${lo} mV)\n`;
|
||||||
|
if (hi >= 3600) s += ` ATTENTION : ${hi} mV, la protection de surtension est proche.\n`;
|
||||||
|
else if (hi >= 3500) s += ` la cellule ${nHi} entre dans la zone haute.\n`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function journal(nom, texte) {
|
||||||
|
fs.mkdirSync(DOSSIER, { recursive: true });
|
||||||
|
const f = path.join(DOSSIER, nom);
|
||||||
|
fs.appendFileSync(f, texte);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
const horodate = () => new Date().toLocaleString('fr-FR', { timeZone: 'Europe/Paris' });
|
||||||
|
const tampon = () => new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Paris' }).replace(/[-: ]/g, '').slice(0, 12);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------- main
|
||||||
|
async function main() {
|
||||||
|
if (arg('detecter')) {
|
||||||
|
const p = portsCandidats();
|
||||||
|
if (!p.length) {
|
||||||
|
console.log('Aucun port serie. Le cable n\'est pas branche, ou son pilote ne charge pas.');
|
||||||
|
console.log('Apres branchement : dmesg | tail -20 doit nommer ftdi_sio, ch341, cp210x ou pl2303.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('Ports serie visibles :');
|
||||||
|
for (const x of p) console.log(` ${x.dev.padEnd(14)} ${x.nom}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dev = arg('port');
|
||||||
|
if (!dev) {
|
||||||
|
const p = portsCandidats();
|
||||||
|
if (p.length !== 1) {
|
||||||
|
console.log(p.length ? 'Plusieurs ports, precisez --port :' : 'Aucun port serie detecte.');
|
||||||
|
for (const x of p) console.log(` ${x.dev} ${x.nom}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
dev = p[0].dev;
|
||||||
|
console.log(`Port retenu : ${dev}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = new Ligne(dev, VITESSE);
|
||||||
|
process.on('exit', () => l.fermer());
|
||||||
|
|
||||||
|
// Reveil : on envoie une entree et on regarde ce qui revient.
|
||||||
|
// On ne suppose rien - si l'invite n'apparait pas, on montre l'octet brut.
|
||||||
|
l.ecrire('\r');
|
||||||
|
let r = await l.lireJusqua(FINS, 3000);
|
||||||
|
if (!r.motif) {
|
||||||
|
l.vider(); l.ecrire('~\r'); // certains firmwares veulent ~
|
||||||
|
r = await l.lireJusqua(FINS, 3000);
|
||||||
|
}
|
||||||
|
if (!r.motif) {
|
||||||
|
console.log(`Pas d'invite a ${VITESSE} bauds. Recu : ${JSON.stringify(r.texte.slice(-200))}`);
|
||||||
|
console.log('Essayez --vitesse 1200 (port EPON) ou verifiez le cable.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
l.vider();
|
||||||
|
|
||||||
|
if (arg('commande')) {
|
||||||
|
const cmd = arg('commande');
|
||||||
|
const txt = await commande(l, String(cmd));
|
||||||
|
console.log(txt);
|
||||||
|
console.log('--- journalise dans ' + journal(`${tampon()}-console.txt`,
|
||||||
|
`\n===== ${horodate()} =====\n${cmd}\n${txt}\n`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg('historique')) {
|
||||||
|
const depuis = +(arg('depuis', '1')) || 1;
|
||||||
|
const nom = `${tampon()}-historique.txt`;
|
||||||
|
console.log(`Extraction de l'historique a partir de l'enregistrement ${depuis}.`);
|
||||||
|
console.log('Le module en garde 1798, un toutes les 30 minutes : comptez plusieurs minutes.');
|
||||||
|
let f = journal(nom, `# releve du ${horodate()} sur ${dev}\n`);
|
||||||
|
f = journal(nom, await commande(l, 'info', 8000));
|
||||||
|
for (let i = depuis; i < 1800; i += 15) {
|
||||||
|
const txt = await commande(l, `datalist history dtls ${i}`, 20000);
|
||||||
|
journal(nom, txt);
|
||||||
|
if (!/\d{2}-\d{2}-\d{2}/.test(txt)) break;
|
||||||
|
process.stdout.write(`\r enregistrement ${i}... `);
|
||||||
|
}
|
||||||
|
console.log(`\nEcrit dans ${f}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg('suivi')) {
|
||||||
|
const pas = Math.max(10, +(arg('suivi', '30')) || 30);
|
||||||
|
const nom = `${tampon()}-suivi.csv`;
|
||||||
|
const f = path.join(DOSSIER, nom);
|
||||||
|
fs.mkdirSync(DOSSIER, { recursive: true });
|
||||||
|
if (!fs.existsSync(f)) {
|
||||||
|
fs.writeFileSync(f, 'heure_reelle,etat,courant_mA,' +
|
||||||
|
Array.from({ length: 15 }, (_, i) => `c${i + 1}_mV`).join(',') + ',ecart_mV\n');
|
||||||
|
}
|
||||||
|
console.log(`Suivi toutes les ${pas} s, cellule par cellule -> ${f}`);
|
||||||
|
console.log('L\'heure enregistree est l\'heure REELLE de cette machine, pas celle du module');
|
||||||
|
console.log('(l\'horloge interne du module derive de plusieurs heures).');
|
||||||
|
console.log('Ctrl-C pour arreter.\n');
|
||||||
|
for (;;) {
|
||||||
|
const txt = await commande(l, 'data', 10000);
|
||||||
|
const cel = cellules(txt);
|
||||||
|
if (cel) {
|
||||||
|
const v = cel.map((c) => c.mV);
|
||||||
|
const hi = Math.max(...v), lo = Math.min(...v);
|
||||||
|
fs.appendFileSync(f, [
|
||||||
|
new Date().toISOString(), cel[0].etat, cel[0].mA, ...v, hi - lo,
|
||||||
|
].join(',') + '\n');
|
||||||
|
const nHi = cel.find((c) => c.mV === hi).n;
|
||||||
|
console.log(`${horodate()} ${String(cel[0].etat).padEnd(7)} ` +
|
||||||
|
`${String((cel[0].mA / 1000).toFixed(2)).padStart(8)} A ` +
|
||||||
|
`haute ${hi} mV (cellule ${nHi}) ecart ${String(hi - lo).padStart(3)} mV` +
|
||||||
|
(hi >= 3600 ? ' ATTENTION zone de protection' : ''));
|
||||||
|
} else {
|
||||||
|
console.log(`${horodate()} reponse non exploitable`);
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, pas * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Par defaut : un instantane complet.
|
||||||
|
const nom = `${tampon()}-instantane.txt`;
|
||||||
|
let tout = `# releve du ${horodate()} sur ${dev} a ${VITESSE} bauds\n`;
|
||||||
|
for (const c of ['info', 'stat', 'data']) {
|
||||||
|
const txt = await commande(l, c, 12000);
|
||||||
|
tout += `\n===== ${c} =====\n${txt}\n`;
|
||||||
|
if (c === 'info') {
|
||||||
|
const m = txt.match(/Barcode\s*:\s*(\S+)/); const a = txt.match(/Device address\s*:\s*(\d+)/);
|
||||||
|
console.log(`Module ${m ? m[1] : '?'}, adresse ${a ? a[1] : '?'}`);
|
||||||
|
}
|
||||||
|
if (c === 'stat') {
|
||||||
|
for (const k of ['Bat HV Times', 'Bat OV Times', 'SOH Times', 'SOH ', 'CYCLE Times']) {
|
||||||
|
const m = txt.match(new RegExp(k.trim() + '\\s*:\\s*(\\S+)'));
|
||||||
|
if (m) console.log(` ${k.trim().padEnd(14)} ${m[1]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (c === 'data') {
|
||||||
|
const cel = cellules(txt);
|
||||||
|
if (cel) { console.log(); console.log(tableau(cel)); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Journalise dans ' + journal(nom, tout));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error('Echec : ' + e.message); process.exit(1); });
|
||||||