Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/firmware-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ jobs:
- name: Test heater interlock state machine
run: ./tests/run_interlock_host_test.sh

- name: Check out pinned dragon-core for host fixtures
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: justinh-rahb/dragon-core
ref: 4e041d864763d468a50e9649807827dd83dd54bc
path: .test-deps/dragon-core

- name: Check out matching ESP-IDF headers and JSON dependency
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: espressif/esp-idf
ref: v5.3
path: .test-deps/esp-idf

- name: Test candidate diagnostics and configuration
run: |
git -C .test-deps/esp-idf submodule update --init --depth 1 components/json/cJSON
DRAGON_CORE_PATH="$PWD/.test-deps/dragon-core" IDF_PATH="$PWD/.test-deps/esp-idf" bash tests/run_bringup_host_test.sh

- name: Check canonical product identity
run: sh tests/check_identity_contract.sh

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ _autosave-*
*.drl
*.pos
*.rpt

# Read-only dependencies for host characterization fixtures
.test-deps/
2 changes: 2 additions & 0 deletions components/jj_board/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
idf_component_register(SRCS "jj_board.c" INCLUDE_DIRS "include")
target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror)
25 changes: 25 additions & 0 deletions components/jj_board/include/jj_board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdbool.h>
#include <stddef.h>

typedef enum {
JJ_GPIO_UNASSIGNED, JJ_GPIO_PROVISIONAL, JJ_GPIO_BLOCKED, JJ_GPIO_VERIFIED
} jj_gpio_status_t;

typedef struct {
const char *role;
int gpio; // -1 means unavailable, never a usable assignment.
jj_gpio_status_t status;
} jj_board_role_t;

typedef struct {
const char *profile;
bool production;
bool heater_available;
const jj_board_role_t *roles;
size_t role_count;
} jj_board_t;

const jj_board_t *jj_board_profile(void);
const char *jj_gpio_status_name(jj_gpio_status_t status);
30 changes: 30 additions & 0 deletions components/jj_board/jj_board.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "jj_board.h"

static const jj_board_role_t roles[] = {
{"heater_enable", -1, JJ_GPIO_UNASSIGNED},
{"fan_power", -1, JJ_GPIO_UNASSIGNED},
{"fan_pwm", -1, JJ_GPIO_UNASSIGNED},
{"fan_tach", -1, JJ_GPIO_UNASSIGNED},
{"temperature_input", -1, JJ_GPIO_UNASSIGNED},
};

const jj_board_t *jj_board_profile(void)
{
// A build profile, not runtime detection of the connected module.
static const jj_board_t board = {
"tinys3d_candidate", false, false, roles, sizeof(roles) / sizeof(roles[0])
};
return &board;
}

const char *jj_gpio_status_name(jj_gpio_status_t status)
{
switch (status) {
case JJ_GPIO_UNASSIGNED: return "unassigned";
case JJ_GPIO_PROVISIONAL: return "provisional";
case JJ_GPIO_BLOCKED: return "blocked";
case JJ_GPIO_VERIFIED: return "verified";
default: return "unknown";
}
}
6 changes: 6 additions & 0 deletions components/jj_bringup/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
idf_component_register(
SRCS "jj_bringup.c" "jj_bringup_json.c"
INCLUDE_DIRS "include"
REQUIRES jj_board json dc_wifi esp_app_format esp_hw_support spi_flash heap esp_netif esp_system esp_timer esp_wifi esp_psram
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror)
44 changes: 44 additions & 0 deletions components/jj_bringup/include/jj_bringup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "cJSON.h"
#include <stdbool.h>
#include <stdint.h>

typedef enum {
JJ_PSRAM_NOT_ATTEMPTED, JJ_PSRAM_UNAVAILABLE, JJ_PSRAM_INITIALIZED, JJ_PSRAM_UNKNOWN
} jj_psram_state_t;

