Initial commit: RNodeTHV4 boundary mode firmware for Heltec V4
Bridges LoRa mesh and TCP/WiFi backbone networks using microReticulum. Based on microReticulum_Firmware with boundary mode additions: - BoundaryMode.h: State management and EEPROM persistence - BoundaryConfig.h: WiFi captive portal for configuration - TcpInterface.h: TCP backbone interface with HDLC framing - Display.h: Custom OLED layout with network status indicators - Transport/Identity library patches for embedded memory constraints
This commit is contained in:
501
BoundaryConfig.h
Normal file
501
BoundaryConfig.h
Normal file
@@ -0,0 +1,501 @@
|
||||
// Copyright (C) 2026, Boundary Mode Extension
|
||||
// Based on microReticulum_Firmware by Mark Qvist
|
||||
//
|
||||
// BoundaryConfig.h — Captive-portal web configuration for Boundary Mode.
|
||||
// When triggered (first boot with no config, or button hold >5s),
|
||||
// the device starts a WiFi AP with a web form for all settings:
|
||||
// WiFi STA credentials, TCP backbone params, LoRa radio params,
|
||||
// and optional AP-mode TCP server.
|
||||
//
|
||||
// This program 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.
|
||||
|
||||
#ifndef BOUNDARY_CONFIG_H
|
||||
#define BOUNDARY_CONFIG_H
|
||||
|
||||
#ifdef BOUNDARY_MODE
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <WebServer.h>
|
||||
#include <DNSServer.h>
|
||||
|
||||
// ─── Config Portal State ─────────────────────────────────────────────────────
|
||||
static bool config_portal_active = false;
|
||||
static WebServer* config_server = nullptr;
|
||||
static DNSServer* config_dns = nullptr;
|
||||
|
||||
static const char CONFIG_AP_SSID[] = "RNode-Boundary-Setup";
|
||||
static const uint16_t DNS_PORT = 53;
|
||||
static const uint16_t HTTP_PORT = 80;
|
||||
|
||||
// Forward declarations
|
||||
void config_portal_start();
|
||||
void config_portal_stop();
|
||||
void config_portal_loop();
|
||||
bool config_portal_is_active();
|
||||
bool boundary_needs_config();
|
||||
|
||||
// ─── Common bandwidth values (Hz) ───────────────────────────────────────────
|
||||
// These match Reticulum standard channel plans
|
||||
struct BwOption { uint32_t hz; const char* label; };
|
||||
static const BwOption BW_OPTIONS[] = {
|
||||
{ 7800, "7.8 kHz" },
|
||||
{ 10400, "10.4 kHz" },
|
||||
{ 15600, "15.6 kHz" },
|
||||
{ 20800, "20.8 kHz" },
|
||||
{ 31250, "31.25 kHz" },
|
||||
{ 41700, "41.7 kHz" },
|
||||
{ 62500, "62.5 kHz" },
|
||||
{125000, "125 kHz" },
|
||||
{250000, "250 kHz" },
|
||||
{500000, "500 kHz" },
|
||||
};
|
||||
static const int BW_OPTIONS_COUNT = sizeof(BW_OPTIONS) / sizeof(BW_OPTIONS[0]);
|
||||
|
||||
// ─── HTML Page Generation ────────────────────────────────────────────────────
|
||||
|
||||
static void config_send_html() {
|
||||
// Read current values from EEPROM/globals for pre-population
|
||||
char cur_ssid[33] = "";
|
||||
char cur_psk[33] = "";
|
||||
|
||||
for (int i = 0; i < 32; i++) {
|
||||
cur_ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i));
|
||||
if (cur_ssid[i] == (char)0xFF) cur_ssid[i] = '\0';
|
||||
}
|
||||
cur_ssid[32] = '\0';
|
||||
|
||||
for (int i = 0; i < 32; i++) {
|
||||
cur_psk[i] = EEPROM.read(config_addr(ADDR_CONF_PSK + i));
|
||||
if (cur_psk[i] == (char)0xFF) cur_psk[i] = '\0';
|
||||
}
|
||||
cur_psk[32] = '\0';
|
||||
|
||||
// Current LoRa values (from globals, which were loaded from EEPROM)
|
||||
uint32_t cur_freq = lora_freq;
|
||||
uint32_t cur_bw = lora_bw;
|
||||
int cur_sf = lora_sf;
|
||||
int cur_cr = lora_cr;
|
||||
int cur_txp = lora_txp;
|
||||
if (cur_txp == 0xFF) cur_txp = 28; // Default TX power
|
||||
|
||||
// Default frequency if not set
|
||||
if (cur_freq == 0) cur_freq = 914875000; // 914.875 MHz default
|
||||
if (cur_bw == 0) cur_bw = 125000; // 125 kHz default
|
||||
if (cur_sf == 0) cur_sf = 10; // SF10 default
|
||||
if (cur_cr < 5 || cur_cr > 8) cur_cr = 5; // CR 4/5 default
|
||||
|
||||
// Build the HTML page
|
||||
String html = F(
|
||||
"<!DOCTYPE html><html><head>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>RNode Boundary Setup</title>"
|
||||
"<style>"
|
||||
"body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;margin:0;padding:16px;}"
|
||||
"h1{color:#e94560;font-size:1.4em;margin:0 0 8px;}"
|
||||
"h2{color:#0f3460;background:#e0e0e0;padding:6px 10px;margin:18px -10px 10px;font-size:1em;border-radius:4px;}"
|
||||
"form{max-width:480px;margin:0 auto;}"
|
||||
"label{display:block;margin:8px 0 2px;font-size:0.9em;color:#aaa;}"
|
||||
"input,select{width:100%;padding:8px;margin:2px 0 6px;box-sizing:border-box;"
|
||||
"background:#16213e;border:1px solid #0f3460;color:#e0e0e0;border-radius:4px;font-size:0.95em;}"
|
||||
"input:focus,select:focus{border-color:#e94560;outline:none;}"
|
||||
".row{display:flex;gap:10px;}.row>div{flex:1;}"
|
||||
".note{font-size:0.8em;color:#666;margin:2px 0 8px;}"
|
||||
"button{width:100%;padding:12px;margin:20px 0;background:#e94560;color:#fff;"
|
||||
"border:none;border-radius:4px;font-size:1.1em;cursor:pointer;}"
|
||||
"button:hover{background:#c73e54;}"
|
||||
".ok{background:#16213e;padding:20px;border-radius:8px;text-align:center;}"
|
||||
".ok h1{color:#0f0;}"
|
||||
"</style></head><body>"
|
||||
"<h1>📡 RNode Boundary Node</h1>"
|
||||
"<form method='POST' action='/save'>"
|
||||
);
|
||||
|
||||
// ── WiFi STA Section ──
|
||||
html += F(
|
||||
"<h2>📶 WiFi Network</h2>"
|
||||
"<label>WiFi</label>"
|
||||
"<select name='wifi_en'>"
|
||||
);
|
||||
html += F("<option value='1'");
|
||||
if (boundary_state.wifi_enabled) html += F(" selected");
|
||||
html += F(">Enabled</option>");
|
||||
html += F("<option value='0'");
|
||||
if (!boundary_state.wifi_enabled) html += F(" selected");
|
||||
html += F(">Disabled (LoRa-only repeater)</option>");
|
||||
html += F("</select>");
|
||||
|
||||
html += F(
|
||||
"<label>SSID</label>"
|
||||
"<input name='ssid' maxlength='32' placeholder='Your WiFi network' value='"
|
||||
);
|
||||
html += String(cur_ssid);
|
||||
html += F(
|
||||
"'>"
|
||||
"<label>Password</label>"
|
||||
"<input name='psk' type='password' maxlength='32' placeholder='WiFi password' value='"
|
||||
);
|
||||
html += String(cur_psk);
|
||||
html += F("'>");
|
||||
|
||||
// ── TCP Backbone Section ──
|
||||
html += F(
|
||||
"<h2>🌐 TCP Backbone</h2>"
|
||||
"<label>Mode</label>"
|
||||
"<select name='tcp_mode'>"
|
||||
);
|
||||
html += F("<option value='0'");
|
||||
if (boundary_state.tcp_mode == 0) html += F(" selected");
|
||||
html += F(">Disabled</option>");
|
||||
html += F("<option value='1'");
|
||||
if (boundary_state.tcp_mode == 1) html += F(" selected");
|
||||
html += F(">Client (connect to backbone)</option>");
|
||||
html += F("</select>");
|
||||
|
||||
html += F("<label>Backbone Host</label>");
|
||||
html += F("<input name='bb_host' maxlength='63' placeholder='e.g. 192.168.1.100' value='");
|
||||
html += String(boundary_state.backbone_host);
|
||||
html += F("'>");
|
||||
|
||||
html += F("<label>Backbone Port</label>");
|
||||
html += F("<input name='bb_port' type='number' min='1' max='65535' value='");
|
||||
html += String(boundary_state.backbone_port);
|
||||
html += F("'>");
|
||||
|
||||
// ── Local TCP Server Section ──
|
||||
html += F(
|
||||
"<h2>📡 Local TCP Server (optional)</h2>"
|
||||
"<p class='note'>Run a TCP server on the same WiFi network so local devices can connect. "
|
||||
"Uses Access Point mode (does not forward announces).</p>"
|
||||
"<label>Local TCP Server</label>"
|
||||
"<select name='ap_tcp_en'>"
|
||||
);
|
||||
html += F("<option value='0'");
|
||||
if (!boundary_state.ap_tcp_enabled) html += F(" selected");
|
||||
html += F(">Disabled</option>");
|
||||
html += F("<option value='1'");
|
||||
if (boundary_state.ap_tcp_enabled) html += F(" selected");
|
||||
html += F(">Enabled</option>");
|
||||
html += F("</select>");
|
||||
|
||||
html += F("<label>TCP Port</label>");
|
||||
html += F("<input name='ap_tcp_port' type='number' min='1' max='65535' value='");
|
||||
html += String(boundary_state.ap_tcp_port);
|
||||
html += F("'>");
|
||||
|
||||
// ── LoRa Radio Section ──
|
||||
html += F(
|
||||
"<h2>📻 LoRa Radio</h2>"
|
||||
);
|
||||
|
||||
// Frequency — show in MHz for human-friendliness
|
||||
char freq_str[16];
|
||||
dtostrf((double)cur_freq / 1000000.0, 1, 3, freq_str);
|
||||
html += F("<label>Frequency (MHz)</label>");
|
||||
html += F("<input name='freq' type='text' placeholder='914.875' value='");
|
||||
html += String(freq_str);
|
||||
html += F("'>");
|
||||
html += F("<p class='note'>e.g. 914.875, 868.000, 433.000</p>");
|
||||
|
||||
// Bandwidth — dropdown
|
||||
html += F("<label>Bandwidth</label><select name='bw'>");
|
||||
for (int i = 0; i < BW_OPTIONS_COUNT; i++) {
|
||||
html += F("<option value='");
|
||||
html += String(BW_OPTIONS[i].hz);
|
||||
html += "'";
|
||||
if (BW_OPTIONS[i].hz == cur_bw) html += F(" selected");
|
||||
html += ">";
|
||||
html += BW_OPTIONS[i].label;
|
||||
html += F("</option>");
|
||||
}
|
||||
html += F("</select>");
|
||||
|
||||
// Spreading Factor — dropdown 6-12
|
||||
html += F("<label>Spreading Factor</label><select name='sf'>");
|
||||
for (int sf = 6; sf <= 12; sf++) {
|
||||
html += F("<option value='");
|
||||
html += String(sf);
|
||||
html += "'";
|
||||
if (sf == cur_sf) html += F(" selected");
|
||||
html += ">SF";
|
||||
html += String(sf);
|
||||
html += F("</option>");
|
||||
}
|
||||
html += F("</select>");
|
||||
|
||||
// Coding Rate — dropdown 5-8 (maps to 4/5 through 4/8)
|
||||
html += F("<label>Coding Rate</label><select name='cr'>");
|
||||
for (int cr = 5; cr <= 8; cr++) {
|
||||
html += F("<option value='");
|
||||
html += String(cr);
|
||||
html += "'";
|
||||
if (cr == cur_cr) html += F(" selected");
|
||||
html += ">4/";
|
||||
html += String(cr);
|
||||
html += F("</option>");
|
||||
}
|
||||
html += F("</select>");
|
||||
|
||||
// TX Power
|
||||
html += F("<label>TX Power (dBm)</label>");
|
||||
html += F("<input name='txp' type='number' min='2' max='");
|
||||
#ifdef PA_MAX_OUTPUT
|
||||
html += String(PA_MAX_OUTPUT);
|
||||
#else
|
||||
html += "22";
|
||||
#endif
|
||||
html += F("' value='");
|
||||
html += String(cur_txp);
|
||||
html += F("'>");
|
||||
|
||||
#ifdef PA_MAX_OUTPUT
|
||||
html += F("<p class='note'>Max output for this board: ");
|
||||
html += String(PA_MAX_OUTPUT);
|
||||
html += F(" dBm (with PA)</p>");
|
||||
#endif
|
||||
|
||||
// ── Submit ──
|
||||
html += F(
|
||||
"<button type='submit'>Save & Reboot</button>"
|
||||
"</form></body></html>"
|
||||
);
|
||||
|
||||
config_server->send(200, "text/html", html);
|
||||
}
|
||||
|
||||
// ─── Handle POST /save ──────────────────────────────────────────────────────
|
||||
|
||||
static void config_handle_save() {
|
||||
// ── WiFi STA credentials ──
|
||||
String ssid = config_server->arg("ssid");
|
||||
String psk = config_server->arg("psk");
|
||||
|
||||
// Write SSID to config EEPROM area
|
||||
for (int i = 0; i < 32; i++) {
|
||||
uint8_t c = (i < (int)ssid.length()) ? ssid[i] : 0x00;
|
||||
EEPROM.write(config_addr(ADDR_CONF_SSID + i), c);
|
||||
}
|
||||
EEPROM.write(config_addr(ADDR_CONF_SSID + 32), 0x00);
|
||||
|
||||
// Write PSK
|
||||
for (int i = 0; i < 32; i++) {
|
||||
uint8_t c = (i < (int)psk.length()) ? psk[i] : 0x00;
|
||||
EEPROM.write(config_addr(ADDR_CONF_PSK + i), c);
|
||||
}
|
||||
EEPROM.write(config_addr(ADDR_CONF_PSK + 32), 0x00);
|
||||
|
||||
// Set WiFi mode to STA
|
||||
EEPROM.write(eeprom_addr(ADDR_CONF_WIFI), WR_WIFI_STA);
|
||||
|
||||
// ── WiFi enable setting ──
|
||||
boundary_state.wifi_enabled = (config_server->arg("wifi_en").toInt() == 1);
|
||||
|
||||
// ── TCP backbone settings ──
|
||||
boundary_state.tcp_mode = (uint8_t)config_server->arg("tcp_mode").toInt(); // 0=disabled, 1=client
|
||||
if (boundary_state.tcp_mode > 1) boundary_state.tcp_mode = 0;
|
||||
boundary_state.tcp_port = (uint16_t)config_server->arg("tcp_port").toInt();
|
||||
if (boundary_state.tcp_port == 0) boundary_state.tcp_port = 4242;
|
||||
|
||||
String bb_host = config_server->arg("bb_host");
|
||||
memset(boundary_state.backbone_host, 0, sizeof(boundary_state.backbone_host));
|
||||
strncpy(boundary_state.backbone_host, bb_host.c_str(), sizeof(boundary_state.backbone_host) - 1);
|
||||
|
||||
boundary_state.backbone_port = (uint16_t)config_server->arg("bb_port").toInt();
|
||||
if (boundary_state.backbone_port == 0) boundary_state.backbone_port = 4242;
|
||||
|
||||
// ── Local TCP server settings ──
|
||||
boundary_state.ap_tcp_enabled = (config_server->arg("ap_tcp_en").toInt() == 1);
|
||||
boundary_state.ap_tcp_port = (uint16_t)config_server->arg("ap_tcp_port").toInt();
|
||||
if (boundary_state.ap_tcp_port == 0) boundary_state.ap_tcp_port = 4242;
|
||||
|
||||
// Save boundary config to EEPROM
|
||||
boundary_save_config();
|
||||
|
||||
// ── LoRa radio settings ──
|
||||
String freq_str = config_server->arg("freq");
|
||||
double freq_mhz = freq_str.toDouble();
|
||||
if (freq_mhz > 0) {
|
||||
lora_freq = (uint32_t)(freq_mhz * 1000000.0);
|
||||
}
|
||||
|
||||
String bw_str = config_server->arg("bw");
|
||||
uint32_t bw_val = (uint32_t)bw_str.toInt();
|
||||
if (bw_val > 0) lora_bw = bw_val;
|
||||
|
||||
int sf_val = config_server->arg("sf").toInt();
|
||||
if (sf_val >= 6 && sf_val <= 12) lora_sf = sf_val;
|
||||
|
||||
int cr_val = config_server->arg("cr").toInt();
|
||||
if (cr_val >= 5 && cr_val <= 8) lora_cr = cr_val;
|
||||
|
||||
int txp_val = config_server->arg("txp").toInt();
|
||||
if (txp_val >= 2 && txp_val <= 30) lora_txp = txp_val;
|
||||
|
||||
// Save LoRa config to EEPROM (reuse existing eeprom_conf functions)
|
||||
// Write directly since hw_ready may not be set yet
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_BW) + 0, lora_bw >> 24);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_BW) + 1, lora_bw >> 16);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_BW) + 2, lora_bw >> 8);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_BW) + 3, lora_bw);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 0, lora_freq >> 24);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 1, lora_freq >> 16);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 2, lora_freq >> 8);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_FREQ) + 3, lora_freq);
|
||||
eeprom_update(eeprom_addr(ADDR_CONF_OK), CONF_OK_BYTE);
|
||||
|
||||
EEPROM.commit();
|
||||
|
||||
// ── Send confirmation page ──
|
||||
String ok = F(
|
||||
"<!DOCTYPE html><html><head>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
|
||||
"<title>Saved</title>"
|
||||
"<style>"
|
||||
"body{font-family:sans-serif;background:#1a1a2e;color:#e0e0e0;padding:40px;"
|
||||
"display:flex;align-items:center;justify-content:center;min-height:80vh;}"
|
||||
".ok{background:#16213e;padding:30px;border-radius:12px;text-align:center;max-width:400px;}"
|
||||
"h1{color:#4caf50;margin-bottom:16px;}"
|
||||
"p{color:#aaa;}"
|
||||
"</style></head><body>"
|
||||
"<div class='ok'>"
|
||||
"<h1>✅ Configuration Saved</h1>"
|
||||
"<p>Device will reboot in 3 seconds and connect to your WiFi network.</p>"
|
||||
"<p style='color:#666;font-size:0.85em;'>If the device cannot connect, hold the button for 5+ seconds to re-enter setup.</p>"
|
||||
"</div></body></html>"
|
||||
);
|
||||
config_server->send(200, "text/html", ok);
|
||||
|
||||
// Give the response time to send
|
||||
delay(3000);
|
||||
|
||||
// Reboot
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// ─── Captive Portal redirect ─────────────────────────────────────────────────
|
||||
static void config_handle_redirect() {
|
||||
config_server->sendHeader("Location", "http://10.0.0.1/", true);
|
||||
config_server->send(302, "text/plain", "Redirecting to setup...");
|
||||
}
|
||||
|
||||
// ─── Check if config is needed ───────────────────────────────────────────────
|
||||
bool boundary_needs_config() {
|
||||
// Check if WiFi SSID is configured
|
||||
char ssid[33];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
ssid[i] = EEPROM.read(config_addr(ADDR_CONF_SSID + i));
|
||||
if (ssid[i] == (char)0xFF) ssid[i] = '\0';
|
||||
}
|
||||
ssid[32] = '\0';
|
||||
|
||||
// Also check boundary mode enable flag
|
||||
uint8_t bmode = EEPROM.read(config_addr(ADDR_CONF_BMODE));
|
||||
|
||||
// Need config if no SSID set and boundary not yet configured
|
||||
if (ssid[0] == '\0' && bmode != BOUNDARY_ENABLE_BYTE) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Start Config Portal ─────────────────────────────────────────────────────
|
||||
void config_portal_start() {
|
||||
if (config_portal_active) return;
|
||||
|
||||
Serial.println("[Config] Starting configuration portal...");
|
||||
|
||||
// Stop any existing WiFi
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.disconnect(true, true);
|
||||
WiFi.mode(WIFI_MODE_NULL);
|
||||
delay(100);
|
||||
|
||||
// Start AP
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(CONFIG_AP_SSID, NULL); // Open AP for easy setup
|
||||
delay(150);
|
||||
|
||||
IPAddress ap_addr(10, 0, 0, 1);
|
||||
IPAddress ap_mask(255, 255, 255, 0);
|
||||
WiFi.softAPConfig(ap_addr, ap_addr, ap_mask);
|
||||
|
||||
Serial.print("[Config] AP started: ");
|
||||
Serial.println(CONFIG_AP_SSID);
|
||||
Serial.print("[Config] IP: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
|
||||
// Start DNS server for captive portal (redirect all domains to us)
|
||||
config_dns = new DNSServer();
|
||||
config_dns->start(DNS_PORT, "*", ap_addr);
|
||||
|
||||
// Start web server
|
||||
config_server = new WebServer(HTTP_PORT);
|
||||
config_server->on("/", HTTP_GET, config_send_html);
|
||||
config_server->on("/save", HTTP_POST, config_handle_save);
|
||||
config_server->onNotFound(config_handle_redirect); // Captive portal catch-all
|
||||
config_server->begin();
|
||||
|
||||
config_portal_active = true;
|
||||
|
||||
Serial.println("[Config] Portal ready — connect to WiFi: " + String(CONFIG_AP_SSID));
|
||||
|
||||
#if HAS_DISPLAY
|
||||
if (disp_ready) {
|
||||
// Show config mode on display
|
||||
stat_area.fillScreen(SSD1306_BLACK);
|
||||
stat_area.setCursor(0, 0);
|
||||
stat_area.println("CONFIG MODE");
|
||||
stat_area.println("");
|
||||
stat_area.println("Connect to:");
|
||||
stat_area.println(CONFIG_AP_SSID);
|
||||
stat_area.println("");
|
||||
stat_area.println("Open browser");
|
||||
stat_area.println("http://10.0.0.1");
|
||||
display.clearDisplay();
|
||||
display.drawBitmap(0, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
|
||||
display.display();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ─── Stop Config Portal ──────────────────────────────────────────────────────
|
||||
void config_portal_stop() {
|
||||
if (!config_portal_active) return;
|
||||
|
||||
Serial.println("[Config] Stopping configuration portal");
|
||||
|
||||
if (config_server) {
|
||||
config_server->stop();
|
||||
delete config_server;
|
||||
config_server = nullptr;
|
||||
}
|
||||
if (config_dns) {
|
||||
config_dns->stop();
|
||||
delete config_dns;
|
||||
config_dns = nullptr;
|
||||
}
|
||||
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_MODE_NULL);
|
||||
config_portal_active = false;
|
||||
}
|
||||
|
||||
// ─── Portal Loop — call from main loop() ─────────────────────────────────────
|
||||
void config_portal_loop() {
|
||||
if (!config_portal_active) return;
|
||||
if (config_dns) config_dns->processNextRequest();
|
||||
if (config_server) config_server->handleClient();
|
||||
}
|
||||
|
||||
// ─── Is portal active? ──────────────────────────────────────────────────────
|
||||
bool config_portal_is_active() {
|
||||
return config_portal_active;
|
||||
}
|
||||
|
||||
#endif // BOUNDARY_MODE
|
||||
#endif // BOUNDARY_CONFIG_H
|
||||
Reference in New Issue
Block a user