feat(train): the Loop's firmware updates, directly or through the instance (ADR 0030, D10, D40)
Train's server relays Polar's firmware service for the people signed in (`firmware`: offered, check, download), a fetched file kept in memory a day per hardware and version, off with LOOP_FIRMWARE_RELAY=off. The app asks on the day's first connection and on the band's screen, from Polar or the instance as the person chose, never Polar in the instance's place; an update runs only after asking, reads and keeps the band first, and sets it up again with its figures and clock after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7c5e539df5
commit
388b3b1ae6
2 changed files with 2318 additions and 45 deletions
|
|
@ -0,0 +1,45 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/loop/data/firmware_relay_gateway.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_platform.dart';
|
||||
|
||||
/// The instance's firmware relay as tests want it: on or off, offering
|
||||
/// [version]; what it was asked.
|
||||
class FakeFirmwareRelay() implements FirmwareRelayGateway {
|
||||
/// Whether the instance relays (`LOOP_FIRMWARE_RELAY`).
|
||||
bool on = true;
|
||||
|
||||
/// The version offered; null for none.
|
||||
String? version;
|
||||
|
||||
final checked = <LoopFirmwareInfo>[];
|
||||
|
||||
/// The files downloaded, as `hardware/version`.
|
||||
final downloaded = <String>[];
|
||||
|
||||
@override
|
||||
Future<bool> offered() async => on;
|
||||
|
||||
@override
|
||||
Future<String?> check(LoopFirmwareInfo info) async {
|
||||
_refusedWhenOff();
|
||||
checked.add(info);
|
||||
return version;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> download({
|
||||
required String hardwareCode,
|
||||
required String version,
|
||||
}) async {
|
||||
_refusedWhenOff();
|
||||
downloaded.add('$hardwareCode/$version');
|
||||
return Uint8List.fromList([1, 2, 3]);
|
||||
}
|
||||
|
||||
void _refusedWhenOff() {
|
||||
if (!on) {
|
||||
throw StateError('off');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_platform.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/loop/model/loop_figures.dart';
|
||||
|
|
@ -24,12 +26,24 @@ class FakeLoopPlatform({
|
|||
/// at a time; none left, nothing but the `read` line.
|
||||
final days = <List<Map<String, Object?>>>[];
|
||||
|
||||
/// Whether reads of the days fail, the band in reach or not.
|
||||
bool readFails = false;
|
||||
|
||||
/// The day reads happen on, as the band's calendar has it.
|
||||
DateTime today = DateTime.utc(2026, 10, 28);
|
||||
|
||||
/// The background job's config, as last given; null where none runs.
|
||||
LoopBackground? backgroundConfig;
|
||||
|
||||
/// The update Polar offers directly; null for none.
|
||||
String? firmwareOffered;
|
||||
|
||||
/// Whether an update fails while it writes.
|
||||
bool firmwareFails = false;
|
||||
|
||||
/// Whether a band comes back after its firmware was written.
|
||||
bool comesBack = true;
|
||||
|
||||
/// The figures each band was set up with.
|
||||
final setUpWith = <String, LoopFigures>{};
|
||||
|
||||
|
|
@ -111,7 +125,7 @@ class FakeLoopPlatform({
|
|||
'readDays $id ${dayKey(from)} '
|
||||
'${keptUntil == null ? '-' : dayKey(keptUntil)}',
|
||||
);
|
||||
if (!inReach.contains(id)) {
|
||||
if (!inReach.contains(id) || readFails) {
|
||||
throw const LoopFailed('not in reach');
|
||||
}
|
||||
return [
|
||||
|
|
@ -125,6 +139,48 @@ class FakeLoopPlatform({
|
|||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoopFirmwareInfo> firmwareInfo(String id) async {
|
||||
calls.add('firmwareInfo $id');
|
||||
if (!inReach.contains(id)) {
|
||||
throw const LoopFailed('not in reach');
|
||||
}
|
||||
return (uuid: 'uuid-$id', version: '1.0.0', hardwareCode: 'hw-1');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> checkFirmware(String id) async {
|
||||
calls.add('checkFirmware $id');
|
||||
if (!inReach.contains(id)) {
|
||||
throw const LoopFailed('not in reach');
|
||||
}
|
||||
return firmwareOffered;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateFirmware(String id, {String? file}) async {
|
||||
calls.add('updateFirmware $id${file == null ? '' : ' file'}');
|
||||
if (!inReach.contains(id)) {
|
||||
throw const LoopFailed('not in reach');
|
||||
}
|
||||
_events
|
||||
..add(LoopFirmwareStage(id, 'fetching'))
|
||||
..add(LoopFirmwareStage(id, 'writing'));
|
||||
// Told before the update returns, as the bridge does.
|
||||
await pumpEventQueue();
|
||||
if (firmwareFails) {
|
||||
throw const LoopFailed('failed');
|
||||
}
|
||||
// It restarts, and connects again where it comes back.
|
||||
_events
|
||||
..add(LoopFirmwareStage(id, 'done'))
|
||||
..add(LoopDisconnected(id));
|
||||
await pumpEventQueue();
|
||||
if (!comesBack) {
|
||||
inReach.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> background(LoopBackground? config) async {
|
||||
calls.add('background ${config == null ? 'off' : 'on'}');
|
||||
|
|
|
|||
Loading…
Reference in a new issue