Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changes/kconfig-tunables.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
kind: changed
---
Move build-time settings into Kconfig

## Release Note
Build-time settings can now be changed in menuconfig

- Task stacks and priorities, timeouts, retry intervals, queue and buffer sizes are now set in `menuconfig` under OpenShock, each next to the component that uses it
- The default API and firmware CDN domains, hostname, SoftAP SSID prefix, captive portal IP address and venue info URL are set there too
- The log level can be overridden in `menuconfig`; by default it still follows the build mode
Comment thread
coderabbitai[bot] marked this conversation as resolved.
82 changes: 82 additions & 0 deletions components/captive_portal/Kconfig.openshock
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Sourced into the OpenShock menu by main/Kconfig.projbuild

menu "Captive portal"

config OPENSHOCK_PORTAL_AP_IP
string "SoftAP IPv4 address"
default "4.3.2.1"
help
The fixed address the portal's SoftAP, DHCP server, DNS responder and captive
portal API serve from, on a /24 network. Must be a dotted-decimal IPv4 address
(checked at compile time).

config OPENSHOCK_PORTAL_MAX_CLIENTS
int "Clients (SoftAP stations and websockets)"
range 1 8
default 4
help
The SoftAP's station limit and the number of websocket clients the portal
serves. Must stay below the HTTP server's socket count.

config OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS
int "HTTP server sockets"
range 2 10
default 8
help
Browsers open several connections per page load; with only 4 the LRU purge
evicted the websocket (server push, so it looks idle). Also sizes the broadcast
fd list. httpd needs 3 lwIP sockets besides these, and the DNS server, the
gateway connection and an outbound HTTP request need one each (checked against
LWIP_MAX_SOCKETS).

config OPENSHOCK_PORTAL_HTTPD_STACK_SIZE
int "HTTP server task stack (bytes)"
range 4096 32768
default 8192

config OPENSHOCK_PORTAL_HTTPD_TIMEOUT_S
int "HTTP socket send/receive timeout (s)"
range 1 60
default 5

config OPENSHOCK_PORTAL_MAX_MESSAGE_SIZE
int "Largest local websocket message (bytes)"
range 1024 32768
default 4096

config OPENSHOCK_PORTAL_WS_MAX_QUEUED_SENDS
int "Queued websocket sends"
range 4 256
default 32
help
Outgoing websocket messages waiting for httpd. Bounds the memory a burst of
broadcasts can hold while no client drains them; past it, new ones are dropped.

config OPENSHOCK_PORTAL_STARTUP_GRACE_S
int "Startup grace period (s)"
range 0 600
default 30
help
On a fully configured hub, how long after boot the portal waits for the gateway
connection before it opens. It stays closed if the hub connects in time.

config OPENSHOCK_PORTAL_AUTO_CLOSE_S
int "Auto-close delay (s)"
range 10 3600
default 300
help
Once the hub is online and no client is connected, the portal closes after this long.

config OPENSHOCK_PORTAL_MANAGER_TASK_STACK_SIZE
int "Portal manager task stack (bytes)"
range 4096 16384
default 6144

config OPENSHOCK_PORTAL_ACCOUNT_LINK_TASK_STACK_SIZE
int "Account link task stack (bytes)"
range 4096 16384
default 8192
help
Runs the HTTPS request that exchanges a link code for a token.

endmenu
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,9 @@ const char* const TAG = "CaptivePortalInstance";
#include <string_view>

const uint16_t HTTP_PORT = 80;
// Browsers open several connections per page load; with only 4 slots the LRU purge evicted the (server-push, therefore
// "idle" looking) WebSocket. Needs CONFIG_LWIP_MAX_SOCKETS >= this + 3 (+ DNS/HTTP client). Also sizes the broadcast
// fd list, which must hold every open socket.
const size_t HTTP_MAX_OPEN_SOCKETS = 8;
static_assert(CONFIG_LWIP_MAX_SOCKETS >= HTTP_MAX_OPEN_SOCKETS + 3, "httpd needs 3 lwIP sockets besides its client sockets");

