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
5 changed files with 2318 additions and 45 deletions

View file

@ -208,6 +208,20 @@
"loopBattery": "Akku {level} %",
"loopCharging": "Akku {level} %, lädt",
"loopFirmware": "Firmware {version}",
"loopFirmwareCheck": "Nach Firmware-Update suchen",
"loopFirmwareCurrent": "Die Firmware des Armbands ist aktuell.",
"loopFirmwareOffered": "Firmware {version} verfügbar",
"loopFirmwareViaPolar": "Direkt von Polar geholt: Polar erfährt die Adresse dieses Telefons (oder die seines VPN), dazu Firmware-Version und Hardware-Code des Armbands.",
"loopFirmwareViaInstance": "Über deine Instanz geholt: Polar erfährt die Adresse der Instanz, nicht die dieses Telefons, dazu Firmware-Version und Hardware-Code des Armbands.",
"loopFirmwareErases": "Das Update löscht das Armband. Train sichert zuerst, was darauf ist, und richtet es danach wieder mit deinem Profil ein.",
"loopFirmwareUpdate": "Aktualisieren",
"loopFirmwareUpdateTitle": "Firmware des Armbands aktualisieren?",
"loopFirmwareUpdateText": "Train sichert zuerst, was auf dem Armband ist, da das Update es löscht. Das Armband startet neu, was einige Minuten dauert, und Train richtet es danach wieder mit deinem Profil ein. Lass es bis dahin beim Telefon.",
"loopFirmwareStage": "{stage, select, reading{Sichere, was auf dem Armband ist …} fetching{Hole die Firmware …} preparing{Bereite das Armband vor …} writing{Schreibe die Firmware …} finalizing{Schließe das Update ab …} done{Geschrieben} restarting{Das Armband startet neu …} other{Aktualisiere …}}",
"loopFirmwareStageHint": "Lass das Armband beim Telefon und Train geöffnet.",
"loopFirmwareUpdated": "Firmware aktualisiert; das Armband ist wieder eingerichtet.",
"loopFirmwareThroughInstance": "Firmware über deine Instanz",
"loopFirmwareThroughInstanceHint": "Polar erfährt dann die Adresse der Instanz statt die dieses Telefons. Aus, fragt das Telefon Polar direkt.",
"loopProfileChanged": "Dein Körper hat sich geändert, seit das Band eingerichtet wurde",
"loopProfileChangedHint": "Senden richtet das Band mit deinen jetzigen Angaben neu ein.",
"loopSend": "Ans Band senden",

View file

@ -264,6 +264,22 @@
"@loopCharging": {"placeholders": {"level": {"type": "int"}}},
"loopFirmware": "Firmware {version}",
"@loopFirmware": {"placeholders": {"version": {"type": "String", "example": "1.2.3"}}},
"loopFirmwareCheck": "Check for a firmware update",
"loopFirmwareCurrent": "The band's firmware is up to date.",
"loopFirmwareOffered": "Firmware {version} available",
"@loopFirmwareOffered": {"placeholders": {"version": {"type": "String", "example": "2.1.0"}}},
"loopFirmwareViaPolar": "Fetched from Polar directly: Polar learns this phone's address (or its VPN's), with the band's firmware version and hardware code.",
"loopFirmwareViaInstance": "Fetched through your instance: Polar learns the instance's address, not this phone's, with the band's firmware version and hardware code.",
"loopFirmwareErases": "The update erases the band. Train first keeps what is on it, then sets it up again with your profile.",
"loopFirmwareUpdate": "Update",
"loopFirmwareUpdateTitle": "Update the band's firmware?",
"loopFirmwareUpdateText": "Train first keeps what is on the band, as the update erases it. The band restarts, which takes a few minutes, and Train then sets it up again with your profile. Keep it near the phone until then.",
"loopFirmwareStage": "{stage, select, reading{Keeping what is on the band…} fetching{Fetching the firmware…} preparing{Preparing the band…} writing{Writing the firmware…} finalizing{Finishing the update…} done{Written} restarting{The band restarts…} other{Updating…}}",
"@loopFirmwareStage": {"placeholders": {"stage": {"type": "String"}}},
"loopFirmwareStageHint": "Keep the band near the phone, and Train open.",
"loopFirmwareUpdated": "Firmware updated; the band is set up again.",
"loopFirmwareThroughInstance": "Firmware through your instance",
"loopFirmwareThroughInstanceHint": "Polar then learns the instance's address rather than this phone's. Off, the phone asks Polar directly.",
"loopProfileChanged": "Your body changed since the band was set up",
"loopProfileChangedHint": "Sending sets the band up again with your figures as they are now.",
"loopSend": "Send to the band",

