Fist commit
This commit is contained in:
46
lib/README
Executable file
46
lib/README
Executable file
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
198
lib/timbre_core/CivilTime.cpp
Executable file
198
lib/timbre_core/CivilTime.cpp
Executable file
@@ -0,0 +1,198 @@
|
||||
#include "CivilTime.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace timbre_core {
|
||||
namespace {
|
||||
|
||||
constexpr std::int64_t kSecondsPerDay = 86400;
|
||||
|
||||
std::int64_t floorDiv(std::int64_t value, std::int64_t divisor) noexcept {
|
||||
const std::int64_t quotient = value / divisor;
|
||||
const std::int64_t remainder = value % divisor;
|
||||
return remainder != 0 && ((remainder < 0) != (divisor < 0)) ? quotient - 1 : quotient;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CivilTime::isLeapYear(std::int32_t year) noexcept {
|
||||
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
|
||||
}
|
||||
|
||||
std::uint8_t CivilTime::daysInMonth(std::int32_t year, std::uint8_t month) noexcept {
|
||||
static constexpr std::uint8_t days[] = {
|
||||
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||
};
|
||||
if (month < 1 || month > 12) {
|
||||
return 0;
|
||||
}
|
||||
if (month == 2 && isLeapYear(year)) {
|
||||
return 29;
|
||||
}
|
||||
return days[month];
|
||||
}
|
||||
|
||||
bool CivilTime::isValidDate(std::int32_t year, std::uint8_t month, std::uint8_t day) noexcept {
|
||||
// The conversion routines are useful well beyond the current product
|
||||
// lifetime, but keep the public civil value inside int32_t.
|
||||
if (year < std::numeric_limits<std::int32_t>::min() ||
|
||||
year > std::numeric_limits<std::int32_t>::max()) {
|
||||
return false;
|
||||
}
|
||||
return day >= 1 && day <= daysInMonth(year, month);
|
||||
}
|
||||
|
||||
std::uint8_t CivilTime::weekdayForDate(std::int32_t year, std::uint8_t month,
|
||||
std::uint8_t day) noexcept {
|
||||
if (!isValidDate(year, month, day)) {
|
||||
return 0;
|
||||
}
|
||||
// 1970-01-01 was Thursday (4 with Sunday == 0).
|
||||
const std::int64_t days = daysFromCivil(year, month, day);
|
||||
std::int64_t weekday = (days + 4) % 7;
|
||||
if (weekday < 0) {
|
||||
weekday += 7;
|
||||
}
|
||||
return static_cast<std::uint8_t>(weekday);
|
||||
}
|
||||
|
||||
std::int64_t CivilTime::daysFromCivil(std::int32_t year, std::uint8_t month,
|
||||
std::uint8_t day) noexcept {
|
||||
// Howard Hinnant's civil calendar transform. It is valid for the full
|
||||
// practical range of the public model and does not depend on time_t.
|
||||
std::int64_t y = year;
|
||||
y -= month <= 2 ? 1 : 0;
|
||||
const std::int64_t era = (y >= 0 ? y : y - 399) / 400;
|
||||
const std::int64_t yearOfEra = y - era * 400;
|
||||
const std::int64_t monthPrime = month + (month > 2 ? -3 : 9);
|
||||
const std::int64_t dayOfYear = (153 * monthPrime + 2) / 5 + day - 1;
|
||||
const std::int64_t dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
|
||||
return era * 146097 + dayOfEra - 719468;
|
||||
}
|
||||
|
||||
bool CivilTime::civilFromDays(std::int64_t days, std::int32_t& year,
|
||||
std::uint8_t& month, std::uint8_t& day) noexcept {
|
||||
// The inverse transform is kept separate so weekday/date validation and
|
||||
// the scheduler can use exactly the same arithmetic.
|
||||
days += 719468;
|
||||
const std::int64_t era = (days >= 0 ? days : days - 146096) / 146097;
|
||||
const std::int64_t dayOfEra = days - era * 146097;
|
||||
const std::int64_t yearOfEra =
|
||||
(dayOfEra - dayOfEra / 1460 + dayOfEra / 36524 - dayOfEra / 146096) / 365;
|
||||
std::int64_t y = yearOfEra + era * 400;
|
||||
const std::int64_t dayOfYear =
|
||||
dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100);
|
||||
const std::int64_t monthPrime =
|
||||
(5 * dayOfYear + 2) / 153;
|
||||
const std::int64_t d = dayOfYear - (153 * monthPrime + 2) / 5 + 1;
|
||||
const std::int64_t m = monthPrime + (monthPrime < 10 ? 3 : -9);
|
||||
y += m <= 2;
|
||||
|
||||
if (y < std::numeric_limits<std::int32_t>::min() ||
|
||||
y > std::numeric_limits<std::int32_t>::max()) {
|
||||
return false;
|
||||
}
|
||||
year = static_cast<std::int32_t>(y);
|
||||
month = static_cast<std::uint8_t>(m);
|
||||
day = static_cast<std::uint8_t>(d);
|
||||
return true;
|
||||
}
|
||||
|
||||
CivilDateTime CivilTime::fromUnixSeconds(std::int64_t utcSeconds) noexcept {
|
||||
CivilDateTime result;
|
||||
(void)fromUnixSeconds(utcSeconds, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CivilTime::fromUnixSeconds(std::int64_t utcSeconds, CivilDateTime& result) noexcept {
|
||||
// Use floor seconds so epochs before 1970 are not biased toward 1969.
|
||||
const std::int64_t localSeconds = utcSeconds + kUtcOffsetSeconds;
|
||||
const std::int64_t days = floorDiv(localSeconds, kSecondsPerDay);
|
||||
std::int64_t secondOfDay = localSeconds - days * kSecondsPerDay;
|
||||
if (secondOfDay < 0) {
|
||||
secondOfDay += kSecondsPerDay;
|
||||
}
|
||||
|
||||
std::int32_t year = 0;
|
||||
std::uint8_t month = 0;
|
||||
std::uint8_t day = 0;
|
||||
if (!civilFromDays(days, year, month, day)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.year = year;
|
||||
result.month = month;
|
||||
result.day = day;
|
||||
result.hour = static_cast<std::uint8_t>(secondOfDay / 3600);
|
||||
result.minute = static_cast<std::uint8_t>((secondOfDay / 60) % 60);
|
||||
result.second = static_cast<std::uint8_t>(secondOfDay % 60);
|
||||
result.weekday = weekdayForDate(year, month, day);
|
||||
return true;
|
||||
}
|
||||
|
||||
CivilDateTime CivilTime::fromUnixMilliseconds(std::int64_t utcMilliseconds) noexcept {
|
||||
const std::int64_t seconds = floorDiv(utcMilliseconds, 1000);
|
||||
return fromUnixSeconds(seconds);
|
||||
}
|
||||
|
||||
std::int64_t CivilTime::toUnixSeconds(const CivilDateTime& localTime) noexcept {
|
||||
std::int64_t result = 0;
|
||||
(void)toUnixSeconds(localTime, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CivilTime::toUnixSeconds(const CivilDateTime& localTime, std::int64_t& result) noexcept {
|
||||
if (!localTime.valid()) {
|
||||
return false;
|
||||
}
|
||||
const std::int64_t localSeconds =
|
||||
CivilTime::daysFromCivil(localTime.year, localTime.month, localTime.day) * kSecondsPerDay +
|
||||
static_cast<std::int64_t>(localTime.hour) * 3600 +
|
||||
static_cast<std::int64_t>(localTime.minute) * 60 +
|
||||
static_cast<std::int64_t>(localTime.second);
|
||||
result = localSeconds - kUtcOffsetSeconds;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::int64_t CivilTime::toUnixMilliseconds(const CivilDateTime& localTime) noexcept {
|
||||
return toUnixSeconds(localTime) * 1000;
|
||||
}
|
||||
|
||||
bool CivilDateTime::valid() const noexcept {
|
||||
return CivilTime::isValidDate(year, month, day) && hour < 24 &&
|
||||
minute < 60 && second < 60;
|
||||
}
|
||||
|
||||
std::uint16_t CivilDateTime::minuteOfDay() const noexcept {
|
||||
return static_cast<std::uint16_t>(static_cast<std::uint16_t>(hour) * 60u + minute);
|
||||
}
|
||||
|
||||
std::int64_t CivilDateTime::dateKey() const noexcept {
|
||||
return static_cast<std::int64_t>(year) * 10000LL +
|
||||
static_cast<std::int64_t>(month) * 100LL + day;
|
||||
}
|
||||
|
||||
std::int64_t CivilDateTime::minuteSerial() const noexcept {
|
||||
return static_cast<std::int64_t>(CivilTime::daysFromCivil(year, month, day)) * 1440LL +
|
||||
minuteOfDay();
|
||||
}
|
||||
|
||||
bool CivilDateTime::operator==(const CivilDateTime& other) const noexcept {
|
||||
return year == other.year && month == other.month && day == other.day &&
|
||||
hour == other.hour && minute == other.minute && second == other.second;
|
||||
}
|
||||
|
||||
std::int64_t LocalMinuteKey::serial() const noexcept {
|
||||
const std::int64_t y = date / 10000LL;
|
||||
const std::int64_t m = (date / 100LL) % 100LL;
|
||||
const std::int64_t d = date % 100LL;
|
||||
if (y < std::numeric_limits<std::int32_t>::min() ||
|
||||
y > std::numeric_limits<std::int32_t>::max() || m < 1 || m > 12 || d < 1 || d > 31) {
|
||||
return std::numeric_limits<std::int64_t>::min();
|
||||
}
|
||||
return CivilTime::daysFromCivil(static_cast<std::int32_t>(y),
|
||||
static_cast<std::uint8_t>(m),
|
||||
static_cast<std::uint8_t>(d)) * 1440LL + minuteOfDay;
|
||||
}
|
||||
|
||||
} // namespace timbre_core
|
||||
95
lib/timbre_core/CivilTime.h
Executable file
95
lib/timbre_core/CivilTime.h
Executable file
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "Model.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
struct CivilDateTime {
|
||||
std::int32_t year = 1970;
|
||||
std::uint8_t month = 1; // 1..12
|
||||
std::uint8_t day = 1; // 1..31
|
||||
std::uint8_t hour = 0; // 0..23
|
||||
std::uint8_t minute = 0; // 0..59
|
||||
std::uint8_t second = 0; // 0..59
|
||||
std::uint8_t weekday = 0; // 0 = Sunday, 6 = Saturday
|
||||
|
||||
bool valid() const noexcept;
|
||||
std::uint16_t minuteOfDay() const noexcept;
|
||||
std::int64_t dateKey() const noexcept;
|
||||
std::int64_t minuteSerial() const noexcept;
|
||||
bool operator==(const CivilDateTime& other) const noexcept;
|
||||
bool operator!=(const CivilDateTime& other) const noexcept { return !(*this == other); }
|
||||
};
|
||||
|
||||
using CivilDateTimePoint = CivilDateTime;
|
||||
|
||||
struct LocalMinuteKey {
|
||||
std::int64_t date = 0; // YYYYMMDD
|
||||
std::uint16_t minuteOfDay = 0;
|
||||
|
||||
static LocalMinuteKey from(const CivilDateTime& value) noexcept {
|
||||
return LocalMinuteKey{value.dateKey(), value.minuteOfDay()};
|
||||
}
|
||||
|
||||
std::int64_t serial() const noexcept;
|
||||
std::int64_t dateKey() const noexcept { return date; }
|
||||
std::uint16_t minute() const noexcept { return minuteOfDay; }
|
||||
bool operator==(const LocalMinuteKey& other) const noexcept {
|
||||
return date == other.date && minuteOfDay == other.minuteOfDay;
|
||||
}
|
||||
bool operator!=(const LocalMinuteKey& other) const noexcept { return !(*this == other); }
|
||||
bool operator<(const LocalMinuteKey& other) const noexcept { return serial() < other.serial(); }
|
||||
bool operator>(const LocalMinuteKey& other) const noexcept { return serial() > other.serial(); }
|
||||
bool operator<=(const LocalMinuteKey& other) const noexcept { return serial() <= other.serial(); }
|
||||
bool operator>=(const LocalMinuteKey& other) const noexcept { return serial() >= other.serial(); }
|
||||
};
|
||||
|
||||
class CivilTime {
|
||||
public:
|
||||
// The product deliberately has one fixed civil offset: UTC-06:00.
|
||||
static constexpr std::int32_t kUtcOffsetSeconds = -6 * 60 * 60;
|
||||
static constexpr std::int32_t kUtcOffsetMinutes = -6 * 60;
|
||||
static constexpr std::int32_t UTC_OFFSET_SECONDS = kUtcOffsetSeconds;
|
||||
static constexpr std::int32_t UTC_OFFSET_MINUTES = kUtcOffsetMinutes;
|
||||
|
||||
static bool isLeapYear(std::int32_t year) noexcept;
|
||||
static std::uint8_t daysInMonth(std::int32_t year, std::uint8_t month) noexcept;
|
||||
static bool isValidDate(std::int32_t year, std::uint8_t month, std::uint8_t day) noexcept;
|
||||
static std::uint8_t weekdayForDate(std::int32_t year, std::uint8_t month,
|
||||
std::uint8_t day) noexcept;
|
||||
|
||||
static CivilDateTime fromUnixSeconds(std::int64_t utcSeconds) noexcept;
|
||||
static bool fromUnixSeconds(std::int64_t utcSeconds, CivilDateTime& result) noexcept;
|
||||
static CivilDateTime fromUnix(std::int64_t utcSeconds) noexcept {
|
||||
return fromUnixSeconds(utcSeconds);
|
||||
}
|
||||
static bool fromUnix(std::int64_t utcSeconds, CivilDateTime& result) noexcept {
|
||||
return fromUnixSeconds(utcSeconds, result);
|
||||
}
|
||||
static CivilDateTime fromUtc(std::int64_t utcSeconds) noexcept {
|
||||
return fromUnixSeconds(utcSeconds);
|
||||
}
|
||||
static CivilDateTime utcToLocal(std::int64_t utcSeconds) noexcept {
|
||||
return fromUnixSeconds(utcSeconds);
|
||||
}
|
||||
|
||||
static CivilDateTime fromUnixMilliseconds(std::int64_t utcMilliseconds) noexcept;
|
||||
static std::int64_t toUnixSeconds(const CivilDateTime& localTime) noexcept;
|
||||
static bool toUnixSeconds(const CivilDateTime& localTime, std::int64_t& result) noexcept;
|
||||
static std::int64_t toUnixMilliseconds(const CivilDateTime& localTime) noexcept;
|
||||
|
||||
static LocalMinuteKey minuteKey(const CivilDateTime& localTime) noexcept {
|
||||
return LocalMinuteKey::from(localTime);
|
||||
}
|
||||
|
||||
// These helpers are public because they are useful to a scheduler and are
|
||||
// deterministic, allocation-free operations on an ESP32.
|
||||
static std::int64_t daysFromCivil(std::int32_t year, std::uint8_t month,
|
||||
std::uint8_t day) noexcept;
|
||||
static bool civilFromDays(std::int64_t days, std::int32_t& year,
|
||||
std::uint8_t& month, std::uint8_t& day) noexcept;
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
603
lib/timbre_core/Model.h
Executable file
603
lib/timbre_core/Model.h
Executable file
@@ -0,0 +1,603 @@
|
||||
#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;
|
||||
259
lib/timbre_core/PatternEngine.cpp
Executable file
259
lib/timbre_core/PatternEngine.cpp
Executable file
@@ -0,0 +1,259 @@
|
||||
#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
|
||||
151
lib/timbre_core/PatternEngine.h
Executable file
151
lib/timbre_core/PatternEngine.h
Executable file
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
|
||||
#include "Model.h"
|
||||
#include "ScheduleValidator.h"
|
||||
#include "SessionClock.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
class PatternEngine {
|
||||
public:
|
||||
using OutputCallback = std::function<void(RelayId, bool)>;
|
||||
using ClockCallback = std::function<std::uint64_t()>;
|
||||
using SimpleOutputCallback = std::function<void(bool)>;
|
||||
|
||||
struct Request {
|
||||
Pattern pattern{};
|
||||
std::uint64_t startAtMs = 0;
|
||||
};
|
||||
|
||||
static constexpr std::size_t kQueueCapacity = kPatternQueueCapacity;
|
||||
static constexpr std::uint8_t kNoPhase = 0xFFu;
|
||||
static constexpr std::uint64_t kMillisecondsPerSecond = 1000ULL;
|
||||
|
||||
PatternEngine() {
|
||||
initializeDefaults();
|
||||
}
|
||||
explicit PatternEngine(OutputCallback output) : output_(std::move(output)) {
|
||||
initializeDefaults();
|
||||
}
|
||||
explicit PatternEngine(ClockCallback clock, OutputCallback output = OutputCallback())
|
||||
: clock_(std::move(clock)), output_(std::move(output)) {
|
||||
initializeDefaults();
|
||||
}
|
||||
explicit PatternEngine(MonotonicClock& clock, OutputCallback output = OutputCallback())
|
||||
: clockInterface_(&clock), output_(std::move(output)) {
|
||||
initializeDefaults();
|
||||
}
|
||||
explicit PatternEngine(const AppState& state, OutputCallback output = OutputCallback())
|
||||
: output_(std::move(output)) {
|
||||
initializeDefaults();
|
||||
setPatterns(state);
|
||||
}
|
||||
|
||||
void setClock(ClockCallback clock) {
|
||||
clockInterface_ = nullptr;
|
||||
clock_ = std::move(clock);
|
||||
}
|
||||
void setClock(MonotonicClock& clock) {
|
||||
clock_ = ClockCallback();
|
||||
clockInterface_ = &clock;
|
||||
}
|
||||
void setOutput(OutputCallback output) {
|
||||
output_ = std::move(output);
|
||||
simpleOutput_ = SimpleOutputCallback();
|
||||
}
|
||||
void setSimpleOutput(SimpleOutputCallback output) {
|
||||
simpleOutput_ = std::move(output);
|
||||
output_ = OutputCallback();
|
||||
}
|
||||
|
||||
void setPattern(const Pattern& pattern) noexcept;
|
||||
void setPatterns(const AppState& state) noexcept;
|
||||
void setState(const AppState& state) noexcept { setPatterns(state); }
|
||||
void configure(const AppState& state) noexcept { setPatterns(state); }
|
||||
|
||||
// Queueing is non-blocking and bounded. A request is a snapshot, so a
|
||||
// later configuration commit cannot mutate an already queued sequence.
|
||||
bool enqueue(PatternId id);
|
||||
bool enqueue(PatternId id, std::uint64_t startAtMs);
|
||||
bool enqueue(const Pattern& pattern);
|
||||
bool enqueue(const Pattern& pattern, std::uint64_t startAtMs);
|
||||
bool enqueue(PatternId id, const AppState& state, std::uint64_t startAtMs);
|
||||
bool enqueue(PatternId id, const AppState& state);
|
||||
|
||||
bool start(PatternId id, std::uint64_t nowMs);
|
||||
bool start(const Pattern& pattern, std::uint64_t nowMs);
|
||||
bool start(PatternId id, const AppState& state, std::uint64_t nowMs);
|
||||
bool start(PatternId id, const AppState& state);
|
||||
|
||||
// Returns true when the state/output changed. It performs no waiting and
|
||||
// consumes at most one bounded transition burst (six phases).
|
||||
bool update(std::uint64_t nowMs);
|
||||
bool update();
|
||||
bool tick(std::uint64_t nowMs) { return update(nowMs); }
|
||||
bool tick() { return update(); }
|
||||
|
||||
// Stops the active sequence and guarantees an OFF output. Pending queue
|
||||
// entries are retained; cancelAll() removes them as well.
|
||||
void stop(std::uint64_t nowMs);
|
||||
void stop();
|
||||
void finish() { stop(); }
|
||||
void cancelAll() noexcept;
|
||||
|
||||
bool running() const noexcept { return active_; }
|
||||
bool outputOn() const noexcept { return outputOn_; }
|
||||
PatternId activePatternId() const noexcept { return activePattern_.id; }
|
||||
const Pattern* activePattern() const noexcept { return active_ ? &activePattern_ : nullptr; }
|
||||
std::uint8_t activePhase() const noexcept { return active_ ? phase_ : kNoPhase; }
|
||||
std::uint64_t deadlineMs() const noexcept { return active_ ? deadlineMs_ : 0; }
|
||||
std::uint64_t deadline() const noexcept { return deadlineMs(); }
|
||||
std::uint64_t nextDeadlineMs() const noexcept { return deadlineMs(); }
|
||||
std::size_t queueSize() const noexcept { return queueSize_; }
|
||||
std::size_t queueCapacity() const noexcept { return kQueueCapacity; }
|
||||
bool configured(PatternId id) const noexcept {
|
||||
const std::size_t index = patternIndex(id);
|
||||
return index < kMaxPatterns && (configuredPatternMask_ & (1u << index)) != 0;
|
||||
}
|
||||
RelayId lastOutputRelay() const noexcept { return lastOutputRelay_; }
|
||||
bool reservedRelayDriven() const noexcept { return reservedRelayDriven_; }
|
||||
|
||||
private:
|
||||
void initializeDefaults() noexcept;
|
||||
std::uint64_t readClock() const;
|
||||
bool validPattern(const Pattern& pattern) const noexcept;
|
||||
bool enqueueSnapshot(const Pattern& pattern, std::uint64_t startAtMs);
|
||||
bool startSnapshot(const Pattern& pattern, std::uint64_t nowMs);
|
||||
void pump(std::uint64_t nowMs);
|
||||
void finishActive() noexcept;
|
||||
void emit(bool on) noexcept;
|
||||
static std::uint64_t addDuration(std::uint64_t deadline, std::uint8_t seconds) noexcept;
|
||||
|
||||
ClockCallback clock_;
|
||||
MonotonicClock* clockInterface_ = nullptr;
|
||||
OutputCallback output_;
|
||||
SimpleOutputCallback simpleOutput_;
|
||||
|
||||
std::array<Pattern, kMaxPatterns> patterns_{};
|
||||
std::uint8_t configuredPatternMask_ =
|
||||
static_cast<std::uint8_t>((1u << kMaxPatterns) - 1u);
|
||||
|
||||
std::array<Request, kQueueCapacity> queue_{};
|
||||
std::size_t queueHead_ = 0;
|
||||
std::size_t queueSize_ = 0;
|
||||
|
||||
Pattern activePattern_{};
|
||||
bool active_ = false;
|
||||
std::uint8_t phase_ = kNoPhase;
|
||||
std::uint64_t deadlineMs_ = 0;
|
||||
std::uint64_t lastUpdateMs_ = 0;
|
||||
bool hasUpdateTime_ = false;
|
||||
bool outputOn_ = false;
|
||||
RelayId lastOutputRelay_ = RelayId::Main;
|
||||
bool reservedRelayDriven_ = false;
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
214
lib/timbre_core/ScheduleValidator.cpp
Executable file
214
lib/timbre_core/ScheduleValidator.cpp
Executable file
@@ -0,0 +1,214 @@
|
||||
#include "ScheduleValidator.h"
|
||||
|
||||
namespace timbre_core {
|
||||
namespace {
|
||||
|
||||
ValidationResult failure(ValidationCode code, std::size_t index = 0) noexcept {
|
||||
return ValidationResult(code, index);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* ValidationResult::message() const noexcept {
|
||||
switch (code) {
|
||||
case ValidationCode::Ok:
|
||||
return "ok";
|
||||
case ValidationCode::TooManyProfiles:
|
||||
return "too many profiles";
|
||||
case ValidationCode::TooManyPatterns:
|
||||
return "too many patterns";
|
||||
case ValidationCode::TooManySchedules:
|
||||
return "too many schedules";
|
||||
case ValidationCode::TooManyHolidays:
|
||||
return "too many holidays";
|
||||
case ValidationCode::InvalidProfileId:
|
||||
return "invalid profile id";
|
||||
case ValidationCode::DuplicateProfileId:
|
||||
return "duplicate profile id";
|
||||
case ValidationCode::InvalidPatternId:
|
||||
return "invalid pattern id";
|
||||
case ValidationCode::DuplicatePatternId:
|
||||
return "duplicate pattern id";
|
||||
case ValidationCode::InvalidPhaseCount:
|
||||
return "invalid phase count";
|
||||
case ValidationCode::InvalidPhaseDuration:
|
||||
return "invalid phase duration";
|
||||
case ValidationCode::PatternTotalTooLarge:
|
||||
return "pattern total is too large";
|
||||
case ValidationCode::InvalidHolidayId:
|
||||
return "invalid holiday id";
|
||||
case ValidationCode::DuplicateHolidayId:
|
||||
return "duplicate holiday id";
|
||||
case ValidationCode::InvalidHolidayDate:
|
||||
return "invalid holiday date";
|
||||
case ValidationCode::InvalidScheduleId:
|
||||
return "invalid schedule id";
|
||||
case ValidationCode::DuplicateScheduleId:
|
||||
return "duplicate schedule id";
|
||||
case ValidationCode::InvalidMinute:
|
||||
return "invalid minute";
|
||||
case ValidationCode::InvalidWeekdayMask:
|
||||
return "invalid weekday mask";
|
||||
case ValidationCode::UnknownProfile:
|
||||
return "schedule references an unknown profile";
|
||||
case ValidationCode::UnknownPattern:
|
||||
return "schedule references an unknown pattern";
|
||||
case ValidationCode::InvalidActiveProfile:
|
||||
return "active profile does not exist";
|
||||
}
|
||||
return "validation error";
|
||||
}
|
||||
|
||||
ValidationResult ScheduleValidator::validateProfile(const Profile& profile,
|
||||
std::size_t index) noexcept {
|
||||
if (profile.id == kInvalidId) {
|
||||
return failure(ValidationCode::InvalidProfileId, index);
|
||||
}
|
||||
return ValidationResult{};
|
||||
}
|
||||
|
||||
ValidationResult ScheduleValidator::validatePattern(const Pattern& pattern,
|
||||
std::size_t index) noexcept {
|
||||
if (!isPatternId(pattern.id)) {
|
||||
return failure(ValidationCode::InvalidPatternId, index);
|
||||
}
|
||||
if (pattern.phaseCount > kMaxPatternPhases) {
|
||||
return failure(ValidationCode::InvalidPhaseCount, index);
|
||||
}
|
||||
|
||||
std::uint16_t total = 0;
|
||||
for (std::size_t i = 0; i < pattern.phaseCount; ++i) {
|
||||
if (!validPhaseDuration(pattern.phases[i])) {
|
||||
return failure(ValidationCode::InvalidPhaseDuration, index);
|
||||
}
|
||||
total = static_cast<std::uint16_t>(total + pattern.phases[i]);
|
||||
if (!validPatternTotal(total)) {
|
||||
return failure(ValidationCode::PatternTotalTooLarge, index);
|
||||
}
|
||||
}
|
||||
return ValidationResult{};
|
||||
}
|
||||
|
||||
ValidationResult ScheduleValidator::validateHoliday(const Holiday& holiday,
|
||||
std::size_t index) noexcept {
|
||||
if (holiday.id == kInvalidId) {
|
||||
return failure(ValidationCode::InvalidHolidayId, index);
|
||||
}
|
||||
if (!validHolidayDate(holiday.month, holiday.day)) {
|
||||
return failure(ValidationCode::InvalidHolidayDate, index);
|
||||
}
|
||||
return ValidationResult{};
|
||||
}
|
||||
|
||||
bool ScheduleValidator::validHolidayDate(std::uint8_t month, std::uint8_t day) noexcept {
|
||||
// Holidays are recurring month/day values. February 29 is valid and is
|
||||
// naturally inert in non-leap years.
|
||||
if (month < 1 || month > 12 || day < 1) {
|
||||
return false;
|
||||
}
|
||||
static constexpr std::uint8_t maximumDays[] = {
|
||||
0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
|
||||
};
|
||||
return day <= maximumDays[month];
|
||||
}
|
||||
|
||||
ValidationResult ScheduleValidator::validateSchedule(const Schedule& schedule,
|
||||
const AppState& state,
|
||||
std::size_t index) noexcept {
|
||||
if (schedule.id == kInvalidId) {
|
||||
return failure(ValidationCode::InvalidScheduleId, index);
|
||||
}
|
||||
if (!validMinute(schedule.minuteOfDay)) {
|
||||
return failure(ValidationCode::InvalidMinute, index);
|
||||
}
|
||||
if (!validWeekdayMask(schedule.weekdayMask)) {
|
||||
return failure(ValidationCode::InvalidWeekdayMask, index);
|
||||
}
|
||||
if (state.profile(schedule.profileId) == nullptr) {
|
||||
return failure(ValidationCode::UnknownProfile, index);
|
||||
}
|
||||
if (state.pattern(schedule.patternId) == nullptr ||
|
||||
!isPatternId(schedule.patternId)) {
|
||||
return failure(ValidationCode::UnknownPattern, index);
|
||||
}
|
||||
const Pattern* pattern = state.pattern(schedule.patternId);
|
||||
if (pattern == nullptr || !validatePattern(*pattern).valid) {
|
||||
return failure(ValidationCode::UnknownPattern, index);
|
||||
}
|
||||
return ValidationResult{};
|
||||
}
|
||||
|
||||
ValidationResult ScheduleValidator::validate(const AppState& state) noexcept {
|
||||
if (state.profiles.size() > kMaxProfiles) {
|
||||
return failure(ValidationCode::TooManyProfiles, state.profiles.size());
|
||||
}
|
||||
if (state.patterns.size() != kMaxPatterns) {
|
||||
return failure(ValidationCode::TooManyPatterns, state.patterns.size());
|
||||
}
|
||||
if (state.schedules.size() > kMaxSchedules) {
|
||||
return failure(ValidationCode::TooManySchedules, state.schedules.size());
|
||||
}
|
||||
if (state.holidays.size() > kMaxHolidays) {
|
||||
return failure(ValidationCode::TooManyHolidays, state.holidays.size());
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < state.profiles.size(); ++i) {
|
||||
const ValidationResult result = validateProfile(state.profiles[i], i);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
for (std::size_t j = 0; j < i; ++j) {
|
||||
if (state.profiles[j].id == state.profiles[i].id) {
|
||||
return failure(ValidationCode::DuplicateProfileId, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < state.patterns.size(); ++i) {
|
||||
const ValidationResult result = validatePattern(state.patterns[i], i);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
for (std::size_t j = 0; j < i; ++j) {
|
||||
if (state.patterns[j].id == state.patterns[i].id) {
|
||||
return failure(ValidationCode::DuplicatePatternId, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
|
||||
const ValidationResult result = validateHoliday(state.holidays[i], i);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
for (std::size_t j = 0; j < i; ++j) {
|
||||
if (state.holidays[j].id == state.holidays[i].id) {
|
||||
return failure(ValidationCode::DuplicateHolidayId, i);
|
||||
}
|
||||
if (state.holidays[j].enabled && state.holidays[i].enabled &&
|
||||
state.holidays[j].month == state.holidays[i].month &&
|
||||
state.holidays[j].day == state.holidays[i].day) {
|
||||
return failure(ValidationCode::DuplicateHolidayId, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
|
||||
const ValidationResult result = validateSchedule(state.schedules[i], state, i);
|
||||
if (!result.valid) {
|
||||
return result;
|
||||
}
|
||||
for (std::size_t j = 0; j < i; ++j) {
|
||||
if (state.schedules[j].id == state.schedules[i].id) {
|
||||
return failure(ValidationCode::DuplicateScheduleId, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.profiles.size() != 0 && state.profile(state.activeProfileId) == nullptr) {
|
||||
return failure(ValidationCode::InvalidActiveProfile, 0);
|
||||
}
|
||||
return ValidationResult{};
|
||||
}
|
||||
|
||||
} // namespace timbre_core
|
||||
92
lib/timbre_core/ScheduleValidator.h
Executable file
92
lib/timbre_core/ScheduleValidator.h
Executable file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "Model.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
enum class ValidationCode : std::uint8_t {
|
||||
Ok = 0,
|
||||
None = 0,
|
||||
TooManyProfiles,
|
||||
TooManyPatterns,
|
||||
TooManySchedules,
|
||||
TooManyHolidays,
|
||||
InvalidProfileId,
|
||||
DuplicateProfileId,
|
||||
InvalidPatternId,
|
||||
DuplicatePatternId,
|
||||
InvalidPhaseCount,
|
||||
InvalidPhaseDuration,
|
||||
PatternTotalTooLarge,
|
||||
InvalidHolidayId,
|
||||
DuplicateHolidayId,
|
||||
InvalidHolidayDate,
|
||||
InvalidScheduleId,
|
||||
DuplicateScheduleId,
|
||||
InvalidMinute,
|
||||
InvalidWeekdayMask,
|
||||
UnknownProfile,
|
||||
UnknownPattern,
|
||||
InvalidActiveProfile
|
||||
};
|
||||
|
||||
using ValidationError = ValidationCode;
|
||||
|
||||
struct ValidationResult {
|
||||
bool valid = true;
|
||||
ValidationCode code = ValidationCode::Ok;
|
||||
ValidationCode error = ValidationCode::Ok;
|
||||
std::size_t index = 0;
|
||||
|
||||
constexpr ValidationResult() = default;
|
||||
explicit constexpr ValidationResult(ValidationCode resultCode,
|
||||
std::size_t resultIndex = 0) noexcept
|
||||
: valid(resultCode == ValidationCode::Ok),
|
||||
code(resultCode),
|
||||
error(resultCode),
|
||||
index(resultIndex) {}
|
||||
|
||||
constexpr bool ok() const noexcept { return valid; }
|
||||
constexpr bool isValid() const noexcept { return valid; }
|
||||
constexpr explicit operator bool() const noexcept { return valid; }
|
||||
const char* message() const noexcept;
|
||||
};
|
||||
|
||||
class ScheduleValidator {
|
||||
public:
|
||||
static ValidationResult validate(const AppState& state) noexcept;
|
||||
static ValidationResult validateState(const AppState& state) noexcept { return validate(state); }
|
||||
static ValidationResult validateAppState(const AppState& state) noexcept { return validate(state); }
|
||||
static bool valid(const AppState& state) noexcept { return validate(state).valid; }
|
||||
static bool isValid(const AppState& state) noexcept { return valid(state); }
|
||||
|
||||
static ValidationResult validateProfile(const Profile& profile,
|
||||
std::size_t index = 0) noexcept;
|
||||
static ValidationResult validatePattern(const Pattern& pattern,
|
||||
std::size_t index = 0) noexcept;
|
||||
static ValidationResult validateSchedule(const Schedule& schedule,
|
||||
const AppState& state,
|
||||
std::size_t index = 0) noexcept;
|
||||
static ValidationResult validateHoliday(const Holiday& holiday,
|
||||
std::size_t index = 0) noexcept;
|
||||
|
||||
static std::uint16_t patternTotalSeconds(const Pattern& pattern) noexcept {
|
||||
return pattern.totalSeconds();
|
||||
}
|
||||
static bool validPhaseDuration(std::uint8_t value) noexcept {
|
||||
return value <= kMaxPhaseDurationSeconds;
|
||||
}
|
||||
static bool validPatternTotal(std::uint16_t totalSeconds) noexcept {
|
||||
return totalSeconds <= kMaxPatternTotalSeconds;
|
||||
}
|
||||
static bool validWeekdayMask(std::uint8_t value) noexcept {
|
||||
return value != 0 && (value & static_cast<std::uint8_t>(~kAllWeekdaysMask)) == 0;
|
||||
}
|
||||
static bool validMinute(std::uint16_t minute) noexcept { return minute < 1440; }
|
||||
static bool validHolidayDate(std::uint8_t month, std::uint8_t day) noexcept;
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
123
lib/timbre_core/Scheduler.cpp
Executable file
123
lib/timbre_core/Scheduler.cpp
Executable file
@@ -0,0 +1,123 @@
|
||||
#include "Scheduler.h"
|
||||
|
||||
#include "ScheduleValidator.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
void Scheduler::reset() noexcept {
|
||||
hasLastKey_ = false;
|
||||
lastKey_ = LocalMinuteKey{};
|
||||
}
|
||||
|
||||
bool Scheduler::isHoliday(const AppState& state, std::uint8_t month,
|
||||
std::uint8_t day) noexcept {
|
||||
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
|
||||
const Holiday& holiday = state.holidays[i];
|
||||
if (holiday.enabled && holiday.month == month && holiday.day == day) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Scheduler::scheduleMatches(const Schedule& schedule, const AppState& state,
|
||||
const CivilDateTime& localTime) noexcept {
|
||||
if (!schedule.enabled || schedule.minuteOfDay != localTime.minuteOfDay()) {
|
||||
return false;
|
||||
}
|
||||
const Profile* activeProfile = state.profile(state.activeProfileId);
|
||||
if (activeProfile == nullptr || !activeProfile->enabled ||
|
||||
schedule.profileId != state.activeProfileId) {
|
||||
return false;
|
||||
}
|
||||
const std::uint8_t weekday = CivilTime::weekdayForDate(localTime.year, localTime.month,
|
||||
localTime.day);
|
||||
if ((schedule.weekdayMask & static_cast<std::uint8_t>(1u << weekday)) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (isHoliday(state, localTime.month, localTime.day)) {
|
||||
return false;
|
||||
}
|
||||
const Pattern* pattern = state.pattern(schedule.patternId);
|
||||
return pattern != nullptr && isPatternId(schedule.patternId) &&
|
||||
ScheduleValidator::validatePattern(*pattern).valid;
|
||||
}
|
||||
|
||||
ScheduleDecision Scheduler::poll(const AppState& state,
|
||||
const CivilDateTime& localTime,
|
||||
bool firstObservation) {
|
||||
ScheduleDecision decision;
|
||||
decision.firstObservation = firstObservation;
|
||||
|
||||
if (!localTime.valid()) {
|
||||
decision.invalidTime = true;
|
||||
return decision;
|
||||
}
|
||||
|
||||
const LocalMinuteKey currentKey = LocalMinuteKey::from(localTime);
|
||||
const std::int64_t currentSerial = currentKey.serial();
|
||||
if (currentSerial == std::numeric_limits<std::int64_t>::min()) {
|
||||
decision.invalidTime = true;
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (hasLastKey_) {
|
||||
const std::int64_t previousSerial = lastKey_.serial();
|
||||
if (currentSerial <= previousSerial) {
|
||||
decision.alreadyProcessed = true;
|
||||
return decision;
|
||||
}
|
||||
const std::int64_t distance = currentSerial - previousSerial;
|
||||
decision.missedMinutes = distance > 1 ? static_cast<std::uint64_t>(distance - 1) : 0;
|
||||
}
|
||||
|
||||
// Consume the current key before evaluating matches. Therefore a failed
|
||||
// lookup (holiday, disabled profile, etc.) cannot be replayed later.
|
||||
lastKey_ = currentKey;
|
||||
hasLastKey_ = true;
|
||||
|
||||
if (firstObservation) {
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (isHoliday(state, localTime.month, localTime.day)) {
|
||||
decision.blockedByHoliday = true;
|
||||
return decision;
|
||||
}
|
||||
|
||||
const Schedule* selected = nullptr;
|
||||
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
|
||||
const Schedule& candidate = state.schedules[i];
|
||||
if (!scheduleMatches(candidate, state, localTime)) {
|
||||
continue;
|
||||
}
|
||||
// Stable IDs, rather than array positions, define deterministic
|
||||
// precedence when two schedules intentionally share a minute.
|
||||
if (selected == nullptr || candidate.id < selected->id) {
|
||||
selected = &candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected != nullptr) {
|
||||
decision.due = true;
|
||||
decision.scheduleId = selected->id;
|
||||
decision.patternId = selected->patternId;
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
|
||||
ScheduleDecision Scheduler::poll(const AppState& state,
|
||||
SessionClock& clock,
|
||||
std::int64_t utcEpochSeconds) {
|
||||
const SessionClock::Observation observation = clock.observe(utcEpochSeconds);
|
||||
if (!observation.valid) {
|
||||
ScheduleDecision decision;
|
||||
decision.invalidTime = true;
|
||||
return decision;
|
||||
}
|
||||
return poll(state, observation.local, observation.firstObservation);
|
||||
}
|
||||
|
||||
} // namespace timbre_core
|
||||
90
lib/timbre_core/Scheduler.h
Executable file
90
lib/timbre_core/Scheduler.h
Executable file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "CivilTime.h"
|
||||
#include "Model.h"
|
||||
#include "SessionClock.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
struct ScheduleDecision {
|
||||
bool due = false;
|
||||
bool firstObservation = false;
|
||||
bool alreadyProcessed = false;
|
||||
bool blockedByHoliday = false;
|
||||
bool invalidTime = false;
|
||||
ScheduleId scheduleId = kInvalidId;
|
||||
PatternId patternId = PatternId::A;
|
||||
std::uint64_t missedMinutes = 0;
|
||||
|
||||
constexpr explicit operator bool() const noexcept { return due; }
|
||||
constexpr bool triggered() const noexcept { return due; }
|
||||
};
|
||||
|
||||
using ScheduleMatch = ScheduleDecision;
|
||||
|
||||
class Scheduler {
|
||||
public:
|
||||
Scheduler() = default;
|
||||
|
||||
// Only the currently observed local minute is considered. If the source
|
||||
// jumped from minute N to N+K, the intermediate minutes are intentionally
|
||||
// discarded rather than replayed.
|
||||
ScheduleDecision poll(const AppState& state,
|
||||
const CivilDateTime& localTime,
|
||||
bool firstObservation = false);
|
||||
ScheduleDecision process(const AppState& state,
|
||||
const CivilDateTime& localTime,
|
||||
bool firstObservation = false) {
|
||||
return poll(state, localTime, firstObservation);
|
||||
}
|
||||
|
||||
// Convenience integration with SessionClock. The caller supplies the
|
||||
// current wall value; the injected monotonic source remains authoritative
|
||||
// inside SessionClock.
|
||||
ScheduleDecision poll(const AppState& state,
|
||||
SessionClock& clock,
|
||||
std::int64_t utcEpochSeconds);
|
||||
ScheduleDecision poll(const AppState& state,
|
||||
const SessionClock::Observation& observation) {
|
||||
if (!observation.valid) {
|
||||
ScheduleDecision decision;
|
||||
decision.invalidTime = true;
|
||||
return decision;
|
||||
}
|
||||
return poll(state, observation.local, observation.firstObservation);
|
||||
}
|
||||
ScheduleDecision process(const AppState& state,
|
||||
SessionClock& clock,
|
||||
std::int64_t utcEpochSeconds) {
|
||||
return poll(state, clock, utcEpochSeconds);
|
||||
}
|
||||
ScheduleDecision tick(const AppState& state,
|
||||
const CivilDateTime& localTime,
|
||||
bool firstObservation = false) {
|
||||
return poll(state, localTime, firstObservation);
|
||||
}
|
||||
ScheduleDecision evaluate(const AppState& state,
|
||||
const CivilDateTime& localTime,
|
||||
bool firstObservation = false) {
|
||||
return poll(state, localTime, firstObservation);
|
||||
}
|
||||
|
||||
bool hasLastKey() const noexcept { return hasLastKey_; }
|
||||
const LocalMinuteKey& lastKey() const noexcept { return lastKey_; }
|
||||
const LocalMinuteKey& key() const noexcept { return lastKey_; }
|
||||
void reset() noexcept;
|
||||
|
||||
static bool isHoliday(const AppState& state, std::uint8_t month,
|
||||
std::uint8_t day) noexcept;
|
||||
static bool scheduleMatches(const Schedule& schedule,
|
||||
const AppState& state,
|
||||
const CivilDateTime& localTime) noexcept;
|
||||
|
||||
private:
|
||||
bool hasLastKey_ = false;
|
||||
LocalMinuteKey lastKey_{};
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
134
lib/timbre_core/SessionClock.cpp
Executable file
134
lib/timbre_core/SessionClock.cpp
Executable file
@@ -0,0 +1,134 @@
|
||||
#include "SessionClock.h"
|
||||
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
std::uint64_t SessionClock::readMonotonic() const {
|
||||
if (clock_ != nullptr) {
|
||||
return clock_->nowMs();
|
||||
}
|
||||
if (callback_) {
|
||||
return callback_();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SessionClock::setObservation(std::int64_t wallSeconds, std::uint64_t monotonicMs,
|
||||
bool first, bool regression, bool resynchronized) {
|
||||
Observation observation;
|
||||
observation.valid = CivilTime::fromUnixSeconds(wallSeconds, observation.local);
|
||||
observation.firstObservation = first;
|
||||
observation.monotonicRegression = regression;
|
||||
observation.resynchronized = resynchronized;
|
||||
observation.monotonicMs = monotonicMs;
|
||||
observation.utcEpochSeconds = wallSeconds;
|
||||
if (!observation.valid) {
|
||||
lastObservation_ = observation;
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
if (first) {
|
||||
anchorMonotonicMs_ = monotonicMs;
|
||||
anchorWallSeconds_ = wallSeconds;
|
||||
}
|
||||
lastMonotonicMs_ = monotonicMs;
|
||||
lastWallSeconds_ = wallSeconds;
|
||||
lastLocal_ = observation.local;
|
||||
lastObservation_ = observation;
|
||||
return true;
|
||||
}
|
||||
|
||||
SessionClock::Observation SessionClock::observeAt(std::int64_t utcEpochSeconds,
|
||||
std::uint64_t monotonicMs) {
|
||||
if (!initialized_) {
|
||||
(void)setObservation(utcEpochSeconds, monotonicMs, true, false, false);
|
||||
return lastObservation_;
|
||||
}
|
||||
|
||||
if (monotonicMs < lastMonotonicMs_) {
|
||||
// A monotonic source must never run backwards. Keep the last good
|
||||
// anchor and reject the sample rather than manufacturing elapsed time.
|
||||
Observation observation = lastObservation_;
|
||||
observation.valid = false;
|
||||
observation.firstObservation = false;
|
||||
observation.monotonicRegression = true;
|
||||
observation.resynchronized = false;
|
||||
observation.monotonicMs = monotonicMs;
|
||||
lastObservation_ = observation;
|
||||
return observation;
|
||||
}
|
||||
|
||||
const std::uint64_t delta = monotonicMs - lastMonotonicMs_;
|
||||
const std::int64_t predicted = lastWallSeconds_ +
|
||||
static_cast<std::int64_t>(delta / 1000ULL);
|
||||
// A wall-clock correction is resynchronized at the current sample, never
|
||||
// expanded into a stream of historical minutes.
|
||||
const std::uint64_t difference = predicted >= utcEpochSeconds
|
||||
? static_cast<std::uint64_t>(predicted - utcEpochSeconds)
|
||||
: static_cast<std::uint64_t>(utcEpochSeconds - predicted);
|
||||
const bool resynchronized = difference > 1ULL;
|
||||
(void)setObservation(utcEpochSeconds, monotonicMs, false, false, resynchronized);
|
||||
return lastObservation_;
|
||||
}
|
||||
|
||||
SessionClock::Observation SessionClock::observe(std::int64_t utcEpochSeconds) {
|
||||
return observeAt(utcEpochSeconds, readMonotonic());
|
||||
}
|
||||
|
||||
SessionClock::Observation SessionClock::tick() {
|
||||
if (!initialized_) {
|
||||
return Observation{};
|
||||
}
|
||||
const std::uint64_t sample = readMonotonic();
|
||||
if (sample < lastMonotonicMs_) {
|
||||
Observation observation = lastObservation_;
|
||||
observation.valid = false;
|
||||
observation.firstObservation = false;
|
||||
observation.monotonicRegression = true;
|
||||
observation.resynchronized = false;
|
||||
observation.monotonicMs = sample;
|
||||
lastObservation_ = observation;
|
||||
return observation;
|
||||
}
|
||||
const std::uint64_t delta = sample - lastMonotonicMs_;
|
||||
// Do not use a platform wall clock and do not add a whole missed interval
|
||||
// to a queue. This is the monotonic, bounded session-time path.
|
||||
const std::int64_t wall = lastWallSeconds_ + static_cast<std::int64_t>(delta / 1000ULL);
|
||||
(void)setObservation(wall, sample, false, false, false);
|
||||
return lastObservation_;
|
||||
}
|
||||
|
||||
std::uint64_t SessionClock::monotonicNowMs() const {
|
||||
return readMonotonic();
|
||||
}
|
||||
|
||||
std::uint64_t SessionClock::elapsedMs() const noexcept {
|
||||
if (!initialized_) {
|
||||
return 0;
|
||||
}
|
||||
const std::uint64_t sample = monotonicNowMs();
|
||||
return sample < lastMonotonicMs_ ? 0 : sample - lastMonotonicMs_;
|
||||
}
|
||||
|
||||
std::uint64_t SessionClock::sessionElapsedMs() const noexcept {
|
||||
if (!initialized_) {
|
||||
return 0;
|
||||
}
|
||||
const std::uint64_t sample = monotonicNowMs();
|
||||
return sample < anchorMonotonicMs_ ? 0 : sample - anchorMonotonicMs_;
|
||||
}
|
||||
|
||||
void SessionClock::reset() noexcept {
|
||||
initialized_ = false;
|
||||
anchorMonotonicMs_ = 0;
|
||||
lastMonotonicMs_ = 0;
|
||||
anchorWallSeconds_ = 0;
|
||||
lastWallSeconds_ = 0;
|
||||
lastLocal_ = CivilDateTime{};
|
||||
lastObservation_ = Observation{};
|
||||
}
|
||||
|
||||
} // namespace timbre_core
|
||||
91
lib/timbre_core/SessionClock.h
Executable file
91
lib/timbre_core/SessionClock.h
Executable file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include "CivilTime.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
// Hardware integration supplies this interface. The core never calls a
|
||||
// platform-specific tick function itself.
|
||||
class MonotonicClock {
|
||||
public:
|
||||
virtual ~MonotonicClock() = default;
|
||||
virtual std::uint64_t nowMs() const = 0;
|
||||
};
|
||||
|
||||
class SessionClock {
|
||||
public:
|
||||
using Callback = std::function<std::uint64_t()>;
|
||||
|
||||
struct Observation {
|
||||
bool valid = false;
|
||||
bool firstObservation = false;
|
||||
bool monotonicRegression = false;
|
||||
bool resynchronized = false;
|
||||
std::uint64_t monotonicMs = 0;
|
||||
std::int64_t utcEpochSeconds = 0;
|
||||
CivilDateTime local{};
|
||||
|
||||
constexpr operator bool() const noexcept { return valid; }
|
||||
constexpr bool isFirstObservation() const noexcept { return firstObservation; }
|
||||
};
|
||||
|
||||
explicit SessionClock(Callback callback) : callback_(std::move(callback)) {}
|
||||
explicit SessionClock(MonotonicClock& clock) : clock_(&clock) {}
|
||||
SessionClock(const SessionClock&) = delete;
|
||||
SessionClock& operator=(const SessionClock&) = delete;
|
||||
|
||||
// The first wall-clock observation establishes an anchor and returns
|
||||
// firstObservation=true. It never attempts to replay older wall time.
|
||||
Observation observe(std::int64_t utcEpochSeconds);
|
||||
Observation poll(std::int64_t utcEpochSeconds) { return observe(utcEpochSeconds); }
|
||||
Observation sync(std::int64_t utcEpochSeconds) { return observe(utcEpochSeconds); }
|
||||
Observation update(std::int64_t utcEpochSeconds) { return observe(utcEpochSeconds); }
|
||||
|
||||
// Deterministic injection for tests and for a caller that already has a
|
||||
// monotonic sample. It follows exactly the same first-observation rule.
|
||||
Observation observeAt(std::int64_t utcEpochSeconds, std::uint64_t monotonicMs);
|
||||
|
||||
// Advance only from the injected monotonic source. No wall-clock catch-up
|
||||
// is performed, so a sleep or a clock correction cannot create old events.
|
||||
Observation tick();
|
||||
Observation update() { return tick(); }
|
||||
|
||||
bool initialized() const noexcept { return initialized_; }
|
||||
bool hasObservation() const noexcept { return initialized_; }
|
||||
bool hasFirstObservation() const noexcept { return initialized_; }
|
||||
bool lastWasFirstObservation() const noexcept { return lastObservation_.firstObservation; }
|
||||
std::uint64_t monotonicNowMs() const;
|
||||
std::uint64_t now() const { return monotonicNowMs(); }
|
||||
std::uint64_t elapsedMs() const noexcept;
|
||||
std::uint64_t sessionElapsedMs() const noexcept;
|
||||
std::uint64_t elapsedSinceAnchorMs() const noexcept { return sessionElapsedMs(); }
|
||||
std::int64_t utcEpochSeconds() const noexcept { return lastWallSeconds_; }
|
||||
std::int64_t wallEpochSeconds() const noexcept { return lastWallSeconds_; }
|
||||
std::int64_t anchorUtcEpochSeconds() const noexcept { return anchorWallSeconds_; }
|
||||
const CivilDateTime& localTime() const noexcept { return lastLocal_; }
|
||||
const CivilDateTime& civilTime() const noexcept { return lastLocal_; }
|
||||
const Observation& lastObservation() const noexcept { return lastObservation_; }
|
||||
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
std::uint64_t readMonotonic() const;
|
||||
bool setObservation(std::int64_t wallSeconds, std::uint64_t monotonicMs,
|
||||
bool first, bool regression, bool resynchronized);
|
||||
|
||||
Callback callback_;
|
||||
MonotonicClock* clock_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
std::uint64_t anchorMonotonicMs_ = 0;
|
||||
std::uint64_t lastMonotonicMs_ = 0;
|
||||
std::int64_t anchorWallSeconds_ = 0;
|
||||
std::int64_t lastWallSeconds_ = 0;
|
||||
CivilDateTime lastLocal_{};
|
||||
Observation lastObservation_{};
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
75
lib/timbre_core/StateRepository.h
Executable file
75
lib/timbre_core/StateRepository.h
Executable file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "Model.h"
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
// Persistence is represented only by this small interface. Implementations
|
||||
// may use a file system on the final product, but the domain layer remains
|
||||
// portable and has no filesystem dependency.
|
||||
class StateRepository {
|
||||
public:
|
||||
virtual ~StateRepository() = default;
|
||||
|
||||
// Canonical names used by TimbreService.
|
||||
virtual bool load(AppState& destination) { return read(destination); }
|
||||
virtual bool commit(const AppState& source) { return save(source); }
|
||||
|
||||
// Compatibility hooks for small repositories that naturally use read/write
|
||||
// terminology. Overriding either pair is sufficient; the defaults above
|
||||
// bridge the two naming conventions without recursion.
|
||||
virtual bool read(AppState& destination) {
|
||||
(void)destination;
|
||||
return false;
|
||||
}
|
||||
virtual bool write(const AppState& source) {
|
||||
(void)source;
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool save(const AppState& source) { return write(source); }
|
||||
bool loadState(AppState& destination) { return load(destination); }
|
||||
bool commitState(const AppState& source) { return commit(source); }
|
||||
};
|
||||
|
||||
class MemoryStateRepository final : public StateRepository {
|
||||
public:
|
||||
bool load(AppState& destination) override {
|
||||
if (!loadSucceeds_ || !hasValue_) {
|
||||
return false;
|
||||
}
|
||||
destination = value_;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool commit(const AppState& source) override {
|
||||
++commitAttempts_;
|
||||
if (!commitSucceeds_) {
|
||||
return false;
|
||||
}
|
||||
value_ = source;
|
||||
hasValue_ = true;
|
||||
++commitCount_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void seed(const AppState& state) {
|
||||
value_ = state;
|
||||
hasValue_ = true;
|
||||
}
|
||||
|
||||
void setCommitSucceeds(bool succeeds) noexcept { commitSucceeds_ = succeeds; }
|
||||
void setLoadSucceeds(bool succeeds) noexcept { loadSucceeds_ = succeeds; }
|
||||
std::size_t commitAttempts() const noexcept { return commitAttempts_; }
|
||||
std::size_t commitCount() const noexcept { return commitCount_; }
|
||||
|
||||
private:
|
||||
AppState value_{};
|
||||
bool hasValue_ = false;
|
||||
bool loadSucceeds_ = true;
|
||||
bool commitSucceeds_ = true;
|
||||
std::size_t commitAttempts_ = 0;
|
||||
std::size_t commitCount_ = 0;
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
10
lib/timbre_core/TimbreCore.h
Executable file
10
lib/timbre_core/TimbreCore.h
Executable file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Model.h"
|
||||
#include "CivilTime.h"
|
||||
#include "SessionClock.h"
|
||||
#include "ScheduleValidator.h"
|
||||
#include "Scheduler.h"
|
||||
#include "PatternEngine.h"
|
||||
#include "StateRepository.h"
|
||||
#include "TimbreService.h"
|
||||
305
lib/timbre_core/TimbreService.cpp
Executable file
305
lib/timbre_core/TimbreService.cpp
Executable file
@@ -0,0 +1,305 @@
|
||||
#include "TimbreService.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace timbre_core {
|
||||
namespace {
|
||||
|
||||
bool containsProfile(const AppState& state, ProfileId id) noexcept {
|
||||
return state.profile(id) != nullptr;
|
||||
}
|
||||
|
||||
bool containsSchedule(const AppState& state, ScheduleId id) noexcept {
|
||||
return state.schedule(id) != nullptr;
|
||||
}
|
||||
|
||||
bool containsHoliday(const AppState& state, HolidayId id) noexcept {
|
||||
return state.holiday(id) != nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TimbreService::fail(ServiceCode code, const ValidationResult& validation) {
|
||||
lastResult_ = ServiceResult{false, code, validation};
|
||||
return false;
|
||||
}
|
||||
|
||||
void TimbreService::observeIds(const AppState& state) noexcept {
|
||||
nextProfileId_.reset();
|
||||
nextScheduleId_.reset();
|
||||
nextHolidayId_.reset();
|
||||
for (std::size_t i = 0; i < state.profiles.size(); ++i) {
|
||||
nextProfileId_.observe(state.profiles[i].id);
|
||||
}
|
||||
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
|
||||
nextScheduleId_.observe(state.schedules[i].id);
|
||||
}
|
||||
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
|
||||
nextHolidayId_.observe(state.holidays[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
Id TimbreService::nextAvailableProfileId(const AppState& state) {
|
||||
for (;;) {
|
||||
const Id id = nextProfileId_.next();
|
||||
if (id != kInvalidId && !containsProfile(state, id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Id TimbreService::nextAvailableScheduleId(const AppState& state) {
|
||||
for (;;) {
|
||||
const Id id = nextScheduleId_.next();
|
||||
if (id != kInvalidId && !containsSchedule(state, id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Id TimbreService::nextAvailableHolidayId(const AppState& state) {
|
||||
for (;;) {
|
||||
const Id id = nextHolidayId_.next();
|
||||
if (id != kInvalidId && !containsHoliday(state, id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool TimbreService::load() {
|
||||
AppState candidate;
|
||||
if (repository_ == nullptr || !repository_->load(candidate)) {
|
||||
return fail(ServiceCode::LoadFailed, ValidationResult{});
|
||||
}
|
||||
const ValidationResult validation = ScheduleValidator::validate(candidate);
|
||||
if (!validation.valid) {
|
||||
return fail(ServiceCode::InvalidRepositoryState, validation);
|
||||
}
|
||||
ram_ = candidate;
|
||||
observeIds(ram_);
|
||||
loaded_ = true;
|
||||
lastResult_ = ServiceResult{true, ServiceCode::Ok, ValidationResult{}};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimbreService::commitCurrent() {
|
||||
const ValidationResult validation = ScheduleValidator::validate(ram_);
|
||||
if (!validation.valid) {
|
||||
return fail(ServiceCode::ValidationFailed, validation);
|
||||
}
|
||||
if (repository_ == nullptr || !repository_->commit(ram_)) {
|
||||
return fail(ServiceCode::CommitFailed, ValidationResult{});
|
||||
}
|
||||
lastResult_ = ServiceResult{true, ServiceCode::Ok, ValidationResult{}};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimbreService::addProfile(const Profile& profile) {
|
||||
if (ram_.profiles.full()) {
|
||||
return fail(ServiceCode::LimitExceeded, ValidationResult{});
|
||||
}
|
||||
const ProfileId id = profile.id == kInvalidId ? nextAvailableProfileId(ram_) : profile.id;
|
||||
if (containsProfile(ram_, id)) {
|
||||
return fail(ServiceCode::DuplicateId, ValidationResult{});
|
||||
}
|
||||
Profile copy = profile;
|
||||
copy.id = id;
|
||||
const StableIdAllocator savedAllocator = nextProfileId_;
|
||||
const bool committed = transact([©](AppState& candidate) {
|
||||
if (!candidate.profiles.push_back(copy)) {
|
||||
return false;
|
||||
}
|
||||
if (candidate.profiles.size() == 1) {
|
||||
candidate.activeProfileId = copy.id;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!committed) {
|
||||
nextProfileId_ = savedAllocator;
|
||||
}
|
||||
return committed;
|
||||
}
|
||||
|
||||
bool TimbreService::updateProfile(ProfileId id, const Profile& profile) {
|
||||
if (!containsProfile(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
Profile copy = profile;
|
||||
copy.id = id;
|
||||
return transact([©](AppState& candidate) {
|
||||
Profile* target = candidate.profile(copy.id);
|
||||
if (target == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*target = copy;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::removeProfile(ProfileId id) {
|
||||
if (!containsProfile(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
return transact([id](AppState& candidate) {
|
||||
std::size_t index = 0;
|
||||
for (; index < candidate.profiles.size(); ++index) {
|
||||
if (candidate.profiles[index].id == id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= candidate.profiles.size() || !candidate.profiles.erase(index)) {
|
||||
return false;
|
||||
}
|
||||
if (candidate.activeProfileId == id) {
|
||||
candidate.activeProfileId = candidate.profiles.empty()
|
||||
? 0
|
||||
: candidate.profiles[0].id;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::addSchedule(const Schedule& schedule) {
|
||||
if (ram_.schedules.full()) {
|
||||
return fail(ServiceCode::LimitExceeded, ValidationResult{});
|
||||
}
|
||||
const ScheduleId id = schedule.id == kInvalidId
|
||||
? nextAvailableScheduleId(ram_)
|
||||
: schedule.id;
|
||||
if (containsSchedule(ram_, id)) {
|
||||
return fail(ServiceCode::DuplicateId, ValidationResult{});
|
||||
}
|
||||
Schedule copy = schedule;
|
||||
copy.id = id;
|
||||
const StableIdAllocator savedAllocator = nextScheduleId_;
|
||||
const bool committed = transact([©](AppState& candidate) {
|
||||
if (!candidate.schedules.push_back(copy)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!committed) {
|
||||
nextScheduleId_ = savedAllocator;
|
||||
}
|
||||
return committed;
|
||||
}
|
||||
|
||||
bool TimbreService::updateSchedule(ScheduleId id, const Schedule& schedule) {
|
||||
if (!containsSchedule(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
Schedule copy = schedule;
|
||||
copy.id = id;
|
||||
return transact([©](AppState& candidate) {
|
||||
Schedule* target = candidate.schedule(copy.id);
|
||||
if (target == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*target = copy;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::removeSchedule(ScheduleId id) {
|
||||
if (!containsSchedule(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
return transact([id](AppState& candidate) {
|
||||
for (std::size_t i = 0; i < candidate.schedules.size(); ++i) {
|
||||
if (candidate.schedules[i].id == id) {
|
||||
return candidate.schedules.erase(i);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::addHoliday(const Holiday& holiday) {
|
||||
if (ram_.holidays.full()) {
|
||||
return fail(ServiceCode::LimitExceeded, ValidationResult{});
|
||||
}
|
||||
const HolidayId id = holiday.id == kInvalidId
|
||||
? nextAvailableHolidayId(ram_)
|
||||
: holiday.id;
|
||||
if (containsHoliday(ram_, id)) {
|
||||
return fail(ServiceCode::DuplicateId, ValidationResult{});
|
||||
}
|
||||
Holiday copy = holiday;
|
||||
copy.id = id;
|
||||
const StableIdAllocator savedAllocator = nextHolidayId_;
|
||||
const bool committed = transact([©](AppState& candidate) {
|
||||
if (!candidate.holidays.push_back(copy)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!committed) {
|
||||
nextHolidayId_ = savedAllocator;
|
||||
}
|
||||
return committed;
|
||||
}
|
||||
|
||||
bool TimbreService::updateHoliday(HolidayId id, const Holiday& holiday) {
|
||||
if (!containsHoliday(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
Holiday copy = holiday;
|
||||
copy.id = id;
|
||||
return transact([©](AppState& candidate) {
|
||||
Holiday* target = candidate.holiday(copy.id);
|
||||
if (target == nullptr) {
|
||||
return false;
|
||||
}
|
||||
*target = copy;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::removeHoliday(HolidayId id) {
|
||||
if (!containsHoliday(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
return transact([id](AppState& candidate) {
|
||||
for (std::size_t i = 0; i < candidate.holidays.size(); ++i) {
|
||||
if (candidate.holidays[i].id == id) {
|
||||
return candidate.holidays.erase(i);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::setActiveProfile(ProfileId id) {
|
||||
if (!containsProfile(ram_, id)) {
|
||||
return fail(ServiceCode::NotFound, ValidationResult{});
|
||||
}
|
||||
return transact([id](AppState& candidate) {
|
||||
candidate.activeProfileId = id;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::setPattern(PatternId patternIdentifier, const Pattern& pattern) {
|
||||
if (!isPatternId(patternIdentifier) ||
|
||||
patternIdentifier != pattern.id ||
|
||||
!ScheduleValidator::validatePattern(pattern).valid) {
|
||||
return fail(ServiceCode::ValidationFailed, ValidationResult{});
|
||||
}
|
||||
return transact([patternIdentifier, pattern](AppState& candidate) {
|
||||
const std::size_t index = patternIndex(patternIdentifier);
|
||||
if (index >= candidate.patterns.size()) {
|
||||
return false;
|
||||
}
|
||||
candidate.patterns[index] = pattern;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool TimbreService::setPatterns(const std::array<Pattern, kMaxPatterns>& patterns) {
|
||||
return transact([&patterns](AppState& candidate) {
|
||||
candidate.patterns = patterns;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace timbre_core
|
||||
136
lib/timbre_core/TimbreService.h
Executable file
136
lib/timbre_core/TimbreService.h
Executable file
@@ -0,0 +1,136 @@
|
||||
#pragma once
|
||||
|
||||
#include "ScheduleValidator.h"
|
||||
#include "StateRepository.h"
|
||||
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace timbre_core {
|
||||
|
||||
enum class ServiceCode : std::uint8_t {
|
||||
Ok = 0,
|
||||
NotLoaded,
|
||||
LoadFailed,
|
||||
InvalidRepositoryState,
|
||||
MutationRejected,
|
||||
ValidationFailed,
|
||||
CommitFailed,
|
||||
NotFound,
|
||||
LimitExceeded,
|
||||
DuplicateId
|
||||
};
|
||||
|
||||
struct ServiceResult {
|
||||
bool ok = true;
|
||||
ServiceCode code = ServiceCode::Ok;
|
||||
ValidationResult validation{};
|
||||
|
||||
constexpr bool succeeded() const noexcept { return ok; }
|
||||
constexpr explicit operator bool() const noexcept { return ok; }
|
||||
};
|
||||
|
||||
class TimbreService {
|
||||
public:
|
||||
explicit TimbreService(StateRepository& repository) : repository_(&repository) {}
|
||||
|
||||
bool load();
|
||||
bool loaded() const noexcept { return loaded_; }
|
||||
const AppState& state() const noexcept { return ram_; }
|
||||
const AppState& ram() const noexcept { return ram_; }
|
||||
const ServiceResult& lastResult() const noexcept { return lastResult_; }
|
||||
ServiceCode lastError() const noexcept { return lastResult_.code; }
|
||||
|
||||
// Copy, mutate, validate, persist, and only then publish to ram_. The
|
||||
// lambda may return bool (false rejects the transaction) or void.
|
||||
template <typename Mutator>
|
||||
bool transact(Mutator&& mutator) {
|
||||
AppState candidate = ram_;
|
||||
bool accepted = true;
|
||||
if constexpr (std::is_void_v<std::invoke_result_t<Mutator, AppState&>>) {
|
||||
std::forward<Mutator>(mutator)(candidate);
|
||||
} else {
|
||||
accepted = static_cast<bool>(
|
||||
std::forward<Mutator>(mutator)(candidate));
|
||||
}
|
||||
if (!accepted) {
|
||||
return fail(ServiceCode::MutationRejected, ValidationResult{});
|
||||
}
|
||||
|
||||
if (candidate.revision == std::numeric_limits<std::uint32_t>::max()) {
|
||||
candidate.revision = 0;
|
||||
} else {
|
||||
++candidate.revision;
|
||||
}
|
||||
const ValidationResult validation = ScheduleValidator::validate(candidate);
|
||||
if (!validation.valid) {
|
||||
return fail(ServiceCode::ValidationFailed, validation);
|
||||
}
|
||||
if (!repository_->commit(candidate)) {
|
||||
return fail(ServiceCode::CommitFailed, ValidationResult{});
|
||||
}
|
||||
|
||||
// This is the only assignment to the live model in a transaction.
|
||||
ram_ = candidate;
|
||||
loaded_ = true;
|
||||
lastResult_ = ServiceResult{true, ServiceCode::Ok, ValidationResult{}};
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Mutator>
|
||||
bool mutate(Mutator&& mutator) {
|
||||
return transact(std::forward<Mutator>(mutator));
|
||||
}
|
||||
|
||||
template <typename Mutator>
|
||||
bool apply(Mutator&& mutator) {
|
||||
return transact(std::forward<Mutator>(mutator));
|
||||
}
|
||||
|
||||
bool replace(const AppState& replacement) {
|
||||
return transact([&replacement](AppState& candidate) {
|
||||
candidate = replacement;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
bool commitCurrent();
|
||||
bool commit() { return commitCurrent(); }
|
||||
|
||||
bool addProfile(const Profile& profile);
|
||||
bool updateProfile(ProfileId id, const Profile& profile);
|
||||
bool removeProfile(ProfileId id);
|
||||
bool deleteProfile(ProfileId id) { return removeProfile(id); }
|
||||
|
||||
bool addSchedule(const Schedule& schedule);
|
||||
bool updateSchedule(ScheduleId id, const Schedule& schedule);
|
||||
bool removeSchedule(ScheduleId id);
|
||||
bool deleteSchedule(ScheduleId id) { return removeSchedule(id); }
|
||||
|
||||
bool addHoliday(const Holiday& holiday);
|
||||
bool updateHoliday(HolidayId id, const Holiday& holiday);
|
||||
bool removeHoliday(HolidayId id);
|
||||
bool deleteHoliday(HolidayId id) { return removeHoliday(id); }
|
||||
|
||||
bool setActiveProfile(ProfileId id);
|
||||
bool setPattern(PatternId id, const Pattern& pattern);
|
||||
bool setPatterns(const std::array<Pattern, kMaxPatterns>& patterns);
|
||||
|
||||
private:
|
||||
bool fail(ServiceCode code, const ValidationResult& validation);
|
||||
void observeIds(const AppState& state) noexcept;
|
||||
Id nextAvailableProfileId(const AppState& state);
|
||||
Id nextAvailableScheduleId(const AppState& state);
|
||||
Id nextAvailableHolidayId(const AppState& state);
|
||||
|
||||
StateRepository* repository_ = nullptr;
|
||||
AppState ram_{};
|
||||
StableIdAllocator nextProfileId_{};
|
||||
StableIdAllocator nextScheduleId_{};
|
||||
StableIdAllocator nextHolidayId_{};
|
||||
bool loaded_ = false;
|
||||
ServiceResult lastResult_{};
|
||||
};
|
||||
|
||||
} // namespace timbre_core
|
||||
13
lib/timbre_core/library.json
Executable file
13
lib/timbre_core/library.json
Executable file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "timbre_core",
|
||||
"version": "0.1.0",
|
||||
"description": "Portable, bounded domain core for the TimbreESP project",
|
||||
"keywords": "timbre, esp32, scheduler",
|
||||
"license": "MIT",
|
||||
"frameworks": "*",
|
||||
"build": {
|
||||
"flags": [
|
||||
"-std=c++17"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user