604 lines
19 KiB
C++
Executable File
604 lines
19 KiB
C++
Executable File
#pragma once
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <initializer_list>
|
|
#include <utility>
|
|
|
|
namespace timbre_core {
|
|
|
|
// The data model deliberately uses bounded storage: it is safe for the ESP32
|
|
// heap and never needs PSRAM or a dynamic container.
|
|
constexpr std::size_t kMaxProfiles = 8;
|
|
constexpr std::size_t kMaxPatterns = 3;
|
|
constexpr std::size_t kMaxProfileOrPatternSlots = 8;
|
|
constexpr std::size_t kMaxSchedules = 100;
|
|
constexpr std::size_t kMaxHolidays = 64;
|
|
constexpr std::size_t kPatternQueueCapacity = 8;
|
|
constexpr std::size_t kPatternCount = kMaxPatterns;
|
|
constexpr std::size_t kCanonicalPatternCount = kMaxPatterns;
|
|
constexpr std::size_t kMaxConfiguredPatterns = 8;
|
|
constexpr std::size_t kProfileLimit = kMaxProfiles;
|
|
constexpr std::size_t kMaxPatternPhases = 6;
|
|
constexpr std::uint8_t kMaxPhaseDurationSeconds = 99;
|
|
constexpr std::uint16_t kMaxPatternTotalSeconds = 600;
|
|
|
|
// Upper-case aliases make the limits convenient for small configuration code.
|
|
constexpr std::size_t MAX_PROFILES = kMaxProfiles;
|
|
constexpr std::size_t MAX_PATTERNS = kMaxPatterns;
|
|
constexpr std::size_t MAX_PROFILE_PATTERN_SLOTS = kMaxProfileOrPatternSlots;
|
|
constexpr std::size_t MAX_SCHEDULES = kMaxSchedules;
|
|
constexpr std::size_t MAX_HOLIDAYS = kMaxHolidays;
|
|
constexpr std::size_t PATTERN_QUEUE_CAPACITY = kPatternQueueCapacity;
|
|
constexpr std::uint8_t MAX_PHASE_DURATION = kMaxPhaseDurationSeconds;
|
|
constexpr std::uint16_t MAX_PATTERN_TOTAL = kMaxPatternTotalSeconds;
|
|
|
|
using Id = std::uint16_t;
|
|
using EntityId = Id;
|
|
using ProfileId = Id;
|
|
using PatternIdValue = Id;
|
|
using ScheduleId = Id;
|
|
using HolidayId = Id;
|
|
using StableId = Id;
|
|
constexpr Id kInvalidId = 0xFFFFu;
|
|
|
|
enum class PatternId : std::uint8_t {
|
|
A = 0,
|
|
B = 1,
|
|
C = 2
|
|
};
|
|
|
|
// RelayId is intentionally separate from PatternId. A/B/C are sound
|
|
// patterns; they are never relay identifiers.
|
|
enum class RelayId : std::uint8_t {
|
|
Main = 0,
|
|
Primary = 0,
|
|
MainRelay = 0,
|
|
Relay1 = 0,
|
|
MainGPIO22 = 0,
|
|
Reserved = 1,
|
|
ReservedRelay = 1,
|
|
Relay2 = 1,
|
|
ReservedGPIO23 = 1,
|
|
Invalid = 0xFF
|
|
};
|
|
|
|
constexpr std::uint8_t kMainRelayGpio = 22;
|
|
constexpr std::uint8_t kReservedRelayGpio = 23;
|
|
constexpr std::uint8_t kMainRelayGPIO = kMainRelayGpio;
|
|
constexpr std::uint8_t kReservedRelayGPIO = kReservedRelayGpio;
|
|
constexpr std::uint8_t RELAY_MAIN_GPIO = kMainRelayGpio;
|
|
constexpr std::uint8_t RELAY_RESERVED_GPIO = kReservedRelayGpio;
|
|
constexpr std::uint8_t kRelayCount = 2;
|
|
constexpr RelayId kPrimaryRelay = RelayId::Main;
|
|
constexpr RelayId kReservedRelay = RelayId::Reserved;
|
|
constexpr std::uint8_t kAllWeekdaysMask = 0x7F;
|
|
constexpr std::uint8_t kSundayMask = 1u << 0;
|
|
constexpr std::uint8_t kMondayMask = 1u << 1;
|
|
constexpr std::uint8_t kTuesdayMask = 1u << 2;
|
|
constexpr std::uint8_t kWednesdayMask = 1u << 3;
|
|
constexpr std::uint8_t kThursdayMask = 1u << 4;
|
|
constexpr std::uint8_t kFridayMask = 1u << 5;
|
|
constexpr std::uint8_t kSaturdayMask = 1u << 6;
|
|
|
|
constexpr bool isPatternId(PatternId id) noexcept {
|
|
return static_cast<std::uint8_t>(id) <= static_cast<std::uint8_t>(PatternId::C);
|
|
}
|
|
|
|
constexpr std::size_t patternIndex(PatternId id) noexcept {
|
|
return isPatternId(id) ? static_cast<std::size_t>(id) : kMaxPatterns;
|
|
}
|
|
|
|
constexpr bool isRelayId(RelayId id) noexcept {
|
|
return id == RelayId::Main || id == RelayId::Reserved;
|
|
}
|
|
|
|
constexpr std::uint8_t gpioForRelay(RelayId id) noexcept {
|
|
return id == RelayId::Main ? kMainRelayGpio
|
|
: (id == RelayId::Reserved ? kReservedRelayGpio : 0xFFu);
|
|
}
|
|
|
|
constexpr bool isReservedRelay(RelayId id) noexcept {
|
|
return id == RelayId::Reserved;
|
|
}
|
|
|
|
constexpr std::size_t relayIndex(RelayId id) noexcept {
|
|
return id == RelayId::Main ? 0u
|
|
: (id == RelayId::Reserved ? 1u : kRelayCount);
|
|
}
|
|
|
|
constexpr RelayId relayIdFromIndex(std::size_t index) noexcept {
|
|
return index == 0 ? RelayId::Main
|
|
: (index == 1 ? RelayId::Reserved : RelayId::Invalid);
|
|
}
|
|
|
|
constexpr RelayId relayIdFromGpio(std::uint8_t gpio) noexcept {
|
|
return gpio == kMainRelayGpio ? RelayId::Main
|
|
: (gpio == kReservedRelayGpio ? RelayId::Reserved : RelayId::Invalid);
|
|
}
|
|
|
|
constexpr bool isKnownRelayGpio(std::uint8_t gpio) noexcept {
|
|
return gpio == kMainRelayGpio || gpio == kReservedRelayGpio;
|
|
}
|
|
|
|
constexpr std::uint8_t relayGpio(RelayId id) noexcept { return gpioForRelay(id); }
|
|
|
|
constexpr PatternId patternIdFromIndex(std::size_t index) noexcept {
|
|
return index < kMaxPatterns ? static_cast<PatternId>(index) : PatternId::A;
|
|
}
|
|
|
|
constexpr PatternId patternIdFromChar(char value) noexcept {
|
|
return value == 'A' || value == 'a' ? PatternId::A
|
|
: (value == 'B' || value == 'b' ? PatternId::B
|
|
: (value == 'C' || value == 'c' ? PatternId::C : PatternId::A));
|
|
}
|
|
|
|
constexpr char patternChar(PatternId id) noexcept {
|
|
return id == PatternId::A ? 'A'
|
|
: (id == PatternId::B ? 'B'
|
|
: (id == PatternId::C ? 'C' : '?'));
|
|
}
|
|
|
|
enum class Weekday : std::uint8_t {
|
|
Sunday = 0,
|
|
Monday = 1,
|
|
Tuesday = 2,
|
|
Wednesday = 3,
|
|
Thursday = 4,
|
|
Friday = 5,
|
|
Saturday = 6
|
|
};
|
|
|
|
template <std::size_t Capacity>
|
|
class FixedText {
|
|
public:
|
|
FixedText() = default;
|
|
|
|
explicit FixedText(const char* value) { assign(value); }
|
|
|
|
FixedText& operator=(const char* value) {
|
|
assign(value);
|
|
return *this;
|
|
}
|
|
|
|
void assign(const char* value) noexcept {
|
|
std::size_t index = 0;
|
|
if (value != nullptr) {
|
|
while (value[index] != '\0' && index + 1 < Capacity) {
|
|
data_[index] = value[index];
|
|
++index;
|
|
}
|
|
}
|
|
while (index < Capacity) {
|
|
data_[index] = '\0';
|
|
++index;
|
|
}
|
|
length_ = 0;
|
|
while (length_ < Capacity && data_[length_] != '\0') {
|
|
++length_;
|
|
}
|
|
}
|
|
|
|
const char* c_str() const noexcept { return data_.data(); }
|
|
std::size_t size() const noexcept { return length_; }
|
|
std::size_t length() const noexcept { return length_; }
|
|
bool empty() const noexcept { return length_ == 0; }
|
|
|
|
char operator[](std::size_t index) const noexcept {
|
|
return index < length_ ? data_[index] : '\0';
|
|
}
|
|
|
|
bool operator==(const FixedText& other) const noexcept {
|
|
if (length_ != other.length_) {
|
|
return false;
|
|
}
|
|
for (std::size_t i = 0; i < length_; ++i) {
|
|
if (data_[i] != other.data_[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool operator!=(const FixedText& other) const noexcept { return !(*this == other); }
|
|
bool operator==(const char* other) const noexcept {
|
|
if (other == nullptr) {
|
|
return length_ == 0;
|
|
}
|
|
for (std::size_t i = 0; i < length_; ++i) {
|
|
if (other[i] == '\0' || other[i] != data_[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return other[length_] == '\0';
|
|
}
|
|
bool operator!=(const char* other) const noexcept { return !(*this == other); }
|
|
|
|
private:
|
|
std::array<char, Capacity> data_{};
|
|
std::size_t length_ = 0;
|
|
};
|
|
|
|
template <typename T, std::size_t Capacity>
|
|
class FixedVector {
|
|
public:
|
|
using value_type = T;
|
|
using iterator = T*;
|
|
using const_iterator = const T*;
|
|
|
|
std::size_t size() const noexcept { return size_; }
|
|
std::size_t capacity() const noexcept { return Capacity; }
|
|
bool empty() const noexcept { return size_ == 0; }
|
|
bool full() const noexcept { return size_ == Capacity; }
|
|
void clear() noexcept { size_ = 0; }
|
|
|
|
bool push_back(const T& value) noexcept {
|
|
if (full()) {
|
|
return false;
|
|
}
|
|
data_[size_++] = value;
|
|
return true;
|
|
}
|
|
|
|
template <typename... Args>
|
|
bool emplace_back(Args&&... args) noexcept {
|
|
if (full()) {
|
|
return false;
|
|
}
|
|
data_[size_++] = T(std::forward<Args>(args)...);
|
|
return true;
|
|
}
|
|
|
|
bool pop_back() noexcept {
|
|
if (empty()) {
|
|
return false;
|
|
}
|
|
--size_;
|
|
return true;
|
|
}
|
|
|
|
bool erase(std::size_t index) noexcept {
|
|
if (index >= size_) {
|
|
return false;
|
|
}
|
|
for (std::size_t i = index + 1; i < size_; ++i) {
|
|
data_[i - 1] = data_[i];
|
|
}
|
|
--size_;
|
|
return true;
|
|
}
|
|
|
|
T& operator[](std::size_t index) noexcept { return data_[index]; }
|
|
const T& operator[](std::size_t index) const noexcept { return data_[index]; }
|
|
T& back() noexcept { return data_[size_ - 1]; }
|
|
const T& back() const noexcept { return data_[size_ - 1]; }
|
|
T& front() noexcept { return data_[0]; }
|
|
const T& front() const noexcept { return data_[0]; }
|
|
|
|
// No throwing allocator or unchecked access is needed by the core. The
|
|
// caller can use size() before this helper when a reference is required.
|
|
T* find_if_index(std::size_t index) noexcept {
|
|
return index < size_ ? &data_[index] : nullptr;
|
|
}
|
|
const T* find_if_index(std::size_t index) const noexcept {
|
|
return index < size_ ? &data_[index] : nullptr;
|
|
}
|
|
|
|
T* data() noexcept { return data_.data(); }
|
|
const T* data() const noexcept { return data_.data(); }
|
|
iterator begin() noexcept { return data_.data(); }
|
|
const_iterator begin() const noexcept { return data_.data(); }
|
|
iterator end() noexcept { return data_.data() + size_; }
|
|
const_iterator end() const noexcept { return data_.data() + size_; }
|
|
|
|
private:
|
|
std::array<T, Capacity> data_{};
|
|
std::size_t size_ = 0;
|
|
};
|
|
|
|
struct Profile {
|
|
union {
|
|
ProfileId id;
|
|
ProfileId profileId;
|
|
};
|
|
FixedText<32> name{};
|
|
bool enabled = true;
|
|
|
|
Profile() : id(0) {}
|
|
explicit Profile(ProfileId identifier) : id(identifier) {}
|
|
Profile(ProfileId identifier, const char* profileName) : id(identifier), name(profileName) {}
|
|
};
|
|
|
|
struct Pattern {
|
|
// Six alternating durations, in seconds: ON, OFF, ON, OFF, ...
|
|
// phaseCount permits a short final pattern while retaining a fixed buffer.
|
|
PatternId id = PatternId::A;
|
|
std::uint8_t phaseCount = static_cast<std::uint8_t>(kMaxPatternPhases);
|
|
union {
|
|
std::array<std::uint8_t, kMaxPatternPhases> phases;
|
|
std::array<std::uint8_t, kMaxPatternPhases> durations;
|
|
std::array<std::uint8_t, kMaxPatternPhases> phaseSeconds;
|
|
};
|
|
|
|
Pattern() : phases{} {}
|
|
explicit Pattern(PatternId patternIdentifier) : id(patternIdentifier), phases{} {}
|
|
|
|
Pattern(PatternId patternIdentifier, std::initializer_list<std::uint8_t> values)
|
|
: id(patternIdentifier), phaseCount(0), phases{} {
|
|
for (std::uint8_t value : values) {
|
|
if (phaseCount < kMaxPatternPhases) {
|
|
phases[phaseCount++] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
Pattern(PatternId patternIdentifier, const std::array<std::uint8_t, kMaxPatternPhases>& values)
|
|
: id(patternIdentifier), phases(values) {}
|
|
|
|
explicit Pattern(const std::array<std::uint8_t, kMaxPatternPhases>& values)
|
|
: phases(values) {}
|
|
|
|
Pattern(std::uint8_t onSeconds, std::uint8_t offSeconds, std::uint8_t onSeconds2,
|
|
std::uint8_t offSeconds2, std::uint8_t onSeconds3, std::uint8_t offSeconds3)
|
|
: phases{onSeconds, offSeconds, onSeconds2, offSeconds2, onSeconds3, offSeconds3} {}
|
|
|
|
std::uint16_t totalSeconds() const noexcept {
|
|
std::uint16_t total = 0;
|
|
const std::size_t count = phaseCount <= kMaxPatternPhases ? phaseCount : 0;
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
total = static_cast<std::uint16_t>(total + phases[i]);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
bool valid() const noexcept {
|
|
if (!isPatternId(id) || phaseCount > kMaxPatternPhases) {
|
|
return false;
|
|
}
|
|
std::uint16_t total = 0;
|
|
for (std::size_t i = 0; i < phaseCount; ++i) {
|
|
if (phases[i] > kMaxPhaseDurationSeconds) {
|
|
return false;
|
|
}
|
|
total = static_cast<std::uint16_t>(total + phases[i]);
|
|
}
|
|
return total <= kMaxPatternTotalSeconds;
|
|
}
|
|
|
|
bool phaseIsOn(std::size_t index) const noexcept {
|
|
return index < phaseCount && (index & 1u) == 0u;
|
|
}
|
|
};
|
|
|
|
struct Schedule {
|
|
union {
|
|
ScheduleId id;
|
|
ScheduleId scheduleId;
|
|
};
|
|
union {
|
|
ProfileId profileId;
|
|
ProfileId profile;
|
|
};
|
|
union {
|
|
PatternId patternId;
|
|
PatternId pattern;
|
|
};
|
|
union {
|
|
std::uint16_t minuteOfDay;
|
|
std::uint16_t minute;
|
|
};
|
|
|
|
union {
|
|
std::uint8_t weekdayMask;
|
|
std::uint8_t days;
|
|
std::uint8_t dayMask;
|
|
};
|
|
|
|
bool enabled = true;
|
|
|
|
Schedule()
|
|
: id(kInvalidId),
|
|
profileId(0),
|
|
patternId(PatternId::A),
|
|
minuteOfDay(0),
|
|
weekdayMask(kAllWeekdaysMask) {}
|
|
|
|
Schedule(ScheduleId scheduleIdentifier,
|
|
ProfileId profileIdentifier,
|
|
PatternId patternIdentifier,
|
|
std::uint16_t minute,
|
|
std::uint8_t daysMask,
|
|
bool isEnabled = true)
|
|
: id(scheduleIdentifier),
|
|
profileId(profileIdentifier),
|
|
patternId(patternIdentifier),
|
|
minuteOfDay(minute),
|
|
weekdayMask(daysMask),
|
|
enabled(isEnabled) {}
|
|
|
|
// Convenience overload for callers that naturally specify time before the
|
|
// pattern identifier.
|
|
Schedule(ScheduleId scheduleIdentifier,
|
|
ProfileId profileIdentifier,
|
|
std::uint16_t minute,
|
|
PatternId patternIdentifier,
|
|
std::uint8_t daysMask,
|
|
bool isEnabled = true)
|
|
: id(scheduleIdentifier),
|
|
profileId(profileIdentifier),
|
|
patternId(patternIdentifier),
|
|
minuteOfDay(minute),
|
|
weekdayMask(daysMask),
|
|
enabled(isEnabled) {}
|
|
|
|
static Schedule at(ScheduleId scheduleIdentifier,
|
|
ProfileId profileIdentifier,
|
|
PatternId patternIdentifier,
|
|
std::uint8_t hour,
|
|
std::uint8_t minute,
|
|
std::uint8_t daysMask,
|
|
bool isEnabled = true) {
|
|
return Schedule(scheduleIdentifier, profileIdentifier, patternIdentifier,
|
|
static_cast<std::uint16_t>(hour) * 60u + minute,
|
|
daysMask, isEnabled);
|
|
}
|
|
|
|
std::uint8_t hourPart() const noexcept {
|
|
return static_cast<std::uint8_t>(minuteOfDay / 60u);
|
|
}
|
|
std::uint8_t minutePart() const noexcept {
|
|
return static_cast<std::uint8_t>(minuteOfDay % 60u);
|
|
}
|
|
};
|
|
|
|
struct Holiday {
|
|
union {
|
|
HolidayId id;
|
|
HolidayId holidayId;
|
|
};
|
|
std::uint8_t month = 1;
|
|
std::uint8_t day = 1;
|
|
bool enabled = true;
|
|
|
|
Holiday() : id(kInvalidId) {}
|
|
Holiday(HolidayId holidayIdentifier, std::uint8_t holidayMonth,
|
|
std::uint8_t holidayDay, bool isEnabled = true)
|
|
: id(holidayIdentifier), month(holidayMonth), day(holidayDay), enabled(isEnabled) {}
|
|
|
|
Holiday(std::uint8_t holidayMonth, std::uint8_t holidayDay)
|
|
: month(holidayMonth), day(holidayDay) {}
|
|
|
|
static Holiday recurring(std::uint8_t holidayMonth, std::uint8_t holidayDay,
|
|
HolidayId holidayIdentifier = kInvalidId,
|
|
bool isEnabled = true) {
|
|
return Holiday(holidayIdentifier, holidayMonth, holidayDay, isEnabled);
|
|
}
|
|
};
|
|
|
|
struct AppState {
|
|
FixedVector<Profile, kMaxProfiles> profiles;
|
|
std::array<Pattern, kMaxPatterns> patterns;
|
|
FixedVector<Schedule, kMaxSchedules> schedules;
|
|
FixedVector<Holiday, kMaxHolidays> holidays;
|
|
union {
|
|
ProfileId activeProfileId;
|
|
ProfileId activeProfile;
|
|
};
|
|
std::uint32_t revision = 0;
|
|
|
|
AppState() : activeProfileId(0) {
|
|
for (std::size_t i = 0; i < kMaxPatterns; ++i) {
|
|
patterns[i].id = patternIdFromIndex(i);
|
|
}
|
|
}
|
|
|
|
std::size_t profileCount() const noexcept { return profiles.size(); }
|
|
std::size_t scheduleCount() const noexcept { return schedules.size(); }
|
|
std::size_t holidayCount() const noexcept { return holidays.size(); }
|
|
|
|
Pattern* pattern(PatternId id) noexcept {
|
|
const std::size_t index = patternIndex(id);
|
|
return index < kMaxPatterns ? &patterns[index] : nullptr;
|
|
}
|
|
|
|
const Pattern* pattern(PatternId id) const noexcept {
|
|
const std::size_t index = patternIndex(id);
|
|
return index < kMaxPatterns ? &patterns[index] : nullptr;
|
|
}
|
|
|
|
Profile* profile(ProfileId id) noexcept {
|
|
for (std::size_t i = 0; i < profiles.size(); ++i) {
|
|
if (profiles[i].id == id) {
|
|
return &profiles[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const Profile* profile(ProfileId id) const noexcept {
|
|
for (std::size_t i = 0; i < profiles.size(); ++i) {
|
|
if (profiles[i].id == id) {
|
|
return &profiles[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Schedule* schedule(ScheduleId id) noexcept {
|
|
for (std::size_t i = 0; i < schedules.size(); ++i) {
|
|
if (schedules[i].id == id) {
|
|
return &schedules[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const Schedule* schedule(ScheduleId id) const noexcept {
|
|
for (std::size_t i = 0; i < schedules.size(); ++i) {
|
|
if (schedules[i].id == id) {
|
|
return &schedules[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Holiday* holiday(HolidayId id) noexcept {
|
|
for (std::size_t i = 0; i < holidays.size(); ++i) {
|
|
if (holidays[i].id == id) {
|
|
return &holidays[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
const Holiday* holiday(HolidayId id) const noexcept {
|
|
for (std::size_t i = 0; i < holidays.size(); ++i) {
|
|
if (holidays[i].id == id) {
|
|
return &holidays[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
// Monotonic IDs are never inferred from a vector position. Deleting an item
|
|
// therefore cannot silently change the identity of another item.
|
|
class StableIdAllocator {
|
|
public:
|
|
StableIdAllocator() = default;
|
|
explicit StableIdAllocator(Id first) noexcept : next_(first == 0 ? 1 : first) {}
|
|
|
|
Id nextValue() const noexcept { return next_; }
|
|
void setNextValue(Id value) noexcept { next_ = value == 0 ? 1 : value; }
|
|
|
|
Id next() noexcept {
|
|
for (;;) {
|
|
if (next_ == 0 || next_ == kInvalidId) {
|
|
next_ = 1;
|
|
}
|
|
const Id result = next_++;
|
|
if (result != 0 && result != kInvalidId) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|
|
void observe(Id id) noexcept {
|
|
if (id != kInvalidId && id >= next_) {
|
|
next_ = static_cast<Id>(id + 1u);
|
|
}
|
|
}
|
|
|
|
void reset(Id first = 1) noexcept { next_ = first == 0 ? 1 : first; }
|
|
|
|
private:
|
|
Id next_ = 1;
|
|
};
|
|
|
|
} // namespace timbre_core
|
|
|
|
// A short namespace alias keeps the library pleasant to use from both the
|
|
// application and small native tests.
|
|
namespace timbre = timbre_core;
|