Repository navigation
Conversation
Replace per-track UI objects with a bounded PSRAM-backed path table and draw only the five visible rows into the existing screen sprite. Preserve recursive AAC discovery, navigation, loading, and SD recovery while making allocation limits explicit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
djdefi
added a commit
that referenced
this pull request
Aug 22, 2026
…sfade, manual-override guard, bounded fill Addresses review issues #2, #3, #4, #5 on PR CircuitMess#44 (kept together: the fixes share the same tickTransition()/tick() flow and DjAssistController actuator, and were reviewed as a set). Issue #2 (HIGH, boundary semantics): resolveBoundary() reports the NEXT upcoming boundary every tick, so it is true almost always - WAIT_BOUNDARY was advancing immediately instead of actually waiting. Add DjAssistBoundaryHint::reached, set only by the new capture-once-then- compare DjAssistController::resolveWaitBoundaryReached() (latches ONE target frame the first tick the step is current, then just compares the live playhead against it every tick after). DjAssistEngine::tick() now gates WAIT_BOUNDARY strictly on `reached`. Issue #3 (HIGH, cancel/failure rollback): a terminal cancel/failure left plan-owned mix/sync/started-deck side effects active. Add a bounded DjAssistController::tickRollback() state machine (MIX -> SYNC -> STOP_DECK -> DONE, mirroring the forward plan, one actuator submit/poll per tick, only undoing steps this plan itself submitted, waiting for each command's real applied/failed/superseded result) plus the pure DjAssistBridge::nextRollbackPhase()/evaluateCommandOutcome() helpers it's built from. Never touches pre-existing user state, and deliberately never restarts fromDeck once STOP_DECK has run. Issue #4 (HIGH, crossfade confirmation + manual override): the crossfade poll declared APPLIED from elapsed-time math alone, discarding the actual setMix() result, and a manual mix override mid-ramp could be silently overwritten by the next programmatic tick. The actuator now tracks the exact final-value setMix() command id and only reports APPLIED once DjSession confirms it applied (SUPERSEDED triggers exactly one resubmit). Add DjAssistGuardSnapshot::manualMixOverride, computed by DjAssistBridge::detectManualMixOverride() (a bounded scan of recent command results for a non-system SET_MIX submitted after arming) and checked unconditionally in guardOk() - not skipped once crossfadeSubmitted like the plain mix-threshold check, closing exactly the gap the review flagged. Issue #5 (HIGH, hot-loop SD I/O): refreshCandidateTable()'s fill loop reused DJ_ASSIST_DEFAULT_SCAN_BUDGET (256), a synchronous multi-record SD read burst inside DjSession::loop(). Add a separate DJ_ASSIST_FILL_RECORDS_PER_TICK (1) for the genuine-I/O fill path, leaving the pure in-memory CPU scoring scan's 256 budget untouched. Test coverage added: driveUntilCommandVisible()/ testTransitionWaitsForBoundaryStep() updated for `reached` semantics plus a regression proving hasPhrase/hasDownbeat alone never advances the step; a manual-mix-override-during-crossfade regression in testTransitionManualOverride(); new dj_assist_bridge_selfcheck.cpp cases for nextRollbackPhase() (full sequence + every skip-combination), evaluateCommandOutcome() (exhaustive status mapping), and detectManualMixOverride() (watermark boundary, origin/type gating, rejected-command exclusion, null/empty). All host self-checks (DjAssistSelfCheck, DjAssistBridgeSelfCheck, plus the full ctest suite) pass under -Wall -Wextra -Werror + ASan/UBSan. DjAssistController.cpp/DjSession.cpp remain validated only by the real firmware build per existing project precedent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
djdefi
added a commit
that referenced
this pull request
Aug 22, 2026
… fill, bounded boundary tolerance Fourth-round fixes from focused re-review of eca7930 (4 material issues, 3 HIGH + 1 MEDIUM): 1. HIGH - rollback ownership was inferred from which steps were *submitted*, so a target deck already playing/synced before arming would have rollback incorrectly stop/release state the transition never introduced, and a manual mix override mid-crossfade could be silently overwritten by the MIX rollback phase restoring armedMix. DjAssistTransitionPlan now captures armedToPlaying/armedToSynced from the guard snapshot at arm() time (DjAssistEngine::armTransition). DjAssistBridge::nextRollbackPhase() is resignatured to take manualMixOccurred/syncOwnedByPlan/startDeckOwnedByPlan instead of raw submitted flags: it skips MIX whenever a manual mix occurred (even if crossfade was submitted), and skips SYNC/STOP_DECK whenever the corresponding step was already true at arm time (an idempotent no-op, not a plan-owned mutation). DjAssistController::tickRollback() computes ownership fresh every tick from the plan baseline and the durable mix-generation counter (see #2). 2. HIGH - recentResults is a bounded/evictable presentation ring, not a durable control channel; a burst of results could evict the final crossfade command's outcome or a manual-override marker before the controller polled it, stranding the transition or missing an override. Added DjSession::assistTrackCommand()/assistTrackedStatus(): a single controller-owned tracked-command slot, retained until consumed, updated from the same command-finish path loop() already uses (mutex- protected). Added DjSession::assistNonSystemMixGeneration(): a monotonic counter incremented only on non-system SET_MIX apply(), captured at arm() time and compared every guard build - replacing the removed DjAssistBridge::detectManualMixOverride() ring scan entirely. DjAssistSessionActuator (poll/pollCrossfade) and the crossfade final-command wait now use the durable tracked status exclusively. 3. HIGH - refreshCandidateTable() synchronously read one metadata record per DjSession::loop() tick, including full string/provenance decode, directly on the audio/session loop; resolveBoundary()'s phrase lookup rescanned up to 128 records every tick whenever no future phrase existed (a permanently-terminal track). Candidate-table hydration now runs entirely off the session loop: DjAssistController owns a genuine background Task (fillTaskTrampoline/fillWorkerStep) that reads one record at a time with the real (possibly SD-backed) I/O performed outside candidateMutex_, taking the lock only for the brief bookkeeping/array-write; staleness is re-checked under the lock both before and after the read to survive a concurrent library-generation reset. Main-thread scan state (scanCursor_/suggestionCount_) is separate from background-owned fill state and is reset only when candidateTableReady() reports a new generation. Added DjAssistPhraseCacheState/phraseCacheNeedsRescan()/updatePhraseCache() to cache a found-or-terminal phrase result per deck, invalidated only by an identity change, a different deck, a backward seek, or a previously-found boundary being passed - never by forward playback alone while terminal, closing the up-to-128-rescans-per-tick gap. 4. MEDIUM - resolveWaitBoundaryReached() fired as soon as currentFrame >= target with no tolerance, so a boundary could release arbitrarily late relative to the actual audio position. Added DjAssistBridge::evaluateBoundaryArrival(), reusing the established DJ_QUANTIZE_TOLERANCE_FRAMES policy: REACHED only at/within tolerance past the target, MISSED beyond it (never releases late - the caller re-captures the next boundary via resolveBoundary() and keeps waiting), NOT_YET otherwise. Also removes DJ_ASSIST_FILL_RECORDS_PER_TICK, a leftover constant from the old per-tick synchronous fill model that issue #3's background task made dead code. Tests: new/updated coverage in dj_assist_selfcheck.cpp (arm() captures toDeck playing/sync baseline both true and false) and dj_assist_bridge_selfcheck.cpp (nextRollbackPhase ownership-vs-submitted distinction and manual-override-skips-mix; evaluateBoundaryArrival not-yet/reached-at-tolerance/missed-beyond-tolerance; phraseCacheNeedsRescan/updatePhraseCache empty/found-not-yet-passed/ terminal-not-rescanned/identity-or-deck-change/backward-seek). All 8 host CMake self-check targets (including DjAssistSelfCheck, DjAssistBridgeSelfCheck) pass under C++11 -Wall -Wextra -Werror + ASan/UBSan, and tests/wireless_ui_self_test.mjs (43 checks) passes unchanged. Firmware validated with isolated cm:esp32@1.8.3 (CircuitOS 1.7.1) against Library 78a9f3d7e52fd151dc50872812ae2ed1da308af1: - default: 1,380,014 B flash (38%) / 47,160 B RAM (14%) - wireless (-DJAYD_ENABLE_WIRELESS): 1,974,606 B flash (54%) / 52,856 B RAM (16%) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
djdefi
added a commit
that referenced
this pull request
Aug 23, 2026
…ource-deck sync, close candidate-generation race, add real controller integration tests Round-5 review fixes against 8d387ce (4 issues: 1 HIGH, 3 MEDIUM): 1. guardOk() now relinquishes toDeckStartOwnedByPlan/toDeckSyncOwnedByPlan the instant it detects a user divergence on that exact property, before failing - a user re-asserting play/sync on the target deck after the plan's own step already applied no longer causes rollback to stop/ release the user's own newer intent. guardOk() is now non-const to support this. 2. guardOk() also compares the SOURCE deck's (fromDeck) sync intent generation every tick (previously unguarded) - a non-system sync command on the outgoing deck is a genuine manual override and now aborts the transition instead of silently continuing past it. There is no rollback ownership for fromDeck sync (the plan never touches it), so this is a plain abort with no additional ownership bookkeeping. 3. Closed the remaining candidate-generation check-then-lock race: DjSession::libraryGeneration is now std::atomic<uint32_t> and is bumped BEFORE refreshLibraryMetadata()/invalidateLibraryMetadata() mutate the reader (still inside metadataMutex for sequencing, not required for the lock-free load itself). assistLibraryGeneration() is now a lock-free read, safe to call a second time from inside DjAssistController's own candidateMutex_ critical section with no lock-ordering/deadlock risk. assistTrackEntry() captures its revision under the SAME metadataMutex acquisition as the entry read (not a separate probe). fillWorkerStep()'s commit path re-checks both the entry's captured revision AND a fresh in-lock live-generation reload AND fillCursor_==cursor before writing, discarding (not committing) any record whose generation moved in any of these windows; candidateTableReady() re-loads the live generation inside its own lock immediately before deciding readiness. 4. Added DjAssistSessionPort (src/DjAssist/DjAssistSessionPort.h): the Arduino-free interface DjAssistController now depends on instead of a concrete DjSession - DjSession implements it unchanged in behavior. This lets the real, unmodified DjAssistController.cpp/DjAssistEngine.cpp be exercised by a new host test, DjAssistIntegrationSelfCheck (tests/dj_assist_integration_selfcheck.cpp), against a fake DjAssistSessionPort whose command admission reuses the exact real, shared admitAssistCommand()/DjCommandQueue/DjCommandResults/ DjAssistTrackedCommand/DjAssistIntentGenerations primitives DjSession itself uses - not a parallel reimplementation. Seven scenarios, all passing under g++ -std=c++11 -Wall -Wextra -Werror -fsanitize=address,undefined: full default transition (arm->boundary->tempo-lock->sync->crossfade-> stop->release->complete), cancel/rollback restoring only plan-owned state, user play-divergence relinquishing rollback ownership (issue #1), source-deck sync divergence aborting the transition (issue #2), manual mix mid-crossfade aborting with rollback skipping the mix phase, queue-wide supersede/removal through the real admission coordinator, and the candidate-fill generation race (issue #3) being caught and discarded. Host stubs (tests/host_stubs/{Arduino.h,Sync/Mutex.h, Util/Task.h,host_arduino_shim.cpp}) provide the minimal Arduino/ CircuitOS surface DjAssistController.cpp needs (micros/delay/ps_malloc, Mutex, Task) without pulling in real ESP32 headers. CMakeLists.txt: added the DjAssistIntegrationSelfCheck target (same -Wall -Wextra -Werror + conditional ASan/UBSan treatment as the other DjAssist self-checks), building the real DjAssistController.cpp/ DjAssistEngine.cpp/DjAssistScoring.cpp/DjAssistSessionBridge.cpp against the new test and host stubs (BEFORE on the include path so the stubs win over any real CircuitOS headers on a fully-configured machine). Preserves all prior approved commits unamended (boundary tolerance, 4 KiB response bound, durable tracked-slot routing, admission-time intent generations, queue-wide origin-priority scan, LOCK_TEMPO/ENABLE_SYNC ownership unification, phrase cache metadata-revision keying). Testing: - DjAssistSelfCheck, DjAssistBridgeSelfCheck, DjSessionSelfCheck, DjAssistIntegrationSelfCheck, LibraryIndexSelfCheck, DjBeatEngineSelfCheck, MetadataIntegrationSelfCheck, WirelessApiSelfCheck: all pass via CMake + ctest under ASan/UBSan. - Full isolated arduino-cli build against JayD-Library 78a9f3d (cm:esp32@1.8.3 / CircuitOS 1.7.1): default: 1,384,114 B flash / 47,160 B RAM (round-4: 1,381,390 / 47,160) wireless: 1,485,306 B flash / 52,768 B RAM (round-4: 1,481,442 / 52,768) Auto DJ remains blocked/out of scope for this PR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.
SongListcurrently allocates aScrollLayout/LinearLayoutplus oneListItemsprite/object per discovered track before returning. This keeps therecursive synchronous browse contract from CircuitMess#42, but
stores paths in a bounded PSRAM-backed payload and draws only five visible rows
into the existing screen sprite.
Limits are explicit: 4,096 tracks, 255 bytes per path, and 128 KiB total path
payload. Hitting a content limit keeps the indexed prefix selectable and shows
SD card LIMIT; allocation failure clears partial state and showsMemory error!. The buffers are compacted after scanning and freed on refreshor destruction. No persistent index is added here.
Origin trail:
31132f1introduced the SongList object tree in
src/Screens/SongList/SongList.cpp.166501cadded file discovery; recursive traversal followed in
f262065.1d664d3added hidden-file filtering.
805fd31added per-
ListItemselected-name scrolling.c6e611apreserves the live mixer while this synchronous screen is open. This PR is
based directly on that commit and does not change
MixScreenownership.Validation:
arduino-cli compile --fqbn cm:esp32:jaydwithJayD-Library#19 passes.
Against
c6e611a, flash is 1,762,826 bytes vs. 1,764,870 (-2,044), and staticRAM is 47,152 bytes vs. 47,160 (-8). A Jay-D was detected, but runtime
heap/open-close measurements were not collected because its serial port was
already owned by another session's flashing process; no hardware heap claim is
made.
Follow-up, intentionally out of scope: persistent
/.jayd-library.idxcreation/invalidation.