Skip to content

Commit 18af8d6

Browse files
knaufinatorclaude
andcommitted
firmware(mini): fix native DEMO playback + boot-hold race; bridge: PLAY grammar + query passthrough
DEMO rig-static root cause: the dedicated prio-6 PlaybackTask on core 1 entered its loop once and was never scheduled again (instrumented: 'TASK up' printed, then zero heartbeats and idx frozen at 0 while prio-7 CueTask on the same core ran fine). Playback now runs INLINE in CueTask (playbackStep): single clock, fractional accumulator paces the file rate against cueLoopHz, ZOH into the shared target. Verified on hardware: idx advances, telemetry varies, loop works. Boot home-hold race: a SOURCE:/PLAY: command arriving during the 3s hold was clobbered by the boot-source apply; runtime choice now wins. Bridge: play 'loop' sends firmware grammar PLAY:LOOP=1 + PLAY:START (bare PLAY:LOOP was 'unknown'); allowlist adds PLAY:STATUS and SOURCE? queries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 906238d commit 18af8d6

2 files changed

Lines changed: 48 additions & 29 deletions

File tree

bridge/control_api.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -328,11 +328,15 @@ def _set_source(self, source: Optional[str]) -> dict:
328328
return {"source": self.source, "motion_gated": self.source != SOURCE_LIVE}
329329

330330
def _play(self, action: Optional[str]) -> dict:
331-
mapping = {"start": "PLAY:START", "stop": "PLAY:STOP", "loop": "PLAY:LOOP"}
332-
cmd = mapping.get(action)
333-
if cmd is None:
331+
# Firmware grammar (main.cpp): PLAY:LOOP=1 enables looping (bare
332+
# PLAY:LOOP is 'unknown'); loop = enable looping AND start playing.
333+
mapping = {"start": ["PLAY:START"], "stop": ["PLAY:STOP"],
334+
"loop": ["PLAY:LOOP=1", "PLAY:START"]}
335+
cmds = mapping.get(action)
336+
if cmds is None:
334337
raise _ApiError(f"bad_play_action:{action}")
335-
self._serial.send_cmd(cmd)
338+
for c in cmds:
339+
self._serial.send_cmd(c)
336340
self.play_state = {"start": "playing", "stop": "stopped",
337341
"loop": "looping"}[action]
338342
self._broadcast(self._status_event())
@@ -344,7 +348,7 @@ def _play(self, action: Optional[str]) -> dict:
344348
# (those have dedicated, state-tracked verbs) or arbitrary strings.
345349
_CMD_ALLOW = ("MCA:", "MCA?", "SERVO:", "TELRATE:", "TELRATE?",
346350
"CONFIG:", "CONFIG?", "BITS:", "BITS?", "SCALE?", "VERSION?",
347-
"FINGERPRINT?")
351+
"FINGERPRINT?", "PLAY:STATUS", "SOURCE?")
348352

349353
def _cmd(self, text: Optional[str]) -> dict:
350354
if not text or not isinstance(text, str):

