Services¶
The services in battletime/services/ keep the watch's state and speak its protocols: files in the
external flash, settings, the clock, the step and health logs, installed apps, updates, and what
the phone sends. Each is a no_std crate with no Bluetooth and no hardware in it, so its tests run
on a computer, most of them against the flash store in RAM. The firmware carries the
bytes between them and the radio, the screen and the flash.
Storage¶
Flash store (services/flash_store)¶
battletime-flash-store keeps the watch's files, records and logs in the 3 MB FILE_SYSTEM region of
the external flash, async and with no heap. It knows how data is written, not what it is, so a new
kind of data needs no new format:
- Files are named by path and written whole, at once or a part at a time as an upload arrives. Each is a list of up to 64 sectors in any order, committed by a header written last. Files of 1 KB or less are kept as records. There are no folders; a path's slashes only name it.
- Records are small values rewritten often, keyed by their owner's app number, appended to a ring with a table in RAM of each key's newest copy. Writes are refused before the ring is half full, and each app may use 4 KB.
- Logs are appended over time, batched in RAM, and drop their oldest sector when the ring wraps.
FileAccessis the trait the services reach files through, so they need not name the store's sizes;ram_flash(a feature) gives their tests a flash in RAM. Itsfor_each_file_with_lengthwalks every file with its length, which folder listings are built from. The store'sfile_extentssays where a part of a file lies, for a DMA read.
It survives a power cut at any write or erase, which its tests check exhaustively. Built for the watch it takes 2,700 B of RAM and about 17 KB of code. flash-store.md describes the format, storage.md what the watch keeps in it, and firmware.md how the firmware reaches it.
Slot logs (services/slot_log)¶
battletime-slot-log keeps days of six-minute slots in the flash store, in blocks of whole days: blank
files of one sector, /day_blocks/<log>/<n>, block n starting n blocks of days after
1970-01-01. A slot is programmed into its place when it ends, with program_file, so it needs no
erase and a block costs one erase in all. Each byte is stored flipped by the log's mask, so a slot
never written reads as the log's empty value, and a slot keeps the first value written to it. A
block is deleted when a new day starts once all its days are older than DAYS_KEPT (90). Older
firmware kept each day as a file of its own, <folder>/<year>/<month>-<day>; those are read beside
the blocks and deleted once older than 90 days. The step log keeps its days here too, and two more
logs live here whole:
- Heart rate,
/logs/heart: one byte a slot, the rounded average of the beats per minute the sensor measured in it (Average). A slot without a reading holds 0. - Battery,
/logs/battery: one byte a slot, the charge in percent at the slot's end, with the top bit set while charging. A slot the watch was off for holds0xff.
A block of 3,872 bytes holds 16 days of these, or 8 of steps. SlotClock says when a slot has
ended; the firmware's battery task holds it, as firmware.md describes.
Settings and time¶
Settings (services/settings)¶
battletime-settings defines the watch-wide settings: the 12 or 24 hour clock, units, the step goal,
the brightness, the screen timeout, the always-on screen, chimes, do not disturb and sleep mode,
whether alarms ring in do not disturb, which touch wakes the screen, raise to wake, lower to sleep,
shake to wake and its threshold, Bluetooth, whether updates are taken, how often the heart rate is
measured in the background, the watch face, and whether the watch advertises under InfiniTime's
name for Gadgetbridge.
- One place for limits. Each
Settinghas a fixed number, the name apps pass toset_setting, and its lowest, highest and default values.Settings::setrefuses a value outside them, so nothing that reaches the store, from an app or from the phone, can be out of range. - The file.
/settings/systemholds a format byte, then one 5-byte record per setting: its number and its value as a little-endiani32. A record for an unknown setting or with a bad value is skipped, so a file from older or newer firmware still reads. - What a restart resets. Bluetooth is written but never read back
(
Setting::survives_restart), so a restart always turns the radio on: every app, the Settings app included, is installed over Bluetooth, so a watch saved with it off could never be reached again. Updates allowed only until the next restart read back as off. - Apps.
battletime-app-host'ssettingsmodule maps each setting to the system value apps read, such assettings.chimes, and hands the open app asettings_changeevent for each change.
The theme is kept apart, as eight colours in /settings/theme, since the companion app can set any
colours; the Settings app and settings.theme pick and read it by preset number.
Current Time (services/current_time)¶
battletime-current-time holds the wall clock's logic and turns its milliseconds since 1970 into the
Bluetooth Current Time service's two values and back, so the time handling is tested on a computer.
WallClockkeeps UTC as an offset from the timer's uptime, which the caller passes in, plus aLocalTimeInformationzone; it gives UTC and local time, and sets either, keeping UTC or the local time when the zone changes, as companion-protocol.md describes.WallClock::set_by_handsets the local time the Settings app chose, given as a year, a day of it from 1 and a minute of that day, throughlocal_time_from_day_and_minute, which refuses a day or minute that does not exist.parse_time_linereads Gadgetbridge'ssetTime(...)andE.setTimeZone(...)lines.encode_current_timegives the 10 bytes of 0x2A2B: the date, the time, the day of the week from Monday as 1, the 256ths of a second and an adjust reason of 0.decode_current_timereads a write of at least 7 bytes and refuses a year outside 1970 to 2200 or a date or time that does not exist.LocalTimeInformationholds 0x2A0F's time zone and daylight saving offset in quarter hours and refuses values the field does not allow, while taking its "unknown" values, which count as 0 inoffset_in_milliseconds.
The phone¶
Companion messages (services/companion)¶
battletime-companion reads the companion app's messages and writes the watch's replies, as
companion-protocol.md sets out: newline-ended JSON lines, GB({...}) as
Gadgetbridge sends them or bare. LineAssembler joins writes into lines and drops one longer than
640 bytes; parse_line reads a message with serde-json-core, without allocating, and rounds
fractions as they arrive so a fractional temperature still reads.
Message t |
Read as |
|---|---|
settings |
A preset theme, single colours and any watch-wide setting, each checked against its limits; with none, a request for the current settings, answered by settings_report. |
notify |
A notification's id, app, title and body, unescaped and cut at a whole character to 24, 32 and 120 bytes. |
notify- |
The notification with this id was dismissed on the phone. |
call, musicinfo, musicstate |
A call starting, answered or ended; the player's track; whether it plays and where. |
weather |
Kelvin turned into Celsius, OpenWeatherMap codes into the sky names apps use. |
nav |
The next step of the phone's navigation, or its end. |
find, vibrate |
Ring the watch until stopped; buzz a number of times. Their one value, n, is read by hand, since a JSON parser for one field cost about 850 B each. |
status |
The phone asks for the battery and steps. |
slots |
Where the update and recovery slots are, or a request to move them (slots.rs). |
state |
The phone rewrote an app's saved state, so the app takes it up. |
Any other message is named by its t. phone.rs gives each of Gadgetbridge's messages its own
small struct, so its parse never adds to the others' stack. PhoneUpdates keeps the latest of each
kind until the shell takes them, the larger ones on the heap, and PhoneData keeps the music,
weather and navigation and answers the system values apps read them through. The crate also writes
the status reply and the music, call, findPhone and request lines an app's effects send.
infinitime.rs reads writes to InfiniTime's own GATT services in their byte formats into the same
messages: Simple Weather into Weather (with sunrise and sunset) and Forecast, and, with the
single-values feature, the music and navigation characteristics into one PhoneField each, which
PhoneUpdates keeps in order and PhoneData applies to the music or navigation it holds. It also
gives the music event for an app's control and the motion service's bytes. The firmware turns the
feature on only with its music or navigation service, so a build without them carries none of it.
It holds the standard services' content too, so the firmware only serves the bytes:
advertising: the advertisement InfiniTime sends (flags, the Heart Rate and legacy DFU services, 0 dBm), the scan response with the name,GADGETBRIDGE_NAME("InfiniTime"), and the intervals: fast, 20 to 30 ms, for 30 seconds after a start, a disconnect or Bluetooth coming back on, then about once a second.alert: a New Alert, written to the watch the way InfiniTime reads it or notified by a phone in the standard's layout, turned into a notification, or a call for the incoming call category.device_information: the Device Information strings, the same set InfiniTime serves, and the serial number from the chip's factory id.heart_rate_measurement: the Heart Rate service's value, a zero flags byte and the beats per minute in one byte.
The phone's services (services/phone_client)¶
battletime-phone-client is the watch's GATT client for the phone's own Current Time and Alert
Notification services, which InfiniTime's ServiceDiscovery, CurrentTimeClient and
AlertNotificationClient use. PhoneClient is a state machine with no I/O: next takes the answer
to the last request and gives any value it read and the next request, so the firmware only carries
bytes and every step is tested on a computer against a pretend phone.
It finds each service with Find By Type Value, reads 0x2A2B and 0x2A0F with Read By Type, reads the
Alert Notification service's declarations a page at a time for New Alert and the control point,
finds New Alert's CCCD with Find Information, then writes the CCCD and the control point for every
category. An answer cut short at the end of the firmware's buffer is asked for again from the last
whole entry, and any other answer than the one expected ends that step. A phone that serves neither
service is left alone. firmware/src/phone_services.rs drives it; see
firmware.md and
bluetooth.md.
Notifications (services/notifications)¶
battletime-notifications keeps the notifications the watch holds, newest first: the latest 5 from
the phone and from apps, as InfiniTime keeps. Each one's text waits on the heap, about 200 B each,
so an empty list costs almost nothing, and a notification with the id of one already kept replaces
it, since the phone sends an update that way. Apps' own notifications take ids counting down from
-1, so none takes a phone's.
NotificationList answers the notifications and unread_notifications system values, and
carries out apps' notify, dismiss_notification and mark_notifications_read. It also gives
each notification an icon and a colour from the app that sent it: a call in the theme's success
colour, a missed call in danger, an email in warning, texts, chats and voicemail in a fixed blue,
calendar events in a fixed purple and the rest in the accent.
Health and power¶
Steps (services/steps)¶
battletime-steps keeps the step history the firmware serves to apps as steps_today and
step_history, and its log in /steps.
- Readings.
StepHistory::recordtakes the chip's count and the local time. It adds what the count gained since the last reading, or the whole count when it is lower, which means the chip started again, to the six-minute slot being recorded, to that slot's hour and to the trip count. Steps found at a reading belong to the slot of the reading before. - Days. It remembers today and the six days before as 24 hourly counts each, 336 bytes. At
local midnight each day moves one back. A clock set to another day, such as by the phone after
a restart, forgets them, and
log::storereads them again from the log. - The log. When a slot ends with steps,
log::storeprograms them into that slot's place in the day's block,/day_blocks/steps/<n>, through the slot logs; a day older firmware kept as a file,/steps/<year>/<month>-<day>, is read beside its block. The trip count stays in RAM and is saved in/steps/tripwhen a day ends and when the screen goes dark, and only when it changed, throughlog::save_trip_if_changed. Each slot is a little-endianu16, as wasp-os's step logger writes them, so a full day is 480 bytes and a block holds 8 days. Slots without steps are never written, so a quiet night costs no flash writes. When a new day starts,log::forget_old_daysdeletes the blocks and files whose days are all older than 90, taken from the slot logs. - Start.
log::load_historyreads the remembered days and the trip count back. The chip starts its count from 0 at every start, so a restart loses only the slot in progress, and the trip steps taken since the screen last went dark.
Midnight is local midnight, from the clock's time zone.
Battery (services/battery)¶
battletime-battery turns each battery reading into the charge the watch shows. Battery::measure
moves a smoothed voltage a quarter of the way to each reading, reads the charge along InfiniTime's
measured curve, lets the charge only rise on the charger and only fall off it, caps it at 99 % while
charging, and calls the battery full, at 100 % and not charging, once two readings in a row find
power with the charge pin idle, until the charger is taken off.
Each reading also gives its events: the charger plugged in or taken off, the battery full, and the
charge below 10 % off the charger, once until the charger is next connected. wakes_screen says
which of them light the screen, given do not disturb.
For the bootloader, its updater and the reloader it turns a reading from
battletime-battery-reading into the status
page's battery line, and allows a bootloader write only with power present and at least 3.7 V.
Apps and updates¶
Packages (services/packages)¶
battletime-packages is the package manager for the apps and watch faces on the external flash. Each
app has a folder, /apps/<id>/, with app.ntapp, its compiled image, and state, what it saved.
The folders are the whole record: there is no list of installed apps to fall out of step with them.
Packagesworks on the mounted file system.installchecks an image already written to the app's folder with the sameAppImage::parseandverifythe app host uses, and refuses the shared library, an image whose id is not the folder's name, and one whose stored facts differ from a fresh analysis; a bad image is left where it is, turned off, so the phone can send it again.removedeletes the folder with the saved state.set_enabledhides an app from the launcher with adisabledmarker, without deleting it;installandchange_enabledturn an app on or off only oncechecksallows it.listreturns every app whose image header holds its folder's id, in id order, with the heap it needs from the image's stored facts, reading only the header, the section table, the facts and three strings throughimage_reader;enabledgives the ones the launcher shows.read_iconandread_nameread an app's icon or name alone, for the launcher, andruns_while_closedreads only its subscriptions.read_imagehands the shell an image to load into RAM;read_image_inreads one from/temp/<id>/as well, for a temporary app, which no listing sees. An image larger than the heap an app may have is never read:listgives it by its id, turned off, andread_imagerefuses it.
facesmakes a face'swatch_facenumber from its id and picks the face to show, or the one a step on or back reaches.checksdecides whether an app may be turned on or off, from the list alone. Turning one on is refused when its need, with the shell's index of app ids (and its full list of apps for an app that reads it), is overAPP_HEAP_BUDGET_BYTES, or when it would push a launcher (an app with anAppListor readinginstalled_apps) over, since a launcher holds each listed app's name and icon and reads their images one at a time. Turning one off is refused for the launcher and the last face on. Other apps are not checked again when one is turned on, since only a launcher's need grows with it.commandsanswers the packages characteristic's JSON lines (apps,install,remove,enable,launcher, andrunandstopfor a temporary app) and says what changed, so the shell can read the apps again.launcherreads/settings/launcher, the bytes in which the companion says how the launcher looks and where every app goes, andarrangeputs the listed apps in group order, any the file leaves out in a last group, Other. The shell keeps the file, the order and each group's run of it, which apps read aslauncher_groups, and reads the names in place. It walks the bytes and the apps rather than parsing text and sorting, which keeps it near 1 KB of flash.libraryfinds the shared library inside a firmware image, where its image header says (firmware_library), and reads the version it records. The library ships with the firmware and is never written on the watch.
Updates (services/updates)¶
battletime-updates is the on-watch half of a firmware update, between the Bluetooth layer that
uploads an image and the bootloader that installs it. It knows nothing of the upload protocol:
UpdateReceivertakes a firmware image withbegin(size), thenreceive(chunk)in order, thenfinish(). It writes through embassy-boot'sFirmwareUpdaterinto the update slot, erasing each sector just before writing it.finishchecks that every byte arrived, that the image's header is sound and linked forFIRMWARE, and that its CRC-32 matches, then marks a swap for the next reset.read_backlets the protocol above check its own checksum first.confirm_running_firmwaretells the bootloader the running firmware works. Until it is called the next reset rolls back, and no new update is accepted.RecoveryReceiverwrites the recovery firmware into its own slot the same way. It erases the start of the slot first and writes the header's magic last, so the bootloader never sees a half-written recovery firmware.change_slotsmoves the update and recovery slots by writing a slot table record, refused while a firmware is on trial. The companion'sslotsmessage reaches it.
Firmware updates over Bluetooth (services/dfu)¶
battletime-dfu speaks Nordic's legacy DFU, the protocol InfiniTime uses, so Gadgetbridge, ITD, Siglo
and BattleTime's tools update BattleTime without changes. LegacyDfu is the state machine, with no
Bluetooth in it: control point commands and packet writes go in, notifications and a reset request
come out, and the bytes go to battletime-updates. activity reports the step it is on and
Outcome::failure why a step was refused, for the status page. Its 256-byte write buffer and
64-byte init packet are on the heap only while an update runs; a heap without room for them
answers START_DFU with OPERATION_FAILED.
An upload cut off by a lost link is kept. The same image sent again (the same type, size and init
packet) carries on from where it stopped for a phone that asks with REPORT_RECEIVED_IMAGE_SIZE
before the image; a phone that does not ask starts from the first byte. The trouble feature adds
the GATT service for TrouBLE, and the tests run whole updates through it. See
updates.md.
Files over Bluetooth (services/blefs)¶
battletime-blefs speaks Adafruit's BLE file transfer protocol, InfiniTime's FSService, so ITD,
Gadgetbridge and the companion app reach the watch's files. FileTransfer turns one command into
its answers against the flash store's FileAccess; it remembers the file being read or written
between commands.
- Folders. The store has no folders, so a folder is the files whose paths start with it: making one always succeeds, and listing, deleting or moving one acts on what it holds.
- Uploads. Uploads of 1 KB or less are gathered in RAM and written whole, so they become records; larger ones stream into the store and replace the old file only when the last piece arrives, and one cut short resumes only while the store still holds it open. A move copies, then deletes.
- Locked folders. While updates are off the firmware locks
/appsand/temp, so any write, delete, new folder or move that touches them, or the root above them, is refused, while reads and other folders work as before.
The trouble feature adds the GATT service. See
companion-protocol.md.
Keeping the watch healthy¶
Crash record (services/crash_record)¶
battletime-crash-record says why the watch last restarted: the chip's RESETREAS bits in words
(RestartReason), and a record of the last panic, out-of-memory or HardFault. The record is 60
bytes of little-endian words in RAM that a reset keeps: CRSH, the kind, eight detail words, 16
bytes of the open app's id, and a CRC-32 of the rest, so RAM that came up with noise never reads as
a crash. last_restart_words gives the words the diagnostics characteristic carries. screen lays
out and draws the crash page and the page shown when an app needs more memory than is free, in the
status page's font, so they need nothing from the shared library. How the firmware fills the record
is in firmware.md.
Liveness (services/liveness)¶
battletime-liveness judges whether the shell and the radio still make progress, so the watchdog is
fed only while they do. The watchdog task checks twice a second with each part's counter: the
shell's loop count, and the radio's interrupt count while it advertises or holds a connection. A
quiet shell is asked to prove it still runs (ping_shell). A shell stopped for six checks, or a
radio with work stopped for twelve, is judged stopped. Checks are counted rather than timed, so a
long blocking job that also stops the watchdog task never counts against a part.
Foreground (services/foreground)¶
battletime-foreground says whether the wearer is watching the screen move, so background work that
blocks every task waits for a pause. may_start holds work back while the shell is part way
through a transition or a sliced redraw, or a finger is down, but never longer than a second, so a
screen kept busy cannot starve it. The firmware's use of it is in
firmware.md.
Task table (services/task_table)¶
battletime-task-table keeps the firmware's tasks, with how often each has run and how long it has
spent running, for the About app's Tasks page. The firmware feeds it from embassy's trace hooks: a
task is added when it is spawned, timed each time the executor polls it, and removed when it ends. A
task names itself on its first run, since the hooks know a task only by its address.