Skip to content

Add staged PID control architecture - #9

Open
danielbrownjr wants to merge 3 commits into
feat/control-authority-modelfrom
feat/pid-control-architecture
Open

danielbrownjr wants to merge 3 commits into
feat/control-authority-modelfrom
feat/pid-control-architecture

Conversation

@danielbrownjr

Copy link
Copy Markdown
Collaborator

Stacked on #7. Base is feat/control-authority-model, not main. This consumes the control-authority semantics introduced in #7 (CONTROL_NO_AUTHORITY, STATE_REFRESH_REQUIRED, AUTO_AUTHORITY_UNAVAILABLE, active authority, control inhibit) and must not be merged independently of its base.

Summary

Implements Jump Jet's approved PID-family control architecture through the controller/interlock boundary. No heater actuator, GPIO, PWM, or time-proportioning is added. Production remains cold-safe.

  • New JumpJet-local jj_control layer over the pinned dragon-core dc_pid (same pin, 4e041d8; dc_pid added to main/idf_component.yml).
  • jj_interlock remains the sole heat-safety authority, now staged into two evaluations under one owner.
  • PID gains and timing are provisional scaffolding (zero gains; 0.25 s period mirroring the existing task cadence), not characterized tuning. Phase 3 plant evidence decides real values.

Architecture

All three steps run inside the existing authority critical section in jj_authority_control_step():

  1. Stage 1 — jj_interlock_evaluate() runs the single set of eligibility predicates and returns a dominant block reason (diagnostics/HMI only) plus a controller disposition aggregated over every applicable inhibit, with conservative precedence SUSPEND > HOLD > RUN. Diagnostic priority never decides disposition (e.g. fan-proof-pending + no-authority → SUSPEND even though fan_proof_pending is dominant). PR Add lease-safe control authority model #7's check order and fault latching are unchanged.
  2. jj_controller_step() consumes that eligibility without re-deriving any safety predicate: SUSPEND → IDLE + reset; HOLD → step with integration frozen, state preserved; RUN → step with integration.
  3. Stage 2 — jj_interlock_authorize() consumes the interlock-retained Stage-1 result exactly once, matched by a same-cycle sequence token. It authorizes only RUN with a VALID controller and a finite 0–100 % request. Stale, replayed, forged, or missing reports — and revocation or fault clear between the stages — fail cold (control_sequence_invalid).

Classification is an exhaustive switch with no default, so a new block reason fails the -Werror build until classified. Only FAN_PROOF_PENDING HOLDs; NOT_COMMISSIONED, MANUAL_TARGET_INVALID, CONTROL_NO_AUTHORITY, STATE_REFRESH_REQUIRED, and all other inhibits SUSPEND + RESET.

Controller lifecycle and outputs

  • Lifecycle is IDLE / VALID / INVALID. IDLE is ordinary; INVALID means a step was expected but dc_pid rejected it.
  • A latched fault keeps the controller IDLE for the entire latch duration; recovery remains the existing safe-OFF clear path.
  • AUTOMATIC remains IDLE: no Phase-4 chamber-target policy exists yet.
  • heater_requested is replaced by distinct requested_duty_pct (raw, unmodified dc_pid output; 0 unless VALID), heater_authorized, and allowed_duty_pct (0 unless authorized). No product code compares temperature to target — there is no target-crossing override outside dc_pid.
  • No delivered-duty field or product duty limiter is added (actuator layer and limiter ownership remain open).

HMI

The primary panel now shows Controller (Idle / Running / Invalid), Requested duty, and Allowed duty. INVALID uses the existing fault styling and stays visible even when the dominant constraint is fan_proof_pending (HOLD + INVALID). Unknown/missing controller data never renders as healthy. Messages added for controller_invalid and control_sequence_invalid. Raw P/I/D terms stay out of the primary UI.

OTA

The guard is unchanged and still requires both !heater_authorized and !thermal_management_required — "unauthorized" alone is not "thermally safe". Regression tests prove the Stage-1 de-authorized snapshot preserves thermal_management_required (including with cooldown or fault thermal management asserted), and a 1,152-case sweep shows Stage 1 and Stage 2 always agree on it, so the staged window cannot create a false OTA-safe condition.

