Add Cédric LLM context pack, full firmware spec, and spindle direction contract #1

Merged
Antoine merged 3 commits from docs/nick-llm-context-20260602 into main 2026-06-02 18:57:37 +00:00
21 changed files with 689 additions and 133 deletions
Showing only changes of commit beeb521ca7 - Show all commits

View File

@@ -17,7 +17,7 @@ shared contracts -> schemas, logs, machine capabilities, provenance
Normand can operate the polisher manually from the touchscreen, with: 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; - KWR75B-CAN force/torque sensor integration;
- ODrive S1 + M8325s spindle drive integration; - ODrive S1 + M8325s spindle drive integration;
- Teensy-side fast safety and setpoint/telemetry loop; - Teensy-side fast safety and setpoint/telemetry loop;

View File

@@ -22,13 +22,13 @@
- [ ] Host↔Teensy serial link with ACK/NACK and CRC. - [ ] Host↔Teensy serial link with ACK/NACK and CRC.
- [ ] KWR75B-CAN receive path with measured sample rate and stale-frame watchdog. - [ ] KWR75B-CAN receive path with measured sample rate and stale-frame watchdog.
- [ ] Encoder acquisition for table and arm. - [ ] 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. - [ ] Force actuator command path selected and documented.
## Phase 3 — Manual mode MVP ## Phase 3 — Manual mode MVP
- [ ] Host UI/manual-mode workflow with geometric gate. - [ ] 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. - [ ] Teensy inner loop: setpoint ramping, force PID, telemetry, fast interlocks.
- [ ] Manual-session log and telemetry CSV written to `/data/manual/{session_id}/`. - [ ] Manual-session log and telemetry CSV written to `/data/manual/{session_id}/`.
- [ ] Status file updated at `/data/status.json`. - [ ] Status file updated at `/data/status.json`.

View File

@@ -36,6 +36,15 @@ Requirements:
- `SEGMENT_DONE` - `SEGMENT_DONE`
- `ABORT_COMPLETE` - `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 ## v1 production subset
Manual v1 only needs: Manual v1 only needs:

View File

@@ -34,6 +34,8 @@ spindle_motor_temp_c
arm_angle_linearized_deg arm_angle_linearized_deg
table_rpm_setpoint table_rpm_setpoint
spindle_rpm_setpoint spindle_rpm_setpoint
spindle_direction_setpoint
spindle_direction_actual
force_actuator_cmd force_actuator_cmd
estop_active estop_active
interlock_state interlock_state
@@ -50,6 +52,7 @@ fz_contact_n
- Sensor validity is explicit; never substitute fake good values. - Sensor validity is explicit; never substitute fake good values.
- Gaps are detectable from timestamps. - Gaps are detectable from timestamps.
- Header names are stable because downstream analysis will depend on them. - 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 ## CSV baseline

View File

@@ -16,6 +16,7 @@
- force N - force N
- table RPM - table RPM
- spindle 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 - optional force modulation harmonic/amplitude/phase
7. Operator presses Start. 7. Operator presses Start.
8. Host sends `MANUAL_START`. 8. Host sends `MANUAL_START`.
@@ -27,6 +28,6 @@
- Manual mode cannot start with stale geometry. - Manual mode cannot start with stale geometry.
- Manual mode uses the same safety/interlocks as job mode. - 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. - Telemetry always runs while force/motion is active.
- Tool removal uses the documented mechanical sequence; no powered Zero-G in v1. - Tool removal uses the documented mechanical sequence; no powered Zero-G in v1.

View File

@@ -16,6 +16,7 @@ This checklist condenses the current P11 firmware/control spec for implementatio
- [ ] Force PID tracks setpoint within agreed commissioning tolerance. - [ ] Force PID tracks setpoint within agreed commissioning tolerance.
- [ ] Force modulation uses live table encoder angle. - [ ] Force modulation uses live table encoder angle.
- [ ] Table and spindle RPM follow commands. - [ ] 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. - [ ] Pause time excluded from segment polishing time.
## Telemetry ## Telemetry
@@ -39,7 +40,7 @@ This checklist condenses the current P11 firmware/control spec for implementatio
## Manual mode ## Manual mode
- [ ] `MANUAL` reachable from `IDLE`. - [ ] `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. - [ ] Geometric gate blocks stale geometry.
- [ ] Setpoint changes are timestamped events. - [ ] Setpoint changes are timestamped events.
- [ ] Manual-session log emitted on exit. - [ ] Manual-session log emitted on exit.

