/* * MTG RFID Companion - Registration web server (SoftAP) */ #include #include #include "esp_log.h" #include "esp_wifi.h" #include "esp_netif.h" #include "esp_http_server.h" #include "cJSON.h" #include "rfid_manager.h" #include "storage_manager.h" #include "scryfall_client.h" #include "web_server.h" static const char *TAG = "web"; #define AP_SSID "MTG-Companion" #define AP_PASS "" #define HTTPD_BODY_MAX (2048) static const char SPA_PAGE[] = "" "" "MTG RFID Companion" "

MTG RFID Companion

" "

Pair a scanned card with its Scryfall name.

" "

Last scanned UID

waiting...

" "

Card name

" "" "" "" "

" ""; static httpd_handle_t s_server = NULL; static esp_err_t handle_root(httpd_req_t *req); static esp_err_t handle_last_uid(httpd_req_t *req); static esp_err_t handle_bind(httpd_req_t *req); static const httpd_uri_t URI_ROOT = { .uri = "/", .method = HTTP_GET, .handler = handle_root, .user_ctx = NULL }; static const httpd_uri_t URI_LAST_UID = { .uri = "/api/last_uid", .method = HTTP_GET, .handler = handle_last_uid, .user_ctx = NULL }; static const httpd_uri_t URI_BIND = { .uri = "/api/bind", .method = HTTP_POST, .handler = handle_bind, .user_ctx = NULL }; static void send_json(httpd_req_t *req, int status, const char *body) { httpd_resp_set_status(req, status == 200 ? "200 OK" : "400 Bad Request"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, body, -1); } static char *url_decode(const char *src) { if (src == NULL) { return NULL; } size_t len = strlen(src); if (len > HTTPD_BODY_MAX - 1) { len = HTTPD_BODY_MAX - 1; } char *out = malloc(len + 1); if (out == NULL) { return NULL; } size_t o = 0; for (size_t i = 0; i < len && src[i] != '\0'; i++) { if (src[i] == '%' && i + 2 < len) { int hi = src[i + 1] >= 'a' ? src[i + 1] - 'a' + 10 : src[i + 1] >= 'A' ? src[i + 1] - 'A' + 10 : src[i + 1] - '0'; int lo = src[i + 2] >= 'a' ? src[i + 2] - 'a' + 10 : src[i + 2] >= 'A' ? src[i + 2] - 'A' + 10 : src[i + 2] - '0'; if (hi < 0 || hi > 15 || lo < 0 || lo > 15) { free(out); return NULL; } out[o++] = (char)((hi << 4) | lo); i += 2; } else if (src[i] == '+') { out[o++] = ' '; } else { out[o++] = src[i]; } } out[o] = '\0'; return out; } /* ------------------------------------------------------------------ */ /* Endpoints */ /* ------------------------------------------------------------------ */ static esp_err_t handle_root(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); httpd_resp_send(req, SPA_PAGE, (int)sizeof(SPA_PAGE) - 1); return ESP_OK; } static esp_err_t handle_last_uid(httpd_req_t *req) { char buf[80]; const char *uid = rfid_manager_last_uid(); if (uid == NULL || strlen(uid) == 0) { snprintf(buf, sizeof(buf), "{\"uid\":\"\"}\n"); } else { snprintf(buf, sizeof(buf), "{\"uid\":\"%s\"}\n", uid); } send_json(req, 200, buf); return ESP_OK; } static esp_err_t handle_bind(httpd_req_t *req) { if (req->content_len <= 0 || req->content_len >= HTTPD_BODY_MAX) { send_json(req, 400, "{\"ok\":false,\"error\":\"bad body\"}"); return ESP_OK; } char *form = malloc((size_t)req->content_len + 1); if (form == NULL) { send_json(req, 500, "{\"ok\":false,\"error\":\"no mem\"}"); return ESP_OK; } int got = httpd_req_recv(req, form, (size_t)req->content_len); if (got != req->content_len) { free(form); send_json(req, 400, "{\"ok\":false,\"error\":\"short body\"}"); return ESP_OK; } form[got] = '\0'; char *uid = NULL; char *name = NULL; const char *u = strstr(form, "uid="); const char *n = strstr(form, "name="); if (u) { char *end = strchr(u, '&'); (void)end; uid = url_decode(u + 4); } if (n) { name = url_decode(n + 5); } free(form); if (uid == NULL || name == NULL || strlen(uid) == 0 || strlen(name) == 0) { free(uid); free(name); send_json(req, 400, "{\"ok\":false,\"error\":\"missing uid or name\"}"); return ESP_OK; } esp_err_t ret = storage_manager_bind_uid(uid, name); if (ret != ESP_OK) { free(uid); free(name); send_json(req, 400, "{\"ok\":false,\"error\":\"persist failed\"}"); return ESP_OK; } scryfall_fetch_request(name, uid); char ok[192]; snprintf(ok, sizeof(ok), "{\"ok\":true,\"uid\":\"%s\",\"name\":\"%s\"}\n", uid, name); ESP_LOGI(TAG, "paired UID %s -> %s", uid, name); send_json(req, 200, ok); free(uid); free(name); return ESP_OK; } /* ------------------------------------------------------------------ */ /* SoftAP + server */ /* ------------------------------------------------------------------ */ esp_err_t web_server_start(void) { if (s_server != NULL) { return ESP_OK; } /* AP netif must exist before wifi start */ esp_netif_t *ap = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); if (ap == NULL) { ap = esp_netif_create_default_wifi_ap(); if (ap == NULL) { ESP_LOGE(TAG, "failed to create AP netif"); return ESP_FAIL; } } esp_err_t ret = esp_wifi_stop(); /* safe even if not started */ if (ret != ESP_OK && ret != ESP_ERR_WIFI_NOT_STARTED) { ESP_LOGE(TAG, "wifi stop failed: %s", esp_err_to_name(ret)); return ret; } ret = esp_wifi_set_mode(WIFI_MODE_APSTA); if (ret != ESP_OK) { ESP_LOGE(TAG, "set mode APSTA failed: %s", esp_err_to_name(ret)); return ret; } wifi_config_t wifi_cfg = { .ap = { .ssid_len = sizeof(AP_SSID) - 1, .max_connection = 4, .authmode = (AP_PASS[0] == '\0') ? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK, }, }; strncpy((char *)wifi_cfg.ap.ssid, AP_SSID, sizeof(wifi_cfg.ap.ssid) - 1); if (AP_PASS[0] != '\0') { strncpy((char *)wifi_cfg.ap.password, AP_PASS, sizeof(wifi_cfg.ap.password) - 1); } ret = esp_wifi_set_config(WIFI_IF_AP, &wifi_cfg); if (ret != ESP_OK) { ESP_LOGE(TAG, "set AP config failed: %s", esp_err_to_name(ret)); return ret; } esp_netif_ip_info_t ip_info = { .ip = { .addr = ESP_IP4TOADDR(192, 168, 4, 1) }, .netmask = { .addr = ESP_IP4TOADDR(255, 255, 255, 0) }, .gw = { .addr = ESP_IP4TOADDR(192, 168, 4, 1) }, }; ESP_ERROR_CHECK(esp_netif_set_ip_info(ap, &ip_info)); ret = esp_wifi_start(); if (ret != ESP_OK) { ESP_LOGE(TAG, "wifi start (APSTA) failed: %s", esp_err_to_name(ret)); return ret; } ESP_ERROR_CHECK(esp_netif_dhcps_start(ap)); httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 6 * 1024; config.max_uri_handlers = 8; config.lru_purge_enable = true; config.keep_alive_enable = true; config.max_open_sockets = 4; ret = httpd_start(&s_server, &config); if (ret != ESP_OK) { ESP_LOGE(TAG, "httpd_start failed: %s", esp_err_to_name(ret)); return ret; } ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_ROOT)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_LAST_UID)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_BIND)); ESP_LOGI(TAG, "SoftAP '%s' at 192.168.4.1, web server ready", AP_SSID); return ESP_OK; } bool web_server_is_running(void) { return s_server != NULL; }