Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- name: Build and run the host tests
run: |
cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c -lm -o kg_test
cc -Wall -Wextra -Werror -I src tests/test_data.c src/data/kg_*.c src/third_party/tjpgd/tjpgd.c -lm -o kg_test
./kg_test tests/data/openmeteo-montreal.json

simulator:
Expand All @@ -40,7 +40,7 @@ jobs:
KG_TIME=10:09:30 KG_FACE=$face sim/build/kronograf_sim --shot shots/face$face.png
done
for app in launcher chrono timer alarms steps weather settings notifs world sky level torch remote system \
net net:wifi net:link net:ping net:trace net:dns net:ports net:lan net:speed net:ble net:cidr; do
radio video net net:wifi net:link net:ping net:trace net:dns net:ports net:lan net:speed net:ble net:cidr; do
KG_TIME=10:09:30 KG_APP=$app sim/build/kronograf_sim --shot shots/${app/:/-}.png
done
- uses: actions/upload-artifact@v4
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Release

# Pushing a tag such as v0.5.0 builds the firmware on GitHub — where include/secrets.h does
# not exist, so the binaries carry no Wi-Fi credentials or API key — and publishes it.
# A tag with a suffix (v0.7.0-beta.1, v0.7.0-rc.1) is published as a pre-release: never
# "Latest", for testing builds from a feature branch before they reach main.
on:
push:
tags: ["v*"]
Expand Down Expand Up @@ -40,7 +42,16 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
cat > notes.md <<EOF
PRE=""
case "$GITHUB_REF_NAME" in *-*) PRE="--prerelease" ;; esac
if [ -n "$PRE" ]; then
echo "> [!NOTE]" > notes.md
echo "> **Pre-release** for testing, built from a development branch: it may still contain bugs." >> notes.md
echo "" >> notes.md
else
: > notes.md
fi
cat >> notes.md <<EOF
Firmware for the **LilyGO T-Watch 2020 V1**, built by GitHub Actions from \`${GITHUB_SHA::7}\`.
These binaries contain no Wi-Fi credentials and no API key.

Expand All @@ -53,4 +64,4 @@ jobs:
**After flashing:** pick a network in Settings → Wi-Fi. For STM bus departures, add your API key in
Settings → STM buses, or over the console: \`tools/kg.py bus key <your key>\`.
EOF
gh release create "$GITHUB_REF_NAME" dist/* --title "Kronograf $GITHUB_REF_NAME" --notes-file notes.md
gh release create "$GITHUB_REF_NAME" dist/* --title "Kronograf $GITHUB_REF_NAME" --notes-file notes.md $PRE
8 changes: 8 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,11 @@ Each entry: decision — rationale. Most recent at the bottom.
**D55. Offline astronomy and time zones.** The world clock evaluates POSIX TZ rules itself (`kg_tz.c`) instead of switching libc's TZ, which would disturb the watch's own local time; it matches the C library on twelve zones every 37 minutes over two years. Sunrise, sunset and civil twilight use NOAA's sunrise equation (±1–2 min); the Moon's phase uses the elongation with Meeus' main periodic terms, within 20 minutes of the 2024 and 2025 eclipses. All at the home location of `kg_weather.h`.

**D56. Not yet verified on the watch** (built and checked in the simulator only, the watch being away): IR emission (the LED can be seen through a phone camera), accelerometer axes and signs in the Level app (`accel` console command), and the AXP202 charge-current reading (1 A shown while charging looks too high). **Verified on 2026-10-14** by OTA: boot of 0.6.0, ping (0 % loss), ARP sweep (23 devices), BLE scan (22 devices), speed test (D52), internal RAM 32 % free with Wi-Fi and BLE on.

## Stage 9 — 2026-10-14: radio and video, development workflow

**D57. Feature branches and pre-releases.** `main` only receives work that was tested on the watch. A feature lives on a branch with a pull request (CI builds and tests it); a tag with a suffix (`v0.7.0-beta.1`) makes `release.yml` publish a GitHub pre-release from that branch — still built by GitHub Actions, without secrets (D48), never marked "Latest". After validation the pull request is merged and a plain `vX.Y.Z` tag publishes the release. This stage is the first one to follow it (`feature/media`).

**D58. Web radio: MP3 over plain HTTP, decoded on the watch.** minimp3 (CC0, vendored) decodes in a task on core 0 (priority 3), fed by a 96 KB ring buffer in PSRAM that a network task (priority 2) fills; playback starts after 32 KB (~2 s) and rebuffers after an underrun. The decoder's 16 KB scratch and its buffers come from PSRAM for the session only (patch `MINIMP3_SCRATCH`, D40's internal-RAM budget). Output: mono mix, quadratic volume, I²S at the stream's rate to the MAX98357A; tones are dropped while the radio owns the speaker. ICY metadata gives the song title (UTF-8 or Latin-1). The radio keeps Wi-Fi on with the screen off (`net_keep`), at the cost of light sleep. Stations were checked to serve MP3 over plain HTTP — HTTPS would cost two 16 KB TLS buffers and HLS/AAC are out of reach; redirects are followed (Radio Swiss Classic has two). The same loops run in the simulator (`KG_NET_REAL=1`), through SDL audio: Radio Paradise, BBC World Service, KEXP, FIP and Radio Swiss Classic play without underruns. **Measured on the watch (beta.1 → beta.2):** plays without underruns, Bluetooth on, titles shown (accents included). Stack high-water marks led to a 7 KB network task (it peaked at 5.9 of 6 KB with DNS and redirects; its 1.5 KB staging buffer moved into the session, in PSRAM) and a 3.5 KB decoder task (1.5 KB used); 6 I²S DMA buffers. Internal RAM while playing: ~24 % free (57 KB, low-water 36 KB) — below the 30 % target, the rest going to the Wi-Fi / lwIP buffers of a continuous stream; back to 31 % once stopped. OTA during playback fails for lack of memory: stop the radio first.

**D59. Video: MJPEG only.** H.264 is beyond the ESP32 without a hardware decoder; MJPEG (or a JPEG snapshot URL, polled every second) is what small cameras serve. JPEG images are cut out of the byte stream by their SOI / EOI markers (multipart headers ignored), decoded by TJpgDec (vendored, RGB565, scale 1/1–1/8 so the image is at most 4/3 of the screen, then centred and cropped), into three 240 × 240 frames in PSRAM: the decoder never writes the frame shown or the one LVGL holds. The video only runs while its app is on screen, under the toolbox's Wi-Fi lease; the screen stays on for 10 minutes at most. A test source: `tools/mjpeg_test_server.py` (ffmpeg); 15 fps at 320 × 240 decoded in the simulator. **Measured on the watch:** 5–6 fps at 320 × 240 with Bluetooth on (~400 kbit/s, the network ceiling of D52), colours right, internal RAM 27 % free while playing. The test server is `tools/mjpeg_test_server.py` (multi-client; ffmpeg's built-in server only takes one client at a time and stayed stuck on a stale one).
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/storagebits/kronograf/actions/workflows/ci.yml/badge.svg)](https://github.com/storagebits/kronograf/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**A from-scratch firmware for the LilyGO T-Watch 2020**: an aviation chronograph face, "time circuits" 7-segment displays straight out of *Back to the Future*, weather and real-time bus departures, iPhone notifications, a network engineer's toolbox on the wrist (Wi-Fi analyzer, ping, traceroute, LAN scan, speed test…), a TV remote, a world clock and a sun & moon almanac. ESP32, LVGL 8.3, and a macOS simulator that builds exactly the same interface.
**A from-scratch firmware for the LilyGO T-Watch 2020**: an aviation chronograph face, "time circuits" 7-segment displays straight out of *Back to the Future*, weather and real-time bus departures, iPhone notifications, a network engineer's toolbox on the wrist (Wi-Fi analyzer, ping, traceroute, LAN scan, speed test…), web radio, an MJPEG camera viewer, a TV remote, a world clock and a sun & moon almanac. ESP32, LVGL 8.3, and a macOS simulator that builds exactly the same interface.

<p align="center">
<img src="docs/screens/kronograf-demo.gif" width="360" alt="Demo: aviator face, time circuits with the lightning easter egg, night face, launcher, weather, an iPhone notification, then the network toolbox and its Wi-Fi analyzer">
Expand All @@ -16,7 +16,8 @@
| | |
|---|---|
| **Faces** | *Aviator chronograph* (smooth hands, battery / minutes / steps sub-dials, date window, weather complication, notification badge) · *Time circuits* (three 7-segment rows: next alarm, present time, last sync) · *Night* (time only, dark red, dimmed backlight). Swipe sideways between faces. |
| **Apps** | Stopwatch, timer, alarms, world clock, weather + buses, sun & moon, 7-day step history, level, torch, network toolbox, TV remote, system monitor, settings — a launcher of 3 × 3 pages drawn like sub-dials, swiped sideways. |
| **Apps** | Stopwatch, timer, alarms, world clock, weather + buses, sun & moon, 7-day step history, level, torch, network toolbox, radio, video, TV remote, system monitor, settings — a launcher of 3 × 3 pages drawn like sub-dials, swiped sideways. |
| **Radio & video** | *Radio*: MP3 web radio on the speaker (Radio Paradise, BBC World Service, KEXP, FIP, Radio Swiss Classic, or any http:// MP3 stream), song titles from the stream, volume, keeps playing with the screen off · *Video*: an MJPEG camera (ESP32-CAM, IP camera, 3D printer) or a JPEG snapshot URL, full screen at ~10–15 fps. Decoded on the watch (minimp3, TJpgDec). |
| **Network toolbox** | *Wi-Fi analyzer* (2.4 GHz channel map, every network with channel, signal and security) · *Link* (SSID, BSSID, signal, address, gateway, DNS, MAC, public IP and provider) · *Ping* with an RTT history and loss · *Traceroute* (ICMP, TTL 1–24) · *DNS lookup* · *Port check* (16 common TCP services) · *LAN scan* (ARP sweep of the /24, private MACs spotted) · *Speed test* (10 s download) · *Bluetooth scan* (BLE devices and their makers) · *Subnet calculator* (offline). Wi-Fi only stays on while the toolbox is open. |
| **World & sky** | Twelve cities from Honolulu to Auckland with summer time, day / night and day shift, plus Zulu time · sunrise, sunset, civil twilight, day length and its daily change, the Moon's phase drawn to scale and the next full moon — computed offline. |
| **Hardware tools** | *TV remote* over the infrared LED (Samsung, LG, Sony, Philips; power, volume, channels, mute, input, and "all TVs") · *Level* from the accelerometer, zeroed on any surface · *Torch* in white or red at full backlight · *System*: battery voltage and current from the AXP202, its temperature, RAM and PSRAM, CPU, frame rate, uptime, reset cause. |
Expand Down Expand Up @@ -47,6 +48,8 @@ cmake -S sim -B sim/build -DCMAKE_BUILD_TYPE=Release && cmake --build sim/build
sim/build/kronograf_sim # ×3 window, the mouse is your finger
KG_TIME=10:09:30 sim/build/kronograf_sim --shot shot.png # headless PNG screenshot
KG_NET_REAL=1 KG_APP=net:trace sim/build/kronograf_sim # the toolbox, for real, from the Mac
KG_NET_REAL=1 KG_APP=radio sim/build/kronograf_sim # web radio through the Mac's speakers
tools/mjpeg_test_server.py # an MJPEG test stream for the Video app
tools/make_screens.sh # screenshots + the overview sheet
tools/make_demo_gif.sh # re-records the demo GIF (ffmpeg)
```
Expand Down Expand Up @@ -99,6 +102,7 @@ screenshot f LVGL capture → PNG lcdtest R | G | B test pat
battlog battery-life log power <min> current measured on USB, simulated battery
weather [refresh] bus [refresh | stops <codes> | key <key>] ble [on | off | forget]
net [<tool> [target]] network toolbox: ping, trace, dns, ports, lan, speed, public, ble
radio [play <n> | stop | vol <n> | url <u>] video [play | stop | url <u>]
sys system information accel accelerometer in g
ir [<brand> <key>] sends a TV code app net:<tool> opens a network tool
sleep | wake | bl <n> | timeout <s> | face <n> | app <name> | timer <s> | coredump | reboot
Expand All @@ -109,7 +113,8 @@ sleep | wake | bl <n> | timeout <s> | face <n> | app <name> | timer <s> | coredu
```
src/ui/ LVGL interface, identical on watch and simulator (faces, apps, theme, fonts)
src/data/ plain C, host-tested: Open-Meteo, GTFS-RT (streamed protobuf), ANCS, network probes and
toolbox runners (BSD sockets), time zones, sun and moon, infrared codes
toolbox runners (BSD sockets), time zones, sun and moon, infrared codes, radio and video
src/third_party/ minimp3 (CC0) and TJpgDec (ChaN), vendored — see its README
src/hal/hal.h the only hardware interface the UI sees
src/hal/device/ T-Watch: display (esp_lcd, DMA), AXP202, BMA423, network, BLE, safeguards
src/hal/sim/ simulated implementation
Expand Down Expand Up @@ -138,14 +143,26 @@ Development went in stages, each one flashed and verified on the watch.
- ✅ **5**: iPhone notifications.
- 🧪 **6**: infrared TV remote — built and simulated, to be checked on the watch.
- 🧪 **8**: network toolbox, world clock, sun & moon, level, torch, system monitor — built, simulated, network tools run for real from the simulator; on-watch check pending.
- 🧪 **9**: web radio and MJPEG video — on the `feature/media` branch, in pre-release.
- 🔜 **7**: polish and a battery-life campaign.

**Known limitations.** This board has no 32 kHz crystal, so light sleep is impossible while BLE is active: battery life with notifications is being measured (D41). The development unit's micro-USB connector is unreliable (B2), hence OTA updates and block flashing.

## Development workflow

`main` only receives tested work. New features go on a branch (`feature/…`) with a pull request: CI builds and tests it, and a **pre-release** can be cut from the branch for testing on a watch — tag it `vX.Y.Z-beta.N` and GitHub Actions publishes it as a pre-release, never as "Latest". Once validated, the pull request is merged into `main` and a plain `vX.Y.Z` tag publishes the release.

```sh
git switch -c feature/my-idea # work, commit
git push -u origin feature/my-idea # then open a pull request: CI runs on it
git tag -a v0.7.0-beta.1 -m "…" && git push origin v0.7.0-beta.1 # pre-release for testing
```

## Licence and credits

Kronograf's code is released under the [MIT licence](LICENSE). Dependencies and assets:

- [minimp3](https://github.com/lieff/minimp3) (CC0) and [TJpgDec](http://elm-chan.org/fsw/tjpgd/) (ChaN's free licence), in `src/third_party/`.
- [LVGL](https://lvgl.io) 8.3 (MIT), [NimBLE-Arduino](https://github.com/h2zero/NimBLE-Arduino) (Apache 2.0), [XPowersLib](https://github.com/lewisxhe/XPowersLib) and [SensorLib](https://github.com/lewisxhe/SensorLib) (MIT), [stb_image_write](https://github.com/nothings/stb) (public domain / MIT).
- [Barlow Condensed](https://fonts.google.com/specimen/Barlow+Condensed) and [Allerta Stencil](https://fonts.google.com/specimen/Allerta+Stencil) fonts, under the SIL Open Font License (`assets/fonts/OFL-*.txt`).
- [My-TTGO-Watch](https://github.com/sharandac/My-TTGO-Watch) and the LilyGO library served as references for the pinout and init sequences, without reusing their code (see `docs/AUDIT.md`).
Expand Down
Binary file modified docs/screens/kronograf-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screens/sim/t2-face1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screens/sim/t6-launcher-tools.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screens/sim/t9-radio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screens/sim/t9-video-setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screens/sim/t9-video.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion include/kg_version.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/* Firmware version, shown in System and on the console. Bumped with each release tag. */
#pragma once
#define KG_VERSION "0.6.1"
#define KG_VERSION "0.7.0-beta.2"
5 changes: 3 additions & 2 deletions sim/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ target_compile_options(lvgl PRIVATE -w)

file(GLOB_RECURSE UI_SRC ${ROOT}/src/ui/*.c)
file(GLOB DATA_SRC ${ROOT}/src/data/*.c)
add_executable(kronograf_sim main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${UI_SRC} ${DATA_SRC})
add_executable(kronograf_sim main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${ROOT}/src/hal/sim/hal_sim_media.c
${ROOT}/src/third_party/tjpgd/tjpgd.c ${UI_SRC} ${DATA_SRC})
target_compile_definitions(kronograf_sim PRIVATE KG_ROOT="${ROOT}")
target_include_directories(kronograf_sim PRIVATE ${ROOT}/src ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(kronograf_sim PRIVATE ${ROOT}/include)
Expand All @@ -25,4 +26,4 @@ if(UNIX AND NOT APPLE)
endif()
# Our code builds warning-free (generated fonts are excluded from -Werror).
file(GLOB UI_OWN ${ROOT}/src/ui/*.c)
set_source_files_properties(main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${UI_OWN} ${DATA_SRC} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra;-Werror")
set_source_files_properties(main.c ${ROOT}/src/hal/sim/hal_sim.c ${ROOT}/src/hal/sim/hal_sim_net.c ${ROOT}/src/hal/sim/hal_sim_media.c ${UI_OWN} ${DATA_SRC} PROPERTIES COMPILE_OPTIONS "-Wall;-Wextra;-Werror")
Binary file added sim/demo-camera.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading