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 main(List args) async { final host = args.isNotEmpty ? args[0] : '192.168.1.75'; 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 = >>{}; 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 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()) { 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; 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); } else if (m['notification'] != null) { print('${stamp()} ◀ ${m['notification']}'); } }); Future> call(String me, [Map p = const {}]) { final i = ++id; final c = Completer>(); 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); print('\n… écoute pendant ${seconds}s\n'); await Future.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(); }