-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.cpp
More file actions
589 lines (556 loc) · 30.3 KB
/
Copy pathmain.cpp
File metadata and controls
589 lines (556 loc) · 30.3 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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2026 Dobrev IT Ltd
//
// This file is part of RetiMesh Node.
//
// RetiMesh Node is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the
// Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// RetiMesh Node is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
// Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with RetiMesh Node. If not, see <https://www.gnu.org/licenses/>.
// ============================================================================
// RetiMesh Node — standalone Reticulum LoRa gateway for ESP32-S3
//
// main.cpp owns the pieces the modules share — the two ring buffers and
// the FreeRTOS task layout — and wires everything together in setup().
//
// ┌────────────────────────── CORE 0 ──────────────────────────┐
// │ Wi-Fi / LwIP stack (ESP-IDF system tasks) │
// │ AsyncTCP event task socket I/O for ports 80 and 4242 │
// │ displayTask OLED status page, 2 Hz │
// └────────────────────────────────────────────────────────────┘
// ┌────────────────────────── CORE 1 ──────────────────────────┐
// │ radioTask (prio 5) SX1262 IRQs, CSMA, fragmentation │
// │ rns task (prio 3) Reticulum Transport + interfaces │
// │ loopTask (prio 1) heartbeat log │
// └────────────────────────────────────────────────────────────┘
//
// Packet flow end to end:
//
// Sideband/RNS client this node LoRa channel
// ─────────────────────────────────────────────────────────────────────
// TCP :4242 ──HDLC──► AsyncTCP task ──► tcpInRing ─┐
// ├─► Transport (rns task)
// RF ──► radioTask ──► rxRing ─────────────────────┘ │
// TCP :4242 ◄──HDLC── sendTo() ◄─────────────────────────────┤
// RF ◄── radioTask ◄── txRing ◄───────────────────────────────┘
//
// The rings carry raw RNS packets (one item = one packet, <= ~508 B).
// Transport routes them per Reticulum's rules and each interface's mode;
// packet payloads stay end-to-end encrypted between the peers.
// ============================================================================
#include <Arduino.h>
#include <LittleFS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/ringbuf.h>
#include <esp_heap_caps.h>
#include "Config.h"
#include "Settings.h"
#include "WifiManager.h"
#include "RetiTransportServer.h"
#include "LoRaRadio.h"
#include "Display.h"
#include "RnsAnnounce.h"
#include "LxmfInbox.h"
#include "RnsAdmin.h"
#include "RnsTransport.h"
#include "SdCard.h"
#include "StoreHome.h"
#include "AutoInterface.h"
#include "Power.h"
#include "Pmu.h"
#include "Gps.h"
#include "Rtc.h"
#include "Diag.h"
#include "BoardInit.h"
#include "LocalLink.h"
#include "Bootloader.h"
#include "Watchdog.h"
#include "Buzzer.h"
#include "Bq25896.h"
#include "Imu.h"
#include "Compass.h"
#include "I2cReg.h"
#include <Wire.h>
#include "Leds.h"
#include "Maintenance.h"
#include "ConsoleServer.h"
#include "PppUart.h"
#include "OtaDevice.h"
#include "OtaUpdate.h"
NodeStats g_stats;
// Ring buffer storage lives in PSRAM where the board has any (only tasks touch
// the rings; the radio ISR just posts a notification). The control blocks stay
// internal.
//
// Where there is no PSRAM the storage comes out of the internal heap, and that
// is worth saying: the fallback used to happen in silence, and a Heltec
// Wireless Stick was paying 26916 B for these three rings while every figure
// it reported said it had heap to spare (Diag.h). It also used to allocate the
// control block before it knew whether the storage existed, and leak it on the
// way past — a few hundred bytes of internal RAM, on precisely the boards that
// have none to lose.
static bool sRingsInPsram = true;
static RingbufHandle_t psramRing(size_t bytes) {
if (uint8_t* storage = (uint8_t*)heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) {
if (auto* cb = (StaticRingbuffer_t*)heap_caps_malloc(sizeof(StaticRingbuffer_t),
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))
return xRingbufferCreateStatic(bytes, RINGBUF_TYPE_NOSPLIT, storage, cb);
heap_caps_free(storage); // no control block: the storage is no use on its own
}
sRingsInPsram = false;
return xRingbufferCreate(bytes, RINGBUF_TYPE_NOSPLIT);
}
// The directions of the bridge. NOSPLIT keeps every item (= one RNS
// packet) contiguous, so consumers get a plain pointer + length.
static RingbufHandle_t txRing = nullptr; // TCP -> LoRa
static RingbufHandle_t rxRing = nullptr; // LoRa -> Transport
static RingbufHandle_t tcpInRing = nullptr; // TCP clients -> Transport
// The ways a host reaches this node (LocalLink.h). Registered before the
// services start so /api/status and the console can list every one from the
// first request, including the ones this board or build cannot offer.
static LocalLink::WifiApLink apLink;
static LocalLink::WifiStaLink staLink;
#if HAS_USB_NCM
static LocalLink::UsbNcmLink usbLink;
#else
static LocalLink::UnavailableLink usbLink(LocalLink::Type::UsbNcm, "usb0", BOARD_USB_NCM,
BOARD_USB_NCM ? "this build runs the chip's USB as a serial port, not as the composite device"
: "this board's USB is a serial bridge, not the chip's own");
#endif
#if HAS_PPP
static LocalLink::PppLink pppLink;
#else
static LocalLink::UnavailableLink pppLink(LocalLink::Type::PppUart, "ppp0", false,
"this board has no bridge UART to carry PPP");
#endif
// Whether the task that drives Reticulum was created; the transport is only
// "online" if something is running its loop.
static bool sRnsTaskUp = false;
// The version floor an update is judged against, and where it is remembered.
// Opened in setup() rather than here: NVS is not up this early.
static Ota::NvsStore otaStore;
static Ota::Floor<Ota::NvsStore> otaFloor(otaStore);
void setup() {
// Prefer PSRAM for anything larger than a few hundred bytes — packet
// buffers, JSON documents, strings, Transport containers. Done first so
// every later allocation benefits.
if (psramFound()) heap_caps_malloc_extmem_enable(PSRAM_MALLOC_THRESHOLD);
#if HAS_PPP
// The port is shared with PPP (PppUart.h): the driver's receive ring is
// PPP's receive ring, and a transmit queue lets a whole frame — or a
// whole console reply — leave in one write. Both are sized before the
// driver is installed, which is the only time they can be: the switch
// cannot give them back later, so a board that carries PPP pays for the
// rings whether or not it is switched on. What the switch does give back
// is the interface and the reader task. Neither size may be zero, and a
// smaller pair for the console alone is not on offer for the same reason:
// setTxBufferSize(0) does not mean unbuffered, it means the driver never
// installs and the port goes silent — no console and no log at all.
Serial.setRxBufferSize(PPP_RX_RING_BYTES);
Serial.setTxBufferSize(PPP_TX_QUEUE_BYTES);
#endif
Serial.begin(115200);
#if HAS_USB_NCM
// On the composite device the log rides the ACM port with the console,
// as it rode the USB-Serial/JTAG port before. The core routes it there
// for the JTAG unit on its own and for the OTG CDC only when asked.
Serial.setDebugOutput(true);
// And the port's own reboot — the 1200-baud touch, esptool's DTR/RTS
// pattern — is off from here, the first moment the sketch has: the core
// would otherwise restart into the ROM from inside its USB task, past
// the sequencer, the detach and the marks (UsbNcm.cpp, onLineCoding).
Serial.enableReboot(false);
#endif
delay(300); // let the USB CDC host attach
log_i("%s %s on %s (IDF %s)", FW_NAME, FW_VERSION, BOARD_NAME, esp_get_idf_version());
// The board itself, before any driver: the peripheral rail up on boards that
// gate one, and the chip selects idled on boards where three devices share a
// bus. Both are no-ops elsewhere. Nothing below this line — not the
// filesystem, not a bus, not the radio — is safe to run before it on a board
// that needs it, because the parts are simply not powered yet.
BoardInit::begin();
// Before anything else that could itself fail: read why the last run ended.
// The reset register survives the reboot but not a second one, so it is only
// ever readable here.
Diag::begin();
// From here every subsystem says what it cost in the RAM that decides
// (Diag.h). The bill is per board and is read, not estimated.
Diag::costStart();
Leds::begin(); // claimed and off until the services are up
// Before any task starts, so every subscription below lands on a configured
// watchdog rather than a default 5-second one that our slower passes would
// trip honestly.
Watchdog::begin();
// Filesystem first — the web app and the bulletin board live here.
if (!LittleFS.begin(true)) {
log_e("LittleFS mount failed even after format");
}
Diag::cost("littlefs");
settings.load(); // NVS: radio channel, AP, admin
Diag::cost("settings");
#if HAS_PMU
// Before anything touches SPI or I2C: on boards with a power-management
// chip the transceiver and display rails come up off, so probing the
// radio first would simply find nothing.
Pmu::begin();
#endif
Power::begin(); // profile (CPU clock, Wi-Fi sleep) + battery gauge
Diag::cost("power");
// Before the radio, the transport, or anything that can write a timestamp.
// A board with a clock of its own is already at the right time here; without
// this, time() counts from the epoch until the receiver gets a fix, and an
// LXMF message sent in between is stamped 1970 and buried at the bottom of
// its recipient's list (Rtc.h).
Rtc::begin();
txRing = psramRing(TX_RING_BYTES);
rxRing = psramRing(RX_RING_BYTES);
tcpInRing = psramRing(TCP_IN_RING_BYTES);
log_i("packet rings: tx %u, rx %u, tcp-in %u B in %s", (unsigned)TX_RING_BYTES,
(unsigned)RX_RING_BYTES, (unsigned)TCP_IN_RING_BYTES,
sRingsInPsram ? "PSRAM" : "internal RAM (this board has none)");
{
// Both figures, because only one of them can hold a task stack and it is
// not the larger one (Diag.h). The rings themselves are PSRAM's where
// there is PSRAM, which is why this line is worth reading beside the cost
// of the subsystem that follows it.
const Diag::Heap h = Diag::heap();
log_i("memory: %lu B internal free (%lu of it 8-bit, %lu largest block), %lu B PSRAM free "
"(threshold %d B)",
(unsigned long)h.freeInternal, (unsigned long)h.freeDram, (unsigned long)h.largestDramBlock,
(unsigned long)h.freePsram, PSRAM_MALLOC_THRESHOLD);
}
configASSERT(txRing && rxRing && tcpInRing);
Diag::cost("packet rings");
nodeIdentity.begin(); // Reticulum identity keys (NVS)
Diag::cost("identity");
// Bring up services. Radio failure is survivable: the AP + web UI stay
// up and report "radio offline" so the node can be diagnosed in place.
g_stats.displayPresent = display.begin(); // probes I2C; clears the panel if found
Diag::cost("display");
#if HAS_BQ25896 || HAS_IMU || HAS_COMPASS
// After the display, deliberately: the case's I2C parts sit behind the
// switched peripheral rail, and the panel is what brings that rail up and
// settles it (Panel.h). Powering it a second time from here left the panel
// dark — one owner for the rail, and the probe simply comes later.
I2cReg::mainBus(); // up already if a panel or keyboard got here first
Bq25896::begin();
Imu::begin();
Compass::begin(); // after the accelerometer: it asks for gravity
Diag::cost("i2c case parts");
#endif
#if HAS_SD
// Before the card task exists: it is the task that reads the card's
// ownership marker for everyone else, and it needs somewhere to put it.
StoreHome::begin();
sdCard.begin(); // mounts; nothing polls the slot yet
// A move of the store asked for before the last restart happens here, and
// here specifically: after the card is mounted and before anything else in
// the node is alive. It is seconds of solid filesystem work, and it used to
// run further down, by which time the web server was answering requests and
// the card task was polling the same card. Both of those reach into state
// this is in the middle of changing, and the node died in the attempt often
// enough that a migration reliably cost two boots instead of one — visible
// only when nobody had a serial monitor attached, because watching it
// changed the timing enough to hide it.
// The node's name before the move, not after: the migration writes it onto
// the card it is claiming, and this used to happen inside the Wi-Fi
// start-up further down, so a card adopted at boot got an owner with no
// name on it.
wifiManager.resolveNames();
StoreHome::runPendingMigration();
#endif
// Where the store lives is settled here, for every board and whether or not
// the transport is switched on. It used to be decided inside the transport's
// own start-up, behind its enabled check, so a node with the transport off
// never decided at all: the store's filesystem stayed pointed at flash while
// the data sat on the card, every page and API answer said so, and an operator
// acting on that could have the card's real store overwritten by the copy in
// flash at the next boot.
StoreHome::chooseAtBoot();
#if HAS_SD
// Only now: everything above drives the card directly, and the poll's
// removal check answers a read it lost to that traffic by unmounting the
// card out from under whoever is using it.
sdCard.startPolling(); // optional; hot-plug polled on core 0
Diag::cost("sd card");
#endif
#if HAS_GPS
Gps::begin(); // NMEA reader task; powers the receiver rail
Diag::cost("gps");
#endif
LocalLink::add(&apLink);
LocalLink::add(&staLink);
LocalLink::add(&usbLink);
LocalLink::add(&pppLink);
// The one to watch, and it bills itself in two parts: the radio the switch
// is meant to buy, and the web server every node pays for either way.
wifiManager.begin(); // radio + web server, or web server alone with Wi-Fi off
// Before the links, not after: PPP hands the console its own reader when
// it takes the port (PppUart.h), and a begin() that ran afterwards would
// point the console back at the UART the reader is already draining —
// output fine, nothing ever read.
// The maintenance console on the port the host already has. Its HELLO is
// the line a flashing tool looks for, so it goes out before the services
// that make the log busy. Where PPP shares the port, the console reads
// and writes through the PPP driver's view of it, which hands the console
// its bytes while the console owns the port and PPP its frames otherwise.
// The UART directly: with PPP off there is no reader between the two, and
// when PPP takes the port over it points the console at its own stream
// (PppUart.h).
Maintenance::begin(Serial);
LocalLink::begin();
Diag::cost("local links");
g_stats.radioOnline = loraRadio.begin(txRing, rxRing, settings.radio());
Diag::cost("lora radio");
// Transport first, then the things that hand it peers. It builds the queue
// those peers are announced on, and it takes long enough — mounting and
// walking the store — that a client which was connected before the restart
// reconnects inside the gap. Accepting first meant that client's arrival was
// posted to a queue that did not exist yet.
g_stats.transportOnline = RnsTransport::begin(txRing, rxRing, tcpInRing);
Diag::cost("reticulum");
transportServer.begin(tcpInRing);
Diag::cost("rns tcp server");
#if HAS_AUTOINTERFACE
// Zero-config peering on the Wi-Fi links (RNS AutoInterface). Always
// begun, even with Wi-Fi off: the heartbeat and /api/status ask it for a
// peer count, and the lock they take exists only once begin() has run. It
// decides for itself whether there is a netif worth joining.
AutoInterface::begin(tcpInRing);
Diag::cost("autointerface");
#endif
// ---- Task layout (see the diagram above) -------------------------------
Diag::startTask(LoRaRadio::radioTask, "radio", RADIO_TASK_STACK, &loraRadio, 5, 1);
// Before the panel, and deliberately. This task used to ask last, after the
// web server and — on a GUI board — a 16 KB display stack had already taken
// their pick of a heap that fragments as it fills. A V4 lost the draw and
// ran nine hours with a radio, a portal and a Wi-Fi link, routing nothing,
// because the one task that makes it a Reticulum node could not be created.
// A screen can wait for memory; the reason the node exists cannot.
//
// The RNS task owns every call into microReticulum (Transport is
// single-threaded): interface loops, forwarding, announces, persistence.
sRnsTaskUp = Diag::startTask([](void*) {
// A backstop, and only that. RnsTransport::loop() now guards both halves
// of its own pass and each of those catches everything, so nothing the
// library throws reaches here any more — what is left is a throw from the
// reporting itself, or from a future edit to loop() outside either guard.
// Kept because the cost is nothing and the one task that must keep running
// must not be ended by an allocation (Diag.h); described honestly because
// a reader working out which layer contains what should not be sent to
// this one.
Watchdog::watch();
for (;;) {
Watchdog::feed();
Diag::guard("the rns task", [] { RnsTransport::loop(); });
vTaskDelay(pdMS_TO_TICKS(10));
}
}, "rns", 12288, nullptr, 3, 1);
// 12 KB, not 16. Measured at 12 KB on a board in service, the deepest this
// task goes is 6.64 KB — a little more than the 16 KB builds suggested, so
// the margin is 1.85x rather than the 2x it was cut for. Kept, because the
// failure being fixed is a contiguous block that could not be found and a
// quarter off the ask is worth having, and because an overflow here is not
// silent: the canary and the end-of-stack watchpoint are both on, so it
// panics, reboots and says so, which is the same recovery the rest of this
// block provides.
#if HAS_DISPLAY
// 6 KB: the panel driver and the I2C stack are deep enough that 4 KB left
// only ~700 bytes on a T-Beam, where the battery reading adds a PMU
// transaction to every network page.
// The LVGL shell renders whole widget trees on this stack; the page
// stack's 6 KB starved it in the first bench build.
// 16 KB with the GUI: a settings form builds forty widgets inside one
// click callback inside lv_timer_handler, and 12 KB was the first
// suspect when that tap took the node down.
Diag::startTask(Display::displayTask, "display", HAS_LVGL_UI ? 16384 : 6144, &display, 1, 0);
#endif
{
// Online means Reticulum is both initialised and being driven: begin()
// succeeding says only that the tables were built, and a node with no task
// running its loop routes nothing and announces nothing while every status
// it serves says "online".
//
// A node that gets here is a radio, a portal and a Wi-Fi link with nothing
// behind them: it looks entirely healthy from outside and routes nothing.
// One ran that way for nine hours. The condition was already detected and
// logged here and then lived with, which is the wrong answer for a node on
// a mast — so it restarts instead, and a fresh heap almost always has the
// contiguous block the task could not get.
//
// Bounded, because a board that can never fit the task would otherwise
// spend its life rebooting, and a node that is up and half-working can at
// least be reached and told something. The count is persisted because the
// evidence of the last attempt does not survive the restart that follows
// it, and cleared on the first boot that works.
Preferences boot;
const bool remembered = boot.begin("bootfault", false);
const uint32_t tries = remembered ? boot.getUInt("rns", 0) : 0;
if (!sRnsTaskUp) {
g_stats.transportOnline = false;
// The count is what makes the loop bounded, so the restart is offered
// only once the count is known to have been written. A namespace that
// would not open and a write that failed — NVS full, which is not a
// remote possibility on the boot where memory ran out — would otherwise
// leave `tries` at zero for ever and turn the bound into no bound at
// all: a node rebooting every twenty seconds, which is worse than the
// degraded one this falls back to.
const bool again = remembered && tries < REQUIRED_TASK_RETRIES &&
boot.putUInt("rns", tries + 1) == sizeof(uint32_t);
if (again) {
log_e("Reticulum is not being driven: the rns task did not start "
"(attempt %lu of %u) — restarting, which is usually enough",
(unsigned long)(tries + 1), (unsigned)REQUIRED_TASK_RETRIES);
// Scheduled, not immediate: the rest of setup() still has to run. In
// particular an image installed over the air marks itself healthy
// further down, and a node that rebooted before reaching that would be
// rolled back by the bootloader for a fault that had nothing to do
// with the image.
Bootloader::reboot(Bootloader::Source::Fault);
} else {
log_e("Reticulum is not being driven: the rns task did not start and "
"will not be restarted again — staying up so the node can still "
"be reached and reconfigured, but it is routing nothing");
}
} else if (tries) {
boot.putUInt("rns", 0);
log_i("the rns task started after %lu restart%s", (unsigned long)tries,
tries == 1 ? "" : "s");
}
boot.end();
}
// Last, so the figure beside it is what the node has left to run on.
Diag::cost("tasks");
// Everything is up, which is the bar an updated image has to clear: the
// bootloader started it on approval and puts the previous one back on the
// next boot unless it is told, here, that this one works. Saying so also
// settles the version floor — the same statement, recorded for the next
// update to be judged against. On a first boot after a cable flash there is
// nothing pending and nothing staged, and this does nothing at all.
otaStore.begin();
Ota::begin(otaFloor);
{
const Ota::Settlement s = Ota::confirmBoot(otaFloor);
if (s.advanced) log_i("update confirmed: the version floor is now %lu", (unsigned long)s.floor);
if (!Ota::canSelfUpdate())
log_w("this board has a single app partition: it cannot install its own updates, "
"and an update needs a cable");
}
// The audible version of the lines below, for a device carried rather than
// benched: two notes up means the node is running before the panel says so.
Buzzer::begin();
Buzzer::boot();
// Billed like every other subsystem, because on one board this is not free:
// a speaker behind an I2S amplifier needs a task and a DMA ring where a piezo
// needs a PWM channel, and it is measured at about 5.4 KB of internal RAM on
// the T-Deck — a quarter of what that board has left. A cost that size should
// appear in the same list as everything else rather than be discovered later
// by whoever is short of a stack (Diag.h).
Diag::cost("sounder");
// setup() runs on loopTask, so this subscribes the task that loop() below
// feeds. Last, so nothing during start-up — a card mount, a store migration,
// a first announce — is measured against a timeout meant for steady running.
Watchdog::watch();
if (settings.links().wifiEnabled())
log_i("RetiMesh Node up — join \"%s\", portal http://%s, RNS TCP :%d",
wifiManager.ssid(), AP_IP.toString().c_str(), RNS_TCP_PORT);
else
log_i("RetiMesh Node up — Wi-Fi is off; HTTP :%d and RNS TCP :%d answer on any other local link, "
"and WIFI ON at the console turns the access point back on", HTTP_PORT, RNS_TCP_PORT);
}
// Arduino's loopTask (core 1, prio 1): scheduled restarts, the maintenance
// console, link bookkeeping and a heartbeat.
void loop() {
static uint32_t lastBeat = 0;
Watchdog::feed();
wifiManager.tick();
Bootloader::tick(); // may not return: this is where restarts happen
// Before the console reads: this is what hands it a network session when
// one arrives, and takes it back when the caller goes (ConsoleServer.h).
ConsoleServer::poll();
Maintenance::poll();
LocalLink::poll(millis());
// Messages that arrived on the Reticulum task, written here. Flash is slow
// enough that doing it where they arrive delayed the delivery proof and
// stalled the job loop that link and receipt timeouts run on (LxmfInbox.h).
Rns::Inbox::poll();
// A command that arrived as a message, run here rather than in the packet
// callback it landed in: it means the console's parser, the settings, and
// sometimes a restart (RnsAdmin.h).
Rns::Admin::poll();
Leds::tick(millis());
// The magnetometer, sampled whether or not anyone is asking. Its hard-iron
// offsets are found from the extremes each axis reaches, and those are only
// reached while the board is being turned — which is precisely when nobody is
// reading a console (Compass.h).
Compass::poll();
// Every pass, not on the heartbeat: a crash 29 s after the last beat would
// otherwise be recorded as having happened 29 s earlier, and a node stuck in
// a restart loop would report every run as zero seconds — indistinguishable
// from one that never got past setup(). One word into RTC RAM, no flash.
Diag::tick(millis() / 1000);
g_stats.heapMinFree = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
g_stats.psramFree = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
if (millis() - lastBeat >= 30000) {
lastBeat = millis();
// The battery sampler rides whoever calls battery(); with the glass
// asleep nobody did, and the discharge history's clock silently stopped
// on exactly the battery-first nodes it exists for. The heartbeat asks.
Power::battery();
const uint32_t uptimeS = millis() / 1000;
log_i("up %lus | peers tcp %u auto %u | lora rx/tx %u/%u (drop %u) | announces rx/tx %u/%u",
(unsigned long)uptimeS, (unsigned)g_stats.tcpClients, (unsigned)AutoInterface::peerCount(),
(unsigned)g_stats.loraRxPackets, (unsigned)g_stats.loraTxPackets,
(unsigned)(g_stats.loraRxDropRing + g_stats.loraRxDropReasm +
g_stats.loraRxDropPartial), (unsigned)g_stats.announcesRx,
(unsigned)g_stats.announcesTx);
// Where the losses went. Silent unless there are some: on a quiet channel
// this line never appears, and when it does the five figures say which of
// the five causes is responsible rather than leaving one number to guess at.
// Spurious interrupts are deliberately not in this sum: nothing was lost
// when one arrives, TxDone shares the line so a steady trickle is normal,
// and including them made the loss report fire on an idle node.
const uint32_t lost = g_stats.loraRxDropRing + g_stats.loraRxDropReasm +
g_stats.loraRxDropPartial + g_stats.loraRxCrcErrors +
g_stats.loraRxBadLength;
if (lost)
log_i("lora rx losses: ring %u, reassembly %u, partial %u, crc %u, length %u, "
"spurious irq %u (kept %u)",
(unsigned)g_stats.loraRxDropRing, (unsigned)g_stats.loraRxDropReasm,
(unsigned)g_stats.loraRxDropPartial, (unsigned)g_stats.loraRxCrcErrors,
(unsigned)g_stats.loraRxBadLength, (unsigned)g_stats.loraRxSpuriousIrq,
(unsigned)g_stats.loraRxPackets);
// Reticulum's tables are the other thing that grows with traffic, and the
// one a heap figure alone will not explain.
RnsTransport::Tables t = RnsTransport::tables();
log_i("tables: paths %lu links %lu (%lu active, %lu pending) dests %lu announces %lu (%lu held) rates %lu snap %lums",
(unsigned long)t.paths, (unsigned long)t.links, (unsigned long)t.activeLinks,
(unsigned long)t.pendingLinks, (unsigned long)t.destinations,
(unsigned long)t.announces, (unsigned long)t.heldAnnounces, (unsigned long)t.rates,
(unsigned long)t.snapWalkMaxMs);
#if HAS_GPS
// The satellite count is the number that tells you whether the antenna
// has a view of the sky; the sentence count tells you the receiver is
// wired up at all.
Gps::Fix g = Gps::fix();
if (g.enabled)
log_i("gnss: %s, %u sats, %lu sentences%s%s", g.valid ? "fix" : "searching",
g.satellites, (unsigned long)g.sentences,
g.timeValid ? ", utc " : "", g.timeValid ? g.utc : "");
#endif
// Heap, per-task stack headroom and the low-water warnings. The task list
// lives in Diag.h so this and /api/status can never watch different sets —
// the GNSS task, which carries the smallest stack of the lot, was missing
// from the copy that used to live here.
Diag::report();
}
vTaskDelay(pdMS_TO_TICKS(200));
}