Skip to content
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/firmware-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,24 @@ jobs:
- name: Test heater interlock state machine
run: ./tests/run_interlock_host_test.sh

- name: Test control authority state machine
run: ./tests/run_authority_host_test.sh

- name: Test browser control client
run: node --test tests/control-client.test.cjs

- name: Check browser JavaScript syntax
run: node --check components/jj_portal/web/control-client.js

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

- name: Check API and product contract
run: sh tests/check_api_contract.sh

- name: Check control ordering, locking, and fault policy
run: sh tests/check_control_safety_contract.sh

- name: Enforce production actuation allowlist
run: sh tests/check_actuation_allowlist.sh

Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ heater safety decisions local to Jump Jet.
- `dc_wifi`, `dc_portal`, `dc_ui`, `dc_evlog`, and read-only `dc_prusa` pinned to
dragon-core v0.32.0 (`4e041d864763d468a50e9649807827dd83dd54bc`)
- exactly three product modes: OFF, MANUAL, and AUTOMATIC; boot/reset is always OFF
- orthogonal NONE/REMOTE/AUTOMATIC/REACQUIRING authority, recoverable control
inhibits, latched safety faults, and autonomous cooldown state
- lease-bound MANUAL demand with explicit takeover, atomic revision-validated
mutations, and a reacquire-plus-refresh requirement after control loss
- product-local AUTOMATIC authority that does not depend on a browser heartbeat
- manual target policy of 30–50 °C inclusive with a 45 °C default and rejection,
never clamping, outside that range
- `dc_prusa`'s existing 15-second freshness result used directly, with no second
Expand Down Expand Up @@ -66,6 +71,9 @@ fault-injection evidence. The browser and API are never the safety boundary.

