various bug fixes
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* MTG RFID Companion - Registration web server (SoftAP)
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
@@ -25,28 +26,52 @@ static const char SPA_PAGE[] =
|
||||
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
|
||||
"<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
||||
"<title>MTG RFID Companion</title><style>"
|
||||
"body{font-family:system-ui;background:#1a1a2e;color:#eee;max-width:560px;margin:2em auto;padding:1em}"
|
||||
"h1{color:#ffd75a}.uid{font-size:1.6em;letter-spacing:2px;color:#7fd77f;word-break:break-all}"
|
||||
"input,.btn{font-size:1em;padding:.5em;border-radius:6px;border:1px solid #555;background:#222;color:#eee}"
|
||||
"button{margin:.4em}.sug{list-style:none;padding:0}.sug li{cursor:pointer;padding:.5em;border-bottom:1px solid #444}"
|
||||
"body{font-family:system-ui,-apple-system,sans-serif;background:#141424;color:#eee;max-width:600px;margin:1.5em auto;padding:1em}"
|
||||
"h1{color:#ffd75a;margin-bottom:0.2em}h2{color:#ffd75a;font-size:1.15em;margin-top:1.2em}"
|
||||
".uid{font-size:1.5em;font-family:monospace;letter-spacing:2px;color:#7fd77f;word-break:break-all;background:#1e1e32;padding:.4em .6em;border-radius:6px;border:1px solid #333}"
|
||||
"input{font-size:1em;padding:.5em .8em;border-radius:6px;border:1px solid #555;background:#222;color:#eee;width:calc(100% - 1.6em);box-sizing:border-box}"
|
||||
"button,.btn{font-size:.95em;padding:.5em 1em;border-radius:6px;border:1px solid #555;background:#2a2a44;color:#eee;cursor:pointer;margin:.3em 0}"
|
||||
"button:hover,.btn:hover{background:#3a3a5c}"
|
||||
".btn-primary{background:#ffd75a;color:#111;border:none;font-weight:bold}"
|
||||
".btn-primary:hover{background:#ffe380}"
|
||||
".btn-del{background:#4a1e1e;color:#ff9999;border:1px solid #772222;font-size:.85em;padding:.4em .8em;cursor:pointer;border-radius:6px}"
|
||||
".btn-del:hover{background:#772222;color:#fff}"
|
||||
".sug{list-style:none;padding:0;margin:.4em 0;background:#1f1f33;border-radius:6px;border:1px solid #333}"
|
||||
".sug li{cursor:pointer;padding:.5em .8em;border-bottom:1px solid #333}"
|
||||
".sug li:hover{background:#2a2a48;color:#ffd75a}"
|
||||
".ok{color:#7fd77f}.bad{color:#ff6b6b}"
|
||||
".card-item{display:flex;align-items:center;gap:12px;background:#1e1e32;border:1px solid #33334c;border-radius:8px;padding:8px 12px;margin-bottom:8px}"
|
||||
".card-thumb{width:52px;height:52px;object-fit:cover;border-radius:6px;border:1px solid #444;background:#111;flex-shrink:0}"
|
||||
".card-info{flex:1;min-width:0}"
|
||||
".card-title{font-weight:bold;color:#ffd75a;font-size:1.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}"
|
||||
".card-uid{font-family:monospace;color:#7fd77f;font-size:.85em;margin-top:2px}"
|
||||
".card-none{color:#777;font-style:italic;padding:.8em 0}"
|
||||
".header-row{display:flex;align-items:center;justify-content:space-between}"
|
||||
"</style></head><body>"
|
||||
"<h1> MTG RFID Companion</h1>"
|
||||
"<p>Pair a scanned card with its Scryfall name.</p>"
|
||||
"<h2>Last scanned UID</h2><p class=\"uid\" id=\"uid\">waiting...</p>"
|
||||
"<h2>Card name</h2>"
|
||||
"<input id=\"name\" placeholder=\"Type a card name\" autocomplete=\"off\">"
|
||||
"<button onclick=\"bindCard()\">Pair UID with card</button>"
|
||||
"<h1>MTG RFID Companion</h1>"
|
||||
"<p style=\"color:#aaa;margin-top:0\">Pair card sleeves and manage your scanned deck.</p>"
|
||||
"<div style=\"background:#1a1a2e;padding:1em;border-radius:8px;border:1px solid #2d2d48;margin-bottom:1.5em\">"
|
||||
"<h2 style=\"margin-top:0\">Last Scanned Tag</h2>"
|
||||
"<p class=\"uid\" id=\"uid\">waiting...</p>"
|
||||
"<h2>Pair With Card Name</h2>"
|
||||
"<input id=\"name\" placeholder=\"Type card name (e.g. Black Lotus)\" autocomplete=\"off\">"
|
||||
"<div style=\"margin-top:.6em\"><button class=\"btn-primary\" onclick=\"bindCard()\">Pair Card</button></div>"
|
||||
"<ul class=\"sug\" id=\"res\"></ul>"
|
||||
"<p id=\"status\"></p>"
|
||||
"</div>"
|
||||
"<div class=\"header-row\">"
|
||||
"<h2>Paired Cards (<span id=\"count\">0</span>)</h2>"
|
||||
"<button class=\"btn\" style=\"font-size:.8em\" onclick=\"loadCards()\">Refresh</button>"
|
||||
"</div>"
|
||||
"<div id=\"cards-box\"><p class=\"card-none\">Loading paired cards...</p></div>"
|
||||
"<script>"
|
||||
"var lastUid='';var suggestions=[];"
|
||||
"function poll(){fetch('/api/last_uid').then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.uid&&j.uid!==lastUid){lastUid=j.uid;document.getElementById('uid').textContent=j.uid;"
|
||||
" suggestions=[];document.getElementById('res').innerHTML='';show('','');}})"
|
||||
" .catch(function(){});setTimeout(poll,1000);}"
|
||||
"function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}"
|
||||
"function show(cls,msg){document.getElementById('status').className=cls;document.getElementById('status').textContent=msg;}"
|
||||
"function poll(){fetch('/api/last_uid').then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.uid&&j.uid!==lastUid){lastUid=j.uid;document.getElementById('uid').textContent=j.uid;"
|
||||
" suggestions=[];document.getElementById('res').innerHTML='';show('','');}"
|
||||
"}).catch(function(){});setTimeout(poll,1000);}"
|
||||
"function autocomplete(){var q=document.getElementById('name').value;"
|
||||
" if(q.length<3){suggestions=[];document.getElementById('res').innerHTML='';return;}"
|
||||
" fetch('https://api.scryfall.com/cards/autocomplete?q='+encodeURIComponent(q))"
|
||||
@@ -58,13 +83,38 @@ static const char SPA_PAGE[] =
|
||||
"document.getElementById('name').oninput=autocomplete;"
|
||||
"function bindCard(){var n=document.getElementById('name').value.trim();"
|
||||
" if(!lastUid||!n){show('bad','Scan a card and enter a name first');return;}"
|
||||
" show('','Pairing...');"
|
||||
" fetch('/api/bind',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
|
||||
" body:'uid='+encodeURIComponent(lastUid)+'&name='+encodeURIComponent(n)})"
|
||||
" .then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.ok){show('ok','Paired '+j.uid+' -> '+j.name);document.getElementById('name').value='';"
|
||||
" document.getElementById('res').innerHTML='';}"
|
||||
" document.getElementById('res').innerHTML='';loadCards();}"
|
||||
" else{show('bad','Error: '+j.error);}}).catch(function(e){show('bad','Network: '+e);});}"
|
||||
"poll();"
|
||||
"function unbindCard(u){if(!confirm('Unbind card '+u+'?'))return;"
|
||||
" fetch('/api/unbind',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
|
||||
" body:'uid='+encodeURIComponent(u)})"
|
||||
" .then(function(r){return r.json();}).then(function(j){"
|
||||
" if(j.ok){loadCards();show('ok','Unbound '+u);}"
|
||||
" else{show('bad','Error unbinding '+u);}}).catch(function(e){show('bad','Network: '+e);});}"
|
||||
"function loadCards(){"
|
||||
" fetch('/api/cards').then(function(r){return r.json();}).then(function(data){"
|
||||
" var box=document.getElementById('cards-box');var uids=Object.keys(data);"
|
||||
" document.getElementById('count').textContent=uids.length;"
|
||||
" if(uids.length===0){box.innerHTML='<p class=\"card-none\">No cards paired yet. Scan a card above to begin!</p>';return;}"
|
||||
" uids.sort(function(a,b){return data[a].localeCompare(data[b]);});"
|
||||
" var html='';"
|
||||
" uids.forEach(function(u){"
|
||||
" var nm=esc(data[u]);"
|
||||
" html+='<div class=\"card-item\">';"
|
||||
" html+='<img class=\"card-thumb\" src=\"/api/art?uid='+u+'\" alt=\"art\" onerror=\"this.style.display=\\'none\\'\">';"
|
||||
" html+='<div class=\"card-info\"><div class=\"card-title\">'+nm+'</div><div class=\"card-uid\">'+u+'</div></div>';"
|
||||
" html+='<button class=\"btn-del\" onclick=\"unbindCard(\\''+u+'\\')\">Unbind</button>';"
|
||||
" html+='</div>';"
|
||||
" });"
|
||||
" box.innerHTML=html;"
|
||||
" }).catch(function(e){document.getElementById('cards-box').innerHTML='<p class=\"bad\">Failed to load cards: '+e+'</p>';});"
|
||||
"}"
|
||||
"poll();loadCards();"
|
||||
"</script></body></html>";
|
||||
|
||||
static httpd_handle_t s_server = NULL;
|
||||
@@ -72,6 +122,9 @@ 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
|
||||
@@ -82,6 +135,15 @@ static const httpd_uri_t URI_LAST_UID = {
|
||||
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)
|
||||
{
|
||||
@@ -174,18 +236,29 @@ static esp_err_t handle_bind(httpd_req_t *req)
|
||||
|
||||
char *uid = NULL;
|
||||
char *name = NULL;
|
||||
const char *u = strstr(form, "uid=");
|
||||
const char *n = strstr(form, "name=");
|
||||
char *u = strstr(form, "uid=");
|
||||
char *n = strstr(form, "name=");
|
||||
if (u) {
|
||||
char *end = strchr(u, '&');
|
||||
(void)end;
|
||||
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);
|
||||
@@ -212,6 +285,114 @@ static esp_err_t handle_bind(httpd_req_t *req)
|
||||
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 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -286,7 +467,7 @@ esp_err_t web_server_start(void)
|
||||
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.stack_size = 6 * 1024;
|
||||
config.max_uri_handlers = 8;
|
||||
config.max_uri_handlers = 12;
|
||||
config.lru_purge_enable = true;
|
||||
config.keep_alive_enable = true;
|
||||
config.max_open_sockets = 4;
|
||||
@@ -300,6 +481,9 @@ esp_err_t web_server_start(void)
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user