Add optional mothership IP override to the provisioning flow so nodes on networks where mDNS is blocked (enterprise WiFi, mesh, VLANs) can connect on first boot without manual intervention. - Add ms_ip field to provisioning Payload and request structs - Firmware writes ms_ip to both NVS_KEY_MS_IP and NVS_KEY_MS_IP_PROV - Discovery prefers provisioned IP on first attempt, falls back to mDNS, then cached IP - Web Serial wizard adds Mothership IP field in Network Troubleshooting - Auto-populates IP when browser accesses dashboard by IP address - Document when/how to use the override in docs/notes/mdns-override.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
185 lines
6.1 KiB
C
185 lines
6.1 KiB
C
#include "provision.h"
|
|
#include "spaxel.h"
|
|
#include "esp_log.h"
|
|
#include "nvs_flash.h"
|
|
#include "nvs.h"
|
|
#include "cJSON.h"
|
|
#include "driver/uart.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include <string.h>
|
|
|
|
static const char *TAG = "provision";
|
|
|
|
#define PROVISION_UART UART_NUM_0
|
|
#define PROVISION_BAUD_RATE 115200
|
|
#define PROVISION_WINDOW_MS_FRESH 120000 // 2 min for unprovisioned boards
|
|
#define PROVISION_WINDOW_MS_REPROV 15000 // 15 s for already-provisioned boards
|
|
#define UART_RX_BUF_SIZE 1024
|
|
#define MAX_LINE_LEN 768
|
|
|
|
void provision_listen_window(void) {
|
|
uart_config_t uart_cfg = {
|
|
.baud_rate = PROVISION_BAUD_RATE,
|
|
.data_bits = UART_DATA_8_BITS,
|
|
.parity = UART_PARITY_DISABLE,
|
|
.stop_bits = UART_STOP_BITS_1,
|
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
|
.source_clk = UART_SCLK_DEFAULT,
|
|
};
|
|
|
|
if (uart_param_config(PROVISION_UART, &uart_cfg) != ESP_OK) {
|
|
ESP_LOGW(TAG, "UART param config failed, skipping provision window");
|
|
return;
|
|
}
|
|
if (uart_driver_install(PROVISION_UART, UART_RX_BUF_SIZE, 0, 0, NULL, 0) != ESP_OK) {
|
|
ESP_LOGW(TAG, "UART driver install failed, skipping provision window");
|
|
return;
|
|
}
|
|
|
|
char mac_str[18];
|
|
mac_to_str(g_state.mac, mac_str, sizeof(mac_str));
|
|
|
|
uint32_t window_ms = g_state.provisioned
|
|
? PROVISION_WINDOW_MS_REPROV
|
|
: PROVISION_WINDOW_MS_FRESH;
|
|
|
|
// Signal that firmware is ready for provisioning (includes MAC for display)
|
|
char ready_msg[64];
|
|
snprintf(ready_msg, sizeof(ready_msg), "SPAXEL READY %s\n", mac_str);
|
|
uart_write_bytes(PROVISION_UART, ready_msg, strlen(ready_msg));
|
|
|
|
ESP_LOGI(TAG, "Provisioning window open for %u ms (MAC: %s)", (unsigned)window_ms, mac_str);
|
|
|
|
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(window_ms);
|
|
char line[MAX_LINE_LEN];
|
|
int line_pos = 0;
|
|
|
|
while (xTaskGetTickCount() < deadline) {
|
|
uint8_t ch;
|
|
int n = uart_read_bytes(PROVISION_UART, &ch, 1, pdMS_TO_TICKS(50));
|
|
if (n <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (ch == '\r') {
|
|
continue; // ignore CR
|
|
}
|
|
|
|
if (ch == '\n') {
|
|
line[line_pos] = '\0';
|
|
line_pos = 0;
|
|
|
|
if (strlen(line) == 0) {
|
|
continue;
|
|
}
|
|
|
|
cJSON *root = cJSON_Parse(line);
|
|
if (!root) {
|
|
const char *err_resp = "{\"ok\":false,\"error\":\"invalid_json\"}\n";
|
|
uart_write_bytes(PROVISION_UART, err_resp, strlen(err_resp));
|
|
continue;
|
|
}
|
|
|
|
cJSON *prov = cJSON_GetObjectItem(root, "provision");
|
|
if (!prov) {
|
|
cJSON_Delete(root);
|
|
const char *err_resp = "{\"ok\":false,\"error\":\"missing_provision_key\"}\n";
|
|
uart_write_bytes(PROVISION_UART, err_resp, strlen(err_resp));
|
|
continue;
|
|
}
|
|
|
|
esp_err_t err = provision_write_nvs(prov);
|
|
cJSON_Delete(root);
|
|
|
|
if (err == ESP_OK) {
|
|
char resp[80];
|
|
snprintf(resp, sizeof(resp), "{\"ok\":true,\"mac\":\"%s\"}\n", mac_str);
|
|
uart_write_bytes(PROVISION_UART, resp, strlen(resp));
|
|
ESP_LOGI(TAG, "Provisioning complete via serial");
|
|
uart_driver_delete(PROVISION_UART);
|
|
return;
|
|
} else {
|
|
const char *err_resp = "{\"ok\":false,\"error\":\"nvs_write_failed\"}\n";
|
|
uart_write_bytes(PROVISION_UART, err_resp, strlen(err_resp));
|
|
}
|
|
} else if (line_pos < MAX_LINE_LEN - 1) {
|
|
line[line_pos++] = (char)ch;
|
|
} else {
|
|
// Line too long — flush buffer
|
|
line_pos = 0;
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Provisioning window closed (no provisioning received)");
|
|
uart_driver_delete(PROVISION_UART);
|
|
}
|
|
|
|
esp_err_t provision_write_nvs(cJSON *prov) {
|
|
nvs_handle_t nvs;
|
|
esp_err_t err = nvs_open(SPAXEL_NAMESPACE, NVS_READWRITE, &nvs);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "NVS open failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
cJSON *ssid = cJSON_GetObjectItem(prov, "wifi_ssid");
|
|
if (ssid && cJSON_IsString(ssid) && strlen(ssid->valuestring) > 0) {
|
|
nvs_set_str(nvs, NVS_KEY_WIFI_SSID, ssid->valuestring);
|
|
} else {
|
|
nvs_close(nvs);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
cJSON *pass = cJSON_GetObjectItem(prov, "wifi_pass");
|
|
if (pass && cJSON_IsString(pass)) {
|
|
nvs_set_str(nvs, NVS_KEY_WIFI_PASS, pass->valuestring);
|
|
}
|
|
|
|
cJSON *node_id = cJSON_GetObjectItem(prov, "node_id");
|
|
if (node_id && cJSON_IsString(node_id)) {
|
|
nvs_set_str(nvs, NVS_KEY_NODE_ID, node_id->valuestring);
|
|
}
|
|
|
|
cJSON *token = cJSON_GetObjectItem(prov, "node_token");
|
|
if (token && cJSON_IsString(token)) {
|
|
nvs_set_str(nvs, NVS_KEY_NODE_TOKEN, token->valuestring);
|
|
}
|
|
|
|
cJSON *mdns_name = cJSON_GetObjectItem(prov, "ms_mdns");
|
|
if (mdns_name && cJSON_IsString(mdns_name)) {
|
|
nvs_set_str(nvs, NVS_KEY_MS_MDNS, mdns_name->valuestring);
|
|
}
|
|
|
|
cJSON *ms_ip = cJSON_GetObjectItem(prov, "ms_ip");
|
|
if (ms_ip && cJSON_IsString(ms_ip) && strlen(ms_ip->valuestring) > 0) {
|
|
nvs_set_str(nvs, NVS_KEY_MS_IP, ms_ip->valuestring);
|
|
nvs_set_str(nvs, NVS_KEY_MS_IP_PROV, ms_ip->valuestring);
|
|
}
|
|
|
|
cJSON *port = cJSON_GetObjectItem(prov, "ms_port");
|
|
if (port && cJSON_IsNumber(port) && port->valueint > 0) {
|
|
nvs_set_u16(nvs, NVS_KEY_MS_PORT, (uint16_t)port->valueint);
|
|
}
|
|
|
|
cJSON *debug_flag = cJSON_GetObjectItem(prov, "debug");
|
|
if (debug_flag) {
|
|
nvs_set_u8(nvs, NVS_KEY_DEBUG, cJSON_IsTrue(debug_flag) ? 1 : 0);
|
|
}
|
|
|
|
cJSON *ntp_server = cJSON_GetObjectItem(prov, "ntp_server");
|
|
if (ntp_server && cJSON_IsString(ntp_server)) {
|
|
nvs_set_str(nvs, NVS_KEY_NTP_SERVER, ntp_server->valuestring);
|
|
}
|
|
|
|
nvs_set_u8(nvs, NVS_KEY_PROVISIONED, 1);
|
|
nvs_set_u8(nvs, NVS_KEY_SCHEMA_VER, NVS_SCHEMA_VERSION);
|
|
|
|
err = nvs_commit(nvs);
|
|
nvs_close(nvs);
|
|
|
|
if (err == ESP_OK) {
|
|
g_state.provisioned = true;
|
|
}
|
|
return err;
|
|
}
|