/**************************************************************************** ** ScStw Libraries ** Copyright (C) 2020 Itsblue development ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ****************************************************************************/ #include "../headers/scstwrace.h" ScStwRace::ScStwRace(QObject *parent) : QObject(parent) { this->state = IDLE; // configure the loop that waits for the sound effect to finish this->soundPlayer = new ScStwSoundPlayer(); // configure timer that handles the delay between the start commands this->startDelayTimer = new QTimer(this); startDelayTimer->setSingleShot(true); this->startWaitLoop = new QEventLoop(this); connect(this->startDelayTimer, &QTimer::timeout, this->startWaitLoop, &QEventLoop::quit); connect(this, &ScStwRace::currentStartDelayChanged, this, &ScStwRace::detailsChanged); connect(this, &ScStwRace::timersChanged, this, &ScStwRace::detailsChanged); connect(this, &ScStwRace::stateChanged, this, &ScStwRace::detailsChanged); // write default settings this->startSoundSettings.insert(ScStwSoundPlayer::Start, {{"Enabled", true}, {"Delay", 1}}); this->writeStartSoundSetting(ScStwSoundPlayer::AtYourMarks, false, 0); this->writeStartSoundSetting(ScStwSoundPlayer::Ready, false, 0); this->setSoundVolume(1.0); this->competitionMode = false; } // -------------------------- // --- Main Functionality --- // -------------------------- ScStw::StatusCode ScStwRace::start(bool asyncronous) { if(this->state == WAITING) { if(this->getIsReadyForNextState()) { this->startWaitLoop->exit(LoopManualExit); return ScStw::Success; } else { return ScStw::TimersNotReadyError; } } else if(this->state != IDLE) { return ScStw::CurrentStateNotVaildForOperationError; } if(!this->getIsReadyForNextState()) return ScStw::TimersNotReadyError; this->setState(PREPAIRING); if(asyncronous) { QTimer::singleShot(1, [=](){this->playSoundsAndStartTimers();}); } else this->playSoundsAndStartTimers(); return ScStw::Success; } ScStw::StatusCode ScStwRace::stop() { if(this->state != RUNNING && this->state != STARTING) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "+ [INFO] stopping race"; double timeOfStop = QDateTime::currentMSecsSinceEpoch(); ScStw::StatusCode returnCode = ScStw::Success; foreach(ScStwTimer *speedTimer, this->timers){ if(!speedTimer->stop(timeOfStop) && speedTimer->getState() != ScStwTimer::DISABLED){ returnCode = ScStw::InternalErrorTimerOperationFailed; } } if(returnCode == ScStw::Success) { this->setState(STOPPED); } return returnCode; } void ScStwRace::handleTimerStop() { if(this->state == RUNNING) { // find out which timer has won double lowestStoppedTime = -1; QList timersWhichHaveWonIds; // iterate through all timers and find the lowest time taht was stopped foreach(ScStwTimer * timer, this->timers) { if(timer->getCurrentTime() > 0 && (timer->getCurrentTime() <= lowestStoppedTime || lowestStoppedTime < 0)) { // this is the timer with the lowest stopped time lowestStoppedTime = timer->getCurrentTime(); } } // append the timer(s) with the lowest stopped time to the winner list foreach(ScStwTimer * timer, this->timers) { if(timer->getCurrentTime() > 0 && (timer->getCurrentTime() <= lowestStoppedTime || lowestStoppedTime < 0) && timer->getState() != ScStwTimer::RUNNING ) { // this is the timer with the lowest stopped time timersWhichHaveWonIds.append(timer); } } // update the states of all timers foreach(ScStwTimer * timer, this->timers) { if(timer->getState() == ScStwTimer::RUNNING) continue; if(timersWhichHaveWonIds.contains(timer)) { timer->setResult(ScStwTimer::WON); } else { timer->setResult(ScStwTimer::LOST); } } } } ScStw::StatusCode ScStwRace::reset() { if(this->state != STOPPED && this->state != INCIDENT) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "+ [INFO] resetting race"; ScStw::StatusCode returnCode = ScStw::Success; foreach(ScStwTimer *speedTimer, this->timers){ if(!speedTimer->reset() && speedTimer->getState() != ScStwTimer::DISABLED) { returnCode = ScStw::InternalErrorTimerOperationFailed; } } if(returnCode == ScStw::Success) this->setState(IDLE); this->soundPlayer->cancel(); return returnCode; } ScStw::StatusCode ScStwRace::cancel() { if(this->state != PREPAIRING && this->state != WAITING && this->state != STARTING && this->state != RUNNING) return ScStw::CurrentStateNotVaildForOperationError; qDebug() << "[INFO][RACE] cancelling race"; ScStw::StatusCode returnCode = ScStw::Success; foreach(ScStwTimer *timer, this->timers){ if(!timer->cancel() && timer->getState() != ScStwTimer::DISABLED) returnCode = ScStw::InternalErrorTimerOperationFailed; } if(returnCode != ScStw::Success) return returnCode; this->setState(STOPPED); this->startWaitLoop->exit(LoopCancelExit); this->soundPlayer->cancel(); this->startDelayTimer->stop(); emit this->currentStartDelayChanged(); return returnCode; } int ScStwRace::handleFalseStart() { if(this->getState() != STARTING && this->getState() != RUNNING) return ScStw::CurrentStateNotVaildForOperationError; int returnCode = ScStw::Success; // cancel all running timers foreach(ScStwTimer *timer, this->timers) { if(!timer->cancel() && timer->getState() != ScStwTimer::DISABLED && timer->getState() != ScStwTimer::FAILED) returnCode = ScStw::InternalErrorTimerOperationFailed; } this->setState(STOPPED); this->soundPlayer->cancel(); this->soundPlayer->play(ScStwSoundPlayer::FalseStart, this->soundVolume); return returnCode; } bool ScStwRace::playSoundsAndStartTimers() { if(this->state != PREPAIRING) return true; // The check if all timers are ready has already happened at this point qDebug() << "now playing at marks sound"; if(!this->doDelayAndSoundOfCurrentStartState()) return false; // check if the start was cancelled if(!this->isStarting()) return false; qDebug() << "Now in waiting state"; this->setState(WAITING); // do climber readiness tests // wait until both climbers are ready // if the automatic ready tone is enabled, wait for the climbers to become ready if(this->competitionMode && this->startSoundSettings.contains(ScStwSoundPlayer::Ready) && this->startSoundSettings[ScStwSoundPlayer::Ready]["Enabled"].toBool()) { qDebug() << "[RACE][INFO] Now waiting for climbers"; // get delay int minimumReadyDelay = 1000; if(this->startSoundSettings[ScStwSoundPlayer::Ready]["Delay"].toInt() > 1000) minimumReadyDelay = this->startSoundSettings[ScStwSoundPlayer::Ready]["Delay"].toInt(); this->startDelayTimer->setInterval(minimumReadyDelay); // 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 bool timerTriggered = true; do { if(!this->getIsReadyForNextState()) { this->startDelayTimer->stop(); timerTriggered = false; } else if(this->startDelayTimer->isActive()) { timerTriggered = true; } else { this->startDelayTimer->stop(); this->startDelayTimer->start(); timerTriggered = true; } emit this->currentStartDelayChanged(); int loopExitCode = this->startWaitLoop->exec(); switch (loopExitCode) { case LoopAutomaticExit: break; case LoopManualExit: // prevent manual stop timerTriggered = false; break; case LoopCancelExit: return false; } //qDebug() << "At end of loop: remaining time: " << this->startDelayTimer->remainingTime() << " timer triggered: " << timerTriggered << " ready for next state: " << this->isReadyForNextState(); } while(this->startDelayTimer->remainingTime() > 0 || !timerTriggered || !this->getIsReadyForNextState()); qDebug() << "[RACE][DEBUG] Wait finished, starting now!"; // play ready tone if(!this->soundPlayer->play(ScStwSoundPlayer::Ready, this->soundVolume)) { qDebug() << "Ready sound redturned false!"; this->setState(INCIDENT); return false; } } else if(this->competitionMode) { // wait for climbers and manual start int loopExitCode; do { loopExitCode = this->startWaitLoop->exec(); if(loopExitCode == LoopCancelExit) return false; } while(loopExitCode != LoopManualExit || !this->getIsReadyForNextState()); } else { qDebug() << "now playing ready sound"; if(!this->doDelayAndSoundOfCurrentStartState()) { this->cancel(); return false; } } qDebug() << "now in starting state"; // enter starting state this->setState(STARTING); // play start tone qDebug() << "now playing start sound"; double timeOfSoundPlaybackStart; if(!this->doDelayAndSoundOfCurrentStartState(&timeOfSoundPlaybackStart)) { this->setState(INCIDENT); return false; } // perform start // start all timers bool startOk = true; foreach(ScStwTimer *timer, this->timers){ if(!timer->start(timeOfSoundPlaybackStart + 3100) && timer->getState() != ScStwTimer::DISABLED){ startOk = false; } } if(!startOk) { qDebug() << "[ERROR][START] error staring all timers"; this->setState(INCIDENT); return false; } if(!this->soundPlayer->waitForSoundFinish()) { qDebug() << "[ERROR][START] start sound wait error"; this->setState(INCIDENT); return false; } // check if a false start occured if(!this->isStarting()) return true; this->setState(RUNNING); return true; } bool ScStwRace::doDelayAndSoundOfCurrentStartState(double *timeOfSoundPlaybackStart) { ScStwSoundPlayer::StartSound sound; switch (this->state) { case PREPAIRING: sound = ScStwSoundPlayer::AtYourMarks; break; case WAITING: sound = ScStwSoundPlayer::Ready; break; case STARTING: sound = ScStwSoundPlayer::Start; break; default: return false; } if(this->startSoundSettings.contains(sound) && this->startSoundSettings[sound]["Enabled"].toBool()) { if(sound != ScStwSoundPlayer::Start && this->startSoundSettings[sound]["Delay"].toInt() > 0) { // perform the delay before the start // get delay int thisSoundDelay = this->startSoundSettings[sound]["Delay"].toInt(); // perform next action if(thisSoundDelay > 0) { this->startDelayTimer->setInterval(thisSoundDelay); this->startDelayTimer->start(); emit this->currentStartDelayChanged(); if(this->startWaitLoop->exec() == LoopCancelExit) return false; } } if(!this->isStarting()) return false; if(!this->soundPlayer->play(sound, this->soundVolume, timeOfSoundPlaybackStart)) return false; } return true; } void ScStwRace::setState(RaceState newState) { if(newState != this->state) { qDebug() << "[INFO][RACE] state changed: " << newState; this->state = newState; emit this->stateChanged(newState); if(this->state == IDLE) { // if we changed to IDLE -> handle timer enable / disable if(this->competitionModeChangedRecently && this->competitionMode) { this->enableAllTimers(); this->competitionModeChangedRecently = false; } if(!this->competitionMode) { foreach(ScStwTimer* timer, this->timers) { if(timer->getWantsToBeDisabled() && timer->getState() != ScStwTimer::DISABLED) this->handleTimerWantsToBeDisabledChange(timer, timer->getWantsToBeDisabled()); } } } } } void ScStwRace::refreshTimerStates() { qDebug() << "[INFO][MAIN] refreshing timer states"; // check if the race is over bool raceIsOver = true; foreach(ScStwTimer * timer, this->timers){ if(timer->getState() < ScStwTimer::WON && timer->getState() != ScStwTimer::WAITING){ // if the timer is not in stoped state raceIsOver = false; break; } else if(timer->getState() == ScStwTimer::WAITING) { this->handleTimerStop(); } else if (timer->getState() == ScStwTimer::FAILED) { this->handleFalseStart(); } } if(raceIsOver) this->setState(STOPPED); } // ------------------------ // --- helper functions --- // ------------------------ bool ScStwRace::getIsReadyForNextState() { if(!this->competitionMode) { return true; } switch (this->state) { case IDLE: { foreach (ScStwTimer *timer, this->timers) { if(timer->getState() == ScStwTimer::DISABLED) continue; if(timer->getReadyState() == ScStwTimer::ExtensionIsNotConnected || timer->getReadyState() == ScStwTimer::ExtensionBatteryNotFine) { qDebug() << "Timer ready state is: " << timer->getReadyState(); timer->technicalIncident(); foreach (ScStwTimer *subTimer, this->timers) { if(timer != subTimer && (timer->getReadyState() == ScStwTimer::ExtensionIsNotConnected || timer->getReadyState() == ScStwTimer::ExtensionBatteryNotFine)) subTimer->technicalIncident(); else if(timer != subTimer) subTimer->setState(ScStwTimer::CANCELLED); } this->setState(INCIDENT); qDebug() << "[ERROR][RACE] Could not start due to not-ready timers"; return false; } } break; case WAITING: { foreach (ScStwTimer *timer, this->timers) { if(timer->getReadyState() != ScStwTimer::IsReady) return false; } break; } default: break;; } } return true; } /** * @brief ScStwRace::handleTimerEnable function to enable timers at the right moment to prevent them from bricking the state machine * @param {ScStwExtensionControlledTimer*} timer timer to be enabled */ void ScStwRace::handleTimerWantsToBeDisabledChange(ScStwTimer* timer, bool wantsToBeDisabled) { if(this->competitionMode) return; if(this->state == IDLE) { timer->setDisabled(wantsToBeDisabled); } } void ScStwRace::setCompetitionMode(bool competitionMode) { if(this->competitionMode == competitionMode) return; qDebug() << "Setting competition mode to " << competitionMode; this->competitionMode = competitionMode; if(this->state != IDLE) this->competitionModeChangedRecently = true; else if(this->competitionMode) this->enableAllTimers(); } void ScStwRace::enableAllTimers() { if(this->state != IDLE) return; qDebug() << "ENABLING ALL TIMERS"; foreach (ScStwTimer *timer, this->timers) { timer->setDisabled(false); } } QVariantMap ScStwRace::getCurrentStartDelay() { QVariantMap currentStartDelay = { {"total", -1.0}, {"progress", -1.0} }; switch (this->state) { case WAITING: if(!this->startSoundSettings[ScStwSoundPlayer::Ready]["Enabled"].toBool()) return currentStartDelay; if(!this->getIsReadyForNextState()) // indicate that we are waiting for climbers and the progress shall be zero currentStartDelay["progress"] = -1; return currentStartDelay; case PREPAIRING: { if(!this->startSoundSettings[ScStwSoundPlayer::AtYourMarks]["Enabled"].toBool()) return currentStartDelay; break; } default: return currentStartDelay; break; } // get the total delay and the delay progress of the next action timer double remaining = this->startDelayTimer->remainingTime(); currentStartDelay["total"] = this->startDelayTimer->interval(); if(remaining < 0) { remaining = currentStartDelay["total"].toDouble(); } currentStartDelay["progress"] = 1 - (remaining / currentStartDelay["total"].toDouble()); if(currentStartDelay["progress"].toDouble() < 0) currentStartDelay["progress"] = 0; return currentStartDelay; } bool ScStwRace::writeStartSoundSetting(ScStwSoundPlayer::StartSound sound, bool enabled, int delay) { if(sound != ScStwSoundPlayer::AtYourMarks && sound != ScStwSoundPlayer::Ready) return false; QVariantMap setting = {{"Enabled", enabled}, {"Delay", delay}}; if(!this->startSoundSettings.contains(sound)) this->startSoundSettings.insert(sound, setting); else this->startSoundSettings[sound] = setting; return true; } bool ScStwRace::setSoundVolume(double volume) { if(volume >= 0 && volume <= 1) { this->soundVolume = volume; return true; } else { return false; } } bool ScStwRace::addTimer(ScStwTimer *timer) { if(this->state != IDLE) return false; foreach(ScStwTimer *existingTimer, this->timers) { if(existingTimer == timer) return true; } this->timers.append(timer); connect(timer, &ScStwTimer::stateChanged, this, &ScStwRace::refreshTimerStates); connect(timer, &ScStwTimer::stateChanged, this, &ScStwRace::timersChanged); connect(timer, &ScStwTimer::wantsToBeDisabledChanged, this, &ScStwRace::handleTimerWantsToBeDisabledChange); connect(timer, &ScStwTimer::reactionTimeChanged, this, &ScStwRace::timersChanged); connect(timer, &ScStwTimer::readyStateChanged, this->startWaitLoop, &QEventLoop::quit); connect(timer, &ScStwTimer::readyStateChanged, this, &ScStwRace::handleTimerReadyStateChange); connect(timer, &ScStwTimer::readyStateChanged, this, &ScStwRace::isReadyForNextStateChanged); if(this->competitionMode && timer->getState() == ScStwTimer::DISABLED) timer->setDisabled(false); return true; } ScStwRace::RaceState ScStwRace::getState() { return this->state; } QList ScStwRace::getTimers() { return this->timers; } QVariantList ScStwRace::getTimerDetailList() { QVariantList tmpTimers; foreach(ScStwTimer * timer, this->timers){ QVariantMap tmpTimer; tmpTimer.insert("id", this->timers.indexOf(timer)); tmpTimer.insert("state", timer->getState()); tmpTimer.insert("currentTime", timer->getCurrentTime()); tmpTimer.insert("reactionTime", timer->getReactionTime()); tmpTimer.insert("text", timer->getText()); tmpTimer.insert("letter", timer->getLetter()); tmpTimer.insert("readyState", timer->getReadyState()); tmpTimers.append(tmpTimer); } return tmpTimers; } QVariantMap ScStwRace::getDetails() { QVariantMap tmpDetails; tmpDetails.insert("state", this->getState()); tmpDetails.insert("competitionMode", this->competitionMode); tmpDetails.insert("readySoundEnabled", this->startSoundSettings[ScStwSoundPlayer::Ready]["Enabled"].toBool()); tmpDetails.insert("currentStartDelay", this->getCurrentStartDelay()); tmpDetails.insert("timers", this->getTimerDetailList()); if(this->state == WAITING) tmpDetails.insert("isReadyForNextState", this->getIsReadyForNextState()); return tmpDetails; } bool ScStwRace::isStarting() { return this->state == PREPAIRING || this->state == WAITING || this->state == STARTING; } void ScStwRace::handleTimerReadyStateChange(ScStwTimer::ReadyState readyState) { Q_UNUSED(readyState) // only continue if the current state is waiting if(this->state == WAITING) emit this->timersChanged(); } bool ScStwRace::getCompetitionMode() { return this->competitionMode; } bool ScStwRace::getReadySoundEnabled() { return this->startSoundSettings[ScStwSoundPlayer::Ready]["Enabled"].toBool(); }