Skip to content

Commit 9f67054

Browse files
committed
harden runtime and add live network validation
1 parent 4c88dfb commit 9f67054

21 files changed

+1388
-29
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@
1111
/bin/
1212
/TODO.md
1313
/.codex/
14+
/KernelWeb/
15+
.claude
16+
CLAUDE.md
17+
CLAUDE_CHANGELOG.md
18+
CODEX_CHANGELOG.md
19+
/examples/LiveNetworkBaseline/LocalSecrets.h
20+
/examples/LiveNetworkNode/LocalSecrets.h
21+
/scripts/.live-network/
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
#include <ZeroKernel.h>
2+
3+
#if defined(ARDUINO_ARCH_ESP8266)
4+
#include <ESP8266WiFi.h>
5+
#elif defined(ARDUINO_ARCH_ESP32)
6+
#include <WiFi.h>
7+
#else
8+
#error "LiveNetworkBaseline requires ESP8266 or ESP32."
9+
#endif
10+
11+
#include <PubSubClient.h>
12+
#include "LocalSecrets.h"
13+
14+
namespace {
15+
16+
WiFiClient g_httpClient;
17+
WiFiClient g_mqttTransport;
18+
PubSubClient g_mqttClient(g_mqttTransport);
19+
20+
const unsigned long kSamplePeriodUs = 100000UL;
21+
const unsigned long kDispatchPeriodMs = 500UL;
22+
const unsigned long kSummaryPeriodMs = 10000UL;
23+
const unsigned long kMissThresholdUs = 1500UL;
24+
const unsigned long kHttpTimeoutMs = 900UL;
25+
const unsigned long kWiFiRetryBaseMs = 1000UL;
26+
const unsigned long kWiFiRetryMaxMs = 16000UL;
27+
const unsigned long kMqttRetryBaseMs = 1000UL;
28+
const unsigned long kMqttRetryMaxMs = 12000UL;
29+
30+
unsigned long g_startedAtMs = 0;
31+
unsigned long g_lastSummaryAtMs = 0;
32+
unsigned long g_lastDispatchAtMs = 0;
33+
unsigned long g_nextExpectedUs = 0;
34+
unsigned long g_sampleRuns = 0;
35+
unsigned long g_lagAccumUs = 0;
36+
unsigned long g_maxLagUs = 0;
37+
unsigned long g_fastMisses = 0;
38+
unsigned long g_sensorValue = 900;
39+
unsigned long g_wifiAttempts = 0;
40+
unsigned long g_wifiTransitions = 0;
41+
unsigned long g_httpOk = 0;
42+
unsigned long g_httpFail = 0;
43+
unsigned long g_mqttOk = 0;
44+
unsigned long g_mqttFail = 0;
45+
unsigned long g_queueMax = 0;
46+
unsigned long g_nextWiFiAttemptAtMs = 0;
47+
unsigned long g_currentWiFiRetryMs = kWiFiRetryBaseMs;
48+
unsigned long g_nextMqttAttemptAtMs = 0;
49+
unsigned long g_currentMqttRetryMs = kMqttRetryBaseMs;
50+
bool g_lastWiFiConnected = false;
51+
52+
unsigned long applyBackoffWithJitter(unsigned long nowMs,
53+
unsigned long baseDelayMs,
54+
unsigned long* currentDelayMs,
55+
unsigned long maxDelayMs,
56+
unsigned long salt) {
57+
const unsigned long delayMs = (*currentDelayMs == 0) ? baseDelayMs : *currentDelayMs;
58+
const unsigned long jitterWindow = delayMs / 4UL;
59+
const unsigned long jitter = jitterWindow == 0 ? 0 : (salt % (jitterWindow + 1UL));
60+
unsigned long nextDelay = delayMs;
61+
if (maxDelayMs > 0 && nextDelay < maxDelayMs) {
62+
const unsigned long doubled = nextDelay * 2UL;
63+
nextDelay = doubled > maxDelayMs ? maxDelayMs : doubled;
64+
}
65+
*currentDelayMs = nextDelay;
66+
return nowMs + delayMs + jitter;
67+
}
68+
69+
void ensureWiFi() {
70+
const unsigned long nowMs = millis();
71+
const bool connected = (WiFi.status() == WL_CONNECTED);
72+
73+
if (connected != g_lastWiFiConnected) {
74+
g_lastWiFiConnected = connected;
75+
if (connected) {
76+
++g_wifiTransitions;
77+
g_currentWiFiRetryMs = kWiFiRetryBaseMs;
78+
g_nextWiFiAttemptAtMs = 0;
79+
} else {
80+
g_mqttClient.disconnect();
81+
}
82+
}
83+
84+
if (connected) {
85+
return;
86+
}
87+
88+
if (g_nextWiFiAttemptAtMs != 0 && nowMs < g_nextWiFiAttemptAtMs) {
89+
return;
90+
}
91+
92+
WiFi.begin(kWiFiSsid, kWiFiPassword);
93+
++g_wifiAttempts;
94+
g_nextWiFiAttemptAtMs =
95+
applyBackoffWithJitter(nowMs, kWiFiRetryBaseMs, &g_currentWiFiRetryMs,
96+
kWiFiRetryMaxMs, g_wifiAttempts);
97+
}
98+
99+
bool ensureMqtt() {
100+
if (WiFi.status() != WL_CONNECTED) {
101+
g_mqttClient.disconnect();
102+
return false;
103+
}
104+
105+
if (!g_mqttClient.connected()) {
106+
const unsigned long nowMs = millis();
107+
if (g_nextMqttAttemptAtMs != 0 && nowMs < g_nextMqttAttemptAtMs) {
108+
return false;
109+
}
110+
111+
char clientId[48];
112+
snprintf(clientId, sizeof(clientId), "zk-baseline-%lu", nowMs);
113+
if (!g_mqttClient.connect(clientId)) {
114+
g_nextMqttAttemptAtMs =
115+
applyBackoffWithJitter(nowMs, kMqttRetryBaseMs, &g_currentMqttRetryMs,
116+
kMqttRetryMaxMs, g_httpFail + g_mqttFail + 1UL);
117+
return false;
118+
}
119+
120+
g_currentMqttRetryMs = kMqttRetryBaseMs;
121+
g_nextMqttAttemptAtMs = 0;
122+
}
123+
124+
g_mqttClient.loop();
125+
return g_mqttClient.connected();
126+
}
127+
128+
bool sendHttpNow(const char* body, size_t length) {
129+
if (WiFi.status() != WL_CONNECTED) {
130+
return false;
131+
}
132+
133+
g_httpClient.stop();
134+
g_httpClient.setTimeout(kHttpTimeoutMs);
135+
if (!g_httpClient.connect(kHttpHost, kHttpPort)) {
136+
g_httpClient.stop();
137+
return false;
138+
}
139+
140+
g_httpClient.print("POST ");
141+
g_httpClient.print(kHttpPath);
142+
g_httpClient.print(" HTTP/1.1\r\nHost: ");
143+
g_httpClient.print(kHttpHost);
144+
g_httpClient.print("\r\nConnection: close\r\nContent-Type: application/json\r\nContent-Length: ");
145+
g_httpClient.print(length);
146+
g_httpClient.print("\r\n\r\n");
147+
g_httpClient.write(reinterpret_cast<const uint8_t*>(body), length);
148+
149+
const unsigned long deadline = millis() + kHttpTimeoutMs;
150+
String statusLine;
151+
while (millis() < deadline) {
152+
while (g_httpClient.available()) {
153+
const char ch = static_cast<char>(g_httpClient.read());
154+
if (ch == '\r') {
155+
continue;
156+
}
157+
if (ch == '\n') {
158+
if (statusLine.length() > 0) {
159+
const bool ok =
160+
statusLine.startsWith("HTTP/1.1 2") || statusLine.startsWith("HTTP/1.0 2");
161+
g_httpClient.stop();
162+
return ok;
163+
}
164+
continue;
165+
}
166+
if (statusLine.length() < 64) {
167+
statusLine += ch;
168+
}
169+
}
170+
delay(1);
171+
}
172+
173+
g_httpClient.stop();
174+
return false;
175+
}
176+
177+
void sampleTask() {
178+
const unsigned long nowUs = micros();
179+
if (g_nextExpectedUs == 0) {
180+
g_nextExpectedUs = nowUs;
181+
}
182+
183+
if (nowUs < g_nextExpectedUs) {
184+
return;
185+
}
186+
187+
const unsigned long lagUs = nowUs - g_nextExpectedUs;
188+
g_lagAccumUs += lagUs;
189+
if (lagUs > g_maxLagUs) {
190+
g_maxLagUs = lagUs;
191+
}
192+
if (lagUs > kMissThresholdUs) {
193+
++g_fastMisses;
194+
}
195+
196+
++g_sampleRuns;
197+
++g_sensorValue;
198+
do {
199+
g_nextExpectedUs += kSamplePeriodUs;
200+
} while (g_nextExpectedUs <= nowUs);
201+
202+
if (nowUs > g_nextExpectedUs + (kSamplePeriodUs * 2UL)) {
203+
g_nextExpectedUs = nowUs + kSamplePeriodUs;
204+
}
205+
}
206+
207+
void dispatchTask() {
208+
const unsigned long nowMs = millis();
209+
if ((nowMs - g_lastDispatchAtMs) < kDispatchPeriodMs) {
210+
return;
211+
}
212+
g_lastDispatchAtMs = nowMs;
213+
214+
char payload[96];
215+
const int written = snprintf(payload, sizeof(payload),
216+
"{\"seq\":%lu,\"sensor\":%lu,\"board\":\"baseline\"}",
217+
g_sampleRuns, g_sensorValue);
218+
if (written <= 0) {
219+
return;
220+
}
221+
222+
const bool httpOk = sendHttpNow(payload, static_cast<size_t>(written));
223+
if (httpOk) {
224+
++g_httpOk;
225+
} else {
226+
++g_httpFail;
227+
}
228+
229+
const bool mqttConnected = ensureMqtt();
230+
if (mqttConnected && g_mqttClient.publish(kMqttTopic, payload)) {
231+
++g_mqttOk;
232+
} else {
233+
++g_mqttFail;
234+
}
235+
}
236+
237+
unsigned long percentage(unsigned long ok, unsigned long fail) {
238+
const unsigned long total = ok + fail;
239+
if (total == 0) {
240+
return 100;
241+
}
242+
return (ok * 100UL) / total;
243+
}
244+
245+
void reportTask() {
246+
const unsigned long nowMs = millis();
247+
if ((nowMs - g_lastSummaryAtMs) < kSummaryPeriodMs) {
248+
return;
249+
}
250+
g_lastSummaryAtMs = nowMs;
251+
252+
const unsigned long avgLagUs = g_sampleRuns == 0 ? 0 : g_lagAccumUs / g_sampleRuns;
253+
char line[320];
254+
snprintf(line, sizeof(line),
255+
"BASELINE_LIVE_NET window_ms=%lu sample_runs=%lu fast_avg_lag_us=%lu "
256+
"fast_max_lag_us=%lu fast_miss=%lu wifi_attempts=%lu wifi_reconnects=%lu "
257+
"http_ok=%lu http_fail=%lu http_rate=%lu mqtt_ok=%lu mqtt_fail=%lu "
258+
"mqtt_rate=%lu queue_max=%lu",
259+
nowMs - g_startedAtMs,
260+
g_sampleRuns,
261+
avgLagUs,
262+
g_maxLagUs,
263+
g_fastMisses,
264+
g_wifiAttempts,
265+
g_wifiTransitions,
266+
g_httpOk,
267+
g_httpFail,
268+
percentage(g_httpOk, g_httpFail),
269+
g_mqttOk,
270+
g_mqttFail,
271+
percentage(g_mqttOk, g_mqttFail),
272+
g_queueMax);
273+
Serial.println(line);
274+
}
275+
276+
} // namespace
277+
278+
void setup() {
279+
Serial.begin(115200);
280+
delay(100);
281+
282+
WiFi.mode(WIFI_STA);
283+
#if defined(ARDUINO_ARCH_ESP32)
284+
WiFi.setAutoReconnect(false);
285+
WiFi.persistent(false);
286+
#elif defined(ARDUINO_ARCH_ESP8266)
287+
WiFi.setAutoReconnect(false);
288+
WiFi.persistent(false);
289+
#endif
290+
291+
g_mqttClient.setServer(kMqttHost, kMqttPort);
292+
g_startedAtMs = millis();
293+
}
294+
295+
void loop() {
296+
ensureWiFi();
297+
ensureMqtt();
298+
sampleTask();
299+
dispatchTask();
300+
reportTask();
301+
delay(1);
302+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#ifndef ZEROKERNEL_LIVE_NETWORK_BASELINE_LOCAL_SECRETS_H
2+
#define ZEROKERNEL_LIVE_NETWORK_BASELINE_LOCAL_SECRETS_H
3+
4+
static const char* kWiFiSsid = "replace-with-ssid";
5+
static const char* kWiFiPassword = "replace-with-password";
6+
static const char* kHttpHost = "192.168.1.10";
7+
static const uint16_t kHttpPort = 3000;
8+
static const char* kHttpPath = "/api/data";
9+
static const char* kMqttHost = "192.168.1.10";
10+
static const uint16_t kMqttPort = 1883;
11+
static const char* kMqttTopic = "zerokernel/live/telemetry";
12+
13+
#endif

0 commit comments

Comments
 (0)