CricketOne: first chip and camera bring-up
Order 1 was placed on 1 Nov 2026. It was the owner’s call to order one of each:
| Part | Qty | Arrives (Amazon estimate) | Note |
|---|---|---|---|
| OV9281 global-shutter camera, 2-pack | 1 pack | Mon 2 Nov | The pack includes 2 × 15 → 22-pin cables for the Pi Zero, and 2 × 15 → 15-pin |
| SanDisk High Endurance 32 GB microSD | 1 | Mon 2 Nov | |
| Raspberry Pi Zero 2 W | 1 | Fri 6 Nov | One Pi: programming now, and the same Pi goes into the first stump |
Bring-up extras, also placed by the owner on 1 Nov 2026 ($25.98 before tax):
| Part | Arrives (Amazon estimate) | Why |
|---|---|---|
| CanaKit 5 V 2.5 A micro-USB power supply | Mon 2 Nov | Steady power while the camera runs; a laptop port or weak charger makes the Pi throttle or reset |
| Geekworm C296 heatsink for the Pi Zero 2 W | Mon 2 Nov | Keeps the bench test B1 from overheating the Pi and giving a false fail. Bench only: in the stump the heat bar does this job |
| SanDisk MobileMate microSD reader | Tue 3 Nov | To flash the card on this PC |
The owner already has a tool to measure the lens and camera hole, so the caliper was not bought.
Not bought, and not needed yet:
- the separate ribbons, because the camera pack has them;
- the pin headers, which only the order 2 sensors need.
Radxa ZERO 3W kit, also placed by the owner on 1 Nov 2026 ($111.97 before tax). The owner decided to try both boards and take one to production for now:
| Part | Arrives (Amazon estimate) | Why |
|---|---|---|
| Radxa ZERO 3W, 2 GB, no eMMC | Mon 2 Nov | The second board: RK3566 with a 0.8 TOPS NPU, secure boot and a certified radio; same 65 × 30 size as the Pi |
| Radxa heatsink 5519A | Mon 2 Nov | Made for this board |
| CanaKit 3.5 A USB-C power supply | Sun 1 Nov | The Radxa takes USB-C power |
| SanDisk High Endurance 32 GB microSD | Sun 1 Nov | Its own card, so both boards run side by side |
The second camera from the pack goes on the Radxa, with the pack’s second 15 → 22-pin cable: the ZERO 3W has the same 22-pin camera connector as the Pi Zero. So nothing is spare any more.
All five Amazon orders together were $309.51 with tax.
1. Have these ready by Friday
- The power supplies, heatsinks and card reader from the orders above.
- Your measuring tool for the lens height.
- A 2.4 GHz Wi-Fi network name and password. The Pi Zero 2 W has no 5 GHz Wi-Fi. Most home routers give both bands under one name, which is fine.
- Raspberry Pi Imager on this PC: raspberrypi.com/software.
- For focusing and the lens step:
- a tennis ball;
- an A3 print of a 13 × 9 chessboard of 30 mm squares, glued flat to foam board;
- something to stand it on 0.5-0.6 m from the camera.
- Something soft to set the Pi and camera on. Hold the boards by their edges, and never plug or unplug the ribbon while the Pi has power.
2. Monday 2 and Tuesday 3 Nov: the camera, then the card (about 30 min)
- Monday: look at the camera.
-
Take one camera out of the pack and photograph both sides.
-
Measure and write down four things (row R21):
- Lens height: from the board’s front face to the very front of the lens.
- Lens barrel: the outside diameter at its front.
- Board: width and height, and whether its corners are square or rounded (and how round).
- Board depth: from the back of the board to its front face, with any parts on the back.
-
Check the 15 → 22-pin cable is in the pack.
-
Write the numbers into the stump design (R52). Open
products/cricketone/build/cad-s/lens.jsonand fill themeasuredblock (today’s date, then the numbers in mm; leave theestblock as it is):Field in measuredWhat to measure Est. today datetoday, e.g. “2026-11-02” - reachMm2.1 lens height 7.5 barrelDiaMm2.2 lens barrel 7.0 boardWMm,boardHMm,cornerRMm2.3 board: width across (the sensor’s long side, left to right), height, corner radius (0 if square) 24, 25, 0 boardDepthMm2.4 board depth 6.0 Since R54 the head is drawn from these board numbers too: lens-check also prints a “board” line (gap to the head’s parts, at least 0.3 mm, and the plastic left outside the corner pockets, at least 1.5 mm). A board up to 25 mm wide fits (25.5 does not); try one with
node lens-check.mjs --boardw 25 --boardh 24. -
Run the re-check in a terminal in that folder (about 30 s):
node lens-check.mjs. It runs the head fit, the window trace and the camera views with your numbers and prints PASS, or what is wrong and a line starting FIX: with the one number to change (R53):bracket.backMmin lens.json, how far the camera bracket sits back. Put that number in, then runnode cricketone-s-cad.mjs,node maker-files-s.mjsandnode lens-check.mjsagain until it prints PASS. To try numbers without saving them:node lens-check.mjs --reach 7.2 --barrel 7.8(add--back 0.3to try a set-back). -
What to expect (from
node lens-check.mjs --grid, 1 Nov, re-run for R53; rev S head with the Ø9 → Ø11 window and the disc ledge at y 15.6):Lens height Barrel Ø6 Barrel Ø7 Barrel Ø8 Barrel Ø9 6.0-6.5 mm view cut by the hole view cut by the hole view cut by the hole view cut, barrel on the hole wall 7.0 mm fits, back 0 fits, back 0 fits, back 0.05 view cut, barrel on the hole wall 7.5 mm (est.) fits, back 0.35 fits, back 0.45 (drawn now) fits, back 0.55 view cut, barrel on the hole wall 8.0 mm or more too long for the head too long too long too long “Back” is
bracket.backMm: how far the head’s camera bracket sits back (tilted 12°, the lens’s top front edge reaches further forward than its centre). The head is drawn now at 0.45 for the est. lens; a shorter lens needs a smaller number, because too far back the Ø9 hole cuts the view. An M12 lens (barrel Ø12 or more) does not fit the head. -
Send me the photos and what
node lens-check.mjsprinted. The next lab run checks the number and re-makes the head files; no head top goes to the printer before lens-check prints PASS with your numbers.
-
- Tuesday, when the card reader arrives: flash the card.
- Open Raspberry Pi Imager and choose:
- Device: Raspberry Pi Zero 2 W
- OS: Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit)
- Storage: the SanDisk card
- Click Next → Edit settings.
- On the General tab:
- hostname:
cricketone - username:
pi, with a password of your choice - Wi-Fi: your network name and password, country
US - locale: your time zone
- hostname:
- On the Services tab, tick Enable SSH with password login.
- Save, then click Yes to write the card.
- Open Raspberry Pi Imager and choose:
3. Friday 6 Nov: day one with the Pi (about 2 h, mostly waiting)
- Fit the heatsink.
- Peel the thermal pad and press it on the square chip in the middle of the Pi.
- Set the heatsink on it, with the copper shim if the kit’s sheet says to use it.
- Screw it down evenly. Don’t overtighten.
- Connect the camera with the Pi unpowered.
- Lift the black latch on the Pi Zero’s camera socket.
- Push in the narrow 22-pin end, contacts toward the board, and close the latch.
- Fit the wide 15-pin end to the camera the same way.
- Boot.
- Put the card in.
- Plug the CanaKit supply into the Pi’s PWR micro-USB port (the one at the corner).
- Wait 2 minutes. The first boot takes longer.
- Copy the software from this PC. First pull the latest from GitHub into
Documents\iemi-labs(the lab runs fix the programs until Friday). Then, in PowerShell, run:
Answerscp -r C:\Users\imran.pathan\Documents\iemi-labs\products\cricketone\software [email protected]:/home/pi/cricketoneyesto the fingerprint question, then type the Pi’s password. - Log in and set up.
- Log in:
ssh [email protected] - On the Pi:
cd ~/cricketone && bash setup.sh && sudo reboot - Setup installs about 11 libraries and takes 15-30 minutes on the Pi Zero (est.). It keeps a log in
~/setup-log.txt, which day one sends back. A “cryptoauthlib did not build” warning is fine for now: only order 2 needs it.
- Log in:
- Run day one.
-
Log in again and run:
cd ~/cricketone && bash day1.sh -
It runs, keeping going past any failure:
- the preflight;
- the camera list and a first still frame;
- all eleven self-tests;
- the card speed test;
- bench test B1 three ways: raw, without the clip, and with the clip.
It takes about 15 minutes.
-
- Send the results back. On this PC, run:
Then tell me it’s there. I’ll read every line, fix anything that broke and write the results into the design.scp [email protected]:day1-results.tar.gz $HOME\Downloads\
If the camera is not listed (02-cameras.txt says “No cameras available”):
- Power off.
- Check that both ribbon ends are straight and latched, with the contacts the right way.
- If it’s still missing:
- Run
sudo nano /boot/firmware/config.txt. - Change
dtoverlay=ov9281todtoverlay=ov9281,arducam. - Reboot.
- Run
Some OV9281 boards wire the power pins the Arducam way.
This is the first time our Python programs run on a real Pi. So far they have only been checked by the Node simulations, so expect a few small fixes on day one. That is what the result file is for.
3b. The Radxa ZERO 3W (from Tue 3 Nov, when the card reader is here)
The Radxa doesn’t need the Pi, so it can start before Friday.
- Flash its card.
- Download the latest Radxa OS CLI image for the ZERO 3 (
radxa-zero3_bookworm_cli_…img.xz) from Radxa’s ZERO 3 releases. - In Raspberry Pi Imager choose Use custom with that file, the second SanDisk card, and write it. Imager’s settings don’t apply to Radxa OS.
- Download the latest Radxa OS CLI image for the ZERO 3 (
- Set the Wi-Fi before the first boot.
- Open the card’s config drive on this PC and edit
before.txt. - Add the line
connect_wi-fi "<your 2.4 GHz network>" "<password>"and save. - The user is
radxawith the passwordradxa; change it after logging in.
- Open the card’s config drive on this PC and edit
- Do not fit the Pi camera cable to the Radxa (corrected 3 Nov 2026). Radxa’s design has a top-contact camera socket and specifies a same-side 22 → 15-pin cable, but some boards ship with a bottom-contact socket: ours did. On those, the Pi ribbon reverses every pin; ours drew enough to smell hot and dropped off the Wi-Fi (Radxa forum).
- Ours (bottom-contact) takes an opposite-side link: the 22-pin B-type cable → the 22-pin extension adapter → the narrow end of the pack’s cable → the camera. Bench only.
- First power-on with any camera on the Radxa: watch the temperature over SSH and unplug if it climbs past 70 °C.
- Radxa OS has no OV9281 overlay (only the Pi v1.3 and v2 cameras), so the camera on the Radxa stays a driver project (row R23).
- Fit the heatsink, then power the board from the USB-C supply.
- Copy the software and set up. In PowerShell on this PC:
Then on the Radxa:scp -r C:\Users\imran.pathan\Documents\iemi-labs\products\cricketone\software [email protected]:/home/radxa/cricketonecd ~/cricketone/radxa && bash setup-radxa.sh && sudo reboot - Run its day one:
cd ~/cricketone/radxa && bash radxa_day1.sh. Send back~/radxa-day1-results.tar.gz.- Set-up now builds our camera driver on the board (row R23), so it takes a few minutes longer and needs the internet. Radxa OS’s kernel for the ZERO 3W has no driver for this camera, and Radxa’s own one runs 30 fps; ours runs 120. Line
05-driverin the results should saydriver=ov9281_iemi. - Expect about 120 fps. If set-up printed
WARN: ov9281_iemi not builtorFAIL, send its output back with the results.
- Set-up now builds our camera driver on the board (row R23), so it takes a few minutes longer and needs the internet. Radxa OS’s kernel for the ZERO 3W has no driver for this camera, and Radxa’s own one runs 30 fps; ours runs 120. Line
- If the camera isn’t found: check the ribbon, then try
bash setup-radxa.sh ispand reboot. It routes the camera through the board’s image processor instead.
What’s ours here: Radxa doesn’t list this camera for the ZERO 3W, so our setup combines two of Radxa’s own configuration files:
- their Pi-camera file for the ZERO 3 (the connector’s pins);
- their OV9281 file for the ROCK 4D (the camera’s settings).
Like theirs, ours holds the camera’s power pin on, so the driver needs no patch. It hasn’t run on a real board yet.
If radxa-zero3.local doesn’t answer: the Wi-Fi line didn’t take. Find the board’s address in your router’s device list, or use a micro-HDMI cable and a USB-C keyboard.
3c. Pi and Radxa together (from Fri 6 Nov)
Both boards on the same Wi-Fi, each with its camera. This is the first test of two stumps working together.
- Link and clock:
- On the Radxa:
python3 ~/cricketone/pair_test.py --serve - On the Pi:
python3 ~/cricketone/pair_test.py --to radxa-zero3.local - Pass: round trip under 10 ms for 95 % of pings, and the clocks agree within 1 ms (est. marks from the staggered-shutter plan).
- Run it again after both boards sync their clocks to each other with chrony.
- On the Radxa:
- Two cameras, one ball:
- point both cameras at the same patch of floor about 1 m apart;
- roll the tennis ball through;
- each board writes its ball points with times, and we fit one track from the two.
- Pull one stump’s power (R38: pair check, about 10 minutes for each side):
- On the Pi:
bash ~/cricketone/pair_wifi.sh home <name> <pass>. On the Radxa:bash ~/cricketone/pair_wifi.sh far <name> <pass>. Restartcricketone-callson both. - Join your laptop to the
CricketOne-<name>Wi-Fi. It stands in for the phone. Copypair_test.pyto it; it needs Python 3 and nothing else. - Run
python3 pair_test.py --watch 10.42.0.2 10.42.0.3 --pull home. After 20 s it says pull the power on the home stump now. Pull the Pi’s power, roll a ball past the Radxa’s camera twice, and plug the Pi back in after about a minute. - Wait until it prints PASS or FAIL (up to 8 minutes; it waits for the held clips to be deleted 120 s after the Pi is back). It writes
watch-result.json. - Pass marks (est., from the R36 and R37 sims): the Radxa notes the Pi lost in under 3 s; the Radxa takes the Wi-Fi over in under 16 s; the laptop is off the network under 30 s; every call line is read once; the Pi comes back and joins; each held clip is deleted on time.
- Then run it again with
--pull far --releaseand pull the Radxa’s power. The laptop should stay on the network (under 5 s off), with no takeover. The held clips go as soon as the Radxa is back, because--releasesends the umpire’s answer. - If it notes that a clock moved over the power cut, write the number down.
- On the Pi:
- Pair time (R39, 5 minutes): with both stumps on the pair Wi-Fi as in step 3, run
chronyc trackingon each.- The keeper shows
Reference ID : 7F7F0101 ()(its own clock). The other shows the keeper’s address (10.42.0.2 or .3) and a system time within 1 ms. - Pull the far stump’s power for a minute. When it is back, its
calls.jsonlhas aclock-syncedand aclock-stepnote within about 5 s ofcricketone-callsstarting, and its lines before them say"clock": "unsynced". - Run step 1 (
pair_test.py --to) again: the clocks must agree within 1 ms.
- The keeper shows
- Clock floor (R40, 5 minutes): on each stump run
python3 ~/cricketone/clock_floor.py --selftest(7 of 7) andsystemctl status cricketone-clockfloor(active, exited).- Pull the power on BOTH stumps together for about two minutes, then plug both back in.
- On each,
cat ~/cricketone/clock-floor.json.signedshows a step forward, andcalls.jsonlhas aclock-floornote. No line after the cut has an earlier time than the last line before it. - Compare
datewith your phone: the stumps should be behind by about the time they were off plus the boot (about 2-3 min), not up to an hour. Write the number down.
- Phone time (R41, 5 minutes):
python3 ~/cricketone/call_link.py --selftestmust printall passon each stump.- Pull the power on BOTH stumps for about five minutes, then plug both back in and wait for
clock-floornotes (step 5). - From the laptop on the pair Wi-Fi, send your phone’s time to the keeper (the one whose
chronyc trackingshows7F7F0101):curl -s -X POST http://10.42.0.2:8001/time -d "{\"t\": $(date +%s%3N), \"acc\": 300, \"auto\": true}"(use .3 if that is the keeper; the laptop’s clock must be on network time). The other stump answersnot-keeper. - Pass: the keeper answers
step; within about 5 s (no balls) itscalls.jsonlhas aphone-timenote, and the other stump’s has aclock-stepnote a few seconds later.dateon both now agrees with the phone within 1 s. Write the step down. - Send it again: the answer is
ok. Send a time 10 s earlier: the answer isbehindand nothing moves.
- Pull the power on BOTH stumps for about five minutes, then plug both back in and wait for
4. The next days, one stage at a time
| Day | Stage | Pass mark | Steps (software/README.md) |
|---|---|---|---|
| Sat 7 | Live view and focus, on each board you have: run python3 live.py, open http://cricketone.local:8000/ (the Radxa’s own address on the Radxa) on your phone, aim at something 20 m away and turn the lens until the focus number on the top line peaks; lock it and write the number down |
A sharp image across the frame; the top line reads about 30 fps | 6 |
| Sat 7 | Lens calibration B1.0: 30 poses of the chessboard on its stand | PASS: lens.json written (0.05 px or less) |
6 |
| Sun 8 | The ball finder on real frames: roll and toss the tennis ball across the view with b1_bench.py --track |
Ball found in at least 95 % of frames it is in, 8 ms a frame or less | 5, 7 |
| Sun 8 | Clips, on each board you have: clip_keeper.py --trigger-every 20 for 2 minutes (row R28: the same program on the Pi and the Radxa) |
6 clips and 6 strips, no lost line; on the Radxa each clip line says frames": 300 |
13 |
| Mon 9 | Signed calls with the bench key and the phone link speed: call_link.py --keygen, --link-test, then the speed page on the phone |
Calls verify on the phone; the speed is written down (R11 assumed 10 Mbit/s) | 14 |
Things that wait for order 2: the bails, the knock and tamper switches, the cells and soak, drops and ball hits (steps 9-12 and 15). They need the sensors, the cells and the built stump.
4b. Collar bond test, before the T1 head hits (R49)
The printed cap is glued on the aluminium collar’s neck and in its bore. A ball on the head loads that joint, and no data sheet gives a number on printed PA12 that reaches the 7.98 MPa we need, so test it before any head hit. Numbers from sim/lap-r49.mjs and sim/bond-r65.mjs (docs/DESIGN.md “### R49”, “### R65”).
Prep (R65), coupons and the real cap alike: dry the printed parts 4 h at 60-70 °C; abrade the PA12 with 180 grit, flood with isopropyl alcohol for 15 s and wipe, twice, and bond within 1 h; abrade the aluminium lands with fine grit through the anodise seal (or have them masked from anodising), wipe with alcohol, bond within 4 h. Purge the first 3 cm from every new 10:1 nozzle. Hall wires on the real cap (R69): use 30 AWG silicone wire, not the 26 AWG (three 26 AWG do not pass the Ø2 hole). First clear each Hall hole (R71, Ø2.5): blow through, run a Ø2 drill by hand from the plug face up to the pocket floor, and push a Ø1 wire right through into the groove. Cut three wires 190 mm per Hall (R70: R69’s 170 was short), solder them to the Hall, thread the three down its hole and out of the collar’s open spigot, then seat the Hall on its pocket floor (marked face up) and tack it (R73): one small dot of LOCTITE 454 gel at the top of each end gap, never on the pocket floor or near the hole mouth, and nothing standing above the groove floor; wait 1 h, check the wires still slide, and only then mix the cap glue. Tack the loose cap away from the collar, so the glue’s fumes cannot cloud the lens or window. Use the three loose wires, not a cable (R72: no 3-core cable this fine passes the Ø2.5 hole and crimps into the GH plug); to thread them, tin the three bare ends together into one stiff tip and feed it down from the pocket like a needle. The hole slants back about 26° (R70), so the wires come out of the plug face already behind the camera bracket’s plate. As the cap goes home, draw the wires down lightly by hand from below (about 0.5 N, R72; the pull seats the Hall on its pocket floor, not its pins) so no loop forms under the plug face; they turn gently (a 4 mm bend or more) straight down behind the plate. Cut off the tinned 10 mm, then, after the 24 h clamp, pot each Hall pocket (R73, R74): from one fresh 10:1 nozzle of the cap’s DP8910NS (R74: not the gel; the pot sits under rain in the groove, and DP8910NS is the glue the L2 coupons soak in water), fill it from the groove over the Hall’s feet, the joints and the bare wire ends, to about 0.3 mm under the groove floor (a ball can then pull on the copper and the pot, not on the solder, and rain stays off the joints); then crimp the GH contacts below the collar (the 0.8 mm silicone insulation sits in the contact’s 0.8-1.0 mm range), then push the plug up to its place; the spare 80 mm folds once behind the plate. At the dry fit (step 6) check that the wires run behind the plate, not over it. On the real cap (R68): cut, measure and lay the pad on the crown before mixing; then put the glue on the cap’s plug and on the neck, not in the collar’s bore, and leave the plug’s bottom 1 mm bare. A bead in the bore is pushed down by the 11 mm plug onto the pad and towards the camera board 4 mm below; on the plug it is wiped up and out at the seam. Wipe the seam, then press and clamp. About 0.3 ml a cap; under 5 min from the first bead.
- L1, dry (6 coupons). Three neck rings and printed caps per adhesive (3M DP8910NS first, DP8010 Blue second; both 10:1, one gun). Prep as above, mask the ring’s end face with PTFE tape, glue the lap, cure 24 h at 20 °C or warmer. Hold the ring’s flats in a vise, put a strap wrench on the cap and turn it with the torque wrench. Pass: 52.2 N·m or more (R67: the 11 mm plug needs 6.77 MPa for ×1.5; R65’s 61.5 was for the 8 mm plug), and the break is in the plastic or in the glue, not bare on the metal. Write each number down.
- L2, wet and hot (6 coupons, the best adhesive). Three after 24 h in water (pass 41.8 N·m), three after 4 h at 60 °C, each turned within 2 minutes of leaving the oven (pass 34.8 N·m, R67; R65: the acrylics lose a third or more of their strength hot, so this is the hard one).
- L3, the real joint. Glue the first stump’s top on its collar with the winner. Hold the collar’s spigot in soft jaws, hang 23.4 kg 300 mm above the joint for 60 s (R65: 69 N·m, a 40 m/s ball on the cap). Pass: no crack, no creep over 0.2 mm. Then the T1 head hits.
- Screw lock (R66), on every bonded cap. After the 24 h cure, run an M2 tap through each of the collar’s four countersunk holes (z 683.5, R78) into the cap’s plug (R67: now 11 mm long, z 679-690), 5 mm deep, then drive four M2 × 8 countersunk A2 screws snug by hand, heads flush or just under the surface. Seal them (R74): with the same DP8910NS shot that pots the Hall pockets, put a dab on each screw’s thread and in each countersink before driving it, and wipe flush; each tapped hole crosses the glue line and ends 1.14 mm from a Hall hole, so a dry screw lets rain into the cap. No anaerobic thread locker on the plastic. R66 found that in full sun the glue line runs at the collar’s temperature (a white collar about 12 K over the air, blue 21 K), and by the data sheets the bond drops under ×1.0 above about 27 °C air; the screws keep the cap on if the glue lets go: R79 found each screw crushes the plastic before it tears out or shears (×2.1 and ×2.0 or more), so the cap stays captive on any day; with no glue the plug and screws take a full 40 m/s ball only on a cool day (×1.03 at 23 °C, ×0.67 white at 40 °C). Rock check (R79), before each match and after any ball on the cap: push the cap sideways by thumb, both ways. It must not move or click. If it rocks, the glue has let go: the stump comes out of play and the cap is re-glued (4d), never played on.
- If no adhesive passes L1, or the winner fails L2 hot, stop and note the numbers. R67 already made the plug as long as the bore allows (11 mm), so the next step would be a moulded or mechanically locked cap for production.
- Check the plug (R67). Before gluing, push the printed top home dry on a collar with the bracket and the pad in: the plug’s foot (z 679) must squeeze the pad on the crown (top z 677.5) and the cap’s skirt must sit on the collar’s face with no gap. If it rocks, the crown or the plug is long: measure the gap as in 4d step 1.
- Groove flood test (R74), after the pot and screws have cured 24 h and before the T1 hits. Stand the stump upright, lay a strip of dry tissue up the collar’s open spigot bore, fill the bail groove to the brim with water and a few drops of food dye, and top it up for 2 h. Then pull the tissue: any dye means a leak at a pot or a screw; note which side. Tip the stump: the groove should shed the rest over its ends. The full IP65 and IP67 tests are T5.
- Breather vent (R75), on the bare collar, before the bracket, camera and cap go in. At 10-25 °C, wipe the flat inside the bore (z 650-660; R77: 40° round from the back toward the button, not at the back) with isopropyl alcohol and let it dry. Wear powder-free gloves, lift a Gore VE90308 off its reel by the edge with round-tip tweezers (never touch the white membrane), centre it over the Ø3 hole and press it flat for 5 s or more with the flat end of an Ø8 dowel, firmly (Gore: 8 N, about a heavy thumb press). Look through the hole from outside with a torch: the membrane fills it and nothing of the adhesive ring shows in the hole. In the flood test (step 7) and at T5, aim the 3-minute IPX5 jet straight at the vent hole as well: any dye or water inside means it goes outside in a recess instead (docs/DESIGN.md “### R75”).
4c. T1 head hits on the camera side (R55, head from R56)
A ball on the camera window (z 662) loads the head far more than one at 650 mm: the glue line to the collar sees 118 N·m at 40 m/s (R48 used 31), and the resin window and the 1.85 mm skin over the camera-board pockets break first (docs/DESIGN.md “### R55”, sim/head-hit-r55.mjs). So these hits wait for row R56’s head (aluminium up to z 690, glass window), and always go on a spare head, after section 4b.
- Where. H-W: on the window, 662 mm up, straight down the pitch. H-P: on the pocket line, 669 mm up, 50° round from the window (each side once). Mark both with a paint pen.
- Speeds. 10 m/s, then 15, 20, 25, 30, 35, 40 m/s, one hit each at H-W then H-P. Stop at the first failed look.
- Pass marks (R56’s head as drawn, sim/head-r56.mjs, est.): glass window ×2.3, ledge ×6.42, pocket skin ×2.63: all hold to 40 m/s, and a window hit loads no glue line. Add H-C: one hit on the cap at 700 mm (joint ×2.54 with the plug glued; the plug alone ×0.35, so the cap must not turn or lift after it). With today’s printed top, expect the resin window and the pocket skin to fail at 10 m/s: that is a known result, not a test to run on the first stump’s own head.
- Look at, after every hit (head off with the hex key):
- the window: no star, crack or chip; the disc’s rim still flush (run a fingernail round it); no light between the disc and its ledge;
- round the window recess: no white ring (the ledge starting to punch in);
- over the two board-corner pockets (each side of the window): no white crazing or dent; inside, no mark on the camera board’s corners;
- the glue line (z 645 today, z 690 on R56’s head): no gap or white line; the top does not turn by hand;
- the camera:
live.pyfocus number within 5 % of before, and the image centre moved no more than 2 px (the aim check); - write the speed, the look and a photo into the T1 result file.
4d. Camera bracket aim: drop-in and tap test (R59)
The camera bracket drops into the collar and is held still by a silicone pad under the cap plug (docs/DESIGN.md “### R59”, sim/aim-r59.mjs). Do this with the first collar and the 6061 bracket (R60: a printed bracket creeps under the pad and is too soft for the tap test), before the bail-off test.
-
Pick the pad. Drop the bracket in, press the cap on dry (no glue) and measure the gap from the crown top to the plug with feeler gauges or a depth gauge. Measure each sheet with calipers in three places (a sheet may be ±0.3 mm off its label, R62) and take the sheet whose measured thickness is the gap plus 15-30 % (squeeze = (sheet - gap) / sheet). If none lands in it (gaps of about 1.28-1.40 or 1.70-1.75 mm), lay a 0.1 or 0.2 mm shim half ring under the pad and pick again. Cut the pad with the compass cutter set to Ø31.4 and then Ø27.6 from the same centre, sheet taped to a cutting mat, several light turns; halve it on a straight edge laid 3 mm behind the centre, parallel to a diameter, and keep the bigger half (R63: a centre cut overhangs the crown’s front ends). Use 50A sheet whose test report gives compression set at 22 h / 175 °C of 25 % or less (R61, R62). Write the pad’s thickness, any shim and the squeeze before fitting.
-
Ten drop-ins. Fix the collar in a vee block with the camera running (
live.py) and a printed target 3 m away. Ten times: lift the bracket out, drop it back, press the cap on with the printed press (R64,build/cad-s/press/stl/, PETG, no supports): the collar stands on press-foot with the camera ribbon and 6-pin plug out through its tunnel, press-cup goes on the dome with its key in the bail groove (the groove shows through both end windows), and a 6 in one-hand bar clamp squeezes the two firmly (300 N or more; the parts take the clamp’s full 1,334 N). Check the cap sits down at the front and the back with a 0.05 mm feeler (R63: the pad pushes behind the axis and a lighter press lets the back lift), save a frame. For the bonded cap, glue as in 4b (glue on the plug, not in the bore, R68). Leave the clamp on the same way while the bond cures: 24 h at 20 °C or warmer, 48 h below that, and do not move the head before then (R65; the glue and prep are in section 4b). Write the target centre (x, y) and its turn for each. Pass: every frame finds the target within 15 px of the first (the re-aim’s room, R59 says up to 15 px). -
Tap test. With the cap clamped, tap the collar with a pencil from the front, back and both sides (well under a knock: the motion sensor must not saturate). Pass: the target centre moves no more than 0.16 px (0.01°) from before to after, on the average of 30 frames.
-
Repeat step 3 after the soak test, to see whether the pad has relaxed in the heat. Then take the cap off, measure the pad’s thickness where it was squeezed, and measure the dry gap again (R61). If the pad has lost more than 0.1 mm, cut a new one by step 1 before T1 (R61 expects under 0.01 mm of set after one soak). Write all of it into the B1 result file.
-
Loose-cap check (R80), with the cap pressed on dry, before it is glued. Run
python3 capcheck.py --selftest(must print12/12 ok). Then, withlive.pyand the target as in step 2: rock the cap by thumb 10 times (it rocks on its glue gap) and save a frame: the target must not move more than 0.16 px (the camera does not see a rock, R80). Back the press off and shim the cap up 0.3 mm, then 0.6 mm, with feeler gauges under the skirt, tap the collar each time and save 30 frames: write how far the target moved. R80 expects nothing at 0.3 mm and a jump well past 0.8 px (0.05°, the flag) at 0.6 mm; if the target moves at 0.3 mm, the pad is too thin (step 1). -
The cap check in the match program (R81). On the Pi, run
python3 track2d.py --selftest,python3 capcheck.py --selftest,python3 call_link.py --selftestandpython3 stump.py --selftest(the last must printpass R81 cap-hit overandall pass). Then start the three services, knock the stump with a rubber mallet with no ball, and check the app (orcurl http://<stump>:8001/calls) shows aknockline and, about 1 s later, anaskline with"result":"check-cap"and reasonknock-alone. Answer it withcurl -X POST -d '{"firm": true}' http://<stump>:8001/cap: anotelinecap-firmfollows. -
The re-aim offset (R82). On the Pi run
python3 clip_keeper.py --selftest(the sixR82lines must printPASS, thenall pass). Stand B1.15’s printed far wicket 20 m away (or as far as the room allows) in view, runpython3 clip_keeper.py --set-aim auto(R83: it finds the three tops in about 2 s and prints them; check they sit on the stump tops inlive.py; if it says “far wicket not found”, read the tops’ pixels offlive.pyand runpython3 clip_keeper.py --set-aim U,V U,V U,V) (it writes"aim"intolens.jsonandaim-patches.npy). Start the services, knock the stump with the mallet and readclips/clip-log.jsonl: areaimline withoffunder 0.05 andstoredtrue. Then shim the cap 0.6 mm as in step 5, knock again: areaimline withoff0.05 or more and anaim-jumpask in the app; take the shim out, knock, answer “firm”: a second steadyreaimline and the ask clears. Write the frame time of eachreaimline (the Pi Zero 2 W’s cost per frame). R84: while the far wicket stands there, hold a white PA12 offcut, a blue-painted offcut and a piece of the cyan stripe vinyl beside it in turn, and write the grey levellive.pyshows for each and for the background into the B1 result file. R85: with the printed far wicket’s bails on,--set-aim automust also print “the marks are the stump tops” and the true tops (check them onlive.py); take the bails off and run it again: on a white background it must print “cannot tell” (unsure), never “lower edge” or “lower down”. Write both answers into the B1 result file. R86: with the bails on, dim the room lights to about a fifth and run--set-aim autoagain: it must print a sure answer (top or edge) with the right tops onlive.py, or “cannot tell” (unsure), never wrong tops; write the answer and the seconds it took (about 7 s at worst, est.) into the B1 result file, then leave the aim stored, dim the lights and check onlive.pythat the re-aim (R82) still holds the far stumps. R87: stand a black card behind the printed far wicket, lights at about a fifth, and run--set-aim auto: it must print “the marks are the stump tops” with the right tops onlive.py, or “cannot tell”, never wrong tops. Then store the aim in full light, dim the lights to a fifth and knock the stump: thereaimline inclips/clip-log.jsonlmust still show the marks found andoffunder 0.05 (R87 made the patch match allow for the light; before it, dimming lost every mark). Write the grass-card grey levellive.pyshows in full light into the B1 result file (R87 assumes 105). R88, when the light goes: runpython3 stump.py --selftest(it must printpass R88 the light falls). With the aim stored and the services running, dim the room lights in steps: the heartbeat’slightin the log must fall with them; at about a third of the start the app must showlight-set-aimandlight-falling, under a fifthlight-low, and a ball rolled past the stump must come asunsurewith"why": "light"and a clip; lights back up:light-backand sure calls again. A bail lifted in the dim light must still turn the light red at once. Write the light values at each step into the B1 result file. R89, the light from the camera itself:python3 stump.py --selftestmust also printpass R89 heads and grass apart. Run the B1 frame tests withclip_keeper.py --no-gain-loop(4x held). In full light, write the far heads’ grey level at 4x fromlive.pyinto the B1 result file (R89 assumes 225). With the loop on, dim the lights in steps: the heartbeat’sgainmust rise (up to 15.5) whilefieldfalls with the lights, and the heads must never read over 230 onlive.py. Lay a brown card over the grass patch under the far wicket in full light:lightfalls,fieldholds, and after 30 s alight-checknote reaches the app; no ball call changes. -
The exposure in poor light and the time stamp’s edge (R90). On the Pi run
python3 stump.py --selftest(pass R88 the light falls, thenall pass). (a) The stamp’s edge: point the camera at an LED driven from a GPIO pin, run it at 800 µs, switch the LED on at a logged time and find the first frame that shows it fully lit and the last dark one: if the frame times sit about 800 µs after the switch, the board stamps the exposure’s end (STAMP_EDGE“end”, R90 assumes this); if about 0, its start: changeSTAMP_EDGEincamera.pyand write it into the B1 result file. Do it on both boards. (b) With the aim stored, dim the room lights to about a tenth in steps: the heartbeat’sexposuremust rise to 800 beforegainrises past 4. Roll a red ball across the far wicket at each step withlive.py: write the light at which the tracker loses it (R90 expects about 0.05x; R89 was 0.3x). -
Setting the aim in poor light (R91). On the Pi run
python3 stump.py --selftest(pass R88 the light falls, thenall pass). Stand B1.15’s printed far wicket in view with the room lights dimmed to about a tenth and runpython3 clip_keeper.py --set-aim auto: it must first printbrightness set:with the exposure near 800 µs (the gain after it), then the three tops (check them onlive.py) or “cannot tell”, never wrong tops. Write the exposure, gain and seconds into the B1 result file. Then, lights still at a tenth and the services running, knock the stump with the mallet: thereaimline inclips/clip-log.jsonlmust show the marks found andoffunder 0.05 withstoredtrue. Film the knock with the phone at 240 fps beside the stump and write how long the stump rings and how far its top moves (R91 assumed 12-40 Hz and up to 0.6°, est.). -
The set-up against a bright sky (R92). Hang a bright window, a lamp on white card or a phone torch in the top fifth of the picture, above the printed far wicket, with the room lights at about a tenth. Run
python3 clip_keeper.py --set-aim auto:brightness set:must show the exposure near 800 µs as in step 9 (not held near 100 µs by the bright patch), then the three tops or “cannot tell”, never wrong tops. Movelens.jsonaside and run it again to see the old whole-picture metering hold the brightness back; write both exposures and gains into the B1 result file. On the pitch at B1, photograph a grey card against the evening sky behind the far wicket to measure the real sky-to-grass ratio. -
The bail line’s floor from the picture’s own noise (R93). With the printed far wicket and wooden-coloured card bails on a white card screen, run
python3 clip_keeper.py --set-aim autoin room light and again with the lights at about a tenth and a bright lamp in the top of the picture: each look prints its noise (sd, grey levels) and the bail line’s floor. Write both into the B1 result file. The tops must come out sure or “cannot tell”, never wrong; then cover the lens and take 30 dark frames to see the sensor’s fixed-pattern noise (R93 open question 1). -
A busy background behind the far wicket (R94). Stand B1.15’s printed far wicket in front of a busy poster, a bookshelf or a printed crowd photo (no white card behind it) and run
python3 clip_keeper.py --set-aim auto: it must say “cannot tell” (tap the tops in the app), never give tops. Then put the white card screen back: it must find the tops as in step 9. On the pitch at B1, photograph the real sight-screen, the boards and the mown grass behind the far wicket so the busy measure can be checked on them (R94 open question 1). -
Tapping the far tops (R95). With the busy poster still behind the printed far wicket, read the middle top off live.py on a phone zoomed to about 32 px across, then run
python3 clip_keeper.py --set-aim tap:U,Vwith that pixel: it must print the three marks to confirm (a busy background), and store them only with--confirm. Then try wrong taps: the next stump (U + 4.5), a gap (U + 2.2) and a head-length high (V - 5.5): each must say why and store nothing. With the white card back, a tap must store at once from the bails. Note each tap’s distance from the true top: it gives our scorers’ finger spread (R95 open question 2). -
A light background behind the far wicket (R96). Hang a pale poster or a white bed sheet behind the printed far wicket (no sight-screen card) and tap the middle top as in step 13:
--set-aim tap:U,Vmust print the three marks found from “tubes in a light crowd” to confirm, and store them only with--confirm. A tap a head-length high (V - 5.5) and the next stump (U + 4.5) must store nothing. Photograph the painted stump’s head edge close up (it must be sharp and square to the tube) and write the grey levels of the blue tube, the white head and the sheet into the B1 result file (R96 open questions 2 and 3). -
A mixed background behind the far wicket (R97). Hang a poster with both very light and very dark patches (a printed crowd photo in strong light, or a black-and-white checked cloth) behind the printed far wicket and tap the middle top as in step 13, also 2 px to each side of it: each must print the three marks to confirm (found from “heads across the tap” or “tubes across the tap”, or the heads or tubes alone), on the stumps and not in the gaps, and store them only with
--confirm. The next stump (U + 4.5) must show its marks a whole stump to the side, so the scorer sees it and refuses. Write in the B1 result file how many of 10 taps set the aim. -
Wrong taps in a crowd (R98). With the mixed poster still behind the printed far wicket, tap the next stump (U + 4.5 and U - 4.5), a gap (U + 2.2) and 3 px high and low: each must store nothing and say why (“two wickets a stump apart could fit” or “cannot see three white heads”). Then tap the middle top 10 times: write in the B1 result file how many set the aim, the “how” each printed (heads, tubes, or both, agreed across the tap), and how many places across the tap passed, so we can check the 2 px spread on the real camera.
-
One more look after a refusal (R99). With the mixed poster behind the printed far wicket, cover one outer stump’s head with a finger-sized grey card so the tap is refused, then pull the card away within half a second of tapping:
--set-aim tap:U,Vmust wait about 1 s, look again and print the three marks to confirm. Leave the card on: it must still store nothing. Then tap the middle top 10 times while someone slowly moves the poster: write in the B1 result file how many set on the first look, how many on the second, and the time each took. -
The scorer’s second tap (R100). With the mixed poster behind the printed far wicket and the grey card on one outer head, tap the middle top: it must be refused. Pull the card away and tap again:
--set-aim tap:U,Vmust look twice, print the three marks and ask to confirm (it must never store a crowd tap without--confirm). Put the card back and tap three times: after the second refusal it must print what to change (the sight-screen, or set the aim between overs). Tap the next stump (U + 4.5) three times in a row: nothing stored. Write the seconds between your taps into the B1 result file (R100 open question 2). -
A still crowd in poor light (R101). Store an aim first on the plain grey card (step 7). Then pin the mixed poster still behind the printed far wicket, dim the lamp to the 0.05x step and tap the middle top three times: every tap must be refused, and after the second refusal it must print what to change and “the last stored aim stays in use”. Delete the
aimentry from lens.json and tap three times again: it must print “no aim seen in the last 34 min: ball calls stay unsure until an aim is set” (R102). Nothing is stored either way. -
The tube in the sun (R102). Stand the bare tube upright in the base on the bench with the camera bracket in its collar and an aim stored on the printed far wicket. Shine a 500 W work lamp on one side from 0.5 m for 20 min, then switch it off for 20 min: the log must show a re-aim every 60 s (“reaim” lines, every one stored, none over 0.05°), the switch-off must start a re-aim at once and then one every 10 s, and lens.json’s aim must show a new “seen” time. Write the largest offset the re-aims read and the tube’s side temperatures (a kitchen thermometer on each side) into the B1 result file, so we can check the bend against the sim.
-
The roll from the crease (R103). Tape a 25 mm white line across the floor 1.22 m in front of the stump on a green mat (the near popping crease), store the aim, and write the line into lens.json under aim “crease”: its slope and its v at the picture’s centre, read off
live.pyat the left and right ends. Lean the stump 0.5° sideways with a 1.7 mm shim under one side of the base: the next re-aim must send a heartbeat “roll” of about 0.5° (the log’s “roll” lines), and back near 0° when the shim is out. Stand two white-sock feet on the line and repeat: the roll must stay within 0.02°. Write each roll read into the B1 result file, so we can check the crease read against the sim. -
The tracker uses the roll (R104). With step 21’s taped line and the 1.7 mm shim in, wait for a re-aim, then roll a white ball across the mat 3 m in front of the stump. In the log, the ball’s points must be turned by the heartbeat’s “roll” (about 0.5°) before the track fit (
track2d.unroll). Then runpython3 stump.py --selftest: “R104 the roll too old” must pass (a ball 35 min after the newest roll is unsure with why “roll”). Write the roll and the track’s line with and without it into the B1 result file. -
The far end’s roll over the pair link (R105). With both boards on the pair Wi-Fi (
call_link.py --partneron each) and step 21’s 1.7 mm shim under one stump only, wait for a re-aim on each. The other stump’s log must show a “partner-roll” of about 0.5° with its age in s, androll.jsonmust hold the shimmed stump’s own roll. Switch the shimmed stump off: 34 min later its partner’s both-ends ball calls must turn unsure with why “partner-roll”. Then runpython3 call_link.py --selftest,python3 track2d.py --selftestandpython3 stump.py --selftest: the R105 lines must pass. Write the rolls each stump saw into the B1 result file. -
The ball points over the pair Wi-Fi (R106). Re-run
pair_wifi.shon both boards, theniw dev wlan0 get power_saveon each must say “off”. Roll a white ball across the mat in front of both stumps 50 times: each stump’s log must show the partner’s parts for every ball (11-12 parts, each twice, the last with “last”), and the ball messages must say “ends”: 2 on at least 49 of 50. Then pull the access point’s power for 5 s while a ball is rolled: that ball must say “ends”: 1 within 0.1 s of its own track. Runpython3 track2d.py --selftest: the “pair_parts” line must pass. Write how many parts came late or not at all into the B1 result file, so we can check the link model. -
A hole in the far track (R107). In step 24’s 50 balls and the soak test, every ball message that says “hole” must also say “ends”: 2, and its hole must never be 0 or the last part. Write each such ball’s hole and line into the B1 result file, with how many balls fell back to “ends”: 1, so we can check how often one part alone goes missing on the real link. Run
python3 track2d.py --selftest: the “partner hole” line must pass. -
The call’s sigma with the roll and the clock (R108). For every sure ball in step 24’s 50 balls and the soak test, write the fit’s own sigma (line, height) and
track2d.call_sigma()with each camera’s roll and clock term into the B1 result file, with how many balls the larger sigma made unsure. On the bench pair, also write each camera’s measured roll spread and clock spread, so the 0.013° and 0.05 ms intrack2d.SUREcan be replaced with measured values. Runpython3 track2d.py --selftest: the “call sigma” line must pass. -
The swing that changes in flight (R109). For every sure ball in step 24’s 50 balls and the soak test, write the fitted swing change (m/s³), the drift term and
track2d.call_sigma()with it into the B1 result file, and how many sure balls fall outside their own 2 sigma (about 1 in 20 expected). Rolled balls barely swing, so the real spreads fortrack2d.SUREcome from the B2/B3 ball-cannon flights. Runpython3 track2d.py --selftest: the “model terms” line must pass. -
The fit’s time on the brain (R110). For every ball in step 24’s 50 balls and the soak test, write the fit’s and sigma’s time (ms), the iterations and the flight steps into the B1 result file, with
track2d.FIT(2 ms flight step, stop once a step gains under 0.01 %) and the camera loop running.track2d.fit_budget_ok()must pass for every ball: 800 ms leaves the call on the lights and in the app within 1 s. The sim expects about 164 ms a ball and 381 ms at worst (est.); if the Pi is slower, share the Jacobian columns over three cores first. Runpython3 track2d.py --selftest: the “fit time” line must pass. -
The numpy flight’s time on the brain (R111). Run
python3 track2d.py --fittimeon the Pi with the camera loop running: it flies 13 tracks together withtrack2d.fit_flight()and prints the time of one batched step and of a ball (6394 batched steps, the P1 sim median). Write both into the B1 result file. The sim expects about 0.8 ms a step and 5.2 s a ball (est., 8 us a numpy call), over the 800 ms budget, which is why R112 moves the step loop into C; if the Pi does a ball in under 800 ms, numpy is enough and the C step is not needed. -
The C flight (R112).
setup.shbuildslibflight.sofromflight.c(day1.sh builds it if set-up did not).python3 track2d.py --selftestmust print “flight C: 13 tracks … largest difference from numpy” under 1e-9; if it says “not built”, rungcc -O2 -ffp-contract=off -shared -fPIC -o libflight.so flight.c -lmin the software folder and send the error back.--fittimethen prints the C flight’s time a track step and a ball (33,366 track steps, the P1 sim median). The sim expects about 1.45 us a step and 49 ms of flight a ball (est.); pass mark: under 400 ms, half the budget. -
The whole fit (R113).
python3 track2d.py --selftestmust print “fit ball: … call off by” under 2 mm and “sure True”.--fittimethen prints “fit ball bench”: the time of one wholefit_ball()on the Pi, with its flights, reads and solves. Write it into the B1 result file. The sim expects about 70 ms a ball (median) and 167 ms for the worst (est.); pass mark: under 400 ms, half the budget. If the self-test fails, send its line back: the fit then waits for the fix, the flight and the cameras do not. -
The first guess (R114).
python3 track2d.py --selftestmust print “first guess: bounce” within 2 ms and “fitted from it: … call off by” under 3 mm with “sure True”: the stump now fits a ball from its own looks alone.--fittimethen prints “first guess bench”: write it into the B1 result file. The sim expects about 48 ms a ball (median) and 114 ms for the worst (est.), 123 ms median with the fit; pass mark: guess and fit together under 400 ms. If the self-test fails, send its line back. -
Wrong looks (R115).
python3 track2d.py --selftestmust print “false looks: guess dropped” with “(all 6 false True)”, “call off by” under 3 mm with “sure True”, and “squeezed gate: … sure False”. In the B1 and B2 videos, note every frame where the finder’s look is off the fitted track by more than 2 px (fit_ball()returns how many it dropped): that count replaces the set rates the sim used. -
A false look close to the ball (R116).
python3 track2d.py --selftestmust print “near look: clean sigma x” under 1.5 with “call unmoved True”, and “one look 1.2 px off: sigma … ->” with the second pair at least the first. In the B1 result file, write each ball’s sigma with and without the leave-one-out term (fit_ball(..., fit=dict(FIT, loo_sigma=False))): on real looks the two should be close; a ball where they are far apart had looks the fit leaned on. -
A block of spoiled looks (R117).
python3 track2d.py --selftestmust print “block: clean sigma x” under 2.5 with “call unmoved True”, and “a sleeve on 4 looks: sigma … ->” with the second pair at least the first. In the B1 result file, write each ball’s number of blocks (fit_ball()['work']['blocks']) and its sigma with and without the block term (fit=dict(FIT, block_sigma=False)); on real looks the two should be close. -
Bring back the real looks (R118).
python3 track2d.py --selftestmust print “bring-back: clean 0 back, call unmoved True” and, with the false looks, “sure True” with the call off under 3 mm. In the B1 result file, write each ball’s looks the guess dropped (first_guess()['dropped']) and brought back (fit_ball()['work']['back']); on a clear bench view both should be 0 or close. -
A guess that keeps the real looks (R119).
python3 track2d.py --selftestmust print “guess loose to tight: clean dropped 0” and, with the false looks, “all dropped True” and “sure True” with the call off under 3 mm. In the B1 result file, write each ball’s guess rounds (first_guess()['rounds']) and the looks it dropped; on a clear bench view both should be 0. -
Say why a call is unsure (R121).
python3 track2d.py --selftestmust print “unsure reasons: clean ball sure with none True, 9/9 made-up results named right”. In the B1 result file, write each unsure ball’s reason (unsure_reason(fit_ball(...))) next to its looks after the bounce (after), distance at the last look (last_y) and looks left out (hidden); on a clear bench view no ball should say “clutter”. -
Did the clutter tip the call (R122).
python3 track2d.py --selftestmust print “clutter tipped: ball 30 clutter (seen 12.6 mm), ball 51 wide (seen 14.5 mm)”. In the B1 result file, write each unsure ball’ssig_fillandseen_sigma(res, 0)/seen_sigma(res, 1)next to its reason; on a clear bench viewsig_fillequalssig_fit(nothing left out). -
The clutter margin on fresh balls (R123).
python3 track2d.py --selftestmust pass_tip_selftestwith the floor case (a filled sigma far under the count’s says “wide”). In the B1 result file, writelooksandhiddennext to each unsure ball’s seen sigma; on the app and the watch the clutter reason now reads “on the edge, and clutter hid frames: with a clear view it may have been sure”. -
The B1 result file reads back (R124).
python3 track2d.py --selftestmust print “B1 row: 24 fields True, sure True, why None, hidden 0, sig_fill = sig_fit True”. With looks from both ends (pair_test.py), runpython3 b1_bench.py --balls looks.json --no-camera(or add--ballsto the B1 run), copy b1-result.json off the Pi and runnode products/cricketone/sim/b1-read-r124.mjs b1-result.json: 0 fields missing, and the reader agrees on every reason and seen sigma; on a clear bench view clutter, fillNotFit, guessDropped and broughtBack are 0, and looFar and blockFar are about 10-20 of 100 (the sim’s clear view, est.). -
The bench pass marks (R125).
node products/cricketone/sim/b1-read-r124.mjs b1-result.jsonnow ends with one line. On a clear bench (no false looks in view) with at least 50 called balls it must print “B1 bench: PASS”: per 100 called balls, guess drops 2 or fewer, looks left out 4 or fewer, bring-backs 2 or fewer, clutter 2 or fewer, leans on one look 28 or fewer, on one stretch 31 or fewer (marks from the sim, est.; the leave-one-out ratio at the 95th percentile is printed but no longer a mark, R126). A FAIL names the marks: drops or looks left out mean something in view looks like a ball (clear it and run again); one look or one stretch means the fit is worse than the sim on clean looks (check the camera noise and timing first). Write the real clear-bench rates into docs/DESIGN.md “### R125”. -
Camera noise the fit was not told (R126). The same reader now prints “mark rmsOver”: the balls per 100 whose fit rms is over 1.1x the told 0.25 px. On a clear bench it must be 5 or fewer (0-2 in the sim; 16-27 when the stump rolls four times what the fit is told, est.). Over 5 with no clutter marks failing means the camera noise or the roll is not what the fit is told: redo the roll from the crease (steps 21 and 23) and the ball finder’s centroid noise from
b1_bench.py --track, set the fit’s told noise and PX_TOLD in sim/b1-read-r124.mjs to the measured value together, and run the 50 balls again. Twice the told roll is not seen in 100 balls; it costs a few sure calls, not wrong ones. Write the clear-bench rmsOver into docs/DESIGN.md “### R126”. -
Measure the finder’s centroid noise (R127). Before the 50 balls, put the ball on a thin stand (a golf tee in modelling clay) at the distance that gives a 3 px ball (about 20 m), lit as for the balls, and run
python3 b1_bench.py --still --no-encode --frames 0 --out b1-still.json. It asks 8 times to take the ball off (64 frames) and put it back a few mm from the last place (250 frames), keeping clear of the view. It prints each hold’s spread and writespx_told(K 1.1 x the spread, est.). One hold can be 20 % off, so do all 8; at least 4 holds must keep half their frames. Run it in bright and in dull light and keep the larger. Then fit the 50 balls with--px <px_told>(or run--stilltogether with the balls); the reader uses the file’s px_told for the rms mark and says which it used. Write the measured px_told for both lights into docs/DESIGN.md “### R127”. Swing the ball (R128). A real ball’s shine pulls the found centre toward one pixel, which a ball held still never shows (it read the noise up to half too low in sun), so the step now swings it by default: tie the ball to about 1.1 m of thread from a garden cane laid over two chair backs, at the same 20 m, and swing it round in a circle about 0.36 m across (one turn in about 2 s; give it a fresh push every hold). The program follows the ball and writesswing: true, K 1.02 (est.). Keep the cane and chairs out of the 15 x 15 px round the ball. Run it in bright and in dull light and keep the larger (the dull one, in the sim).--heldkeeps the old still-on-a-stand hold for a comparison; write both numbers into docs/DESIGN.md “### R128”. -
Measure the lean toward the sun (R129). On a sunny day, with the ball still on its stand at about 20 m (a 3 px ball), run
python3 b1_bench.py --still --held --no-encode --frames 0 --out b1-lean-sun.json(at each “put the ball on the stand” prompt leave it where it is and press Enter), then, without moving the ball or the stump, hold a board so it shades the ball (not the camera) and run it again with--out b1-lean-shade.json. The difference in the mean centre is the lean (about 0.9 px with the sun to the side, est.); LEAN_K = lean / (D x |s|), wheresun_lean(u, v, D, az, el, cam_az, tilt, k=1)gives D x s from the clock, the place and the stump’s heading. Both files now hold the run’smean_px(the lean is the sun run’s minus the shaded run’s) and itslight(the grass’s grey with the ball off, exposure, gain, grey per exposure x gain and the UTC time). Row R130: the grass calibration forsun_share()is the sun run’s per_exp_gain / ((0.1 + sin el) x clear_beam(el)), el fromsun_dir()at its time (est.; on a clear day only). Write LEAN_K and the calibration into docs/DESIGN.md “### R129” and “### R130”. Until both are set the tracker never corrects. -
The partial-cloud lean and the pitch heading (R130).
python3 track2d.py --selftestmust print “share: g(0.25) 0.509, full sun 1.000, cloud 0.000; heading 212 True, 32 False”. On a day of broken or thin cloud, with the ball still as in step 45, run the sun run three more times as the light changes and note, for each, the step between lit grass and a person’s shadow on the grass in view (stand 5 m to the side of the ball, in view):sun_share()from that step and the lean (mean_px against the shaded run) should followlean_share()(half the full lean at a quarter share, est.). For the heading, in the app set the pitch outline on the map at the ground, then lay the phone flat on the pitch along it 1 m from the stumps: the app takes the map’s heading and the compass only picks the striker’s end; if they differ by over 15° it asks again (steel nearby or the outline is on the wrong strip). Write the shares, leans and the two headings into docs/DESIGN.md “### R130”. -
Find a person’s shadow on the bench lawn (R131).
python3 track2d.py --selftestmust also print “shadow: half sun reads 0.5” (within 0.1), a cloud reading under 0.1 and “sweater alone None”. Then on a sunny or thinly clouded day, with the stump at the end of the bench lawn as for step 45 and the pitch heading set in the app (step 46), have one person in a white top stand 1 m in front of the camera and shuffle for 30 s while the stump runsShadowFinder.step()on the 4 x 4 binned Y plane (2 frames a second) withsun_dir()and the heading. It should read a shadow in most frames (94 % in the sim, est.) andsun_share()from its lit and shadow greys should agree with the shaded-board step of step 46 within 0.15. Then lay a dark sweater and wet a patch of the lawn 2 m to the side with nobody in view: it must read no shadow. Have the person stand with the sun straight behind the camera too and note how often a shadow is found (71-84 % at the bowler’s end in the sim). Write the found share, the agreement and the sweater and wet-patch results into docs/DESIGN.md “### R131”. Keep the share (R132). The self-test must also print the “hold:” line (kept 0.5 told 0.15, a cloud under 0.3 told 0.3, 2 min later 0.8 told 0.4). Then have the person step out of view for 60 s whileshare_hold()runs on the grass grey each second: on a steady sky the share must stay within 0.05 of the last read; when a cloud comes over (or the shaded board is held over the camera’s patch of grass), the held share must fall with the grass and agree with the shaded-board step of step 46 within 0.3, and with the fresh read when the person steps back in. Write the held and re-read shares into docs/DESIGN.md “### R132”. -
The 2 s mean of shadow reads (R133).
python3 track2d.py --selftestmust also print “mean: share 0.02 grass 100.0 from 4 reads in 2 s”. On a fully overcast day, with a person in a white top shuffling 1 m in front of the camera on the bench lawn as in step 47, runShadowFinder.step()andsun_share()for 60 s and log each read andshare_mean()of the last 2 s: the single reads should sit at 0 to a few hundredths, the mean under 0.03 (sim: corrects 1.3 mm at the 95th percentile instead of 2.2 mm). Write both into docs/DESIGN.md “### R133”. -
Set LEAN_K tighter, and read the heading from a shadow (R134).
python3 track2d.py --selftestmust also print “floor: lean_k 0.318 from alternated holds; shadow heading 3.0 sd 0.53, with the map 2.8”. Then redo step 45 in one run, not two:python3 b1_bench.py --still --held --no-encode --frames 0 --out b1-lean-a.jsonand at the prompts hold the board over the ball for holds 2, 3, 6 and 7 only (sun, shade, shade, sun, sun, shade, shade, sun:LEAN_HOLDS), so the aim’s drift as the sun warms the stump cancels. Turn the ball 180° on its stand (its other side to the sun) and run it again (b1-lean-b.json). Do both in the morning (sun about 25° up) and again near midday. Give the four runs tolean_k_fit()with each run’ssun_lean(..., k=1): the runs should agree within 5 % and K is their mean (sim: 6.4 % as step 45 was written, 2.5 % this way). A lawn of only 10 m is fine (the lean is 1.8 px, sim 3.2 %); note the distance. For the heading, if a plain set of stumps is to hand, stand it 10-20 m down the lawn in the camera’s view on a sunny morning, read its shadow tip’s offset across the camera’s line in mm (25.7 mm a px at 20 m) and give it tostump_shadow_heading()withsun_dir()and the phone map’s heading of the lawn line: it should agree with the map within 2°. Write K, its spread and the heading into docs/DESIGN.md “### R134”. -
Fill the lean table, and read the new ball’s brightness (R135).
python3 track2d.py --selftestmust also print “shape: k at c -0.296 0.359 (in True), behind the camera 0.294 (in False); 80-over shine 1.049”. On the step 45 stand on a sunny day, turn the stump so the sun is at five angles to its camera: nearly behind the ball (sun about 45° off the line of sight on the ball’s far side), just behind it, square, just behind the camera and nearly behind the camera. For each, one alternated run as step 49 (b1_bench.py --still --held --no-encode --frames 0 --out b1-shape-<n>.json) andsun_c()for the ball’s place. Each run’s K oversun_lean(..., k=1)is one row ofLEAN_TABLE(c, k);LEAN_TABLE_REFis their mean. The sim’s table peaks with the sun just behind the ball (0.357) and falls to 0.27-0.29 both ways; the bench’s table should have the same shape. Then read the new ball’s brightness over the grass (the finder’s sum over the threshold over its size, mean of 250 frames) forLEAN_BRIGHT0, and again with a ball from a used box (est. 40-80 overs) to checkLEAN_SHINE_B(sim: about 0.7 % a grey level). Nothing bought. -
Two more sun angles, and the new ball (R136).
python3 track2d.py --selftestmust also print “terms: both ends square …, sun behind one …; shine old ball 1.049, new ball 1.000”. On the step 50 stand add two more angles: the sun far behind the ball (about 30° off the camera’s line of sight on the ball’s far side, c about -0.85) and far behind the camera (c about +0.85), one alternated run each (b1-shape-6.json,b1-shape-7.json); add both rows toLEAN_TABLE(sim: balls past the table 11 % -> 3 %,sim/terms-r136.mjs). Glare from a sun in front of the camera is expected at c -0.85: note whether the finder still finds the ball on every frame. Then feed a used ball’s brightness for one over into aShineWindow, callnew_ball()and checkfactor()is 1.0 before the new ball’s first over ends. Write the two rows into docs/DESIGN.md “### R136”. Nothing bought. -
The sun in front of the camera (R137).
python3 track2d.py --selftestmust also print “glare: near the sun’s clip True, away False, own clipped pixels False; told 0.250 / 0.385 px”. On a sunny late afternoon, turn the step 50 stand so the sun is low in front of the camera, about 10° outside the picture, and roll a white and then a red ball across the lawn 10-20 m away, 20 times each:b1_bench.py --still --no-encode --frames 600 --out b1-glare-<ball>.json. Log the finder’s found rate and, for each look,look_px(peak, thresh)andin_sun_clip(). Then turn it until the sun is just inside the top of the picture and roll again. The sim says the ball is found on 91-98 % of looks and only looks next to the sun’s image are clipped; note any bright spots that move when the stand turns (lens ghosts, not modelled) and whether the finder takes one for a ball. Wipe the window, then mist it with water and roll again (the wet case). Write the found rates and the clip into docs/DESIGN.md “### R137”. Nothing bought. -
The sightscreen in full sun, and the short frame (R138).
python3 track2d.py --selftestmust also print “clip exposure: on the clipped screen 50 us, on grass 100, own clipped pixels 100, no window 100”. On the step 52 afternoon, turn the stand so the sun is behind the camera and a white board (a sheet of white-painted ply about 1 x 1 m, standing in for the sightscreen) is in full sun 10-15 m in front of it. Roll a red ball across in front of the board 20 times at the set exposure and 20 times withwindow_exposure()choosing each frame:b1_bench.py --still --no-encode --frames 600 --out b1-screen-<way>.json. Log, per look, whether the window was clipped, the exposure used, the finder’s found rate andlook_px(). The sim says about 98 % of looks over a sunlit white screen clip at the set exposure and none at half; note how quickly the camera takes the new exposure (it must be the very next frame) and whether the ball is found in that frame. Then roll the red ball with only the blue sky behind it (the board laid flat) and log its contrast to noise (sim: about 7-8, found on 59-70 % of looks; R139 looks at it). Write the found rates into docs/DESIGN.md “### R138”. Nothing bought. -
The red ball against the sky, and a filter (R139). First find which filter the module has: point a TV remote at the camera in a dark room and press a button; a bright flashing spot in
b1_bench.py --stillmeans no IR cut (write “IR cut: no” in build/cad-s/lens.json’sabout), nothing means it has one. Then, on the step 53 afternoon with the sun behind the camera, roll a red ball 20 times with only the blue sky behind it (as step 53’s last part) and 20 times with a piece of orange glass, orange acrylic or an orange photo filter held flat over the window, if one is to hand (do not buy one for this):b1_bench.py --still --no-encode --frames 600 --out b1-sky-<way>.json. Log the contrast to noise and the found rate each way. Do the same with a white ball on a black cloth and under a white LED lamp at dusk, to see the light lost. The sim says the red ball on the sky goes from about 7.5 (found 59-70 %) to over 40 (found on every look), and the light under LED lamps drops to about half. Write the numbers into docs/DESIGN.md “### R139”. Nothing bought. -
The orange glass at dusk and on the pitch (R140). Keep the step 54 piece of orange glass (or acrylic, or a photo filter: do not buy one for this). Hold it flat over the window and log the light read (
b1_bench.py --still --frames 60) with and without it: in sun, under thick cloud, and 20 minutes after sunset (the sim says 66 %, 57 % and 22 % of the light). At dusk, run--set-aim autoon a wicket against a black cloth with and without the glass (sim: it holds as fitted and fails through the glass). In daylight, roll a red ball across a dry, worn patch of ground 20 times with and without the glass (b1-pitch-<way>.json) and log the contrast to noise (sim: the red ball and the ground become about the same grey through the glass). Write the numbers into docs/DESIGN.md “### R140”. Nothing bought. -
The orange camera’s own lean table (R141).
python3 track2d.py --selftestmust also print “band: orange cameras [1, 4], dusk aim clear True orange False; lean orange/clear 1.038”. If the step 54 piece of orange glass is to hand (do not buy one), hold it flat over the window and repeat step 50’s five sun angles with it, then without it, in one alternated run:b1_bench.py --still --no-encode --frames 600 --out b1-lean-<way>-<angle>.json. Fill LEAN_TABLE_BAND[‘orange’] from the run with the glass and log its ratio to the clear table at each angle (the sim says 1.036-1.040). Write the ratios into docs/DESIGN.md “### R141”. Nothing bought. -
The pink ball on a grey day (R142).
python3 track2d.py --selftestmust also print “pink: overcast pitch lead orange, red None, clear day None”. On a fully overcast day (no shadows on the ground), if a pink ball is to hand (do not buy one), roll it slowly across the dry pitch 3-5 m in front of the camera, once with the step 54 piece of orange glass held over the window and once without:b1_bench.py --still --no-encode --frames 600 --out b1-pink-<way>.json. Log the share of frames the finder found the ball in each run (the sim says nearly every frame both ways, from the ball’s glow and its bright top and dark bottom, est.). If the clear run finds under half, the orange camera alone carries the pink ball’s pitch looks on grey days: note it in docs/DESIGN.md “### R142”. Nothing bought. -
A shiny new ball and a worn one (R143).
python3 track2d.py --selftestmust also print “gloss: new red side-lit 1.0304, over 20 1.0101; white orange 1.0038; past the table 1.0226; over 60 pink 1.0044”. On a sunny day with the sun square to the rail, if a new ball and a worn ball are to hand (do not buy them), run step 51’s lean measurement once with each:b1_bench.py --still --no-encode --frames 600 --out b1-gloss-<new|worn>.json. Log the lean (px) of each and their ratio; the sim says a new red ball leans about 3 % more than a worn one with the sun square (est.). If the measured gap is under 1 % or over 6 %, put the measured value in GLOSS_LEAN for that ball and band. With a pink ball on a fully grey day, step 57’s runs also say whether the glow matters: the sim finds the pitch looks from the ball’s shading alone. -
The new ball found from the shine window (R144).
python3 track2d.py --selftestmust also print “gloss count: new ball found from the window, 2 overs on; a noise dip undone, 24 overs; replacement kept 24; log 0; late log kept 2”. With step 58’s two balls (do not buy them), roll the worn ball for six throws, then the new one for six, without logging a new ball in the app, and read the shine window after each six (ShineWindow.factor()). The sim says the window falls about 5 % from an 80-over ball to a new one (est.); GlossCount must reset on the new ball’s six. If the measured fall is under 3.5 %, set SHINE_DROP to half of it. Also log the highlight’s peak each throw: if its spread is under a tenth of a new ball’s gloss, the highlight may be fused with the log (R144 left it out). -
A wet ball is not a new ball (R145).
python3 track2d.py --selftestmust also print “wet: early fall blocked, 32 overs; wet fall at the new-ball time, no answer 81, yes the next over 2; dry fall 2; sign True then False”. With step 58’s worn ball (do not buy one), roll it for six throws dry, then wipe it with a wet cloth and roll it for six more, then six more as it dries, reading the shine window after each six (ShineWindow.factor()). The sim assumes a fully wet ball falls 6-12 % and dries to 40 % an over (est.); log the measured fall and how many sixes it takes to come back. Also look at the frames just after each wet bounce: if drops leave the ball in at least half the bounces, wet_sign()’s spray read is worth building on the camera; write both into docs/DESIGN.md “### R145”. Nothing bought. -
A wet ball’s own lean (R146).
python3 track2d.py --selftestmust also print “wet lean: worn red side-lit 1.0041 dry, 1.0134 wet; new ball 1.0304 dry, 1.0345 wet; told 0.30 mm -> 0.34 mm wet”. With step 58’s worn ball and lamp (do not buy anything), measure the lean at the side-lit angle dry, then wiped with a wet cloth, then as it dries (six looks each), as in step 58. The sim assumes a fully wet ball’s highlight is 0.25-0.75 of a full lacquer coat’s (est.): if the wet lean is more than WET_GLOSS 0.2 of a coat would give, log it and the shine window’s fall alongside, so the next row can size the table from the fall; write both into docs/DESIGN.md “### R146”. Nothing bought. -
How wet is the ball (R147).
python3 track2d.py --selftestmust also print “wet size: reading 0.90, film 0.45 on a sign, 0.45 with none at over 30, held at over 76; false sign 0.00; ref 1.04 then 1.05; new ball (0.0, False)”. On step 61’s worn ball and lamp (nothing bought): log the shine window’s factor dry, then just wiped, then every 2 minutes as it dries, beside the lean at each look. The sim reads wetness as the fall over WET_FALL 0.09 (a fully wet ball falls 6-12 %, est.): write the real fall of a fully wet ball and whether lean tracks fall into docs/DESIGN.md “### R147”. -
The day’s own wet fall (R148). No new self-test line (track2d.py is unchanged). On step 61’s worn ball and lamp (nothing bought): soak the ball fully in water, then log the shine window’s factor, the highlight’s width in pixels and the lean at each look for 2 minutes; wipe it with a towel as an umpire would and log the same again. The sim keeps one WET_FALL 0.09 and finds the film’s coat share is what is left: write the real fall of a fully wet ball, how much a towel leaves, and whether the highlight’s width tracks the lean’s extra into docs/DESIGN.md “### R148”.
-
The film’s coat share from the wet highlight (R149). No new self-test line (track2d.py is unchanged). With step 63’s soaked worn ball, at 20 m from one camera in step 61’s lamp light (nothing bought): log the highlight’s brightness and its width (second moment, px) on every look for 2 minutes wet, then dry, three times. The sim keeps WET_FULL 0.5: write the width read’s spread over each 6-ball stretch (the sim assumed 0.15 of the water’s share; under about 0.05 it is worth a new row) and the wet against dry width into docs/DESIGN.md “### R149”.
-
A wet seam’s own lean (R150). No new self-test line (track2d.py is unchanged). With step 63’s soaked worn ball at 20 m from one camera in step 61’s lamp light (nothing bought): turn the ball in 15° steps so the seam crosses the highlight, and log the lean and the highlight’s width at each step (six looks), wet and then dry. The sim puts a wet seam’s glint at 0.1-0.3 of the film’s highlight (est.) and finds it adds under 0.05 mm: if the wet-minus-dry lean jumps by more than 0.1 mm when the seam crosses the highlight, write it into docs/DESIGN.md “### R150” for a new row.
-
Dew first (R151). No new self-test line (track2d.py is unchanged). Climate records show nearly all wet balls with the sun out are dew-wet (96 % over the three grounds, all of Mumbai’s), in the evening under a low sun. So do steps 61 and 62 before 63-65, with the ball misted (a fine spray from 30 cm, no drips, as evening dew) and step 61’s lamp 10-30° above the bench, then repeat at 45°. Log the lean and the shine window’s factor dry, misted, and every 2 minutes as it dries, and write whether a misted ball’s extra lean is within WET_GLOSS 0.2 of a coat into docs/DESIGN.md “### R151”.
5. What the results decide
Which board goes into the first stump (owner, 1 Nov 2026: “try both and take one to production for now”). We fill one sheet with both boards’ results and pick by these, in order:
One program fills the sheet (row R26). Put both result packs in Downloads and run, on the Pi or any computer with Python 3:
python3 compare_boards.py --pi day1-results.tar.gz --radxa radxa-day1-results.tar.gz
It prints this table filled in, the pick by the rules below and the camera speed notes, and writes board-pick.json. Anything not back yet says “not yet”. For question 1, power the board off and on, run day one again and add each new pack with another --pi or --radxa.
| # | Question | Pi Zero 2 W | Radxa ZERO 3W |
|---|---|---|---|
| 1 | Camera runs reliably (10 power cycles out of 10) | from day1.sh | from radxa_day1.sh |
| 2 | Raw frame rate at 1280 × 800 (pass 119 fps) | B1 raw | 20-raw-1280x800 (120 fps with our ov9281_iemi driver, R23) |
| 3 | Our program’s frame rate with the clip running | B1 | 27-b1-radxa, through camera.py (no clip yet) |
| 4 | Pose model rate (players’ body points) | CPU only, about 3 a second (est.) | NPU, if the runtime is there |
| 5 | Secure boot, for signed calls a club can trust | no | yes |
| 6 | Heat at the end of the run | from day one | from day one |
| 7 | Price and supply | $15 list, out of stock; $85 now | $30 list; $70 now |
- If both pass 1-3: the Radxa goes into the first stump. It has the NPU and secure boot, and the Pi stays the bench board.
- If only the Pi passes: the Pi goes in, and the Radxa keeps working on its driver.
- If only the Radxa passes: the Radxa goes in, and the Pi stays the bench board (row R26 added this case).
- One start is not ten: a pick from one day-one run is provisional until each board has started its camera 10 times out of 10.
- Either way: the stump CAD fits both, since they’re the same 65 × 30 size; only the heatsink and power connector differ. Our own RV1126B board, or a ready-made module, stays the later production step.
The camera speed results:
- B1 at 119 fps or more with the clip running: that board passes, and order 2 goes ahead as listed in the first stump section 7.
- Raw reaches 119 fps but our program does not: the limit is the ISP or our code, not the camera. We try 1280 × 720 and recording clips only between deliveries.
- Under 119 fps even raw: the stump runs 1280 × 720 or 1280 × 400, and the simulation’s 120 fps figures stand until our RV1126B board (row Q10).
- The finder takes more than 8 ms a frame: its two hot loops move to C with NEON before the stump is built.
- Lens front under 7 mm from the board: the camera hole in the tube grows (design row Q12) before the tube is cut.
Order 2 is not placed until day one is back and these are answered.
6. While the parts are on the way (the 30-minute lab runs)
- Read
day1.shand every program it calls once more for anything that would stop on a fresh Raspberry Pi OS Lite. Done 1 Nov 2026 (row R20): one stop found and fixed (the secure-element library’s build could have stopped set-up), and set-up’s log now comes back with the results. - Do the same for
radxa/setup-radxa.sh,radxa/radxa_day1.shand what they call, on a fresh Radxa OS. Started 1 Nov 2026 (row R23): its kernel has no camera driver, so set-up now builds ours; row R24 (done 1 Nov 2026) checked the camera overlay against that kernel’s device tree: every node there, and the day-one script now finds the camera by its whole name (m00_b_ov9281 2-0060); row R25 (done 1 Nov 2026) read every set-up step against a fresh image: the driver build now keeps compiler warnings as warnings (Radxa’s kernel stops on them and was built with an older compiler than the image has), the signed-call self-test gets its library, and set-up says FAIL straight away if the boot menu does not name our camera overlay. Row R26 (done 1 Nov 2026) joins both boards’ results into the section 5 sheet:compare_boards.py. - Port the camera part of
b1_bench.pyandclip_keeper.pyto plain V4L2 for the Radxa, behind one camera module both boards use. Done for B1 on 1 Nov 2026 (row R22):camera.py. It also found that Radxa’s driver sends 10-bit only and always picks its 30 fps mode; row R23 changed the driver (done 1 Nov 2026:radxa/build-ov9281.shbuilds our 120 fpsov9281_iemion the board; the ZERO 3W’s own 5.10 kernel has no OV9281 driver).live.pymoved to it on 1 Nov 2026 (row R27: both boards can be focused, with a focus number);clip_keeper.pymoved to it on 1 Nov 2026 (row R28): the match camera service now runs on either board; the Radxa makes its 10 s clip from a 30 fps ring in memory (307 MB of its 2 GB) with ffmpeg, which its set-up now installs. Row R29 (done 1 Nov 2026) put the bails, lights and sensors behind one board table (header.py): on the Radxa the I²C wires go on pins 27 and 28, not 3 and 5 (its I2C3 serves its own USB-C chip), and its set-up now sets the input pull-ups the Radxa kernel would have silently skipped. Next (row R30): the drop and T1 tests’ frame counters oncamera.py. - Turn the Monday photos and lens height into the camera mount and hole check. Prepared 1 Nov 2026 (row R21): the table in section 2. It also found that the tilted camera board reached through the soft case, so case-top has two small corner windows. The next run puts the measured numbers into the CAD.
- Keep order 2 (FIRST-STUMP.md section 7) up to date. Nothing is bought.