/** * AES fire automaton - scheduling fire patterns for multiple effectors * $Id$ * $HeadURL$ * $Author$ */ #include #include #include #include #include #include #include "fire_automaton.h" namespace AreaEffectSimulation { FireAutomaton::FireAutomaton(QObject *parent) : QObject(parent) , m_nextIdx(0) , m_tick(0) , m_durationMSecs(0) , m_intervalMSecs(0) , m_remainingTickMSecs(0) , m_state(State::Uninitialized) , m_logger(getLogger("Utils.FireAutomaton")) { m_timer.setSingleShot(true); connect(&m_timer, &QTimer::timeout, this, &FireAutomaton::nextTick, Qt::UniqueConnection); } /** * @brief Initializes the fire automaton and derives a fire sequence from given parameters * * @param time Time used for the fire sequence calculation in milliseconds * @param timeType Type of the given time * @param rounds Number of rounds to be fired * @param effectorIds Ids of the effector that shall be used to fire rounds * @param pattern Fire pattern to be used * @return true when the automaton is succesfully initialized */ bool FireAutomaton::init(int32_t time, TimeType type, int32_t rounds, const QVector& effectorIds, const QString& pattern) { if(m_state != State::Stopped && m_state != State::Uninitialized) { ERROR_LOG(m_logger) << " init failed - fire automaton must be in state unitialized or stopped."; return false; } if(time < 0) { ERROR_LOG(m_logger) << " init failed - time invalid."; return false; } if(effectorIds.isEmpty()) { ERROR_LOG(m_logger) << " init failed - effector Ids invalid."; return false; } if(rounds <= 0) { ERROR_LOG(m_logger) << " init failed - number of rounds invalid."; return false; } if(pattern.isEmpty()) { ERROR_LOG(m_logger) << " init failed - invalid fire pattern."; return false; } m_effectorIds = effectorIds; m_tick = 0; m_remainingTickMSecs = 0; m_nextIdx = 0; if(!readPattern(pattern)) { ERROR_LOG(m_logger) << " init failed - firing pattern invalid."; return false; } if(!calculateSchedule(rounds)) { ERROR_LOG(m_logger) << " init failed - firing schedule invalid."; return false; } const int32_t maxTick = m_schedule.size(); switch(type) { case TimeType::Cadence: m_intervalMSecs = time; m_durationMSecs = (maxTick - 1) * time; break; case TimeType::Duration: m_durationMSecs = time; if(maxTick == 1) { m_intervalMSecs = 0; } else { m_intervalMSecs = m_durationMSecs / (maxTick - 1); // ensure atleast 1 ms spacing between ticks if(m_intervalMSecs <= 0) { m_intervalMSecs = 1; } } break; } setState(State::Stopped); return true; } void FireAutomaton::setState(State state) { m_state = state; } void FireAutomaton::nextTick() { QVector fireIds = getEffectorIds(m_tick); for(auto id : fireIds) { emit fire(id); } ++m_tick; if(m_tick >= m_schedule.size()) { stop(); emit finished(); return; } if(m_state != State::Stepping) { m_timer.start(m_intervalMSecs); } } /** * @brief Starts the fire automaton's sequence derived from the fire pattern and parameters * * The sequence can only be started in state stopped and sets the state to running. After the sequence is finished, the state will be set to stopped. * * @return True when the automaton is succesfully resumed */ bool FireAutomaton::start() { if(m_state != State::Stopped) { ERROR_LOG(m_logger) << " start failed - fire automaton must be in state stopped."; return false; } setState(State::Running); m_timer.start(0); return true; } /** * @brief Resumes a previously stopped fire automaton * * In the state paused the fire automaton sequence execution will be resumed and the state set to running. * * @return True when the automaton is succesfully resumed */ bool FireAutomaton::resume() { if(m_state != State::Paused) { ERROR_LOG(m_logger) << " resume failed - fire automaton must be in state paused."; return false; } setState(State::Running); m_timer.start(m_remainingTickMSecs); return true; } /** * @brief Pauses a running fire automaton sequence * * In the state running the fire automaton sequence execution will be halted and the state set to paused. * * @return True when the automaton is succesfully paused */ bool FireAutomaton::pause() { if(m_state != State::Running) { ERROR_LOG(m_logger) << " pause failed - fire automaton must be in state running."; return false; } m_remainingTickMSecs = m_timer.remainingTime(); m_timer.stop(); setState(State::Paused); return true; } /** * @brief Stops and resets the fire automaton * * Forces the state stopped and resets the automaton sequence execution. */ void FireAutomaton::stop() { m_tick = 0; m_remainingTickMSecs = 0; m_timer.stop(); setState(State::Stopped); } /** * @brief Steps through the derived fire automaton sequence * * Stepping can only be started from the state stopped and sets the state to stepping. After stepping through the whole sequence, the state will be set to stopped. * * @return True when step is succesfully performed */ bool FireAutomaton::performStep() { if(m_state != State::Stopped && m_state != State::Stepping) { ERROR_LOG(m_logger) << " performStep failed - fire automaton must be in state stepping or stopped."; return false; } m_state = State::Stepping; nextTick(); return true; } /** * @brief Returns the current state of the fire automaton * * @return Fire automaton state */ FireAutomaton::State FireAutomaton::getState() const { return m_state; } bool FireAutomaton::readPattern(const QString& pattern) { bool fill = false; #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList elements = pattern.split(QStringLiteral(","), Qt::SkipEmptyParts); #else QStringList elements = pattern.split(QStringLiteral(","), QString::SkipEmptyParts); #endif for(const QString& element : elements) { FireAutomaton::PatternElement patternElement; if(!patternElementFromString(element, patternElement)) { WARNING_LOG(m_logger) << " skipped invalid firing pattern elements."; continue; } if(patternElement == FireAutomaton::PatternElement::NextFill || patternElement == FireAutomaton::PatternElement::AllFill) { if(!fill) { fill = true; } else { ERROR_LOG(m_logger) << " a pattern may contain at most one fill element."; return false; } } m_pattern.append(patternElement); } if(m_pattern.empty()) { ERROR_LOG(m_logger) << " error occured reading firing pattern."; return false; } return true; } bool FireAutomaton::patternElementFromString(const QString& str, FireAutomaton::PatternElement& out) { const QString key = str.trimmed().toLower(); if(key == QStringLiteral("all")) out = FireAutomaton::PatternElement::All; else if(key == QStringLiteral("all*")) out = FireAutomaton::PatternElement::AllFill; else if(key == QStringLiteral("next")) out = FireAutomaton::PatternElement::Next; else if(key == QStringLiteral("next*")) out = FireAutomaton::PatternElement::NextFill; else if(key == QStringLiteral("idle")) out = FireAutomaton::PatternElement::Idle; else return false; return true; } QVector FireAutomaton::getEffectorIds(int32_t tick) const { return m_schedule[tick]; } int32_t FireAutomaton::getMaxTick() const { return m_schedule.size(); } bool FireAutomaton::calculateSchedule(int32_t rounds) { int32_t minCounter = 0; // count needed rounds for(const FireAutomaton::PatternElement& element : m_pattern) { if(element == FireAutomaton::PatternElement::All) minCounter += m_effectorIds.size(); else if(element == FireAutomaton::PatternElement::Next) ++minCounter; } int32_t fillCounter = rounds - minCounter; m_schedule.clear(); for(const FireAutomaton::PatternElement& element : m_pattern) { // stop when all rounds used if(rounds <= 0) break; if(element == FireAutomaton::PatternElement::All) { if(rounds >= m_effectorIds.size()) { m_schedule.append(m_effectorIds); rounds -= m_effectorIds.size(); } else { m_schedule.append(m_effectorIds.mid(0,rounds)); rounds = 0; } } else if(element == FireAutomaton::PatternElement::AllFill && fillCounter > 0) { const int32_t times = fillCounter / m_effectorIds.size(); const int32_t remainder = fillCounter % m_effectorIds.size(); for(int32_t i=0; i < times; i++) { m_schedule.append(m_effectorIds); rounds -= m_effectorIds.size(); } if(remainder > 0) { m_schedule.append(m_effectorIds.mid(0, remainder)); rounds -= remainder; } } else if(element == FireAutomaton::PatternElement::Idle) { m_schedule.append(QVector{}); } else if(element == FireAutomaton::PatternElement::Next) { m_schedule.append(QVector{m_effectorIds[m_nextIdx]}); m_nextIdx = (m_nextIdx + 1) % m_effectorIds.size(); rounds--; } else if(element == FireAutomaton::PatternElement::NextFill && fillCounter > 0) { for(int32_t i = 0; i < fillCounter; i++) { m_schedule.append(QVector{m_effectorIds[i % m_effectorIds.size()]}); rounds--; } } } if(rounds > 0) { ERROR_LOG(m_logger) << " scheduling error - fire pattern does not provide sufficient slots to schedule all rounds. " << rounds; return false; } return true; } } // namespace AreaEffectSimulation