mini/main/main.cpp

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -590,30 +590,36 @@ static inline void decodeSeqFrame(const uint8_t* p, float raw[6]) {
590590
// PlaybackTask is now a PRODUCER: it paces to the file rate and pushes each
591591
// sample into the shared target (raw M6P2 -> TGT_RAW so CueTask cues it;
592592
// baked M6P1 -> TGT_BAKED). CueTask does the IK/servo at cueLoopHz.
593-
static void PlaybackTask(void* pv) {
594-
(void)pv;
595-
uint16_t rate = (seqRateHz ? seqRateHz : 50);
596-
TickType_t period = pdMS_TO_TICKS(1000 / rate);
597-
if (period < 1) period = 1;
598-
TickType_t last = xTaskGetTickCount();
599-
for (;;) {
600-
if (playbackActive && seqSamples && seqCount) {
601-
float raw[6];
602-
decodeSeqFrame(seqSamples + (size_t)playbackIdx * seqStride, raw);
603-
writeTarget(raw, g_framesRaw ? TGT_RAW : TGT_BAKED);
604-
uint32_t nxt = playbackIdx + 1;
605-
if (nxt >= seqCount) {
606-
if (playbackLoop) {
607-
nxt = seqLoopPoint;
608-
} else {
609-
playbackActive = false;
610-
nxt = 0;
611-
serial_printf("PLAY:DONE\r\n");
612-
}
593+
// Playback producer, run INLINE from CueTask each cue tick (no separate task:
594+
// a dedicated prio-6 PlaybackTask on core 1 was observed entering its loop and
595+
// then never being scheduled again on this build — folding the producer into
596+
// the one task that demonstrably runs also gives a single clock, no cross-task
597+
// handoff). Paces the sequence at seqRateHz via a fractional accumulator and
598+
// pushes the current frame into the shared target as ZOH.
599+
static void playbackStep(uint16_t cueRateHz) {
600+
static float acc = 0.0f;
601+
if (!(playbackActive && seqSamples && seqCount)) { acc = 0.0f; return; }
602+
603+
float raw[6];
604+
decodeSeqFrame(seqSamples + (size_t)playbackIdx * seqStride, raw);
605+
writeTarget(raw, g_framesRaw ? TGT_RAW : TGT_BAKED);
606+
607+
// Advance at the file rate regardless of the cue loop rate.
608+
acc += (float)(seqRateHz ? seqRateHz : 50) / (float)(cueRateHz ? cueRateHz : 50);
609+
while (acc >= 1.0f) {
610+
acc -= 1.0f;
611+
uint32_t nxt = playbackIdx + 1;
612+
if (nxt >= seqCount) {
613+
if (playbackLoop) {
614+
nxt = seqLoopPoint;
615+
} else {
616+
playbackActive = false;
617+
nxt = 0;
618+
serial_printf("PLAY:DONE\r\n");
619+
break;
613620
}
614-
playbackIdx = nxt;
615621
}
616-
vTaskDelayUntil(&last, period);
622+
playbackIdx = nxt;
617623
}
618624
}
619625

@@ -639,6 +645,10 @@ static void CueTask(void* pv) {
639645
}
640646
const float dt = 1.0f / (float)curRate;
641647

648+
// DEMO producer: feed the shared target from the embedded sequence
649+
// (inline; see playbackStep comment). No-op unless playbackActive.
650+
playbackStep(curRate);
651+
642652
float ch[6]; int64_t ts; int fmt;
643653
readTarget(ch, &ts, &fmt);
644654
int64_t age = esp_timer_get_time() - ts;
@@ -1305,7 +1315,7 @@ extern "C" void app_main(void) {
13051315
serial_printf("PLAY: sequence ready -- %u samples @ %uHz, %d-bit, %s, loop@%u (~%.1fs)\r\n",
13061316
(unsigned)seqCount, (unsigned)seqRateHz, seqBits, g_framesRaw ? "raw" : "baked",
13071317
(unsigned)seqLoopPoint, (double)seqCount / (seqRateHz ? seqRateHz : 50));
1308-
xTaskCreatePinnedToCore(PlaybackTask, "Playback", 4096, NULL, 6, NULL, 1);
1318+
// Playback runs inline in CueTask (playbackStep) — no separate task.
13091319
} else {
13101320
serial_printf("PLAY: no valid embedded sequence\r\n");
13111321
}
@@ -1323,8 +1333,13 @@ extern "C" void app_main(void) {
13231333
#endif
13241334

13251335
// ── Apply the boot source (OFF / DEMO / LIVE), default = DEMO ─────
1336+
// Skip if a runtime SOURCE:/PLAY: command already changed the source
1337+
// during the hold (don't clobber an operator's explicit choice).
13261338
Source bootSrc = loadBootSource();
1327-
if (bootSrc == SRC_DEMO && !haveSeq) {
1339+
if (g_source != SRC_OFF) {
1340+
serial_printf("SOURCE: runtime source=%s set during hold - boot default skipped\r\n",
1341+
sourceName(g_source));
1342+
} else if (bootSrc == SRC_DEMO && !haveSeq) {
13281343
serial_printf("SOURCE: boot=DEMO but no sequence -> OFF\r\n");
13291344
setSource(SRC_OFF);
13301345
} else {

0 commit comments

Comments
 (0)