feat: prove MVP capacity and make the Go/No-Go decision
This commit is contained in:
@@ -268,7 +268,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record,
|
|||||||
|
|
||||||
## Milestone 004 — Prove MVP capacity and make the Go/No-Go decision
|
## Milestone 004 — Prove MVP capacity and make the Go/No-Go decision
|
||||||
|
|
||||||
**Status:** `READY`
|
**Status:** `IN PROGRESS`
|
||||||
**Depends on:** Milestone 003
|
**Depends on:** Milestone 003
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
@@ -298,6 +298,14 @@ Before running the capacity test, define numerical limits based on the board spe
|
|||||||
|
|
||||||
Do not adjust a threshold after seeing the result unless the execution record contains an explicit justification.
|
Do not adjust a threshold after seeing the result unless the execution record contains an explicit justification.
|
||||||
|
|
||||||
|
### Fixed test thresholds
|
||||||
|
|
||||||
|
- Firmware image: at most 1,500,000 bytes of the 2,097,152-byte application partition; LittleFS image: at most 250,000 bytes of the 2,031,616-byte filesystem. These reserve room for the game engine and complete offline interface.
|
||||||
|
- Minimum free heap: at least 96,000 bytes throughout the run. Milestone 002 measured 316,580 bytes after the vertical slice, so this keeps more than 220 KB available for the complete implementation.
|
||||||
|
- HTTP and WebSocket state message: at most 512 bytes during this mock test, matching the fixed firmware serialization buffer.
|
||||||
|
- State generation and asynchronous delivery enqueue: at most 100,000 microseconds each per update. This leaves substantial margin below the 2-second polling interval.
|
||||||
|
- Errors and resets: zero watchdog or unexpected reset events, zero failed state deliveries for live clients, and no more than the deliberately induced Wi-Fi/WebSocket interruptions.
|
||||||
|
|
||||||
### Acceptance criteria
|
### Acceptance criteria
|
||||||
|
|
||||||
- Two players and eight spectators simultaneously receive the state intended for their roles.
|
- Two players and eight spectators simultaneously receive the state intended for their roles.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ board = esp32-c6-devkitm-1
|
|||||||
framework = espidf
|
framework = espidf
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
|
board_build.sdkconfig_defaults = sdkconfig.defaults
|
||||||
lib_deps = https://github.com/joltwallet/esp_littlefs.git#v1.20.4
|
lib_deps = https://github.com/joltwallet/esp_littlefs.git#v1.20.4
|
||||||
monitor_port = /dev/ttyACM0
|
monitor_port = /dev/ttyACM0
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
CONFIG_LWIP_MAX_SOCKETS=16
|
||||||
+207
-22
@@ -24,10 +24,21 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
static const char *const kLogTag = "vertical_slice";
|
static const char *const kLogTag = "vertical_slice";
|
||||||
static const char *const kBuildVersion = "m002";
|
static const char *const kBuildVersion = "m004";
|
||||||
static const char *const kLittlefsBasePath = "/littlefs";
|
static const char *const kLittlefsBasePath = "/littlefs";
|
||||||
static const char *const kLittlefsPartitionLabel = "littlefs";
|
static const char *const kLittlefsPartitionLabel = "littlefs";
|
||||||
static const size_t kFileChunkBytes = 1024;
|
static const size_t kFileChunkBytes = 1024;
|
||||||
|
enum {
|
||||||
|
kCapacityPlayerCount = 2,
|
||||||
|
kCapacitySpectatorCount = 8,
|
||||||
|
kCapacitySessionCount = kCapacityPlayerCount + kCapacitySpectatorCount,
|
||||||
|
kCapacityGames = 20,
|
||||||
|
kCapacityChangesPerGame = 200,
|
||||||
|
kCapacityBoardCells = 100,
|
||||||
|
kCapacityShipsPerBoard = 10,
|
||||||
|
kCapacityTickMs = 25,
|
||||||
|
kMaxStateMessageBytes = 512,
|
||||||
|
};
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool configured;
|
bool configured;
|
||||||
@@ -42,12 +53,40 @@ static wifi_state_t s_wifi_state = {0};
|
|||||||
static bool s_littlefs_mounted;
|
static bool s_littlefs_mounted;
|
||||||
static httpd_handle_t s_server;
|
static httpd_handle_t s_server;
|
||||||
static esp_timer_handle_t s_state_timer;
|
static esp_timer_handle_t s_state_timer;
|
||||||
static uint32_t s_state_version;
|
static portMUX_TYPE s_capacity_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
static uint32_t s_public_counter;
|
|
||||||
|
|
||||||
typedef enum { VIEW_PLAYER_1, VIEW_PLAYER_2, VIEW_SPECTATOR } view_role_t;
|
typedef enum { VIEW_PLAYER_1, VIEW_PLAYER_2, VIEW_SPECTATOR } view_role_t;
|
||||||
typedef struct { int fd; view_role_t role; } ws_client_t;
|
typedef struct { int fd; view_role_t role; } ws_client_t;
|
||||||
static ws_client_t s_ws_clients[4];
|
static ws_client_t s_ws_clients[kCapacitySessionCount];
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t x;
|
||||||
|
uint8_t y;
|
||||||
|
uint8_t length;
|
||||||
|
bool horizontal;
|
||||||
|
uint8_t hits;
|
||||||
|
} mock_ship_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t cells[2][kCapacityBoardCells];
|
||||||
|
mock_ship_t ships[2][kCapacityShipsPerBoard];
|
||||||
|
uint8_t session_ids[kCapacitySessionCount][16];
|
||||||
|
uint16_t completed_games;
|
||||||
|
uint16_t player_wins[kCapacityPlayerCount];
|
||||||
|
uint32_t game_id;
|
||||||
|
uint32_t version;
|
||||||
|
uint16_t changes_in_game;
|
||||||
|
uint8_t current_player;
|
||||||
|
bool running;
|
||||||
|
uint32_t initial_free_heap_bytes;
|
||||||
|
uint32_t minimum_free_heap_bytes;
|
||||||
|
uint32_t maximum_json_bytes;
|
||||||
|
uint32_t maximum_generation_us;
|
||||||
|
uint32_t maximum_delivery_enqueue_us;
|
||||||
|
uint32_t websocket_reconnections;
|
||||||
|
} capacity_state_t;
|
||||||
|
|
||||||
|
static capacity_state_t s_capacity_state;
|
||||||
|
|
||||||
static view_role_t role_from_query(httpd_req_t *request) {
|
static view_role_t role_from_query(httpd_req_t *request) {
|
||||||
char query[48] = {0};
|
char query[48] = {0};
|
||||||
@@ -66,12 +105,29 @@ static const char *role_name(view_role_t role) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void remember_ws_client(int fd, view_role_t role) {
|
static void remember_ws_client(int fd, view_role_t role) {
|
||||||
|
size_t reusable_index = sizeof(s_ws_clients) / sizeof(s_ws_clients[0]);
|
||||||
for (size_t index = 0; index < sizeof(s_ws_clients) / sizeof(s_ws_clients[0]); ++index) {
|
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) {
|
if (s_ws_clients[index].fd == fd) {
|
||||||
s_ws_clients[index] = (ws_client_t){.fd = fd, .role = role};
|
s_ws_clients[index].role = role;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (reusable_index == sizeof(s_ws_clients) / sizeof(s_ws_clients[0]) &&
|
||||||
|
(s_ws_clients[index].fd == 0 ||
|
||||||
|
httpd_ws_get_fd_info(s_server, s_ws_clients[index].fd) != HTTPD_WS_CLIENT_WEBSOCKET)) {
|
||||||
|
reusable_index = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (reusable_index != sizeof(s_ws_clients) / sizeof(s_ws_clients[0])) {
|
||||||
|
const bool reconnect = s_ws_clients[reusable_index].fd != 0;
|
||||||
|
s_ws_clients[reusable_index] = (ws_client_t){.fd = fd, .role = role};
|
||||||
|
if (reconnect) {
|
||||||
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
if (s_capacity_state.running) ++s_capacity_state.websocket_reconnections;
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGW(kLogTag, "websocket client table is full");
|
||||||
}
|
}
|
||||||
|
|
||||||
static view_role_t role_for_fd(int fd) {
|
static view_role_t role_for_fd(int fd) {
|
||||||
@@ -81,16 +137,51 @@ static view_role_t role_for_fd(int fd) {
|
|||||||
return VIEW_SPECTATOR;
|
return VIEW_SPECTATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char public_cell(uint8_t cell, bool reveal_ships) {
|
||||||
|
if (cell == 1 && !reveal_ships) return '0';
|
||||||
|
return (char)('0' + cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int build_state_payload(char *payload, size_t payload_size, view_role_t role) {
|
||||||
|
capacity_state_t snapshot;
|
||||||
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
snapshot = s_capacity_state;
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
|
||||||
|
char board_a[kCapacityBoardCells + 1];
|
||||||
|
char board_b[kCapacityBoardCells + 1];
|
||||||
|
const bool reveal_player_1 = role == VIEW_PLAYER_1;
|
||||||
|
const bool reveal_player_2 = role == VIEW_PLAYER_2;
|
||||||
|
for (size_t index = 0; index < kCapacityBoardCells; ++index) {
|
||||||
|
board_a[index] = public_cell(snapshot.cells[0][index], reveal_player_1);
|
||||||
|
board_b[index] = public_cell(snapshot.cells[1][index], reveal_player_2);
|
||||||
|
}
|
||||||
|
board_a[kCapacityBoardCells] = '\0';
|
||||||
|
board_b[kCapacityBoardCells] = '\0';
|
||||||
|
return snprintf(payload, payload_size,
|
||||||
|
"{\"version\":%" PRIu32 ",\"game_id\":%" PRIu32
|
||||||
|
",\"viewer\":\"%s\",\"phase\":\"capacity\",\"current_player\":%u"
|
||||||
|
",\"boards\":[\"%s\",\"%s\"],\"wins\":[%u,%u]}",
|
||||||
|
snapshot.version, snapshot.game_id, role_name(role), snapshot.current_player,
|
||||||
|
board_a, board_b, snapshot.player_wins[0], snapshot.player_wins[1]);
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t send_state(httpd_handle_t server, int fd, view_role_t role) {
|
static esp_err_t send_state(httpd_handle_t server, int fd, view_role_t role) {
|
||||||
char payload[128];
|
const int64_t started_us = esp_timer_get_time();
|
||||||
const int length = snprintf(payload, sizeof(payload),
|
char payload[kMaxStateMessageBytes];
|
||||||
"{\"version\":%" PRIu32 ",\"public_counter\":%" PRIu32
|
const int length = build_state_payload(payload, sizeof(payload), role);
|
||||||
",\"viewer\":\"%s\"}", s_state_version, s_public_counter,
|
|
||||||
role_name(role));
|
|
||||||
if (length < 0 || (size_t)length >= sizeof(payload)) return ESP_FAIL;
|
if (length < 0 || (size_t)length >= sizeof(payload)) return ESP_FAIL;
|
||||||
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_TEXT,
|
httpd_ws_frame_t frame = {.final = true, .type = HTTPD_WS_TYPE_TEXT,
|
||||||
.payload = (uint8_t *)payload, .len = (size_t)length};
|
.payload = (uint8_t *)payload, .len = (size_t)length};
|
||||||
return httpd_ws_send_frame_async(server, fd, &frame);
|
const esp_err_t result = httpd_ws_send_frame_async(server, fd, &frame);
|
||||||
|
const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - started_us);
|
||||||
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
if ((uint32_t)length > s_capacity_state.maximum_json_bytes) s_capacity_state.maximum_json_bytes = length;
|
||||||
|
if (elapsed_us > s_capacity_state.maximum_delivery_enqueue_us) {
|
||||||
|
s_capacity_state.maximum_delivery_enqueue_us = elapsed_us;
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void broadcast_state(void *unused) {
|
static void broadcast_state(void *unused) {
|
||||||
@@ -107,23 +198,109 @@ static void broadcast_state(void *unused) {
|
|||||||
|
|
||||||
static void state_timer_callback(void *unused) {
|
static void state_timer_callback(void *unused) {
|
||||||
(void)unused;
|
(void)unused;
|
||||||
++s_state_version;
|
bool changed = false;
|
||||||
++s_public_counter;
|
const int64_t started_us = esp_timer_get_time();
|
||||||
if (s_server != NULL) httpd_queue_work(s_server, broadcast_state, NULL);
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
if (s_capacity_state.running) {
|
||||||
|
const uint16_t shot = s_capacity_state.changes_in_game++ % kCapacityBoardCells;
|
||||||
|
const uint8_t target = s_capacity_state.current_player == 0 ? 1 : 0;
|
||||||
|
s_capacity_state.cells[target][shot] = (shot % 5U == 0U) ? 3 : 2;
|
||||||
|
++s_capacity_state.version;
|
||||||
|
s_capacity_state.current_player = (shot % 5U == 0U) ? s_capacity_state.current_player
|
||||||
|
: (uint8_t)(1U - s_capacity_state.current_player);
|
||||||
|
const uint32_t free_heap = esp_get_free_heap_size();
|
||||||
|
if (free_heap < s_capacity_state.minimum_free_heap_bytes) {
|
||||||
|
s_capacity_state.minimum_free_heap_bytes = free_heap;
|
||||||
|
}
|
||||||
|
if (s_capacity_state.changes_in_game == kCapacityChangesPerGame) {
|
||||||
|
++s_capacity_state.completed_games;
|
||||||
|
++s_capacity_state.player_wins[s_capacity_state.completed_games % kCapacityPlayerCount];
|
||||||
|
s_capacity_state.changes_in_game = 0;
|
||||||
|
++s_capacity_state.game_id;
|
||||||
|
memset(s_capacity_state.cells, 0, sizeof(s_capacity_state.cells));
|
||||||
|
if (s_capacity_state.completed_games == kCapacityGames) s_capacity_state.running = false;
|
||||||
|
}
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - started_us);
|
||||||
|
if (elapsed_us > s_capacity_state.maximum_generation_us) s_capacity_state.maximum_generation_us = elapsed_us;
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
if (changed && s_server != NULL) httpd_queue_work(s_server, broadcast_state, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t state_handler(httpd_req_t *request) {
|
static esp_err_t state_handler(httpd_req_t *request) {
|
||||||
char payload[128];
|
char payload[kMaxStateMessageBytes];
|
||||||
const int length = snprintf(payload, sizeof(payload),
|
const int length = build_state_payload(payload, sizeof(payload), role_from_query(request));
|
||||||
"{\"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;
|
if (length < 0 || (size_t)length >= sizeof(payload)) return ESP_FAIL;
|
||||||
httpd_resp_set_type(request, "application/json");
|
httpd_resp_set_type(request, "application/json");
|
||||||
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||||
return httpd_resp_send(request, payload, length);
|
return httpd_resp_send(request, payload, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void make_capacity_state(capacity_state_t *state) {
|
||||||
|
memset(state, 0, sizeof(*state));
|
||||||
|
state->game_id = 1;
|
||||||
|
state->initial_free_heap_bytes = esp_get_free_heap_size();
|
||||||
|
state->minimum_free_heap_bytes = state->initial_free_heap_bytes;
|
||||||
|
for (size_t player = 0; player < 2; ++player) {
|
||||||
|
for (size_t ship = 0; ship < kCapacityShipsPerBoard; ++ship) {
|
||||||
|
state->ships[player][ship] = (mock_ship_t){
|
||||||
|
.x = (uint8_t)((ship * 3U) % 10U), .y = (uint8_t)((ship * 7U) % 10U),
|
||||||
|
.length = (uint8_t)(1U + (ship % 4U)), .horizontal = (ship % 2U) == 0U};
|
||||||
|
state->cells[player][ship * 10U] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (size_t session = 0; session < kCapacitySessionCount; ++session) {
|
||||||
|
for (size_t byte = 0; byte < sizeof(state->session_ids[session]); ++byte) {
|
||||||
|
state->session_ids[session][byte] = (uint8_t)(session * 17U + byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t capacity_run_handler(httpd_req_t *request) {
|
||||||
|
if (request->content_len != 0) {
|
||||||
|
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body");
|
||||||
|
}
|
||||||
|
capacity_state_t fresh_state;
|
||||||
|
make_capacity_state(&fresh_state);
|
||||||
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
if (s_capacity_state.running) {
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
httpd_resp_set_status(request, "409 Conflict");
|
||||||
|
return httpd_resp_send(request, "capacity run already active", HTTPD_RESP_USE_STRLEN);
|
||||||
|
}
|
||||||
|
fresh_state.running = true;
|
||||||
|
s_capacity_state = fresh_state;
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
httpd_resp_set_type(request, "application/json");
|
||||||
|
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||||
|
return httpd_resp_sendstr(request,
|
||||||
|
"{\"ok\":true,\"games\":20,\"changes_per_game\":200,\"sessions\":10}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t capacity_metrics_handler(httpd_req_t *request) {
|
||||||
|
capacity_state_t snapshot;
|
||||||
|
portENTER_CRITICAL(&s_capacity_lock);
|
||||||
|
snapshot = s_capacity_state;
|
||||||
|
portEXIT_CRITICAL(&s_capacity_lock);
|
||||||
|
char response[512];
|
||||||
|
const int length = snprintf(
|
||||||
|
response, sizeof(response),
|
||||||
|
"{\"running\":%s,\"completed_games\":%u,\"changes_in_game\":%u,\"sessions\":10"
|
||||||
|
",\"initial_free_heap_bytes\":%" PRIu32 ",\"current_free_heap_bytes\":%u"
|
||||||
|
",\"minimum_free_heap_bytes\":%" PRIu32 ",\"maximum_json_bytes\":%" PRIu32
|
||||||
|
",\"maximum_generation_us\":%" PRIu32 ",\"maximum_delivery_enqueue_us\":%" PRIu32
|
||||||
|
",\"websocket_reconnections\":%" PRIu32 ",\"reset_reason\":%d}",
|
||||||
|
snapshot.running ? "true" : "false", snapshot.completed_games, snapshot.changes_in_game,
|
||||||
|
snapshot.initial_free_heap_bytes, (unsigned int)esp_get_free_heap_size(),
|
||||||
|
snapshot.minimum_free_heap_bytes, snapshot.maximum_json_bytes, snapshot.maximum_generation_us,
|
||||||
|
snapshot.maximum_delivery_enqueue_us, snapshot.websocket_reconnections, (int)esp_reset_reason());
|
||||||
|
if (length < 0 || (size_t)length >= sizeof(response)) return ESP_FAIL;
|
||||||
|
httpd_resp_set_type(request, "application/json");
|
||||||
|
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||||
|
return httpd_resp_send(request, response, length);
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t websocket_handler(httpd_req_t *request) {
|
static esp_err_t websocket_handler(httpd_req_t *request) {
|
||||||
const int fd = httpd_req_to_sockfd(request);
|
const int fd = httpd_req_to_sockfd(request);
|
||||||
if (request->method == HTTP_GET) {
|
if (request->method == HTTP_GET) {
|
||||||
@@ -393,7 +570,8 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
|||||||
static esp_err_t start_http_server(void) {
|
static esp_err_t start_http_server(void) {
|
||||||
httpd_handle_t server = NULL;
|
httpd_handle_t server = NULL;
|
||||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
config.max_uri_handlers = 5;
|
config.max_uri_handlers = 7;
|
||||||
|
config.max_open_sockets = 12;
|
||||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||||
config.lru_purge_enable = true;
|
config.lru_purge_enable = true;
|
||||||
ESP_RETURN_ON_ERROR(httpd_start(&server, &config), kLogTag, "http server start failed");
|
ESP_RETURN_ON_ERROR(httpd_start(&server, &config), kLogTag, "http server start failed");
|
||||||
@@ -401,12 +579,18 @@ static esp_err_t start_http_server(void) {
|
|||||||
const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler};
|
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 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 state = {.uri = "/api/state", .method = HTTP_GET, .handler = state_handler};
|
||||||
|
const httpd_uri_t capacity_run = {.uri = "/api/capacity/run", .method = HTTP_POST,
|
||||||
|
.handler = capacity_run_handler};
|
||||||
|
const httpd_uri_t capacity_metrics = {.uri = "/api/capacity/metrics", .method = HTTP_GET,
|
||||||
|
.handler = capacity_metrics_handler};
|
||||||
const httpd_uri_t websocket = {.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler,
|
const httpd_uri_t websocket = {.uri = "/ws", .method = HTTP_GET, .handler = websocket_handler,
|
||||||
.is_websocket = true};
|
.is_websocket = true};
|
||||||
const httpd_uri_t static_files = {.uri = "/*", .method = HTTP_GET, .handler = static_file_handler};
|
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, &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, &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, &state), kLogTag, "state route registration failed");
|
||||||
|
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &capacity_run), kLogTag, "capacity run route registration failed");
|
||||||
|
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &capacity_metrics), kLogTag, "capacity metrics 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, &websocket), kLogTag, "websocket route registration failed");
|
||||||
ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &static_files), kLogTag, "static 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);
|
ESP_LOGI(kLogTag, "http server started on port %d", config.server_port);
|
||||||
@@ -444,10 +628,11 @@ void app_main(void) {
|
|||||||
ESP_ERROR_CHECK(esp_netif_init());
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
mount_littlefs();
|
mount_littlefs();
|
||||||
|
make_capacity_state(&s_capacity_state);
|
||||||
ESP_ERROR_CHECK(start_http_server());
|
ESP_ERROR_CHECK(start_http_server());
|
||||||
const esp_timer_create_args_t state_timer_args = {.callback = state_timer_callback, .name = "state_tick"};
|
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_create(&state_timer_args, &s_state_timer));
|
||||||
ESP_ERROR_CHECK(esp_timer_start_periodic(s_state_timer, 5000000));
|
ESP_ERROR_CHECK(esp_timer_start_periodic(s_state_timer, (uint64_t)kCapacityTickMs * 1000U));
|
||||||
result = start_wifi();
|
result = start_wifi();
|
||||||
if (result != ESP_OK) {
|
if (result != ESP_OK) {
|
||||||
ESP_LOGW(kLogTag, "wifi unavailable: %s", esp_err_to_name(result));
|
ESP_LOGW(kLogTag, "wifi unavailable: %s", esp_err_to_name(result));
|
||||||
|
|||||||
Reference in New Issue
Block a user