typedef struct {
const char *idf_version;
const char *project;
const char *version;
int chip_model; // ESP-IDF esp_chip_model_t numeric value.
unsigned chip_revision; // ESP-IDF: major * 100 + minor.
unsigned chip_cores;
int reset_reason; // ESP-IDF esp_reset_reason_t numeric value.
bool flash_known;
uint32_t flash_bytes;
const char *configured_flash_image_mode;
const char *configured_flash_image_frequency;
jj_psram_state_t psram_state;
uint32_t psram_bytes;
uint64_t sampled_at_ms;
} jj_boot_observation_t;

typedef enum { JJ_OBSERVATION_UNKNOWN, JJ_OBSERVATION_FALSE, JJ_OBSERVATION_TRUE } jj_observation_t;
typedef struct {
uint64_t sampled_at_ms;
uint64_t sample_finished_at_ms;
uint32_t free_internal_8bit_bytes;
uint32_t minimum_free_internal_8bit_bytes;
const char *wifi_state;
jj_observation_t sta_associated;
jj_observation_t sta_ipv4_available;
jj_observation_t dhcp_client_started;
} jj_runtime_observation_t;

// Called once before the portal starts; no probing or memory initialization.
void jj_bringup_init(void);
cJSON *jj_bringup_snapshot_json(void);
// Pure serialization boundary, shared with host fixtures.
cJSON *jj_bringup_json(const jj_boot_observation_t *boot, const jj_runtime_observation_t *runtime);
87 changes: 87 additions & 0 deletions components/jj_bringup/jj_bringup.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "jj_bringup.h"
#include "dc_wifi.h"
#include "esp_app_desc.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_heap_caps.h"
#include "esp_netif.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_wifi.h"
#include "sdkconfig.h"
#if CONFIG_SPIRAM
#include "esp_psram.h"
#endif

static jj_boot_observation_t s_boot;

void jj_bringup_init(void)
{
const esp_app_desc_t *app = esp_app_get_description();
esp_chip_info_t chip;
esp_chip_info(&chip);
s_boot = (jj_boot_observation_t) {
.idf_version = esp_get_idf_version(), .project = app->project_name,
.version = app->version, .chip_model = chip.model,
.chip_revision = chip.revision, .chip_cores = chip.cores,
.reset_reason = esp_reset_reason(),
.configured_flash_image_mode = CONFIG_ESPTOOLPY_FLASHMODE,
.configured_flash_image_frequency = CONFIG_ESPTOOLPY_FLASHFREQ,
.psram_state = JJ_PSRAM_NOT_ATTEMPTED,
.sampled_at_ms = (uint64_t)(esp_timer_get_time() / 1000),
};
s_boot.flash_known = esp_flash_get_physical_size(NULL, &s_boot.flash_bytes) == ESP_OK;
#if CONFIG_SPIRAM
if (esp_psram_is_initialized()) {
s_boot.psram_state = JJ_PSRAM_INITIALIZED;
s_boot.psram_bytes = (uint32_t)esp_psram_get_size();
} else {
#if CONFIG_SPIRAM_BOOT_INIT
s_boot.psram_state = JJ_PSRAM_UNAVAILABLE;
#else
s_boot.psram_state = JJ_PSRAM_NOT_ATTEMPTED;
#endif
}
#endif
}

static const char *wifi_state_name(dc_wifi_state_t state)
{
switch (state) {
case DC_WIFI_STATE_INIT: return "init";
case DC_WIFI_STATE_STA_CONNECTING: return "sta_connecting";
case DC_WIFI_STATE_STA_CONNECTED: return "sta_connected";
case DC_WIFI_STATE_AP_PORTAL: return "ap_portal";
default: return "unknown";
}
}

