-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenericBaseProject.cpp
More file actions
303 lines (246 loc) · 9.94 KB
/
Copy pathgenericBaseProject.cpp
File metadata and controls
303 lines (246 loc) · 9.94 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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// ----------------------------
// Library Defines - Need to be defined before library import
// ----------------------------
#define ESP_DRD_USE_SPIFFS true
// ----------------------------
// Configuration
// ----------------------------
// Preconfigured WiFi credentials live in an untracked "secrets.h" so they are
// never committed to the repository. Copy secrets.h.example to secrets.h and
// fill in your own values. These are tried first on boot, with a fallback to
// the WiFiManager captive portal if the connection fails.
#if __has_include("secrets.h")
#include "secrets.h"
#else
#error "Missing secrets.h - copy secrets.h.example to secrets.h and set your WiFi credentials"
#endif
#define WIFI_CONNECT_TIMEOUT 5000 // per-attempt timeout for the preconfigured WiFi connection
#define WIFI_CONNECT_ATTEMPTS 10 // attempts before falling back to the WiFiManager portal
// ----------------------------
// Standard Libraries
// ----------------------------
#include <WiFi.h>
#include <FS.h>
#include "SPIFFS.h"
// ----------------------------
// Additional Libraries - each one of these will need to be installed.
// ----------------------------
#include <ESP_DoubleResetDetector.h>
// A library for checking if the reset button has been pressed twice
// Can be used to enable config mode
// Can be installed from the library manager (Search for "ESP_DoubleResetDetector")
// https://github.com/khoih-prog/ESP_DoubleResetDetector
#include <ezTime.h>
// Library used for getting the time and converting session time
// to users timezone
// Search for "ezTime" in the Arduino Library manager
// https://github.com/ropg/ezTime
// ----------------------------
// Internal includes
// ----------------------------
#include "genericBaseProject.h"
#include "otaUpdate.h"
#include "wifiManagerHandler.h"
#include "cheapYellowLCD.h"
#include "holidayService.h" // named public holidays for the zones
#include "marketHolidays.h" // cached/fetched exchange holiday calendars
#include "uiPages.h" // getPosixFallback - offline timezone rules
#include "wifiWatch.h" // runtime WiFi-loss supervision
// Number of seconds after reset during which a
// subseqent reset will be considered a double reset.
#define DRD_TIMEOUT 10
// RTC Memory Address for the DoubleResetDetector to use
#define DRD_ADDRESS 0
// NTP sync monitoring variables
unsigned long lastSyncTime = 0;
unsigned long syncCount = 0;
bool ntpSyncStatus = false;
CheapYellowDisplay cyd;
ProjectDisplay *projectDisplay = &cyd;
Timezone myTZ;
// Handle ezTime NTP synchronization and sync monitoring.
//
// IMPORTANT: ezTime is not thread-safe. This runs from the main loop (the same
// context that reads the time and calls setLocation()) so that all ezTime
// access happens on a single core. A previous version ran events() in a task
// pinned to Core 0 while the main loop read the clock on Core 1, which is an
// unsynchronized concurrent access to ezTime's internal state.
static void handleTimeSync() {
static unsigned long lastStatusReport = 0;
static time_t lastKnownTime = 0;
// Store time before events() call
time_t timeBefore = UTC.now();
// Handle ezTime events for NTP synchronization
events();
// Check if time was updated (indicates sync occurred)
time_t timeAfter = UTC.now();
// Detect if a sync just happened
if (timeAfter != lastKnownTime && timeAfter > 1000000000) { // Valid timestamp
if (lastKnownTime > 0 && abs(timeAfter - timeBefore) > 1) {
// Time jumped significantly - likely a sync occurred
syncCount++;
lastSyncTime = millis();
ntpSyncStatus = true;
Log.println("NTP Sync #" + String(syncCount) + " - " + UTC.dateTime() + " (Uptime: " + String(millis()/1000/60) + "min)");
}
lastKnownTime = timeAfter;
}
// Report sync status every 30 minutes (production frequency)
if (millis() - lastStatusReport > 1800000) { // 30 minutes
lastStatusReport = millis();
Log.println("NTP Status: " + String(syncCount) + " syncs, Last: " + String((millis() - lastSyncTime)/1000/60) + "min ago");
}
}
void baseProjectSetup()
{
projectDisplay->displaySetup();
bool forceConfig = false;
bool wifiConnected = false;
// Initialize double reset detector
drd = new DoubleResetDetector(DRD_TIMEOUT, DRD_ADDRESS);
if (drd->detectDoubleReset())
{
Log.println(F("Forcing config mode as there was a Double reset detected"));
forceConfig = true;
}
// Initialize SPIFFS
bool spiffsInitSuccess = SPIFFS.begin(false) || SPIFFS.begin(true);
if (!spiffsInitSuccess)
{
Log.println("SPIFFS initialisation failed!");
while (1)
yield(); // Stay here twiddling thumbs waiting
}
Log.println("\r\nInitialisation done.");
// Try to load existing config first
if (!projectConfig.fetchConfigFile())
{
Log.println("No saved config found, will use defaults or WiFiManager");
}
// Load the cached market holiday calendars (SPIFFS is up); the weekly
// network refresh is scheduled later from the main loop.
marketHolidaysBegin();
// Step 1: Try preconfigured WiFi credentials first. A single attempt can
// fail even when the network is fine (AP busy, weak signal on boot), so
// retry several times before falling back to the config portal.
if (!forceConfig)
{
Log.println("Attempting to connect with preconfigured WiFi...");
WiFi.mode(WIFI_STA);
for (int attempt = 1; attempt <= WIFI_CONNECT_ATTEMPTS && !wifiConnected; attempt++)
{
Log.print("WiFi connect attempt ");
Log.print(attempt);
Log.print("/");
Log.print(WIFI_CONNECT_ATTEMPTS);
Log.print(" ");
// Reset any half-finished connection state from the previous attempt
WiFi.disconnect();
delay(100);
WiFi.begin(PRECONFIGURED_SSID, PRECONFIGURED_PASSWORD);
unsigned long startTime = millis();
while (WiFi.status() != WL_CONNECTED && (millis() - startTime) < WIFI_CONNECT_TIMEOUT)
{
delay(500);
Log.print(".");
}
Log.println();
if (WiFi.status() == WL_CONNECTED)
{
wifiConnected = true;
}
}
if (wifiConnected)
{
Log.println("Connected with preconfigured WiFi!");
Log.print("IP address: ");
Log.println(WiFi.localIP());
// Use preconfigured settings if no saved config
if (projectConfig.timeZone.length() == 0)
{
projectConfig.timeZone = PRECONFIGURED_TIMEZONE;
projectConfig.twentyFourHour = false;
projectConfig.usDateFormat = true;
Log.println("Using preconfigured timezone and settings");
}
}
else
{
Log.println("All preconfigured WiFi attempts failed, falling back to WiFiManager");
forceConfig = true;
}
}
// Step 2: If preconfigured WiFi failed or forced config, use WiFiManager
if (!wifiConnected)
{
setupWiFiManager(forceConfig, projectConfig, projectDisplay);
}
// Final check to ensure WiFi is connected. Bounded: both paths above are
// supposed to have us connected by now, so if we're still offline after
// 30 seconds something is wedged - reboot and run the whole sequence
// again rather than hanging here forever.
unsigned long wifiWaitStart = millis();
while (WiFi.status() != WL_CONNECTED)
{
if (millis() - wifiWaitStart > 30000UL)
{
Log.println("\nStill no WiFi after portal/connect - rebooting to retry");
drd->stop(); // avoid the reboot registering as a double reset
delay(1000);
ESP.restart();
}
Log.print(".");
delay(500);
}
Log.println("");
Log.println("WiFi connected");
Log.print("IP address: ");
Log.println(WiFi.localIP());
// Enable over-the-air firmware updates now that the network is up
setupOTA();
Log.println("Waiting for time sync");
// Configure NTP sync frequency for production
setInterval(1800); // Sync every 30 minutes (production setting)
setDebug(ERROR); // Only show errors, not all debug info
Log.println("Performing initial NTP sync...");
waitForSync();
Log.println("Initial NTP sync complete!");
Log.println();
Log.println("UTC: " + UTC.dateTime());
// EEPROM cache slot 4 (slots 0-3 belong to the world-clock zones in
// ClockLogic.cpp), so this zone survives timezone-server outages too. The
// cache payload is uppercased, hence the case-insensitive name check.
if (!(myTZ.setCache(4 * EEPROM_CACHE_LEN) &&
myTZ.getOlson().equalsIgnoreCase(projectConfig.timeZone)))
{
if (!myTZ.setLocation(projectConfig.timeZone))
{
// Timezone server unreachable and nothing cached: preset zones
// carry built-in POSIX rules so local time is still correct.
const char *posix = getPosixFallback(projectConfig.timeZone);
if (posix)
{
myTZ.setPosix(posix);
Log.println("Timezone server unreachable - using built-in POSIX rules");
}
}
}
Log.print(projectConfig.timeZone);
Log.print(F(": "));
Log.println(myTZ.dateTime());
Log.println("-------------------------");
}
void baseProjectLoop()
{
drd->loop();
// Handle ezTime NTP events on the main core (ezTime is not thread-safe).
handleTimeSync();
// Service over-the-air update requests
handleOTA();
// Watch the WiFi link: offline indicator, reconnect kicks, self-heal reboot
wifiWatchService();
// Schedule the weekly holiday-calendar refresh (fetch runs on core 0)
marketHolidaysService();
// Track the zones' local years for the public-holiday tables
holidaysService();
}