Phase 6: SoftAP MTG-Companion web server, /api/last_uid + /api/bind UID pairing, embedded pairing SPA
This commit is contained in:
@@ -7,5 +7,6 @@ idf_component_register(SRCS
|
|||||||
"ui_task.c"
|
"ui_task.c"
|
||||||
"wifi_manager.c"
|
"wifi_manager.c"
|
||||||
"scryfall_client.c"
|
"scryfall_client.c"
|
||||||
|
"web_server.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
)
|
)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "ui_task.h"
|
#include "ui_task.h"
|
||||||
#include "wifi_manager.h"
|
#include "wifi_manager.h"
|
||||||
#include "scryfall_client.h"
|
#include "scryfall_client.h"
|
||||||
|
#include "web_server.h"
|
||||||
|
|
||||||
static const char *TAG = "MAIN";
|
static const char *TAG = "MAIN";
|
||||||
|
|
||||||
@@ -26,10 +27,11 @@ void app_main(void)
|
|||||||
ESP_ERROR_CHECK(wifi_manager_init());
|
ESP_ERROR_CHECK(wifi_manager_init());
|
||||||
ESP_ERROR_CHECK(wifi_manager_start());
|
ESP_ERROR_CHECK(wifi_manager_start());
|
||||||
ESP_ERROR_CHECK(scryfall_client_start());
|
ESP_ERROR_CHECK(scryfall_client_start());
|
||||||
|
ESP_ERROR_CHECK(web_server_start());
|
||||||
|
|
||||||
ESP_ERROR_CHECK(ui_task_start());
|
ESP_ERROR_CHECK(ui_task_start());
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 6+.");
|
ESP_LOGI(TAG, "Boot sequence complete. Ready for Phase 7+.");
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <sys/unistd.h>
|
#include <sys/unistd.h>
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_littlefs.h"
|
#include "esp_littlefs.h"
|
||||||
|
#include "cJSON.h"
|
||||||
#include "storage_manager.h"
|
#include "storage_manager.h"
|
||||||
|
|
||||||
static const char *TAG = "storage";
|
static const char *TAG = "storage";
|
||||||
@@ -109,6 +110,64 @@ esp_err_t storage_write_text(const char *path, const char *text)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t storage_manager_bind_uid(const char *uid, const char *card_name)
|
||||||
|
{
|
||||||
|
cJSON *root = NULL;
|
||||||
|
char *existing = NULL;
|
||||||
|
size_t existing_len = 0;
|
||||||
|
|
||||||
|
if (storage_read_text(STORAGE_MAPPINGS, &existing, &existing_len) == ESP_OK && existing != NULL) {
|
||||||
|
root = cJSON_Parse(existing);
|
||||||
|
}
|
||||||
|
free(existing);
|
||||||
|
|
||||||
|
if (root == NULL || !cJSON_IsObject(root)) {
|
||||||
|
if (root) cJSON_Delete(root);
|
||||||
|
root = cJSON_CreateObject();
|
||||||
|
}
|
||||||
|
if (root == NULL) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON_DeleteItemFromObject(root, uid);
|
||||||
|
cJSON_AddItemToObject(root, uid, cJSON_CreateString(card_name));
|
||||||
|
|
||||||
|
char *out = cJSON_Print(root);
|
||||||
|
esp_err_t ret = out ? storage_write_text(STORAGE_MAPPINGS, out) : ESP_ERR_NO_MEM;
|
||||||
|
free(out);
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t storage_manager_lookup_name(const char *uid, char **out)
|
||||||
|
{
|
||||||
|
*out = NULL;
|
||||||
|
char *buf = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
if (storage_read_text(STORAGE_MAPPINGS, &buf, &len) != ESP_OK || buf == NULL) {
|
||||||
|
free(buf);
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *root = cJSON_Parse(buf);
|
||||||
|
free(buf);
|
||||||
|
if (root == NULL || !cJSON_IsObject(root)) {
|
||||||
|
if (root) cJSON_Delete(root);
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *item = cJSON_GetObjectItem(root, uid);
|
||||||
|
esp_err_t ret = (item && cJSON_IsString(item) && item->valuestring) ? ESP_OK : ESP_ERR_NOT_FOUND;
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
*out = strdup(item->valuestring);
|
||||||
|
if (*out == NULL) {
|
||||||
|
ret = ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t seed_defaults(void)
|
static esp_err_t seed_defaults(void)
|
||||||
{
|
{
|
||||||
/* Create /spiffs/cards directory */
|
/* Create /spiffs/cards directory */
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ esp_err_t storage_read_text(const char *path, char **out_buf, size_t *out_len);
|
|||||||
*/
|
*/
|
||||||
esp_err_t storage_write_text(const char *path, const char *text);
|
esp_err_t storage_write_text(const char *path, const char *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Add/replace a UID->card-name mapping in /spiffs/mappings.json.
|
||||||
|
*/
|
||||||
|
esp_err_t storage_manager_bind_uid(const char *uid, const char *card_name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lookup a card name for a UID in mappings.json. Caller frees *out.
|
||||||
|
* @return ESP_OK and non-NULL *out on success; ESP_ERR_NOT_FOUND otherwise.
|
||||||
|
*/
|
||||||
|
esp_err_t storage_manager_lookup_name(const char *uid, char **out);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
304
main/web_server.c
Normal file
304
main/web_server.c
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
/*
|
||||||
|
* MTG RFID Companion - Registration web server (SoftAP)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#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 AP_IP "192.168.4.1"
|
||||||
|
#define AP_MASK "255.255.255.0"
|
||||||
|
|
||||||
|
#define HTTPD_BODY_MAX (2048)
|
||||||
|
|
||||||
|
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}"
|
||||||
|
".ok{color:#7fd77f}.bad{color:#ff6b6b}"
|
||||||
|
"</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>"
|
||||||
|
"<ul class=\"sug\" id=\"res\"></ul>"
|
||||||
|
"<p id=\"status\"></p>"
|
||||||
|
"<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 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))"
|
||||||
|
" .then(function(r){return r.json();}).then(function(j){"
|
||||||
|
" suggestions=(j.data||[]).slice(0,8);var h='';"
|
||||||
|
" suggestions.forEach(function(n,i){h+='<li onclick=\"pick('+i+')\">'+esc(n)+'</li>';});"
|
||||||
|
" document.getElementById('res').innerHTML=h;}).catch(function(){});}"
|
||||||
|
"function pick(i){document.getElementById('name').value=suggestions[i];document.getElementById('res').innerHTML='';}"
|
||||||
|
"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;}"
|
||||||
|
" 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='';}"
|
||||||
|
" else{show('bad','Error: '+j.error);}}).catch(function(e){show('bad','Network: '+e);});}"
|
||||||
|
"poll();"
|
||||||
|
"</script></body></html>";
|
||||||
|
|
||||||
|
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 %s, web server ready", AP_SSID, AP_IP);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool web_server_is_running(void)
|
||||||
|
{
|
||||||
|
return s_server != NULL;
|
||||||
|
}
|
||||||
28
main/web_server.h
Normal file
28
main/web_server.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* MTG RFID Companion - Registration web server (SoftAP)
|
||||||
|
*
|
||||||
|
* Phase 6: Hosts the SoftAP "MTG-Companion" (192.168.4.1) and serves the
|
||||||
|
* card-pairing SPA plus /api/last_uid and /api/bind endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start the SoftAP and HTTP server. Idempotent.
|
||||||
|
*/
|
||||||
|
esp_err_t web_server_start(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief True if the HTTP server is running.
|
||||||
|
*/
|
||||||
|
bool web_server_is_running(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user