/* * MTG RFID Companion - Registration web server (SoftAP) */ #include #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 card sleeves and manage your scanned deck.

" "
" "

Last Scanned Tag

" "

waiting...

" "

Pair With Card Name

" "" "
" "
    " "

    " "
    " "
    " "

    Paired Cards (0)

    " "" "
    " "

    Loading paired cards...

    " ""; 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 esp_err_t handle_cards(httpd_req_t *req); static esp_err_t handle_art(httpd_req_t *req); static esp_err_t handle_unbind(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 const httpd_uri_t URI_CARDS = { .uri = "/api/cards", .method = HTTP_GET, .handler = handle_cards, .user_ctx = NULL }; static const httpd_uri_t URI_ART = { .uri = "/api/art", .method = HTTP_GET, .handler = handle_art, .user_ctx = NULL }; static const httpd_uri_t URI_UNBIND = { .uri = "/api/unbind", .method = HTTP_POST, .handler = handle_unbind, .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; char *u = strstr(form, "uid="); char *n = strstr(form, "name="); if (u) { char *end = strchr(u, '&'); if (end) *end = '\0'; uid = url_decode(u + 4); if (end) *end = '&'; } if (n) { char *end = strchr(n, '&'); if (end) *end = '\0'; name = url_decode(n + 5); if (end) *end = '&'; } free(form); if (uid) { size_t ulen = strlen(uid); while (ulen > 0 && (uid[ulen - 1] == ' ' || uid[ulen - 1] == '\r' || uid[ulen - 1] == '\n')) { uid[--ulen] = '\0'; } } 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; } static esp_err_t handle_cards(httpd_req_t *req) { char *text = NULL; size_t len = 0; if (storage_read_text(STORAGE_MAPPINGS, &text, &len) == ESP_OK && text != NULL) { httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, text, (ssize_t)len); free(text); } else { send_json(req, 200, "{}\n"); } return ESP_OK; } static esp_err_t handle_art(httpd_req_t *req) { char query[64] = {0}; char uid[32] = {0}; if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { httpd_query_key_value(query, "uid", uid, sizeof(uid)); } if (uid[0] == '\0') { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing uid"); return ESP_OK; } char path[64]; snprintf(path, sizeof(path), STORAGE_CARDS_DIR "/%s.jpg", uid); FILE *f = fopen(path, "rb"); if (!f) { httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Art not found"); return ESP_OK; } httpd_resp_set_type(req, "image/jpeg"); httpd_resp_set_hdr(req, "Cache-Control", "public, max-age=86400"); char chunk[1024]; size_t n = 0; while ((n = fread(chunk, 1, sizeof(chunk), f)) > 0) { if (httpd_resp_send_chunk(req, chunk, (ssize_t)n) != ESP_OK) { fclose(f); return ESP_FAIL; } } fclose(f); httpd_resp_send_chunk(req, NULL, 0); return ESP_OK; } static esp_err_t handle_unbind(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 *u = strstr(form, "uid="); if (u) { char *end = strchr(u, '&'); if (end) *end = '\0'; uid = url_decode(u + 4); if (end) *end = '&'; } free(form); if (uid) { size_t ulen = strlen(uid); while (ulen > 0 && (uid[ulen - 1] == ' ' || uid[ulen - 1] == '\r' || uid[ulen - 1] == '\n')) { uid[--ulen] = '\0'; } } if (uid == NULL || strlen(uid) == 0) { free(uid); send_json(req, 400, "{\"ok\":false,\"error\":\"missing uid\"}"); return ESP_OK; } esp_err_t ret = storage_manager_unbind_uid(uid); if (ret != ESP_OK) { free(uid); send_json(req, 404, "{\"ok\":false,\"error\":\"uid not found\"}"); return ESP_OK; } char ok[128]; snprintf(ok, sizeof(ok), "{\"ok\":true,\"uid\":\"%s\"}\n", uid); ESP_LOGI(TAG, "unbound UID %s via web", uid); send_json(req, 200, ok); free(uid); 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) }, }; /* The AP netif has DHCP running by default; stop it before assigning a * static IP, otherwise esp_netif_set_ip_info fails with * ESP_ERR_ESP_NETIF_DHCP_NOT_STOPPED. */ esp_netif_dhcps_stop(ap); ret = esp_netif_set_ip_info(ap, &ip_info); if (ret != ESP_OK) { ESP_LOGE(TAG, "set AP IP failed: %s", esp_err_to_name(ret)); return ret; } ret = esp_wifi_start(); if (ret != ESP_OK) { ESP_LOGE(TAG, "wifi start (APSTA) failed: %s", esp_err_to_name(ret)); return ret; } (void)esp_netif_dhcps_start(ap); /* harmless if someone else started it */ httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.stack_size = 6 * 1024; config.max_uri_handlers = 12; 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_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_CARDS)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_ART)); ESP_ERROR_CHECK(httpd_register_uri_handler(s_server, &URI_UNBIND)); 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; }