-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNavienLearner.h
More file actions
208 lines (166 loc) · 9.71 KB
/
NavienLearner.h
File metadata and controls
208 lines (166 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/*
Copyright (c) 2026 David Carson (dacarson)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#pragma once
#include <stdint.h>
#include <time.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "BucketStore.h"
#include "PeakFinder.h"
// Arduino String (WString.h) — forward declare so this header does not depend
// on Arduino.h; translation units that call checkNewSchedule() must include it.
class String;
// ---------------------------------------------------------------------------
// Cold-start event transported from Core 1 → Core 0
// ---------------------------------------------------------------------------
struct PendingColdStart {
int dow; // day of week at tap-open time (0=Sun)
int bucket; // 5-minute bucket index at tap-open time (0–287)
float demand_weight; // 0.5 (short cold-pipe tap) or 1.0 (genuine demand)
float recency_weight; // current year's recency weight multiplier (3.0)
bool recircAtStart; // was recirc running when the run started?
// (used by Core 0 to update measured-efficiency counters)
};
// ---------------------------------------------------------------------------
// Rolling measured-efficiency window — 4 weeks × 7 days
// ---------------------------------------------------------------------------
struct WeekMeasured {
uint16_t total[7]; // cold-starts observed per day-of-week this week
uint16_t covered[7]; // cold-starts where recirc was already running
};
// ---------------------------------------------------------------------------
// NavienLearner
// ---------------------------------------------------------------------------
class NavienLearner {
public:
NavienLearner();
// Initialise: create FreeRTOS queue and BucketStore.
// Returns false if queue allocation fails; the learner is then silently
// inactive but does not crash. Must be called before onNavienState().
bool begin();
// Called from the water packet callback on Core 1 for every RS-485 packet.
// Detects cold-starts and enqueues PendingColdStart events for Core 0.
// Returns immediately if the learner is disabled.
void onNavienState(bool consumption_active,
bool recirculation_active,
time_t now);
// Access to the cross-core cold-start queue (consumed by Core 0 task).
QueueHandle_t coldStartQueue() const { return _coldStartQueue; }
// Access to the BucketStore for Core 0 updates.
BucketStore &bucketStore() { return _store; }
// Rolling measured efficiency — 4-week window.
// Indexed [week_slot][dow]; week_slot rotates on Sunday midnight.
const WeekMeasured *measuredWindow() const { return _measured; }
uint8_t measuredHead() const { return _measuredHead; }
// Advance the rolling window to a new week (called at Sunday midnight).
void advanceMeasuredWeek();
// Signal the Core 0 task to run a recompute immediately (e.g. after
// POST /buckets seeds new bucket data). Safe to call from any core.
void requestRecompute() { _recomputeRequested = true; }
// Ingest a sparse bucket payload from POST /buckets (called from Core 1
// during bootstrap only). Parses JSON, merges or replaces _buckets in
// RAM, writes atomically to LittleFS, then sets _recomputeRequested.
// Returns the number of individual buckets written, or -1 on error
// (schema mismatch, parse failure, or save failure).
// 'replaced' is set to reflect the value of the "replace" field.
int ingestBucketPayload(const char *json, bool &replaced);
// Called from FakeGatoScheduler::loop() on Core 1. Non-blocking: returns
// false immediately if no new schedule is ready or the mutex is held.
// Returns true and fills out_json with the schedule JSON if ready.
bool checkNewSchedule(String &out_json);
// Persist _measured[] and _measuredHead to /navien/measured.bin.
// Safe to call from any core (coarse stats, no lock taken).
// Returns true on success.
bool saveMeasured();
// Load _measured[] and _measuredHead from /navien/measured.bin.
// Called during begin(); silently succeeds (leaves zeroed RAM) if the
// file is absent or corrupt.
bool loadMeasured();
// Wall time of the last completed RECOMPUTE_WRITE (0 = never).
time_t lastRecomputeTime() const { return _lastRecomputeTime; }
// Per-day predicted efficiency from the last recompute (NAN if insufficient
// bucket data). Index 0=Sunday .. 6=Saturday.
const float *predictedEfficiency() const { return _predictedEfficiency; }
// Append a Learner Status HTML section to page. Called from the web status
// callback on Core 1; reads _measured[] as coarse stats without locking.
void appendStatusHTML(String &page) const;
bool isDisabled() const { return _learnerDisabled; }
// Cold-start detection thresholds (seconds)
static constexpr uint32_t COLD_GAP_SEC = 600; // 10 min
static constexpr uint32_t MIN_DURATION_GENUINE_SEC = 60; // 6 × 10s
static constexpr uint32_t MIN_DURATION_RECIRC_SEC = 30; // 3 × 10s
static constexpr uint32_t RECIRC_HOT_WINDOW_SEC = 900; // 15 min — matches config.py RECIRC_WINDOW_MINUTES
// Recency weight for live (current-year) data, matching Python [3, 2]
static constexpr float RECENCY_WEIGHT_CURRENT = 3.0f;
// Core 0 task entry point — launched by begin().
static void learnerTask(void *pvParam);
private:
// Task state machine states (Core 0).
enum TaskState { IDLE, DECAY_CHECK, RECOMPUTE_LOAD, RECOMPUTING, RECOMPUTE_WRITE };
// Compute demand_weight from run characteristics.
// Returns 0.0 if the event should be discarded.
float computeDemandWeight(bool recircAtStart, uint32_t durationSec) const;
// Core 0 state machine helpers.
void idleStep(); // called every IDLE tick: queue drain, midnight check
void decayCheck(); // apply annual weighted_score decay if year has rolled over
void recomputeWrite(); // builds JSON and hands off to Core 1 via mutex
void broadcastUDP(); // broadcasts learner JSON packet over UDP (Phase 8)
// --- Cold-start detector state (Core 1 only) ---
time_t _lastActiveTime; // last time consumption_active was true
bool _inRun; // currently inside an active run
time_t _runStart; // when the current run started
uint32_t _runDurationSec; // elapsed seconds of current run
int _runDow; // day-of-week pinned at run start (0=Sun)
int _runBucket; // 5-min bucket index pinned at run start
bool _recircAtStart; // was recirc active (or recently active) when run started?
time_t _lastRecircActiveTime; // last time recirculation_active was true (0 = never)
// --- Cross-core queue (capacity 1, Core 1 writes, Core 0 reads) ---
QueueHandle_t _coldStartQueue;
// --- Core 0 task ---
TaskHandle_t _taskHandle;
volatile bool _recomputeRequested; // set from any core, cleared on Core 0
TaskState _taskState;
int _recomputeDay; // 0–6; current day being processed in RECOMPUTING
time_t _lastRecomputeTime24h; // wall time of last 24h recompute trigger (0 = never)
bool _startupDecayDone; // true once the one-shot startup decay check has run
time_t _lastRecomputeTime; // wall time of last RECOMPUTE_WRITE (0 = never)
// Capacity for the schedule JSON buffer.
// Includes per-slot score metadata (`"score":%.3f`) used by learnerStatus.
// Worst-case expanded payload is now ~1.65 KB; 2048 keeps safe headroom.
static constexpr int SCHEDULE_JSON_CAPACITY = 2048;
// --- Schedule handoff (Core 0 writes, Core 1 reads — guarded by mutex) ---
SemaphoreHandle_t _scheduleHandoffMutex;
char _pendingScheduleJSON[SCHEDULE_JSON_CAPACITY];
bool _newScheduleReady;
// --- Recompute results (Core 0 only) ---
TimeSlot _weekSlots[7][MAX_SLOTS_PER_DAY]; // slots per day from last recompute
int _weekSlotCount[7]; // slot count per day (0–MAX_SLOTS_PER_DAY)
float _predictedEfficiency[7]; // per-day predicted efficiency (Phase 7)
// --- Measured efficiency rolling window (Core 0 writes only) ---
// Updated in idleStep() when consuming cold-start events from the queue.
// Phase 7 Telnet/UI readers on Core 1 should treat these as coarse stats
// (no lock needed for read-only display, but values may be mid-update).
WeekMeasured _measured[4];
uint8_t _measuredHead; // index of current week slot (0–3)
// --- Persistent bucket storage (Core 0 reads/writes) ---
BucketStore _store;
bool _learnerDisabled;
};