Repository navigation
usbhost, audioif, cameraif and displayif live here now; MicroPython pin moves to v1.30.0-preview-160 - #115
Merged
Conversation
Three bugs, and only the first was the one I set out to fix. 1. mclk_multiple was hard-coded to 512, with a comment claiming that was what board_peripherals asked its PWM for -- which stopped being true when the board moved to 256fs, and nothing noticed because the two never compared notes. It also made 44.1 and 48 kHz impossible: 512fs puts MCLK at 22.6 and 24.6 MHz, which the P4's I2S PLL rejects. It is a keyword now, defaulting to 256, and soundcard.py passes the board's published AUDIO_OUT.wire.mck_fs -- which is the entire point of publishing the wire. 2. The decimator truncated. 48000/rate in integer arithmetic gave N=1 at 32 kHz, a rate the pump accepted, so a 48 kHz stream went undecimated into a 32 kHz wire and played about a fifth flat with nothing reporting a fault. Rates that do not divide the host rate exactly are refused now. 3. The conversion always emitted mono. It averaged L+R regardless of what the sink was opened with, which was correct for every board that existed before the audio format contract, because they were all mono. Opened as stereo it fed half the samples the hardware was clocking for, and the stream played an octave up -- Brad heard 'Alvin and the Chipmunks'. The byte counter agreed before the code changed: 95,893 B/s against an expected 192,000. Measured after, at 48 kHz stereo over two minutes of real playback from Windows: 22,845,696 bytes at 191,980 B/s against 192,000 nominal, within 0.01%, zero timeouts, zero shed. Correct pitch, no distortion. A first attempt at (3) added a mono-to-stereo expansion branch that writes two samples for every one it reads -- in a buffer where input and output are the same memory. Unreachable while the USB descriptor is fixed at stereo, so it would have waited quietly for whoever made it configurable. Removed, and pump_start refuses that combination rather than leaving the trap.
…nnel count Verified on the P4 with two minutes of real playback from Windows at 48 kHz stereo: 191,980 B/s against 192,000 nominal, zero timeouts, zero shed, correct pitch and no distortion by ear. A separate, pre-existing dropout remains on this path (usbif#13): USB feedback is disabled by deliberate choice, so host and sink clocks drift. Not caused by this change and present before it.
…spike Two things the workspace had no way to do. First, pull an audioif graph without allocating: audiocore.get_buffer() m_mallocs a copy of every block, so it fails micropython.heap_lock() before any node gets a chance to, and the allocation-free claim could only ever be measured, never gated. audiopump.pull() is the same pull with nothing between it and the protocol. Second, run that pull on a thread with no interpreter state at all -- a plain pthread, the unix stand-in for a FreeRTOS task. It resolves the audiosample protocol once on the interpreter thread and the loop calls through the resolved pointers, because the funnel every node uses (audiosample_get_buffer) calls mp_proto_get_or_throw and audiosample_check_for_deinit, and both of those raise. Throwaway. One global pump, no ring, no timing, nothing staged.
…I2S sink and the park protocol
…nput node and a round-trip probe
…se it and kill the next pump
The pull is wrapped in audioif_pump_lock_acquire_pump/release_pump -- one block, and not the sink write, which blocks for a DMA block and would make every knob wait for the speaker. retarget() no longer needs a park. Everything that can raise happens first on the interpreter thread; then the lock, three stores, unlock, and the pump sees the whole new tail or the whole old one. That is the handoff page's registry entry. The soft-reset type-word net moves inside the lock and after the retarget is applied: checking it before called every live class swap a re-inited heap (err=4 on every swap storm). spawn() refuses a file-backed tail with a sentence. audioif's WaveFile and MP3Decoder refuse the pump from the inside, so one deeper in a graph publishes AUDIOIF_PUMP_FAULT_UNPUMPABLE and goes silent instead of reading a file from a thread that cannot raise. The status block gains the fault code and the lock's three costs, and lock_stats() / lock_reset() / fault() read them without a pump running.
audiopump_get calls the resolved protocol directly -- that is the point of resolving it once on the interpreter thread -- so it bypasses audiosample_get_buffer and the deinit guard that lives there. The guard covers every node INSIDE the graph and not the one the pump holds. A class that deinits its own Mixer between two pulls is legitimate; the pump writing into that Mixer's freed buffers is not. One compare per block, and the result is silence with AUDIOIF_PUMP_FAULT_DEINITED published rather than a write through NULL.
…s of source audiopump.Ring: Python writes PCM, the pump's thread pulls blocks out through the same protocol every other node speaks. Because it is a node, a pushed stream can be the tail by itself or sit behind a Mixer with a chain of effects on it. Lock-free SPSC with 32-bit acquire/release indices rather than the 64-bit volatile counters the output ring uses -- a 64-bit store is two stores on RV32 and Xtensa, which is a latent tear on both boards we ship. write() never takes the pump lock; clear() and deinit() do, because both write the consumer's own state. Underrun is a whole block of silence, a count, and the data stays in the ring. Overrun is a short write and a count. Neither raises and neither blocks.
…e audio clock Live playing already worked. Sequenced music did not: the drum machine, the soundtrack player and a MIDI-file player all keep time in Python on the interpreter thread, so a collection or a redraw lands as a late note. audiopump.Events is a queue of frame-stamped operations the pump applies at the top of each block, inside the lock it already holds, in a few stores. Six operations -- press, release, release_all, play, stop, level -- each one chosen because its apply half is stores and there is no call beneath it that can allocate or raise. Everything that could refuse happens at schedule time on the interpreter thread, including two that are easy to miss: the note's envelope recalculation, which reads a namedtuple with mp_obj_get_float, and the level's float box. audiopump.now() is the clock: frames pulled since spawn(), 32-bit and wrapping with a release store, for the reason the push ring found. What is audible is that minus the sink's depth, and the API means the pull. audiopump.Tap is a lossy ring the pump memcpys its block into after the tail. Its capacity rounds up to a power of two so the position stays correct across the counter's wrap. Also here: the output ring's 64-bit cross-thread indices, which tear on RV32 and Xtensa, are 32-bit atomics now, with the positions tracked separately rather than recomputed as w % len -- a ring whose length does not divide 2^32 jumps when the counter wraps. spawn(pace=True) is unix-only and holds each block until its wall-clock moment. Without it no desktop statement about a late note means anything.
…ping index w and r are 32-bit and wrapping, which is right. Deriving the buffer position from them as `w % cap` is not: a capacity that does not divide 2^32 makes that position jump by `2^32 % cap` when the index wraps, and the write lands on top of frames the consumer has not read. Once per 4 GB, about six hours of 48 kHz stereo -- the working day of a streaming player. Same trap, same fix as the output ring on spike/lap-events. docs/spikes/probes/audiodev_ring_wrap.py in the anchor pushes the real 4 GB and shows it: against this file before the fix, in_digest 2ba544f55779a907 and out_digest 2a8ae79dbee91349; after it, both 2ba544f55779a907. Two things the probe needed before it could fail at all -- the ring has to be HOLDING data at the wrap (one block in, one block out puts both positions through the same expression and the round trip survives anything), and the blocks have to differ from each other (the same 32 kB written over the top of itself destroys nothing a digest can see). 4.25 GB with neither passed the broken ring.
…inary MicroPython's windows port has no `_thread` module, so a C pump thread is not an optimisation there -- it is the only way a second thread exists in that interpreter. MinGW-w64's gcc on this box is thread-model=win32, so <pthread.h> compiles and pthread_create does not link, which makes the POSIX branch a trap rather than a fallback. Hence a third branch. _beginthreadex, joined with WaitForSingleObject and closed with CloseHandle. A CreateThread handle dropped on the floor is one handle leaked per spawn, and a player that stops and starts a note at a time spawns per note. QueryPerformanceCounter, divided before it is scaled: ticks * 1000000 overflows a signed 64-bit at about ten days of uptime on a 10 MHz QPC, and the pump would then report negative block times. The frequency is latched on the interpreter thread in prepare(), before the pump exists. pace= on a high-resolution waitable timer. Sleep()'s floor is the scheduler tick, 15.6 ms, which is three blocks at 48 kHz/256. _O_BINARY on the file sink. Without it the CRT turns every 0x0A byte of PCM into 0x0D 0x0A and the byte-identity proof compares a mangled file against a good one. SwitchToThread in the park spin rather than Sleep(0), which only yields to an equal-or-higher-priority thread on the same core. audioif needed nothing: the pump lock's CRITICAL_SECTION branch had never been compiled, and it compiled and held exactly as written. Proof and how to re-run: docs/spikes/live-audio-path-windows.md in the anchor.
The ring, the event queue, the tap and the Windows driver are now on spike/live-audio-path; the ring's buffer positions are tracked rather than derived from a wrapping index.
WebAssembly has no threads in the build we ship -- no -pthread, no SharedArrayBuffer -- and emscripten's pthread_create still LINKS, so keying off anything but __EMSCRIPTEN__ gets you a module that aborts the tab on spawn(). The branch is taken at compile time and there is no pthread in the binary. audiopump_run() is now begin/blocks/end and the loop's accumulators live in the context, so the SAME loop can be entered a block at a time and pick up where it left off -- one digest, one block count, one wall clock across every call. A threaded port still runs it once, to completion, and never looks at service_mode. spawn() on wasm adopts the tail and returns without pulling. audiopump. service(max_blocks) runs the loop on the calling thread; audiopump.threaded() says which shape a driver is on. park() is a store, because the loop is never running when Python holds the only thread. Two things the wasm branch decides differently on purpose: - the output ring BLOCKS the pump rather than dropping. A block that will not fit is not pulled at all, so RING_OVF stays 0 and a ring too small to hold one block stalls instead of losing audio. That is the fix the desktop driver has been asking for, in the form a single-threaded port can have it. - service() returns one packed small int, blocks << 2 | why, not a tuple. On this port service() IS the pull's entry point, so heap_lock() around it has to hold, and a tuple is an allocation per tick. The gate catches the tuple version: 200 blocks then ALLOC.
…ker are not one channel pair i2s_start's duplex opened the RX half of the same channel pair, which is right when one port carries both directions (the P4's ES8311) and cannot reach a board where they are separate devices. On the LilyGO T-Embed the MAX98357A is on port 1 with no MCLK at all and the ES7210 is on port 0 with its own pins and a 256fs MCLK, so RX has to be a second channel on a second peripheral. in_port / in_bclk / in_ws / in_mclk. When in_port names a different peripheral the RX channel is opened on it with its own GPIO config, and the TX config stops claiming din -- otherwise the microphone is routed into the speaker's port as well. Single-port duplex takes the same code path it always did. Two ports are two clock dividers off one PLL: handed the same divisor, so nothing observable drifts, but nothing locks them the way one channel pair locks BCLK and WS either. Measured on the board: mic -> Bitcrusher -> speaker, 15020 blocks in 40 s at 4 x 128, clock +0.00 %, zero starved bytes, zero input timeouts, and rx-tx read once a second stayed at 0 for the whole run. 5.3-8.0 ms out of the speaker.
…ndows thread The wasm branch and the windows branch each added their own arm to the same five places in audiopump.c, and each defined a flag the other did not know about. Every hunk keeps both: AUDIOPUMP_ESP, AUDIOPUMP_WIN and AUDIOPUMP_WASM are now all defined in all four arms, so a guard that names one does not silently read 0 because the branch that set it was never merged. One resolution is not mechanical. The windows branch factored the pthread/_beginthreadex difference out into audiopump_thread_start() and audiopump_thread_join() under `#if !AUDIOPUMP_ESP` -- which is true on wasm, where there is no `audiopump_thread` to declare and pthread_create aborts the module. The guard is `!AUDIOPUMP_ESP && AUDIOPUMP_THREADED` now. spawn() and join() already branch to audiopump_service_live before they could reach it; this is the declaration, not the call. Both sink behaviours stand as each port wrote them: windows opens with _O_BINARY through audiopump_sink_open() and writes with _write(), wasm opens with plain open() and the loop's write() branch, and the pace -- which only unix and windows have -- still reads the wall_start alias the wasm restructure left in run_blocks for it.
_usbif.FN_VIDEO (bit 32) has been in the C module all along and
examples/usbif_webcam.py uses it, but neither Device.FUNCTIONS nor
NativeDevice._BITS carried the name -- so device().functions("cdc", "uvc")
raised and a webcam costume had to poke _usbif directly, which is exactly
what the portable surface exists to avoid (#8).
The name is "uvc", not "video". The portable names are deliberately not the C
ones: FN_AUDIO is already "uac", and "uvc" is what the usbif.uvc module and
host_start() already take.
The costume vocabulary is now checked in all three places it is written down.
TestDeviceFunctionNames asserts the two Python tables agree with each other,
that they cover exactly the FN_* set parsed out of src/mod_usbif.c, that each
portable name carries its C bit, and that the call the issue says raised --
_names_to_mask(("cdc", "uvc")) -- returns 33 and round-trips. Changing
FN_VIDEO to 64 in the C source fails it with "32 != 64 : uvc should be
FN_VIDEO", so a C-side renumber cannot drift past this again.
Verified: 160 tests OK (156 before), 22 skipped as before, ruff clean.
Not done, deliberately: examples/usbif_webcam.py:132 still calls
_usbif.dev_functions(FN_CDC | FN_VIDEO) rather than the portable call it can
now make. That edit is a board change and this session has no board -- and
this repo already carries #11 and cameraif#4 because work was merged on a
static review with its checklist unticked. It can move whenever someone has
the hardware to watch it enumerate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CspnXSJV31fvtN4uNPFKHq
The engine went to audioif. What stays is the half that could never have gone with it: the thread, the recursive priority-inheriting mutex, the clock, the pacing, the two waits, and the hardware -- the I2S channel, the microphone Input and the round-trip probe. It binds by defining audioif_port_driver(), which overrides the weak default in the engine at link time. The hook table sits in the SAME translation unit as MP_REGISTER_MODULE(MP_QSTR__audioif, ...) on purpose: weak override has one trap, and it is that a static archive can let the default satisfy the reference first and never pull this object in, which wins silently. The module table holds an undefined reference to _audioif_module, so this object is always in the link, archive or not. audiopump.driver() is the run-time check that says it worked. Three platform branches, and a fourth that is the point: on __EMSCRIPTEN__ this file compiles to nothing. That build has no threads -- pthread_create links and then aborts the whole module at run time, the same compiles-and-is- silently-wrong shape AUDIOIF_DRIVER_ESP32 exists for -- and no hardware, so no driver binds and audiopump.driver() says "none". Which is true. The hardware names are this module's now: i2s_start, i2s_stop, i2s_dma_bytes, i2s_rx_bytes, Input, rt_probe. lock_stats, lock_reset and fault stayed on audiopump, because they count the engine.
What _audioif provides, call by call, including the two-port in_port form and what it is honest about (0 bytes of drift over forty seconds on the T-Embed, not proof of indefinite lock). What each port implements and why WebAssembly needs no driver at all. How it is built into a Make port and a CMake port, and why micropython.cmake defines its own esp32 macro. And what to do to add a port -- rp2 being the obvious next -- with the binding rule stated before the page that explains it.
i2s = audiobusio.I2SOut(bit_clock, word_select, data)
i2s.play(sample, loop=True)
is a CircuitPython program, and on this firmware the engine under it is the
pump: the graph is pulled on a thread of its own and the blocks go straight
into the I2S DMA. Nothing about the call says so. That is the point --
displayif already exposes mipidsi, spibus, i80bus and rgbmatrix, and this is
the audio one.
A C module, not frozen Python: it has to be there on a bare firmware, and a
frozen module is only there if the board's manifest pulls the usermod's in,
which our own builds override routinely.
Pins are displayif's convention and not a second one: an int, a machine.Pin,
or a string the port's Pin() understands.
Two paths under one play(). A signed 16-bit sample is handed to the pump as
it stands, zero copy, mono opening a mono channel. Anything else -- a WAV or
an MP3 that reads through the VFS inside get_buffer, or the unsigned array
CircuitPython's own I2SOut docstring uses -- goes through a ring the
interpreter fills and converts, which is what CircuitPython does too.
_audioif's i2s_start is now an argument parser in front of
audiopump_i2s_open(), so there is one implementation of the open and both
faces use it.
reap() services the loop before it asks whether it has finished, so 'while i2s.playing: pass' is a wait on WebAssembly rather than a hang.
It was a read of whether the pump had physically reached its park. unpark() clears park_req and wakes the thread; ctx.parked stays set until that thread's spin loop notices, so 'i2s.resume(); i2s.paused' was intermittently True. It passed four runs of the gate and failed the fifth. The park is still the mechanism underneath.
… for files
Two changes, and the second is the one that was actually starving the
docstring example.
A RawSample that is u8/u16/mono no longer goes through a ring the
interpreter fills -- it goes to the pump with a conversion scratch, and the
ring is left to the one source that genuinely needs an interpreter: a file.
And the DMA descriptor is sized in TIME. Fixed at 240 frames it is 5 ms at
48 kHz and thirty at 8 kHz, and at the low rates i2s_channel_write times out
once per descriptor and loses bytes. Measured on the P4, two seconds a row:
8000 Hz, 240 frames 67 timeouts, 63 432 of 64 000 bytes written
8000 Hz, 64 frames 0 timeouts, 64 008
16000 Hz, 240 frames 67 timeouts; 80 frames: 0
22050 Hz, 240 frames 13 timeouts; 110 frames: 0
48000 Hz, 240 frames 0 timeouts
The control that settles it: a signed 16-bit STEREO sample at 8 kHz, which
is converted by nothing at all, starves the same 67. rate/200 keeps 48 kHz
on the 240 that is already proven. The docstring example over ten seconds is
now 0 starved and 320 000 of 320 000 bytes of DMA, against 334 and 319 680.
The driver has no Python and no test a runner could execute without building
it, so the whole of CI is: build an interpreter with it, then ask it to play
something. It needs a checkout of audioif beside this one and nothing else --
no workspace, no overlay, no board.
busio_probe.py audiobusio held to what CircuitPython documents it to
do: 32 legs over the constructor, play/stop, loop,
pause/resume, a Mixer, a Synthesizer, a WaveFile and
fifty play/stop rounds. --fault deaf makes every byte
count read as zero and nine legs fail, which is the
plant that cannot be satisfied by accident.
sink_probe.py the engine's own digest of what it pulled against a
digest of the file the sink wrote. The sink is the one
place a block stops being the engine's and becomes a
platform's, and a short write there raises nothing.
cp_example_probe.py CircuitPython's I2SOut docstring example, character for
character apart from the three pin arguments.
Every planted fault runs first and must fail, the way audioif's clean-build
workflow runs its own probes. When this repository becomes part of the public
audioif, the job moves into clean-build.yml as a second cell and loses the
extra checkout.
…eams A host stream in another C module moves packets into and out of the same USBDevice.Ring a device-side stream uses: its size and level, an all-or-nothing put, a get, and a staged write with a commit, so a video frame built from many packets is published whole or not at all.
The speaker, microphone and webcam-on-the-panel examples, the S3 frame grab, and the soft-reset, ring-size and duplex loopback checks leave usbif.auto and the deleted UAC and UVC bindings for the packages. Each keeps what it prints and what it measures.
…ines go The other half of the previous commit, which removed usbif_host_uac.c, usbif_host_uvc.c and tests/unix_c_sink.py and moved the examples; the two build together. usb.host.audio and usb.host.video keep their API, and the class logic moves to Python over usb.core's control transfers: SET_INTERFACE (now sent by set_interface_altsetting, as PyUSB does), the sample rate, a 2.0 clock's RANGE, the camera's PROBE and COMMIT. C keeps only the moving of packets: usbif_host_stream.c opens an isochronous endpoint of an interface claimed through usbif_host_xfer.c and moves its packets to or from a ring in the host task. The ring is a machine.USBDevice.Ring, the same one a device-side stream uses, or for a microphone an audiodsp audiopump.Ring the pump plays from (Microphone.start(ring)). The generic pieces are host-tested headers: PCM pacing for an OUT stream (shared/usbif_iso_pace.h, a 2.0 device's interval of frames rather than its maximum packet) and UVC payloads into whole frames, each led in the ring by its length, the first after a start dropped (shared/usbif_uvc_frame.h). The queue sizing that was UVC's serves every stream (usbif_iso_queue.h). Speaker.c_sink() gives another native module the playing stream as a pcm_c_sink, through the same gate as Speaker.write(); the gate now takes any ring's room and push. Camera(encoding="yuy2") now matches the YUY2 format whatever its case. usb.core also gains read(..., raise_on_timeout=False), as on CircuitPython: a timeout returns 0, allocates nothing, and leaves the transfer armed for the next read, and the endpoint's interface is looked up once rather than from the whole descriptor each read. running() and stats() report the host. Removed with them: the _usbif host_uac_* and host_uvc_* bindings, and the byte ring from the firmware (it stays as the C sink tests' ring). lib/usbif's UacHostOutput and uvc paths call those bindings and no longer work on a board; the packages replace them.
The tethering binding's address formatter was defined on every port and used only with a USB host, so a unix build with -Werror stopped on it as unused.
hid_host, host_enum, midi_host, usb_serial and the three drive examples move off usbif.auto onto usb.core, usb.host.hid, usb.host.cdc, usb.host.msc and midiports.usb_host. hid_host carries its own small key decoder and now lights the lock LEDs. Their tests follow them, and the UAC and UVC descriptor-parser tests move to tests/test_descriptor_parsers.py, against the packages' own parsers.
usb.core and the class packages replaced them, and nothing calls them any more. Removed: the usbif Python package (usbif.auto, the desktop backends, MidiPort and the MIDI parser, uac/uvc/uac_audio, hid_keyboard) and its tests; the C CDC, HID, MSC and MIDI host drivers with the USB-MIDI packet codec and their bindings (host_cdc_*, host_hid_*, host_msc_*, host_midi_*), capabilities() and the FN_* constants; the transfer-order test for those drivers and the costume-era USBDevice check. Kept: the host core and its event ring, the generic transfers under usb.core, the isochronous streams under usb.host.audio and usb.host.video, and the tethering adapter's driver. The manifest now names only the C module: the packages are installed with mip. The leak test now holds the packages to being the only importers of _usbif.
errno has no EPIPE on the ESP32 port, so usb.core's error mapping raised AttributeError for every USB error that wasn't a timeout -- a stop on an unplugged camera, say -- and usb.host.msc's handling of a stalled data phase did the same. Both fall back to MicroPython's numbers now.
read(..., raise_on_timeout=False) gave the transfer the poll's timeout, so ESP-IDF completed it as TIMED_OUT and the next poll raised ETIMEDOUT, which allocates: on a board, a heap-locked polling loop failed where the unix fake had passed. A kept transfer now waits for data however long it takes, and a TIMED_OUT completion in a poll reads as nothing yet.
A camera whose packets are all over the host's in_limit (an ESP32-S3 takes 600 bytes; a webcam at full speed often asks 640) was refused with only the list of its modes. The error now names the smallest packet it asked for and the limit.
A webcam's microphone often offers only stereo (the C920e: 16, 24 or 32 kHz, two channels), and a wake-word or speech consumer takes one. Asked for one channel such a microphone now opens its 16-bit stereo stream, and each frame reaches the ring as the mean of its two samples, averaged in the packet's own buffer as it lands: no memory, no copy, nothing for Python to do. Microphone.downmix says when it happened. host_stream takes downmix=True; shared/usbif_downmix.h is host-tested, with a planted fault CI must catch.
…for more 640 bytes is what a computer takes beside the serial port, but an ESP32-S3 hosting the webcam takes at most 600 and refused every mode. 512 works with any host; main(packet=640) gives a computer 640x480 at about 12 frames a second as before. The example also imports without starting, for a program that runs it.
…variant may carry a manifest.py build_mp.py now uses a board variant's own manifest.py as FROZEN_MANIFEST, as it already did for port variants (frozen_manifest(), with a test). The DEV-KIT's includes modules/manifest.py, then freezes usb-core, the usb-host-* and usb-device-* packages, micropython-lib's usb-device-midi/cdc/hid and midiports' board half, only when the build carries usbif. Other targets freeze what they did.
…ames work
FatFs was built with FF_LFN_UNICODE 0, so names crossed its API in code
page 437 while MicroPython's str is UTF-8. On a card written by a PC,
os.listdir("/sd/...") raised UnicodeError, os.listdir(b"...") gave code
page bytes, and opening a name such as "Ravel - Boléro.mid" gave ENOENT;
names with characters outside code page 437 ("Dvořák") were reachable
only by their short names.
FF_LFN_UNICODE now follows MICROPY_FATFS_LFN_UNICODE and defaults to 2
(UTF-8) on every port that enables LFN. It costs about 400 bytes of code
and no RAM. docs/upstream/fatfs-utf8-filenames.md is the draft issue.
…nd the packages leave for pydevices usbif's C module, now modules/usbhost, renames everywhere: _usbhost, mod_usbhost.c, usbhost_*.c and .h, usermod_usbhost, USBHOST_* macros. Its one MicroPython patch joins the overlay as 0039 (the OTG controller handed to the host). The Python packages and their tests move to pydevices, beside midiports; the examples, the proof matrix, the stream and host checks and the C tests stay here, and .github/workflows/usbhost.yml runs the C tests with their planted faults, the examples' checks and the unix build. The DEV-KIT H2 variant freezes the USB packages from modules/pydevices when the build carries usbhost. usbif's modules.lock line and modules/usbif go.
…ock lines go Each keeps its name, since that is what users import and what --modules names. The build finds them as before; audioif's glue now finds audiodsp as its sibling in modules/.
…and regenerate the series against it From 3843e4a0e (v1.30.0-preview-133), 27 upstream commits: stm32 and mimxrt boards, the test runner's requires: syntax, and X25519 in mbedtls (esp32's sdkconfig.base). Every patch applied clean; 27 of the 40 (the overlay's 39 and cameraif's two) had drifted by some lines since they were written and are regenerated with their own headers, cumulatively at the new pin. No added or removed line changed in any of them.
… points at usbhost Their workflows under modules/*/.github could never run. audioif.yml and displayif.yml do what the old ones did, built with build_mp.py at the pin: audioif's probes with their planted faults beside audiodsp, displayif's qstr reachability, usdl2 smoke, lifecycle API and packed fill. cameraif.yml keeps its flake8.
…one runner, each with a planted fault enumerate, msc and hid run against what's on the host's port; hid_typed, cdc, midi, audio and video need a peer board on that port (--peer) and say "skipped" without one. --plant turns each check's fault on and the run ends PLANTS CAUGHT n of n. run_matrix.py now names its board on every hold command, so it can't find or stop another holder running beside it.
…nger fills the schedule queue MicroPython's lib/micropython-lib points at ee4bb8f, whose usb-device-midi 0.1.0 schedules one _on_rx per OUT completion and calls schedule() before it re-arms the endpoint. TinyUSB's task runs with the scheduler locked, so a burst of one-event packets (about 0.3 ms apart from Windows) queues an _on_rx per packet, fills all 8 slots, and the RuntimeError skips the re-arm: the device never hears MIDI again. The DEV-KIT image freezes that copy, and boards whose /lib is staged from the submodule get it too. micropython-lib fixed it on 2026-09-16 (90a3385, 4fa59bd; 0.1.1). Both commits are carried unchanged in patches/micropython-lib/, which apply_patches.py puts on the submodule after every update and skips once the submodule has them; --check tries them on the pin's submodule, and the build record names them. On the bench, M1 over USB now passes paced and burst on an ESP32-P4 at high speed and an ESP32-S3 at full speed; with 0.1.0 put back, the burst fails after 15 notes.
…l-commit pin works clone --branch takes a tag or a branch, not a commit, and UPSTREAM is now b70ae1aca. clone_at already fetches a 40-character commit directly and a tag as a tag.
…B packages and the pump's USB ring pydevices 323c97f has usb-core and the usb-host and usb-device packages the DEV-KIT freezes; audiodsp c23f67a has Ring.from_usb and the C producer's write that usbhost's capture stream uses.
…'s read of the file Pillow 13 honours bits= for grey and writes 1, 2 and 4-bit files, where 12 wrote 8-bit ones. A decoder scales those samples to full range, so the in-memory image is no longer what the file holds. pngio matches Pillow's decode of the file on 12.3 and 13.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The USB host, audio, camera and display modules are now part of this repo, with their history kept, and build from
modules/.modules/usbhost(formerly usbif; the binding is_usbhost),modules/audioif,modules/cameraif(with IMX219 support and auto-exposure) andmodules/displayif. Theirmodules.locklines are gone, and their CI runs here:usbhost.yml,audioif.yml,displayif.yml,cameraif.yml.machine.USBDevice(Ring, stream, feedback, stream stats), UVC payloads and frames of any size, a streaming OUT endpoint feeding a C sink,USBDevice.Ringfrom C for a USB host's isochronous streams, USBDevice at high speed, and FatFs names in UTF-8 so non-ASCII filenames work.manifest.py.run_host.pydrives the host-side checks from one runner, each with a planted fault.