View File

@@ -141,7 +141,7 @@ Program execution exists as a contract scaffold and future path, but the v1 prod
### v1 must include ### v1 must include
- `MANUAL` state reachable from `IDLE` only. - `MANUAL` state reachable from `IDLE` only.
- Operator live controls for force, table RPM, spindle RPM, and optional force modulation. - 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`. - Mandatory geometric gate before `MANUAL` or `RUNNING`.
- Full telemetry at **≥100 Hz** with a single Teensy monotonic timestamp source. - Full telemetry at **≥100 Hz** with a single Teensy monotonic timestamp source.
- Manual-session log emitted on exit. - Manual-session log emitted on exit.
@@ -214,7 +214,7 @@ Shop-floor sequence:
- `configured_arm_amplitude_deg` - `configured_arm_amplitude_deg`
- `configured_arm_center_deg` - `configured_arm_center_deg`
5. Operator confirms each value; no skip path. 5. Operator confirms each value; no skip path.
6. Operator enters initial force, table RPM, spindle RPM, optional modulation. 6. Operator enters initial force, table RPM, spindle RPM, spindle rotation direction (`cw`/`ccw`), optional modulation.
7. Host sends `MANUAL_START`; Teensy ACK/NACKs. 7. Host sends `MANUAL_START`; Teensy ACK/NACKs.
8. Telemetry begins at ≥100 Hz. 8. Telemetry begins at ≥100 Hz.
9. Operator adjusts live setpoints; each change is logged as an event. 9. Operator adjusts live setpoints; each change is logged as an event.
@@ -263,6 +263,8 @@ Shop-floor sequence:
- `force_setpoint_n` - `force_setpoint_n`
- `table_rpm_setpoint` - `table_rpm_setpoint`
- `spindle_rpm_setpoint` - `spindle_rpm_setpoint`
- `spindle_direction_setpoint`
- `spindle_direction_actual`
- `force_actuator_cmd` - `force_actuator_cmd`
- `estop_active` - `estop_active`
- `interlock_state` - `interlock_state`

View File

@@ -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

View File

@@ -4,7 +4,8 @@ This repo foundation is derived from the current P11 / Fullum polisher-control p
Primary source documents: Primary source documents:
- Fullum PolisherMachine Control & Firmware Specification v1 - `software-suite/control/firmware/Fullum-Polisher-Machine-Control-Firmware-Spec-Report.md` / `.pdf` — Cédric-facing report wrapper.
- `software-suite/control/firmware/Fullum-Polisher-Machine-Control-Firmware-Spec-v1.md` — transcluded implementation spec body.
- Controller / Bridge / Digital Twin Architecture Plan - Controller / Bridge / Digital Twin Architecture Plan
- polisher-control — System Definition - polisher-control — System Definition
- polisher-control — Roadmap - polisher-control — Roadmap

View File

@@ -1,6 +1,50 @@
from enum import StrEnum
CONTROLLER_SCHEMA_VERSION = "controller-job.v1" CONTROLLER_SCHEMA_VERSION = "controller-job.v1"
RUN_LOG_SCHEMA_VERSION = "run-log.v1" RUN_LOG_SCHEMA_VERSION = "run-log.v1"
MANUAL_SESSION_SCHEMA_VERSION = "manual-session-log.v1" MANUAL_SESSION_SCHEMA_VERSION = "manual-session-log.v1"
MACHINE_CAPABILITIES_SCHEMA_VERSION = "machine-capabilities.v1" MACHINE_CAPABILITIES_SCHEMA_VERSION = "machine-capabilities.v1"
MACHINE_ID = "fullum-alpha" MACHINE_ID = "fullum-alpha"
CONTROLLER_VERSION_PREFIX = "polisher-control/" 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

View File

@@ -28,6 +28,8 @@ RECOMMENDED_CHANNELS = [
"arm_angle_linearized_deg", "arm_angle_linearized_deg",
"table_rpm_setpoint", "table_rpm_setpoint",
"spindle_rpm_setpoint", "spindle_rpm_setpoint",
"spindle_direction_setpoint",
"spindle_direction_actual",
"force_actuator_cmd", "force_actuator_cmd",
"estop_active", "estop_active",
"interlock_state", "interlock_state",

View File

@@ -107,6 +107,16 @@
"name": "force_setpoint_n", "name": "force_setpoint_n",
"unit": "see docs/05-telemetry-channel-spec-v1.md", "unit": "see docs/05-telemetry-channel-spec-v1.md",
"sample_rate_hz": 100 "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": { "safety_limits": {
@@ -126,5 +136,9 @@
"Final ODrive runtime interface and command scaling.", "Final ODrive runtime interface and command scaling.",
"Final SV2A-2150 torque/current command scaling and Iq monitor mapping.", "Final SV2A-2150 torque/current command scaling and Iq monitor mapping.",
"Final safety relay model and wiring details." "Final safety relay model and wiring details."
],
"supported_spindle_directions": [
"cw",
"ccw"
] ]
} }

View File

@@ -65,13 +65,23 @@
"description": "Explicit record of every constraint, clip, or approximation applied.", "description": "Explicit record of every constraint, clip, or approximation applied.",
"items": { "items": {
"type": "object", "type": "object",
"required": ["source_pass_id", "field", "description"], "required": [
"source_pass_id",
"field",
"description"
],
"properties": { "properties": {
"source_pass_id": { "type": "string" }, "source_pass_id": {
"field": { "type": "string" }, "type": "string"
},
"field": {
"type": "string"
},
"planned_value": {}, "planned_value": {},
"translated_value": {}, "translated_value": {},
"description": { "type": "string" } "description": {
"type": "string"
}
} }
} }
} }
@@ -86,31 +96,71 @@
"duration_s", "duration_s",
"commanded_force_n", "commanded_force_n",
"commanded_table_rpm", "commanded_table_rpm",
"commanded_spindle_rpm" "commanded_spindle_rpm",
"commanded_spindle_direction"
], ],
"properties": { "properties": {
"segment_id": { "type": "string" }, "segment_id": {
"sequence_index": { "type": "integer", "minimum": 1 }, "type": "string"
},
"sequence_index": {
"type": "integer",
"minimum": 1
},
"source_pass_id": { "source_pass_id": {
"type": "string", "type": "string",
"description": "Back-reference to the planning pass this segment came from." "description": "Back-reference to the planning pass this segment came from."
}, },
"duration_s": { "type": "number", "minimum": 0 }, "duration_s": {
"commanded_force_n": { "type": "number", "minimum": 0 }, "type": "number",
"commanded_table_rpm": { "type": "number", "minimum": 0 }, "minimum": 0
"commanded_spindle_rpm": { "type": "number", "minimum": 0 }, },
"commanded_cam_amplitude_deg": { "type": "number" }, "commanded_force_n": {
"commanded_cam_offset_deg": { "type": "number" }, "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": { "force_modulation": {
"type": "object", "type": "object",
"properties": { "properties": {
"harmonic": { "type": "integer" }, "harmonic": {
"amplitude_n": { "type": "number" }, "type": "integer"
"phase_deg": { "type": "number" } },
"amplitude_n": {
"type": "number"
},
"phase_deg": {
"type": "number"
}
} }
}, },
"dither_profile": { "type": "string" }, "dither_profile": {
"notes": { "type": "string" } "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."
}
} }
} }
} }

View File

@@ -25,7 +25,8 @@
"phase_deg": 37.2 "phase_deg": 37.2
}, },
"dither_profile": "none", "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": [] "translation_losses": []

View File

@@ -36,7 +36,8 @@
"amplitude_n": 5.0, "amplitude_n": 5.0,
"phase_deg": 37.2 "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": { "predicted_outcome": {
@@ -53,7 +54,10 @@
} }
}, },
"uncertainty": { "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." "notes": "Uncertainty dominated by Preston coefficient calibration spread."
}, },
"attachments": [ "attachments": [

View File

@@ -5,31 +5,85 @@
"machine_name": "Fullum Swing-Arm Polisher", "machine_name": "Fullum Swing-Arm Polisher",
"controller_version": "polisher-control/0.1.0", "controller_version": "polisher-control/0.1.0",
"last_verified": "2026-06-01T00:00:00Z", "last_verified": "2026-06-01T00:00:00Z",
"supported_motion_families": ["swing-arm-rosette"], "supported_motion_families": [
"force_range_n": [5, 200], "swing-arm-rosette"
"table_rpm_range": [0.5, 10.0], ],
"spindle_rpm_range": [10, 120], "force_range_n": [
"cam_amplitude_range_deg": [1.0, 31.3], 5,
"cam_offset_range_deg": [-30.0, 30.0], 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": { "force_modulation": {
"supported": true, "supported": true,
"max_harmonics": 3, "max_harmonics": 3,
"max_amplitude_n": 24.0, "max_amplitude_n": 24.0,
"notes": "m=2 astigmatism and m=3 trefoil are proven channels. m=1 coma is unreliable (score 0.09)." "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": { "segment_duration_limits": {
"min_s": 30, "min_s": 30,
"max_s": 7200 "max_s": 7200
}, },
"pause_resume_support": true, "pause_resume_support": true,
"telemetry_channels": [ "telemetry_channels": [
{ "name": "table_rpm", "unit": "RPM", "sample_rate_hz": 100 }, {
{ "name": "spindle_rpm", "unit": "RPM", "sample_rate_hz": 100 }, "name": "table_rpm",
{ "name": "force_n", "unit": "N", "sample_rate_hz": 100 }, "unit": "RPM",
{ "name": "arm_angle_deg", "unit": "degrees", "sample_rate_hz": 100 }, "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_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": { "safety_limits": {
"max_force_n": 200, "max_force_n": 200,
@@ -38,14 +92,18 @@
"notes": "Force hard-limited by load cell interlock. RPM limits are VFD/servo configured." "notes": "Force hard-limited by load cell interlock. RPM limits are VFD/servo configured."
}, },
"known_constraints": [ "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.", "Force modulation bandwidth limited to ~5 Hz by actuator response.",
"Table encoder is absolute but may have 1-2 count jitter.", "Table encoder is absolute but may have 1-2 count jitter.",
"Serial interface to Teensy limits command update rate to ~50 Hz effective." "Serial interface to Teensy limits command update rate to ~50 Hz effective."
], ],
"unknowns": [ "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.", "Thermal drift effect on force sensor over runs longer than 1 hour.",
"Arm pivot play magnitude under varying force loads." "Arm pivot play magnitude under varying force loads."
],
"supported_spindle_directions": [
"cw",
"ccw"
] ]
} }

View File

@@ -20,7 +20,8 @@
"table_rpm": 4.0, "table_rpm": 4.0,
"spindle_rpm": 40.0, "spindle_rpm": 40.0,
"cam_amplitude_deg": 31.3, "cam_amplitude_deg": 31.3,
"cam_offset_deg": 0.0 "cam_offset_deg": 0.0,
"spindle_direction": "cw"
}, },
"actual": { "actual": {
"force_n_mean": 44.7, "force_n_mean": 44.7,
@@ -37,7 +38,8 @@
"commanded_summary": { "commanded_summary": {
"force_n": 45.0, "force_n": 45.0,
"table_rpm": 4.0, "table_rpm": 4.0,
"spindle_rpm": 40.0 "spindle_rpm": 40.0,
"spindle_direction": "cw"
}, },
"actual_summary": { "actual_summary": {
"force_n_mean": 44.7, "force_n_mean": 44.7,

View File

@@ -55,15 +55,21 @@
}, },
"strategy_summary": { "strategy_summary": {
"type": "object", "type": "object",
"required": ["intent"], "required": [
"intent"
],
"properties": { "properties": {
"intent": { "type": "string" }, "intent": {
"type": "string"
},
"confidence": { "confidence": {
"type": "number", "type": "number",
"minimum": 0, "minimum": 0,
"maximum": 1 "maximum": 1
}, },
"notes": { "type": "string" } "notes": {
"type": "string"
}
} }
}, },
"passes": { "passes": {
@@ -77,11 +83,17 @@
"type": "object", "type": "object",
"description": "Summary of predicted post-polish surface state.", "description": "Summary of predicted post-polish surface state.",
"properties": { "properties": {
"removal_rms_nm": { "type": "number" }, "removal_rms_nm": {
"removal_pv_nm": { "type": "number" }, "type": "number"
},
"removal_pv_nm": {
"type": "number"
},
"residual_zernikes": { "residual_zernikes": {
"type": "object", "type": "object",
"additionalProperties": { "type": "number" } "additionalProperties": {
"type": "number"
}
} }
} }
}, },
@@ -91,22 +103,35 @@
"properties": { "properties": {
"removal_rms_nm_range": { "removal_rms_nm_range": {
"type": "array", "type": "array",
"items": { "type": "number" }, "items": {
"type": "number"
},
"minItems": 2, "minItems": 2,
"maxItems": 2 "maxItems": 2
}, },
"notes": { "type": "string" } "notes": {
"type": "string"
}
} }
}, },
"attachments": { "attachments": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"required": ["filename", "role"], "required": [
"filename",
"role"
],
"properties": { "properties": {
"filename": { "type": "string" }, "filename": {
"role": { "type": "string" }, "type": "string"
"description": { "type": "string" } },
"role": {
"type": "string"
},
"description": {
"type": "string"
}
} }
} }
} }
@@ -121,37 +146,81 @@
"force_n", "force_n",
"table_rpm", "table_rpm",
"spindle_rpm", "spindle_rpm",
"spindle_direction",
"motion_family" "motion_family"
], ],
"properties": { "properties": {
"pass_id": { "type": "string" }, "pass_id": {
"sequence_index": { "type": "integer", "minimum": 1 }, "type": "string"
"objective": { "type": "string" }, },
"preset_name": { "type": "string" }, "sequence_index": {
"duration_s": { "type": "number", "minimum": 0 }, "type": "integer",
"force_n": { "type": "number", "minimum": 0 }, "minimum": 1
"table_rpm": { "type": "number", "minimum": 0 }, },
"spindle_rpm": { "type": "number", "minimum": 0 }, "objective": {
"motion_family": { "type": "string" }, "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": { "motion_params": {
"type": "object", "type": "object",
"additionalProperties": true "additionalProperties": true
}, },
"zone": { "type": "string" }, "zone": {
"dither_profile": { "type": "string" }, "type": "string"
},
"dither_profile": {
"type": "string"
},
"force_modulation": { "force_modulation": {
"type": "object", "type": "object",
"properties": { "properties": {
"harmonic": { "type": "integer" }, "harmonic": {
"amplitude_n": { "type": "number" }, "type": "integer"
"phase_deg": { "type": "number" } },
"amplitude_n": {
"type": "number"
},
"phase_deg": {
"type": "number"
}
} }
}, },
"acceptance": { "acceptance": {
"type": "object", "type": "object",
"additionalProperties": true "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."
}
} }
} }
} }

View File

@@ -12,7 +12,8 @@
"supported_motion_families", "supported_motion_families",
"force_range_n", "force_range_n",
"table_rpm_range", "table_rpm_range",
"spindle_rpm_range" "spindle_rpm_range",
"supported_spindle_directions"
], ],
"properties": { "properties": {
"schema_version": { "schema_version": {
@@ -38,7 +39,9 @@
}, },
"supported_motion_families": { "supported_motion_families": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"minItems": 1 "minItems": 1
}, },
"force_range_n": { "force_range_n": {
@@ -60,22 +63,36 @@
"type": "object", "type": "object",
"description": "Force modulation capabilities. Omit entirely if unknown.", "description": "Force modulation capabilities. Omit entirely if unknown.",
"properties": { "properties": {
"supported": { "type": "boolean" }, "supported": {
"max_harmonics": { "type": "integer" }, "type": "boolean"
"max_amplitude_n": { "type": "number" }, },
"notes": { "type": "string" } "max_harmonics": {
"type": "integer"
},
"max_amplitude_n": {
"type": "number"
},
"notes": {
"type": "string"
}
} }
}, },
"supported_dither_profiles": { "supported_dither_profiles": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"description": "List of dither profile names the controller can handle. Empty = none supported." "description": "List of dither profile names the controller can handle. Empty = none supported."
}, },
"segment_duration_limits": { "segment_duration_limits": {
"type": "object", "type": "object",
"properties": { "properties": {
"min_s": { "type": "number" }, "min_s": {
"max_s": { "type": "number" } "type": "number"
},
"max_s": {
"type": "number"
}
} }
}, },
"pause_resume_support": { "pause_resume_support": {
@@ -85,12 +102,23 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"required": ["name", "unit"], "required": [
"name",
"unit"
],
"properties": { "properties": {
"name": { "type": "string" }, "name": {
"unit": { "type": "string" }, "type": "string"
"sample_rate_hz": { "type": "number" }, },
"notes": { "type": "string" } "unit": {
"type": "string"
},
"sample_rate_hz": {
"type": "number"
},
"notes": {
"type": "string"
}
} }
} }
}, },
@@ -98,21 +126,46 @@
"type": "object", "type": "object",
"description": "Hard safety limits that the controller enforces regardless of job requests.", "description": "Hard safety limits that the controller enforces regardless of job requests.",
"properties": { "properties": {
"max_force_n": { "type": "number" }, "max_force_n": {
"max_table_rpm": { "type": "number" }, "type": "number"
"max_spindle_rpm": { "type": "number" }, },
"notes": { "type": "string" } "max_table_rpm": {
"type": "number"
},
"max_spindle_rpm": {
"type": "number"
},
"notes": {
"type": "string"
}
} }
}, },
"known_constraints": { "known_constraints": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"description": "Free-text list of known limitations, quirks, or warnings." "description": "Free-text list of known limitations, quirks, or warnings."
}, },
"unknowns": { "unknowns": {
"type": "array", "type": "array",
"items": { "type": "string" }, "items": {
"type": "string"
},
"description": "Capabilities that have NOT been verified. Explicit unknowns prevent fake certainty." "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": { "$defs": {
@@ -120,7 +173,9 @@
"oneOf": [ "oneOf": [
{ {
"type": "array", "type": "array",
"items": { "type": "number" }, "items": {
"type": "number"
},
"minItems": 2, "minItems": 2,
"maxItems": 2, "maxItems": 2,
"description": "[min, max] range." "description": "[min, max] range."

View File

@@ -49,7 +49,12 @@
}, },
"result_state": { "result_state": {
"type": "string", "type": "string",
"enum": ["completed", "completed_with_pause", "aborted", "faulted"] "enum": [
"completed",
"completed_with_pause",
"aborted",
"faulted"
]
}, },
"segments": { "segments": {
"type": "array", "type": "array",
@@ -61,31 +66,72 @@
"commanded_summary": { "commanded_summary": {
"type": "object", "type": "object",
"properties": { "properties": {
"force_n": { "type": "number" }, "force_n": {
"table_rpm": { "type": "number" }, "type": "number"
"spindle_rpm": { "type": "number" } },
"table_rpm": {
"type": "number"
},
"spindle_rpm": {
"type": "number"
},
"spindle_direction": {
"type": "string",
"enum": [
"cw",
"ccw"
]
}
} }
}, },
"actual_summary": { "actual_summary": {
"type": "object", "type": "object",
"properties": { "properties": {
"force_n_mean": { "type": "number" }, "force_n_mean": {
"force_n_min": { "type": "number" }, "type": "number"
"force_n_max": { "type": "number" }, },
"table_rpm_mean": { "type": "number" }, "force_n_min": {
"spindle_rpm_mean": { "type": "number" } "type": "number"
},
"force_n_max": {
"type": "number"
},
"table_rpm_mean": {
"type": "number"
},
"spindle_rpm_mean": {
"type": "number"
}
} }
}, },
"alarms": { "alarms": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"required": ["timestamp", "code", "message"], "required": [
"timestamp",
"code",
"message"
],
"properties": { "properties": {
"timestamp": { "type": "string", "format": "date-time" }, "timestamp": {
"code": { "type": "string" }, "type": "string",
"message": { "type": "string" }, "format": "date-time"
"severity": { "type": "string", "enum": ["info", "warning", "critical"] } },
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"severity": {
"type": "string",
"enum": [
"info",
"warning",
"critical"
]
}
} }
} }
}, },
@@ -93,11 +139,21 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"required": ["timestamp", "type"], "required": [
"timestamp",
"type"
],
"properties": { "properties": {
"timestamp": { "type": "string", "format": "date-time" }, "timestamp": {
"type": { "type": "string" }, "type": "string",
"detail": { "type": "string" } "format": "date-time"
},
"type": {
"type": "string"
},
"detail": {
"type": "string"
}
} }
} }
}, },
@@ -120,51 +176,107 @@
"result_state" "result_state"
], ],
"properties": { "properties": {
"segment_id": { "type": "string" }, "segment_id": {
"source_pass_id": { "type": "string" }, "type": "string"
"commanded_duration_s": { "type": "number" }, },
"actual_duration_s": { "type": "number" }, "source_pass_id": {
"type": "string"
},
"commanded_duration_s": {
"type": "number"
},
"actual_duration_s": {
"type": "number"
},
"result_state": { "result_state": {
"type": "string", "type": "string",
"enum": ["completed", "completed_with_pause", "aborted", "faulted", "skipped"] "enum": [
"completed",
"completed_with_pause",
"aborted",
"faulted",
"skipped"
]
}, },
"commanded": { "commanded": {
"type": "object", "type": "object",
"properties": { "properties": {
"force_n": { "type": "number" }, "force_n": {
"table_rpm": { "type": "number" }, "type": "number"
"spindle_rpm": { "type": "number" }, },
"cam_amplitude_deg": { "type": "number" }, "table_rpm": {
"cam_offset_deg": { "type": "number" } "type": "number"
},
"spindle_rpm": {
"type": "number"
},
"cam_amplitude_deg": {
"type": "number"
},
"cam_offset_deg": {
"type": "number"
},
"spindle_direction": {
"type": "string",
"enum": [
"cw",
"ccw"
]
}
} }
}, },
"actual": { "actual": {
"type": "object", "type": "object",
"properties": { "properties": {
"force_n_mean": { "type": "number" }, "force_n_mean": {
"force_n_min": { "type": "number" }, "type": "number"
"force_n_max": { "type": "number" }, },
"table_rpm_mean": { "type": "number" }, "force_n_min": {
"spindle_rpm_mean": { "type": "number" } "type": "number"
},
"force_n_max": {
"type": "number"
},
"table_rpm_mean": {
"type": "number"
},
"spindle_rpm_mean": {
"type": "number"
}
} }
}, },
"pause_windows": { "pause_windows": {
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
"required": ["paused_at", "resumed_at"], "required": [
"paused_at",
"resumed_at"
],
"properties": { "properties": {
"paused_at": { "type": "string", "format": "date-time" }, "paused_at": {
"resumed_at": { "type": "string", "format": "date-time" }, "type": "string",
"reason": { "type": "string" } "format": "date-time"
},
"resumed_at": {
"type": "string",
"format": "date-time"
},
"reason": {
"type": "string"
}
} }
} }
}, },
"anomaly_flags": { "anomaly_flags": {
"type": "array", "type": "array",
"items": { "type": "string" } "items": {
"type": "string"
}
}, },
"notes": { "type": "string" } "notes": {
"type": "string"
}
} }
} }
} }

View 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