Files
TimbreESP32/lib/timbre_core/Model.h
Angel Ivan 2a9eabce14 Fist commit
2026-10-02 12:08:21 -06:00

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;