The cat collar. BLE-only , it senses on the cat and runs one connectable advertisement carrying an 8-byte telemetry packet plus a custom audio service. When the station connects and subscribes, the collar streams paired audio + IMU in 100 ms frames for training capture. It never touches WiFi/internet, which keeps it tiny and low-power, and it always runs on its own battery.
| File | Responsibility |
|---|---|
main.c |
boot + the telemetry loop (orchestration only) |
ble.c |
advertising, identity, the audio GATT service, notify |
audio.c |
PDM mic capture -> 8 kHz µ-law |
streaming.c |
the decoupled reader/sender threads that assemble + send frames |
battery.c |
1S LiPo charge via the onboard divider |
imu.c |
LSM6DS3TR-C continuous sampler |
activity.c |
rules-based activity gate (cascade tier 0: low-power rest / wake) |
production.c |
rolling IMU window: runs inference and writes real telemetry |
classifier.c |
the confidence-gated action cascade (IMU first, audio confirms when unsure) |
tflm_classifier.cpp |
TensorFlow Lite Micro backend for the cascade (runtime-loaded models) |
model_loader.h |
runtime model-install API (clf_set_imu_model / clf_set_audio_model) |
ota.c |
OTA receive state machine: stages models to flash, defers the swap |
audio_codec.h |
µ-law + the model-input audio representation |
| bytes | field |
|---|---|
| 0-1 | company id (0xFFFF, test) |
| 2 | version |
| 3 | state (0 sleep, 1 rest, 2 active, 3 walk, 4 play, 5 groom) |
| 4 | activity (0-100) |
| 5-6 | steps (uint16, little-endian) |
| 7 | battery (0-100) |
The collar's BLE address identifies which collar it is, so no id goes in the payload.
The inference engine is TensorFlow Lite Micro (tflm_classifier.cpp). It runs a
confidence-gated cascade: the IMU model classifies first, and the audio model confirms only when the
IMU is unsure. No model is compiled into the firmware , each stage loads its .tflite at runtime
(clf_set_imu_model / clf_set_audio_model, the future OTA path). Until a model lands the engine
runs model-free: clf_*_model_present() is false and the cascade returns UNKNOWN, so the
firmware is safe to run with no model at all. The input pre-processing (per-window normalization,
int8 quantization from the model's own scale/zero-point) mirrors the training pipeline so on-device
inference matches what was trained.
Two things still pending: the telemetry state/activity/steps are simulated for now (a dwell-based
behaviour state machine; real battery is wired) and switch to real classifier output once a model is
loaded; and the OTA delivery that pushes a trained .tflite to the collar is Phase 3. See the
root README for the model + OTA plan. The cascade itself already runs at capture
time in streaming.c on the IMU window paired with each clip.
./build.ps1
build.ps1 finds the local NCS toolchain itself, runs west build -b xiao_ble/nrf52840/sense, and copies the
UF2. The XIAO nRF52840 has no debug COM port for flashing , flashing is always UF2
drive-copy: double-tap RESET so the board mounts as a USB drive, then re-run (the script copies
the UF2 across). Artifact: build/collar/zephyr/zephyr.uf2. Verified with NCS v3.3.1.
Console note: the firmware uses the legacy USB device stack (CONFIG_USB_DEVICE_STACK +
CONFIG_USB_CDC_ACM) and calls usb_enable(), because the new (NEXT) stack's CDC console silently
drops output on this board. Keep exactly one zephyr,cdc-acm-uart node.
Open the CDC serial port (any baud) , MEOW> collar id=cat_xxxxxx and state= steps= batt= print
every 2 s. The port only appears once the app is running (not in the UF2 bootloader). That
MEOW> collar id= line is what the dashboard reads over Web Serial to register the collar.