diff --git a/trikControl/src/tonePlayer.cpp b/trikControl/src/tonePlayer.cpp index f0d71640f..dd9936069 100644 --- a/trikControl/src/tonePlayer.cpp +++ b/trikControl/src/tonePlayer.cpp @@ -81,6 +81,10 @@ void TonePlayer::play(int freqHz, int durationMs) void TonePlayer::stop() { mTimer.stop(); - mOutput->suspend(); - mOutput->reset(); + // Calling suspend()/reset() on an already-stopped output crashes ALSA with + // snd_pcm_drain assertion failure (pcm == nullptr). + if (mOutput->state() != QAudio::StoppedState) { + mOutput->suspend(); + mOutput->reset(); + } } diff --git a/trikGui/qml/MainMenu.qml b/trikGui/qml/MainMenu.qml index a0c34bd84..545212a04 100644 --- a/trikGui/qml/MainMenu.qml +++ b/trikGui/qml/MainMenu.qml @@ -27,6 +27,15 @@ Rectangle { } function onShowRunningCodeComponent(programName) { + // If a RunningCodeComponent is already on the stack, replace it + // instead of pushing a second one (happens when a new script starts + // before the previous one's hide signal arrives). + if (runningCodeObject !== null) { + if (runningCodeObject === stack.currentItem) { + stack.pop(); + } + runningCodeObject = null; + } var page = stack.push("RunningCodeComponent.qml"); if (page) { if (programName === "direct command") { @@ -88,14 +97,12 @@ Rectangle { // "Object destroyed while signal handler is in progress" crash. var saved = _mainMenuView.graphicsWidgetObject; _mainMenuView.graphicsWidgetObject = null; - Qt.callLater(function () { - if (saved === stack.currentItem) { - stack.pop(); - if (stack.currentItem && stack.currentItem.idList) { - stack.currentItem.idList.focus = true; - } + if (saved === stack.currentItem) { + stack.pop(); + if (stack.currentItem && stack.currentItem.idList) { + stack.currentItem.idList.focus = true; } - }); + } } else { _mainMenuView.graphicsWidgetObject = null; } diff --git a/trikGui/runningCode.cpp b/trikGui/runningCode.cpp index d8c1527a1..e5e14af35 100644 --- a/trikGui/runningCode.cpp +++ b/trikGui/runningCode.cpp @@ -14,6 +14,8 @@ #include "runningCode.h" +#include + using namespace trikGui; RunningCode::RunningCode(Controller &controller, QObject *parent) : QObject(parent), mController(controller) { @@ -36,9 +38,13 @@ void RunningCode::setProgram(const QString &programName, int scriptId) { mProgramName = programName; mScriptId = scriptId; } + void RunningCode::abortScript() { - mController.abortExecution(); + // Defer to the next event loop iteration so the QML signal handler that + // called us (RunningCodeComponent or GraphicsWidget) finishes before abort() + // runs and eventually destroys the component. + QTimer::singleShot(0, &mController, &Controller::abortExecution); } int RunningCode::scriptId() const { return mScriptId; } diff --git a/trikScriptRunner/include/trikScriptRunner/trikJavaScriptRunner.h b/trikScriptRunner/include/trikScriptRunner/trikJavaScriptRunner.h index fab009e4c..d5e5209ae 100644 --- a/trikScriptRunner/include/trikScriptRunner/trikJavaScriptRunner.h +++ b/trikScriptRunner/include/trikScriptRunner/trikJavaScriptRunner.h @@ -57,6 +57,7 @@ public Q_SLOTS: void run(const QString &script, const QString &fileName = "") override; void runDirectCommand(const QString &command) override; void abort() override; + void resetBrick() override; void brickBeep() override; void setWorkingDirectory(const QString &workingDir) override; diff --git a/trikScriptRunner/include/trikScriptRunner/trikPythonRunner.h b/trikScriptRunner/include/trikScriptRunner/trikPythonRunner.h index 27ef43426..2b3704c37 100644 --- a/trikScriptRunner/include/trikScriptRunner/trikPythonRunner.h +++ b/trikScriptRunner/include/trikScriptRunner/trikPythonRunner.h @@ -56,6 +56,7 @@ public Q_SLOTS: void run(const QString &script, const QString &fileName = "") override; void runDirectCommand(const QString &command) override; void abort() override; + void resetBrick() override; void brickBeep() override; void setWorkingDirectory(const QString &workingDir) override; diff --git a/trikScriptRunner/include/trikScriptRunner/trikScriptRunner.h b/trikScriptRunner/include/trikScriptRunner/trikScriptRunner.h index 6feab61f5..36fdae38f 100644 --- a/trikScriptRunner/include/trikScriptRunner/trikScriptRunner.h +++ b/trikScriptRunner/include/trikScriptRunner/trikScriptRunner.h @@ -77,18 +77,33 @@ public Q_SLOTS: /// See corresponding TrikScriptRunnerInterface method void abort() override; /// See corresponding TrikScriptRunnerInterface method + void resetBrick() override; + /// See corresponding TrikScriptRunnerInterface method void brickBeep() override; void setWorkingDirectory(const QString &workingDir) override; private: TrikScriptRunnerInterface * fetchRunner(ScriptType stype); + void runPending(); + + + // For the script for deferred launch if a start signal + // arrives before the previous one has stopped. + struct PendingRun { + QString script; + ScriptType stype; + QString fileName; + }; trikControl::BrickInterface &mBrick; QPointer mMailbox; QPointer mScriptControl; std::vector> mScriptRunnerArray; ScriptType mLastRunner; + bool mScriptRunning = false; + bool mAbortInProgress = false; + QScopedPointer mPendingRun; }; } diff --git a/trikScriptRunner/include/trikScriptRunner/trikScriptRunnerInterface.h b/trikScriptRunner/include/trikScriptRunner/trikScriptRunnerInterface.h index 50a5f8e82..efc98617e 100644 --- a/trikScriptRunner/include/trikScriptRunner/trikScriptRunnerInterface.h +++ b/trikScriptRunner/include/trikScriptRunner/trikScriptRunnerInterface.h @@ -164,6 +164,10 @@ public Q_SLOTS: /// be stopped as well. virtual void abort() = 0; + /// Resets brick hardware (motors, sensors, audio) after script has stopped. + /// Must be called only after completed() signal is received. + virtual void resetBrick() = 0; + /// Plays "beep" sound. virtual void brickBeep() = 0; diff --git a/trikScriptRunner/src/trikJavaScriptRunner.cpp b/trikScriptRunner/src/trikJavaScriptRunner.cpp index f57dcd33a..fc997a2c3 100644 --- a/trikScriptRunner/src/trikJavaScriptRunner.cpp +++ b/trikScriptRunner/src/trikJavaScriptRunner.cpp @@ -104,7 +104,6 @@ void TrikJavaScriptRunner::run(const QString &script, const QString &fileName) { const int scriptId = mMaxScriptId++; QLOG_INFO() << "TrikJavaScriptRunner: new script" << scriptId << "from file" << fileName; - mScriptEngineWorker->stopScript(); if (!fileName.isEmpty()) { mScriptFileNames[scriptId] = fileName; @@ -124,6 +123,12 @@ void TrikJavaScriptRunner::abort() // Ugly and unsafe to call these methods from an incorrect thread, but who cares ... if (mScriptEngineWorker && !mFinishing ) { mScriptEngineWorker->stopScript(); + } +} + +void TrikJavaScriptRunner::resetBrick() +{ + if (mScriptEngineWorker && !mFinishing) { mScriptEngineWorker->resetBrick(); } } diff --git a/trikScriptRunner/src/trikPythonRunner.cpp b/trikScriptRunner/src/trikPythonRunner.cpp index fccfa8bc9..99ccc7cc0 100644 --- a/trikScriptRunner/src/trikPythonRunner.cpp +++ b/trikScriptRunner/src/trikPythonRunner.cpp @@ -83,7 +83,6 @@ TrikPythonRunner::~TrikPythonRunner() void TrikPythonRunner::run(const QString &script, const QString &fileName) { QFileInfo scriptFile = QFileInfo(fileName); - mScriptEngineWorker->stopScript(); mScriptEngineWorker->run(script, scriptFile); } @@ -121,6 +120,12 @@ void TrikPythonRunner::abort() { if (mScriptEngineWorker) { mScriptEngineWorker->stopScript(); + } +} + +void TrikPythonRunner::resetBrick() +{ + if (mScriptEngineWorker) { mScriptEngineWorker->resetBrick(); } } diff --git a/trikScriptRunner/src/trikScriptRunner.cpp b/trikScriptRunner/src/trikScriptRunner.cpp index bcfdcb206..30e2f9799 100644 --- a/trikScriptRunner/src/trikScriptRunner.cpp +++ b/trikScriptRunner/src/trikScriptRunner.cpp @@ -54,6 +54,13 @@ TrikScriptRunner::TrikScriptRunner(trikControl::BrickInterface &brick } mScriptControl->setParent(this); + connect(this, &TrikScriptRunnerInterface::startedScript, + this, [this](const QString &, int) { mScriptRunning = true; }); + connect(this, &TrikScriptRunnerInterface::startedDirectScript, + this, [this](int) { mScriptRunning = true; }); + connect(this, &TrikScriptRunnerInterface::completed, + this, [this](const QString &, int) { mScriptRunning = false; }); + #ifndef TRIK_NOPYTHON // TrikPythonRunner must be initialized early during trikGui startup; // otherwise the first script execution can take over 6 seconds. @@ -161,13 +168,37 @@ TrikScriptRunnerInterface * TrikScriptRunner::fetchRunner(ScriptType stype) void TrikScriptRunner::run(const QString &script, ScriptType stype, const QString &fileName) { + if (mAbortInProgress) { + // Abort is in progress (we're inside wait.exec()). Save as pending — will run after abort finishes. + QLOG_INFO() << "Run deferred, abort in progress"; + mPendingRun.reset(new PendingRun{script, stype, fileName}); + return; + } + abort(); - auto prevRunner = mLastRunner; - auto runner = fetchRunner(stype); - if (prevRunner != stype) { - runner->abort(); + + // Abort() calls runPending() at the end — if a newer run arrived during the wait + // or during resetBrick()'s inner loop, it was already started (mScriptRunning=true). + // Skip this older run to avoid double-starting. + if (mScriptRunning) { + QLOG_INFO() << "Run skipped, pending script was started inside abort"; + return; + } + + // startedScript arrives via queued connection (worker thread), + // so the next run() call could arrive before mScriptRunning becomes true from the signal. + mScriptRunning = true; + fetchRunner(stype)->run(script, fileName); +} + +void TrikScriptRunner::runPending() +{ + if (!mPendingRun) { + return; } - runner->run(script, fileName); + auto pending = *mPendingRun; + mPendingRun.reset(); + run(pending.script, pending.stype, pending.fileName); } void TrikScriptRunner::runDirectCommand(const QString &command) @@ -177,7 +208,41 @@ void TrikScriptRunner::runDirectCommand(const QString &command) void TrikScriptRunner::abort() { + mPendingRun.reset(); + + if (!mScriptRunning) { + return; + } + + if (mAbortInProgress) { + return; + } + + mAbortInProgress = true; + QEventLoop wait; + auto conn = connect(this, &TrikScriptRunnerInterface::completed, + &wait, &QEventLoop::quit); fetchRunner(mLastRunner)->abort(); + // Do not proceed with further script launch code — instead wait for complete + // termination until the completed signal arrives. Since everything is asynchronous, + // exact on-time stopping is not guaranteed, but this works in practice and fixes + // the button-spamming hang in TrikStudio and on the controller. + wait.exec(); + disconnect(conn); + + // Keep mAbortInProgress=true during resetBrick() — it internally spins its own + // QEventLoop (cross-thread invoke). Any run() calls arriving there will be + // deferred as pending and started after abort() fully returns. + fetchRunner(mLastRunner)->resetBrick(); + mAbortInProgress = false; + // If a run() arrived during wait.exec() or resetBrick()'s inner loop, start it now. + // This covers the case where abort() is called directly (not via run()), e.g. from abortExecution(). + runPending(); +} + +void TrikScriptRunner::resetBrick() +{ + fetchRunner(mLastRunner)->resetBrick(); } void TrikScriptRunner::brickBeep()