@@ -225,6 +225,40 @@ void RunningBehavior::onAlarm(int code)
225225 emit transition (this , new AlarmBehavior (code));
226226}
227227
228+ void RunningBehavior::doOnMachineState (MachineState state)
229+ {
230+ // Fallback for the missed-Run race: a very short program (e.g. M5 + M30)
231+ // can complete fast enough that the device transitions Idle -> Run -> Idle
232+ // entirely between two status polls. Then onMachineStateChanged never
233+ // fires (polled state matches the cached one), and we'd stay parked in
234+ // NoMoreCommands forever.
235+ //
236+ // doOnMachineState fires on every poll, so it catches this case. We only
237+ // act when onMachineStateChanged would NOT have fired — m_machineState is
238+ // updated AFTER this hook, so a match here means no change was detected.
239+
240+ if (state != MachineState::Idle) {
241+ return ;
242+ }
243+ if (m_stage != Stage::NoMoreCommands) {
244+ return ;
245+ }
246+ if (state != m_communicator->machineState ()) {
247+ // State actually changed — onMachineStateChanged handled it.
248+ return ;
249+ }
250+ if (!m_communicator->isCommandBufferEmpty () || !m_communicator->isQueueEmpty ()) {
251+ return ;
252+ }
253+ if (m_program.hasMoreCommands ()) {
254+ return ;
255+ }
256+
257+ qDebug () << " [Behavior][Running] Polled Idle without state change (missed Run pulse), transitioning to Idle" ;
258+ Core::instance ().timer ().stopExecution ();
259+ emit transition (this , new IdleBehavior ());
260+ }
261+
228262bool RunningBehavior::doAction (const Action &action)
229263{
230264 if (action.type () == Action::Type::Pause && m_stage == Stage::Running) {
0 commit comments