Merge pull request 'Add Cédric LLM context pack, full firmware spec, and spindle direction contract' (#1) from docs/nick-llm-context-20260602 into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
14
README.md
14
README.md
@@ -17,7 +17,7 @@ shared contracts -> schemas, logs, machine capabilities, provenance
|
||||
|
||||
Normand can operate the polisher manually from the touchscreen, with:
|
||||
|
||||
- safe force/table/spindle control;
|
||||
- safe force/table/spindle control, including clockwise/counter-clockwise toolhead rotation selection;
|
||||
- KWR75B-CAN force/torque sensor integration;
|
||||
- ODrive S1 + M8325s spindle drive integration;
|
||||
- Teensy-side fast safety and setpoint/telemetry loop;
|
||||
@@ -37,11 +37,13 @@ The controller should be boring, deterministic, auditable, and conservative.
|
||||
|
||||
## Start here
|
||||
|
||||
1. Read [`docs/00-start-here.md`](docs/00-start-here.md).
|
||||
2. Read [`docs/02-v1-scope.md`](docs/02-v1-scope.md).
|
||||
3. Review the state machine and safety rules in [`docs/03-architecture.md`](docs/03-architecture.md).
|
||||
4. Use [`docs/08-commissioning-checklist.md`](docs/08-commissioning-checklist.md) during hardware bring-up.
|
||||
5. Track implementation using [`ROADMAP.md`](ROADMAP.md).
|
||||
1. Read [`docs/LLM_CONTEXT.md`](docs/LLM_CONTEXT.md) if you are Cédric's LLM/coding assistant or need a compact project brief.
|
||||
2. Read [`docs/00-start-here.md`](docs/00-start-here.md).
|
||||
3. Read [`docs/02-v1-scope.md`](docs/02-v1-scope.md).
|
||||
4. Review the state machine and safety rules in [`docs/03-architecture.md`](docs/03-architecture.md).
|
||||
5. Use [`docs/11-feature-request-intake.md`](docs/11-feature-request-intake.md) before adding new features.
|
||||
6. Use [`docs/08-commissioning-checklist.md`](docs/08-commissioning-checklist.md) during hardware bring-up.
|
||||
7. Track implementation using [`ROADMAP.md`](ROADMAP.md).
|
||||
|
||||
## Repository map
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
- [ ] Host↔Teensy serial link with ACK/NACK and CRC.
|
||||
- [ ] KWR75B-CAN receive path with measured sample rate and stale-frame watchdog.
|
||||
- [ ] Encoder acquisition for table and arm.
|
||||
- [ ] ODrive command/telemetry interface selected and documented.
|
||||
- [ ] ODrive command/telemetry interface selected and documented, including sign mapping for `cw`/`ccw` physical tool rotation.
|
||||
- [ ] Force actuator command path selected and documented.
|
||||
|
||||
## Phase 3 — Manual mode MVP
|
||||
|
||||
- [ ] Host UI/manual-mode workflow with geometric gate.
|
||||
- [ ] Manual setpoints: force, table RPM, spindle RPM, optional modulation.
|
||||
- [ ] Manual setpoints: force, table RPM, spindle RPM, spindle direction (`cw`/`ccw`), optional modulation.
|
||||
- [ ] Teensy inner loop: setpoint ramping, force PID, telemetry, fast interlocks.
|
||||
- [ ] Manual-session log and telemetry CSV written to `/data/manual/{session_id}/`.
|
||||
- [ ] Status file updated at `/data/status.json`.
|
||||
|
||||
@@ -30,4 +30,9 @@ Neither side invents polishing strategy.
|
||||
- test checklists;
|
||||
- telemetry schema clarifications;
|
||||
- state-machine/safety edge cases;
|
||||
- concise implementation notes in `docs/nick-generated/`.
|
||||
- concise implementation notes in `docs/nick-generated/`;
|
||||
- feature-request classification using `docs/11-feature-request-intake.md`.
|
||||
|
||||
## If you are using an LLM/coding assistant
|
||||
|
||||
Start with [`LLM_CONTEXT.md`](LLM_CONTEXT.md), then load the narrower docs relevant to the change you are making. Do not let the LLM infer optical strategy or change safety/protocol/telemetry contracts without Antoine approval.
|
||||
|
||||
@@ -36,6 +36,15 @@ Requirements:
|
||||
- `SEGMENT_DONE`
|
||||
- `ABORT_COMPLETE`
|
||||
|
||||
## Spindle direction field
|
||||
|
||||
`MANUAL_START`, `SETPOINT`, and future `SEGMENT_START` payloads include `spindle_direction` / `commanded_spindle_direction` with stable values:
|
||||
|
||||
- `cw` — clockwise
|
||||
- `ccw` — counter-clockwise
|
||||
|
||||
Physical convention: direction is observed from above the toolhead looking down toward the mirror/tool contact. The ODrive sign mapping is a commissioning item and must be configured so UI/protocol values match physical rotation. Direction changes are explicit setpoint changes; do not infer direction from a signed RPM. RPM remains a non-negative magnitude.
|
||||
|
||||
## v1 production subset
|
||||
|
||||
Manual v1 only needs:
|
||||
|
||||
@@ -34,6 +34,8 @@ spindle_motor_temp_c
|
||||
arm_angle_linearized_deg
|
||||
table_rpm_setpoint
|
||||
spindle_rpm_setpoint
|
||||
spindle_direction_setpoint
|
||||
spindle_direction_actual
|
||||
force_actuator_cmd
|
||||
estop_active
|
||||
interlock_state
|
||||
@@ -50,6 +52,7 @@ fz_contact_n
|
||||
- Sensor validity is explicit; never substitute fake good values.
|
||||
- Gaps are detectable from timestamps.
|
||||
- Header names are stable because downstream analysis will depend on them.
|
||||
- Spindle RPM remains a non-negative magnitude; spindle direction is logged separately as `cw` / `ccw` rather than encoded as signed RPM.
|
||||
|
||||
## CSV baseline
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- force N
|
||||
- table RPM
|
||||
- spindle RPM
|
||||
- spindle rotation direction: clockwise (`cw`) or counter-clockwise (`ccw`) as viewed from above the toolhead looking down toward the mirror/tool contact
|
||||
- optional force modulation harmonic/amplitude/phase
|
||||
7. Operator presses Start.
|
||||
8. Host sends `MANUAL_START`.
|
||||
@@ -27,6 +28,6 @@
|
||||
|
||||
- Manual mode cannot start with stale geometry.
|
||||
- Manual mode uses the same safety/interlocks as job mode.
|
||||
- Every setpoint change is logged.
|
||||
- Every setpoint change is logged, including spindle direction changes.
|
||||
- Telemetry always runs while force/motion is active.
|
||||
- Tool removal uses the documented mechanical sequence; no powered Zero-G in v1.
|
||||
|
||||
@@ -16,6 +16,7 @@ This checklist condenses the current P11 firmware/control spec for implementatio
|
||||
- [ ] Force PID tracks setpoint within agreed commissioning tolerance.
|
||||
- [ ] Force modulation uses live table encoder angle.
|
||||
- [ ] Table and spindle RPM follow commands.
|
||||
- [ ] Toolhead spindle direction follows clockwise/counter-clockwise operator/job selection and is logged explicitly.
|
||||
- [ ] Pause time excluded from segment polishing time.
|
||||
|
||||
## Telemetry
|
||||
@@ -39,7 +40,7 @@ This checklist condenses the current P11 firmware/control spec for implementatio
|
||||
## Manual mode
|
||||
|
||||
- [ ] `MANUAL` reachable from `IDLE`.
|
||||
- [ ] Live setpoint adjustment works.
|
||||
- [ ] Live setpoint adjustment works for force, table RPM, spindle RPM, spindle direction, and optional modulation.
|
||||
- [ ] Geometric gate blocks stale geometry.
|
||||
- [ ] Setpoint changes are timestamped events.
|
||||
- [ ] Manual-session log emitted on exit.
|
||||
|
||||
160
docs/11-feature-request-intake.md
Normal file
160
docs/11-feature-request-intake.md
Normal file
@@ -0,0 +1,160 @@
|
||||
---
|
||||
title: Polisher-Control Feature Request Intake
|
||||
status: draft
|
||||
requested_by: Antoine Letarte
|
||||
generated_by: Nick / Hermes
|
||||
project: P11-Polisher-Fullum
|
||||
repo: polisher-control
|
||||
source_truth: false
|
||||
created: 2026-06-02
|
||||
privacy: technical-only
|
||||
---
|
||||
|
||||
# Feature Request Intake — Polisher-Control
|
||||
|
||||
## Purpose
|
||||
|
||||
Use this note when Antoine or Cédric proposes a new feature for `polisher-control`.
|
||||
|
||||
The goal is to avoid coding a feature into the wrong layer. Some requests belong in the machine controller, some belong in `polisher-post`, and some belong in `polisher-sim`.
|
||||
|
||||
## Fast classification
|
||||
|
||||
Before implementation, classify the request.
|
||||
|
||||
| Class | Belongs where | Examples |
|
||||
|---|---|---|
|
||||
| Execution feature | `polisher-control` | manual UI behavior, host state machine, Teensy loop, drive/sensor integration, telemetry logging, alarms, `/data/` artifacts |
|
||||
| Contract feature | `polisher-control` + `shared/schemas` + usually `polisher-post` | new telemetry channel, new protocol field, new log field, new machine-capability property |
|
||||
| Planning/intelligence feature | `polisher-sim` / `polisher-post` | choosing pass strategy, metrology interpretation, calibration updates, optimization, dwell planning |
|
||||
| Safety/scope feature | approval-gated | interlock behavior, E-stop response, force limits, fault reset policy, remote operation, powered manipulation modes |
|
||||
|
||||
Rule: if the feature decides **what polishing should be done**, it is probably not a `polisher-control` feature. If it executes/logs/enforces what was already approved, it probably is.
|
||||
|
||||
## Intake template
|
||||
|
||||
Copy this block into an issue, PR, or `docs/nick-generated/YYYY-MM-DD-feature-name.md` when scoping a feature.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Feature Intake — <short feature name>
|
||||
status: draft
|
||||
requested_by: Antoine / Cédric
|
||||
generated_by: Nick / Hermes
|
||||
project: P11-Polisher-Fullum
|
||||
repo: polisher-control
|
||||
source_truth: false
|
||||
created: YYYY-MM-DD
|
||||
privacy: technical-only
|
||||
---
|
||||
|
||||
# Feature Intake — <short feature name>
|
||||
|
||||
## 1. Requested behavior
|
||||
|
||||
- What should the operator/system be able to do?
|
||||
- What problem does this solve?
|
||||
- Who requested it?
|
||||
|
||||
## 2. Classification
|
||||
|
||||
- [ ] Execution feature in `polisher-control`
|
||||
- [ ] Contract feature touching schemas/protocol/telemetry/logs
|
||||
- [ ] Planning/intelligence feature that belongs upstream
|
||||
- [ ] Safety/scope feature requiring Antoine approval
|
||||
|
||||
Rationale:
|
||||
|
||||
## 3. Operator-visible behavior
|
||||
|
||||
- UI controls / screens / prompts:
|
||||
- Normal workflow:
|
||||
- Fault/warning workflow:
|
||||
- What is logged as operator action:
|
||||
|
||||
## 4. Affected layers
|
||||
|
||||
- Host controller:
|
||||
- Teensy firmware:
|
||||
- Host ↔ Teensy protocol:
|
||||
- Telemetry channels:
|
||||
- Run/manual-session logs:
|
||||
- Machine capability profile:
|
||||
- Shared schemas:
|
||||
- Tests:
|
||||
- Docs/checklists:
|
||||
|
||||
## 5. Safety implications
|
||||
|
||||
- Does it affect force, motion, brakes, interlocks, E-stop, watchdog, or reset behavior?
|
||||
- Does it allow motion in a new condition?
|
||||
- Does it change any limit or threshold?
|
||||
- Does it require a new refusal/NACK reason?
|
||||
- Does it need a hardware interlock or physical confirmation?
|
||||
|
||||
Safety decision:
|
||||
|
||||
- [ ] No safety behavior change
|
||||
- [ ] Safety behavior change; Antoine approval required before implementation
|
||||
|
||||
## 6. Contract implications
|
||||
|
||||
- New/changed protocol messages:
|
||||
- New/changed telemetry channels:
|
||||
- New/changed log fields:
|
||||
- New/changed machine capability fields:
|
||||
- Backward compatibility / schema version impact:
|
||||
|
||||
Contract decision:
|
||||
|
||||
- [ ] No schema/protocol contract change
|
||||
- [ ] Contract change; update schemas/examples/docs/tests together
|
||||
|
||||
## 7. Acceptance checks
|
||||
|
||||
- [ ] Host-side unit test:
|
||||
- [ ] Firmware compile/build check:
|
||||
- [ ] Protocol encode/decode test:
|
||||
- [ ] Schema/example validation:
|
||||
- [ ] State-machine transition test:
|
||||
- [ ] Telemetry channel consistency check:
|
||||
- [ ] Safety refusal/fault test:
|
||||
- [ ] Commissioning checklist update:
|
||||
- [ ] Manual/operator workflow check:
|
||||
|
||||
## 8. Open questions
|
||||
|
||||
- For Antoine:
|
||||
- For Cédric:
|
||||
- For source-spec update:
|
||||
|
||||
## 9. Implementation notes
|
||||
|
||||
- Suggested branch:
|
||||
- Suggested first PR chunk:
|
||||
- Suggested follow-up chunk:
|
||||
```
|
||||
|
||||
## Approval gates
|
||||
|
||||
Ask Antoine before implementing when a request changes any of these:
|
||||
|
||||
- v1 scope boundary;
|
||||
- safety/interlock behavior;
|
||||
- force limits, braking, fault/reset policy;
|
||||
- host ↔ Teensy protocol semantics;
|
||||
- telemetry channel names, units, rates, or meanings;
|
||||
- schema versions or required fields;
|
||||
- boundary between `polisher-control`, `polisher-post`, and `polisher-sim`;
|
||||
- any powered manipulation / Zero-G / remote-control behavior.
|
||||
|
||||
## Implementation chunking recommendation
|
||||
|
||||
For most features, keep PRs small:
|
||||
|
||||
1. **Spec/doc PR:** feature intake, acceptance checks, schema/protocol sketch.
|
||||
2. **Host PR:** state machine / UI workflow / logs / tests.
|
||||
3. **Firmware PR:** message handling / loop behavior / telemetry / compile check.
|
||||
4. **Integration PR:** end-to-end dry-run, examples, commissioning checklist.
|
||||
|
||||
Do not mix a safety contract change with broad unrelated refactors.
|
||||
442
docs/LLM_CONTEXT.md
Normal file
442
docs/LLM_CONTEXT.md
Normal file
@@ -0,0 +1,442 @@
|
||||
---
|
||||
title: Polisher-Control LLM Context Pack
|
||||
status: draft
|
||||
requested_by: Antoine Letarte
|
||||
generated_by: Nick / Hermes
|
||||
project: P11-Polisher-Fullum
|
||||
repo: polisher-control
|
||||
source_truth: false
|
||||
created: 2026-06-02
|
||||
privacy: technical-only
|
||||
---
|
||||
|
||||
# Polisher-Control LLM Context Pack
|
||||
|
||||
## Purpose
|
||||
|
||||
This file is the **first prompt/context file** to give to Cédric's LLM or coding assistant when working in this repository.
|
||||
|
||||
It condenses the approved technical direction for `polisher-control` into one implementation-oriented context pack. It is an aid for coding and design discussions, not the authority itself.
|
||||
|
||||
If this file conflicts with the source specs or with Antoine's explicit direction, stop and ask Antoine before changing architecture, telemetry contracts, safety behavior, or v1 scope.
|
||||
|
||||
## Scope / non-scope
|
||||
|
||||
### In scope for `polisher-control`
|
||||
|
||||
`polisher-control` is the machine-side execution layer. It owns:
|
||||
|
||||
- Teensy firmware and hard-real-time loop.
|
||||
- Raspberry Pi / host controller and touchscreen/manual workflow.
|
||||
- Host ↔ Teensy protocol.
|
||||
- Force setpoint execution and closed-loop Fz control.
|
||||
- Table/spindle/arm-drive command interfaces.
|
||||
- KWR75B-CAN force/torque acquisition.
|
||||
- ODrive S1 + M8325s spindle integration.
|
||||
- State machine, pause/resume/abort, faults, alarms, interlocks.
|
||||
- Telemetry, event logs, manual-session logs, run artifacts, and `/data/` layout.
|
||||
- Conservative machine capability descriptor.
|
||||
|
||||
### Explicitly out of scope
|
||||
|
||||
Do **not** implement or infer:
|
||||
|
||||
- Optical figuring strategy.
|
||||
- Interferometer/metrology interpretation.
|
||||
- Dwell-map optimization or pass planning.
|
||||
- Preston coefficient calibration or learning logic.
|
||||
- Autonomous correction decisions.
|
||||
- Controller-side replanning.
|
||||
- Silent mutation of upstream setpoints.
|
||||
- Remote control of the machine without a physically present operator.
|
||||
|
||||
Core rule: **the controller executes approved setpoints safely; it does not decide polishing strategy.**
|
||||
|
||||
## Source references
|
||||
|
||||
Use repo docs first for implementation, then the source specs if there is ambiguity.
|
||||
|
||||
### Repo implementation docs
|
||||
|
||||
- `README.md` — repository overview and v1 finish line.
|
||||
- `AGENTS.md` — repo rules for LLM/coding agents.
|
||||
- `ROADMAP.md` — implementation roadmap.
|
||||
- `docs/00-start-here.md` — Cédric build brief.
|
||||
- `docs/01-ecosystem-boundaries.md` — `polisher-sim` / `polisher-post` / `polisher-control` split.
|
||||
- `docs/02-v1-scope.md` — v1 delivery boundary.
|
||||
- `docs/03-architecture.md` — host/firmware architecture, state machine, safety posture.
|
||||
- `docs/04-host-teensy-protocol-v1.md` — protocol requirements and message set.
|
||||
- `docs/05-telemetry-channel-spec-v1.md` — telemetry channels and rates.
|
||||
- `docs/06-event-alarm-codes-v1.md` — event/alarm codes.
|
||||
- `docs/07-manual-mode-workflow.md` — touchscreen manual workflow.
|
||||
- `docs/08-commissioning-checklist.md` — bring-up checklist.
|
||||
- `docs/09-acceptance-checklist.md` — pass/fail criteria.
|
||||
- `docs/10-open-questions-for-cedric.md` — implementation questions to close.
|
||||
- `shared/schemas/*.schema.json` — contract schemas mirrored into this repo.
|
||||
- `shared/machine/fullum-alpha.capabilities.v1.json` — draft capability profile.
|
||||
|
||||
### Upstream source-truth docs
|
||||
|
||||
These live in Antoine's P11 project vault and outrank this generated context pack:
|
||||
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/_curation/CONTEXT.md`
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/README.md`
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/software-suite/control/firmware/Fullum-Polisher-Machine-Control-Firmware-Spec-v1.md`
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/05-Implementation/Controller-Bridge-Digital-Twin-Architecture-Plan.md`
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/05-Implementation/Polisher-Control/00-Polisher-Control-System.md`
|
||||
- `/home/papa/obsidian-vault/2-Projects/P11-Polisher-Fullum/05-Implementation/Polisher-Control/01-Polisher-Control-Roadmap.md`
|
||||
|
||||
## Architecture summary
|
||||
|
||||
```text
|
||||
polisher-sim
|
||||
planning / digital twin / metrology / calibration / process intelligence
|
||||
↓
|
||||
polisher-post
|
||||
validation / machine-capability checks / controller-job packaging / log import
|
||||
↓
|
||||
polisher-control
|
||||
safe deterministic machine execution / telemetry / operator workflow
|
||||
↓
|
||||
run logs + manual telemetry return upstream for analysis and calibration
|
||||
```
|
||||
|
||||
### `polisher-sim`
|
||||
|
||||
Owns the question: **what should be done next?**
|
||||
|
||||
It may ingest interferometer maps, model removal, plan passes, estimate uncertainty, calibrate machine-specific behavior, and produce frozen job packages. It must not drive hardware directly.
|
||||
|
||||
### `polisher-post`
|
||||
|
||||
Owns the question: **how do we express the approved plan safely for this machine?**
|
||||
|
||||
It validates schemas, checks machine capabilities, normalizes units, records translation losses, and emits controller-safe `controller-job.v1` packages.
|
||||
|
||||
### `polisher-control`
|
||||
|
||||
Owns the question: **how do we execute these approved setpoints safely now?**
|
||||
|
||||
It runs the machine, enforces safety, records exact reality, and exports artifacts. It must be boring, predictable, auditable, and conservative.
|
||||
|
||||
## v1 finish line
|
||||
|
||||
The v1 finish line is:
|
||||
|
||||
> Normand can operate the polisher manually from the touchscreen, producing clean synchronized telemetry, with safety enforced.
|
||||
|
||||
Program execution exists as a contract scaffold and future path, but the v1 production surface is manual operation.
|
||||
|
||||
### v1 priority order
|
||||
|
||||
1. Safety/interlocks and deterministic state machine.
|
||||
2. Manual mode from touchscreen / host UI.
|
||||
3. Stable host ↔ Teensy setpoint and telemetry protocol.
|
||||
4. KWR75B-CAN force/torque acquisition and stale-frame detection.
|
||||
5. Table/arm encoder acquisition and synchronized timestamps.
|
||||
6. ODrive S1 + M8325s spindle command/telemetry path.
|
||||
7. Run/manual logs and `/data/` file layout.
|
||||
8. Controller-job intake dry-run for future program execution.
|
||||
|
||||
### v1 must include
|
||||
|
||||
- `MANUAL` state reachable from `IDLE` only.
|
||||
- Operator live controls for force, table RPM, spindle RPM, spindle rotation direction (`cw`/`ccw`), and optional force modulation.
|
||||
- Mandatory geometric gate before `MANUAL` or `RUNNING`.
|
||||
- Full telemetry at **≥100 Hz** with a single Teensy monotonic timestamp source.
|
||||
- Manual-session log emitted on exit.
|
||||
- Same safety/interlocks in manual mode as job mode.
|
||||
- KWR75B-CAN sensor status and stale-frame watchdog.
|
||||
- Tool-weight compensation using a configured `fz_tool_weight_offset_n`.
|
||||
- Mechanical safe-removal workflow for the tool/head.
|
||||
|
||||
### v1 must not include unless Antoine explicitly reopens it
|
||||
|
||||
- Powered Zero-G / admittance manipulation mode.
|
||||
- Controller-side optical strategy.
|
||||
- Controller-side calibration/learning.
|
||||
- Remote control of the machine.
|
||||
- Autonomous multi-pass orchestration.
|
||||
- Metrology import/interpretation in the controller.
|
||||
|
||||
## Hardware basis to preserve
|
||||
|
||||
These are current project facts unless Antoine/source specs supersede them:
|
||||
|
||||
- Real-time controller: **Teensy 4.1**.
|
||||
- Host: **Raspberry Pi 4 + touchscreen**.
|
||||
- Primary process force sensor: **Kunwei KWR75B-CAN**, dedicated CAN link to Teensy through isolated transceiver.
|
||||
- Table encoder: **RLS Artos DHR 162 mm**, 20-bit absolute, SSI/RS422 basis.
|
||||
- Arm encoder: **RLS Orbis BR10**, 14-bit absolute, SSI/RS422 basis.
|
||||
- Spindle: **ODrive S1 + M8325s 100KV**, v1 velocity control, CAN 2.0B at 1 Mbps preferred.
|
||||
- Z/force actuator: counterweight-biased **AutomationDirect SV2L-210B** servo with **SV2A-2150** drive, torque/current mode to confirm.
|
||||
- Z brake: NC 24 VDC electromagnetic brake; engages on E-stop or power loss.
|
||||
- Safety relay: Pilz PNOZ X1 or Banner XS26-2 still needs final selection.
|
||||
- Powered Zero-G is **not** part of v1.
|
||||
|
||||
## State machine context
|
||||
|
||||
Required states:
|
||||
|
||||
- `IDLE`
|
||||
- `JOB_LOADED`
|
||||
- `READY`
|
||||
- `RUNNING`
|
||||
- `PAUSED`
|
||||
- `ABORTING`
|
||||
- `COMPLETED`
|
||||
- `ABORTED`
|
||||
- `FAULTED`
|
||||
- `MANUAL`
|
||||
|
||||
Rules:
|
||||
|
||||
- `FAULTED` exits only through explicit operator reset.
|
||||
- Illegal transitions are rejected and logged.
|
||||
- Every transition emits an event.
|
||||
- `JOB_LOADED → READY` requires explicit operator acknowledge.
|
||||
- `IDLE → MANUAL` requires the geometric gate.
|
||||
- Cannot enter `MANUAL` with a job loaded.
|
||||
- Cannot load a job while in `MANUAL`.
|
||||
- A fault in `MANUAL` transitions to `FAULTED`.
|
||||
|
||||
## Manual mode workflow
|
||||
|
||||
Shop-floor sequence:
|
||||
|
||||
1. Hardware HOA selector in **Auto** so Teensy/RPi are active.
|
||||
2. Operator mechanically sets arm amplitude and center on the machine.
|
||||
3. Operator opens **Manual Mode** on touchscreen.
|
||||
4. UI presents mandatory geometric gate for:
|
||||
- `r_menante`
|
||||
- `L_menee`
|
||||
- `R_tool`
|
||||
- `configured_arm_amplitude_deg`
|
||||
- `configured_arm_center_deg`
|
||||
5. Operator confirms each value; no skip path.
|
||||
6. Operator enters initial force, table RPM, spindle RPM, spindle rotation direction (`cw`/`ccw`), optional modulation.
|
||||
7. Host sends `MANUAL_START`; Teensy ACK/NACKs.
|
||||
8. Telemetry begins at ≥100 Hz.
|
||||
9. Operator adjusts live setpoints; each change is logged as an event.
|
||||
10. Operator presses Stop; host sends `MANUAL_STOP`.
|
||||
11. Machine ramps down, returns to `IDLE`, and writes `manual-session-log.v1` + telemetry CSV.
|
||||
|
||||
## Telemetry contract
|
||||
|
||||
### Principles
|
||||
|
||||
- One monotonic Teensy clock anchors all telemetry.
|
||||
- Raw values are logged; filtering/analysis happens upstream.
|
||||
- Commanded/setpoint and actual/measured values stay distinct.
|
||||
- Sensor faults are explicit: validity flags, NaN, sentinel, or event/alarm.
|
||||
- Missing samples must be detectable from timestamps.
|
||||
- Header/channel names must remain stable; future tools will key off them.
|
||||
|
||||
### Core required channels at ≥100 Hz
|
||||
|
||||
- `timestamp_us`
|
||||
- `table_angle_deg`
|
||||
- `arm_angle_deg`
|
||||
- `fz_n`
|
||||
- `mx`
|
||||
- `my`
|
||||
- `mz`
|
||||
- `spindle_rpm_actual`
|
||||
- `table_rpm_actual`
|
||||
- `arm_amplitude_deg_derived`
|
||||
- `arm_center_deg_derived`
|
||||
- `machine_state`
|
||||
|
||||
### Strongly recommended channels
|
||||
|
||||
- `fx_n`
|
||||
- `fy_n`
|
||||
- `ft_status`
|
||||
- `z_servo_iq_v`
|
||||
- `z_brake_engaged`
|
||||
- `spindle_drive_state`
|
||||
- `spindle_drive_error`
|
||||
- `spindle_bus_voltage_v`
|
||||
- `spindle_iq_a`
|
||||
- `spindle_motor_temp_c`
|
||||
- `arm_angle_linearized_deg`
|
||||
- `force_setpoint_n`
|
||||
- `table_rpm_setpoint`
|
||||
- `spindle_rpm_setpoint`
|
||||
- `spindle_direction_setpoint`
|
||||
- `spindle_direction_actual`
|
||||
- `force_actuator_cmd`
|
||||
- `estop_active`
|
||||
- `interlock_state`
|
||||
- `mode`
|
||||
|
||||
## Host ↔ Teensy protocol context
|
||||
|
||||
The protocol is a setpoint/telemetry protocol, not G-code.
|
||||
|
||||
### Host → Teensy messages
|
||||
|
||||
- `HEARTBEAT`
|
||||
- `SEGMENT_START`
|
||||
- `SETPOINT`
|
||||
- `PAUSE`
|
||||
- `RESUME`
|
||||
- `ABORT`
|
||||
- `MANUAL_START`
|
||||
- `MANUAL_STOP`
|
||||
- `ESTOP`
|
||||
|
||||
### Teensy → Host messages
|
||||
|
||||
- `ACK`
|
||||
- `NACK`
|
||||
- `TELEMETRY`
|
||||
- `EVENT`
|
||||
- `SEGMENT_DONE`
|
||||
- `ABORT_COMPLETE`
|
||||
|
||||
### Protocol requirements
|
||||
|
||||
- Version on every frame.
|
||||
- Robust framing; do not rely on timing gaps.
|
||||
- CRC-16 or CRC-32 on every frame.
|
||||
- Every host command receives ACK or NACK.
|
||||
- NACK reason is machine-readable, not free text only.
|
||||
- Deterministic parsing; no UI text parsing in the firmware protocol.
|
||||
- Safety messages bounded-latency and robust to single-frame loss.
|
||||
|
||||
## Safety/interlock context
|
||||
|
||||
Safety is layered:
|
||||
|
||||
- Hardware safety: E-stop circuit, safety relay, brake, drive enables. Independent of software.
|
||||
- Teensy fast safety: force limits, encoder loss, F/T sensor stale/invalid, drive faults, watchdog response.
|
||||
- Host slow safety: state integrity, RPM deviations, segment/manual session orchestration, logging, UI gates.
|
||||
|
||||
Hard-stop faults include:
|
||||
|
||||
- `ESTOP_ACTIVATED`
|
||||
- `FORCE_OVER_LIMIT`
|
||||
- `ENCODER_LOST`
|
||||
- `DRIVE_FAULT`
|
||||
- `FT_SENSOR_INVALID`
|
||||
|
||||
Warnings/recoverable pauses include:
|
||||
|
||||
- `FORCE_UNDER_LIMIT`
|
||||
- `SPINDLE_RPM_DEVIATION`
|
||||
- `TABLE_RPM_DEVIATION`
|
||||
- `HOST_COMMS_TIMEOUT`
|
||||
|
||||
Refused transition / gate reasons include:
|
||||
|
||||
- `GEOMETRY_NOT_VALIDATED`
|
||||
- `ARM_HANDLING_INTERLOCK` if final lock/cam-nut feedback exists.
|
||||
|
||||
## Force control and tool handling
|
||||
|
||||
- Force PID closes on the compensated contact force path.
|
||||
- Store `fz_tool_weight_offset_n` for the installed tool/head configuration.
|
||||
- Keep raw force available for diagnostics as `fz_raw_n` where practical.
|
||||
- Compute compensated contact force as:
|
||||
|
||||
```text
|
||||
fz_contact_n = fz_raw_n - fz_tool_weight_offset_n
|
||||
```
|
||||
|
||||
- If only one canonical force channel is emitted, document that `fz_n` means compensated contact force and include the offset in session/run metadata.
|
||||
- Do not implement a powered Zero-G state in v1.
|
||||
|
||||
Mechanical safe tool-removal workflow:
|
||||
|
||||
1. Stop active manual/job operation.
|
||||
2. Command force to zero and stop table, spindle, and arm drive.
|
||||
3. Confirm compensated force is near zero.
|
||||
4. Confirm machine is in `IDLE` or stopped manual condition.
|
||||
5. Engage/verify swing arm or arc mechanical lock.
|
||||
6. Remove cam arm nut to free arm rotation.
|
||||
7. Move arm aside by hand.
|
||||
8. Remove tool from blank by hand.
|
||||
9. Reinstall/seat tool, restore mechanical configuration, and rerun the geometric gate before software-controlled motion.
|
||||
|
||||
## Run artifacts and data layout
|
||||
|
||||
Write run data to USB SSD, not the RPi SD card.
|
||||
|
||||
Baseline layout:
|
||||
|
||||
```text
|
||||
/data/
|
||||
├── runs/
|
||||
│ └── run-*/
|
||||
│ ├── run-log.v1.json
|
||||
│ ├── telemetry.csv
|
||||
│ ├── telemetry-derived.parquet
|
||||
│ ├── segment-stats.json
|
||||
│ ├── dwell-map.npz
|
||||
│ ├── work-map.npz
|
||||
│ ├── anomaly-windows/
|
||||
│ └── manifest.json
|
||||
├── manual/
|
||||
│ └── manual-*/
|
||||
│ ├── manual-session-log.v1.json
|
||||
│ ├── telemetry.csv
|
||||
│ ├── telemetry-derived.parquet
|
||||
│ ├── segment-stats.json
|
||||
│ ├── dwell-map.npz
|
||||
│ ├── work-map.npz
|
||||
│ └── manifest.json
|
||||
├── capabilities/
|
||||
│ └── machine-capabilities.v1.json
|
||||
└── status.json
|
||||
```
|
||||
|
||||
For v1 hardware proving, raw full-rate telemetry, run/manual logs, stable filenames, and hashes are mandatory. Derived artifacts may begin as a simple post-run routine but should follow the contract from the start.
|
||||
|
||||
## Feature request workflow for Antoine/Cédric
|
||||
|
||||
When Antoine says he wants to add a feature to `polisher-control`, classify it before coding:
|
||||
|
||||
1. **Execution feature** — belongs here if it changes host state machine, protocol, firmware, manual workflow, telemetry, safety, or run artifacts.
|
||||
2. **Contract feature** — may require parallel changes in `shared/schemas`, `polisher-post`, and possibly `polisher-sim`.
|
||||
3. **Planning/intelligence feature** — probably belongs upstream in `polisher-sim` or `polisher-post`, not here.
|
||||
4. **Safety/scope feature** — requires Antoine approval before implementation if it changes interlocks, limits, fault behavior, telemetry schema, protocol semantics, or v1 scope.
|
||||
|
||||
For each proposed feature, capture:
|
||||
|
||||
- requested behavior;
|
||||
- operator-visible behavior;
|
||||
- affected layers: host / Teensy / protocol / telemetry / schemas / docs / tests;
|
||||
- safety implications;
|
||||
- acceptance checks;
|
||||
- open questions for Antoine or Cédric.
|
||||
|
||||
Use `docs/11-feature-request-intake.md` as the template.
|
||||
|
||||
## Open implementation questions to keep visible
|
||||
|
||||
Current control/firmware-facing questions include:
|
||||
|
||||
- Confirm KBSI-240D / table drive command path or replacement.
|
||||
- Confirm table encoder transport and transceiver choice.
|
||||
- Define ODrive runtime command/telemetry path, scaling, fault reset, safe stop, and config export.
|
||||
- Confirm SV2A-2150 torque/current command mapping, enable/fault wiring, current/Iq monitor path.
|
||||
- Confirm Z limit-switch / hard-stop wiring into safety chain.
|
||||
- Confirm NC brake coil driver and brake-engaged diagnostic feedback.
|
||||
- Select safety relay model.
|
||||
- Confirm KWR75B-CAN frame map, byte order, scaling, status bits, and update rate.
|
||||
|
||||
## Acceptance mindset
|
||||
|
||||
A change is not done because code exists. It is done when the relevant acceptance checks pass:
|
||||
|
||||
- state transition tests;
|
||||
- protocol encode/decode/ACK/NACK tests;
|
||||
- schema/example validation;
|
||||
- telemetry channel consistency checks;
|
||||
- safety gate refusal tests;
|
||||
- host-side log artifact tests;
|
||||
- firmware compile/build checks where hardware-specific code is touched;
|
||||
- commissioning checklist item if hardware integration is involved.
|
||||
|
||||
Keep code boring. Prefer explicit rejection over silent assumptions.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Feature Intake — Toolhead Spindle Direction Selection
|
||||
status: draft
|
||||
requested_by: Antoine Letarte / Normand Fullum
|
||||
generated_by: Nick / Hermes
|
||||
project: P11-Polisher-Fullum
|
||||
repo: polisher-control
|
||||
source_truth: false
|
||||
created: 2026-06-02
|
||||
privacy: technical-only
|
||||
---
|
||||
|
||||
# Feature Intake — Toolhead Spindle Direction Selection
|
||||
|
||||
## 1. Requested behavior
|
||||
|
||||
Normand wants the operator to select the toolhead/tool spindle rotation direction between clockwise and counter-clockwise.
|
||||
|
||||
The operator-facing control belongs in manual mode first, and the same field should be carried in future controller-job segments so program execution does not require a protocol/schema redesign.
|
||||
|
||||
## 2. Classification
|
||||
|
||||
- [x] Execution feature in `polisher-control`
|
||||
- [x] Contract feature touching schemas/protocol/telemetry/logs
|
||||
- [ ] Planning/intelligence feature that belongs upstream
|
||||
- [ ] Safety/scope feature requiring Antoine approval beyond this explicit request
|
||||
|
||||
Rationale: the controller is not deciding polishing strategy. It is executing/logging an operator- or job-selected spindle direction. Because this changes the host↔Teensy payload and controller-job/log shape, it is also a contract feature.
|
||||
|
||||
## 3. Operator-visible behavior
|
||||
|
||||
- Manual UI exposes a spindle direction selector next to spindle RPM.
|
||||
- Allowed values are:
|
||||
- `cw` — clockwise
|
||||
- `ccw` — counter-clockwise
|
||||
- Physical convention: direction is viewed from above the toolhead looking down toward the mirror/tool contact.
|
||||
- Default for existing/manual startup can be `cw` unless Antoine/Cédric chooses another default.
|
||||
- Any direction change while running is logged as a timestamped setpoint event.
|
||||
|
||||
## 4. Affected layers
|
||||
|
||||
- Host controller: normalize/validate direction aliases and include direction in manual setpoints/log events.
|
||||
- Teensy firmware: accept direction in `MANUAL_START`, `SETPOINT`, and future `SEGMENT_START`; map to ODrive command sign.
|
||||
- Host ↔ Teensy protocol: add `spindle_direction` / `commanded_spindle_direction` enum values `cw` / `ccw`.
|
||||
- Telemetry channels: add `spindle_direction_setpoint` and `spindle_direction_actual` as recommended channels.
|
||||
- Run/manual-session logs: record direction separately from RPM; RPM remains a non-negative magnitude.
|
||||
- Machine capability profile: advertise `supported_spindle_directions: ["cw", "ccw"]`.
|
||||
- Shared schemas: add direction fields to job/controller-job/run-log contracts.
|
||||
- Tests: assert stable enum values, schema fields, and telemetry channel presence.
|
||||
|
||||
## 5. Safety implications
|
||||
|
||||
- Direction must not be inferred from signed RPM; use explicit enum plus non-negative RPM magnitude.
|
||||
- Direction reversal while spindle is moving should be ramped/handled safely by firmware/ODrive implementation. The scaffold currently defines the contract; Cédric should decide whether live reversal is allowed directly or requires ramp-to-zero before applying the new direction.
|
||||
- ODrive sign mapping is a commissioning item: firmware must be configured so UI `cw`/`ccw` matches physical observed tool rotation.
|
||||
|
||||
Safety decision:
|
||||
|
||||
- [x] No new optical strategy or autonomous behavior.
|
||||
- [x] Implementation should add a safe reversal rule before hardware use.
|
||||
|
||||
## 6. Contract implications
|
||||
|
||||
- New controller-job segment field: `commanded_spindle_direction` enum `cw|ccw`.
|
||||
- New planning job pass field: `spindle_direction` enum `cw|ccw`.
|
||||
- New run-log commanded fields: `spindle_direction`.
|
||||
- New capability field: `supported_spindle_directions`.
|
||||
- New telemetry context channels: `spindle_direction_setpoint`, `spindle_direction_actual`.
|
||||
|
||||
## 7. Acceptance checks
|
||||
|
||||
- [x] Host contract tests for enum values and alias normalization.
|
||||
- [x] Schema field test for `commanded_spindle_direction`.
|
||||
- [x] Telemetry channel test for direction channels.
|
||||
- [ ] Firmware compile/build check once Cédric maps this into Teensy/ODrive code.
|
||||
- [ ] Bench test confirms UI `cw` physically rotates clockwise by the documented convention.
|
||||
- [ ] Bench test confirms UI `ccw` physically rotates counter-clockwise by the documented convention.
|
||||
- [ ] Bench test confirms live reversal behavior is safe or blocked until spindle reaches zero.
|
||||
|
||||
## 8. Open questions
|
||||
|
||||
- Cédric: confirm the ODrive command/sign mapping and whether live direction reversal requires ramp-to-zero.
|
||||
- Antoine/Normand: confirm the physical viewing convention: current draft uses “viewed from above the toolhead looking down toward the mirror/tool contact.”
|
||||
|
||||
## 9. Implementation notes
|
||||
|
||||
Initial scaffold update created on branch `docs/nick-llm-context-20260602`:
|
||||
|
||||
- `host/polisher_control/contracts.py`
|
||||
- `host/polisher_control/telemetry_channels.py`
|
||||
- `tests/test_spindle_direction_contract.py`
|
||||
- `shared/schemas/*.schema.json`
|
||||
- `shared/schemas/examples/*.json`
|
||||
- `shared/machine/fullum-alpha.capabilities.v1.json`
|
||||
- affected docs/checklists
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,11 @@ This repo foundation is derived from the current P11 / Fullum polisher-control p
|
||||
|
||||
Primary source documents:
|
||||
|
||||
- Fullum Polisher — Machine Control & Firmware Specification v1
|
||||
- `docs/reference/fullum-control-firmware-spec/Fullum-Polisher-Machine-Control-Firmware-Spec-Report.pdf` — Cédric-facing rendered report.
|
||||
- `docs/reference/fullum-control-firmware-spec/Fullum-Polisher-Machine-Control-Firmware-Spec-Report.typ` — standalone Typst source for the rendered report.
|
||||
- `docs/reference/fullum-control-firmware-spec/Fullum-Polisher-Machine-Control-Firmware-Spec-v1.md` — canonical implementation spec body copied from the P11 PKM source.
|
||||
- `software-suite/control/firmware/Fullum-Polisher-Machine-Control-Firmware-Spec-Report.md` / `.pdf` in the P11 PKM — upstream source/report location.
|
||||
- `software-suite/control/firmware/Fullum-Polisher-Machine-Control-Firmware-Spec-v1.md` in the P11 PKM — upstream implementation spec body.
|
||||
- Controller / Bridge / Digital Twin Architecture Plan
|
||||
- polisher-control — System Definition
|
||||
- polisher-control — Roadmap
|
||||
|
||||
@@ -1,6 +1,50 @@
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
CONTROLLER_SCHEMA_VERSION = "controller-job.v1"
|
||||
RUN_LOG_SCHEMA_VERSION = "run-log.v1"
|
||||
MANUAL_SESSION_SCHEMA_VERSION = "manual-session-log.v1"
|
||||
MACHINE_CAPABILITIES_SCHEMA_VERSION = "machine-capabilities.v1"
|
||||
MACHINE_ID = "fullum-alpha"
|
||||
CONTROLLER_VERSION_PREFIX = "polisher-control/"
|
||||
|
||||
|
||||
class SpindleDirection(StrEnum):
|
||||
"""Toolhead spindle rotation direction using stable protocol/schema values.
|
||||
|
||||
Physical convention: clockwise/counter-clockwise are as viewed from above the
|
||||
toolhead looking down toward the mirror/tool contact. ODrive sign mapping is
|
||||
a commissioning item and must be configured so these UI/protocol values match
|
||||
the observed tool rotation.
|
||||
"""
|
||||
|
||||
CLOCKWISE = "cw"
|
||||
COUNTER_CLOCKWISE = "ccw"
|
||||
|
||||
@classmethod
|
||||
def allowed_values(cls) -> list[str]:
|
||||
return [direction.value for direction in cls]
|
||||
|
||||
|
||||
_SPINDLE_DIRECTION_ALIASES = {
|
||||
"cw": SpindleDirection.CLOCKWISE,
|
||||
"clockwise": SpindleDirection.CLOCKWISE,
|
||||
"c.w.": SpindleDirection.CLOCKWISE,
|
||||
"ccw": SpindleDirection.COUNTER_CLOCKWISE,
|
||||
"counterclockwise": SpindleDirection.COUNTER_CLOCKWISE,
|
||||
"counter-clockwise": SpindleDirection.COUNTER_CLOCKWISE,
|
||||
"counter_clockwise": SpindleDirection.COUNTER_CLOCKWISE,
|
||||
"anticlockwise": SpindleDirection.COUNTER_CLOCKWISE,
|
||||
}
|
||||
|
||||
|
||||
def normalize_spindle_direction(value: str | SpindleDirection) -> SpindleDirection:
|
||||
"""Normalize UI/protocol aliases to a SpindleDirection enum."""
|
||||
if isinstance(value, SpindleDirection):
|
||||
return value
|
||||
key = value.strip().lower()
|
||||
try:
|
||||
return _SPINDLE_DIRECTION_ALIASES[key]
|
||||
except KeyError as exc:
|
||||
allowed = ", ".join(SpindleDirection.allowed_values())
|
||||
raise ValueError(f"Unsupported spindle direction {value!r}; expected one of: {allowed}") from exc
|
||||
|
||||
@@ -28,6 +28,8 @@ RECOMMENDED_CHANNELS = [
|
||||
"arm_angle_linearized_deg",
|
||||
"table_rpm_setpoint",
|
||||
"spindle_rpm_setpoint",
|
||||
"spindle_direction_setpoint",
|
||||
"spindle_direction_actual",
|
||||
"force_actuator_cmd",
|
||||
"estop_active",
|
||||
"interlock_state",
|
||||
|
||||
@@ -107,6 +107,16 @@
|
||||
"name": "force_setpoint_n",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "spindle_direction_setpoint",
|
||||
"unit": "enum(cw|ccw)",
|
||||
"sample_rate_hz": 10
|
||||
},
|
||||
{
|
||||
"name": "spindle_direction_actual",
|
||||
"unit": "enum(cw|ccw)",
|
||||
"sample_rate_hz": 10
|
||||
}
|
||||
],
|
||||
"safety_limits": {
|
||||
@@ -126,5 +136,9 @@
|
||||
"Final ODrive runtime interface and command scaling.",
|
||||
"Final SV2A-2150 torque/current command scaling and Iq monitor mapping.",
|
||||
"Final safety relay model and wiring details."
|
||||
],
|
||||
"supported_spindle_directions": [
|
||||
"cw",
|
||||
"ccw"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -65,13 +65,23 @@
|
||||
"description": "Explicit record of every constraint, clip, or approximation applied.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["source_pass_id", "field", "description"],
|
||||
"required": [
|
||||
"source_pass_id",
|
||||
"field",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"source_pass_id": { "type": "string" },
|
||||
"field": { "type": "string" },
|
||||
"source_pass_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"planned_value": {},
|
||||
"translated_value": {},
|
||||
"description": { "type": "string" }
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,31 +96,71 @@
|
||||
"duration_s",
|
||||
"commanded_force_n",
|
||||
"commanded_table_rpm",
|
||||
"commanded_spindle_rpm"
|
||||
"commanded_spindle_rpm",
|
||||
"commanded_spindle_direction"
|
||||
],
|
||||
"properties": {
|
||||
"segment_id": { "type": "string" },
|
||||
"sequence_index": { "type": "integer", "minimum": 1 },
|
||||
"segment_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"sequence_index": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"source_pass_id": {
|
||||
"type": "string",
|
||||
"description": "Back-reference to the planning pass this segment came from."
|
||||
},
|
||||
"duration_s": { "type": "number", "minimum": 0 },
|
||||
"commanded_force_n": { "type": "number", "minimum": 0 },
|
||||
"commanded_table_rpm": { "type": "number", "minimum": 0 },
|
||||
"commanded_spindle_rpm": { "type": "number", "minimum": 0 },
|
||||
"commanded_cam_amplitude_deg": { "type": "number" },
|
||||
"commanded_cam_offset_deg": { "type": "number" },
|
||||
"duration_s": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"commanded_force_n": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"commanded_table_rpm": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"commanded_spindle_rpm": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"commanded_cam_amplitude_deg": {
|
||||
"type": "number"
|
||||
},
|
||||
"commanded_cam_offset_deg": {
|
||||
"type": "number"
|
||||
},
|
||||
"force_modulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"harmonic": { "type": "integer" },
|
||||
"amplitude_n": { "type": "number" },
|
||||
"phase_deg": { "type": "number" }
|
||||
"harmonic": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amplitude_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"phase_deg": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dither_profile": { "type": "string" },
|
||||
"notes": { "type": "string" }
|
||||
"dither_profile": {
|
||||
"type": "string"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
},
|
||||
"commanded_spindle_direction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cw",
|
||||
"ccw"
|
||||
],
|
||||
"description": "Toolhead spindle rotation direction, as viewed from above the toolhead looking down toward the mirror/tool contact. ODrive sign mapping is commissioned to match this physical convention."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"phase_deg": 37.2
|
||||
},
|
||||
"dither_profile": "none",
|
||||
"notes": "Direct translation — no segmentation needed for 1200s duration."
|
||||
"notes": "Direct translation \u2014 no segmentation needed for 1200s duration.",
|
||||
"commanded_spindle_direction": "cw"
|
||||
}
|
||||
],
|
||||
"translation_losses": []
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"amplitude_n": 5.0,
|
||||
"phase_deg": 37.2
|
||||
},
|
||||
"notes": "Phase derived from Zernike fit of pre-polish interferogram."
|
||||
"notes": "Phase derived from Zernike fit of pre-polish interferogram.",
|
||||
"spindle_direction": "cw"
|
||||
}
|
||||
],
|
||||
"predicted_outcome": {
|
||||
@@ -53,7 +54,10 @@
|
||||
}
|
||||
},
|
||||
"uncertainty": {
|
||||
"removal_rms_nm_range": [14.0, 24.0],
|
||||
"removal_rms_nm_range": [
|
||||
14.0,
|
||||
24.0
|
||||
],
|
||||
"notes": "Uncertainty dominated by Preston coefficient calibration spread."
|
||||
},
|
||||
"attachments": [
|
||||
|
||||
@@ -5,31 +5,85 @@
|
||||
"machine_name": "Fullum Swing-Arm Polisher",
|
||||
"controller_version": "polisher-control/0.1.0",
|
||||
"last_verified": "2026-06-01T00:00:00Z",
|
||||
"supported_motion_families": ["swing-arm-rosette"],
|
||||
"force_range_n": [5, 200],
|
||||
"table_rpm_range": [0.5, 10.0],
|
||||
"spindle_rpm_range": [10, 120],
|
||||
"cam_amplitude_range_deg": [1.0, 31.3],
|
||||
"cam_offset_range_deg": [-30.0, 30.0],
|
||||
"supported_motion_families": [
|
||||
"swing-arm-rosette"
|
||||
],
|
||||
"force_range_n": [
|
||||
5,
|
||||
200
|
||||
],
|
||||
"table_rpm_range": [
|
||||
0.5,
|
||||
10.0
|
||||
],
|
||||
"spindle_rpm_range": [
|
||||
10,
|
||||
120
|
||||
],
|
||||
"cam_amplitude_range_deg": [
|
||||
1.0,
|
||||
31.3
|
||||
],
|
||||
"cam_offset_range_deg": [
|
||||
-30.0,
|
||||
30.0
|
||||
],
|
||||
"force_modulation": {
|
||||
"supported": true,
|
||||
"max_harmonics": 3,
|
||||
"max_amplitude_n": 24.0,
|
||||
"notes": "m=2 astigmatism and m=3 trefoil are proven channels. m=1 coma is unreliable (score 0.09)."
|
||||
},
|
||||
"supported_dither_profiles": ["none", "default"],
|
||||
"supported_dither_profiles": [
|
||||
"none",
|
||||
"default"
|
||||
],
|
||||
"segment_duration_limits": {
|
||||
"min_s": 30,
|
||||
"max_s": 7200
|
||||
},
|
||||
"pause_resume_support": true,
|
||||
"telemetry_channels": [
|
||||
{ "name": "table_rpm", "unit": "RPM", "sample_rate_hz": 100 },
|
||||
{ "name": "spindle_rpm", "unit": "RPM", "sample_rate_hz": 100 },
|
||||
{ "name": "force_n", "unit": "N", "sample_rate_hz": 100 },
|
||||
{ "name": "arm_angle_deg", "unit": "degrees", "sample_rate_hz": 100 },
|
||||
{ "name": "table_angle_deg", "unit": "degrees", "sample_rate_hz": 100 },
|
||||
{ "name": "timestamp_ms", "unit": "ms", "sample_rate_hz": 100 }
|
||||
{
|
||||
"name": "table_rpm",
|
||||
"unit": "RPM",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "spindle_rpm",
|
||||
"unit": "RPM",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "force_n",
|
||||
"unit": "N",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "arm_angle_deg",
|
||||
"unit": "degrees",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "table_angle_deg",
|
||||
"unit": "degrees",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "timestamp_ms",
|
||||
"unit": "ms",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "spindle_direction_setpoint",
|
||||
"unit": "enum(cw|ccw)",
|
||||
"sample_rate_hz": 10
|
||||
},
|
||||
{
|
||||
"name": "spindle_direction_actual",
|
||||
"unit": "enum(cw|ccw)",
|
||||
"sample_rate_hz": 10
|
||||
}
|
||||
],
|
||||
"safety_limits": {
|
||||
"max_force_n": 200,
|
||||
@@ -38,14 +92,18 @@
|
||||
"notes": "Force hard-limited by load cell interlock. RPM limits are VFD/servo configured."
|
||||
},
|
||||
"known_constraints": [
|
||||
"Cam amplitude is not servo-programmable — must be set mechanically before run.",
|
||||
"Cam amplitude is not servo-programmable \u2014 must be set mechanically before run.",
|
||||
"Force modulation bandwidth limited to ~5 Hz by actuator response.",
|
||||
"Table encoder is absolute but may have 1-2 count jitter.",
|
||||
"Serial interface to Teensy limits command update rate to ~50 Hz effective."
|
||||
],
|
||||
"unknowns": [
|
||||
"Exact force modulation phase accuracy at m=3 harmonic — not yet validated on hardware.",
|
||||
"Exact force modulation phase accuracy at m=3 harmonic \u2014 not yet validated on hardware.",
|
||||
"Thermal drift effect on force sensor over runs longer than 1 hour.",
|
||||
"Arm pivot play magnitude under varying force loads."
|
||||
],
|
||||
"supported_spindle_directions": [
|
||||
"cw",
|
||||
"ccw"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"table_rpm": 4.0,
|
||||
"spindle_rpm": 40.0,
|
||||
"cam_amplitude_deg": 31.3,
|
||||
"cam_offset_deg": 0.0
|
||||
"cam_offset_deg": 0.0,
|
||||
"spindle_direction": "cw"
|
||||
},
|
||||
"actual": {
|
||||
"force_n_mean": 44.7,
|
||||
@@ -37,7 +38,8 @@
|
||||
"commanded_summary": {
|
||||
"force_n": 45.0,
|
||||
"table_rpm": 4.0,
|
||||
"spindle_rpm": 40.0
|
||||
"spindle_rpm": 40.0,
|
||||
"spindle_direction": "cw"
|
||||
},
|
||||
"actual_summary": {
|
||||
"force_n_mean": 44.7,
|
||||
|
||||
@@ -55,15 +55,21 @@
|
||||
},
|
||||
"strategy_summary": {
|
||||
"type": "object",
|
||||
"required": ["intent"],
|
||||
"required": [
|
||||
"intent"
|
||||
],
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"intent": {
|
||||
"type": "string"
|
||||
},
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"passes": {
|
||||
@@ -77,11 +83,17 @@
|
||||
"type": "object",
|
||||
"description": "Summary of predicted post-polish surface state.",
|
||||
"properties": {
|
||||
"removal_rms_nm": { "type": "number" },
|
||||
"removal_pv_nm": { "type": "number" },
|
||||
"removal_rms_nm": {
|
||||
"type": "number"
|
||||
},
|
||||
"removal_pv_nm": {
|
||||
"type": "number"
|
||||
},
|
||||
"residual_zernikes": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "number" }
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -91,22 +103,35 @@
|
||||
"properties": {
|
||||
"removal_rms_nm_range": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["filename", "role"],
|
||||
"required": [
|
||||
"filename",
|
||||
"role"
|
||||
],
|
||||
"properties": {
|
||||
"filename": { "type": "string" },
|
||||
"role": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
"filename": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,37 +146,81 @@
|
||||
"force_n",
|
||||
"table_rpm",
|
||||
"spindle_rpm",
|
||||
"spindle_direction",
|
||||
"motion_family"
|
||||
],
|
||||
"properties": {
|
||||
"pass_id": { "type": "string" },
|
||||
"sequence_index": { "type": "integer", "minimum": 1 },
|
||||
"objective": { "type": "string" },
|
||||
"preset_name": { "type": "string" },
|
||||
"duration_s": { "type": "number", "minimum": 0 },
|
||||
"force_n": { "type": "number", "minimum": 0 },
|
||||
"table_rpm": { "type": "number", "minimum": 0 },
|
||||
"spindle_rpm": { "type": "number", "minimum": 0 },
|
||||
"motion_family": { "type": "string" },
|
||||
"pass_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"sequence_index": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"objective": {
|
||||
"type": "string"
|
||||
},
|
||||
"preset_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"duration_s": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"force_n": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"table_rpm": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"spindle_rpm": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"motion_family": {
|
||||
"type": "string"
|
||||
},
|
||||
"motion_params": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"zone": { "type": "string" },
|
||||
"dither_profile": { "type": "string" },
|
||||
"zone": {
|
||||
"type": "string"
|
||||
},
|
||||
"dither_profile": {
|
||||
"type": "string"
|
||||
},
|
||||
"force_modulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"harmonic": { "type": "integer" },
|
||||
"amplitude_n": { "type": "number" },
|
||||
"phase_deg": { "type": "number" }
|
||||
"harmonic": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amplitude_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"phase_deg": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
"notes": {
|
||||
"type": "string"
|
||||
},
|
||||
"spindle_direction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cw",
|
||||
"ccw"
|
||||
],
|
||||
"description": "Requested toolhead spindle rotation direction using the same physical convention as polisher-control."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"supported_motion_families",
|
||||
"force_range_n",
|
||||
"table_rpm_range",
|
||||
"spindle_rpm_range"
|
||||
"spindle_rpm_range",
|
||||
"supported_spindle_directions"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
@@ -38,7 +39,9 @@
|
||||
},
|
||||
"supported_motion_families": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"force_range_n": {
|
||||
@@ -60,22 +63,36 @@
|
||||
"type": "object",
|
||||
"description": "Force modulation capabilities. Omit entirely if unknown.",
|
||||
"properties": {
|
||||
"supported": { "type": "boolean" },
|
||||
"max_harmonics": { "type": "integer" },
|
||||
"max_amplitude_n": { "type": "number" },
|
||||
"notes": { "type": "string" }
|
||||
"supported": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"max_harmonics": {
|
||||
"type": "integer"
|
||||
},
|
||||
"max_amplitude_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"supported_dither_profiles": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of dither profile names the controller can handle. Empty = none supported."
|
||||
},
|
||||
"segment_duration_limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min_s": { "type": "number" },
|
||||
"max_s": { "type": "number" }
|
||||
"min_s": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_s": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pause_resume_support": {
|
||||
@@ -85,12 +102,23 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "unit"],
|
||||
"required": [
|
||||
"name",
|
||||
"unit"
|
||||
],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"unit": { "type": "string" },
|
||||
"sample_rate_hz": { "type": "number" },
|
||||
"notes": { "type": "string" }
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_rate_hz": {
|
||||
"type": "number"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -98,21 +126,46 @@
|
||||
"type": "object",
|
||||
"description": "Hard safety limits that the controller enforces regardless of job requests.",
|
||||
"properties": {
|
||||
"max_force_n": { "type": "number" },
|
||||
"max_table_rpm": { "type": "number" },
|
||||
"max_spindle_rpm": { "type": "number" },
|
||||
"notes": { "type": "string" }
|
||||
"max_force_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_table_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_spindle_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"known_constraints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Free-text list of known limitations, quirks, or warnings."
|
||||
},
|
||||
"unknowns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Capabilities that have NOT been verified. Explicit unknowns prevent fake certainty."
|
||||
},
|
||||
"supported_spindle_directions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cw",
|
||||
"ccw"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"description": "Toolhead spindle rotation directions supported by the controller UI/protocol."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -120,7 +173,9 @@
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"items": {
|
||||
"type": "number"
|
||||
},
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"description": "[min, max] range."
|
||||
|
||||
@@ -49,7 +49,12 @@
|
||||
},
|
||||
"result_state": {
|
||||
"type": "string",
|
||||
"enum": ["completed", "completed_with_pause", "aborted", "faulted"]
|
||||
"enum": [
|
||||
"completed",
|
||||
"completed_with_pause",
|
||||
"aborted",
|
||||
"faulted"
|
||||
]
|
||||
},
|
||||
"segments": {
|
||||
"type": "array",
|
||||
@@ -61,31 +66,72 @@
|
||||
"commanded_summary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_n": { "type": "number" },
|
||||
"table_rpm": { "type": "number" },
|
||||
"spindle_rpm": { "type": "number" }
|
||||
"force_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"table_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_direction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cw",
|
||||
"ccw"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"actual_summary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_n_mean": { "type": "number" },
|
||||
"force_n_min": { "type": "number" },
|
||||
"force_n_max": { "type": "number" },
|
||||
"table_rpm_mean": { "type": "number" },
|
||||
"spindle_rpm_mean": { "type": "number" }
|
||||
"force_n_mean": {
|
||||
"type": "number"
|
||||
},
|
||||
"force_n_min": {
|
||||
"type": "number"
|
||||
},
|
||||
"force_n_max": {
|
||||
"type": "number"
|
||||
},
|
||||
"table_rpm_mean": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_rpm_mean": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"alarms": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["timestamp", "code", "message"],
|
||||
"required": [
|
||||
"timestamp",
|
||||
"code",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"timestamp": { "type": "string", "format": "date-time" },
|
||||
"code": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"severity": { "type": "string", "enum": ["info", "warning", "critical"] }
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"info",
|
||||
"warning",
|
||||
"critical"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -93,11 +139,21 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["timestamp", "type"],
|
||||
"required": [
|
||||
"timestamp",
|
||||
"type"
|
||||
],
|
||||
"properties": {
|
||||
"timestamp": { "type": "string", "format": "date-time" },
|
||||
"type": { "type": "string" },
|
||||
"detail": { "type": "string" }
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"detail": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -120,51 +176,107 @@
|
||||
"result_state"
|
||||
],
|
||||
"properties": {
|
||||
"segment_id": { "type": "string" },
|
||||
"source_pass_id": { "type": "string" },
|
||||
"commanded_duration_s": { "type": "number" },
|
||||
"actual_duration_s": { "type": "number" },
|
||||
"segment_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"source_pass_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"commanded_duration_s": {
|
||||
"type": "number"
|
||||
},
|
||||
"actual_duration_s": {
|
||||
"type": "number"
|
||||
},
|
||||
"result_state": {
|
||||
"type": "string",
|
||||
"enum": ["completed", "completed_with_pause", "aborted", "faulted", "skipped"]
|
||||
"enum": [
|
||||
"completed",
|
||||
"completed_with_pause",
|
||||
"aborted",
|
||||
"faulted",
|
||||
"skipped"
|
||||
]
|
||||
},
|
||||
"commanded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_n": { "type": "number" },
|
||||
"table_rpm": { "type": "number" },
|
||||
"spindle_rpm": { "type": "number" },
|
||||
"cam_amplitude_deg": { "type": "number" },
|
||||
"cam_offset_deg": { "type": "number" }
|
||||
"force_n": {
|
||||
"type": "number"
|
||||
},
|
||||
"table_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_rpm": {
|
||||
"type": "number"
|
||||
},
|
||||
"cam_amplitude_deg": {
|
||||
"type": "number"
|
||||
},
|
||||
"cam_offset_deg": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_direction": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cw",
|
||||
"ccw"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"actual": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_n_mean": { "type": "number" },
|
||||
"force_n_min": { "type": "number" },
|
||||
"force_n_max": { "type": "number" },
|
||||
"table_rpm_mean": { "type": "number" },
|
||||
"spindle_rpm_mean": { "type": "number" }
|
||||
"force_n_mean": {
|
||||
"type": "number"
|
||||
},
|
||||
"force_n_min": {
|
||||
"type": "number"
|
||||
},
|
||||
"force_n_max": {
|
||||
"type": "number"
|
||||
},
|
||||
"table_rpm_mean": {
|
||||
"type": "number"
|
||||
},
|
||||
"spindle_rpm_mean": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pause_windows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["paused_at", "resumed_at"],
|
||||
"required": [
|
||||
"paused_at",
|
||||
"resumed_at"
|
||||
],
|
||||
"properties": {
|
||||
"paused_at": { "type": "string", "format": "date-time" },
|
||||
"resumed_at": { "type": "string", "format": "date-time" },
|
||||
"reason": { "type": "string" }
|
||||
"paused_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"resumed_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"anomaly_flags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
tests/test_spindle_direction_contract.py
Normal file
33
tests/test_spindle_direction_contract.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from polisher_control.contracts import SpindleDirection, normalize_spindle_direction
|
||||
from polisher_control.telemetry_channels import RECOMMENDED_CHANNELS
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_spindle_direction_enum_uses_stable_wire_values():
|
||||
assert SpindleDirection.CLOCKWISE.value == "cw"
|
||||
assert SpindleDirection.COUNTER_CLOCKWISE.value == "ccw"
|
||||
assert SpindleDirection.allowed_values() == ["cw", "ccw"]
|
||||
|
||||
|
||||
def test_normalize_spindle_direction_accepts_operator_aliases():
|
||||
assert normalize_spindle_direction("cw") == SpindleDirection.CLOCKWISE
|
||||
assert normalize_spindle_direction("clockwise") == SpindleDirection.CLOCKWISE
|
||||
assert normalize_spindle_direction("ccw") == SpindleDirection.COUNTER_CLOCKWISE
|
||||
assert normalize_spindle_direction("counter-clockwise") == SpindleDirection.COUNTER_CLOCKWISE
|
||||
|
||||
|
||||
def test_spindle_direction_is_in_controller_job_segment_schema():
|
||||
schema = json.loads((ROOT / "shared/schemas/controller-job.schema.json").read_text())
|
||||
segment = schema["$defs"]["segment"]
|
||||
|
||||
assert "commanded_spindle_direction" in segment["required"]
|
||||
assert segment["properties"]["commanded_spindle_direction"]["enum"] == ["cw", "ccw"]
|
||||
|
||||
|
||||
def test_spindle_direction_is_logged_as_telemetry_context():
|
||||
assert "spindle_direction_setpoint" in RECOMMENDED_CHANNELS
|
||||
assert "spindle_direction_actual" in RECOMMENDED_CHANNELS
|
||||
Reference in New Issue
Block a user