Brad, 2026-09-03: "I like your idea" — usbif is wiring a UVC camera through board_config.display_drv and today the only JPEG decoder in the workspace is LVGL's, so the camera path needs LVGL in the build. This issue is the plan; pydevices/docs/jpegio-vision.md is the one-page version.
What. Port CircuitPython's jpegio (shared-module/jpegio/JpegDecoder.c, TJpgDec R0.03 underneath) as a native module in displayif: JpegDecoder.open(buffer|file), decode(bitmap, scale=…), same API as CP so CP builds use the native one and MicroPython builds get ours. Camera path becomes MJPEG frame → jpegio → display_drv.blit_rect, no LVGL.
One decoder per firmware. jpegio vendors tjpgd.c; its micropython.cmake/.mk compile it only when the lvgl usermod is NOT in the build, and link against LVGL's jd_prepare/jd_decomp when it is — same pinned R0.03 either way. The org-wide TJpgDec config is CircuitPython's (JD_FORMAT 1 RGB565, JD_USE_SCALE 1); LVGL's decoder is made to honour it (lvgl-bindings issue, linked below), which also un-disables TJPGD on the CircuitPython bindings.
Contract, from the usbif session's measurements (Logitech C920e, 320x240, one camera):
- UVC MJPEG frames are NOT JFIF-first:
SOI → DQT → SOF0 → DHT×4 → COM → APP0/JFIF → DRI → SOS. LVGL's is_jpg() (lv_tjpgd.c:302) demands FF D8 FF E0 … "JFIF" in bytes 0..9 and rejects every webcam frame. jpegio must not sniff beyond SOI; hand the buffer to jd_prepare and let it judge (CP's behaviour).
- This camera sends its DHT tables; many UVC cameras omit them and expect the host to inject the standard ones. Decide explicitly: "valid JPEG in" (something upstream injects tables) or jpegio injects. "Cameras always send DHT" is unproven with n=1.
- Frames carry DRI (restart intervals). The test frame for any config change must have restart markers, not a clean still.
- Output: native-order RGB565 (the driver byteswaps itself). Two things worth more than raw speed: block/row-wise output through TJpgDec's output callback so a preview blits incrementally without a full-frame buffer (600 KB not allocated at 640x480), and exposing the 1/2, 1/4, 1/8 scale factor (pydevices has no resampler; the usbif example upscaled by hand).
- S3 headroom (ESP32_GENERIC_S3, 8 MB PSRAM, 800x480 panel live): ~6.5 MB contiguous free; TJpgDec's ~3 KB work buffer is nothing. Caveat: pooled PSRAM+SRAM number, internal-SRAM headroom not separated, PSRAM decode speed unmeasured.
- Target: on the S3's full-speed host the camera negotiates 320x240 @ 5 fps (~8 KB/frame) — ~5 decodes/s, undemanding. The P4's high-speed host (unproven bench) and its hardware JPEG decoder are where rate matters: keep the decode backend swappable.
- No decode rate exists yet (no successful decode on the bench); do not quote one.
Method note worth keeping: the usbif session's first LVGL attempt "worked" only because the header carried its own w:320,h:240 and LVGL echoed them back; zeroing the header exposed that nothing decoded. Any jpegio check starts from a zeroed header.
Order: decoder module + tests (a DRI frame, a table-less frame, scale factors, block callback) → cmods one-copy build rule → usbif consumer → lvgl-bindings JD_FORMAT change and CP TJPGD re-enable. Backend swap for the P4 later.
Brad, 2026-09-03: "I like your idea" — usbif is wiring a UVC camera through
board_config.display_drvand today the only JPEG decoder in the workspace is LVGL's, so the camera path needs LVGL in the build. This issue is the plan;pydevices/docs/jpegio-vision.mdis the one-page version.What. Port CircuitPython's
jpegio(shared-module/jpegio/JpegDecoder.c, TJpgDec R0.03 underneath) as a native module in displayif:JpegDecoder.open(buffer|file),decode(bitmap, scale=…), same API as CP so CP builds use the native one and MicroPython builds get ours. Camera path becomes MJPEG frame →jpegio→display_drv.blit_rect, no LVGL.One decoder per firmware.
jpegiovendorstjpgd.c; itsmicropython.cmake/.mkcompile it only when the lvgl usermod is NOT in the build, and link against LVGL'sjd_prepare/jd_decompwhen it is — same pinned R0.03 either way. The org-wide TJpgDec config is CircuitPython's (JD_FORMAT 1RGB565,JD_USE_SCALE 1); LVGL's decoder is made to honour it (lvgl-bindings issue, linked below), which also un-disables TJPGD on the CircuitPython bindings.Contract, from the usbif session's measurements (Logitech C920e, 320x240, one camera):
SOI → DQT → SOF0 → DHT×4 → COM → APP0/JFIF → DRI → SOS. LVGL'sis_jpg()(lv_tjpgd.c:302) demandsFF D8 FF E0 … "JFIF"in bytes 0..9 and rejects every webcam frame. jpegio must not sniff beyond SOI; hand the buffer tojd_prepareand let it judge (CP's behaviour).Method note worth keeping: the usbif session's first LVGL attempt "worked" only because the header carried its own
w:320,h:240and LVGL echoed them back; zeroing the header exposed that nothing decoded. Any jpegio check starts from a zeroed header.Order: decoder module + tests (a DRI frame, a table-less frame, scale factors, block callback) → cmods one-copy build rule → usbif consumer → lvgl-bindings JD_FORMAT change and CP TJPGD re-enable. Backend swap for the P4 later.