/**************************************************************************** ** 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) : ScStwRace(nullptr, parent) { } ScStwRace::ScStwRace(ScStwSettings *settings, QObject *parent) : QObject(parent) { this->state = IDLE; this->competitionMode = false; this->autoRefreshTimerText = false; // 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); // init settings this->setSettings(settings); } // -------------------------- // --- 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; } this->refreshCompetitionMode(); 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; } if(this->competitionMode) return this->cancel(); qDebug() << "[INFO][RACE] 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; } ScStw::StatusCode ScStwRace::reset() { if(this->state != STOPPED && this->state != INCIDENT) { return ScStw::CurrentStateNotVaildForOperationError; } qDebug() << "[INFO][RACE] resetting race"; if(this->competitionMode) { for(int i = 0; i < this->timers.length(); i++) this->setTimerDisabled(i, false); } ScStw::StatusCode returnCode = ScStw::Success; foreach(ScStwTimer *timer, this->timers) { if(!timer->reset() && timer->getState() != ScStwTimer::DISABLED && timer->getState() != ScStwTimer::IDLE) { 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; } ScStw::StatusCode ScStwRace::setTimerDisabled(int timerId, bool disabled) { if(timerId < 0 || timerId - 1 > this->timers.length()) return ScStw::ItemNotFoundError; return this->setTimerDisabled(this->timers[timerId], disabled); } ScStw::StatusCode ScStwRace::setTimerDisabled(ScStwTimer* timer, bool disabled) { qDebug() << "[INFO][RACE] Setting timer "<< timer->getLetter() << " to disabled: " << disabled << " this state: " << this->state; if(this->state != IDLE && this->state != WAITING) return ScStw::CurrentStateNotVaildForOperationError; if(!this->timers.contains(timer)) return ScStw::ItemNotFoundError; int enabledTimerCount = 0; foreach(ScStwTimer *timer, this->timers) { if(timer->getState() != ScStwTimer::DISABLED) enabledTimerCount ++; } if(disabled && enabledTimerCount <= 1) return ScStw::LastTimerCannotBeDisabledError; qDebug() << "[INFO][RACE] Setting timer "<< timer->getLetter() << " to disabled: " << disabled; timer->setDisabled(disabled); emit this->timersChanged(); return ScStw::Success; } void ScStwRace::handleTimerReadyStateChange(ScStwTimer::ReadyState readyState) { if(!this->competitionMode || this->state == IDLE || this->state == STOPPED || this->state == INCIDENT ) return; qDebug() << "Some ready state changed: " << readyState; // cancel as a technical incident if extensions are disconnected or run low on battery if(readyState == ScStwTimer::ExtensionIsNotConnected || readyState == ScStwTimer::ExtensionBatteryIsCritical) { this->technicalIncident(); return; } // only continue if the current state is waiting if(this->state == WAITING) { this->startWaitLoop->exit(ScStwRace::LoopReadyStateChangeExit); emit this->timersChanged(); } } bool ScStwRace::playSoundsAndStartTimers() { if(this->state != PREPAIRING) return true; // The check if all timers are ready has already happened at this point ScStwSoundPlayer::PlayResult res = this->doDelayAndSoundOfCurrentStartState(); if(res == ScStwSoundPlayer::Error) { qDebug() << "[ERROR][RACE] error playing at your marks sound"; this->technicalIncident(); return false; } else if(res == ScStwSoundPlayer::Cancelled) { return false; } // check if the start was cancelled if(!this->isStarting()) return false; this->setState(WAITING); // do climber readiness tests // if the automatic ready tone is enabled, wait for the climbers to become ready if(this->competitionMode && this->getSoundEnabledSetting(ScStwSoundPlayer::Ready)) { qDebug() << "[RACE][INFO] Now waiting for climbers"; // get delay int minimumReadyDelay = 1000; if(this->getSoundDelaySetting(ScStwSoundPlayer::Ready) > 1000) minimumReadyDelay = this->getSoundDelaySetting(ScStwSoundPlayer::Ready); 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->setInterval(minimumReadyDelay); 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() << "[DEBUG][RACE] Wait finished, starting now!"; // play ready tone ScStwSoundPlayer::PlayResult res = this->soundPlayer->play(ScStwSoundPlayer::Ready, this->getSoundVolume()); if(res == ScStwSoundPlayer::Error) { qDebug() << "[ERROR][RACE] error playing ready sound"; this->technicalIncident(); return false; } else if(res == ScStwSoundPlayer::Cancelled) { 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 { ScStwSoundPlayer::PlayResult res = this->doDelayAndSoundOfCurrentStartState(); if(res == ScStwSoundPlayer::Error) { qDebug() << "[ERROR][RACE] error playing ready sound"; this->technicalIncident(); return false; } else if(res == ScStwSoundPlayer::Cancelled) { return false; } } // enter starting state this->setState(STARTING); // play start tone double timeOfSoundPlaybackStart; res = this->doDelayAndSoundOfCurrentStartState(&timeOfSoundPlaybackStart); if(res == ScStwSoundPlayer::Error) { qDebug() << "[ERROR][RACE] error playing at your marks sound"; this->technicalIncident(); return false; } else if(res == ScStwSoundPlayer::Cancelled) { return false; } // perform start // start all timers bool startOk = true; foreach(ScStwTimer *timer, this->timers) { if(!timer->start(timeOfSoundPlaybackStart + 3000) && timer->getState() != ScStwTimer::DISABLED) { startOk = false; } } if(!startOk) { qDebug() << "[ERROR][RACE] error staring all timers"; this->technicalIncident(); return false; } if(this->soundPlayer->waitForSoundFinish() == ScStwSoundPlayer::Error) { qDebug() << "[ERROR][RACE] start sound wait error"; this->technicalIncident(); return false; } // check if a false start occured if(!this->isStarting()) return false; this->setState(RUNNING); return true; } ScStwSoundPlayer::PlayResult ScStwRace::doDelayAndSoundOfCurrentStartState(double *timeOfSoundPlaybackStart) { ScStwSoundPlayer::StartSound sound = this->getSoundForState(this->state); if(this->getSoundEnabledSetting(sound)) { if(sound != ScStwSoundPlayer::Start && this->getSoundDelaySetting(sound) > 0) { // perform the delay before the start // get delay int thisSoundDelay = this->getSoundDelaySetting(sound); // perform next action if(thisSoundDelay > 0) { this->startDelayTimer->setInterval(thisSoundDelay); this->startDelayTimer->start(); emit this->currentStartDelayChanged(); if(this->startWaitLoop->exec() == LoopCancelExit) return ScStwSoundPlayer::Cancelled; } } if(!this->isStarting()) return ScStwSoundPlayer::Error; return this->soundPlayer->play(sound, this->getSoundVolume(), timeOfSoundPlaybackStart); } return ScStwSoundPlayer::Success; } 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) { this->refreshCompetitionMode(); } } } void ScStwRace::handleTimerStateChange(ScStwTimer::TimerState newState) { if( newState == ScStwTimer::WON || newState == ScStwTimer::LOST || newState == ScStwTimer::WILDCARD || newState == ScStwTimer::FAILED || this->state == INCIDENT ) return; qDebug() << "[INFO][MAIN] handling timer state change"; // check if the race is over bool raceIsOver = true; int incidentCount = 0; foreach(ScStwTimer * timer, this->timers) { if(timer->getState() < ScStwTimer::WAITING) { // if the timer is not in stoped state raceIsOver = false; } else if(timer->getState() == ScStwTimer::WAITING) this->handleTimerStop(); else if (timer->getState() == ScStwTimer::FAILING) this->handleFalseStart(); else if (timer->getState() == ScStwTimer::INCIDENT) incidentCount ++; } if(incidentCount == this->timers.length()) { this->setState(INCIDENT); this->soundPlayer->play(ScStwSoundPlayer::FalseStart, this->getSoundVolume()); } else if(raceIsOver) this->setState(STOPPED); } void ScStwRace::handleTimerStop() { if(this->state != RUNNING) return; qDebug() << "[INFO][RACE] Got a TIMER STOP"; // find out which timer has won double lowestStoppedTime = -1; // iterate through all timers and find the lowest time that was stopped foreach(ScStwTimer * timer, this->timers) { qDebug() << "Current stopped time is: " << timer->getCurrentTime(); if(!timer->isRunning() && !timer->isDisabled() && (timer->getCurrentTime() <= lowestStoppedTime || lowestStoppedTime < 0)) { // this is the timer with the lowest stopped time lowestStoppedTime = timer->getCurrentTime(); } } qDebug() << "LOWEST Stop time is: " << lowestStoppedTime; // append the timer(s) with the lowest stopped time to the winner list foreach(ScStwTimer * timer, this->timers) { if(timer->getCurrentTime() <= lowestStoppedTime) // this is the timer with the lowest stopped time timer->setResult(ScStwTimer::WON); else timer->setResult(ScStwTimer::LOST); } } void ScStwRace::handleFalseStart() { if(this->state != STARTING && this->state != RUNNING && this->state != STOPPED) return; qDebug() << "[INFO][RACE] Got a FALSE START"; // set lowest to a value that is impossible to reach (start tone is only 3100ms long) double lowestReactionTime = -4000; // iterate through all timers and find the lowest reactiontime that was stopped foreach(ScStwTimer *timer, this->timers) { if( timer->getState() >= ScStwTimer::FAILING && timer->getState() <= ScStwTimer::FAILED && !timer->isDisabled() && ( timer->getReactionTime() < lowestReactionTime || lowestReactionTime == -4000 ) ) { lowestReactionTime = timer->getReactionTime(); } } if(lowestReactionTime == -4000) // no timer has failed return; qDebug() << "LOWEST reaction time is: " << lowestReactionTime; // find out which timer(s) have lost and which get a wildcard foreach(ScStwTimer * timer, this->timers) { qDebug() << "THIS TIMERS reaction time is: " << timer->getReactionTime(); if( timer->getState() >= ScStwTimer::FAILING && timer->getState() <= ScStwTimer::FAILED && !timer->isDisabled() && timer->getReactionTime() <= lowestReactionTime ) { // this is the timer with the lowest stopped time timer->setResult(ScStwTimer::FAILED); qDebug() << "FOUND BAD TIMER"; } else { timer->setResult(ScStwTimer::WILDCARD); } } if(this->state != STOPPED) { this->setState(STOPPED); this->soundPlayer->cancel(); this->soundPlayer->play(ScStwSoundPlayer::FalseStart, this->getSoundVolume()); } } void ScStwRace::technicalIncident() { bool raceIsRunning = this->state == RUNNING; qDebug() << "[ERROR][RACE] Got a technical incident, state is: " << this->state; if(!raceIsRunning) { this->startWaitLoop->exit(LoopCancelExit); if(this->state == STARTING) { this->soundPlayer->cancel(); this->soundPlayer->play(ScStwSoundPlayer::FalseStart, this->getSoundVolume()); } this->setState(INCIDENT); } foreach(ScStwTimer *timer, this->timers) { if(raceIsRunning && timer->getReadyState() == ScStwTimer::ExtensionBatteryIsCritical) continue; else if(timer->getReadyState() == ScStwTimer::ExtensionIsNotConnected || timer->getReadyState() == ScStwTimer::ExtensionBatteryIsCritical) timer->technicalIncident(); else if(!raceIsRunning) timer->setState(ScStwTimer::CANCELLED); } } // ------------------------ // --- 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::ExtensionBatteryIsCritical) { this->technicalIncident(); 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 && timer->getReadyState() != ScStwTimer::IsDisabled) 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) { qDebug() << "Handling timer wants to be disabled"; if(this->competitionMode) return; if(this->state == IDLE) { qDebug() << "Handling timer wants to be disabled: " << wantsToBeDisabled; this->setTimerDisabled(timer, wantsToBeDisabled); } } void ScStwRace::refreshCompetitionMode() { if(this->state != IDLE) return; bool currentCompetitionMode = false; if(this->settings != nullptr) currentCompetitionMode = this->settings->readSetting(ScStwSettings::CompetitionModeSetting).toBool(); if(this->competitionMode != currentCompetitionMode) { qDebug() << "[INFO][RACE] Setting competition mode to " << currentCompetitionMode; this->competitionMode = currentCompetitionMode; } if(this->competitionMode) { foreach (ScStwTimer *timer, this->timers) { timer->setDisabled(false); } } else { foreach(ScStwTimer* timer, this->timers) { if( (timer->getWantsToBeDisabled() && timer->getState() != ScStwTimer::DISABLED) || (!timer->getWantsToBeDisabled() && timer->getState() == ScStwTimer::DISABLED) ) this->handleTimerWantsToBeDisabledChange(timer, timer->getWantsToBeDisabled()); } } } QVariantMap ScStwRace::getCurrentStartDelay() { QVariantMap currentStartDelay = { {"total", -1.0}, {"progress", -1.0} }; switch (this->state) { case WAITING: if(!this->getSoundEnabledSetting(ScStwSoundPlayer::Ready)) return currentStartDelay; if(!this->getIsReadyForNextState()) { // indicate that we are waiting for climbers and the progress shall be zero currentStartDelay["progress"] = 0; return currentStartDelay; } break; case PREPAIRING: { if(!this->getSoundEnabledSetting(ScStwSoundPlayer::AtYourMarks)) 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(); if(remaining < 0) return currentStartDelay; currentStartDelay["total"] = this->startDelayTimer->interval(); currentStartDelay["progress"] = 1 - (remaining / currentStartDelay["total"].toDouble()); if(currentStartDelay["progress"].toDouble() < 0) currentStartDelay["progress"] = 0; return currentStartDelay; } 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::handleTimerStateChange); 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, &ScStwRace::handleTimerReadyStateChange); connect(timer, &ScStwTimer::readyStateChanged, this, &ScStwRace::isReadyForNextStateChanged); if(this->competitionMode && timer->getState() == ScStwTimer::DISABLED) timer->setDisabled(false); else if(!this->competitionMode) this->handleTimerWantsToBeDisabledChange(timer, timer->getWantsToBeDisabled()); return true; } ScStwRace::RaceState ScStwRace::getState() { return this->state; } QList ScStwRace::getTimers() { return this->timers; } double ScStwRace::getSoundVolume() { if(this->settings == nullptr) return 1; return this->settings->readSetting(ScStwSettings::SoundVolumeSetting).toDouble(); } ScStwSoundPlayer::StartSound ScStwRace::getSoundForState(ScStwRace::RaceState state) { switch (state) { case PREPAIRING: return ScStwSoundPlayer::AtYourMarks; break; case WAITING: return ScStwSoundPlayer::Ready; break; case STARTING: return ScStwSoundPlayer::Start; break; default: return ScStwSoundPlayer::StartSound(-1); } } bool ScStwRace::getSoundEnabledSetting(ScStwSoundPlayer::StartSound sound) { ScStwSettings::BaseStationSetting soundEnabledSetting; switch (sound) { case ScStwSoundPlayer::AtYourMarks: soundEnabledSetting = ScStwSettings::AtYourMarksSoundEnableSetting; break; case ScStwSoundPlayer::Ready: soundEnabledSetting = ScStwSettings::ReadySoundEnableSetting; break; case ScStwSoundPlayer::Start: return true; break; default: return false; } if(this->settings == nullptr) return false; return this->settings->readSetting(soundEnabledSetting).toBool(); } int ScStwRace::getSoundDelaySetting(ScStwSoundPlayer::StartSound sound) { ScStwSettings::BaseStationSetting soundDelaySetting; switch (sound) { case ScStwSoundPlayer::AtYourMarks: soundDelaySetting = ScStwSettings::AtYourMarksSoundDelaySetting; break; case ScStwSoundPlayer::Ready: soundDelaySetting = ScStwSettings::ReadySoundDelaySetting; break; case ScStwSoundPlayer::Start: return 0; break; default: return -1; } if(this->settings == nullptr) return -1; return this->settings->readSetting(soundDelaySetting).toInt(); } 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("letter", timer->getLetter()); tmpTimer.insert("readyState", timer->getReadyState()); tmpTimer.insert("text", timer->getText()); tmpTimers.append(tmpTimer); } return tmpTimers; } QVariantMap ScStwRace::getDetails() { QVariantMap tmpDetails; tmpDetails.insert("state", this->getState()); tmpDetails.insert("competitionMode", this->competitionMode); tmpDetails.insert("readySoundEnabled", this->getSoundEnabledSetting(ScStwSoundPlayer::Ready)); tmpDetails.insert("currentStartDelay", this->getCurrentStartDelay()); tmpDetails.insert("timers", this->getTimerDetailList()); if(this->state == WAITING) tmpDetails.insert("isReadyForNextState", this->getIsReadyForNextState()); else tmpDetails.insert("isReadyForNextState", true); return tmpDetails; } bool ScStwRace::isStarting() { return this->state == PREPAIRING || this->state == WAITING || this->state == STARTING; } bool ScStwRace::getCompetitionMode() { return this->competitionMode; } bool ScStwRace::getReadySoundEnabled() { return this->getSoundEnabledSetting(ScStwSoundPlayer::Ready); } ScStwSettings* ScStwRace::getSettings() { return this->settings; } void ScStwRace::setSettings(ScStwSettings* settings) { if(settings == this->settings) return; this->settings = settings; this->refreshCompetitionMode(); emit this->settingsChanged(); } bool ScStwRace::getAutoRefreshTimerText() { return this->autoRefreshTimerText; } void ScStwRace::setAutoRefreshTimerText(bool autoRefresh) { if(autoRefresh == this->autoRefreshTimerText) return; this->autoRefreshTimerText = autoRefresh; if(this->autoRefreshTimerText) { this->timerTextRefreshTimer = new QTimer(this); this->timerTextRefreshTimer->setInterval(1); this->timerTextRefreshTimer->connect( this->timerTextRefreshTimer, &QTimer::timeout, [=] { // refresh timer text if(this->getState() == ScStwRace::RUNNING) { emit this->timersChanged(); } // refresh next start action delay progress if(this->getState() == ScStwRace::WAITING || this->getState() == ScStwRace::PREPAIRING) { emit this->currentStartDelayChanged(); } } ); this->timerTextRefreshTimer->start(); } else if(this->timerTextRefreshTimer != nullptr) { this->timerTextRefreshTimer->deleteLater(); } emit this->autoRefreshTimerTextChanged(); }