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:
Tizian 「ティツィアーン」 2026-10-28 20:52:53 +01:00
commit 388b3b1ae6
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -14,6 +14,7 @@ import 'package:halkyon_train_flutter/src/feature/day/data/loop_lines.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_connect.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_platform.dart';
import 'package:halkyon_train_flutter/src/feature/health/data/health_store.dart';
import 'package:halkyon_train_flutter/src/feature/loop/data/firmware_relay_gateway.dart';
import 'package:halkyon_train_flutter/src/feature/loop/data/loop_store.dart';
import 'package:halkyon_train_flutter/src/feature/loop/data/polar_loop.dart';
import 'package:halkyon_train_flutter/src/feature/loop/model/loop_figures.dart';
@ -30,6 +31,7 @@ import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
import '../support/fake_firmware_relay.dart';
import '../support/fake_health_platform.dart';
import '../support/fake_loop_platform.dart';
import '../support/fake_recorder_platform.dart';
@ -511,7 +513,10 @@ void main() {
}
/// Train signed in on a phone that holds a Loop, its screen open.
Future<void> openLoop(WidgetTester tester) async {
Future<void> openLoop(
WidgetTester tester, {
FirmwareRelayGateway? relay,
}) async {
loop = PolarLoop(
platform,
LoopStore(File('${folder.path}/loop.json')),
@ -519,6 +524,7 @@ void main() {
days: days,
workouts: WorkoutRepository(MemoryWorkoutGateway(), keys),
backgroundLines: File('${folder.path}/loop_days.txt'),
relay: relay,
);
await start(
tester,
@ -683,6 +689,65 @@ void main() {
expect(platform.calls, contains('reset A1'));
expect(find.text('Set up a Loop'), findsOneWidget);
});
testWidgets('a firmware update found says what Polar learns, and is '
'made only after asking (D40)', (tester) async {
platform.firmwareOffered = '2.1.0';
await tester.runAsync(() => profile.save(body));
await openLoop(tester);
await chooseBand(tester);
expect(find.text('Firmware 2.1.0 available'), findsOneWidget);
expect(
find.textContaining("Polar learns this phone's address"),
findsWidgets,
);
// No relay on this instance: the direct way alone.
expect(find.text('Firmware through your instance'), findsNothing);
await tester.tap(find.text('Update'));
await frames(tester);
expect(find.text("Update the band's firmware?"), findsOneWidget);
await tester.tap(find.text('Cancel'));
await settle(tester);
expect(platform.calls, isNot(contains('updateFirmware A1')));
platform.setUpWith.clear();
await tester.tap(find.text('Update'));
await frames(tester);
await tester.tap(find.widgetWithText(FilledButton, 'Update').last);
await frames(tester);
await settle(tester);
expect(platform.calls, contains('updateFirmware A1'));
expect(platform.setUpWith['A1'], isNotNull);
expect(
find.text('Firmware updated; the band is set up again.'),
findsOneWidget,
);
expect(find.text('Firmware 2.1.0'), findsOneWidget);
expect(find.text('Firmware 2.1.0 available'), findsNothing);
});
testWidgets('an instance relaying firmware offers it; chosen, the update '
'is asked for there (D40)', (tester) async {
final relay = FakeFirmwareRelay()..version = '2.1.0';
await tester.runAsync(() => profile.save(body));
await openLoop(tester, relay: relay);
await chooseBand(tester);
await tester.tap(find.text('Firmware through your instance'));
await settle(tester);
await tester.tap(find.text('Check for a firmware update'));
await settle(tester);
expect(relay.checked, hasLength(1));
expect(find.text('Firmware 2.1.0 available'), findsOneWidget);
expect(
find.textContaining("Polar learns the instance's address"),
findsWidgets,
);
});
});
group('Health Connect (ADR 0030, D31)', () {