feat: establish the bounded production architecture
This commit is contained in:
@@ -392,7 +392,7 @@ If all criteria pass, set this milestone to `DONE`, append its execution record,
|
|||||||
|
|
||||||
## Milestone 006 — Establish the bounded production architecture
|
## Milestone 006 — Establish the bounded production architecture
|
||||||
|
|
||||||
**Status:** `READY`
|
**Status:** `DONE`
|
||||||
**Depends on:** Milestone 005
|
**Depends on:** Milestone 005
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
@@ -421,11 +421,22 @@ Create the production firmware and test structure without implementing game beha
|
|||||||
|
|
||||||
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 007 from `BLOCKED` to `READY`.
|
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 007 from `BLOCKED` to `READY`.
|
||||||
|
|
||||||
|
### Execution record
|
||||||
|
|
||||||
|
- Date: 2026-08-28
|
||||||
|
- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2.
|
||||||
|
- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; `esp_littlefs` 1.20.4.
|
||||||
|
- Result: PASS.
|
||||||
|
- Evidence: Added bounded production headers for configuration, types, application ownership, game engine, fleet generation, bot scheduling, sessions, presenter, statistics, transport, diagnostics, and deterministic interfaces. Added a fixed 16-command FIFO and a host test. HTTP diagnostics now include largest free block, connected clients, reset reason, and rejected oversized input. Network-facing code has no access to `application_t`; only the application queue may deliver commands to the future game owner.
|
||||||
|
- Measurements: Firmware build passed with 38,212 / 327,680 B RAM (11.7%) and 1,000,730 / 2,097,152 B flash (47.7%), within the Milestone 005 architecture gate of 1,080,000 B firmware and 220,000 B remaining heap. `make -C test/host run` passed the host command-queue test.
|
||||||
|
- Issues or deviations: No game behavior was implemented; that remains Milestone 007. No PSRAM, exceptions, RTTI, or heap-owning core containers were added.
|
||||||
|
- Next action: Milestone 007 is ready. Do not start it unless explicitly requested.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Milestone 007 — Implement and exhaustively test the game domain core
|
## Milestone 007 — Implement and exhaustively test the game domain core
|
||||||
|
|
||||||
**Status:** `BLOCKED`
|
**Status:** `READY`
|
||||||
**Depends on:** Milestone 006
|
**Depends on:** Milestone 006
|
||||||
|
|
||||||
### Objective
|
### Objective
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
enum {
|
||||||
|
kBoardWidth = 10,
|
||||||
|
kBoardHeight = 10,
|
||||||
|
kBoardCellCount = kBoardWidth * kBoardHeight,
|
||||||
|
kFleetShipCount = 10,
|
||||||
|
kPlayerCapacity = 2,
|
||||||
|
kSpectatorCapacity = 8,
|
||||||
|
kSessionCapacity = kPlayerCapacity + kSpectatorCapacity,
|
||||||
|
kCommandQueueCapacity = 16,
|
||||||
|
kStateMessageCapacity = 512,
|
||||||
|
kRequestBodyCapacity = 192,
|
||||||
|
kSessionTokenBytes = 16,
|
||||||
|
kDisplayNameBytes = 80,
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct { uint64_t (*now_ms)(void *context); void *context; } clock_t;
|
||||||
|
typedef struct { uint32_t (*next_u32)(void *context); void *context; } random_source_t;
|
||||||
|
typedef struct { bool (*schedule_after_ms)(void *context, uint32_t delay_ms); void *context; } scheduler_t;
|
||||||
|
typedef struct { bool (*send)(void *context, int client_id, const char *data, size_t length); void *context; } transport_t;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "command_queue.h"
|
||||||
|
|
||||||
|
/* The application task is the sole owner of game_state_t mutations. */
|
||||||
|
typedef struct { command_queue_t command_queue; game_state_t state; } application_t;
|
||||||
|
|
||||||
|
bool application_enqueue(application_t *application, const app_command_t *command);
|
||||||
|
bool application_take_next_command(application_t *application, app_command_t *command);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_interfaces.h"
|
||||||
|
|
||||||
|
typedef struct { random_source_t random; scheduler_t scheduler; } bot_player_t;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
typedef uint8_t command_type_t;
|
||||||
|
enum { COMMAND_CONFIG, COMMAND_START, COMMAND_SHOT, COMMAND_REMATCH, COMMAND_ABORT };
|
||||||
|
typedef struct { command_type_t type; uint8_t session_index; uint32_t game_id; uint32_t version; coordinate_t coordinate; game_mode_t mode; } app_command_t;
|
||||||
|
typedef struct { app_command_t entries[kCommandQueueCapacity]; uint8_t head; uint8_t tail; uint8_t count; } command_queue_t;
|
||||||
|
|
||||||
|
bool command_queue_push(command_queue_t *queue, const app_command_t *command);
|
||||||
|
bool command_queue_pop(command_queue_t *queue, app_command_t *command);
|
||||||
|
_Static_assert(sizeof(command_queue_t) <= 320, "command queue grew beyond fixed budget");
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t uptime_ms;
|
||||||
|
uint32_t free_heap_bytes;
|
||||||
|
uint32_t minimum_free_heap_bytes;
|
||||||
|
uint32_t largest_free_block_bytes;
|
||||||
|
uint16_t connected_clients;
|
||||||
|
uint16_t rejected_oversized_input;
|
||||||
|
int reset_reason;
|
||||||
|
} diagnostics_t;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_interfaces.h"
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
typedef struct { random_source_t random; } fleet_generator_t;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
/* Rule implementation begins in Milestone 007. */
|
||||||
|
typedef struct { game_state_t state; } game_engine_t;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
|
|
||||||
|
typedef uint8_t cell_t;
|
||||||
|
enum { CELL_UNKNOWN, CELL_WATER, CELL_SHIP, CELL_MISS, CELL_HIT };
|
||||||
|
typedef uint8_t phase_t;
|
||||||
|
enum { PHASE_LOBBY, PHASE_PREPARING, PHASE_IN_PROGRESS, PHASE_FINISHED, PHASE_REMATCH_WAIT };
|
||||||
|
typedef uint8_t role_t;
|
||||||
|
enum { ROLE_PLAYER_1, ROLE_PLAYER_2, ROLE_SPECTATOR };
|
||||||
|
typedef uint8_t game_mode_t;
|
||||||
|
enum { MODE_HUMAN, MODE_BOT };
|
||||||
|
|
||||||
|
typedef struct { uint8_t x; uint8_t y; } coordinate_t;
|
||||||
|
typedef struct { uint8_t x; uint8_t y; uint8_t length; uint8_t hits; bool horizontal; } ship_t;
|
||||||
|
typedef struct { cell_t cells[kBoardCellCount]; ship_t ships[kFleetShipCount]; uint8_t ships_alive; } board_t;
|
||||||
|
typedef struct { uint32_t game_id; uint32_t version; phase_t phase; game_mode_t mode; uint8_t current_player; board_t boards[kPlayerCapacity]; } game_state_t;
|
||||||
|
|
||||||
|
_Static_assert(kBoardCellCount == 100, "board contract changed");
|
||||||
|
_Static_assert(kFleetShipCount == 10, "fleet contract changed");
|
||||||
|
_Static_assert(kSessionCapacity == 10, "session contract changed");
|
||||||
|
_Static_assert(sizeof(coordinate_t) == 2, "coordinate must remain compact");
|
||||||
|
_Static_assert(sizeof(ship_t) <= 6, "ship grew beyond fixed budget");
|
||||||
|
_Static_assert(sizeof(board_t) <= 164, "board grew beyond fixed budget");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
typedef struct { role_t role; bool occupied; uint8_t token[kSessionTokenBytes]; char name[kDisplayNameBytes + 1]; } session_t;
|
||||||
|
typedef struct { session_t entries[kSessionCapacity]; } session_manager_t;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "game_types.h"
|
||||||
|
|
||||||
|
bool state_presenter_write(const game_state_t *state, role_t viewer, char *output, size_t output_size, size_t *written);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct { uint16_t shots; uint16_t hits; uint16_t misses; uint8_t ships_sunk; } match_statistics_t;
|
||||||
|
typedef struct { uint16_t games; uint16_t wins; uint16_t losses; uint32_t shots; uint32_t hits; } cumulative_statistics_t;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "app_interfaces.h"
|
||||||
|
|
||||||
|
typedef struct { transport_t transport; } transport_service_t;
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
# without default 'CMakeLists.txt' file.
|
# without default 'CMakeLists.txt' file.
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c"
|
SRCS "main.c" "application.c" "command_queue.c"
|
||||||
INCLUDE_DIRS "../include"
|
INCLUDE_DIRS "../include"
|
||||||
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
|
REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "application.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
bool application_enqueue(application_t *application, const app_command_t *command) {
|
||||||
|
return application != NULL && command_queue_push(&application->command_queue, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool application_take_next_command(application_t *application, app_command_t *command) {
|
||||||
|
return application != NULL && command_queue_pop(&application->command_queue, command);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include "command_queue.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
bool command_queue_push(command_queue_t *queue, const app_command_t *command) {
|
||||||
|
if (queue == NULL || command == NULL || queue->count == kCommandQueueCapacity) return false;
|
||||||
|
queue->entries[queue->tail] = *command;
|
||||||
|
queue->tail = (uint8_t)((queue->tail + 1U) % kCommandQueueCapacity);
|
||||||
|
++queue->count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool command_queue_pop(command_queue_t *queue, app_command_t *command) {
|
||||||
|
if (queue == NULL || command == NULL || queue->count == 0) return false;
|
||||||
|
*command = queue->entries[queue->head];
|
||||||
|
queue->head = (uint8_t)((queue->head + 1U) % kCommandQueueCapacity);
|
||||||
|
--queue->count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+17
-2
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "esp_check.h"
|
#include "esp_check.h"
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
#include "esp_littlefs.h"
|
#include "esp_littlefs.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
@@ -55,6 +56,7 @@ 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 portMUX_TYPE s_capacity_lock = portMUX_INITIALIZER_UNLOCKED;
|
static portMUX_TYPE s_capacity_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
static uint16_t s_rejected_oversized_input;
|
||||||
|
|
||||||
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;
|
||||||
@@ -260,6 +262,7 @@ static void make_capacity_state(capacity_state_t *state) {
|
|||||||
|
|
||||||
static esp_err_t capacity_run_handler(httpd_req_t *request) {
|
static esp_err_t capacity_run_handler(httpd_req_t *request) {
|
||||||
if (request->content_len != 0) {
|
if (request->content_len != 0) {
|
||||||
|
++s_rejected_oversized_input;
|
||||||
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body");
|
return httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "capacity run accepts no body");
|
||||||
}
|
}
|
||||||
capacity_state_t fresh_state;
|
capacity_state_t fresh_state;
|
||||||
@@ -310,7 +313,10 @@ static esp_err_t websocket_handler(httpd_req_t *request) {
|
|||||||
}
|
}
|
||||||
httpd_ws_frame_t frame = {0};
|
httpd_ws_frame_t frame = {0};
|
||||||
ESP_RETURN_ON_ERROR(httpd_ws_recv_frame(request, &frame, 0), kLogTag, "websocket frame read failed");
|
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;
|
if (frame.len > 64 || frame.type != HTTPD_WS_TYPE_TEXT) {
|
||||||
|
++s_rejected_oversized_input;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
char message[65] = {0};
|
char message[65] = {0};
|
||||||
frame.payload = (uint8_t *)message;
|
frame.payload = (uint8_t *)message;
|
||||||
frame.len = sizeof(message) - 1;
|
frame.len = sizeof(message) - 1;
|
||||||
@@ -462,23 +468,32 @@ static const char *wifi_state_name(const wifi_state_t *state) {
|
|||||||
|
|
||||||
static esp_err_t health_handler(httpd_req_t *request) {
|
static esp_err_t health_handler(httpd_req_t *request) {
|
||||||
const wifi_state_t wifi_state = wifi_state_snapshot();
|
const wifi_state_t wifi_state = wifi_state_snapshot();
|
||||||
|
size_t client_count = kHttpMaxOpenSockets;
|
||||||
|
int clients[kHttpMaxOpenSockets];
|
||||||
|
if (s_server == NULL || httpd_get_client_list(s_server, &client_count, clients) != ESP_OK) client_count = 0;
|
||||||
int8_t rssi_dbm = 0;
|
int8_t rssi_dbm = 0;
|
||||||
wifi_ap_record_t access_point = {0};
|
wifi_ap_record_t access_point = {0};
|
||||||
if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) {
|
if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) {
|
||||||
rssi_dbm = access_point.rssi;
|
rssi_dbm = access_point.rssi;
|
||||||
}
|
}
|
||||||
char response[320];
|
char response[384];
|
||||||
const int written = snprintf(response,
|
const int written = snprintf(response,
|
||||||
sizeof(response),
|
sizeof(response),
|
||||||
"{\"uptime_ms\":%" PRIu64
|
"{\"uptime_ms\":%" PRIu64
|
||||||
",\"wifi_state\":\"%s\",\"rssi_dbm\":%d"
|
",\"wifi_state\":\"%s\",\"rssi_dbm\":%d"
|
||||||
",\"free_heap_bytes\":%u,\"min_free_heap_bytes\":%u"
|
",\"free_heap_bytes\":%u,\"min_free_heap_bytes\":%u"
|
||||||
|
",\"largest_free_block_bytes\":%u,\"connected_clients\":%u"
|
||||||
|
",\"rejected_oversized_input\":%u,\"reset_reason\":%d"
|
||||||
",\"build_version\":\"%s\"}",
|
",\"build_version\":\"%s\"}",
|
||||||
(uint64_t)(esp_timer_get_time() / 1000),
|
(uint64_t)(esp_timer_get_time() / 1000),
|
||||||
wifi_state_name(&wifi_state),
|
wifi_state_name(&wifi_state),
|
||||||
(int)rssi_dbm,
|
(int)rssi_dbm,
|
||||||
(unsigned int)esp_get_free_heap_size(),
|
(unsigned int)esp_get_free_heap_size(),
|
||||||
(unsigned int)esp_get_minimum_free_heap_size(),
|
(unsigned int)esp_get_minimum_free_heap_size(),
|
||||||
|
(unsigned int)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT),
|
||||||
|
(unsigned int)client_count,
|
||||||
|
(unsigned int)s_rejected_oversized_input,
|
||||||
|
(int)esp_reset_reason(),
|
||||||
kBuildVersion);
|
kBuildVersion);
|
||||||
if (written < 0 || (size_t)written >= sizeof(response)) {
|
if (written < 0 || (size_t)written >= sizeof(response)) {
|
||||||
return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed");
|
return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed");
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
||||||
|
|
||||||
|
all: test_command_queue
|
||||||
|
|
||||||
|
test_command_queue: test_command_queue.c ../../src/command_queue.c ../../src/application.c
|
||||||
|
$(CC) $(CFLAGS) $^ -o $@
|
||||||
|
|
||||||
|
run: test_command_queue
|
||||||
|
./test_command_queue
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f test_command_queue
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,24 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "command_queue.h"
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
command_queue_t queue = {0};
|
||||||
|
app_command_t input = {.type = COMMAND_SHOT, .session_index = 1, .game_id = 7, .version = 3, .coordinate = {4, 5}};
|
||||||
|
app_command_t output = {0};
|
||||||
|
assert(!command_queue_pop(&queue, &output));
|
||||||
|
for (int index = 0; index < kCommandQueueCapacity; ++index) {
|
||||||
|
input.coordinate.x = (uint8_t)index;
|
||||||
|
assert(command_queue_push(&queue, &input));
|
||||||
|
}
|
||||||
|
assert(!command_queue_push(&queue, &input));
|
||||||
|
for (int index = 0; index < kCommandQueueCapacity; ++index) {
|
||||||
|
assert(command_queue_pop(&queue, &output));
|
||||||
|
assert(output.coordinate.x == (uint8_t)index);
|
||||||
|
}
|
||||||
|
assert(!command_queue_pop(&queue, &output));
|
||||||
|
puts("command queue tests passed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user