#pragma once #include #include #include #include #include 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(id) <= static_cast(PatternId::C); } constexpr std::size_t patternIndex(PatternId id) noexcept { return isPatternId(id) ? static_cast(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(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 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 data_{}; std::size_t length_ = 0; }; template 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 bool emplace_back(Args&&... args) noexcept { if (full()) { return false; } data_[size_++] = T(std::forward(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 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(kMaxPatternPhases); union { std::array phases; std::array durations; std::array phaseSeconds; }; Pattern() : phases{} {} explicit Pattern(PatternId patternIdentifier) : id(patternIdentifier), phases{} {} Pattern(PatternId patternIdentifier, std::initializer_list values) : id(patternIdentifier), phaseCount(0), phases{} { for (std::uint8_t value : values) { if (phaseCount < kMaxPatternPhases) { phases[phaseCount++] = value; } } } Pattern(PatternId patternIdentifier, const std::array& values) : id(patternIdentifier), phases(values) {} explicit Pattern(const std::array& 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(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(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(hour) * 60u + minute, daysMask, isEnabled); } std::uint8_t hourPart() const noexcept { return static_cast(minuteOfDay / 60u); } std::uint8_t minutePart() const noexcept { return static_cast(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 profiles; std::array patterns; FixedVector schedules; FixedVector 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 + 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;