feat: create firmware skeleton and reproducible build

This commit is contained in:
2026-09-03 01:02:40 +03:00
parent dd16bc7ade
commit 9c274800b5
24 changed files with 827 additions and 31 deletions
+43 -4
View File
@@ -111,7 +111,7 @@ passport were repaired and reverified before returning the milestone to
## Milestone 002 — Firmware skeleton and reproducible build ## Milestone 002 — Firmware skeleton and reproducible build
**Status:** `NOT_STARTED` **Status:** `COMPLETE`
### Objective ### Objective
@@ -146,9 +146,48 @@ builds reproducibly with the locked platform and partition configuration.
### Evidence ### Evidence
Completion evidence is not yet available because this milestone has not - On 2026-09-03, `/home/sasa/.platformio/penv/bin/pio project config` parsed
started. Required evidence includes clean build output, image sizes, asset the updated configuration successfully with the locked platform, ESP-IDF,
reproducibility checks, and a target-board boot log. 4 MB Flash, root partition table, LittleFS, pinned `esp_littlefs`, bounded
LittleFS settings, and the build hook.
- On 2026-09-03, `./scripts/run_host_tests.sh` reported
`health state tests: PASS` and
`web asset reproducibility: PASS
(611eacdb06b81bdb064a9074352ff4c6f82d3af3ad3d01e65705387744b86b21)`.
- After `/home/sasa/.platformio/penv/bin/pio run --target clean`, running
`/home/sasa/.platformio/penv/bin/pio run` on 2026-09-03 completed with
`[SUCCESS]` in 43.10 seconds. PlatformIO reported 14,084 bytes of RAM and
350,232 of 2,097,152 application bytes in use. The independent build hook
reported `firmware.bin uses 350864/2097152 bytes in partition factory`.
- Running `/home/sasa/.platformio/penv/bin/pio run --target buildfs` on
2026-09-03 regenerated `data/index.html.gz`, built the LittleFS image, and
reported `[SUCCESS]`. The independent build hook reported
`littlefs.bin uses 2031616/2031616 bytes in partition littlefs`; the image is
intentionally emitted at full partition size and contains the 683-byte
compressed application asset.
- Two LittleFS builds separated by a clean produced the identical SHA-256
`ad203373be69f4541106b2d0dfd7096adb8132967ab49e2d23c391ede8f4d4f5`.
The generated gzip asset remained
`950c60077c381d4484c11b43762bc5c8378587b6cd91924c344cfbc406771bb9`.
Listing the image showed both file and image creation times normalized to
1970-01-01, demonstrating deterministic compression and packaging.
- `strings .pio/build/esp32-c6-devkitm-1/firmware.bin` found firmware version
`0.2.0` and the startup log formats for version, LittleFS capacity, and health
state. This verifies their presence in the built artifact but is not a
substitute for target-board execution.
- On 2026-09-03, `/home/sasa/.platformio/penv/bin/pio run --target uploadfs
--upload-port /dev/ttyACM0` wrote and hash-verified the 2,031,616-byte
LittleFS image at `0x210000`. A subsequent application upload identified the
target as `ESP32-C6FH4 (QFN32) (revision v0.2)` with embedded 4 MB Flash and
USB Serial/JTAG, wrote the 350,864-byte application image, and verified its
hash.
- A target-board boot capture on 2026-09-03 reported `App version: 0.2.0`,
`SPI Flash Size : 4MB`,
`LittleFS mounted: total=2031616 bytes, used=12288 bytes`,
`HTTP server ready with static and health endpoints`, and
`Health state: serving`. The capture contained no application error after
startup. This supplies the physical-board evidence for acceptance criterion
5.
## Milestone 003 — Wi-Fi provisioning and persistent configuration ## Milestone 003 — Wi-Fi provisioning and persistent configuration
+66
View File
@@ -0,0 +1,66 @@
# maze32
maze32 is an ESP32-C6-hosted browser maze game. The current firmware skeleton
mounts a LittleFS image, serves a compressed static page, and exposes a bounded
`/health` response containing the firmware version and startup state.
## Prerequisites
- PlatformIO Core installed at `/home/sasa/.platformio/penv/bin/pio`;
- a C11 host compiler available as `cc`;
- Python 3.
## Build and verify
Parse the locked PlatformIO configuration:
```sh
/home/sasa/.platformio/penv/bin/pio project config
```
Run the portable host tests and deterministic asset check:
```sh
./scripts/run_host_tests.sh
```
Build the firmware. The PlatformIO hook checks the application image against
the 2 MiB `factory` partition:
```sh
/home/sasa/.platformio/penv/bin/pio run
```
Generate compressed assets explicitly when needed:
```sh
/home/sasa/.platformio/penv/bin/python scripts/generate_web_assets.py
```
Build the LittleFS image. Asset generation runs automatically before packaging,
and the PlatformIO hook checks the image against the 1,984 KiB `littlefs`
partition:
```sh
/home/sasa/.platformio/penv/bin/pio run --target buildfs
```
## Device commands
Upload the application and LittleFS image:
```sh
/home/sasa/.platformio/penv/bin/pio run --target upload
/home/sasa/.platformio/penv/bin/pio run --target uploadfs
```
Monitor startup at 115200 baud:
```sh
/home/sasa/.platformio/penv/bin/pio device monitor
```
A successful boot reports the firmware version, LittleFS capacity and use, the
HTTP service startup, and health state `serving`. Network provisioning is part
of a later milestone; this milestone only establishes the HTTP/static service
that will be attached to that network lifecycle.
@@ -0,0 +1,4 @@
idf_component_register(
SRCS "maze32_health.c"
INCLUDE_DIRS "include"
)
@@ -0,0 +1,30 @@
#ifndef MAZE32_HEALTH_H
#define MAZE32_HEALTH_H
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
MAZE32_HEALTH_BOOTING = 0,
MAZE32_HEALTH_FILESYSTEM_READY,
MAZE32_HEALTH_SERVING,
MAZE32_HEALTH_ERROR,
} maze32_health_state_t;
typedef struct {
const char *version;
maze32_health_state_t state;
int error_code;
} maze32_health_t;
void maze32_health_init(maze32_health_t *health, const char *version);
void maze32_health_set(maze32_health_t *health, maze32_health_state_t state, int error_code);
const char *maze32_health_state_name(maze32_health_state_t state);
int maze32_health_is_healthy(const maze32_health_t *health);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,45 @@
#include "maze32_health.h"
#include <stddef.h>
void maze32_health_init(maze32_health_t *health, const char *version)
{
if (health == NULL) {
return;
}
health->version = version;
health->state = MAZE32_HEALTH_BOOTING;
health->error_code = 0;
}
void maze32_health_set(maze32_health_t *health, maze32_health_state_t state, int error_code)
{
if (health == NULL) {
return;
}
health->state = state;
health->error_code = error_code;
}
const char *maze32_health_state_name(maze32_health_state_t state)
{
switch (state) {
case MAZE32_HEALTH_BOOTING:
return "booting";
case MAZE32_HEALTH_FILESYSTEM_READY:
return "filesystem_ready";
case MAZE32_HEALTH_SERVING:
return "serving";
case MAZE32_HEALTH_ERROR:
return "error";
default:
return "unknown";
}
}
int maze32_health_is_healthy(const maze32_health_t *health)
{
return health != NULL && health->state == MAZE32_HEALTH_SERVING;
}
+5
View File
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "maze32_web.c"
INCLUDE_DIRS "include"
REQUIRES esp_http_server maze32_platform
)
@@ -0,0 +1,18 @@
#ifndef MAZE32_WEB_H
#define MAZE32_WEB_H
#include "esp_err.h"
#include "esp_http_server.h"
#include "maze32_health.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t maze32_web_start(httpd_handle_t *server, maze32_health_t *health);
#ifdef __cplusplus
}
#endif
#endif
+118
View File
@@ -0,0 +1,118 @@
#include "maze32_web.h"
#include <stdio.h>
#include "esp_log.h"
#define MAZE32_STATIC_CHUNK_SIZE 1024
static const char *TAG = "maze32_web";
static const char *INDEX_PATH = "/littlefs/index.html.gz";
static esp_err_t send_index(httpd_req_t *request)
{
FILE *file = fopen(INDEX_PATH, "rb");
if (file == NULL) {
ESP_LOGE(TAG, "Static asset is unavailable: %s", INDEX_PATH);
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
"Static application is unavailable");
return ESP_FAIL;
}
httpd_resp_set_type(request, "text/html; charset=utf-8");
httpd_resp_set_hdr(request, "Content-Encoding", "gzip");
httpd_resp_set_hdr(request, "Cache-Control", "no-cache");
char buffer[MAZE32_STATIC_CHUNK_SIZE];
size_t bytes_read;
esp_err_t result = ESP_OK;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
result = httpd_resp_send_chunk(request, buffer, bytes_read);
if (result != ESP_OK) {
break;
}
}
if (ferror(file) != 0) {
ESP_LOGE(TAG, "Failed while reading static asset: %s", INDEX_PATH);
result = ESP_FAIL;
}
fclose(file);
if (result == ESP_OK) {
result = httpd_resp_send_chunk(request, NULL, 0);
}
return result;
}
static esp_err_t send_health(httpd_req_t *request)
{
const maze32_health_t *health = request->user_ctx;
char response[192];
int length = snprintf(response, sizeof(response),
"{\"status\":\"%s\",\"healthy\":%s,\"version\":\"%s\",\"errorCode\":%d}",
maze32_health_state_name(health->state),
maze32_health_is_healthy(health) ? "true" : "false",
health->version, health->error_code);
if (length < 0 || (size_t)length >= sizeof(response)) {
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
"Health response exceeded its fixed buffer");
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);
}
esp_err_t maze32_web_start(httpd_handle_t *server, maze32_health_t *health)
{
if (server == NULL || health == NULL) {
return ESP_ERR_INVALID_ARG;
}
httpd_config_t configuration = HTTPD_DEFAULT_CONFIG();
configuration.max_uri_handlers = 3;
configuration.max_open_sockets = 7;
configuration.lru_purge_enable = true;
esp_err_t result = httpd_start(server, &configuration);
if (result != ESP_OK) {
return result;
}
const httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = send_index,
.user_ctx = NULL,
};
const httpd_uri_t index = {
.uri = "/index.html",
.method = HTTP_GET,
.handler = send_index,
.user_ctx = NULL,
};
const httpd_uri_t health_uri = {
.uri = "/health",
.method = HTTP_GET,
.handler = send_health,
.user_ctx = health,
};
result = httpd_register_uri_handler(*server, &root);
if (result == ESP_OK) {
result = httpd_register_uri_handler(*server, &index);
}
if (result == ESP_OK) {
result = httpd_register_uri_handler(*server, &health_uri);
}
if (result != ESP_OK) {
httpd_stop(*server);
*server = NULL;
return result;
}
ESP_LOGI(TAG, "HTTP server ready with static and health endpoints");
return ESP_OK;
}
Binary file not shown.
+13
View File
@@ -18,5 +18,18 @@ board_build.filesystem = littlefs
board_build.sdkconfig_defaults = sdkconfig.defaults board_build.sdkconfig_defaults = sdkconfig.defaults
board_upload.flash_size = 4MB board_upload.flash_size = 4MB
lib_deps = https://github.com/joltwallet/esp_littlefs.git#v1.20.4 lib_deps = https://github.com/joltwallet/esp_littlefs.git#v1.20.4
build_flags =
-DCONFIG_LITTLEFS_MAX_PARTITIONS=1
-DCONFIG_LITTLEFS_PAGE_SIZE=256
-DCONFIG_LITTLEFS_OBJ_NAME_LEN=64
-DCONFIG_LITTLEFS_READ_SIZE=128
-DCONFIG_LITTLEFS_WRITE_SIZE=128
-DCONFIG_LITTLEFS_LOOKAHEAD_SIZE=128
-DCONFIG_LITTLEFS_CACHE_SIZE=512
-DCONFIG_LITTLEFS_BLOCK_CYCLES=512
-DCONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=1
extra_scripts = post:scripts/platformio_build.py
monitor_port = /dev/ttyACM0 monitor_port = /dev/ttyACM0
monitor_speed = 115200 monitor_speed = 115200
monitor_dtr = 0
monitor_rts = 0
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Generate deterministic gzip-compressed LittleFS web assets."""
from __future__ import annotations
import argparse
import gzip
import os
import shutil
from pathlib import Path
SUPPORTED_SUFFIXES = {".css", ".html", ".ico", ".js", ".json", ".svg"}
def generate_assets(source_directory: Path, output_directory: Path) -> list[Path]:
source_directory = source_directory.resolve()
output_directory = output_directory.resolve()
if not source_directory.is_dir():
raise ValueError(f"Web source directory does not exist: {source_directory}")
if source_directory == output_directory:
raise ValueError("Web source and output directories must differ")
source_files = sorted(
path for path in source_directory.rglob("*")
if path.is_file() and path.suffix.lower() in SUPPORTED_SUFFIXES
)
if not source_files:
raise ValueError(f"No supported web assets found in {source_directory}")
if output_directory.exists():
shutil.rmtree(output_directory)
output_directory.mkdir(parents=True)
generated_files: list[Path] = []
for source_path in source_files:
relative_path = source_path.relative_to(source_directory)
output_path = output_directory / f"{relative_path}.gz"
output_path.parent.mkdir(parents=True, exist_ok=True)
with source_path.open("rb") as source, output_path.open("wb") as output:
with gzip.GzipFile(
filename="",
mode="wb",
compresslevel=9,
fileobj=output,
mtime=0,
) as compressed:
shutil.copyfileobj(source, compressed)
os.utime(output_path, ns=(0, 0))
generated_files.append(output_path)
for directory in sorted(
(path for path in output_directory.rglob("*") if path.is_dir()),
reverse=True,
):
os.utime(directory, ns=(0, 0))
os.utime(output_directory, ns=(0, 0))
return generated_files
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=Path("web"))
parser.add_argument("--output", type=Path, default=Path("data"))
arguments = parser.parse_args()
generated_files = generate_assets(arguments.source, arguments.output)
for path in generated_files:
print(path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+93
View File
@@ -0,0 +1,93 @@
"""PlatformIO hooks for deterministic assets and partition size limits."""
from __future__ import annotations
import csv
import subprocess
from pathlib import Path
Import("env")
PROJECT_DIRECTORY = Path(env.subst("$PROJECT_DIR"))
BUILD_DIRECTORY = Path(env.subst("$BUILD_DIR"))
PARTITION_FILE = PROJECT_DIRECTORY / "partitions.csv"
ASSET_SCRIPT = PROJECT_DIRECTORY / "scripts" / "generate_web_assets.py"
FILESYSTEM_WRAPPER = PROJECT_DIRECTORY / "scripts" / "reproducible_mklittlefs.py"
def parse_size(value: str) -> int:
value = value.strip()
suffixes = {"K": 1024, "M": 1024 * 1024}
if value[-1:].upper() in suffixes:
return int(value[:-1], 0) * suffixes[value[-1].upper()]
return int(value, 0)
def partition_sizes() -> dict[str, int]:
sizes: dict[str, int] = {}
with PARTITION_FILE.open(newline="", encoding="utf-8") as partition_stream:
for row in csv.reader(partition_stream):
if not row or row[0].lstrip().startswith("#"):
continue
if len(row) < 5:
raise RuntimeError(f"Invalid partition row: {row}")
sizes[row[0].strip()] = parse_size(row[4])
return sizes
def generate_assets(source, target, env) -> None:
command = [
env.subst("$PYTHONEXE"),
str(ASSET_SCRIPT),
"--source",
str(PROJECT_DIRECTORY / "web"),
"--output",
str(PROJECT_DIRECTORY / "data"),
]
subprocess.run(command, check=True)
def enforce_size(path: Path, partition_name: str) -> None:
limits = partition_sizes()
if partition_name not in limits:
raise RuntimeError(f"Missing required partition: {partition_name}")
actual_size = path.stat().st_size
maximum_size = limits[partition_name]
if actual_size > maximum_size:
raise RuntimeError(
f"{path.name} is {actual_size} bytes and exceeds the "
f"{partition_name} partition limit of {maximum_size} bytes"
)
print(
f"maze32 size check: {path.name} uses {actual_size}/{maximum_size} bytes "
f"in partition {partition_name}"
)
def check_application(source, target, env) -> None:
enforce_size(application_image, "factory")
def check_filesystem(source, target, env) -> None:
enforce_size(filesystem_image, "littlefs")
filesystem_image = BUILD_DIRECTORY / "littlefs.bin"
application_image = BUILD_DIRECTORY / f"{env.subst('$PROGNAME')}.bin"
real_filesystem_tool = env.subst("$MKFSTOOL")
env["ENV"]["MAZE32_REAL_MKFSTOOL"] = real_filesystem_tool
env["ENV"]["MAZE32_BUILD_DIR"] = str(BUILD_DIRECTORY)
env.Replace(MKFSTOOL=str(FILESYSTEM_WRAPPER))
env.AddPreAction("$BUILD_DIR/littlefs.bin", generate_assets)
env.AddPostAction("$BUILD_DIR/littlefs.bin", check_filesystem)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", check_application)
env.AddCustomTarget(
"webassets",
None,
generate_assets,
title="Generate deterministic web assets",
description="Compress web sources into the LittleFS data directory",
)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Run mklittlefs with a fixed wall clock for reproducible images."""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from pathlib import Path
def build_time_override(output_path: Path) -> None:
source_path = Path(__file__).with_name("reproducible_time.c")
compiler = shutil.which("cc")
if compiler is None:
raise RuntimeError("A host C compiler named 'cc' is required")
subprocess.run(
[
compiler,
"-shared",
"-fPIC",
"-O2",
str(source_path),
"-o",
str(output_path),
],
check=True,
)
def main() -> int:
real_tool = os.environ.get("MAZE32_REAL_MKFSTOOL")
build_directory = os.environ.get("MAZE32_BUILD_DIR")
if not real_tool or not build_directory:
raise RuntimeError("PlatformIO did not provide the filesystem tool environment")
override_path = Path(build_directory) / "reproducible_time.so"
override_path.parent.mkdir(parents=True, exist_ok=True)
build_time_override(override_path)
environment = os.environ.copy()
existing_preload = environment.get("LD_PRELOAD")
environment["LD_PRELOAD"] = (
f"{override_path}:{existing_preload}" if existing_preload else str(override_path)
)
completed = subprocess.run([real_tool, *sys.argv[1:]], env=environment)
return completed.returncode
if __name__ == "__main__":
raise SystemExit(main())
+37
View File
@@ -0,0 +1,37 @@
#define _GNU_SOURCE
#include <stddef.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
static const time_t FIXED_TIME = 0;
time_t time(time_t *result)
{
if (result != NULL) {
*result = FIXED_TIME;
}
return FIXED_TIME;
}
int gettimeofday(struct timeval *value, void *timezone)
{
(void)timezone;
if (value != NULL) {
value->tv_sec = FIXED_TIME;
value->tv_usec = 0;
}
return 0;
}
int clock_gettime(clockid_t clock_id, struct timespec *value)
{
if (clock_id == CLOCK_REALTIME) {
value->tv_sec = FIXED_TIME;
value->tv_nsec = 0;
return 0;
}
return (int)syscall(SYS_clock_gettime, clock_id, value);
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env sh
set -eu
project_directory=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
test_directory=$(mktemp -d "${TMPDIR:-/tmp}/maze32-host-tests.XXXXXX")
trap 'rm -rf "$test_directory"' EXIT HUP INT TERM
cc -std=c11 -Wall -Wextra -Werror -pedantic \
-I"$project_directory/components/maze32_platform/include" \
"$project_directory/components/maze32_platform/maze32_health.c" \
"$project_directory/test/host/test_health.c" \
-o "$test_directory/test_health"
"$test_directory/test_health"
python3 "$project_directory/scripts/verify_web_assets.py"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""Verify deterministic web generation and the checked-in generated assets."""
from __future__ import annotations
import gzip
import hashlib
import tempfile
from pathlib import Path
from generate_web_assets import generate_assets
PROJECT_DIRECTORY = Path(__file__).resolve().parent.parent
def tree_digest(directory: Path) -> str:
digest = hashlib.sha256()
for path in sorted(item for item in directory.rglob("*") if item.is_file()):
digest.update(path.relative_to(directory).as_posix().encode("utf-8"))
digest.update(b"\0")
digest.update(path.read_bytes())
return digest.hexdigest()
def main() -> int:
source_directory = PROJECT_DIRECTORY / "web"
checked_in_directory = PROJECT_DIRECTORY / "data"
with tempfile.TemporaryDirectory(prefix="maze32-assets-") as temporary:
temporary_directory = Path(temporary)
first = temporary_directory / "first"
second = temporary_directory / "second"
generate_assets(source_directory, first)
generate_assets(source_directory, second)
first_digest = tree_digest(first)
second_digest = tree_digest(second)
checked_in_digest = tree_digest(checked_in_directory)
if first_digest != second_digest:
raise AssertionError("Repeated web-asset generation produced different output")
if first_digest != checked_in_digest:
raise AssertionError("Generated web assets are stale; regenerate the data directory")
index_source = (source_directory / "index.html").read_bytes()
index_compressed = (first / "index.html.gz").read_bytes()
if gzip.decompress(index_compressed) != index_source:
raise AssertionError("Compressed index does not reproduce its source")
print(f"web asset reproducibility: PASS ({first_digest})")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -1,2 +1,4 @@
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_LWIP_MAX_SOCKETS=16 CONFIG_LWIP_MAX_SOCKETS=16
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
+7 -20
View File
@@ -1942,28 +1942,17 @@ CONFIG_ESP_CRYPTO_DPA_PROTECTION_LEVEL=1
# #
# ESP-STDIO # ESP-STDIO
# #
# default: # CONFIG_ESP_CONSOLE_UART_DEFAULT is not set
CONFIG_ESP_CONSOLE_UART_DEFAULT=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
# default:
# CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG is not set
# default:
# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set # CONFIG_ESP_CONSOLE_UART_CUSTOM is not set
# default:
# CONFIG_ESP_CONSOLE_NONE is not set # CONFIG_ESP_CONSOLE_NONE is not set
# default: CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
# CONFIG_ESP_CONSOLE_SECONDARY_NONE is not set
# default:
CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y
# default: # default:
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG_ENABLED=y
# default: # default:
CONFIG_ESP_CONSOLE_UART=y CONFIG_ESP_CONSOLE_UART_NUM=-1
# default: # default:
CONFIG_ESP_CONSOLE_UART_NUM=0 CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=3
# default:
CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0
# default:
CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200
# end of ESP-STDIO # end of ESP-STDIO
# #
@@ -3709,13 +3698,11 @@ CONFIG_ESP32_PHY_MAX_TX_POWER=20
# CONFIG_REDUCE_PHY_TX_POWER is not set # CONFIG_REDUCE_PHY_TX_POWER is not set
# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set # CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set
CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y CONFIG_ESP_SYSTEM_PM_POWER_DOWN_CPU=y
CONFIG_CONSOLE_UART_DEFAULT=y # CONFIG_CONSOLE_UART_DEFAULT is not set
# CONFIG_CONSOLE_UART_CUSTOM is not set # CONFIG_CONSOLE_UART_CUSTOM is not set
# CONFIG_CONSOLE_UART_NONE is not set # CONFIG_CONSOLE_UART_NONE is not set
# CONFIG_ESP_CONSOLE_UART_NONE is not set # CONFIG_ESP_CONSOLE_UART_NONE is not set
CONFIG_CONSOLE_UART=y CONFIG_CONSOLE_UART_NUM=-1
CONFIG_CONSOLE_UART_NUM=0
CONFIG_CONSOLE_UART_BAUDRATE=115200
CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=y CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_VIA_TEE=y CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_VIA_TEE=y
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=y
+4 -6
View File
@@ -1,6 +1,4 @@
# This file was automatically generated for projects idf_component_register(
# without default 'CMakeLists.txt' file. SRCS "main.c"
REQUIRES esp_event esp_netif maze32_platform maze32_web
FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) )
idf_component_register(SRCS ${app_sources})
+83 -1
View File
@@ -1 +1,83 @@
void app_main() {} #include "esp_app_desc.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_littlefs.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "maze32_health.h"
#include "maze32_web.h"
static const char *TAG = "maze32";
static maze32_health_t health;
static httpd_handle_t web_server;
static esp_err_t mount_littlefs(void)
{
const esp_vfs_littlefs_conf_t configuration = {
.base_path = "/littlefs",
.partition_label = "littlefs",
.format_if_mount_failed = false,
.dont_mount = false,
};
esp_err_t result = esp_vfs_littlefs_register(&configuration);
if (result != ESP_OK) {
return result;
}
size_t total_bytes = 0;
size_t used_bytes = 0;
result = esp_littlefs_info(configuration.partition_label, &total_bytes, &used_bytes);
if (result != ESP_OK) {
esp_vfs_littlefs_unregister(configuration.partition_label);
return result;
}
ESP_LOGI(TAG, "LittleFS mounted: total=%u bytes, used=%u bytes",
(unsigned int)total_bytes, (unsigned int)used_bytes);
return ESP_OK;
}
void app_main(void)
{
const esp_app_desc_t *application = esp_app_get_description();
maze32_health_init(&health, application->version);
vTaskDelay(pdMS_TO_TICKS(1500));
ESP_LOGI(TAG, "maze32 firmware version %s starting", application->version);
esp_err_t result = mount_littlefs();
if (result != ESP_OK) {
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result);
ESP_LOGE(TAG, "LittleFS mount failed: %s", esp_err_to_name(result));
return;
}
maze32_health_set(&health, MAZE32_HEALTH_FILESYSTEM_READY, 0);
result = esp_netif_init();
if (result != ESP_OK) {
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result);
ESP_LOGE(TAG, "TCP/IP stack initialization failed: %s", esp_err_to_name(result));
return;
}
result = esp_event_loop_create_default();
if (result != ESP_OK) {
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result);
ESP_LOGE(TAG, "Default event loop initialization failed: %s", esp_err_to_name(result));
return;
}
maze32_health_set(&health, MAZE32_HEALTH_SERVING, 0);
result = maze32_web_start(&web_server, &health);
if (result != ESP_OK) {
maze32_health_set(&health, MAZE32_HEALTH_ERROR, (int)result);
ESP_LOGE(TAG, "HTTP server startup failed: %s", esp_err_to_name(result));
return;
}
ESP_LOGI(TAG, "Health state: %s", maze32_health_state_name(health.state));
}
+33
View File
@@ -0,0 +1,33 @@
#include "maze32_health.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
maze32_health_t health;
maze32_health_init(&health, "test-version");
assert(strcmp(health.version, "test-version") == 0);
assert(health.state == MAZE32_HEALTH_BOOTING);
assert(strcmp(maze32_health_state_name(health.state), "booting") == 0);
assert(maze32_health_is_healthy(&health) == 0);
maze32_health_set(&health, MAZE32_HEALTH_FILESYSTEM_READY, 0);
assert(strcmp(maze32_health_state_name(health.state), "filesystem_ready") == 0);
assert(maze32_health_is_healthy(&health) == 0);
maze32_health_set(&health, MAZE32_HEALTH_SERVING, 0);
assert(strcmp(maze32_health_state_name(health.state), "serving") == 0);
assert(maze32_health_is_healthy(&health) == 1);
maze32_health_set(&health, MAZE32_HEALTH_ERROR, 17);
assert(strcmp(maze32_health_state_name(health.state), "error") == 0);
assert(health.error_code == 17);
assert(maze32_health_is_healthy(&health) == 0);
assert(strcmp(maze32_health_state_name((maze32_health_state_t)99), "unknown") == 0);
printf("health state tests: PASS\n");
return 0;
}
+1
View File
@@ -0,0 +1 @@
0.2.0
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>maze32</title>
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
body { display: grid; min-height: 100vh; margin: 0; place-items: center; background: #10151d; color: #e7edf5; }
main { width: min(32rem, calc(100% - 3rem)); padding: 2rem; border: 1px solid #344255; border-radius: 1rem; background: #18212d; }
h1 { margin-top: 0; letter-spacing: .08em; }
output { color: #79dfa3; }
</style>
</head>
<body>
<main>
<h1>maze32</h1>
<p>The firmware and static application are running.</p>
<p>Device health: <output id="health">checking</output></p>
</main>
<script>
const output = document.querySelector('#health');
fetch('/health', { cache: 'no-store' })
.then(response => response.ok ? response.json() : Promise.reject(new Error('Health request failed')))
.then(health => { output.textContent = `${health.status} · version ${health.version}`; })
.catch(() => { output.textContent = 'unavailable'; });
</script>
</body>
</html>