Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions lib/ble/adapters/_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,47 @@ enum DeviceKind {
other,
}

/// Which of the three reset-help states an entry is in.
enum ResetKind { specific, unverified, noOfficialSite }

/// What to tell a user whose device will not finish pairing. THREE states:
/// - specific: l10n keys of device-specific steps (>= 1), optional official URL;
/// - unverified: no sourced procedure; safe guidance plus an official URL that
/// is pinned to the vendor's host (follow-up list);
/// - noOfficialSite: the vendor has no official site (generic family, no-name
/// brand); generic steps and a nonblank [reason] (follow-up list).
class ResetSteps {
final ResetKind kind;

/// Device-specific steps, or a safe subset of guidance for an unverified device.
final List<String>? keys;

/// Official https help page. Optional for specific, required for unverified,
/// null for noOfficialSite.
final String? officialUrl;

/// Why there is no official site. Non-null only for noOfficialSite.
final String? reason;
const ResetSteps.specific(List<String> this.keys, {this.officialUrl})
: kind = ResetKind.specific,
reason = null;
const ResetSteps.unverified({required String this.officialUrl, this.keys})
: kind = ResetKind.unverified,
reason = null;
const ResetSteps.noOfficialSite({required String this.reason})
: kind = ResetKind.noOfficialSite,
keys = null,
officialUrl = null;
}

/// l10n keys of the generic reset template, shown for unverified and
/// no-official-site entries.
const List<String> kGenericResetStepKeys = <String>[
'pairResetGenericStep1',
'pairResetGenericStep2',
'pairResetGenericStep3',
];

/// One band the app can discover and connect to.
///
/// The wire format itself stays in `protocol` ([BandProfile] = header length,
Expand Down Expand Up @@ -467,6 +508,9 @@ class BandEntry {
/// sensor). Required so a new entry cannot silently bypass recognition.
final DeviceRole role;

/// What to tell a user whose device will not finish pairing (every entry sets it).
final ResetSteps? resetSteps;

/// What the device physically is; drives the pairing lists' filter pills.
final DeviceKind kind;

Expand Down Expand Up @@ -499,6 +543,7 @@ class BandEntry {
this.logsConsoleOutput = false,
this.nameMatcher,
required this.role,
this.resetSteps,
this.kind = DeviceKind.other,
bool canBeMainDevice = true,
}) : _canBeMainDevice = canBeMainDevice,
Expand Down Expand Up @@ -534,6 +579,7 @@ class BandEntry {
this.scanCompanyIds = const <int>[],
this.scanServiceData = const <String>[],
required this.role,
this.resetSteps,
this.kind = DeviceKind.other,
bool canBeMainDevice = true,
}) : _canBeMainDevice = canBeMainDevice,
Expand Down Expand Up @@ -586,6 +632,16 @@ const BandEntry kWhoopGen4 = BandEntry.framed(
id: 'gen4',
role: DeviceRole.band,
kind: DeviceKind.strap,
resetSteps: ResetSteps.unverified(
// Opening the official app may update the band; use only app-free guidance.
keys: <String>[
'pairResetWhoop5Step3',
'pairResetGenericStep2',
'pairResetWhoop5Step4',
],
officialUrl:
'https://support.whoop.com/s/article/WHOOP-4-0-iOS-Connectivity-and-Pairing?language=en_US',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
),
label: 'WHOOP 4',
gatt: GattProfile.gen4,
wire: BandProfile.gen4,
Expand Down Expand Up @@ -630,6 +686,15 @@ const BandEntry kWhoopGen5 = BandEntry.framed(
setClockDriftGated: true,
burstCountGateEnforced: true,
logsConsoleOutput: true,
resetSteps: ResetSteps.specific(<String>[
'pairResetWhoop5Step1',
'pairResetWhoop5Step2',
'pairResetWhoop5Step3',
'pairResetWhoop5Step4',
],
officialUrl:
'https://www.community.whoop.com/t/problem-with-pairing-mode/10783/2',
),
commands: BandWireCommands(
hello: Cmd.getHello,
helloBody: <int>[0x01],
Expand Down Expand Up @@ -662,6 +727,9 @@ const BandEntry kBleHrs = BandEntry.notify(
role: DeviceRole.sensor,
// HRS alone identifies no body position: watches and armbands use it too.
kind: DeviceKind.other,
resetSteps: ResetSteps.noOfficialSite(
reason: 'Generic Bluetooth heart-rate profile; maker unknown',
),
category: DeviceCategory.workoutSensor,
label: 'Bluetooth heart rate sensor',
service: kHeartRateServiceUuid,
Expand Down Expand Up @@ -696,6 +764,10 @@ const BandEntry kPolarPmd = BandEntry.notify(
id: 'polar_pmd',
role: DeviceRole.sensor,
kind: DeviceKind.armband,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://support.polar.com/en/how-can-i-reset-my-sensor',
),
category: DeviceCategory.workoutSensor,
label: 'Polar sensor',
service: kPolarPmdService,
Expand Down Expand Up @@ -737,6 +809,10 @@ const BandEntry kOura = BandEntry.notify(
id: 'oura',
role: DeviceRole.band,
kind: DeviceKind.ring,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://support.ouraring.com/hc/en-us/articles/360025446854-Factory-Reset-an-Oura-Product',
),
category: DeviceCategory.wearable,
label: 'Oura Ring',
service: kOuraService,
Expand Down Expand Up @@ -781,6 +857,10 @@ const BandEntry kCoros = BandEntry.notify(
id: 'coros',
role: DeviceRole.band,
kind: DeviceKind.watch,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://support.coros.com/hc/en-us/articles/360046709991-How-to-remove-your-COROS-watch-properly-from-paired-smart-device',
),
canBeMainDevice: false,
category: DeviceCategory.workoutSensor,
label: 'Coros watch',
Expand Down Expand Up @@ -829,6 +909,10 @@ const BandEntry kGarmin = BandEntry.notify(
id: 'garmin',
role: DeviceRole.band,
kind: DeviceKind.watch,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://support.garmin.com/',
),
canBeMainDevice: false,
category: DeviceCategory.wearable,
label: 'Garmin watch',
Expand Down Expand Up @@ -877,6 +961,10 @@ const BandEntry kUltrahuman = BandEntry.notify(
id: 'ultrahuman',
role: DeviceRole.band,
kind: DeviceKind.ring,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://www.ultrahuman.com/blog/how-to-reset-ultrahuman-ring-air/',
),
category: DeviceCategory.wearable,
label: 'Ultrahuman Ring Air',
service: kUltrahumanCommandService,
Expand Down Expand Up @@ -918,6 +1006,10 @@ const BandEntry kMiBand234 = BandEntry.notify(
id: 'miband234',
role: DeviceRole.band,
kind: DeviceKind.band,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://www.mi.com/global/support/faq/details/KA-515630/',
),
category: DeviceCategory.wearable,
label: 'Mi Band 2/3',
service: kHuami234Service,
Expand Down Expand Up @@ -949,6 +1041,10 @@ const BandEntry kPebble = BandEntry.notify(
id: 'pebble',
role: DeviceRole.band,
kind: DeviceKind.watch,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://help.repebble.com/en/articles/15403048-restarting-resetting-your-watch',
),
category: DeviceCategory.wearable,
label: 'Pebble',
service: kPebbleServiceUuid,
Expand Down Expand Up @@ -1011,6 +1107,10 @@ const BandEntry kColmi = BandEntry.notify(
id: 'colmi',
role: DeviceRole.band,
kind: DeviceKind.ring,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://www.colmi.com/',
),
category: DeviceCategory.wearable,
label: 'Colmi ring',
service: kColmiService,
Expand All @@ -1030,6 +1130,9 @@ const BandEntry kThermometer = BandEntry.notify(
id: 'thermometer',
role: DeviceRole.sensor,
kind: DeviceKind.thermometer,
resetSteps: ResetSteps.noOfficialSite(
reason: 'Generic Bluetooth thermometer; no single maker',
),
category: DeviceCategory.healthMeasurement,
label: 'Bluetooth thermometer',
service: kHtpService,
Expand Down Expand Up @@ -1058,6 +1161,10 @@ const BandEntry kMiScaleComposition = BandEntry.notify(
id: 'miscale_bc',
role: DeviceRole.sensor,
kind: DeviceKind.scale,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://www.mi.com/global/support/',
),
category: DeviceCategory.healthMeasurement,
label: 'Mi Body Composition Scale',
service: kMiScaleBodyCompositionService,
Expand All @@ -1082,6 +1189,10 @@ const BandEntry kMiScale2 = BandEntry.notify(
id: 'miscale2',
role: DeviceRole.sensor,
kind: DeviceKind.scale,
resetSteps: ResetSteps.unverified(
officialUrl:
'https://www.mi.com/ph/support/article/KA-12895',
),
category: DeviceCategory.healthMeasurement,
label: 'Mi Smart Scale 2',
service: kMiScaleWeightService,
Expand Down
16 changes: 16 additions & 0 deletions lib/ble/reset_url.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Check of a vendor help URL against the entry's pinned official host.

/// Null when [url] is acceptable for an entry whose reviewed host is
/// [pinnedHost]; otherwise a short reason. Acceptable: https, parseable, host
/// equal to [pinnedHost] (case-insensitive), no userinfo.
String? resetUrlProblem(String url, {required String pinnedHost}) {
final u = Uri.tryParse(url.trim());
if (u == null || !u.hasScheme) return 'not a URL';
if (u.scheme != 'https') return 'not https';
if (u.host.isEmpty) return 'no host';
if (u.userInfo.isNotEmpty) return 'has userinfo';
if (u.host.toLowerCase() != pinnedHost.toLowerCase()) {
return 'host ${u.host} is not the pinned $pinnedHost';
}
return null;
}
31 changes: 31 additions & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3510,6 +3510,37 @@
}
},
"pairBandNotSensor": "Dieses Gerät ist ein {label}, also ein Band und kein einfacher Sensor. Es wurde nichts gespeichert.",
"pairResetWhoop5Step1": "Nimm das Band vom Handgelenk und warte, bis die grünen Lichter ausgehen.",
"pairResetWhoop5Step2": "Halte das Band an den Seiten und tippe mit zwei Fingern fest auf die Oberseite, bis nur noch Blau blinkt. Wenn Blau und Weiß zusammen leuchten, tippe weiter, bis nur noch Blau übrig ist.",
"pairResetWhoop5Step3": "Wenn die WHOOP-App auf diesem Telefon ist, beende sie zwangsweise.",
"pairResetWhoop5Step4": "Komm hierher zurück und versuche es noch einmal.",
"pairResetGenericStep1": "Öffne die App des Herstellers und entferne dieses Gerät dort.",
"pairResetGenericStep2": "Suche das Gerät in den Bluetooth-Einstellungen deines Telefons und wähle „Ignorieren“ bzw. „Vergessen“.",
"pairResetGenericStep3": "Starte das Gerät neu, halte es nah an dieses Telefon und versuche es noch einmal.",
"pairResetGenericNote": "Das sind allgemeine Tipps. Die genauen Schritte für dieses Gerät haben wir noch nicht geprüft.",
"pairResetOfficialLink": "Hilfeseite des Herstellers öffnen",
"pairResetTitleBand": "Dein Band ist woanders gekoppelt",
"pairResetTitleRing": "Dein Ring ist woanders gekoppelt",
"pairResetBody": "Wir konnten {name} nicht fertig koppeln. Vermutlich ist es noch mit einem anderen Handy oder einer anderen App verbunden.",
"pairResetHeading": "So setzt du es zurück",
"pairResetWhoopTakeover": "Wenn du es hier koppelst, wird es von der WHOOP-App getrennt.",
"pairResetOfficialSub": "{host} · öffnet sich im Browser",
"pairLinkOpenFailed": "Der Link ließ sich nicht öffnen. Die Adresse lautet {url}",
"pairPickDifferentBand": "Anderes Band wählen",
"pairThisIsWhoop": "Das ist ein WHOOP-Band",
"pairThisIsRing": "Das ist ein Ring",
"pairThisIsWatch": "Das ist eine Uhr",
"pairThisIsFitnessBand": "Das ist ein Fitnessarmband",
"pairThisIsStrap": "Das ist ein Herzfrequenzgurt",
"pairThisIsSensor": "Das ist ein Herzfrequenzsensor",
"pairTroubleTitle": "Probleme beim Verbinden?",
"pairTroubleBody": "Versetze das Band in den Kopplungsmodus und versuche es erneut. Klappt es immer noch nicht? Frag uns.",
"pairTroubleModeLink": "Kopplungsmodus von {device}",
"pairTroubleDiscord": "Auf Discord fragen",
"pairTroubleDiscordSub": "Die OpenStrap-Community",
"pairModeTitle": "In den Kopplungsmodus versetzen",
"pairModeNoSite": "Ein generisches Gerät ohne einzelnen Hersteller, daher gibt es keine Herstelleranleitung.",
"pairOuraExistingKeyGuidance": "Nutzt du schon die Oura-App? Lass den Ring in der App gekoppelt und kopple ihn mit seinem vorhandenen Schlüssel. Überspringe die Schritte zum Zurücksetzen unten. Setze ihn nur zurück, wenn du einen neuen Schlüssel möchtest.",
"welcomeCoachKeyReentry": "Dein API-Schlüssel für den Coach bleibt im sicheren Speicher und wurde nicht gesichert. Gib ihn in den KI-Einstellungen erneut ein, um den Coach auf diesem Telefon zu nutzen.",
"welcomeSettingsSkipped": "{n, plural, =1{1 Einstellung in der Sicherung war nicht lesbar und wurde übersprungen} other{{n} Einstellungen in der Sicherung waren nicht lesbar und wurden übersprungen}}",
"@welcomeSettingsSkipped": {
Expand Down
Loading
Loading