Implement onSetContacts for the DM58 (Pro dialect, cmd 0xc0): START (ab06), the contact list in field ab07 as [numberLen | number UTF-16BE | nameLen | name UTF-16BE] entries with a 2-byte offset + xor terminator, then END (ab09). Coordinator advertises getContactsSlotCount so the contacts screen appears. Verified on hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Map drizzle and light rain to icon code 3 (cloud + sun + rain) so they are visually distinct from plain rain (code 7); the watch has no lighter plain-rain icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catalogued the DM58 weather icon codes 0-31 on hardware (34+ are placeholder/unrelated resources) and rewrote the OpenWeatherMap mapping to use the real set: distinct rain/heavy-rain/extreme-rain, showers, snow/heavy-snow/snowstorm, sleet, fog and thunderstorm/hail icons, instead of the earlier best-effort 0-8 guess.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Previously only the current weather (e0 chunk0) was sent, so the watch's
multi-day view stayed stuck on whatever the official app last synced. Add the
forecast as a second, chunked e0 message:
- "ab0b" update timestamp, then a run of hourly entries (ab0d = ts, condition,
temp) and daily entries (ab0e = high/low, condition, day midnight, sunrise,
sunset, moon, humidity). Today comes first (todayAsDaily()), then the per-day
forecasts; up to 7 days and 24 hours.
- Split into <=239-byte chunks "01 e0 abab <idx> ..." ending with
"01 e0 abab fd <xor>", where the xor covers every transmitted byte (verified
against the official app's framing).
- Map OpenWeatherMap condition codes to the watch's icon codes, calibrated on
hardware: 0 clear, 1 partly cloudy, 2 cloudy, 3 light rain, 4 thunderstorm,
5 hail, 6 snow, 7 rain, 8 heavy rain. Also applied to the current-weather
condition byte (was left at 0).
Verified on DM58 hardware: 7 days populated with correct temps/weekdays and
matching icons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Play/pause icon: the watch reads the state from field03 of the music-info
frame (0x03 -> pause icon = playing, 0x02 -> play icon = paused). The values
were inverted, so the icon was always wrong; swap them.
- Populate the music screen on connect by pushing the currently-playing track
via MediaManager.refresh(), instead of leaving it empty until the next event.
- Volume bar: send the phone's real media-volume scale (field04 = max steps,
field05 = current step) so the bar matches the phone and moves one segment per
volume key press. Re-send the track on volume changes. Drops the bogus
standalone volume frame (the official app never sends one).
Verified on DM58 hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read the watch's alarms on connect (01 c7 aaaa) and update Gadgetbridge's alarm
DB from the reply so alarms edited on the watch show up in the app. The reply
puts a 0x0f separator before each alarm block ("aa 01/02/03/04" =
index/weekday-mask/enabled/HH:MM); parse all blocks. Update the DB only, without
re-sending to the watch, so this never overwrites alarms just changed on the watch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds four features to the GloryFit Pro driver, reverse-engineered by capturing
the official app's BLE traffic:
- Find device (onFindDevice): 01 a5 ab 04 01 <01/00> to ring/vibrate the watch.
- Find phone: enable on init (01 a5 ab 0a 01 01); the watch pushes
01 a5 ac 02 01 <01/00> -> GBDeviceEventFindPhone.
- Alarms (onSetAlarms, 5 slots): 01 c7 abab 00 <per-slot TLV> + fd/xor
(weekday bitmask bit0=Sun..bit6=Sat). Verified on hardware (08:30 Sat/Sun).
- Weather (onSendWeather): current conditions as 01 e0 abab 00 <TLV> + fd/xor
(city, temp, high/low, UV, humidity, update time). Forecast chunks and the
condition-code mapping are TODO.
Coordinator advertises supportsFindDevice, getAlarmSlotCount=5, supportsWeather.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Handle the watch's music remote (0xe2 events) as media control:
play / pause / previous / next / volume up / down. Also send the now-playing
artist and title ("01 e2 abab ...") and the phone volume ("01 e2 ab 01 01 <vol>")
to the watch.
Verified on hardware: the buttons control phone playback and the watch shows the
current track. Keeping the play/pause icon in sync and a live volume bar would
require streaming updates ~2x/second (like the official app), which destabilised
the BLE connection here, so that is left as a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read the watch's day summary (c3, "01 c3 aaaa") on connect and on manual sync,
parse today's step total from field 0x0c sub-TLV subfield 0x05 (variable-length
big-endian int), and store it as a delta relative to what was already recorded
today so Gadgetbridge's per-interval sum equals the watch's daily total. Enables
activity tracking (reusing GloryFitStepsSample + GloryFitActivitySampleProvider).
Verified on hardware: step count matches the watch display and updates live.
Note: this tracks today's total going forward; per-minute history backfill from
the c6 stream (incl. heart rate) is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a new device family `gloryfitpro` for smartwatches using the GloryFit Pro
BLE dialect (com.yc.gloryfitpro), with the DM58 (Actions ATS3085L, 1.43" AMOLED)
as the first supported model. This is the variant tracked in #5541 / #5840 (same
78:02:B7 MAC prefix) and #5505; it is not the classic GloryFit protocol, so it
lives in its own package.
Implemented and verified on real hardware (firmware AT328NNV002438):
- discovery + connect (advertises as DM58)
- firmware / model read-out
- battery level + charging state
- time sync (UTC unix timestamp + [0x80|hours][minutes] timezone field)
- notification forwarding (title + body, UTF-16BE)
Protocol notes: packets are framed 01|CMD|MODE|FIELD|LEN|VALUE, where MODE is
aa=get / ab=set / ac=report (doubled aaaa/abab = "all fields"), over the CMD
service 0x55ff (write 0x35f1, notify 0x35f2) and DATA service 0x56ff (write
0x34f1, notify 0x34f2), MTU 247. Multi-packet replies terminate with
01 CMD MODE fd <xor-of-data-packet>.
Steps/activity sync, health-monitoring toggles and other features are left as
follow-ups.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Initial support for MoYoung L70 this and includes fixes so hartrate data works
MoYoung also hads some issues with bad stress and blodoxygen values so filter them out.
Remove the unrelated indication API, client callback timeout, GATT service refresh, and diagnostic logging from the standalone review. Retain only server notification serialization and its bounded callback wait.
Ollee Watch One is a Nordic-UART (nRF) analog smartwatch. This adds a new
all-Kotlin `ollee` device family providing:
- time + location sync
- a single alarm (weekday repetition; one-shot mapped to its next-fire
weekday, as the watch has no true single-fire mode)
- world time (one zone, with the world-time face enable and offset register)
- notification count badge
- battery voltage
- activity step sync (on-watch record drain -> activity samples)
The device family follows the existing all-Kotlin coordinator/support
pattern: OlleeDeviceCoordinator (capabilities + discovery), a thin
OlleeDeviceSupport, and per-feature pure-logic files (frame codec, alarm
record builder, weekday/world-time register composer, faces table, activity
records codec), each covered by JVM unit tests.
The wire protocol was worked out clean-room from HCI snoop logs and BLE
probes of my own watch and companion watch faces; nothing here derives from
decompiled or proprietary sources.
Hardware-verified on an Ollee Watch One (firmware 01.05.0000.01.10):
connect/init, battery voltage, firmware version, time+location sync,
activity step sync, world time, and the alarm (set, acknowledged by the
watch, shown on the Alarm face, and physically ringing at the set time).
Adds home-clock timezone/DST sync and generic World Clocks UI support
for the Casio GBD-200 (module 3506), grounded in a BLE capture of the
official CASIO WATCHES app.
- Coordinator advertises 1 world-clock slot (wire carries 2: slot 0 is
the home clock), label length 18, and the World Clocks settings row.
- New CasioWorldClockCodec builds the 0x1d/0x1e/0x1f frames in the
app's order, with the 0x1d pair frame padded to the captured 15-byte
length; unit-tested against capture-derived fixtures.
- Casio2C2DSupport gains a shared writeClocks() helper that wraps clock
writes in the 0x21 settings-session bracket (21 00 01 open, 21 01 01 /
21 00 04 / 21 01 04 close). Hardware verification showed the GBD-200
silently discards unbracketed clock writes (GATT_SUCCESS, no effect)
and NACKs close frames without a matching open. The 0x1f city-name
frames are optional per device — groundwork for the WS-B1000, whose
protocol is identical except it never uses 0x1f.
- Time sync (onSetTime and watch-requested resync) also writes the home
clock from the phone timezone while preserving the watch's world time
city via read-back, so a city set on the watch survives syncs.
- onSetWorldClocks writes the first enabled world clock from the
generic UI; the watch WT display follows the written city name
(phone-computed city ID 00 00 is accepted).
Hardware-verified on a real GBD-200: on-watch WT survives sync; GB
World Clocks save appears in WT mode; phone TZ changes (Denver <->
Chicago) move the home clock both ways automatically.
A stored night whose start predates the 24h look-back window was re-detected
by SleepAnalysis as a clipped tail (only the samples inside the window). The
frozen-id registry correctly reused the record id and preserved the full span,
but the record was rebuilt with the fragment's stages, overwriting the good
multi-stage record with a single stage. clientRecordVersion = now always won
the HC conflict, so every sync regressed the night.
Two load-bearing causes, both required to fix#6453:
- buildSleepStages set the last stage end to +1ms. The registry persists the
span as epochSeconds, so the reloaded end never equalled the re-detected end.
This is the trigger: a clipped-tail re-detection whose last sample matches the
stored night would otherwise compare changed=false and be skipped, but the
+1ms end exceeded the truncated stored end -> changed=true -> the record was
rewritten with the truncated stage list. End on a whole second so the span
round-trips and the skip-unchanged guard holds.
- The SLEEP fetch window started at sliceStart - 24h regardless of stored
sessions, so any legitimate rewrite of an older night carried only the tail's
stages. Extend the window back to the start of any stored night overlapping
it, clamped to one extra look-back, so SleepAnalysis re-derives the full stage
list. Extracted as pure SleepSyncer.sleepQueryStart for unit testing.
Add stage-level regression tests: the clipped-tail overwrite (the exact #6453
trigger), the epochSecond round-trip, and the fetch-window extension incl.
edges (no rows, no overlap, inclusive boundary, floor clamp, earliest of many,
and a wiring test proving a wider fetch re-derives more stages).
Fixes: https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/6453