Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
oneway_sequence_store.cpp
Go to the documentation of this file.
1/// @file oneway_sequence_store.cpp
2/// @brief Persistent rolling-sequence counters for one-way (1W) transmit.
3/// @ingroup hioc_hub
4
6
7#include "esphome/core/log.h"
8
9#include <cstring>
10
11namespace esphome {
12namespace home_io_control {
13
14namespace {
15
16constexpr const char *const TAG = "home_io_control.oneway_seq";
17
18/// FNV-1 parameters, matching the hash ESPHome uses for its own preference keys.
19constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261U;
20constexpr uint32_t FNV1_PRIME = 16777619U;
21
22/// Preference key for an identity's counter.
23///
24/// Derived from the node address alone, so it is stable across builds, across firmware versions
25/// and across YAML edits that reorder identities — the persisted counter must follow the address
26/// it belongs to, because that is what the receiving device tracks. An index- or name-derived key
27/// would silently hand an identity somebody else's counter after a reorder or a rename.
28uint32_t preference_key(const uint8_t node_id[NODE_ID_SIZE]) {
29 // FNV-1 over the three address bytes, with a fixed prefix so this component's keys cannot
30 // collide with an entity's.
31 uint32_t hash = FNV1_OFFSET_BASIS;
32 hash = (hash ^ static_cast<uint8_t>('1')) * FNV1_PRIME;
33 hash = (hash ^ static_cast<uint8_t>('W')) * FNV1_PRIME;
34 for (uint8_t i = 0; i < NODE_ID_SIZE; i++)
35 hash = (hash ^ node_id[i]) * FNV1_PRIME;
36 return hash;
37}
38
39/// Persist a reservation and commit it to flash before returning.
40///
41/// save() only stages the write — on ESP32 nothing reaches NVS until sync() commits it. Without
42/// the sync the reservation would be lost by exactly the reboot it exists to survive.
43bool persist(ESPPreferenceObject &pref, uint16_t value) {
44 if (!pref.save(&value))
45 return false;
46 return global_preferences->sync();
47}
48
49} // namespace
50
51void OneWaySequenceStore::add_identity(const uint8_t node_id[NODE_ID_SIZE], uint16_t initial_sequence) {
52 if (this->find_(node_id) != nullptr)
53 return; // Registering an address twice would give one transmitter two counters.
54
55 Counter counter{};
56 memcpy(counter.node_id, node_id, NODE_ID_SIZE);
57 counter.pref = global_preferences->make_preference<uint16_t>(preference_key(node_id));
58
59 uint16_t persisted = 0;
60 if (counter.pref.load(&persisted)) {
61 // Resume from whichever is *higher*. Forward is always safe, backward never is, so a YAML
62 // edit can push a desynced counter ahead — which is what `initial_sequence` is for — but can
63 // never drag a live one back into replay territory. seed() is the deliberate way to go back.
64 // Plain numeric comparison, not ring arithmetic: as a config escape hatch, predictable beats
65 // clever, and a user who needs to cross the wrap point should use seed().
66 counter.next = persisted > initial_sequence ? persisted : initial_sequence;
67 } else {
68 counter.next = initial_sequence;
69 }
70 // Nothing is reserved yet: the first next() call writes before it hands anything out.
71 counter.reserved = counter.next;
72
73 this->counters_.push_back(counter);
74}
75
76bool OneWaySequenceStore::next(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out) {
77 Counter *counter = this->find_(node_id);
78 if (counter == nullptr)
79 return false;
80
81 if (counter->next == counter->reserved) {
82 // Block exhausted (or never reserved). Persist the end of the next block before handing out
83 // anything inside it. Unsigned wraparound is intended: the counter is a 16-bit ring.
84 const auto new_reservation = static_cast<uint16_t>(counter->next + ONEWAY_SEQUENCE_STRIDE);
85 if (!persist(counter->pref, new_reservation)) {
86 // Refusing here is the point: transmitting an unreserved sequence risks reusing it after a
87 // reboot, and a device that sees a replay ignores every later command too.
88 ESP_LOGE(TAG, "Could not persist a 1W sequence reservation; refusing to transmit");
89 return false;
90 }
91 counter->reserved = new_reservation;
92 }
93
94 out = counter->next;
95 counter->next = static_cast<uint16_t>(counter->next + 1);
96 return true;
97}
98
99bool OneWaySequenceStore::seed(const uint8_t node_id[NODE_ID_SIZE], uint16_t value) {
100 Counter *counter = this->find_(node_id);
101 if (counter == nullptr)
102 return false;
103
104 if (!persist(counter->pref, value))
105 return false;
106 counter->next = value;
107 // Nothing beyond `value` is reserved, so the next call re-reserves from here.
108 counter->reserved = value;
109 return true;
110}
111
112bool OneWaySequenceStore::peek(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out) const {
113 const Counter *counter = this->find_(node_id);
114 if (counter == nullptr)
115 return false;
116 out = counter->next;
117 return true;
118}
119
120OneWaySequenceStore::Counter *OneWaySequenceStore::find_(const uint8_t node_id[NODE_ID_SIZE]) {
121 for (auto &counter : this->counters_) {
122 if (memcmp(counter.node_id, node_id, NODE_ID_SIZE) == 0)
123 return &counter;
124 }
125 return nullptr;
126}
127
128const OneWaySequenceStore::Counter *OneWaySequenceStore::find_(const uint8_t node_id[NODE_ID_SIZE]) const {
129 for (const auto &counter : this->counters_) {
130 if (memcmp(counter.node_id, node_id, NODE_ID_SIZE) == 0)
131 return &counter;
132 }
133 return nullptr;
134}
135
136} // namespace home_io_control
137} // namespace esphome
bool seed(const uint8_t node_id[NODE_ID_SIZE], uint16_t value)
Force an identity's counter to a specific value and persist it immediately.
void add_identity(const uint8_t node_id[NODE_ID_SIZE], uint16_t initial_sequence)
Register a controller identity and load its persisted counter.
bool next(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out)
Reserve and return the next sequence for an identity.
bool peek(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out) const
The next sequence this identity would hand out, without reserving it.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
static constexpr const char * TAG
static constexpr uint16_t ONEWAY_SEQUENCE_STRIDE
How many sequences one flash write reserves.
Persistent rolling-sequence counters for one-way (1W) transmit.