Make It Yours: User Layers and the Edit-Build-Flash Loop
Part 3 of The Thingino Developer’s Journey / <- Part 2
Sooner or later every firmware developer’s tree becomes an archaeological dig: a debug flag here, a hardcoded MQTT broker there, an experimental package enabled three weeks ago and forgotten. Then you go to send a pull request and spend an evening separating “the fix” from “my life choices.”
Thingino solves this structurally. Everything personal lives in a layered user tree that is gitignored, scoped, and merged into the build automatically. Your checkout stays clean; your customizations persist.
The three layers
user/common/ # applies to every build you make
user/<camera>/ # applies to one camera model
user/<camera>/<ip>/ # applies to one specific device
Narrower scope wins. Build with CAMERA=<camera> IP=192.168.88.31 make and
all three layers apply, device-scope last. Build without IP= and the device
layer is skipped. Want a factory-pure build with no user input at all?
PRISTINE=1 CAMERA=<camera> make
That single variable is your “is it my config or is it a real bug?” switch, and you will come to love it.
What goes in a layer
Each layer directory can contain any of these:
| File / dir | What it does |
|---|---|
local.fragment |
extra Buildroot config symbols appended to .config (e.g. BR2_PACKAGE_STRACE=y) |
local.mk |
user-scoped package source overrides (see Part 5) |
thingino.json |
merged into /etc/thingino.json — core settings, MQTT, motors, web UI |
prudynt.json |
streamer settings — resolution, bitrate, OSD |
overlay/ |
files copied straight into the root filesystem |
opt/ |
files that appear at /opt/ on the camera |
local.uenv.txt |
extra U-Boot environment entries |
A concrete example. Suppose all your cameras should talk to your MQTT broker,
and one particular camera on your bench needs strace and verbose debugging:
user/common/thingino.json -> {"mqtt_sub": {"enabled": true, "host": "192.168.1.10"}}
user/wyze_cam_v3_t31x_gc2053_rtl8189ftv/192.168.88.31/local.fragment
-> BR2_PACKAGE_STRACE=y
Every build gets your broker. Only the bench camera gets the debug tooling. Nothing touches git.
Config fragments participate in dependency tracking: edit a local.fragment
and the next make fast notices and regenerates the config. No stale-config
paranoia required.
The loop: edit, build, flash
With user layers in place, daily development is a tight cycle:
CAMERA=<camera> make fast # incremental build
CAMERA=<camera> IP=192.168.88.31 make ota # flash over the network
make ota pushes the full image to a camera already running Thingino and
flashes it. For most changes the camera is back on its feet in a couple of
minutes. Copying single files by hand is even faster during experimentation —
one dropbear-flavored gotcha: use scp -O (legacy protocol), or scp will
just sit there judging you.
Also handy: back up a camera’s overlay partition (its writable settings) before you do something bold:
CAMERA=<camera> IP=192.168.88.31 make backup-overlay
When a change should be shared
The user tree is for your stuff. When you discover a change that belongs in the product — a package everyone should have, a fixed default — it graduates:
CAMERA=<camera> make menuconfig # make the change in the config UI
CAMERA=<camera> make saveconfig # write it back to the camera defconfig
Then the diff in configs/cameras/<camera>/ becomes your pull request.
make edit-defconfig opens the defconfig directly if you know exactly what
you’re changing. The full reference for the user tree lives in
docs/local-build-settings.md.
What you learned
user/common/->user/<camera>/->user/<camera>/<ip>/, narrow wins.- Fragments, JSON add-ons, overlays, and
opt/cover nearly every customization without touching git. PRISTINE=1answers “is it me or the firmware?”make fast+IP=... make otais the loop;scp -Ois the handshake.
Next up: Part 4 — Adopting a Stray: you found a
camera that isn’t supported yet. Time to fix that, and earn your place in the
configs/cameras/ credits.