/**************************************************************************** ** 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->nextActionTimer = new QTimer(this); nextActionTimer->setSingleShot(true); 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 this->startActionSettings.insert(Start, {{"Enabled", true}, {"Delay", 1}}); this->writeStartActionSetting(AtYourMarks, false, 0); this->writeStartActionSetting(Ready, false, 0); this->setSoundVolume(1.0); this->allowAutomaticTimerDisable = false; } // -------------------------- // --- Main Functionality --- // -------------------------- int ScStwRace::start(bool asyncronous) { if(this->state != IDLE) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "[INFO][RACE] checking timers"; foreach (ScStwTimer *timer, this->timers) { if(timer->getState() == ScStwTimer::DISABLED) continue; if(timer->getReadyState() == ScStwTimer::ExtensionIsNotConnected || timer->getReadyState() == ScStwTimer::ExtensionBatteryNotFine) { if(this->allowAutomaticTimerDisable) { timer->setDisabled(true); continue; } 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 ScStw::TimersNotReadyError; } } qDebug() << "[INFO][RACE] starting race"; this->setState(PREPAIRING); if(asyncronous) { QTimer::singleShot(1, [=](){this->playSoundsAndStartTimers();}); } else this->playSoundsAndStartTimers(); return ScStw::Success; } int ScStwRace::stop() { if(this->state != RUNNING && this->state != STARTING) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "+ [INFO] stopping race"; double timeOfStop = QDateTime::currentMSecsSinceEpoch(); int 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); } } } } int ScStwRace::reset() { if(this->state != STOPPED && this->state != INCIDENT) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "+ [INFO] resetting race"; int 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); return returnCode; } int ScStwRace::cancel() { if(this->state != PREPAIRING && this->state != WAITING && this->state != STARTING && this->state != RUNNING) return ScStw::CurrentStateNotVaildForOperationError; qDebug() << "[INFO][RACE] cancelling race"; int 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->soundPlayer->cancel(this->soundVolume); this->nextActionTimer->stop(); this->nextActionLoop->quit(); this->climberReadyWaitLoop->quit(); this->nextStartAction = None; 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->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 if(!this->doDelayAndSoundOfStartAction(AtYourMarks)) { return false; } // check if the start was cancelled if(!this->isStarting()) return false; 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(); // check if the start was cancelled if(!this->isStarting()) return false; } 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(); // check if the start was cancelled if(!this->isStarting()) return false; } 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; this->doDelayAndSoundOfStartAction(Start, &timeOfSoundPlaybackStart); // 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"; return false; } if(!this->soundPlayer->waitForSoundFinish()) { qDebug() << "[ERROR][START] start sound wait error"; return false; } // check if a false start occured if(!this->isStarting()) return true; nextStartAction = None; emit this->nextStartActionChanged(); this->setState(RUNNING); return true; } bool ScStwRace::doDelayAndSoundOfStartAction(ScStwRace::StartAction action, double *timeOfSoundPlaybackStart) { if(!this->isStarting()) return false; 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->isStarting()) return false; 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; this->state = newState; emit this->stateChanged(newState); if(this->state == IDLE) { // if we changed to IDLE -> handle timer enable / disable if(this->allowAutomaticTimerDisableChanged && !this->allowAutomaticTimerDisable) { this->enableAllTimers(); this->allowAutomaticTimerDisableChanged = false; } if(this->allowAutomaticTimerDisable) { 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 --- // ------------------------ /** * @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->allowAutomaticTimerDisable) return; if(this->state == IDLE) { timer->setDisabled(wantsToBeDisabled); } } void ScStwRace::setAllowAutomaticTimerDisable(bool allow) { if(this->allowAutomaticTimerDisable == allow) return; qDebug() << "Setting allow automatic timer disable to " << allow; this->allowAutomaticTimerDisable = allow; if(this->state != IDLE) this->allowAutomaticTimerDisableChanged = true; else if(!this->allowAutomaticTimerDisable) this->enableAllTimers(); } void ScStwRace::enableAllTimers() { if(this->state != IDLE) return; qDebug() << "ENABLING ALL TIMERS"; foreach (ScStwTimer *timer, this->timers) { timer->setDisabled(false); } } QVariantList ScStwRace::getNextStartActionDetails() { int nextActionDelay = 0; double nextActionDelayProg = -1; if(this->nextStartAction == AtYourMarks || this->nextStartAction == Ready) { // get the total delay and the delay progress of the next action timer double remaining = this->nextActionTimer->remainingTime(); nextActionDelay = this->startActionSettings[this->nextStartAction]["Delay"].toInt(); if(remaining < 0) { remaining = nextActionDelay; } nextActionDelayProg = 1 - (remaining / nextActionDelay); } return { this->nextStartAction, nextActionDelay, nextActionDelayProg }; } bool ScStwRace::writeStartActionSetting(StartAction action, bool enabled, int delay) { if(action != AtYourMarks && action != Ready) return false; QVariantMap setting = {{"Enabled", enabled}, {"Delay", delay}}; if(!this->startActionSettings.contains(action)) this->startActionSettings.insert(action, setting); else this->startActionSettings[action] = 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::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); return true; } ScStwRace::RaceState ScStwRace::getState() { return this->state; } ScStwRace::StartAction ScStwRace::getNextStartAction() { return this->nextStartAction; } 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()); tmpTimers.append(tmpTimer); } return tmpTimers; } bool ScStwRace::isStarting() { return this->state == PREPAIRING || this->state == WAITING || this->state == STARTING; }