#include "PatternEngine.h" #include namespace timbre_core { void PatternEngine::initializeDefaults() noexcept { for (std::size_t i = 0; i < kMaxPatterns; ++i) { patterns_[i].id = patternIdFromIndex(i); } configuredPatternMask_ = static_cast((1u << kMaxPatterns) - 1u); } std::uint64_t PatternEngine::readClock() const { if (clockInterface_ != nullptr) { return clockInterface_->nowMs(); } if (clock_) { return clock_(); } return 0; } bool PatternEngine::validPattern(const Pattern& pattern) const noexcept { return isPatternId(pattern.id) && ScheduleValidator::validatePattern(pattern).valid; } void PatternEngine::setPattern(const Pattern& pattern) noexcept { if (!validPattern(pattern)) { return; } const std::size_t index = patternIndex(pattern.id); if (index < patterns_.size()) { patterns_[index] = pattern; configuredPatternMask_ = static_cast( configuredPatternMask_ | (1u << index)); } } void PatternEngine::setPatterns(const AppState& state) noexcept { bool allValid = true; for (std::size_t i = 0; i < kMaxPatterns; ++i) { if (!validPattern(state.patterns[i])) { allValid = false; break; } } if (!allValid) { return; } patterns_ = state.patterns; configuredPatternMask_ = static_cast((1u << kMaxPatterns) - 1u); } bool PatternEngine::enqueueSnapshot(const Pattern& pattern, std::uint64_t startAtMs) { if (!validPattern(pattern) || queueSize_ >= kQueueCapacity) { return false; } const std::size_t tail = (queueHead_ + queueSize_) % kQueueCapacity; queue_[tail].pattern = pattern; queue_[tail].startAtMs = startAtMs; ++queueSize_; return true; } bool PatternEngine::enqueue(PatternId id) { return enqueue(id, readClock()); } bool PatternEngine::enqueue(PatternId id, std::uint64_t startAtMs) { const std::size_t index = patternIndex(id); if (index >= patterns_.size() || (configuredPatternMask_ & (1u << index)) == 0) { return false; } return enqueueSnapshot(patterns_[index], startAtMs); } bool PatternEngine::enqueue(const Pattern& pattern) { return enqueue(pattern, readClock()); } bool PatternEngine::enqueue(const Pattern& pattern, std::uint64_t startAtMs) { return enqueueSnapshot(pattern, startAtMs); } bool PatternEngine::enqueue(PatternId id, const AppState& state, std::uint64_t startAtMs) { const Pattern* pattern = state.pattern(id); return pattern != nullptr && enqueueSnapshot(*pattern, startAtMs); } bool PatternEngine::enqueue(PatternId id, const AppState& state) { return enqueue(id, state, readClock()); } bool PatternEngine::start(PatternId id, std::uint64_t nowMs) { const std::size_t index = patternIndex(id); if (index >= patterns_.size() || (configuredPatternMask_ & (1u << index)) == 0) { return false; } return startSnapshot(patterns_[index], nowMs); } bool PatternEngine::start(const Pattern& pattern, std::uint64_t nowMs) { return startSnapshot(pattern, nowMs); } bool PatternEngine::start(PatternId id, const AppState& state, std::uint64_t nowMs) { const Pattern* pattern = state.pattern(id); return pattern != nullptr && startSnapshot(*pattern, nowMs); } bool PatternEngine::start(PatternId id, const AppState& state) { return start(id, state, readClock()); } std::uint64_t PatternEngine::addDuration(std::uint64_t deadline, std::uint8_t seconds) noexcept { const std::uint64_t duration = static_cast(seconds) * kMillisecondsPerSecond; if (deadline > std::numeric_limits::max() - duration) { return std::numeric_limits::max(); } return deadline + duration; } bool PatternEngine::startSnapshot(const Pattern& pattern, std::uint64_t nowMs) { if (!validPattern(pattern)) { return false; } if (active_) { finishActive(); } activePattern_ = pattern; active_ = true; phase_ = 0; deadlineMs_ = 0; outputOn_ = false; hasUpdateTime_ = true; lastUpdateMs_ = nowMs; std::size_t firstActivePhase = 0; while (firstActivePhase < pattern.phaseCount && pattern.phases[firstActivePhase] == 0) { ++firstActivePhase; } if (firstActivePhase == pattern.phaseCount) { finishActive(); return true; } phase_ = static_cast(firstActivePhase); emit(pattern.phaseIsOn(firstActivePhase)); deadlineMs_ = addDuration(nowMs, pattern.phases[firstActivePhase]); return true; } void PatternEngine::emit(bool on) noexcept { // The engine has one physical output by design. Reserved GPIO23 is never // passed to an output sink, even if a caller supplies an arbitrary ID. outputOn_ = on; lastOutputRelay_ = RelayId::Main; if (output_) { output_(RelayId::Main, on); } else if (simpleOutput_) { simpleOutput_(on); } } void PatternEngine::finishActive() noexcept { if (active_ && outputOn_) { emit(false); } active_ = false; phase_ = kNoPhase; deadlineMs_ = 0; outputOn_ = false; } void PatternEngine::pump(std::uint64_t nowMs) { // FIFO order is intentional. A future head blocks later requests rather // than reordering a user's schedule. while (!active_ && queueSize_ != 0 && queue_[queueHead_].startAtMs <= nowMs) { const Request request = queue_[queueHead_]; queueHead_ = (queueHead_ + 1) % kQueueCapacity; --queueSize_; (void)startSnapshot(request.pattern, nowMs); } } bool PatternEngine::update(std::uint64_t nowMs) { if (hasUpdateTime_ && nowMs < lastUpdateMs_) { // A clock regression is unsafe for an active relay. Stop the sequence // instead of leaving the last output energized indefinitely. const bool changed = active_ || outputOn_; finishActive(); return changed; } hasUpdateTime_ = true; lastUpdateMs_ = nowMs; bool changed = false; if (active_) { // Deadlines are absolute. A delayed poll walks the finite phase list // and cannot accumulate delay or schedule a phase in the past. while (active_ && nowMs >= deadlineMs_) { if (phase_ + 1u >= activePattern_.phaseCount) { finishActive(); changed = true; break; } ++phase_; emit(activePattern_.phaseIsOn(phase_)); deadlineMs_ = addDuration(deadlineMs_, activePattern_.phases[phase_]); changed = true; } } if (!active_ && queueSize_ != 0) { const std::size_t before = queueSize_; pump(nowMs); changed = changed || before != queueSize_ || active_; } return changed; } bool PatternEngine::update() { if (clockInterface_ == nullptr && !clock_) { return false; } return update(readClock()); } void PatternEngine::stop(std::uint64_t nowMs) { if (hasUpdateTime_ && nowMs < lastUpdateMs_) { // A bad sample must not prevent the safety turn-off. finishActive(); return; } hasUpdateTime_ = true; lastUpdateMs_ = nowMs; finishActive(); } void PatternEngine::stop() { if (clockInterface_ != nullptr || clock_) { stop(readClock()); } else { stop(lastUpdateMs_); } } void PatternEngine::cancelAll() noexcept { queueHead_ = 0; queueSize_ = 0; } } // namespace timbre_core