WIP: Front Panel Updates - #424
Open
erichelgeson wants to merge 40 commits into
Open
Conversation
matching/central_dir/local_file_header were function-local statics in Parse(), leaking state across Parser instances; move them to members cleared by Reset(). Also fix the !buf[idx] == method precedence wart.
The zip updater now also extracts this board's panel bin (bluescsi-v2-frontpanel.bin on I2C targets, bluescsi-ultra-frontpanel.bin on SPI targets) to /firmware/frontpanel.bin before the MCU update reboot, via temp-file + rename so a failed extraction never leaves a torn image. The panel's existing SHA-256 handshake decides whether it reflashes.
create_firmware_zip.sh picks up bluescsi-{v2,ultra}-frontpanel.bin from
panel-fw/ (override with PANEL_BIN_DIR) as stored entries, validating the
ESP32 image magic; skips with a note when absent so local builds work.
Downloads the release assets pinned by utils/frontpanel_version.txt from polpo/open-retro-storage-frontpanel using the FRONTPANEL_GH_TOKEN secret. Missing token fails main/tag builds but only skips fork/PR builds.
The version parsed out of frontpanel.bin's app descriptor dropped any -preN suffix and packed as 0x00MMmmpp, so a prerelease compared equal to its release and the panel never saw same-base-version builds as updates. Pack as 0xMMmmppPP to match the panel's own encoding: the low byte is 0xFF for a final release or N for -preN, sorting prereleases below the matching final. Ships with the corresponding front-panel change.
The front panel now ships one image for v2 (I2C) and Ultra/Ultra Wide
(SPI) — it detects the transport at runtime and persists it in NVS — so
the per-board bluescsi-{v2,ultra}-frontpanel.bin names collapse into
bluescsi-frontpanel.bin. The extractor no longer picks a name per
ENABLE_PANEL_* define, the release zip carries the single bin, and CI
downloads one asset. Pin frontpanel_version.txt to v0.4.1-pre1, the
first panel release under the new name: that tag must exist before the
next main/tag build, or the panel-fw download step fails the build.
RP2040 Pico_DaynaPORT / Pico_Audio_SPDIF compile without the panel (the ~16 KB of panel buffers don't fit alongside CYW43, and SPDIF out shares the panel's SCL pin), but the ini setting still parsed and the log printed 'EnableFrontPanel = Yes' with nothing behind it — which reads as a working panel that never connects (the panel side just sees address NACKs). Log that the build has no panel support and which builds do, right below the settings dump.
The front panel listed .bin files and hid .cue sheets, and loading a .cue directly wedged the eject/next iterator: opening a loose .cue set bin_container (and current_image) to the cue's parent directory, so is_multi_bin_cue() became true and scsiDiskGetNextImageName() tracked the directory name instead of the loaded cue, never advancing. This also made the panel inconsistent with PicoIDE and mishandled a loose multi-bin cue (it showed the raw track .bins). Track a directly-loaded loose .cue by its own filename via a new image_config_t::cue_loaded_directly flag: bin_container stays the parent directory so the cue's .bin tracks still resolve, but the image cycles by the .cue. scsiDiskGetNextImageName() uses the directory-name cursor only for genuine folder-images. Panel side: DirState lists .cue and hides the .bin files a cue references (only when a cue sheet is present, so plain data-.bin CDs stay visible); eject/next/prev use prefer_cue; and the image-name and delete-guard paths resolve a loose cue by its .cue name.
The per-target initiator_target_summary_t array (52 B x NUM_SCSIID = 416 B) added to g_initiator_state for front-panel status reporting was linked into every target, including the RAM-maxed Pico_Audio_SPDIF (DaynaPORT + SPDIF), pushing its RP2040 static RAM 96 bytes past the budget so it failed to link. Its only consumers are the panel protocol handlers, which already self-gate on ENABLE_PANEL_SPI/ENABLE_PANEL_I2C. Gate target_summary and the panel-facing initiator accessors behind ENABLE_PANEL_I2C/ENABLE_PANEL_SPI, routing the summary updates through small initiatorSummary* helpers that compile to no-op inline stubs on non-panel builds. scsiInitiatorMainLoop reads identically either way. Reclaims the 416 bytes: Pico_Audio_SPDIF links again with ~320 bytes of margin; panel targets keep full initiator status reporting.
…o 128 Derived-dir paths overflowed the caller's buffer (dirlen accounted as 0) and too-long names returned 0 mid-ring, so the panel cycled/counted only a subset. Cycling now skips names over the path budget instead of failing. RAM cost offset by rebuilding image_config_t::clear() without its static blank copy and moving scsi_accel_log_state() to flash on RP2040.
…_PATH bump Root scan buffer sized for package names; firmware_name shrunk with a static_assert.
panel_i2c.cpp and panel_sha256_sw.cpp had no placement rule, so RP2040's inverted default pulled them into RAM via the .data catch-all. On Pico the panel builds ENABLE_PANEL_I2C, meaning panel_spi.cpp compiles to nothing and the only live transport was the unplaced one - ~2.5KB of cold init, poll and SHA-256 transform code in the tightest target's RAM.
The image-opening chain (switchNextImage -> scsiDiskOpenHDDImage -> cdromValidateCueSheet) measures ~1.7KB of the 2KB default before SdFat and CUEParser frames, and it is reachable from a plain host-driven eject with no front panel involved. RP2350 already had the headroom; RP2040 needs it more, since it silently corrupts .bss where MSPLIM would fault. Core 1 stays pinned at 2KB - it lives in SCRATCH_X, which PICO_CORE1_STACK_SIZE would otherwise overflow on the Audio SPDIF and DaynaPORT targets.
payload_size is master-supplied and can name up to 64KB. On reads tx_len took it unclamped, so a single 5-byte header asking for 0xFFFF bytes made the REQUEST handler serve off the end of mapped SRAM and fault inside the ISR. On writes the payload counter incremented past the 4096-byte buffer, so the CRC and the path handlers' strnlen ran past it too. The SPI transport already rejects oversize payloads; I2C now matches. Also drop a write that arrives while one is still staged - the payload buffer is the live receive destination, not a snapshot, so a retransmit would commit the new bytes under the queued command's length and CRC - and refresh the device snapshot only while the SCSI bus is idle, since platform_poll() runs inside the transfer loops and the periodic name refresh can block on an SD read.
The refresh was the first statement in panel_spi_poll(), above the bus-busy gate, and platform_poll() is called from inside the SCSI transfer loops. Its periodic getName() can miss the FAT cache and block on an SD read mid-transfer - exactly the CPU-driven delay the hot path forbids.
Every handle_*_async was inlined into one panel_protocol_handle_write blob, summing full_path[192] and seven MAX_FILE_PATH buffers into a single 968-byte frame that then called switchNextImage -> scsiDiskOpenHDDImage -> cdromValidateCueSheet - 2744B against RP2040's stack. Marking the handlers and the name-finding helpers noinline drops the handler to 152B and lets panel_find_prev_image's buffers pop before the image-switch chain runs. check_stack_usage.py could not see this: it is per-function against a 1024-byte threshold, and 968 slips under. Also: refuse an upload whose path is a loaded image - an absolute path could name a live image and O_TRUNC would zero it under the running target, which DELETE and RENAME already refuse; teach panel_path_is_loaded about the panel's own long-lived handles (fw_file, upload, download) so their clusters cannot be freed underneath SdFat; reclaim an abandoned upload's SHA-256 lock and file handle after 60s idle, which otherwise left every later CHECK_FIRMWARE returning an all-zero hash so the panel could never self-update again; and reject a firmware-read offset past EOF instead of underflowing remaining to ~4GB.
Device category codes were dropped on this side; restore them so both copies match. Correct comments claiming the deferred-write buffer is a snapshot.
The receive buffer was also the live DMA/ISR destination, so a panel retransmit could overwrite a queued or in-flight write after its CRC was taken. Drop the newcomer instead.
Downloads held the handle until the next one started, so panel_path_is_loaded reported the file in use and it could not be deleted or renamed. Close on the last chunk, with an idle reclaim for abandoned transfers.
Upstream growth left no RAM for the panel's static buffers. None of these run per-command and each already waits on the SD card.
Test this PRFlash the firmware built from
Must be signed in to GitHub. Build expires after 90 days. BlueSCSI Memory ReportCompared against release Memory Usage
Symbol Region ChangesPico_2_Audio_SPDIF: 8 symbols moved RAM → FLASH (-17.6 KB RAM)
Pico_2_DaynaPORT: 8 symbols moved RAM → FLASH (-17.6 KB RAM)
Pico_Audio_SPDIF: 7 symbols moved RAM → FLASH (-3.5 KB RAM)
Pico_DaynaPORT: 7 symbols moved RAM → FLASH (-3.5 KB RAM)
Ultra: 8 symbols moved RAM → FLASH (-18.9 KB RAM)
Ultra_Wide: 8 symbols moved RAM → FLASH (-19.0 KB RAM)
|
device_is_ejectable() accepted only optical and removable, while the physical eject button (diskEjectAction) has always handled ZIP, floppy, MO and tape as well. The front panel builds its Eject affordance from the wider set, so it offered eject on a ZIP or a floppy and the panel command refused it. Two things had to follow. The non-optical branch of the panel's eject set ejected = true and switched the image - one way only - so a device the panel could now eject could never be closed again. It calls diskPerformEject() instead, the same toggle the button uses, which required exporting it from BlueSCSI_disk.cpp. And tray-open status was reported for optical only, so the web UI's Close button never appeared for any other removable. Verified on the bench across a full device set: removable, floppy, optical and ZIP all eject and close again; fixed disks and the network device are still refused.
The panel learned whether it was talking to an initiator from the mode byte in the GET_DEVICE_LIST reply. That command is async: the ISR accepts it and the main loop completes it. An imaging board does not reach the main loop for long stretches - it is driving the SCSI bus - so the reply never arrives, the panel's operating_mode stayed TARGET, and the whole initiator screen was unreachable on real hardware. GET_PLAYBACK_STATUS is a synchronous read served straight from the ISR, which is why the liveness probe kept working throughout. Put the mode there too, in the byte the struct already reserved, so it costs nothing on the wire and arrives whatever the board is doing. GET_DEVICE_LIST still carries it for callers that already read it.
The panel could not see an initiator's progress. GET_INITIATOR_STATUS is async - the ISR takes it, the main loop completes it - and a board that is imaging is busy driving the SCSI bus and does not get to the main loop. So the one thing that most needs to be live was carried on the one path that stalls. Add PANEL_CMD_GET_INITIATOR_SUMMARY, a read command answered from the ISR, with just what the imaging screen draws on every frame: phase, target, percent, speed, and the found/imaged counts. Everything it reads comes out of g_initiator_state with no bus access, so it is safe there. The async command keeps the per-target detail, which is worth waiting for and tolerates being stale. One mode per response. The playback status stays what it was - target-mode state - and does not learn about imaging; the summary carries no disc or track. What they share is the three bytes that are neither: alive_magic, protocol version, and which mode the board is in, so a panel polling either one can tell it is talking to a live board that has changed mode. The playback struct is repacked while the contract is open: three one-bit states that cost a byte each become a flags byte, alive_magic moves to the front so a reader built against another layout still finds it, and a protocol version rides along - a silent layout mismatch is exactly the failure this change would otherwise have introduced.
panel_spi_poll() disabled the panel's DMA IRQ for the whole of every initiator bus transaction. Imaging issues READ10s back to back, so the interface was dead for entire runs: the ESP32 could not complete a single transaction, could not discover the board was even imaging, and could not attach to one that already was. The panel and the web UI showed nothing at all. There is no hardware reason for it. On Ultra the panel owns DMA_IRQ_3 and its own claimed channels, and scsi_accel_host.cpp uses neither DMA nor interrupts, so the two never contend - and target mode has always run alongside the same ISR. It came in with the original panel-SPI commit, precautionary rather than in answer to a measured problem. Measured on the bench, imaging a 1GB target with the panel polling throughout: 1133 kB/s before, 1123 kB/s after, no parity errors and no retries. About 1%, for a panel that works instead of one that does not. panel_i2c.cpp still has the same suspension. The reasoning applies there too, but the bench cannot image over I2C to prove it, so it stays until it can.
"Imaging SCSI ID 5 at 1337 kB/s" does not answer what it is imaging. The identity is in the per-target table behind GET_INITIATOR_STATUS, which is async and so is exactly the thing an imaging board cannot deliver - the panel and the web UI were left with a rate and nothing else. Carry the current target's type, vendor, product and size in the summary, which the ISR answers. It is a few more bytes on a command that already reads the same target's sector counts for the progress figure.
androda
approved these changes
Sep 3, 2026
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.
Compatible with Front Panel FW v0.5.0