View file

@ -1270,6 +1270,90 @@ abstract class AppLocalizations {
/// **'Firmware {version}'**
String loopFirmware(String version);
/// No description provided for @loopFirmwareCheck.
///
/// In en, this message translates to:
/// **'Check for a firmware update'**
String get loopFirmwareCheck;
/// No description provided for @loopFirmwareCurrent.
///
/// In en, this message translates to:
/// **'The band\'s firmware is up to date.'**
String get loopFirmwareCurrent;
/// No description provided for @loopFirmwareOffered.
///
/// In en, this message translates to:
/// **'Firmware {version} available'**
String loopFirmwareOffered(String version);
/// No description provided for @loopFirmwareViaPolar.
///
/// In en, this message translates to:
/// **'Fetched from Polar directly: Polar learns this phone\'s address (or its VPN\'s), with the band\'s firmware version and hardware code.'**
String get loopFirmwareViaPolar;
/// No description provided for @loopFirmwareViaInstance.
///
/// In en, this message translates to:
/// **'Fetched through your instance: Polar learns the instance\'s address, not this phone\'s, with the band\'s firmware version and hardware code.'**
String get loopFirmwareViaInstance;
/// No description provided for @loopFirmwareErases.
///
/// In en, this message translates to:
/// **'The update erases the band. Train first keeps what is on it, then sets it up again with your profile.'**
String get loopFirmwareErases;
/// No description provided for @loopFirmwareUpdate.
///
/// In en, this message translates to:
/// **'Update'**
String get loopFirmwareUpdate;
/// No description provided for @loopFirmwareUpdateTitle.
///
/// In en, this message translates to:
/// **'Update the band\'s firmware?'**
String get loopFirmwareUpdateTitle;
/// No description provided for @loopFirmwareUpdateText.
///
/// In en, this message translates to:
/// **'Train first keeps what is on the band, as the update erases it. The band restarts, which takes a few minutes, and Train then sets it up again with your profile. Keep it near the phone until then.'**
String get loopFirmwareUpdateText;
/// No description provided for @loopFirmwareStage.
///
/// In en, this message translates to:
/// **'{stage, select, reading{Keeping what is on the band…} fetching{Fetching the firmware…} preparing{Preparing the band…} writing{Writing the firmware…} finalizing{Finishing the update…} done{Written} restarting{The band restarts…} other{Updating…}}'**
String loopFirmwareStage(String stage);
/// No description provided for @loopFirmwareStageHint.
///
/// In en, this message translates to:
/// **'Keep the band near the phone, and Train open.'**
String get loopFirmwareStageHint;
/// No description provided for @loopFirmwareUpdated.
///
/// In en, this message translates to:
/// **'Firmware updated; the band is set up again.'**
String get loopFirmwareUpdated;
/// No description provided for @loopFirmwareThroughInstance.
///
/// In en, this message translates to:
/// **'Firmware through your instance'**
String get loopFirmwareThroughInstance;
/// No description provided for @loopFirmwareThroughInstanceHint.
///
/// In en, this message translates to:
/// **'Polar then learns the instance\'s address rather than this phone\'s. Off, the phone asks Polar directly.'**
String get loopFirmwareThroughInstanceHint;
/// No description provided for @loopProfileChanged.
///
/// In en, this message translates to:

View file

@ -702,6 +702,72 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Firmware $version';
}
@override
String get loopFirmwareCheck => 'Nach Firmware-Update suchen';
@override
String get loopFirmwareCurrent => 'Die Firmware des Armbands ist aktuell.';
@override
String loopFirmwareOffered(String version) {
return 'Firmware $version verfügbar';
}
@override
String get loopFirmwareViaPolar =>
'Direkt von Polar geholt: Polar erfährt die Adresse dieses Telefons (oder die seines VPN), dazu Firmware-Version und Hardware-Code des Armbands.';
@override
String get loopFirmwareViaInstance =>
'Über deine Instanz geholt: Polar erfährt die Adresse der Instanz, nicht die dieses Telefons, dazu Firmware-Version und Hardware-Code des Armbands.';
@override
String get loopFirmwareErases =>
'Das Update löscht das Armband. Train sichert zuerst, was darauf ist, und richtet es danach wieder mit deinem Profil ein.';
@override
String get loopFirmwareUpdate => 'Aktualisieren';
@override
String get loopFirmwareUpdateTitle => 'Firmware des Armbands aktualisieren?';
@override
String get loopFirmwareUpdateText =>
'Train sichert zuerst, was auf dem Armband ist, da das Update es löscht. Das Armband startet neu, was einige Minuten dauert, und Train richtet es danach wieder mit deinem Profil ein. Lass es bis dahin beim Telefon.';
@override
String loopFirmwareStage(String stage) {
String _temp0 = intl.Intl.selectLogic(
stage,
{
'reading': 'Sichere, was auf dem Armband ist …',
'fetching': 'Hole die Firmware …',
'preparing': 'Bereite das Armband vor …',
'writing': 'Schreibe die Firmware …',
'finalizing': 'Schließe das Update ab …',
'done': 'Geschrieben',
'restarting': 'Das Armband startet neu …',
'other': 'Aktualisiere …',
},
);
return '$_temp0';
}
@override
String get loopFirmwareStageHint =>
'Lass das Armband beim Telefon und Train geöffnet.';
@override
String get loopFirmwareUpdated =>
'Firmware aktualisiert; das Armband ist wieder eingerichtet.';
@override
String get loopFirmwareThroughInstance => 'Firmware über deine Instanz';
@override
String get loopFirmwareThroughInstanceHint =>
'Polar erfährt dann die Adresse der Instanz statt die dieses Telefons. Aus, fragt das Telefon Polar direkt.';
@override
String get loopProfileChanged =>
'Dein Körper hat sich geändert, seit das Band eingerichtet wurde';

