Imported from bigjosh/gotham-spincoater (
AGENTS.md). Install upstream withnpx skills add bigjosh/gotham-spincoater. Copyright stays with the author.
Spin coater hardware and device boundaries
- The controller selected by the user is a Raspberry Pi Pico 2 W.
- The project uses six ARCTIC P12 Pro fans, operated together with a ramp to 3,000 RPM and a 30-second hold. Ramp duration and acceptable RPM tolerance have not yet been specified.
- COM4 is another Pico unrelated to this project. Do not open, reset, flash, write to, or otherwise control it.
- COM5 is an unrelated device. Do not open, reset, flash, write to, or otherwise control it.
- Avoid serial auto-connect or all-device programming commands. Explicitly identify the project board before any device interaction; Windows device enumeration is sufficient for initial discovery.
- On 2026-09-08, the newly connected board appeared as the RP2350 UF2 bootloader volume at J:, with USB storage serial 8792B44D9C11021D. Its INFO_UF2.TXT reported Model: Raspberry Pi RP2350 and Board-ID: RP2350. This identifies the chip family, not the exact board variant; Pico 2 W is user-provided identification.
- Drive letters and COM assignments can change after flashing or reconnecting. Recheck the target rather than relying on the recorded drive letter. No firmware was flashed during this initial identification.
- On 2026-09-08, installed official MicroPython v1.29.0 (2026-08-24), standard ARM build for RPI_PICO2_W. The board rebooted as COM7. Its USB parent was USB\VID_2E8A&PID_0005\8792b44d9c11021d, and its USB location matched the bootloader device. Read-only REPL queries confirmed MicroPython v1.29.0, Raspberry Pi Pico 2 W with RP2350, and machine.unique_id() = 8792b44d9c11021d. The serial connection was then closed. COM4 and COM5 were not opened or modified.
- Installation artifact and runtime verification are saved under firmware/. COM7 is the last observed project port, not a permanent identifier; match the board's USB serial before opening it.
- The Pico is mounted on a 52Pi/GeeekPi Pico Breadboard Kit Plus (EP-0172). Preserve GP2-11 for TFT/touch, GP14/15 for buttons, and GP26/27 for joystick. GP12/13 drive RGB/buzzer and GP16/17 drive indicator LEDs; these are not free without disconnecting board links. GP4 is connected to TFT MISO even though the display driver does not read it.
- Fan #0 uses GP18 PWM (physical pin 24) and GP19 tach (physical pin 25). On 2026-09-08 the user explicitly requested active HIGH/LOW PWM at 10 kHz: config.py now selects PWM_PUSH_PULL=True and PWM_HZ=10000. This is a diagnostic outside ARCTIC's 21-28 kHz range; the actual response is not guaranteed. The previous PIO open-drain 25 kHz mode is retained as an option, not the active configuration. Apply fan 12 V after Pico USB power is established; remove fan power before Pico USB power. Use a shared ground. Tach uses a 3.3 V internal pull-up; do not connect a motherboard/hub's higher-voltage tach pull-up.
- The user has no NPN transistor/interface parts for the initial bench test. The original direct open-drain interface used the RP2350 powered FT input limits; this does not establish compatibility for arbitrary active-drive circuits, an RP2040 Pico, or ADC-capable pins.
- A one-fan MicroPython bench UI is installed as main.py and supporting files from device/. It boots at 0%, joystick adjusts selected PWM, BTN1 starts/pauses, and BTN2 stops/resets selection. Only channel #0 is initialized. The six-channel allocation and future required auxiliary-link disconnections are in README.md; no kit hardware modifications have been made.
- The user initially reported accurate tach with PWM disconnected and tens-of-thousands RPM with PWM connected. Monitoring recorded a 57,692.5 RPM peak, consistent with the old 500 us edge-spacing gate accepting every 13th 25 kHz interference edge. After synchronized sampling was deployed, the user confirmed on 2026-09-08: "dude, problem 100% solved!" Treat the reported false-RPM problem as resolved on the real bench setup. This confirms the measurement solution; it does not establish that the electrical disturbances themselves vanished. Do not cap RPM to conceal interference. The requested celebratory writeup is THE_GENIUS_MOVE.md, linked prominently from README.md.
- On 2026-09-08, installed synchronized tach sampling at the user's proposed midpoint of the longer PWM phase. Hardware PWM remains push-pull 10 kHz. Each PWM-wrap DREQ supplies a DMA delay token to a 17-instruction PIO program; PIO reports complete falling-to-falling periods, Python averages the latest eight. Sampling works at 0% and 100%. Config SYNCHRONOUS_TACH=True; only fan #0 remains enabled on GP18/GP19. Six-channel state machines are (0,1,2,3,8,9), leaving PIO1 for wireless. Six fans require six DMA channels; GP12/13 share a PWM slice and use a DMA chain with only one DREQ consumer. Never allocate competing DMA channels on the same DREQ. Clear all chain EN bits before aborting any link.
- Duty changes currently restart the affected slice's samplers and averaging window. Continuous tach measurement during fast automatic ramps remains future work. A 0% command does not establish physical motor stop. Do not change machine.freq() while sampling.
- tools/test_sync_board.py is a synthetic six-sampler test on spare GP0/GP1. It temporarily uses PIO1 SM4 for a generated tach waveform; run only with wireless inactive and those GPIOs unconnected to other hardware. It checks switching spikes, 25/50/75% duty, real capture at constant 0/100% duty, timeout and shared-resource cleanup. Six simultaneous channels all measured the generated 100 Hz signal as 3000 RPM; this is not an actual fan measurement. Transcript: artifacts/sync-board-test.txt. All 57 host tests passed. The user subsequently confirmed the false-RPM problem resolved on the real fan; maximum speed and a six-physical-fan rig have not yet been verified.
- tools/pico.ps1 identifies the board by USB ancestry before serial access and blocks COM4/COM5. Monitor does not interrupt the app; Info/Exec/Deploy/Launch do. Do not interrupt an ongoing user fan run unnecessarily.
- tools/selftest.py uses otherwise-unused GP0/GP1 and PIO SM10/11 for a synthetic test. Never run it after additional devices/fan channels are attached to these pins without first resolving that conflict. Synthetic readings are not actual fan speed measurements.
Current rig state — 2026-09-09
These current details supersede the historical one-fan and PWM-wrap implementation notes above. The user confirmed the kit links for GP12/13/16/17 are disconnected. All six fans are available; current (PWM, tach) GPIO pairs are (18,19), (20,21), (22,28), (1,0), (13,12), (16,17). Read device/config.py and PINOUT.md for the active allocation. GP0/1/20 are no longer spare pins for synthetic output tests.
Current firmware uses one continuously running combined 10 kHz PIO PWM/tach state machine and TX DMA channel per available fan. Shared STOP watcher SM11 uses the final three words of the same 32-word program and six additional one-shot DMA channels to force all PWM pads LOW. Fan SMs are (0,1,2,3,8,9); PIO1 remains reserved for wireless. Total fan/STOP DMA use is 12 of 16 channels. Core 1 owns control, period averaging, and driver operations; core 0 handles UI, Wi-Fi, and persistence. RPM deviations produce recoverable timed warnings, not shutdowns. The configurable rpm_zero_threshold defaults to 60 RPM and affects display only; raw RPM remains available for seeking. The zero-target warning band uses tolerance_rpm.
Idle, disabled, STOP, START, completion, and settings edits retain the same live tach sampler, FIFO, and period window. STOP changes output permission only: GPIO OUTOVER actively forces each PWM pad LOW while measurement continues. Only explicit START rearms the separate STOP latch and clears output overrides after zero commands have drained through the TX FIFO. There is no normal sampler reinitialization or PWM input override; capture_starts should remain 1 through ordinary transitions. Use PioFan.pwm_is_low() to verify raw pad LOW and output-enable status. Never initialize/remux a PWM GPIO while its sampler is running: initialization clears overrides. Only close or explicit recovery from a real driver error tears down capture. Clear every DMA-chain EN before closing any link.
The project board on COM7 received the uninterrupted-capture correction on 2026-09-09, with all seven uploads hash-verified. All 294 host tests passed. A live six-channel test generated internal tach edges without electrically driving tach leads: actual PIO periods and full windows continued through START, held STOP, release, and restart, with every sampler initialized exactly once. Before starting the Python worker, the same test confirmed the hardware STOP DMA forced all six output overrides LOW while all samplers remained active. Warning recovery and driver-error isolation also passed at zero power. Final normal startup reported TOUCH_READY and GOTHAM_READY, retained all-six selection, and stayed IDLE at zero power. Saved recipes/selections were preserved. The user's negative RPM was not reproduced; snapshots now retain period diagnostics and negative producer values are rejected/logged as TACH_NEGATIVE. See artifacts/validation.md for evidence and limits; internally generated pulses are not a physical coast-down measurement. COM4/COM5 remain strictly protected.
Subsequent physical test on 2026-09-09 found that fan #0's tach line stops changing at zero PWM. With only #0 driven at30% for4s, physical falling edges and PIO periods matched and RPM reached approximately1277. A zero-duty command stopped further physical edges BEFORE software disable or hardware STOP was invoked. Sampler PC remained in its normal loop, capture_starts remained1, and no driver errors/overflows occurred. The last RPM stayed valid until its1.5s timeout, then became0/invalid. This does not prove the shaft stopped, and does not establish the fan's internal circuit or all other units' behavior. Do not claim synthetic input tests prove actual P12 Pro coast-down sensing. Evidence: artifacts/physical-coast-tach-diagnostic.txt and artifacts/validation.md. The normal application was rebooted afterward, with saved recipes/selections unchanged.
Publication documentation — 2026-09-10
The user declared the project complete and successful and requested documentation for outside builders. README.md now leads with the six-sample benefit, a sub-US$100 basic-build budget estimate, and a 1–2 hour assembly estimate, followed by parts, preparation, wiring, installation, and a first dry spin. TECHNICAL_REFERENCE.md holds detailed control/electrical/API/development material. firmware/README.md gives portable mpremote instructions selecting each builder's own USB serial; tools/pico.ps1 remains unchanged and locked to this project's board. New unmodified kits MUST set AUX_LINKS_DISCONNECTED=False before uploading, regardless of touchscreen selection. Do not rewrite the deployed configuration or access hardware as part of documentation work. Sample-holder/blade-removal construction is not specified in the source files; do not invent the builder's mechanical method. Preserve the user's successful-project status while distinguishing it from the scope of individual historical tests.