etm-powersync-app/tools/rpc/telemetry_watch.dart
Patrick Schurig bedeabf767 fix(banc): l'adresse du banc passe à 10.42.60.21 — sondes, tests on-device, consignes
Le banc `hems` est renuméroté (192.168.1.75 → 10.42.60.21). Portés : les 11
sondes `tools/rpc/*` et leur README, les 3 tests on-device, le `hint` de
l'écran Installations, les procédures de CLAUDE.md et des deux documents de
cadrage.

Ce qui n'est PAS touché, et c'est délibéré : les relevés datés (RELEVE_LOTC,
RECAP, Prompt 3g2, maquettes) et les fixtures `hems75_*.json`. Un relevé daté
dit ce qui a été observé ce jour-là ; le réécrire échange une trace contre une
cohérence de surface. CLAUDE.md porte la note qui fait le lien : `.75` et
`hems75` désignent la même machine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 06:37:33 +02:00

101 lines
4.1 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
// Observation de la télémétrie d'arbitrage — LECTURE SEULE.
//
// Répond à deux questions que la lecture de code ne tranche pas :
//
// §7-5 « télémétrie vivante » — une trame arrive-t-elle sans qu'on la demande, et
// dans quel délai après un changement réel ?
// §7-6 « fraîcheur » — à quelle cadence le battement de cœur bat-il, et le
// `timestamp` avance-t-il vraiment d'un cycle à l'autre ?
//
// Le second point est le plus important : c'est le battement de cœur qui rend un arbitre
// FIGÉ détectable. Les trames continuent d'arriver tandis que leur `timestamp` cesse
// d'avancer. Sans lui, « installation stable » et « moteur arrêté » sont le même silence.
//
// Usage : dart tools/rpc/telemetry_watch.dart [host] [durée_s]
Future<void> main(List<String> args) async {
final host = args.isNotEmpty ? args[0] : '10.42.60.21';
final seconds = args.length > 1 ? int.parse(args[1]) : 180;
final ws = await WebSocket.connect('ws://$host:4444')
.timeout(const Duration(seconds: 8));
var id = 0;
final pending = <int, Completer<Map<String, dynamic>>>{};
final started = DateTime.now();
DateTime? lastFrameAt;
String? lastCycle;
var frames = 0;
String stamp() =>
'+${DateTime.now().difference(started).inSeconds.toString().padLeft(3)}s';
void report(String origin, Map<String, dynamic> p) {
frames++;
final now = DateTime.now();
final cycle = p['timestamp'] as String? ?? 'AUCUN';
final gap = lastFrameAt == null
? '—'
: '${now.difference(lastFrameAt!).inSeconds}s depuis la précédente';
final moved = lastCycle == null
? ''
: (cycle == lastCycle ? ' ⚠ CYCLE IDENTIQUE' : ' cycle avancé');
lastFrameAt = now;
lastCycle = cycle;
final loads = (p['loads'] as List?) ?? const [];
final budget = p['budget'] as Map?;
print('${stamp()} $origin cycle=$cycle ($gap)$moved');
print(' degradedMode=${p['degradedMode']} '
'budget=${budget == null ? 'ABSENT (repli L2)' : 'surplus ${budget['surplusW']} '
'/ alloué ${budget['allocatedW']} / reliquat ${budget['remainingW']}'}');
for (final l in loads.whereType<Map>()) {
final lock = l['lock'] as Map?;
final mech = l['mechanism'] as Map?;
final dec = l['decision'] as Map?;
print(' · ${l['loadId']} alloué=${l['allocatedW']}W '
'available=${l['available']}'
'${l['faultCode'] != null ? ' DÉFAUT=${l['faultCode']}' : ''}'
'${lock != null ? ' verrou=${lock['kind']}/${lock['remainingS']}s' : ''}'
'${mech != null ? ' ${mech['kind']}' : ''}'
' → ${dec?['code']} ${jsonEncode(dec?['params'] ?? {})}');
}
}
ws.listen((raw) {
final m = jsonDecode(raw as String) as Map<String, dynamic>;
final rid = m['id'];
if (rid is int && pending.containsKey(rid)) {
pending.remove(rid)!.complete(m);
} else if (m['notification'] == 'NymeaEnergy.LoadTelemetryChanged') {
report('◀ LoadTelemetryChanged', m['params'] as Map<String, dynamic>);
} else if (m['notification'] != null) {
print('${stamp()} ◀ ${m['notification']}');
}
});
Future<Map<String, dynamic>> call(String me, [Map<String, dynamic> p = const {}]) {
final i = ++id;
final c = Completer<Map<String, dynamic>>();
pending[i] = c;
ws.add(jsonEncode({'id': i, 'method': me, 'params': p}));
return c.future.timeout(const Duration(seconds: 15));
}
await call('JSONRPC.Hello', {'locale': 'fr_FR'});
await call('JSONRPC.SetNotificationStatus', {'enabled': true});
final snap = await call('NymeaEnergy.GetLoadTelemetry');
report('■ GetLoadTelemetry (instantané)', snap['params'] as Map<String, dynamic>);
print('\n… écoute pendant ${seconds}s\n');
await Future<void>.delayed(Duration(seconds: seconds));
print('\n--- RELEVÉ ---');
print('trames reçues (instantané compris) : $frames en ${seconds}s');
print('dernier cycle vu : $lastCycle');
await ws.close();
}