Add System app (clock, battery, adjustable timezone)#97
Conversation
A small status/settings screen for the badge: - Local time + date, synced over NTP and written to the battery-backed PCF85063A RTC so the clock survives a power cycle even without WiFi. - Battery level and charging / on-battery state. - UP / DOWN adjust the timezone offset (UTC-12 .. +14) live; the choice is saved automatically via the State store (/state/system.json) and restored on the next boot. The RTCs always hold UTC; the offset is display-only. - A re-syncs the time over NTP.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new “System” badge app that displays clock + battery status, supports NTP time sync over WiFi, and persists a user-selected timezone offset across reboots.
Changes:
- Introduces the System app UI (clock, battery indicator, status line) and button controls (sync + timezone adjust).
- Adds WiFi + NTP time sync logic and optional hardware RTC (PCF85063A) read/write support.
- Persists timezone offset using
badgeware.Stateand documents usage/controls.
Show a summary per file
| File | Description |
|---|---|
| badge/apps/system/init.py | Implements the System app runtime: UI rendering, NTP sync, RTC integration, timezone persistence. |
| badge/apps/system/README.md | Documents features and controls for the new System app. |
Copilot's findings
- Files reviewed: 2/3 changed files
- Comments generated: 7
| def init(): | ||
| # Restore the saved timezone, then show the battery-backed time straight | ||
| # away, before networking. | ||
| load_config() | ||
| restore_from_hw_rtc() |
| if __name__ == "__main__": | ||
| run(update) |
| try: | ||
| sys.path.insert(0, "/") | ||
| from secrets import WIFI_SSID as S, WIFI_PASSWORD as P | ||
| sys.path.pop(0) | ||
| WIFI_SSID, WIFI_PASSWORD = S, P | ||
| except ImportError: | ||
| WIFI_SSID, WIFI_PASSWORD = None, None | ||
| return WIFI_SSID is not None |
| import sys | ||
| import os | ||
|
|
||
| sys.path.insert(0, "/system/apps/system") | ||
| os.chdir("/system/apps/system") |
There was a problem hiding this comment.
This matches the established convention in this repo — the badge and copilot-loop apps open with the same two lines. MonaOS apps are launched directly by main.py rather than imported as libraries, and these paths need to be set before the module-level badgeware/font loading that follows, so moving them into init() would make this app boot differently from every other app on the device. Keeping it as-is for consistency; happy to follow if the convention changes repo-wide.
| def load_config(): | ||
| """Restore persisted settings. A missing file just leaves the defaults.""" | ||
| try: | ||
| State.load("system", config) | ||
| except Exception: | ||
| pass | ||
|
|
||
|
|
||
| def save_config(): | ||
| """Persist settings so the timezone survives a reboot.""" | ||
| try: | ||
| State.save("system", config) | ||
| except Exception: | ||
| pass |
| time_valid = False # do we have a real time to display? | ||
| ntp_synced = False # has an NTP sync completed at least once? | ||
| source = "--" # where the shown time came from: "RTC" or "NTP" |
| | Button | Action | | ||
| |----------|----------------------------| | ||
| | **UP** | Timezone +1 hour | | ||
| | **DOWN** | Timezone −1 hour | | ||
| | **A** | Re-sync the time over NTP | |
There was a problem hiding this comment.
Checked the raw bytes (od -c) — every row starts with a single |, there's no doubled pipe. The only non-ASCII character is the typographic minus (U+2212) in "Timezone −1 hour", which GFM renders fine, and the table displays correctly in the Files view. Looks like a false positive, so leaving the README unchanged.
|
checking. |
Copilot review: - Call init() via run(update, init=init) so the saved timezone and hardware-RTC restore actually run at startup - Restore sys.path with try/finally in load_credentials() - Build the "via ..." status line from `source` instead of duplicating the logic (removes dead state) Code review: - Back off 30s after a failed NTP sync instead of re-running the blocking settime() every frame (button A still forces a retry); tick-wraparound safe - Re-issue wlan.connect() after 30s without an IP so an AP that was down at boot doesn't strand the badge on "Connecting..." - Clamp/validate tz_offset loaded from the state file so a corrupt /state/system.json can't crash the app at boot - Check State.save()'s return value (it reports failure as False, not an exception) and surface "save err" on screen, including pre-sync - Catch all exceptions from the secrets.py import so a botched credentials file degrades to "No WiFi config" instead of crashing - Treat the host clock as valid in the desktop simulator (source "SIM") so the sim demos the clock
Adds a small System app — a status + settings screen.
What it does
Controls
The timezone offset (UTC−12 … UTC+14) is changed live with UP / DOWN and saved automatically via the badge
Statestore (/state/system.json), then restored on the next boot. The RTCs always hold UTC; the offset is applied only for display.Files
badge/apps/system/__init__.pybadge/apps/system/icon.png— 24×24 gear iconbadge/apps/system/README.mdTesting
Verified in the simulator: live UP/DOWN timezone adjustment, persistence across a relaunch (
Statereload), and both the synced-clock and the pre-sync ("waiting for time") layouts render without overlap. The gear icon is also running on a physical RP2350 badge.