Validation

  • Browser tests: 17/17 pass.
  • Interlock, authority, and control host suites pass.
  • All 7 contract/static checks pass (including a new controller/safety-boundary check); cppcheck clean with CI flags; git diff --check clean.
  • Mutation testing: controller/interlock 12/12 killed, HMI 7/7 killed, OTA-window 2/2 killed.
  • Local ESP32-S3 build under ESP-IDF v5.3.5: 0 warnings / 0 errors, no ledc/mcpwm/gpio_set_level symbols in the image. That sandbox could not reach the Espressif component registry, so espressif/mdns was supplied via a local, uncommitted override_path. GitHub CI is the authoritative registry-backed target build.
  • Host tests fetch dc_pid at the exact commit pinned in main/idf_component.yml (tests/fetch_dc_pid.sh), so there is no second pin.

Known follow-ups (not in this PR)

Commits

  • b7123d2 feat(control): stage interlock authorization around a PID-family controller
  • 20da76a docs(architecture): describe the staged PID-family control loop
  • 5749120 fix(portal): show controller state and duty; test OTA across staged window

…roller

Introduce jj_control, a product-local layer over the pinned dragon-core
dc_pid, and split jj_interlock into two stages under a single safety
owner:

- Stage 1 (jj_interlock_evaluate) runs the one set of eligibility
  predicates and returns a dominant block reason for diagnostics plus a
  controller disposition aggregated over every applicable inhibit
  (SUSPEND > HOLD > RUN). Check order and fault latching are unchanged;
  diagnostic priority no longer decides controller lifecycle.
- jj_controller_step consumes that result: SUSPEND idles and resets,
  HOLD steps with integration frozen, RUN steps with integration. It
  holds no fan/fault/printer/authority/sensor predicates.
- Stage 2 (jj_interlock_authorize) consumes the interlock-retained
  Stage-1 evaluation exactly once, matched by sequence token, and
  authorizes only RUN with a VALID controller and a finite 0-100 %
  request. Stale, replayed, or unknown tokens, and revocation or fault
  clear between the stages, fail cold.

Controller lifecycle is IDLE/VALID/INVALID. requested_duty_pct is the
unmodified dc_pid output (0 unless VALID); heater_authorized and
allowed_duty_pct replace heater_requested. No product code compares
temperature to target. Only fan-proof-pending HOLDs; NOT_COMMISSIONED,
MANUAL_TARGET_INVALID, and every other inhibit SUSPEND via an exhaustive
switch. AUTOMATIC stays idle with no setpoint.

Both stages and the controller run inside the existing authority
critical section. Gains are zero and the sample period mirrors the
current task cadence; both are provisional pending Phase 3. No heater
actuation is added.

Host tests cover lifecycle transitions, overlap aggregation, provenance,
and a dc_pid shadow trajectory through target crossing; a static contract
check guards the controller/safety boundary. Host tests fetch dc_pid at
the commit pinned in main/idf_component.yml.
Document Stage 1 eligibility, controller disposition, and Stage 2
authorization, the IDLE/VALID/INVALID lifecycle, and which parameters
remain provisional pending Phase 3 evidence.
…indow

Primary HMI:
- Render controller state (Idle / Running / Invalid), requested duty,
  and allowed duty from the existing API fields via a pure
  heaterPresentation() helper. Idle and Running use ordinary styling;
  Invalid reuses the fault style and stays visible regardless of the
  dominant constraint, so HOLD + INVALID is not shown as a healthy hold.
  Missing or unknown controller data renders as "Unknown", never healthy.
- Add operator messages for controller_invalid and
  control_sequence_invalid. No new fault or block reason.

OTA regression coverage (guard unchanged; still requires both
!heater_authorized and !thermal_management_required):
- After an authorized cycle, Stage 1's de-authorized snapshot keeps
  thermal_management_required, including with cooldown_required or
  fault_requires_thermal_management set.
- A sweep shows Stage-1 and Stage-2 snapshots always agree on
  thermal_management_required, so the staged window cannot create an
  OTA-safe condition the final state lacks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant