- C++ 96.6%
- CMake 2.5%
- C 0.9%
| include/jt | ||
| src | ||
| test | ||
| CMakeLists.txt | ||
| idf_component.yml | ||
| README.md | ||
jt-device
The platform's device-side C++: what every firmware (cam, heater, mower) needs to take part in the platform, built on nats-cpp.
-
jt::Guard(include/jt/guard.hpp): signed commands with capability tokens. A device checksdoandadmincommands with only its tenant's signing keys: the token (trusted issuer, expiry, delegation), the holder's signature over subject, time, nonce and payload, replays, revocation, and the command's class on this device (docs/NATS-CONCEPTS.md §6, docs/SUBJECTS.md). -
jt::verifyRelease(include/jt/release.hpp): may a desired version be installed? Signed by a trusted release key, and the statement names this project, version and file (SHA-256, size); the same codes as the platform'sDesired.VerifyFor. -
jt::UpdateAgent,jt::TwinUpdater(include/jt/twin_updater.hpp, ESP-IDF only): firmware updates of protocol v1. After connecting:auto agent = std::make_unique<jt::UpdateAgent>(nc, jt::UpdateConfig{ id, "scale", esp_app_get_description()->version, jt::parseKeyList(JT_RELEASE_KEYS)}); agent->start(); // then agent->step() about once a second, from the app's taskIt finds the twin bucket (retried), reports the boot (
okorrolled_back), watchesdev.<id>.desired, checks, downloads, installs and reboots;jt::imageConfirmed()when the app confirms the image. Used by the cam and the espresso scale.UpdateConfig::onProgress(const UpdateProgress&): state (downloading 0–100 %, installing, failed), the version and the error; for devices with a display. Called from the update task, so hand it to the UI task instead of drawing there.- Restart-loop guard: the version being installed is marked in NVS
(
twin/busy). If the device restarts before the update ended, the boot report isfailed("the device restarted while installing X") and X is not tried again until another version is wanted. - The update task (
jt::kUpdateStack, 12 KB; a real download left 1.1–1.5 KB of 10 KB) runs on internal RAM: installing maps the image, and ESP-IDF asserts on a PSRAM stack while the cache is frozen. jt::reserveUpdateWorker(): call early inapp_mainto take the update task's stack while internal RAM is still in one piece; updates then run on it (the scale: 20 KB free after connecting, but no 10 KB block). It holds 12 KB for good: not for devices as tight as the Gimbal cam (16 KB free).UpdateConfig::runUpdateruns jobs on a task of the app's own instead.- No memory for the update task twice in a row: the device restarts
once per version to install right after boot, when the RAM is in
one piece (NVS
twin/nomemprevents a restart loop).
-
jt::imageCheckStart,jt::imageHealthy(include/jt/image.hpp, ESP-IDF): when a new image counts as good. With a platform once it has the boot report (the update agent callsimageHealthy()); not within 10 min although the network is up → restart into the previous image; without network confirmed anyway; standalone after 60 s. Needs the bootloader's app rollback, harmless without it. The confirm runs on the esp_timer task (reading otadata maps flash, which asserts on a PSRAM stack); a boot report that arrives beforeimageCheckStartstill counts. -
jt::resetReason,jt::lastCrash(include/jt/crash.hpp, ESP-IDF): why the device started and what crashed before; the boot report sends both (reported.reset,reported.crash). The panic handler is wrapped (linker--wrap=esp_panic_handler, set by this component) and copies the reason in IRAM into RTC memory, with up to 10 backtrace addresses: with silent asserts every abort's pc ispanic_abort, the callers say where. Decode them with the ELF of exactly that version (CI keeps it:~/.cache/jotronics/elf/<project>/). -
jt::Presence,jt::chipTemp,jt::addSystemStatus(include/jt/presence.hpp, ESP-IDF):dev.<id>.onlineat every (re)connect anddev.<id>.tel.statusevery n seconds, with the system fields (version,uptime,reset,chipTemp,wifi.rssi,memory.internalKB/largestKB/psramKB) and the app's own;step(nc)about once a second from the app's task.jt::chipTemp()owns the temperature sensor (it can be installed only once). Used by the scale and the cams (protocol v1).
Later: encrypted payloads (xkeys).
Tests
test/vectors.json is made by the platform's Go guard
(go/internal/natsops, TestDeviceVectors); the C++ guard must give the
same code for every vector, so both implementations stay the same.
test/release_vectors.json does the same for releases
(go/internal/platform, TestReleaseVectors, build/jt_release_tests).
cmake -S . -B build -DNATS_CPP_DIR=<nats-cpp checkout>
cmake --build build && build/jt_device_tests
# new vectors after a change of the Go side (commit both)
cd ../../go && JT_WRITE_VECTORS=1 go test ./internal/natsops -run TestDeviceVectors
cd ../../go && JT_WRITE_VECTORS=1 go test ./internal/platform -run TestReleaseVectors
On NixOS: nix-shell -p cmake mbedtls gcc gnumake --run '…'. Not yet a
Nix check: nats-cpp is not reachable from the flake (see docs/PLAN.md,
housekeeping).
In a firmware: an ESP-IDF component (REQUIRES nats-cpp); copy the
headers of a command into jt::CommandHeaders and pass the device's
clock (SNTP) as now_ms.
Releases
jt-device is developed here, next to the Go code its test vectors come
from, and released as its own repository lab/jt-device on the lab
Forgejo (history included):
scripts/jt-device-release.sh v0.2.0 # commit first; pushes main and the tag
Firmware pins the tag (and nats-cpp's, lab/nats-cpp) in
main/idf_component.yml:
jt-device:
git: http://localhost:13000/lab/jt-device.git
version: v0.2.0
Renovate (lab VM, nix/modules/renovate.nix) opens one PR per firmware
repository for new library tags; CI builds it (size budget included,
nothing is published from a branch) and Renovate merges it when green,
which publishes to the dev channel. To try an unreleased change in a
firmware, build it with JT_LOCAL_COMPONENTS pointing at this directory
(and at a nats-cpp checkout).
Versions: patch for fixes, minor for new API (old firmware still builds), major when a firmware has to change.
WiFi power (jt/wifi_power.hpp, C: jt/wifi_power.h)
When the radio may use modem sleep. It stays awake while something needs it
(hold()/release() counted, holdFor(ms)), on external power on battery
devices, and on mains devices while the signal is weak (below -75 dBm, back
after 60 s at -70 dBm or better); otherwise the device's own setting applies.
The twin updater holds it during every download and install.
jt::WifiPowerConfig c; c.powerSave = settingOn; c.batteryDevice = false;
jt::WifiPower::instance().start(c); // after esp_wifi_start()
jt::WifiPower::instance().hold(); // stream started / page opened ...
jt::WifiPower::instance().holdFor(60000); // ... a command arrived
The rule alone is jt::WifiPowerPolicy (host test test/test_wifi_power.cpp).