Commit Graph
467 Commits
Author SHA1 Message Date
José Rebelo 36a3fa7cd2 Forerunner 170: Initial support 2026-06-19 08:30:01 +01:00
Gideon Zenz a666b4acb1 Health Connect: Debounce event-triggered sync and fix resting HR lower bound
Two robustness fixes for the event-triggered Health Connect sync.

Debounce: an activity fetch can broadcast ACTION_NEW_DATA several times in
quick succession - multi-phase fetches and some chatty drivers emit the finish
signal mid-fetch - and each broadcast immediately enqueued a sync. A sync that
runs while the fetch is still in progress reads a half-populated database and
can drop or duplicate records for the in-flight range. Enqueue the per-device
worker with a short initial delay and ExistingWorkPolicy.REPLACE, so a burst of
broadcasts collapses into one sync that runs once the fetch has settled. The
per-device work name is unchanged (devices debounce independently) and the
manual/debug sync paths use separate work names, so "Sync now" is unaffected.

Resting heart rate: the platform RestingHeartRateRecord rejects bpm < 1, but
the filter allowed 0..300, so a 0 bpm sample threw IllegalArgumentException and
aborted that data type's slice. Filter 1..300 instead, matching the regular
HeartRate syncer. Update the existing boundary test accordingly.
2026-06-18 16:18:21 +02:00
José Rebelo 8b6924f60a Merge pull request 'add BundleUtils.addToBundle' (#6284)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6284
2026-06-14 19:55:46 +02:00
Thomas Kuehne 1313392a84 add BundleUtils.addToBundle
- deduplicate code from BangleJSDeviceSupport and IntentApiReceiver
- add support for more standard types
- add BangleJS support for simple JSON arrays and JSON null values:
   - `"nullValue" : null`
   - `"booleanArray" : [true, false]`
   - `"doubleArray" : [1.2, -3.4]`
   - `"longArray" : [12, -34]`
   - `"StringArray" : ["12", "xyz"]`
2026-06-14 10:36:11 +00:00
Thomas Kuehne 16826a3091 fix: SampleProviderTest's use of addGBActivitySamples 2026-06-14 10:31:41 +00:00
José Rebelo cb5c820f4f Merge pull request 'Health Connect: Recover late-arriving step samples to fix step count discrepancies' (#6215)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6215
2026-06-14 00:55:52 +02:00
José Rebelo 0c71905c6c Merge pull request 'Health Connect: Harden sync-start selection against non-positive sample timestamps' (#6207)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6207
2026-06-13 19:50:12 +02:00
duhowandJosé Rebelo 25974bc504 add device name to test 2026-06-13 16:43:12 +02:00
NTeditorandJosé Rebelo 62406519d1 Add realme Buds T100/T200/T300 to AbstractDeviceCoordinatorTest.deviceMatchingByNameTest 2026-06-13 16:38:37 +02:00
José Rebelo 438eb1b80f Merge pull request 'fix typos in device independent comments' (#6279)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6279
2026-06-13 16:33:49 +02:00
Gideon Zenz d4c36e2e36 Health Connect: Exclude bad-measurement sentinels from HR and SpO2 sync
PR #6195 widened the syncer guards to match Health Connect's enforced
bounds, but widened too much: it overlooked that some of Gadgetbridge's
in-band sentinel values fall inside HC's accepted range, so placeholder
readings leaked into Health Connect and showed up as spurious data points.

- HeartRate / RestingHeartRate: 255 is GB's "illegal value" marker for a
  bad measurement (ActivitySample.getHeartRate), and it sits inside HC's
  1..300 / 0..300 range. Exclude it explicitly while keeping HC's bounds.
- SpO2: 0 means "not measured" (sample providers return null on 0 and all
  SpO2 charts filter getSpo2() > 0). Raise the lower bound to 1.

Add HeartRateSyncerTest covering the HeartRateSyncer.sync() path (sentinel
255 and 0 are not inserted; valid range passes) and extend
SyncerRangeValidationTest with the new boundaries.
2026-06-13 11:13:37 +02:00
Thomas Kuehne bb03a2d5eb fix typos in device independent comments 2026-06-12 14:23:06 +00:00
José Rebelo 1f65fc26eb Victron SmartShunt: Initial support 2026-06-11 21:34:11 +01:00
Thomas KuehneandJosé Rebelo 6ad6638605 fix device independent typos 2026-06-10 17:43:46 +02:00
José Rebelo fd03accd4c Xiaomi Smart Band 10 Pro: Experimental support 2026-06-08 21:09:15 +01:00
Ingvar StepanyanandJosé Rebelo c1946573fb Garmin: ExploreSync historical activity import
Wires the new ExploreSync sub-service into ProtocolBufferHandler and GarminSupport so a paired device's historical-line catalog can be fetched independently of FIT — which Garmin Connect Mobile irreversibly archives files out of after the first sync (see issues like #5694 and #5994). Activities the FIT path can no longer see become visible again here.

Each historical line is reconstructed into an ActivityTrack, written as GPX, and persisted as a BaseActivitySummary keyed on the first-point timestamp — the same surface FIT-imported workouts use, so UI and exporters need no ExploreSync awareness. GarminCoordinator now picks the activity track provider from the summary contents (gpxTrack vs rawDetailsPath) instead of hard-coding the FIT one.
2026-06-06 21:26:51 +02:00
Gideon Zenz 8be39c99ea Health Connect: Advance sync cursor for temperature records
TemperatureSyncer is the only syncer implementing HealthConnectSyncer
directly instead of inheriting from AbstractTimeSampleSyncer or
AbstractActivitySampleSyncer, both of which populate latestRecordTimestamp
in their base sync(). Its hand-rolled sync() never set the field, so the
orchestrator had nothing to advance the persisted cursor with and the
TEMPERATURE cursor stayed pinned at its starting value. Every sync then
re-read from that frozen point to now and re-inserted the whole span; the
per-record clientRecordId dedup hid the duplicates in Health Connect, so it
was silent but re-inserted weeks of records on each run.

Return the furthest-forward edge of the inserted records (body record time,
skin record endTime) as latestRecordTimestamp on the success path. The no-op
early returns keep it null, which correctly holds the cursor, matching every
other syncer's "nothing synced" behaviour. The cursor self-heals on the next
successful sync; no reset needed.
2026-06-05 23:29:37 +02:00
Gideon Zenz 05dffc4932 Health Connect: Use sync run timestamp as clientRecordVersion for activity records
The per-minute steps, calories and distance records set clientRecordVersion
to the metric value itself. Health Connect keeps the record with the highest
clientRecordVersion on a clientRecordId collision (newVersion >= existing
overwrites), so each minute became pinned to the largest value ever sent for
it. When a device delivers activity in two phases and revises a minute's value
downward between them, the corrected record carries a lower version and is
silently ignored, leaving Health Connect above the device's own total.

Stamp clientRecordVersion with the sync run's wall-clock instead, shared by
every record in the slice. A later run always outranks the value it previously
wrote for a minute, so the freshest re-read wins regardless of whether the
value rose or fell. The clientRecordId remains the dedup key and is unchanged.
2026-06-05 14:56:03 +02:00
Gideon Zenz 829f9b7c31 Health Connect: Recover late-arriving step samples
The ACTIVITY sync cursor is a single (deviceId, dataType) timestamp shared by
the steps, active-calories and distance syncers. The cursor advances to the
latest record produced by any of them. When a device delivers calorie/distance
detail for the most recent minutes before the matching step detail, the cursor
moves past those minutes on the calorie front; the step minutes that arrive on
the next sync are then clipped by the slice start boundary and lost forever.
Resetting the HC sync state recovered them, confirming the data was present in
the database but skipped.

Give the per-minute steps, calories and distance records a deterministic
clientRecordId (gb-{type}-{manufacturer}-{model}-{endEpochSecond}) so that
re-emitting a record upserts instead of duplicating, mirroring SleepSyncer.
With records now idempotent, extend the per-syncer slice lower bound backwards
by a one-hour lookback so the trailing window is re-emitted on the next sync
and the late minutes are recovered. Heart rate is a grouped series keyed on its
start time, cannot be deduped this way, and does not extend the shared base, so
it keeps strict boundaries and never re-emits.
2026-06-05 14:56:03 +02:00
José Rebelo a3dc90ed92 Matson MA98415: Initial support (#6212)
Protocol source: https://github.com/jakka351/Battery-Monitor-Protocol
2026-05-31 22:16:19 +01:00
José Rebelo 4a03038d3a Colmi V76: Fix workout parsing 2026-05-31 17:35:52 +01:00
José Rebelo 622490d83f Merge pull request 'Health Connect: Drop out-of-range sample values to prevent sync abort' (#6195)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6195
2026-05-30 22:00:59 +02:00
José Rebelo d0c742174f Fix tests 2026-05-30 13:36:08 +01:00
Gideon Zenz c516b6e379 Health Connect: Harden sync-start selection against non-positive sample timestamps
When determining the Health Connect sync start point for activity-sample
based data types (ACTIVITY, SLEEP), getFirstSampleTimestamp inspects only
the single oldest row and discards it if its timestamp is not positive.
If that oldest row has a 0 or otherwise non-positive timestamp, the start
point is null and sync is skipped for every data type backed by that table,
while data types on separate tables keep syncing.

Add a getFirstActivitySample(after) overload returning the oldest sample
with a timestamp strictly greater than the given limit, and use it with 0
so an invalid head row no longer disables start-point determination.
2026-05-29 15:31:12 +02:00
Gideon Zenz 6c70aa371d Health Connect: Drop out-of-range sample values to prevent sync abort
Health Connect enforces hard min/max bounds on each record-type field; a
single out-of-range value caused the record constructor to throw and
killed the entire batch insert. The trigger was a Garmin Venu 3
producing HRV > 200 ms, but several syncers shared the same gap (no
upper bound, only > 0 or no validation at all).

Add explicit per-syncer numeric guards matching the bounds enforced by
HC's record constructors, drop out-of-range samples, log them with the
existing [HC_SYNC] prefix, and continue the sync.

Bounds applied (per-record HC enforcement):
- HRV: 1..200 ms
- VO2Max: 0..100 ml/kg/min
- Weight: 0..1000 kg
- BloodGlucose: 0..900.91 mg/dL (= 50 mmol/L) -- previously unvalidated
- RespiratoryRate: 0..1000 breaths/min
- HeartRate / RestingHR: 1..300 / 0..300 bpm (widened from 20..250 to
  match HC's contract; downstream consumers can apply physiological
  filters)
- SpO2: 0..100 %
- Steps: 1..1000000 per record
- ActiveCalories: 0..1000000 kcal per record
- Distance: 0..1000000 m per record
- Skin temperature delta: -30..30 °C
- Body temperature: existing physiological clamp 25..45 °C kept (HC has
  no enforced bound)

Add Logger.skipOutOfRange extension in syncers/SyncerLogging.kt and
relax HC_SYNC_TAG visibility from private to internal so syncers can
share it.

HeartRateSync aggregates dropped-sample counts into one summary log
line per slice rather than per-sample, to avoid log spam from glitchy
firmware.

convertSample visibility relaxed from protected to internal in the two
abstract base syncers so the new boundary tests can call it directly.

30 new unit tests in SyncerRangeValidationTest cover boundary
conditions per syncer.
2026-05-27 11:44:35 +02:00
Gideon Zenz 7832f8a4a8 Health Connect: Sanitise GPS route to prevent ExerciseRoute crash
ExerciseRoute requires strictly increasing timestamps; duplicate-second
points from device GPS parsers (e.g. Xiaomi Smart Band 9 Pro outdoor
cycling) caused IllegalArgumentException at construction time, aborting
the workout sync entirely.

Extract route construction into buildSanitisedRoute, which:
- drops points outside the workout time window
- drops points with non-finite or out-of-range lat/lng (HC's Location
  constructor enforces lat in [-90, 90], lng in [-180, 180])
- drops points with non-finite or negative hdop/vdop accuracy (HC
  rejects negative accuracy)
- deduplicates points by Instant
- returns null if fewer than two usable points remain
- catches IllegalArgumentException as a safety net so future HC
  invariants degrade gracefully (workout saves without route rather
  than the whole sync aborting)

Logs dropped points with the [HC_SYNC] prefix so users can grep.
2026-05-27 10:20:07 +02:00
José Rebelo 86d42bab78 GPX: Fix parsing of timestamps without timezone
Although these are non-conforming, there are a lot of gpx files online
that do not contain a timezone and fail to be parsed.
2026-05-23 18:42:49 +01:00
Martin BraunandJosé Rebelo b8a85e03ea Add Third Party Alarm Intent API: Set and Dismiss alarms for your device from any app (#6088)
Adds the capability to set and dismiss alarms from third party apps through an Intent API, fixes #5553

**This PR was co-authored by ChatGPT and José Rebelo (passively):**

- Scaffold for the core component `DeviceAlarmReceiver` was taken from José's `DeviceSettingsReceiver`
- The core implementation was written by me by hand, ChatGPT was used to review and find potential issues and bugs
- The `DeviceAlarmReceiverTest` unit tests were written by ChatGPT and reviewed by me

**About these changes:**

- Adds a new developer settings entry for the device that the user needs to enable, so other apps can use the Intent API for such device
- Supports app blacklisting as it is done in the `DeviceSettingsReceiver`
- `ConfigureAlarms.addMissingAlarms()` was migrated to `DBHelper.fillMissingAlarms()` to provide this abstraction globally
- `AlarmReceiver` was migrated to `SunriseSunsetAlarmReceiver` to avoid ambiguity with this feature; the formerly called `AlarmReceiver` was related to setting calendar events based on the sunrise/sunset via an Intent API
- The API is registered in the `DeviceCommunicationService`
- The `ConfigureAlarms` activity updates its list after setting alarms from a third party app by broadcasting `ACTION_SAVE_ALARMS`
- It is possible to set alarms of a device that is offline, it should sync when the device connects next time, unless it downloads the alarms from the device when connecting
- However, this device-to-GadgetBridge alarm sync API is not commonly abstracted, so we cannot respect existing alarms on the devices when changing alarms within the new Intent API; the database is the single source of truth and connecting the device after using the API will reset alarms
- ~~The API was designed to align with the `AlarmClock` API from Android, but~~ there is no feature parity to improve the design and interaction; after all the `AlarmClock` API is interacted with `startActivity`, but `GadgetBridge` uses `sendBroadcast` receivers, which is recommend for background tasks
- Errors are not raised to the caller (i.e. providing unknown or invalid Mac Address of device or trying to set an alarm on a nonexisting slot); these errors are logged though

**EDIT: Additional changes during the development:**

- Since slot IDs are unpredictable, the flow is title-driven (there is no way to provide an ID)
- Dismissing an alarm through the API removes its title
- Only alarms without a title can be overwritten and a title should be set through this API to retain the ability to dismiss it without dismissing all alarms of the user
- Dismissing all alarms regardless of the title is still possible
- The title is exposed in the UI not just when the device supports it, but also when this API is enabled in the developer settings of the device itself

**Example usage:**

- https://gitlab.com/martin-braun/warpclock-plus/-/blob/feat/gb/app/src/main/java/com/antonok/warpclock/AlarmIntentService.kt demonstrates dismissing all alarms and setting multiple alarms (search for `sendBroadcast`)
- `DeviceAlarmReceiverTest` should cover all cases for a more complete overview of the capabilities of the API

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6088
2026-05-21 19:57:07 +02:00
José Rebelo 3c848e6ac0 Moondrop Space Travel 2 Ultra: Initial support 2026-05-19 20:26:16 +01:00
José Rebelo a6a1188a11 Huawei Watch Fit 5 / 5 Pro: Initial support 2026-05-18 22:10:12 +01:00
José Rebelo 61f4396805 Garmin GPSMAP H1: Initial support 2026-05-18 21:49:00 +01:00
José Rebelo 6961ff77db Sinilink: Initial support
Developed and tested against the XY-AP50L
2026-05-17 09:13:29 +01:00
José Rebelo 37ad178dac Redmi Buds 6: Initial support 2026-05-16 17:54:51 +01:00
José Rebelo 22859347f8 Redmi Watch Move: Initial support 2026-05-15 18:23:24 +01:00
Damian PietrasandJosé Rebelo a95bfa1cb0 Fill polish transliteration for missing characters. Before that only one was handled. 2026-05-10 17:30:59 +02:00
Thomas Kuehne 0ccbda5e62 Garmin: add test for fallback sport with subsport=0 2026-05-08 09:38:15 +00:00
Thomas Kuehne d45bcd4db3 Garmin: add basic Send Waypoint activity
- available in device specific settings for supported gadgets
- supports pasting common text formats (e.g. 27°59′18″N 86°55′31″E)
- supports geo: URIs (e.g. geo:27.988333,86.925278,8848)
2026-05-03 16:11:55 +00:00
José Rebelo c40d3e63a8 CMF Buds 2a: Add equalizer and ultra bass 2026-05-03 16:44:35 +01:00
José Rebelo 666252de1a Huawei Watch Ultimate 2: Initial support 2026-05-01 15:43:08 +01:00
Thomas Kuehne c926cadbb8 GPX: export track type 2026-04-30 18:24:01 +00:00
José Rebelo 19660a69ab CMF Buds 2a: Initial support 2026-04-26 10:00:38 +01:00
duhowandJosé Rebelo 0e85d47cc8 Sony: WH-1000XM3 add support for LDAC and NC/AMBIENT button switch (#5990)
Implements missing features from [WH-1000XM3](https://gadgetbridge.org/gadgets/headphones/sony/#device__sony_wh-1000xm3):

- Sound Quality Mode (SBC Codec / HD codecs)
    - Added as a switch (boolean) in UI
    - Note that LDAC option can appear in Andoid Bluetooth Device settings as well as unselected. Some Android devices (eg. Samsung) will also allow changing the BT Codec in Developer Settings, but if this is not enabled, the change won't apply.
- Function of NC/AMBIENT button
    - Allows using either Google Assistant or Alexa Assistant.
    - This model does not support "system assistant", used for other non-Google/Alexa assistants.

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/5990
Co-authored-by: duhow <contacto@duhowpi.net>
Co-committed-by: duhow <contacto@duhowpi.net>
2026-04-19 17:17:16 +02:00
José Rebelo cede254fd8 Huawei Band 11 Pro: Initial support 2026-04-10 23:28:15 +01:00
Thomas KuehneandJosé Rebelo 536ad17c4d Garmin HRM 600: Initial support
Based on feedback in #5633
2026-04-11 00:03:36 +02:00
Thomas Kuehne 9edeec2a57 Garmin: fix start time handling for stopwatch (#4507) and prepare for multi sessions in one file (#5862. #5860)
Prioritise the session specific start time over the file creation time.
2026-04-10 17:33:39 +00:00
Thomas KuehneandJosé Rebelo 6fcb07ea98 GPX: add support for importing older GPX version 1.0 files
Also takes care of slightly quirky GPX 1.1 files that contain extra spaces, XML comments or use floats instead of integers for cadence and heart rate.
2026-03-25 23:17:42 +01:00
José Rebelo 5b161d7e5e Huawei: Fix tests 2026-03-25 17:22:40 +00:00
Gideon ZenzandJosé Rebelo 539031b4b1 SleepAnalysis: Fix awake duration overflow when gap is not bridged
Only commit gap time to awakeSleepDuration when sleep actually resumes
(bridge confirmed), instead of speculatively counting all non-sleep
time as awake. Fixes total duration exceeding session span.
2026-03-24 18:58:24 +01:00
Daniele Gobbetti cf244cd1b4 Garmin: use COORDINATE base field for Weather coordinate fields (untested)
Use the standard field encoding for coordinates. Untested as my watch does not show the contents of the coordinate fields, but verified that they used to be invalid in the test cases and are now properly populated.

Also update test cases to match.
2026-03-22 13:26:14 +01:00
Daniele Gobbetti 93df9e94e6 Garmin: re-enable FitWeather tests and make the Weather Message reproducible
Change the order of the calls to the various setters in GarminSupport to reproduce the former fields' ordering in definition and data messages.
Update the tests in FitWeather to incorporate the new logic for local messages.
Add a new test that uses the legacy "setFieldByName" methods to ensure the serialization is equal.
2026-03-22 09:55:02 +01:00