cJSON *jj_bringup_snapshot_json(void)
{
jj_runtime_observation_t runtime = {
.sampled_at_ms = (uint64_t)(esp_timer_get_time() / 1000),
.free_internal_8bit_bytes = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
.minimum_free_internal_8bit_bytes = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT),
.wifi_state = wifi_state_name(dc_wifi_state()),
};
wifi_ap_record_t ap;
esp_err_t association = esp_wifi_sta_get_ap_info(&ap);
if (association == ESP_OK) runtime.sta_associated = JJ_OBSERVATION_TRUE;
else if (association == ESP_ERR_WIFI_NOT_CONNECT) runtime.sta_associated = JJ_OBSERVATION_FALSE;
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (sta) {
esp_netif_ip_info_t ip = {0};
if (esp_netif_get_ip_info(sta, &ip) == ESP_OK) {
// IP may be cached after disconnection; do not present that as live.
runtime.sta_ipv4_available = esp_netif_is_netif_up(sta) && ip.ip.addr != 0 &&
runtime.sta_associated == JJ_OBSERVATION_TRUE ? JJ_OBSERVATION_TRUE :
runtime.sta_associated == JJ_OBSERVATION_UNKNOWN ? JJ_OBSERVATION_UNKNOWN : JJ_OBSERVATION_FALSE;
}
esp_netif_dhcp_status_t dhcp;
if (esp_netif_dhcpc_get_status(sta, &dhcp) == ESP_OK)
runtime.dhcp_client_started = dhcp == ESP_NETIF_DHCP_STARTED ? JJ_OBSERVATION_TRUE : JJ_OBSERVATION_FALSE;
}
runtime.sample_finished_at_ms = (uint64_t)(esp_timer_get_time() / 1000);
return jj_bringup_json(&s_boot, &runtime);
}
82 changes: 82 additions & 0 deletions components/jj_bringup/jj_bringup_json.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "jj_bringup.h"
#include "jj_board.h"

static void observed_bool(cJSON *object, const char *key, jj_observation_t value)
{
if (value == JJ_OBSERVATION_TRUE || value == JJ_OBSERVATION_FALSE)
cJSON_AddBoolToObject(object, key, value == JJ_OBSERVATION_TRUE);
else cJSON_AddNullToObject(object, key);
}

static const char *psram_status(jj_psram_state_t state)
{
switch (state) {
case JJ_PSRAM_NOT_ATTEMPTED: return "not_attempted";
case JJ_PSRAM_UNAVAILABLE: return "unavailable";
case JJ_PSRAM_INITIALIZED: return "initialized";
default: return "unknown";
}
}

