feat(master): endpoint GET/POST /game/peers #4

Merged
electron merged 1 commits from feat/idf-migration into main 2026-06-10 09:56:10 +00:00
@@ -496,6 +496,130 @@ static esp_err_t handle_scenario_relay_post(httpd_req_t *req) {
return ret;
}
// ─── GET/POST /game/peers ───────────────────────────────────────────────────
// Relay peer registry management over HTTP — replaces the desktop
// NvsConfigurator round-trip for ESP-NOW provisioning.
// POST { "alias": "plip", "mac": "AA:BB:CC:DD:EE:FF" }
// → persists to NVS namespace "peers" (key = alias, blob = 6-byte MAC,
// the exact format main.c seeds from at boot) AND registers the peer
// live in the scenario_mesh table (no reboot needed).
// GET → { "peers": [ { "alias": "...", "mac": "..." }, ... ] }
static esp_err_t handle_peers_post(httpd_req_t *req) {
if (req->content_len <= 0 || req->content_len > 256) {
return send_error(req, "413 Payload Too Large",
"body must be 1..256 bytes");
}
char body[257];
int total = 0;
while (total < (int) req->content_len) {
int got = httpd_req_recv(req, body + total, req->content_len - total);
if (got <= 0) {
if (got == HTTPD_SOCK_ERR_TIMEOUT) continue;
return send_error(req, "400 Bad Request", "recv failed");
}
total += got;
}
body[total] = '\0';
cJSON *root = cJSON_Parse(body);
if (!root) {
return send_error(req, "400 Bad Request", "invalid JSON");
}
const cJSON *alias_j = cJSON_GetObjectItem(root, "alias");
const cJSON *mac_j = cJSON_GetObjectItem(root, "mac");
if (!cJSON_IsString(alias_j) || !cJSON_IsString(mac_j)) {
cJSON_Delete(root);
return send_error(req, "400 Bad Request",
"alias and mac (string) required");
}
const char *alias = alias_j->valuestring;
size_t alias_len = strlen(alias);
if (alias_len == 0 || alias_len > 15) { // NVS key limit
cJSON_Delete(root);
return send_error(req, "400 Bad Request",
"alias must be 1..15 chars (NVS key)");
}
uint8_t mac[6];
if (sscanf(mac_j->valuestring, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) {
cJSON_Delete(root);
return send_error(req, "400 Bad Request",
"mac must be AA:BB:CC:DD:EE:FF");
}
nvs_handle_t h;
esp_err_t err = nvs_open("peers", NVS_READWRITE, &h);
if (err == ESP_OK) {
err = nvs_set_blob(h, alias, mac, sizeof(mac));
if (err == ESP_OK) err = nvs_commit(h);
nvs_close(h);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "peers: NVS write \"%s\" failed: %s",
alias, esp_err_to_name(err));
cJSON_Delete(root);
return send_error(req, "500 Internal Server Error", "NVS write failed");
}
esp_err_t reg = scenario_mesh_register_peer(alias, mac);
if (reg != ESP_OK) {
ESP_LOGW(TAG, "peers: \"%s\" stored in NVS but live registration "
"failed: %s (effective after reboot)",
alias, esp_err_to_name(reg));
}
ESP_LOGI(TAG, "peers: \"%s\" -> %02X:%02X:%02X:%02X:%02X:%02X (%s)",
alias, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5],
reg == ESP_OK ? "live" : "reboot needed");
cJSON_Delete(root);
char resp[128];
snprintf(resp, sizeof(resp),
"{\"ok\":true,\"alias\":\"%s\",\"live\":%s}",
alias, reg == ESP_OK ? "true" : "false");
return send_json(req, "200 OK", resp);
}
static esp_err_t handle_peers_get(httpd_req_t *req) {
cJSON *resp = cJSON_CreateObject();
cJSON *arr = cJSON_AddArrayToObject(resp, "peers");
nvs_handle_t h;
if (nvs_open("peers", NVS_READONLY, &h) == ESP_OK) {
nvs_iterator_t it = NULL;
esp_err_t err = nvs_entry_find("nvs", "peers", NVS_TYPE_BLOB, &it);
while (err == ESP_OK && it != NULL) {
nvs_entry_info_t info;
nvs_entry_info(it, &info);
uint8_t mac[6];
size_t len = sizeof(mac);
if (nvs_get_blob(h, info.key, mac, &len) == ESP_OK && len == 6) {
cJSON *p = cJSON_CreateObject();
cJSON_AddStringToObject(p, "alias", info.key);
char mac_str[18];
snprintf(mac_str, sizeof(mac_str),
"%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
cJSON_AddStringToObject(p, "mac", mac_str);
cJSON_AddItemToArray(arr, p);
}
err = nvs_entry_next(&it);
}
nvs_release_iterator(it);
nvs_close(h);
}
char *resp_str = cJSON_PrintUnformatted(resp);
cJSON_Delete(resp);
if (!resp_str) {
return send_error(req, "500 Internal Server Error",
"response serialize failed");
}
esp_err_t ret = send_json(req, "200 OK", resp_str);
cJSON_free(resp_str);
return ret;
}
// ─── public init ────────────────────────────────────────────────────────────
esp_err_t game_endpoint_init(httpd_handle_t server) {
@@ -529,6 +653,18 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
.handler = handle_scenario_relay_post,
.user_ctx = NULL,
};
static const httpd_uri_t uri_peers_get = {
.uri = "/game/peers",
.method = HTTP_GET,
.handler = handle_peers_get,
.user_ctx = NULL,
};
static const httpd_uri_t uri_peers_post = {
.uri = "/game/peers",
.method = HTTP_POST,
.handler = handle_peers_post,
.user_ctx = NULL,
};
// Bring up the ESP-NOW mesh transport. The master is primarily a sender
// (the relay handler) but we also register the apply adapter so a peer
@@ -567,9 +703,19 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
esp_err_to_name(err));
}
}
// Peer registry management — non-fatal if registration fails.
err = httpd_register_uri_handler(server, &uri_peers_get);
if (err != ESP_OK) {
ESP_LOGW(TAG, "register GET /game/peers: %s", esp_err_to_name(err));
}
err = httpd_register_uri_handler(server, &uri_peers_post);
if (err != ESP_OK) {
ESP_LOGW(TAG, "register POST /game/peers: %s", esp_err_to_name(err));
}
ESP_LOGI(TAG, "game endpoint registered "
"(GET+POST /game/group_profile, POST /game/scenario%s)",
"(GET+POST /game/group_profile, POST /game/scenario%s, "
"GET+POST /game/peers)",
mesh_err == ESP_OK ? ", POST /game/scenario/relay" : "");
return ESP_OK;
}