```sh
./tests/run_interlock_host_test.sh
./tests/run_authority_host_test.sh
node --test tests/control-client.test.cjs
sh tests/check_control_safety_contract.sh
sh tests/check_identity_contract.sh
sh tests/check_api_contract.sh
sh tests/check_actuation_allowlist.sh
Expand All @@ -82,6 +90,7 @@ For ESP-IDF 5.3 or newer:

- [Product and safety contract](docs/PRODUCT_SAFETY_CONTRACT.md)
- [Firmware architecture](docs/ARCHITECTURE.md)
- [Control-authority and mutation API](docs/CONTROL_AUTHORITY.md)
- [Hardware baseline](docs/HARDWARE_BASELINE.md)
- [CZ4060 characterization](docs/hardware/cz4060-characterization.md)
- [Safety verification](docs/SAFETY_VERIFICATION.md)
Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ for every implemented fault. No printer installation at this phase.
- allowlist exact `PRINTING` and fail all other states cold
- derive and implement the bed-target-to-chamber-target production policy; keep
AUTOMATIC unavailable until then
- implement validated, revision-aware API mutations and product settings
- integrate the implemented validated, generation/revision-aware API mutations
with release-intent sensing and persistent product settings
- show temperatures, target, duty, fans, mode, PrusaLink state, bed target,
interlocks, faults, firmware, and network state
- advertise heating capabilities only after the corresponding hardware is commissioned
Expand Down
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ implementation starts, and keep only the summary checkbox here.

## UI, API, and OTA

- [ ] Finalize Jump Jet API v2 state and mutation schemas
- [x] Define Jump Jet API v2 control-authority state and atomic mutation schemas
- [ ] Add chamber, outlet, and case temperature telemetry with sensor status
- [ ] Add target, heater duty, both fan states, mode, and commissioning state
- [ ] Add PrusaLink connection, printer state, bed temperature/target, and sample age
- [ ] Define optional Klipper/Moonraker integration for read-only printer state and bed-target telemetry, with the same stale-data fail-cold semantics as PrusaLink
- [ ] Add visible interlocks, latched fault reason, and safe recovery guidance
- [ ] Add settings for PrusaLink and the bed-target policy with strict bounds
- [ ] Add revision/lease semantics for remote heat commands if manual remote heat is retained
- [x] Add generation/revision/lease semantics for remote Manual commands
- [ ] Test immediate pre-upload OTA rejection while heating
- [ ] Test the second heater-state guard before selecting the uploaded image for boot
- [ ] Test wrong-product, corrupt, interrupted, and rollback OTA paths
Expand Down
6 changes: 6 additions & 0 deletions components/jj_authority/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
idf_component_register(
SRCS "jj_authority.c"
INCLUDE_DIRS "include"
REQUIRES jj_interlock
)
target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror)
129 changes: 129 additions & 0 deletions components/jj_authority/include/jj_authority.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include "jj_interlock.h"
#include <stdbool.h>
#include <stdint.h>

#define JJ_CONTROL_LEASE_ID_LEN 32
#define JJ_CONTROL_OWNER_LEN 31
#define JJ_CONTROL_REQUEST_ID_LEN 63
#define JJ_CONTROL_REQUEST_CACHE_SIZE 8
#define JJ_REMOTE_LEASE_TTL_MS 60000U

typedef enum {
JJ_CONTROL_OK = 0,
JJ_CONTROL_AUTHORITY_REQUIRED,
JJ_CONTROL_AUTHORITY_LOST,
JJ_CONTROL_GENERATION_STALE,
JJ_CONTROL_REVISION_CONFLICT,
JJ_CONTROL_REQUEST_INELIGIBLE,
JJ_CONTROL_REQUEST_CONFLICT,
JJ_CONTROL_HARDWARE_FAULT,
JJ_CONTROL_INVALID,
} jj_control_result_t;

typedef enum {
JJ_MUTATION_OFF = 0,
JJ_MUTATION_MANUAL,
JJ_MUTATION_AUTOMATIC,
JJ_MUTATION_CLEAR_FAULT,
} jj_control_mutation_kind_t;

typedef enum {
JJ_CONTROL_LOSS_NONE = 0,
JJ_CONTROL_LOSS_REMOTE_LEASE_EXPIRED,
JJ_CONTROL_LOSS_EXPLICIT_TAKEOVER,
JJ_CONTROL_LOSS_REMOTE_REACQUIRED,
} jj_control_loss_reason_t;

typedef struct {
char lease_id[JJ_CONTROL_LEASE_ID_LEN + 1];
char owner[JJ_CONTROL_OWNER_LEN + 1];
bool takeover;
} jj_control_acquire_t;

typedef struct {
char lease_id[JJ_CONTROL_LEASE_ID_LEN + 1];
uint32_t generation;
uint32_t expected_revision;
char request_id[JJ_CONTROL_REQUEST_ID_LEN + 1];
jj_control_mutation_kind_t kind;
float target_c;
} jj_control_mutation_t;

typedef struct {
uint32_t revision;
uint32_t generation;
jj_mode_t mode;
float configured_target_c;
bool manual_demand_authorized;
jj_control_authority_t active_authority;
jj_control_inhibit_t control_inhibit;
bool lease_active;
char lease_id[JJ_CONTROL_LEASE_ID_LEN + 1];
char lease_owner[JJ_CONTROL_OWNER_LEN + 1];
uint32_t lease_expires_in_ms;
uint64_t last_control_loss_ms;
jj_control_loss_reason_t last_control_loss_reason;
} jj_control_snapshot_t;

typedef struct {
char request_id[JJ_CONTROL_REQUEST_ID_LEN + 1];
uint64_t signature;
jj_control_result_t result;
} jj_control_request_cache_entry_t;

typedef struct {
jj_control_snapshot_t state;
uint64_t lease_deadline_ms;
uint32_t lease_ttl_ms;
jj_interlock_t *interlock;
jj_control_request_cache_entry_t request_cache[JJ_CONTROL_REQUEST_CACHE_SIZE];
uint8_t next_request_cache_entry;
} jj_authority_t;

void jj_authority_init(
jj_authority_t *state,
jj_interlock_t *interlock,
uint32_t lease_ttl_ms);
jj_control_result_t jj_authority_acquire(
jj_authority_t *state,
const jj_control_acquire_t *request,
uint64_t now_ms,
jj_control_snapshot_t *out);
jj_control_result_t jj_authority_heartbeat(
jj_authority_t *state,
const char *lease_id,
uint32_t generation,
uint64_t now_ms,
jj_control_snapshot_t *out);
jj_control_result_t jj_authority_refresh(
jj_authority_t *state,
const char *lease_id,
uint32_t generation,
uint64_t now_ms,
jj_control_snapshot_t *out);
jj_control_result_t jj_authority_mutate(
jj_authority_t *state,
jj_interlock_t *interlock,
const jj_control_mutation_t *request,
const jj_inputs_t *authoritative_inputs,
uint64_t now_ms,
jj_control_snapshot_t *out);
bool jj_authority_tick(jj_authority_t *state, uint64_t now_ms);
void jj_authority_snapshot(
const jj_authority_t *state,
uint64_t now_ms,
jj_control_snapshot_t *out);
void jj_authority_apply_to_inputs(
const jj_control_snapshot_t *authority,
jj_inputs_t *inputs);
jj_outputs_t jj_authority_control_step(
jj_authority_t *state,
const jj_inputs_t *authoritative_inputs,
uint64_t now_ms,
jj_control_snapshot_t *out);
const char *jj_control_result_str(jj_control_result_t result);
const char *jj_mode_str(jj_mode_t mode);
const char *jj_control_loss_reason_str(jj_control_loss_reason_t reason);
Loading