cJSON *jj_bringup_json(const jj_boot_observation_t *boot, const jj_runtime_observation_t *runtime)
{
if (!boot || !runtime) return NULL;
cJSON *root = cJSON_CreateObject();
if (!root) return NULL;
cJSON_AddNumberToObject(root, "schema", 1);
const jj_board_t *profile = jj_board_profile();
cJSON *board = cJSON_AddObjectToObject(root, "board");
cJSON_AddStringToObject(board, "provenance", "configured_profile_not_detection");
cJSON_AddStringToObject(board, "profile", profile->profile);
cJSON_AddStringToObject(board, "status", "candidate");
cJSON_AddBoolToObject(board, "production", profile->production);
cJSON_AddBoolToObject(board, "heater_available", profile->heater_available);
cJSON *roles = cJSON_AddArrayToObject(board, "gpio_roles");
for (size_t i = 0; i < profile->role_count; ++i) {
const jj_board_role_t *role = &profile->roles[i];
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "role", role->role);
if (role->gpio < 0) cJSON_AddNullToObject(item, "gpio");
else cJSON_AddNumberToObject(item, "gpio", role->gpio);
cJSON_AddStringToObject(item, "status", jj_gpio_status_name(role->status));
cJSON_AddItemToArray(roles, item);
}
cJSON *b = cJSON_AddObjectToObject(root, "boot");
cJSON_AddStringToObject(b, "provenance", "esp_idf_boot_snapshot");
cJSON_AddNumberToObject(b, "sampled_at_uptime_ms", (double)boot->sampled_at_ms);
cJSON_AddStringToObject(b, "idf_version", boot->idf_version);
cJSON_AddStringToObject(b, "project", boot->project);
cJSON_AddStringToObject(b, "version", boot->version);
cJSON_AddNumberToObject(b, "chip_model_id", boot->chip_model);
cJSON_AddNumberToObject(b, "chip_revision_major", boot->chip_revision / 100);
cJSON_AddNumberToObject(b, "chip_revision_minor", boot->chip_revision % 100);
cJSON_AddNumberToObject(b, "chip_cores", boot->chip_cores);
cJSON_AddNumberToObject(b, "reset_reason_id", boot->reset_reason);
if (boot->flash_known) cJSON_AddNumberToObject(b, "detected_flash_bytes", boot->flash_bytes);
else cJSON_AddNullToObject(b, "detected_flash_bytes");
cJSON_AddStringToObject(b, "configured_flash_image_mode", boot->configured_flash_image_mode);
cJSON_AddStringToObject(b, "configured_flash_image_frequency", boot->configured_flash_image_frequency);
cJSON *psram = cJSON_AddObjectToObject(b, "psram");
cJSON_AddStringToObject(psram, "initialization", psram_status(boot->psram_state));
cJSON_AddStringToObject(psram, "physical_presence", "unknown");
// Successful initialization is positive evidence. Disabled/failed is not absence.
if (boot->psram_state == JJ_PSRAM_INITIALIZED) {
cJSON_SetValuestring(cJSON_GetObjectItemCaseSensitive(psram, "physical_presence"), "detected");
cJSON_AddNumberToObject(psram, "detected_bytes", boot->psram_bytes);
} else cJSON_AddNullToObject(psram, "detected_bytes");
cJSON_AddStringToObject(psram, "self_test", "not_attempted");
cJSON *r = cJSON_AddObjectToObject(root, "runtime");
cJSON_AddStringToObject(r, "provenance", "esp_idf_and_dc_wifi_request_sample");
cJSON_AddNumberToObject(r, "sampled_at_uptime_ms", (double)runtime->sampled_at_ms);
cJSON_AddNumberToObject(r, "sample_finished_at_uptime_ms", (double)runtime->sample_finished_at_ms);
cJSON_AddNumberToObject(r, "free_internal_8bit_heap_bytes", runtime->free_internal_8bit_bytes);
cJSON_AddNumberToObject(r, "minimum_free_internal_8bit_heap_bytes", runtime->minimum_free_internal_8bit_bytes);
cJSON *wifi = cJSON_AddObjectToObject(r, "wifi");
cJSON_AddStringToObject(wifi, "core_state", runtime->wifi_state);
cJSON_AddStringToObject(wifi, "provisioning_completion", "unknown");
observed_bool(wifi, "sta_associated", runtime->sta_associated);
observed_bool(wifi, "sta_ipv4_available", runtime->sta_ipv4_available);
observed_bool(wifi, "dhcp_client_started", runtime->dhcp_client_started);
return root;
}
4 changes: 2 additions & 2 deletions components/jj_portal/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
idf_component_register(
SRCS "jj_portal.c"
SRCS "jj_portal.c" "jj_config.c"
INCLUDE_DIRS "include"
REQUIRES jj_identity jj_interlock dc_portal dc_prusa dc_wifi json esp_http_server app_update nvs_flash
REQUIRES jj_bringup jj_identity jj_interlock dc_portal dc_prusa dc_wifi json esp_http_server app_update nvs_flash
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror)
7 changes: 7 additions & 0 deletions components/jj_portal/include/jj_config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "cJSON.h"
#include "esp_err.h"
#include <stddef.h>
cJSON *jj_config_describe(void *ctx);
esp_err_t jj_config_apply(const cJSON *values, void *ctx, char *message, size_t message_size);
Loading