Build numbers¶
Build numbers are SimpleOTA's source of truth for "is this newer than what the device is running?".
Properties¶
| Property | Behavior |
|---|---|
| Scope | Per project. |
| Type | Positive integer. |
| Allocation | Atomic increment of Project.build_counter. |
| Monotonic | Always strictly increasing within a project. |
| Reusable? | Never. Even if you delete the artifact. |
| Gap-fillable? | No. A failed upload still consumes the number. |
| Comparable across projects? | No. Project A's 42 and project B's 42 are unrelated. |
Why monotonic?¶
The decision engine's freshness test is purely:
"Is artifact.build_number > device.current_build_number?"
That comparison is the entire definition of "newer". No semver parsing, no date games: just an integer. Devices report their current build number on every check; SimpleOTA picks the highest assigned and compatible artifact and tells the device to install it if it's higher.
How are they allocated?¶
Inside Project there's a build_counter integer. On artifact upload,
SimpleOTA opens a transaction, takes a SELECT … FOR UPDATE on the
project row, increments the counter, and assigns that value to the new
artifact. This is the only safe way to guarantee monotonicity under
concurrent uploads.
If the transaction commits, the number is gone even if you later delete the artifact. That's intentional; devices may already have recorded the build number, so re-using it would lie about lineage.
Device firmware integration¶
This is the most common source of confusion.
SimpleOTA assigns its own build number on upload. This number has nothing to do with any counter you maintain inside your firmware binary.
The correct flow¶
- First boot: device has never done OTA. Report
current_build_number: 0. - SimpleOTA responds with an offer:
{ "update_available": true, "build_number": 21, ... }. - Device downloads, flashes, and reboots.
- Device saves
21to NVS (or equivalent persistent storage). - After reboot, device reports
current_build_number: 21on every subsequent/check/call.
What NOT to do¶
Never report your own embedded version as current_build_number
If your firmware binary contains #define APP_BUILD 22 (or a git
commit counter, or any internally-tracked number), do not send that
as current_build_number.
SimpleOTA's decision engine compares its own artifact.build_number
against device.current_build_number. If those come from different
counters, the comparison is meaningless:
- Your internal counter is
22. SimpleOTA has assigned build21to the same binary. The engine sees21 ≤ 22→up_to_date. The device never updates. - Your internal counter is
5. SimpleOTA has assigned build21. The engine sees21 > 5→ offers an update the device is already running.
The rule is simple: the value you persist in NVS after a successful
OTA must be the build_number from the /check/ response, and that is
the only value you ever send back as current_build_number.
Your human-readable version string (v1.4.2, git SHA, etc.) belongs in
the version field — it is stored and displayed, but never used for
ordering.
Common gotchas¶
- Don't try to map build numbers to git tags 1:1. They're allocated on upload, not on commit. A failed CI run that gets to the upload step but errors mid-flight will still consume a number.
- Don't expose build numbers as your user-facing version string.
Use whatever is in your firmware (
v1.4.2, git short SHA, etc.) for human display. Build number is plumbing. - Don't hardcode
current_build_numberin your firmware. Read it from the NVS key you wrote after the last successful OTA. On a factory-fresh device with no NVS key, send0.