Acceptance procedure for each incoming batch of the EC-Buying ESP32S3-NxxRxx-128SPIx
(N16R8: 16 MB flash / 8 MB octal PSRAM, native USB 303A:1001, 1.28" GC9A01 + CST816D).
Why this exists. The board is a low-cost part from a supplier who can silently change the touch IC revision or its factory register config between batches. Two specific substitutions would ship a broken product without any visual difference:
- A touch IC whose
DisAutoSleep(0xFE) is 0 or unwritable. The retired C3 Kit's CST816T had 0xFE unreachable and wedged off the bus ~2×/hour. This board's CST816D ships with 0xFE=1 from the factory — butAutoSleepTime(0xF9) is still armed at 2 s underneath it. A batch with 0xFE=0 reverts to the C3's failure class. See [[touch-wedge-v4-regression]] and the C3 constraints section of CLAUDE.md. - A chip-antenna (
SOLDER_TERMINATION) RF build. Measured on our sample: the chip antenna loses association entirely (see RF).
Both are caught below. Run this before the boards go anywhere near production firmware.
-
The left-side POWER LATCH button must be LONG-PRESSED to power the SoC. USB alone does not boot it. A flashing red LED is the charger reporting "no battery" — that is normal, not a fault. Expect to think the first board is DOA. It isn't.
-
esptoolSTUB bulk-reads die on this board's USB-JTAG ("Packet content transfer stopped") at any baud. Use--no-stubforread_flash. Stub writes are fine and fast — normal flashing needs no workaround. -
On a DATA USB cable, the board reboots itself ~2 s into every power-on. You will see the splash, the boot prompt counting
3, 2, 1, a white flash, and the whole boot start over. It looks exactly like a crash or a brownout. It is neither, and the boot ROM says so:rst:0x15 (USB_UART_CHIP_RESET)That code means the USB-Serial/JTAG peripheral was told to reset by the host — firmware cannot produce it. Any program that opens the port shortly after the device re-enumerates triggers it: a serial capture reattaching, an IDE's serial monitor, Windows' own enumerator. It happens on roughly half of power cycles because it depends on whether the open lands inside the boot window.
Judge boot behaviour on a POWER-ONLY USB cable or a wall charger, with no computer attached. Verified: identical board, identical image — resets on a data cable, boots clean every time on power-only. Customers are always in the power-only case, so this artifact never reaches them.
If a board reboots during boot on power-only, that is a fault — capture the serial output on a data cable and look for
Brownout detector was triggeredor aGuru Meditationbacktrace, which are the codes that mean something real.
Serial is 115200. Each board takes ~3 min.
The probe compiles only src/probe/TouchProbe.cpp and talks to the CTP over the same
lgfx::i2c path the shipping TouchWatchdog uses, so ACK/NACK results compare 1:1 with the
C3 finding rather than through a different driver.
pio run -e probe-s3-128-phase2 -t upload -t monitorFlash and monitor from PowerShell, not Git Bash — pio under MSYS can gut its own esptool
package (see [[pio-msys-esptool-trap]]).
The boot census can race the host attaching its serial console, so the probe re-runs the census periodically. If you miss the boot banner, just wait for the next pass — don't reflash.
This is the load-bearing check: it is how we detect a silent IC substitution.
[probe] chip-id (0xA7) = 0xB6
[probe] proj-id (0xA8) = 0x02
[probe] fw-ver (0xA9) = 0x02
[probe] factory-id (0xAA) = 0x04
PASS: the quad reads B6 / 02 / 02 / 04.
Any other quad ⇒ QUARANTINE the whole batch. Do not accept, do not flash production. A different quad means a different touch-IC revision or factory config; it must go through the wedge evaluation (bisection + 24 h soak) again before it can ship. Record the quad you actually got — that value is the evidence for the supplier conversation.
From the read-only config dump (-- config space (read-only, pre-write) --), and the 0xFE
write/readback experiment:
| Register | Name | Expected | If it differs |
|---|---|---|---|
0xFE |
DisAutoSleep |
1 | REJECT/QUARANTINE — batch reverts to C3-style sleep |
0xF9 |
AutoSleepTime |
2 (s) | Informational — sleep is armed under 0xFE; this is why 0xFE matters |
0xFA |
IrqCtl |
0x60 | Flag — factory EnTouch|EnChange. A batch fingerprint, not the operating value (see below) |
0xFB |
AutoReset |
50 (s) | Informational — chip self-reset on gestureless touch |
0xFC |
LongPressTime |
60 (s) | Informational — chip self-reset on long press |
0xFB/0xFC are custom values, not datasheet defaults. They matter for forensics: a
chip-initiated self-reset presents as a ~450 ms register blank, which is easy to misread as a
wedge. Note them so field reports aren't misdiagnosed.
0xFAis a fingerprint, not the running config. An earlier version of this table said the factory0x60"proves the INT-gating requirement" and that "registers are only valid at INT pulses". Both are wrong, and that wording caused a misdiagnosis in #173. Two corrections, measured on a conforming board (chip id=0xB6 proj=0x02 fw=0x02 0xFA=0x60 0xED=1 0xFE=0x01):
- The firmware overwrites it. LovyanGFX's
Touch_CST816S::_check_init()writes0xFA=0x20(change-only) and0xED=20on every touch init. The device never runs on 0x60.- The registers are readable at any time.
Touch_CSTxxx.cppconsults the INT level only insideif (diff_msec < 10 && _wait_cycle)— a rate limiter, not a gate. Any poll spaced ≥10 ms apart does a full I2C read regardless of INT.INT gating is still the right read strategy at frame cadence (it is what fixed the phantom- release double-tap), but "no INT edge ⇒ no reading" is not a property of this chip, and no diagnosis should rest on it. Keep recording 0x60 — a batch that differs is still a supplier conversation — just don't infer read behaviour from it.
Read
0xFA/0xEDfrom a census AFTER the first RST pulse, not from the boot census. The probe prints several censuses and they legitimately disagree, becausetft.init()has already run_check_init()by the time the first one is taken. Measured on board #1, one run:
census 0xFA0xEDwhat you are looking at @phase2-boot0x200x14(20)the driver's values, just written @phase2-periodic(after RST test 1)0x600x01the factory fingerprint The RST pulse resets the chip to its factory config, and nothing re-runs
_check_init()afterwards, so every later census reads the real fingerprint. Judging §3 from the boot census would flag a conforming board — the reference measurement quoted above (0xFA=0x60 0xED=1) is itself a post-reset read.0xFE,0xF9,0xFBand0xFCare unaffected: they read the same in every census, which is why this trap only bites the two registers the driver touches.
The write experiment must show the register is writable:
[probe] 0xFE @<window>: pre=1 write=ok readback=1
PASS: write=ok and readback=1. write=NACK or readback=0 ⇒ REJECT — that is the
C3's DOA class, where the no-sleep insurance cannot be applied at all.
Production insurance: shipping firmware runs
MaintainNoSleep(write + verify) regardless, so a board that merely drifts is re-armed at runtime. That insurance is not a substitute for this gate — it cannot save a batch where 0xFE is unwritable.
The board carries both a 0603-size ceramic chip antenna and a u.FL/IPEX socket, selected
by a 0-ohm link the schematic names SOLDER_TERMINATION (chip) vs FEED_TERMINATION (u.FL) at
ANT1/RF1.
Required: FEED_TERMINATION (u.FL leg) + a YF0026-class FPC antenna fitted.
This section runs in two halves, and the second one cannot happen here. The probe from §1 does no networking at all, so there is no
[WiFi]line on the console while §1–§5 are running. Do the visual check now — link on the u.FL leg, antenna connected — and take the RSSI and reason-204 evidence from the §6 boot, which is the first time production firmware brings up the radio. Reading §4 as a single step in document order leaves an inspector waiting for output that build cannot produce.
Verify visually that the link sits on the u.FL leg and the antenna is connected. Then, on the §6 boot, confirm on serial at bench distance from the AP:
[WiFi] CONNECTED IP=... RSSI=-62 dBm
| Config | RSSI | Association | Verdict |
|---|---|---|---|
Chip antenna (SOLDER_TERMINATION) |
−64 dBm | ≥1 reason-204 retry every association; 17 timeouts in 35 min → total loss | REJECT |
Quectel YF0026 (FEED_TERMINATION) |
−62 dBm | 2.4 s, zero reason-204 | ACCEPT |
| C3 reference (same desk) | −61 dBm | — | baseline |
The near-identical RSSI across all three is the point: RX was never the problem. The chip antenna fails on TX — the handshake frames the device sends to the AP. So a healthy-looking RSSI number alone does not clear a board; zero reason-204 retries is the real criterion.
Any board showing repeated reason-204 disconnects ⇒ reject and check the link/antenna before blaming the AP.
The probe's phase-2 tests confirm the two pins the shipping firmware depends on. Drag a finger on the panel continuously while it runs.
- INT (GPIO11):
[probe] INT hunt: fingers=N falls:...— GPIO11 must rack up falling edges under drag (our sample: 1700+); every other candidate stays flat. - RST (GPIO0): fires 3× — hands off the RESET button while it does.
Expected: chip NACKs while held low, then cleanly re-ACKs after its ~450 ms boot.
[probe] RST test: before=ACK duringLow=NACK/... after=ACK -> ...
A failure here means the pin map differs from include/variants/s3_128.h — quarantine the batch; the variant header (and the watchdog's hard rung) would be wrong.
Verified pin map for reference — hardware and vendor-doc cross-confirmed:
| Function | Pins |
|---|---|
| Display GC9A01 (SPI3) | SCLK=3, MOSI=10, DC=18, CS=2, RST=21, BL=42 (LEDC PWM) |
| Touch CST816D | SDA=8, SCL=9, addr 0x15, TP_RST=0, TP_INT=11 |
| TF card | SCK=41, MOSI=47, MISO=48, CS=40 |
| Buttons / misc | SW_UP=14, SW_PW=15, SW_Down=16, VBUS detect=17, battery ADC=1 (÷2), RTC INT=45 |
GPIO0 is a strap pin. The probe drives it deliberately; nothing else should.
The env is blipscope-s3-128. There is no other one. Not a -prodburn (deleted), not
-cloud or -soak (staging-only bench envs). Flashing one of those ships the wrong backend or
none at all, and the board looks perfectly healthy either way.
pio run -e blipscope-s3-128 -t upload --upload-port COM<n>Always pin
--upload-port. A soak board is usually attached alongside the unit under inspection, and PlatformIO's auto-detect has no idea which is which. The failure is silent and expensive in exactly one direction: it reflashes the board carrying a multi-hour run.
Flash from a checkout of
main, and verify the built image — not the command. This names an env, not a commit: it builds whatever the working tree holds. On 2026-08-08 this checkout sat on an unrelated feature branch while two shipping fixes were merged, so the identical command would have produced a board with no cloud feed and no stale-ladder floor, and nothing about the flash, the boot, or the radar would have looked wrong.git log --oneline -1 # on main, and current grep -ac 'scopes\.valarsystems\.com' .pio/build/blipscope-s3-128/firmware.elf # >= 1 grep -ac 'scopes-staging' .pio/build/blipscope-s3-128/firmware.elf # 0
v11 OR LATER ONLY, and this one is not about the backend. Flash a build that contains the v11 config fix; do not ship a board from an older image and rely on OTA to catch up.
What v10 actually does, stated precisely, because the two failure modes are easy to conflate. v10 has no local-time schedule at all —
QuietHourPolicy.hdoes not exist on it. Its update check ismillis() - lastOtaCheck > 24h, which is uptime-based and therefore DRIFTS: a board that boots at 14:00 reboots at 14:00 every day thereafter, wherever that lands. A v10 board does not reboot at 20:00; it reboots at whatever time its clock happened to start. That drift is the defect v11's quiet hour exists to remove.The manufactured zero is a separate defect, and it only bites in v11. Every build before v11 had a config page whose clock-offset field defaulted to the zone derived from the STORED longitude — which on a factory-fresh board is empty, so the field rendered
0. The first save is the same save that sets the location, so it posted that0back as though the customer had chosen it. On v10 that stored zero changes nothing, because nothing reads a local offset for scheduling. On v11 it would pin the device to UTC and put the 03:00 "local" reboot at 03:00 UTC = 20:00 Pacific — which is why the form fix and the migration are part of v11 rather than follow-ups to it.Three of three bench boards carried that zero. It is what §7 provisioning does by default, not an edge case.
No board ever experiences the 20:00 mode.
configmigration::Apply()runs insetup(), before any quiet-hour decision is taken inloop(), so on the first boot of a v11 image the manufactured zero is already gone by the time anything schedules a reboot.So the choice this note is really about is drift: a board shipped on v10 drifts in front of its customer until the OTA lands, and is then repaired — form and stored value both — on the first v11 boot. Flashing v11 here avoids that window entirely.
grep -ac 'migrated tz-offset' .pio/build/blipscope-s3-128/firmware.elf # >= 1That string exists only in the migration added in v11, so it separates a v11+ image from an older one without trusting the version number. If it returns 0, the tree is older than v11 — see the "flash from a checkout of main" note above, which is the usual cause.
Confirm on boot: radar renders, backlight responds, touch registers a tap, WiFi associates with zero reason-204, and the OTA line reports the expected channel/version:
[build] env=blipscope-s3-128 fw=v<N>
[WiFi] CONNECTED IP=... RSSI=-<nn> dBm
[ota] channel=s3-128 current=<N> latest=<N>
This boot is where §4's serial half gets recorded — the RSSI reading and the zero-reason-204 criterion. Watch it here; the probe build could not produce it.
No per-unit gate for a touch controller that wedges later — on purpose. The quick tap above proves touch works at inspection. A controller that wedges later (after 10 minutes, a day, a month) is handled in the field by the v15 touch-wedge cap: the device stops rebooting and tells the customer on screen, with its Device ID and the support address (docs/v15-touch-wedge-cap.md). That covers a wedge at any time, where an inspection soak would only cover the first minutes.
Every board leaves with its own device key, minted by the Worker. Nobody on the bench
holds DEVICE_KEY_SECRET, and nothing asks for it (docs/provisioning-mint.md).
Bench mode writes the CI factory image, gets the board's key from POST /blipscope/provision,
writes it into NVS, proves it's on the board, and verifies it against the Worker.
- Once per bench machine: put the provisioning token (from the password manager) in
%USERPROFILE%\.config\valar-flasher\provision-token. Check it's there as a boolean, never by printing it:if (Test-Path "$env:USERPROFILE\.config\valar-flasher\provision-token") { 'present' } else { 'absent' } - Run it, naming the port:
.\.venv\Scripts\python.exe .\valar_flasher.py --product Blipscope --bench --ports COM<n>(fromC:\Github\valar-flasher). Protected boards on the same machine are refused whatever you type. - DONE means verified: the key was minted, written, proven on the board, accepted by the
Worker, and the board's row is in
provisioned.csv. What a red tile's reason means:
| the tile's reason | meaning | next |
|---|---|---|
PROVISION_TOKEN rejected (403) |
the token file doesn't match the Worker's secret | fix the file from the password manager; every board this run will fail the same way |
daily mint cap (120) reached |
today's mints are used up | resume after the reset time shown |
salt drift |
the Worker and this firmware derive different ids | stop the batch; nothing was written |
nvs write failed its on-chip hash / NOT verified on-chip |
the NVS write didn't take, or esptool couldn't check it | reseat and re-run that board. The full esptool output is in provision-logs/ |
a DIFFERENT board answered |
the board was swapped mid-run | stop and check the hub |
Sections 1–6 all inspect a board that is already provisioned. The customer's first five
minutes are a different code path, and it is the only one no log can verify after the fact: by
the time a device checks in, it has already survived it. Two bugs shipped into that path in one
week (#166,
#173) and both were found by
accident, when something wiped WiFi unintentionally. Neither announced itself: a dead :80
looks identical to a healthy one, and a recovery gesture that cannot fire logs the same line as
a customer who did not press hard enough.
Fifty boards are about to take this path exactly once each. Run it last, on every board, after §6 passes.
Record it. Start a ledger before step 1 so the pass is evidence rather than scrollback:
Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass',`
'-File','c:\Github\Blipscope\scripts\bench-capture.ps1','-Port','COM<n>','-Label','board-<NN>'A section whose whole premise is "no log can verify this afterwards" should not then be verified by someone remembering they watched it. The recorder redacts credentials at write time, so the ledgers are safe to keep.
-
Wipe Wi-Fi the way a customer does. Stats screen → double-tap
[ Reset Wi-Fi ]. The device forgets the credentials and reboots into the config portal. (Do not use the boot-hold for this — see step 4.) -
Provision through the portal. Join the
Blipscope-XXXXXXAP from a phone, choose the bench SSID, enter the password, save. Do not power-cycle the device by hand. The whole point is the first boot after provisioning, and a customer does not reboot here. -
The config page must answer on that first boot — no power cycle in between. Poll; do not judge on one attempt:
for i in $(seq 1 20); do curl -s -o /dev/null -w "$i: %{http_code} %{size_download}\n" --max-time 3 http://<device-ip>/ sleep 2 done
Expect
200and ~50 KB within 30 s of the device reporting an IP.A refusal in the first ~20 s is normal and is NOT the bug. The network stack answers well before
server.begin()runs, so connections in that gap are actively refused. Measured on a healthy board: IP at boot,ERR_CONNECTION_REFUSEDthrough ~13 s, first200at ~17.5 s uptime. An earlier version of this step said "a refused connection is a fail, not a retry" — that would quarantine healthy batches, and it is also exactly what a customer sees if they type the address the moment setup finishes.A refusal that persists past 30 s is the failure. That is #166: AsyncWebServer lost the bind to the portal's listener, and
begin()returnsvoid, so nothing said so. Re-flashing does not clear it; only a power cycle does — which is precisely why it must be caught here and not by the customer.There is currently no serial line that reports the bind outcome. The liveness check added for #166 was reverted in #172 because the probe socket itself tore down the live listener. Until a safe replacement lands (#181),
curlis the only evidence. Do not skip it. -
Recovery gesture. Power-cycle. Within ~3 s the screen shows TOUCH & HOLD / to reset WiFi. Touch it then — not before — and hold through the countdown:
[wifi-reset] boot touch window: driver=1 polls=56 touched=1 [wifi-reset] boot touch detected; hold to confirm [wifi-reset] hold completed -- clearing credentials (misses=0 longestGap=56ms)If a serial recorder is attached, the FIRST countdown after the power cycle will be interrupted and start over. Do the gesture on the second one. That white flash is the recorder reopening the port inside the boot window, not a board fault, and the ROM says so in the ledger:
rst:0x15 (USB_UART_CHIP_RESET)— a code firmware cannot produce (§0). No DTR/RTS setting avoids it; the trigger is theopen()itself. Keep the recorder attached anyway: an unrecorded pass is scrollback, and this is the one section that exists because no log can verify the path afterwards.The touch has to ARRIVE after the prompt, and this is not a UX preference. A finger already on the glass at power-on is calibrated into the CST816's no-touch baseline by the TP_RST pulse inside
tft.init(), and is then reported as absent until it lifts — measured: 12 s of solid contact, one reset, then zero touches for 48 s with the thumb still down (#173). No board will ever pass a "hold from power-on" test. If one fails this step with the touch arriving correctly, that is a genuine board fault.missescounts polls that saw no contact during the hold; expect 0. A non-zeromisseswith a largelongestGapmeans that batch's touch IC drops samples mid-contact — record it, that is the phantom-release class the C3 had (see §3). The gesture tolerates gaps under 250 ms, so it will still pass; the number is the early warning, not the failure. -
Detail cards actually fill in. Leave the board running until contacts appear, then tap one and read its card.
Expect a type — "Boeing 737-800", "Cessna 172" — not a callsign on its own. And in the ledger, on the boot after provisioning:
[source] Blipscope Cloud: https://scopes.valarsystems.com (active 5000 ms; cfg pending)Why a unit test cannot replace this, and why it is per-unit rather than a bench check. Provisioning writes four NVS keys and none of them is the detail-card source:
config(namespace),cloud-key,cloud-key-fac, andcloud-urlonly when one is passed. The source is therefore unset on every board that leaves the bench, and an unsetlocal-detailsresolves to Off (AircraftManager.cpp §"Detail-card source for a local receiver").Cards still work, because
UseCloudEnrich()returnstrueon its first line —if (useCloudSource) return true;— anduseCloudSourceis!useLocalSource && dataSource != "opensky", which an unwrittendata-sourcesatisfies. ThelocalDetailsterm sits on the line below and is never reached. That consent gate binds only a customer who has explicitly chosen My own ADS-B receiver.That is a code read, and a code read is what this whole section exists to distrust. If the default ever flips — a new
data-sourcevalue, a migration, a reordered branch — every unit ships with a radar that draws blips and cards that say nothing, and nothing anywhere would report it: no log line, no health check, no failing test. The device is working exactly as configured. One tap per board is the cheapest possible observation of it.A failure looks like:
[source]naming OpenSky or a local receiver instead of Blipscope Cloud, or a card that shows the callsign and nothing else.One legitimate confound, so this does not quarantine good boards. A relayed contact (TIS-B/ADS-R) carries a track id rather than an ICAO address, no registry can answer for it, and its card is empty by design — callsign only. That is not a failure. Tap a different contact, or check the
[perf]line in the ledger:[perf] ... enrichReqs=8 enrichOk=8 enrichEmpty=0 enrichNonIcao=0/0 enrichCached=0enrichOkrising is the board asking and being answered.enrichReqs=0while contacts are tracked is the real failure — enrichment never attempted, which is what a wrong source produces.enrichNonIcao=<tail>/<total>counts the relayed contacts and is the confound made countable.
Run this on the FIRST board of the batch, after §7, before the other 49 start. It is the one check the pilot run cannot repeat later, because after this board takes an update it will never be in the starting state again.
Why board #1 and not the bench. Every OTA test to date ran on a unit that already had a
working image in the other slot — the bench board had been flashed both ways many times. A
factory-fresh board has app1 blank. The download path has no reason to care, and that is
exactly the shape of assumption that has cost a week each time it turned out to be wrong: the
CST816 "obviously" reports a held finger, end() "obviously" releases the TLS transport,
begin(url) "obviously" validates a certificate. None of them did. This is the cheapest
possible place to find out, and after board #1 it becomes the most expensive.
-
Confirm the starting state is genuinely virgin. A USB flash writes
app0and resets otadata, so this is the state all 50 ship in:[ota-slot] boot running=app0 @0x020000 state=UNDEFINED next=app1 fw=<N>next=app1with nothing ever written there is the case under test. If this readsapp1, the board is not virgin — reflash over USB and start again. -
Trigger one real update. Build against the pinned pre-release harness (see RELEASING.md — and re-clobber the assets first, the tag holds a mixed matrix from CI):
pio run -e blipscope-s3-128-otatest -t upload
-
The same three checks, from the state the fleet is actually in:
check expected download completes [ota-mem] pct=100 ..., noUpdate failedslot switches [ota-slot] boot running=app1 @0x660000 ... fw=<N+1>survives power loss power-cycle, then running=app1 ... state=VALIDDo not wait for
PENDING_VERIFY— you will never see it, and its absence is not a failure. Rollback IS armed on these builds (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y), so the state genuinely passesNEW → PENDING_VERIFY → VALID. But arduino-esp32'sinitArduino()callsesp_ota_mark_app_valid_cancel_rollback()beforesetup()runs, andLogOtaSlot("boot")is insidesetup(). The transition is real, resolved, and structurally unobservable from where we log it.VALIDon the first post-update boot is the correct and expected reading.This is a correctness gate, not a brick gate. An earlier version of this section said
PENDING_VERIFYrisks "a rollback toapp0, which on a virgin board is a slot that was never written". That is backwards: on a virgin boardapp0is the slot that was written, by the USB flash, andapp1is the blank one. OTA writes the blank slot and boots it; if that image fails, rollback targetsapp0— the working factory image. The brick-by-rollback scenario does not exist, because the rollback target is always the previously-working slot by construction.What §8 actually proves is narrower and still worth a gate: that the download, the write, and the boot-partition switch all behave when the destination has never been written. That is the only state the fleet ships in and the only state this board can be tested from once.
-
Then reflash board #1 to production over USB (
pio run -e blipscope-s3-128 -t upload) so it rejoins the batch in the same state as the other 49 —app0, otadata reset, no pinned OTA base compiled in. Record the result on the batch sheet; the other 49 do not repeat §8.
[ota-slot] boot running=app0 @0x020000 state=UNDEFINED next=app1 fw=5 <- virgin
[ota] channel=s3-128 current=5 latest=6
[ota-mem] pre-update free=236820 largest=135156 free8=236820 tlsOk=1
[ota-mem] pct=0 free=176080 largest=118772 free8=176080 TRIAL tlsOk=1 rej=0
[ota-mem] pct=100 free=180836 largest=118772 free8=180836 TRIAL tlsOk=1 rej=0
[ota-slot] boot running=app1 @0x660000 state=VALID next=app0 fw=6 <- first boot
[ota-slot] boot running=app1 @0x660000 state=VALID next=app0 fw=6 <- after power cycle
Note largest=118772 unchanged across the whole transfer while free8 moves — the plateau
from #163, reproduced during the operation it most mattered for. Read free8 and the trial
result; largest is retained only so the plateau stays visible beside the number that tracks.
A batch is accepted only when, across the sampled boards:
- Identity quad = B6/02/02/04 (100% of boards)
DisAutoSleep0xFE = 1, andwrite=ok/readback=1(100%)IrqCtl0xFA = 0x60 (100%)- RF =
FEED_TERMINATION+ YF0026-class antenna, zero reason-204 (100%) - Touch INT/RST behave per §5 (first 5, then spot-check)
- First-run per §7: portal provisioning completes,
curl http://<ip>/returns 200 on the first boot after setup with no power cycle, the boot touch-to-forget reacheshold completedwithmisses=0, and a tapped aircraft's card shows a type with the boot line reading[source] Blipscope Cloud:(100%) - Board #1 only — §8: one OTA from a virgin
app1completes, the slot switches, and the new image is stillrunning=app1 state=VALIDafter a power cycle. Board #1 does not rejoin the batch until this passes; the other 49 do not repeat it
Any quad mismatch, any unwritable/zero 0xFE, or any reason-204 storm ⇒ quarantine the batch and open a supplier conversation, quoting the measured values. Do not ship a partial batch on the assumption the rest are fine — these are per-batch factory-config properties, not per-unit defects.
Keep with the batch: supplier + PO, date, quantity, the measured identity quad, 0xFE pre/write/
readback, 0xF9/0xFA/0xFB/0xFC values, RSSI + reason-204 count, the §7.5 detail-card
result per unit (type shown / [source] line), and the pass/fail call.
This is the evidence trail for the next supplier conversation — the antenna requirement was won
with exactly this kind of measured A/B.
Before any unit ships: record its Device ID (shown on its config page) against the order number in Daniel's private sheet — never in this repo.