spaxel/firmware/main/provision.c
jedarden 2483d36642
Some checks are pending
CI Benchmark - Fusion Loop Timing / Fusion Loop Timing Benchmark (push) Waiting to run
fix(provision): re-broadcast SPAXEL READY every 1s during window
Previously the ready signal was sent exactly once at window open, so the
browser had to have the serial port open at that exact millisecond after
reboot. Now the firmware broadcasts SPAXEL READY every second for the
full provisioning window (2 min fresh / 15 s reprov), giving the host
ample time to open the port and catch the handshake.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:23:25 -04:00

194 lines
6.6 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).
// Broadcast every 1 s so the host can open the port at any time during
// the window — not just at the exact moment of first boot.
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);
TickType_t last_ready = xTaskGetTickCount();
char line[MAX_LINE_LEN];
int line_pos = 0;
while (xTaskGetTickCount() < deadline) {
// Re-broadcast READY every 1 s so the host can connect at any time
if ((xTaskGetTickCount() - last_ready) >= pdMS_TO_TICKS(1000)) {
uart_write_bytes(PROVISION_UART, ready_msg, strlen(ready_msg));
last_ready = xTaskGetTickCount();
}
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;
}