260 lines
7.5 KiB
C++
Executable File
260 lines
7.5 KiB
C++
Executable File
#include "PatternEngine.h"
|
|
|
|
#include <limits>
|
|
|
|
namespace timbre_core {
|
|
|
|
void PatternEngine::initializeDefaults() noexcept {
|
|
for (std::size_t i = 0; i < kMaxPatterns; ++i) {
|
|
patterns_[i].id = patternIdFromIndex(i);
|
|
}
|
|
configuredPatternMask_ =
|
|
static_cast<std::uint8_t>((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<std::uint8_t>(
|
|
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<std::uint8_t>((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<std::uint64_t>(seconds) * kMillisecondsPerSecond;
|
|
if (deadline > std::numeric_limits<std::uint64_t>::max() - duration) {
|
|
return std::numeric_limits<std::uint64_t>::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<std::uint8_t>(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
|