View file

@ -698,6 +698,72 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Firmware $version';
}
@override
String get loopFirmwareCheck => 'Check for a firmware update';
@override
String get loopFirmwareCurrent => 'The band\'s firmware is up to date.';
@override
String loopFirmwareOffered(String version) {
return 'Firmware $version available';
}
@override
String get loopFirmwareViaPolar =>
'Fetched from Polar directly: Polar learns this phone\'s address (or its VPN\'s), with the band\'s firmware version and hardware code.';
@override
String get loopFirmwareViaInstance =>
'Fetched through your instance: Polar learns the instance\'s address, not this phone\'s, with the band\'s firmware version and hardware code.';
@override
String get loopFirmwareErases =>
'The update erases the band. Train first keeps what is on it, then sets it up again with your profile.';
@override
String get loopFirmwareUpdate => 'Update';
@override
String get loopFirmwareUpdateTitle => 'Update the band\'s firmware?';
@override
String get loopFirmwareUpdateText =>
'Train first keeps what is on the band, as the update erases it. The band restarts, which takes a few minutes, and Train then sets it up again with your profile. Keep it near the phone until then.';
@override
String loopFirmwareStage(String stage) {
String _temp0 = intl.Intl.selectLogic(
stage,
{
'reading': 'Keeping what is on the band…',
'fetching': 'Fetching the firmware…',
'preparing': 'Preparing the band…',
'writing': 'Writing the firmware…',
'finalizing': 'Finishing the update…',
'done': 'Written',
'restarting': 'The band restarts…',
'other': 'Updating…',
},
);
return '$_temp0';
}
@override
String get loopFirmwareStageHint =>
'Keep the band near the phone, and Train open.';
@override
String get loopFirmwareUpdated =>
'Firmware updated; the band is set up again.';
@override
String get loopFirmwareThroughInstance => 'Firmware through your instance';
@override
String get loopFirmwareThroughInstanceHint =>
'Polar then learns the instance\'s address rather than this phone\'s. Off, the phone asks Polar directly.';
@override
String get loopProfileChanged =>
'Your body changed since the band was set up';