// Largest inbound WebSocket message we'll accept (local FlatBuffer commands are tiny).
// Nothing larger is accepted by the local message handler, so don't buffer more than that.
static constexpr size_t MAX_WS_MSG = OpenShock::MessageHandlers::WebSocket::MaxLocalMessageSize;
// httpd needs 3 lwIP sockets besides its client sockets; the DNS server, the gateway connection and an outbound HTTP request need one each
static_assert(CONFIG_LWIP_MAX_SOCKETS >= CONFIG_OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS + 3 + 3, "httpd client sockets would leave no lwIP sockets for the DNS server and outbound connections");
static_assert(CONFIG_OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS > CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS, "every websocket client needs a socket, plus one for plain requests");

// HTTP status lines (esp_http_server needs the full "code reason" string, and stores
// the pointer rather than copying, so these must have static storage duration).
Expand Down Expand Up @@ -367,7 +361,7 @@ static esp_err_t apiAccountLink(httpd_req_t* req)

auto* job = new AccountLinkJob {std::move(instance), asyncReq, std::move(*code)};
// TLS needs a large stack
if (TaskUtils::TaskCreateExpensive(accountLinkTask, "AccountLink", 8192, job, 1, nullptr) != pdPASS) {
if (TaskUtils::TaskCreateExpensive(accountLinkTask, "AccountLink", CONFIG_OPENSHOCK_PORTAL_ACCOUNT_LINK_TASK_STACK_SIZE, job, 1, nullptr) != pdPASS) {
OS_LOGE(TAG, "Failed to create account link task");
delete job;
esp_err_t err = sendResp(asyncReq, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL);
Expand Down Expand Up @@ -651,9 +645,6 @@ namespace {
#error "CONFIG_HTTPD_QUEUE_WORK_BLOCKING must be disabled"
#endif

// Bounds the memory a burst of broadcasts can hold while no client drains them
constexpr size_t kMaxQueuedSends = 32;

void sendToWsClient(httpd_handle_t hd, int fd, httpd_ws_frame_t& frame)
{
if (httpd_ws_send_frame_async(hd, fd, &frame) != ESP_OK) {
Expand All @@ -672,7 +663,7 @@ namespace {
int CaptivePortal::CaptivePortalInstance::idForFd(int fd)
{
ScopedLock lock__(&m_clientsMutex);
for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) {
for (uint8_t i = 0; i < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS; ++i) {
if (m_clients[i].used && m_clients[i].fd == fd) {
return i;
}
Expand All @@ -683,7 +674,7 @@ int CaptivePortal::CaptivePortalInstance::idForFd(int fd)
int CaptivePortal::CaptivePortalInstance::fdForId(uint8_t socketId)
{
ScopedLock lock__(&m_clientsMutex);
if (socketId < MAX_WS_CLIENTS && m_clients[socketId].used) {
if (socketId < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS && m_clients[socketId].used) {
return m_clients[socketId].fd;
}
return -1;
Expand All @@ -694,13 +685,13 @@ uint8_t CaptivePortal::CaptivePortalInstance::onWsOpen(int fd)
ScopedLock lock__(&m_clientsMutex);

// Idempotent: an fd already tracked keeps its id.
for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) {
for (uint8_t i = 0; i < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS; ++i) {
if (m_clients[i].used && m_clients[i].fd == fd) {
return i;
}
}

for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) {
for (uint8_t i = 0; i < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS; ++i) {
if (!m_clients[i].used) {
m_clients[i].used = true;
m_clients[i].fd = fd;
Expand All @@ -719,7 +710,7 @@ void CaptivePortal::CaptivePortalInstance::onWsClose(int fd)
int id = -1;
{
ScopedLock lock__(&m_clientsMutex);
for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) {
for (uint8_t i = 0; i < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS; ++i) {
if (m_clients[i].used && m_clients[i].fd == fd) {
m_clients[i].used = false;
m_clients[i].reasmActive = false;
Expand Down Expand Up @@ -783,7 +774,7 @@ esp_err_t CaptivePortal::CaptivePortalInstance::wsHandler(httpd_req_t* req)
return ESP_OK;
}

if (frame.len > MAX_WS_MSG) {
if (frame.len > CONFIG_OPENSHOCK_PORTAL_MAX_MESSAGE_SIZE) { // Nothing larger is accepted by the local message handler, so don't buffer it
OS_LOGE(TAG, "WebSocket frame too large (%u bytes), dropping", static_cast<unsigned>(frame.len));
return ESP_FAIL;
}
Expand Down Expand Up @@ -845,9 +836,9 @@ void CaptivePortal::CaptivePortalInstance::onWsFrame(int fd, httpd_ws_type_t opc
OS_LOGW(TAG, "WebSocket client %u sent a continuation frame without a message start, dropping it", socketId);
break;
}
// MAX_WS_MSG only bounds single frames; also bound the reassembled message
if (m_clients[idx].reasm.size() + payload.size() > MAX_WS_MSG) {
OS_LOGE(TAG, "WebSocket client %u fragmented message exceeds %u bytes, closing", socketId, static_cast<unsigned>(MAX_WS_MSG));
// The frame check only bounds single frames; also bound the reassembled message
if (m_clients[idx].reasm.size() + payload.size() > CONFIG_OPENSHOCK_PORTAL_MAX_MESSAGE_SIZE) {
OS_LOGE(TAG, "WebSocket client %u fragmented message exceeds %d bytes, closing", socketId, CONFIG_OPENSHOCK_PORTAL_MAX_MESSAGE_SIZE);
m_clients[idx].reasmActive = false;
m_clients[idx].reasm.clear();
m_clients[idx].reasm.shrink_to_fit();
Expand Down Expand Up @@ -935,7 +926,7 @@ bool CaptivePortal::CaptivePortalInstance::queueSend(int fd, bool broadcast, boo

{
ScopedLock lock__(&m_sendMutex);
if (m_sendQueue.size() >= kMaxQueuedSends) {
if (m_sendQueue.size() >= CONFIG_OPENSHOCK_PORTAL_WS_MAX_QUEUED_SENDS) {
OS_LOGW(TAG, "WebSocket send queue full, dropping message");
return false;
}
Expand Down Expand Up @@ -1003,8 +994,8 @@ void CaptivePortal::CaptivePortalInstance::drainSends()
continue;
}

size_t count = HTTP_MAX_OPEN_SOCKETS;
int fds[HTTP_MAX_OPEN_SOCKETS];
size_t count = CONFIG_OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS;
int fds[CONFIG_OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS];
if (httpd_get_client_list(m_server, &count, fds) == ESP_OK) {
for (size_t i = 0; i < count; ++i) {
if (httpd_ws_get_fd_info(m_server, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
Expand Down Expand Up @@ -1059,7 +1050,7 @@ bool CaptivePortal::CaptivePortalInstance::broadcastMessageBIN(std::span<const u
bool CaptivePortal::CaptivePortalInstance::hasClients()
{
ScopedLock lock__(&m_clientsMutex);
for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) {
for (uint8_t i = 0; i < CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS; ++i) {
if (m_clients[i].used) {
return true;
}
Expand Down Expand Up @@ -1129,12 +1120,12 @@ bool CaptivePortal::CaptivePortalInstance::startHttpServer()
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = HTTP_PORT;
config.max_uri_handlers = 40;
config.max_open_sockets = HTTP_MAX_OPEN_SOCKETS;
config.max_open_sockets = CONFIG_OPENSHOCK_PORTAL_HTTPD_MAX_SOCKETS;
config.lru_purge_enable = true;
config.stack_size = 8192;
config.stack_size = CONFIG_OPENSHOCK_PORTAL_HTTPD_STACK_SIZE;
// Shorter than the default 10 s: one stalled or sleeping client blocks the single httpd task for this long.
config.recv_wait_timeout = 5;
config.send_wait_timeout = 5;
config.recv_wait_timeout = CONFIG_OPENSHOCK_PORTAL_HTTPD_TIMEOUT_S;
config.send_wait_timeout = CONFIG_OPENSHOCK_PORTAL_HTTPD_TIMEOUT_S;
config.uri_match_fn = httpd_uri_match_wildcard;
// Same core as the WiFi/lwIP tasks, away from the E-Stop and RF tasks on core 1
config.core_id = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ namespace OpenShock::CaptivePortal {
inline bool ok() const { return m_server != nullptr && m_dnsStarted; }

private:
static constexpr uint8_t MAX_WS_CLIENTS = 4; // matches AP max_connection

struct WsClient {
bool used;
int fd;
Expand Down Expand Up @@ -93,7 +91,7 @@ namespace OpenShock::CaptivePortal {
const char* m_fsHash;
DNSServer m_dnsServer;

WsClient m_clients[MAX_WS_CLIENTS];
WsClient m_clients[CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS];
SimpleMutex m_clientsMutex;

struct WsSend {
Expand Down
27 changes: 13 additions & 14 deletions components/captive_portal/src/captiveportal/Manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const char* const TAG = "CaptivePortal";
#include <esp_wifi_default.h>

#include "SimpleMutex.h"
#include "util/IPv4Utils.h"
#include "util/RetiredList.h"
#include "util/TaskUtils.h"

Expand Down Expand Up @@ -53,8 +54,8 @@ struct ManagerState {
int64_t autoCloseAt = 0; // esp_timer time at which an idle portal closes; 0 = not armed
};

static constexpr int64_t STARTUP_GRACE_PERIOD_US = 30LL * 1'000'000; // 30 seconds
static constexpr int64_t AUTO_CLOSE_DELAY_US = 5LL * 60 * 1'000'000; // 5 minutes
static constexpr int64_t STARTUP_GRACE_PERIOD_US = CONFIG_OPENSHOCK_PORTAL_STARTUP_GRACE_S * 1'000'000LL;
static constexpr int64_t AUTO_CLOSE_DELAY_US = CONFIG_OPENSHOCK_PORTAL_AUTO_CLOSE_S * 1'000'000LL;

// The requested always-enabled setting, and what IsAlwaysEnabled() reports. Written by SetAlwaysEnabled().
static std::atomic<bool> s_alwaysEnabled = false;
Expand All @@ -72,9 +73,7 @@ static std::atomic<bool> s_instanceLive = false;
static RetiredList<CaptivePortalInstance> s_retiredInstances;
static esp_netif_t* s_apNetif = nullptr;

// The captive portal AP always serves from this fixed address; the DNS server and
// RFC8908 handler reference it too (see CaptivePortal::ApIPv4String()).
static const char* const CAPTIVE_PORTAL_AP_IP = "4.3.2.1";
static_assert(IPv4Utils::IsDottedDecimal(CONFIG_OPENSHOCK_PORTAL_AP_IP), "CONFIG_OPENSHOCK_PORTAL_AP_IP must be a dotted-decimal IPv4 address");

static void sendCommand(uint32_t command)
{
Expand Down Expand Up @@ -161,8 +160,8 @@ static bool captiveportal_start()
}

esp_netif_ip_info_t ipInfo = {};
esp_netif_str_to_ip4(CAPTIVE_PORTAL_AP_IP, &ipInfo.ip);
esp_netif_str_to_ip4(CAPTIVE_PORTAL_AP_IP, &ipInfo.gw);
esp_netif_str_to_ip4(CONFIG_OPENSHOCK_PORTAL_AP_IP, &ipInfo.ip);
esp_netif_str_to_ip4(CONFIG_OPENSHOCK_PORTAL_AP_IP, &ipInfo.gw);
esp_netif_str_to_ip4("255.255.255.0", &ipInfo.netmask);

esp_netif_dhcps_stop(s_apNetif);
Expand All @@ -172,7 +171,7 @@ static bool captiveportal_start()
// can open the portal without waiting for their connectivity probes to be redirected. esp_netif keeps the
// pointer, so the string must outlive the netif.
static char captivePortalUri[64];
snprintf(captivePortalUri, sizeof(captivePortalUri), "http://%s/captive-portal/api", CAPTIVE_PORTAL_AP_IP); // RFC8908Handler's API path
snprintf(captivePortalUri, sizeof(captivePortalUri), "http://%s/captive-portal/api", CONFIG_OPENSHOCK_PORTAL_AP_IP); // RFC8908Handler's API path
esp_err_t uriErr = esp_netif_dhcps_option(s_apNetif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captivePortalUri, strlen(captivePortalUri));
if (uriErr != ESP_OK) {
OS_LOGW(TAG, "Failed to set DHCP captive portal URI: %s", esp_err_to_name(uriErr));
Expand All @@ -190,7 +189,7 @@ static bool captiveportal_start()
apConfig.ap.ssid_len = strlen(reinterpret_cast<char*>(apConfig.ap.ssid));
apConfig.ap.channel = 1;
apConfig.ap.authmode = WIFI_AUTH_OPEN;
apConfig.ap.max_connection = 4;
apConfig.ap.max_connection = CONFIG_OPENSHOCK_PORTAL_MAX_CLIENTS;
apConfig.ap.beacon_interval = 100;

esp_err_t err = esp_wifi_set_mode(WIFI_MODE_APSTA);
Expand Down Expand Up @@ -312,12 +311,12 @@ static void captiveportal_tick(ManagerState& state)
return;
}

// Auto-close: no clients connected, WiFi + gateway are up, 5 minutes elapsed
// Auto-close: no clients connected, WiFi + gateway are up, AUTO_CLOSE_DELAY_US elapsed
if (running && !state.alwaysEnabled && connected && !hasClients) {
if (state.autoCloseAt == 0) {
state.autoCloseAt = now + AUTO_CLOSE_DELAY_US;
} else if (now >= state.autoCloseAt) {
OS_LOGI(TAG, "Auto-closing captive portal AP (no clients for 5 minutes)");
OS_LOGI(TAG, "Auto-closing captive portal AP (no clients for %d s)", CONFIG_OPENSHOCK_PORTAL_AUTO_CLOSE_S);
captiveportal_stop(state);
return;
}
Expand Down Expand Up @@ -368,7 +367,7 @@ bool CaptivePortal::Init()
// If device is already fully configured, set a startup grace period before opening portal
if (isDeviceFullyConfigured()) {
initialState->startupGraceEnd = esp_timer_get_time() + STARTUP_GRACE_PERIOD_US;
OS_LOGI(TAG, "Device fully configured, startup grace period of 30s before opening portal");
OS_LOGI(TAG, "Device fully configured, startup grace period of %ds before opening portal", CONFIG_OPENSHOCK_PORTAL_STARTUP_GRACE_S);
}

s_forceCloseDone = xSemaphoreCreateBinary();
Expand All @@ -378,7 +377,7 @@ bool CaptivePortal::Init()
return false;
}

if (TaskUtils::TaskCreateExpensive(captiveportal_managertask, "CaptivePortalManager", 6144, initialState, 1, &s_managerTask) != pdPASS) {
if (TaskUtils::TaskCreateExpensive(captiveportal_managertask, "CaptivePortalManager", CONFIG_OPENSHOCK_PORTAL_MANAGER_TASK_STACK_SIZE, initialState, 1, &s_managerTask) != pdPASS) {
OS_LOGE(TAG, "Failed to create captive portal manager task");
delete initialState;
return false;
Expand Down Expand Up @@ -457,7 +456,7 @@ bool CaptivePortal::IsRunning()

const char* CaptivePortal::ApIPv4String()
{
return CAPTIVE_PORTAL_AP_IP;
return CONFIG_OPENSHOCK_PORTAL_AP_IP;
}

bool CaptivePortal::SendMessageTXT(uint8_t socketId, std::string_view data)
Expand Down
1 change: 1 addition & 0 deletions components/common/host_test/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ idf_component_register(
"test_stringutils.cpp"
"test_convert.cpp"
"test_hexutils.cpp"
"test_ipv4utils.cpp"
"test_semver.cpp"
"test_checksum.cpp"
"test_boarddescriptor.cpp"
Expand Down
Loading
Loading