diff --git a/idf_zacus/components/game_endpoint/game_endpoint.c b/idf_zacus/components/game_endpoint/game_endpoint.c index 4210752..9f55881 100644 --- a/idf_zacus/components/game_endpoint/game_endpoint.c +++ b/idf_zacus/components/game_endpoint/game_endpoint.c @@ -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; }