feat: prove real-time transport, fallback, and state isolation
This commit is contained in:
+110
-1
@@ -40,6 +40,106 @@ typedef struct {
|
||||
static portMUX_TYPE s_wifi_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static wifi_state_t s_wifi_state = {0};
|
||||
static bool s_littlefs_mounted;
|
||||
static httpd_handle_t s_server;
|
||||
static esp_timer_handle_t s_state_timer;
|
||||
static uint32_t s_state_version;
|
||||
static uint32_t s_public_counter;
|
||||
|
||||
typedef enum { VIEW_PLAYER_1, VIEW_PLAYER_2, VIEW_SPECTATOR } view_role_t;
|
||||
typedef struct { int fd; view_role_t role; } ws_client_t;
|
||||
static ws_client_t s_ws_clients[4];
|
||||
|
||||
static view_role_t role_from_query(httpd_req_t *request) {
|
||||
char query[48] = {0};
|
||||
char role[16] = {0};
|
||||
if (httpd_req_get_url_query_len(request) < sizeof(query) &&
|
||||
httpd_req_get_url_query_str(request, query, sizeof(query)) == ESP_OK &&
|
||||
httpd_query_key_value(query, "role", role, sizeof(role)) == ESP_OK) {
|
||||
if (strcmp(role, "player1") == 0) return VIEW_PLAYER_1;
|
||||
if (strcmp(role, "player2") == 0) return VIEW_PLAYER_2;
|
||||
}
|
||||
return VIEW_SPECTATOR;
|
||||
}
|
||||
|
||||
static const char *role_name(view_role_t role) {
|
||||
return role == VIEW_PLAYER_1 ? "player1" : role == VIEW_PLAYER_2 ? "player2" : "spectator";
|
||||
}
|
||||
|
||||
static void remember_ws_client(int fd, view_role_t role) {
|
||||
for (size_t index = 0; index < sizeof(s_ws_clients) / sizeof(s_ws_clients[0]); ++index) {
|
||||
if (s_ws_clients[index].fd == fd || s_ws_clients[index].fd == 0) {
|
||||
s_ws_clients[index] = (ws_client_t){.fd = fd, .role = role};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static view_role_t role_for_fd(int fd) {
|
||||
for (size_t index = 0; index < sizeof(s_ws_clients) / sizeof(s_ws_clients[0]); ++index) {
|
||||
if (s_ws_clients[index].fd == fd) return s_ws_clients[index].role;
|
||||
}
|
||||
return VIEW_SPECTATOR;
|
||||
}
|
||||
|
||||
static esp_err_t send_state(httpd_handle_t server, int fd, view_role_t role) {
|
||||
char payload[128];
|
||||
const int length = snprintf(payload, sizeof(payload),
|
||||
"{\"version\":%" PRIu32 ",\"public_counter\":%" PRIu32
|
||||
",\"viewer\":\"%s\"}", s_state_version, s_public_counter,
|
||||
role_name(role));
|
||||
if (length < 0 || (size_t)length >= sizeof(payload)) return ESP_FAIL;
|
||||
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t *)payload, .len = (size_t)length};
|
||||
return httpd_ws_send_frame_async(server, fd, &frame);
|
||||
}
|
||||
|
||||
static void broadcast_state(void *unused) {
|
||||
(void)unused;
|
||||
size_t count = 4;
|
||||
int clients[4];
|
||||
if (httpd_get_client_list(s_server, &count, clients) != ESP_OK) return;
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
if (httpd_ws_get_fd_info(s_server, clients[index]) == HTTPD_WS_CLIENT_WEBSOCKET) {
|
||||
send_state(s_server, clients[index], role_for_fd(clients[index]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void state_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
++s_state_version;
|
||||
++s_public_counter;
|
||||
if (s_server != NULL) httpd_queue_work(s_server, broadcast_state, NULL);
|
||||
}
|
||||
|
||||
static esp_err_t state_handler(httpd_req_t *request) {
|
||||
char payload[128];
|
||||
const int length = snprintf(payload, sizeof(payload),
|
||||
"{\"version\":%" PRIu32 ",\"public_counter\":%" PRIu32
|
||||
",\"viewer\":\"%s\"}", s_state_version, s_public_counter,
|
||||
role_name(role_from_query(request)));
|
||||
if (length < 0 || (size_t)length >= sizeof(payload)) return ESP_FAIL;
|
||||
httpd_resp_set_type(request, "application/json");
|
||||
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
return httpd_resp_send(request, payload, length);
|
||||
}
|
||||
|
||||
static esp_err_t websocket_handler(httpd_req_t *request) {
|
||||
const int fd = httpd_req_to_sockfd(request);
|
||||
if (request->method == HTTP_GET) {
|
||||
remember_ws_client(fd, role_from_query(request));
|
||||
return ESP_OK;
|
||||
}
|
||||
httpd_ws_frame_t frame = {0};
|
||||
ESP_RETURN_ON_ERROR(httpd_ws_recv_frame(request, &frame, 0), kLogTag, "websocket frame read failed");
|
||||
if (frame.len > 64 || frame.type != HTTPD_WS_TYPE_TEXT) return ESP_OK;
|
||||
char message[65] = {0};
|
||||
frame.payload = (uint8_t *)message;
|
||||
frame.len = sizeof(message) - 1;
|
||||
ESP_RETURN_ON_ERROR(httpd_ws_recv_frame(request, &frame, frame.len), kLogTag, "websocket payload read failed");
|
||||
if (strcmp(message, "{\"type\":\"ping\"}") != 0) return ESP_OK;
|
||||
return send_state(request->handle, fd, role_for_fd(fd));
|
||||
}
|
||||
|
||||
static wifi_state_t wifi_state_snapshot(void) {
|
||||
wifi_state_t snapshot;
|
||||
@@ -293,15 +393,21 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
||||
static esp_err_t start_http_server(void) {
|
||||
httpd_handle_t server = NULL;
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 3;
|
||||
config.max_uri_handlers = 5;
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.lru_purge_enable = true;
|
||||
ESP_RETURN_ON_ERROR(httpd_start(&server, &config), kLogTag, "http server start failed");
|
||||
s_server = server;
|
||||
const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler};
|
||||
const httpd_uri_t health = {.uri = "/api/health", .method = HTTP_GET, .handler = health_handler};
|
||||
const httpd_uri_t state = {.uri = "/api/state", .method = HTTP_GET, .handler = state_handler};
|
||||
const httpd_uri_t websocket = {.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler,
|
||||
.is_websocket = true};
|
||||
const httpd_uri_t static_files = {.uri = "/*", .method = HTTP_GET, .handler = static_file_handler};
|
||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &root), kLogTag, "root route registration failed");
|
||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &health), kLogTag, "health route registration failed");
|
||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &state), kLogTag, "state route registration failed");
|
||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &websocket), kLogTag, "websocket route registration failed");
|
||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &static_files), kLogTag, "static route registration failed");
|
||||
ESP_LOGI(kLogTag, "http server started on port %d", config.server_port);
|
||||
return ESP_OK;
|
||||
@@ -339,6 +445,9 @@ void app_main(void) {
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
mount_littlefs();
|
||||
ESP_ERROR_CHECK(start_http_server());
|
||||
const esp_timer_create_args_t state_timer_args = {.callback = state_timer_callback, .name = "state_tick"};
|
||||
ESP_ERROR_CHECK(esp_timer_create(&state_timer_args, &s_state_timer));
|
||||
ESP_ERROR_CHECK(esp_timer_start_periodic(s_state_timer, 5000000));
|
||||
result = start_wifi();
|
||||
if (result != ESP_OK) {
|
||||
ESP_LOGW(kLogTag, "wifi unavailable: %s", esp_err_to_name(result));
|
||||
|
||||
Reference in New Issue
Block a user