diff --git a/ScStwLibraries/headers/ScStw.hpp b/ScStwLibraries/headers/ScStw.hpp index e7584dd..d7666b1 100644 --- a/ScStwLibraries/headers/ScStw.hpp +++ b/ScStwLibraries/headers/ScStw.hpp @@ -128,7 +128,8 @@ public: InternalError = 950, InternalErrorTimerOperationFailed = 951, ApiVersionNotSupportedError = 952, - CompetitionModeProhibitsThisError = 953 + CompetitionModeProhibitsThisError = 953, + TimersNotReadyError = 501 /*!< One or more timer is not ready */ }; Q_ENUM(ScStw::StatusCode) diff --git a/ScStwLibraries/headers/scstwrace.h b/ScStwLibraries/headers/scstwrace.h index 17213b1..2feff1b 100644 --- a/ScStwLibraries/headers/scstwrace.h +++ b/ScStwLibraries/headers/scstwrace.h @@ -56,8 +56,7 @@ class ScStwRace : public QObject public: explicit ScStwRace(QObject *parent = nullptr); - enum RaceState { IDLE, STARTING, WAITING, RUNNING, STOPPED }; - // will become IDLE, PREPAIRING, WAITING, STARTING, RUNNING, STOPPED later + enum RaceState { IDLE, PREPAIRING, WAITING, STARTING, RUNNING, STOPPED }; Q_ENUM(RaceState) enum StartAction { None = -1, AtYourMarks = 0, Ready = 1, Start = 2 }; @@ -80,6 +79,8 @@ private: QTimer *nextActionTimer; QEventLoop *nextActionLoop; + QTimer *climberReadyWaitTimer; + QEventLoop *climberReadyWaitLoop; // sounds ScStwSoundPlayer * soundPlayer; @@ -141,7 +142,8 @@ private slots: void refreshTimerStates(); void handleTimerWantsToBeDisabledChange(ScStwTimer* timer, bool wantsToBeDisabled); int handleFalseStart(); - bool playSoundsAndStartTimers(StartAction thisAction); + bool playSoundsAndStartTimers(); + bool doDelayAndSoundOfStartAction(StartAction action, double *timeOfSoundPlaybackStart = nullptr); void enableAllTimers(); /** diff --git a/ScStwLibraries/headers/scstwtimer.h b/ScStwLibraries/headers/scstwtimer.h index 51561fa..25e9fda 100644 --- a/ScStwLibraries/headers/scstwtimer.h +++ b/ScStwLibraries/headers/scstwtimer.h @@ -25,6 +25,9 @@ #include #include "ScStw.hpp" +class ScStwRace; +class ScStwRemoteRace; + /*! * \brief The ScStwTimer class is used for advanced time measurement. * @@ -70,6 +73,9 @@ public: */ explicit ScStwTimer(QObject *parent = nullptr, bool directControlEnabled = false, QString letter = "" ); + friend class ScStwRace; + friend class ScStwRemoteRace; + /*! * \brief The TimerState enum contains all state the timer can be in */ @@ -82,7 +88,7 @@ public: LOST, /*!< Timer has lost */ FAILED, /*!< A false start occured */ CANCELLED, /*!< Timer was cancelled */ -// INCIDENT, /*!< There was a technical incident */ + INCIDENT, /*!< There was a technical incident (most likely a disconnected extension) */ DISABLED /*!< Timer is disabled */ }; Q_ENUM(TimerState); @@ -375,6 +381,15 @@ protected slots: */ void setWantsToBeDisabled(bool wantsToBeDisabled); + /*! + * \brief Function to set the timer into INCIDENT state immidieately + * + * The current state of the timer will be ignored! It can only get out of this state by calling ScStwTimer::reset + * + * \see reset + */ + void technicalIncident(); + signals: /*! diff --git a/ScStwLibraries/sources/scstwlibraries.cpp b/ScStwLibraries/sources/scstwlibraries.cpp index 2c1c0fb..c557e26 100644 --- a/ScStwLibraries/sources/scstwlibraries.cpp +++ b/ScStwLibraries/sources/scstwlibraries.cpp @@ -38,6 +38,8 @@ void ScStwLibraries::init() { qmlRegisterUncreatableType("de.itsblue.ScStw", 2, 0, "ScStwSetting", "The ScStwSetting is manage by a ScStwSettings instance and therefore not creatable"); #endif #endif + + qRegisterMetaType("ScStw::PadState"); } diff --git a/ScStwLibraries/sources/scstwrace.cpp b/ScStwLibraries/sources/scstwrace.cpp index 12961cb..74a545a 100644 --- a/ScStwLibraries/sources/scstwrace.cpp +++ b/ScStwLibraries/sources/scstwrace.cpp @@ -1,4 +1,4 @@ - /**************************************************************************** +/**************************************************************************** ** ScStw Libraries ** Copyright (C) 2020 Itsblue development ** @@ -28,10 +28,12 @@ ScStwRace::ScStwRace(QObject *parent) : QObject(parent) // configure timer that handles the delay between the start commands this->nextActionTimer = new QTimer(this); nextActionTimer->setSingleShot(true); - this->nextActionLoop = new QEventLoop(); + this->nextActionLoop = new QEventLoop(this); this->nextStartAction = None; + this->climberReadyWaitLoop = new QEventLoop(this); connect(this->nextActionTimer, &QTimer::timeout, this->nextActionLoop, &QEventLoop::quit); + connect(this->nextActionTimer, &QTimer::timeout, this->climberReadyWaitLoop, &QEventLoop::quit); connect(this, &ScStwRace::nextStartActionChanged, this, &ScStwRace::nextStartActionDetailsChanged); // write default settings @@ -51,15 +53,45 @@ int ScStwRace::start(bool asyncronous) { return ScStw::CurrentStateNotVaildForOperationError; } - qDebug() << "+ [INFO] starting race"; + qDebug() << "[INFO][RACE] checking timers"; + foreach (ScStwTimer *timer, this->timers) { + if(timer->getState() == ScStwTimer::DISABLED) + continue; - this->setState(STARTING); + if(timer->getReadyState() != ScStwTimer::ClimberIsNotReady) { + + if(this->allowAutomaticTimerDisable) { + timer->setDisabled(true); + continue; + } + + qDebug() << "Timer ready state is: " << timer->getReadyState(); + + timer->technicalIncident(); + + foreach (ScStwTimer *subTimer, this->timers) { + if(timer != subTimer && subTimer->getReadyState() != ScStwTimer::ClimberIsNotReady) + subTimer->technicalIncident(); + else if(timer != subTimer) + subTimer->setState(ScStwTimer::CANCELLED); + } + + this->setState(STOPPED); + + qDebug() << "[ERROR][RACE] Could not start due to not-ready timers"; + + return ScStw::TimersNotReadyError; + } + } + + qDebug() << "[INFO][RACE] starting race"; + this->setState(PREPAIRING); if(asyncronous) { - QTimer::singleShot(1, [=](){this->playSoundsAndStartTimers(None);}); + QTimer::singleShot(1, [=](){this->playSoundsAndStartTimers();}); } else - this->playSoundsAndStartTimers(None); + this->playSoundsAndStartTimers(); return ScStw::Success; } @@ -188,34 +220,70 @@ int ScStwRace::handleFalseStart() { return returnCode; } -bool ScStwRace::playSoundsAndStartTimers(StartAction thisAction) { - if(this->state != STARTING) +bool ScStwRace::playSoundsAndStartTimers() { + if(this->state != PREPAIRING) return true; + // The check if all timers are ready has already happened at this point + if(!this->doDelayAndSoundOfStartAction(AtYourMarks)) { + qDebug() << "At marks sound returned false!"; + return false; + } + + qDebug() << "NOW IN WAITING"; + this->setState(WAITING); + + // do climber readiness tests + this->nextStartAction = Ready; + emit this->nextStartActionChanged(); + + // if the automatic ready tone is enabled, wait for the climbers to become ready + if(this->startActionSettings.contains(Ready) && this->startActionSettings[Ready]["Enabled"].toBool()) { + + qDebug() << "[RACE][INFO] Now waiting for climbers"; + + // get delay + int minimumReadyDelay = 1000; + if(this->startActionSettings[Ready]["Delay"].toInt() > 1000 || !this->allowAutomaticTimerDisable) + minimumReadyDelay = this->startActionSettings[Ready]["Delay"].toInt(); + + // wait for climbers to become ready initially + bool allClimbersReady = false; + while (!allClimbersReady) { + allClimbersReady = true; + foreach (ScStwTimer *timer, this->timers) { + if(timer->getReadyState() != ScStwTimer::IsReady) + allClimbersReady = false; + } + + if(!allClimbersReady) + this->climberReadyWaitLoop->exec(); + } + + qDebug() << "[RACE][DEBUG] Initial wait finished"; + + // wait for all climbers to be ready for the ReadyActionDelay, but at least one second continuosly + // the climber ready wait loop will also quit, if the climber steps of the pad + // -> wait for both climbers to stand on the pad for at least one second + do { + this->nextActionTimer->stop(); + this->nextActionTimer->start(minimumReadyDelay); + this->climberReadyWaitLoop->exec(); + } while(this->nextActionTimer->remainingTime() > 0); + + qDebug() << "[RACE][DEBUG] Wait finished, starting now!"; + + // play ready tone + if(!this->soundPlayer->play(Ready, this->soundVolume)) + return false; + } + + // enter starting state + this->setState(STARTING); + + // play start tone double timeOfSoundPlaybackStart; - if(this->startActionSettings.contains(thisAction) && this->startActionSettings[thisAction]["Enabled"].toBool()) { - if(!this->soundPlayer->play(thisAction, this->soundVolume, &timeOfSoundPlaybackStart)) - return false; - if(this->state != STARTING) - return true; - } - - if(thisAction < Start) { - this->nextStartAction = StartAction(thisAction + 1); - qDebug() << "next action is: " << nextStartAction; - - int nextActionDelay = -1; - if(this->startActionSettings.contains(this->nextStartAction) && this->startActionSettings[this->nextStartAction]["Enabled"].toBool()) { - nextActionDelay = this->startActionSettings[this->nextStartAction]["Delay"].toInt(); - } - - // perform next action - this->nextActionTimer->start(nextActionDelay > 0 ? nextActionDelay:1); - if(nextActionDelay > 0) - emit this->nextStartActionChanged(); - this->nextActionLoop->exec(); - return this->playSoundsAndStartTimers(this->nextStartAction); - } + this->doDelayAndSoundOfStartAction(Start, &timeOfSoundPlaybackStart); // perform start @@ -250,6 +318,32 @@ bool ScStwRace::playSoundsAndStartTimers(StartAction thisAction) { } +bool ScStwRace::doDelayAndSoundOfStartAction(ScStwRace::StartAction action, double *timeOfSoundPlaybackStart) { + if(this->startActionSettings.contains(action) && this->startActionSettings[action]["Enabled"].toBool()) { + + this->nextStartAction = action; + emit this->nextStartActionChanged(); + + if(action != Start && this->startActionSettings[action]["Delay"].toInt() > 0) { + // perform the delay before the start + + // get delay + int thisActionDelay = this->startActionSettings[action]["Delay"].toInt(); + + // perform next action + if(thisActionDelay > 0) { + this->nextActionTimer->start(thisActionDelay); + this->nextActionLoop->exec(); + } + } + + if(!this->soundPlayer->play(action, this->soundVolume, timeOfSoundPlaybackStart)) + return false; + } + + return true; +} + void ScStwRace::setState(RaceState newState) { if(newState != this->state) { qDebug() << "[INFO][RACE] state changed: " << newState; @@ -401,6 +495,7 @@ bool ScStwRace::addTimer(ScStwTimer *timer) { connect(timer, &ScStwTimer::wantsToBeDisabledChanged, this, &ScStwRace::handleTimerWantsToBeDisabledChange); connect(timer, &ScStwTimer::stateChanged, this, &ScStwRace::timersChanged); connect(timer, &ScStwTimer::reactionTimeChanged, this, &ScStwRace::timersChanged); + connect(timer, &ScStwTimer::readyStateChanged, this->climberReadyWaitLoop, &QEventLoop::quit); if(!this->allowAutomaticTimerDisable && timer->getState() == ScStwTimer::DISABLED) timer->setDisabled(false); diff --git a/ScStwLibraries/sources/scstwtimer.cpp b/ScStwLibraries/sources/scstwtimer.cpp index 6a69f52..e2b6bff 100644 --- a/ScStwLibraries/sources/scstwtimer.cpp +++ b/ScStwLibraries/sources/scstwtimer.cpp @@ -64,6 +64,12 @@ bool ScStwTimer::start(double timeOfStart) { } } +void ScStwTimer::technicalIncident() { + qDebug() << "[INFO][TIMER] Timer got a technical incident"; + + this->setState(INCIDENT); +} + void ScStwTimer::handleClimberStart(double timeOfStart) { this->reactionTime = timeOfStart - this->startTime; qDebug() << "+ [INFO][TIMER] reaction time: " << this->reactionTime; @@ -80,7 +86,7 @@ bool ScStwTimer::cancel() { if(!(this->state == IDLE || this->state == STARTING || this->state == RUNNING)) return false; - this->setState(CANCELLED); + this->stop(CancelStop, -1); return true; } @@ -119,6 +125,11 @@ bool ScStwTimer::stop(StopReason reason, double timeOfStop) { this->setState(FAILED); break; } + case CancelStop: { + qDebug() << "[INFO][TIMER] Timer was cancelled"; + this->setState(CANCELLED); + break; + } default: { return false; } @@ -201,6 +212,9 @@ void ScStwTimer::setState(TimerState newState){ if(this->state == DISABLED && newState != IDLE) return; + if(this->state == INCIDENT && newState != IDLE) + return; + if(this->state != newState) { this->state = newState; qDebug() << "+ [INFO][TIMER] timer state changed: " << newState;