docs: scaffold polisher-control foundation
This commit is contained in:
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
.venv/
|
||||
venv/
|
||||
*.egg-info/
|
||||
|
||||
# PlatformIO / embedded
|
||||
.pio/
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
|
||||
# Runtime data: never commit real run data
|
||||
/data/
|
||||
data/runs/
|
||||
data/manual/
|
||||
*.parquet
|
||||
*.npz
|
||||
|
||||
# Local config/secrets
|
||||
.env
|
||||
*.local.*
|
||||
secrets.*
|
||||
42
AGENTS.md
Normal file
42
AGENTS.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# AGENTS.md — Polisher-Control Repo Rules
|
||||
|
||||
This repository is an external-collaborator technical workspace for the P11 / Fullum / GigaBIT polisher-control implementation.
|
||||
|
||||
## Scope
|
||||
|
||||
Allowed work:
|
||||
- Teensy firmware
|
||||
- Raspberry Pi / host controller
|
||||
- touchscreen/manual-mode workflow support
|
||||
- host↔Teensy protocol
|
||||
- telemetry/logging/run artifacts
|
||||
- safety/interlocks/alarms/watchdogs
|
||||
- machine capability profile
|
||||
- tests, commissioning scripts, and technical docs
|
||||
|
||||
Out of scope:
|
||||
- optical figuring strategy
|
||||
- metrology interpretation
|
||||
- optimization / calibration logic
|
||||
- client/commercial context
|
||||
- private Antoine notes / unrelated projects
|
||||
- credentials or secrets
|
||||
|
||||
## Architecture rule
|
||||
|
||||
`polisher-control` executes. It does not decide strategy.
|
||||
|
||||
If a change affects architecture, telemetry names/units/rates, safety behavior,
|
||||
protocol semantics, or v1 scope, log it as a proposal and ask Antoine before
|
||||
treating it as accepted direction.
|
||||
|
||||
## Commit hygiene
|
||||
|
||||
Prefer small commits:
|
||||
- `docs: ...`
|
||||
- `firmware: ...`
|
||||
- `host: ...`
|
||||
- `test: ...`
|
||||
- `config: ...`
|
||||
|
||||
Never commit real machine data, secrets, invoices, tokens, or private notes.
|
||||
16
CONTRIBUTING.md
Normal file
16
CONTRIBUTING.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Contributing
|
||||
|
||||
## Working style
|
||||
|
||||
- Keep changes small and reviewable.
|
||||
- Add or update docs when changing protocol, telemetry, state machine, safety, or data layout.
|
||||
- Add tests for host-side state-machine and contract behavior before relying on hardware.
|
||||
- Never hide clipping, rejected commands, or safety changes; emit explicit events/NACKs.
|
||||
|
||||
## Definition of done for a feature
|
||||
|
||||
- Implementation exists.
|
||||
- Test or bench verification exists.
|
||||
- Relevant docs are updated.
|
||||
- Safety or contract impact is stated.
|
||||
- Antoine-facing open questions are listed if the decision is not locked.
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
.PHONY: test validate-json tree
|
||||
|
||||
test:
|
||||
python -m pytest -q
|
||||
|
||||
validate-json:
|
||||
python scripts/validate_json.py
|
||||
|
||||
tree:
|
||||
python scripts/print_tree.py
|
||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Polisher-Control
|
||||
|
||||
Machine-side execution stack for the Fullum / GigaBIT swing-arm polisher.
|
||||
|
||||
This repository is for Cédric's implementation work on the **controller, firmware,
|
||||
manual-mode UI support, telemetry, safety/interlocks, and run artifacts**. It is
|
||||
aligned with the broader Polisher Software Suite:
|
||||
|
||||
```text
|
||||
polisher-sim -> planning / digital twin / calibration / metrology intelligence
|
||||
polisher-post -> validation + packaging of controller-safe jobs
|
||||
polisher-control -> safe machine execution + telemetry + operator workflow
|
||||
shared contracts -> schemas, logs, machine capabilities, provenance
|
||||
```
|
||||
|
||||
## v1 finish line
|
||||
|
||||
Normand can operate the polisher manually from the touchscreen, with:
|
||||
|
||||
- safe force/table/spindle control;
|
||||
- KWR75B-CAN force/torque sensor integration;
|
||||
- ODrive S1 + M8325s spindle drive integration;
|
||||
- Teensy-side fast safety and setpoint/telemetry loop;
|
||||
- host-side state machine, logs, status, and data export;
|
||||
- synchronized telemetry at **>=100 Hz**;
|
||||
- manual-session logs usable by `polisher-sim` / `polisher-post` later;
|
||||
- safety/interlocks enforced and visible in logs.
|
||||
|
||||
## What this repo must NOT become
|
||||
|
||||
`polisher-control` does **not** own optical strategy. It must not interpret
|
||||
interferograms, optimize polishing passes, learn calibration parameters, or invent
|
||||
correction programs. Those decisions belong upstream in `polisher-sim` and
|
||||
`polisher-post`.
|
||||
|
||||
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).
|
||||
|
||||
## Repository map
|
||||
|
||||
```text
|
||||
firmware/teensy/ Teensy 4.1 firmware scaffold and protocol/telemetry structs
|
||||
host/ Python host-controller scaffold for RPi/PC side
|
||||
shared/schemas/ Contract schemas mirrored from the current software suite
|
||||
shared/machine/ Fullum-alpha machine capability profile draft
|
||||
docs/ Cédric-facing implementation docs and checklists
|
||||
docs/nick-generated/ Nick-generated source-grounded guidance notes
|
||||
scripts/ Utility/validation scripts
|
||||
tests/ Host-side contract/state-machine tests
|
||||
```
|
||||
|
||||
## Current source alignment
|
||||
|
||||
Generated: 2026-05-26. This foundation is a repo scaffold and implementation aid,
|
||||
not a replacement for the approved P11 source specifications. If this repo and
|
||||
the source specs disagree, ask Antoine before changing architecture, safety,
|
||||
telemetry contracts, or scope.
|
||||
49
ROADMAP.md
Normal file
49
ROADMAP.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Polisher-Control Roadmap
|
||||
|
||||
## Phase 0 — Repository foundation
|
||||
|
||||
- [x] Create repository structure.
|
||||
- [x] Add ecosystem boundary docs.
|
||||
- [x] Add v1 scope and acceptance checklist.
|
||||
- [x] Add host/firmware scaffolds.
|
||||
- [x] Mirror current shared schemas.
|
||||
|
||||
## Phase 1 — Contracts first
|
||||
|
||||
- [ ] Lock protocol frame format: message IDs, payload layout, CRC, ACK/NACK.
|
||||
- [ ] Lock telemetry channel list, units, rates, validity encoding.
|
||||
- [ ] Lock event/alarm code table.
|
||||
- [ ] Validate `controller-job.v1` and `run-log.v1` fixtures.
|
||||
- [ ] Implement host-side state machine tests before hardware logic grows.
|
||||
|
||||
## Phase 2 — Teensy bench bring-up
|
||||
|
||||
- [ ] Teensy boot/status heartbeat.
|
||||
- [ ] 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.
|
||||
- [ ] 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.
|
||||
- [ ] 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`.
|
||||
|
||||
## Phase 4 — Safety and commissioning
|
||||
|
||||
- [ ] E-stop behavior verified independently of software state.
|
||||
- [ ] Force over-limit, encoder loss, drive fault, F/T invalid/stale faults verified.
|
||||
- [ ] Host watchdog and firmware heartbeat verified.
|
||||
- [ ] FAULTED exits only by explicit operator reset.
|
||||
- [ ] Tool-weight compensation offset commissioned and logged.
|
||||
|
||||
## Phase 5 — Controller-job dry run
|
||||
|
||||
- [ ] `controller-job.v1` 4-gate intake implemented.
|
||||
- [ ] Segment lifecycle exercised on bench/simulator.
|
||||
- [ ] `run-log.v1` emitted with commanded-vs-actual structure.
|
||||
- [ ] Program execution remains disabled for production until Antoine explicitly accepts readiness.
|
||||
33
docs/00-start-here.md
Normal file
33
docs/00-start-here.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Start Here — Cédric Build Brief
|
||||
|
||||
## Mission
|
||||
|
||||
Build the machine-side control stack for the Fullum swing-arm polisher so Normand can run manual polishing sessions safely while producing clean telemetry for the rest of the software suite.
|
||||
|
||||
## v1 priority order
|
||||
|
||||
1. Safety/interlocks and deterministic state machine.
|
||||
2. Manual mode from the touchscreen / host UI.
|
||||
3. Stable host↔Teensy setpoint + telemetry protocol.
|
||||
4. KWR75B-CAN force/torque acquisition and stale-frame detection.
|
||||
5. Table/arm encoder acquisition and synchronized timestamps.
|
||||
6. ODrive spindle command/telemetry path.
|
||||
7. Run/manual logs and `/data/` file layout.
|
||||
8. Controller-job intake dry-run for future program execution.
|
||||
|
||||
## Mental model
|
||||
|
||||
The host says: "run these approved setpoints and log what happens."
|
||||
|
||||
The Teensy says: "accepted/rejected, here is measured reality, here are events/faults."
|
||||
|
||||
Neither side invents polishing strategy.
|
||||
|
||||
## Ask Nick for
|
||||
|
||||
- protocol notes;
|
||||
- firmware module breakdowns;
|
||||
- test checklists;
|
||||
- telemetry schema clarifications;
|
||||
- state-machine/safety edge cases;
|
||||
- concise implementation notes in `docs/nick-generated/`.
|
||||
48
docs/01-ecosystem-boundaries.md
Normal file
48
docs/01-ecosystem-boundaries.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Ecosystem Boundaries
|
||||
|
||||
## polisher-sim
|
||||
|
||||
Owns planning intelligence:
|
||||
- metrology ingestion;
|
||||
- mirror state and residuals;
|
||||
- removal prediction;
|
||||
- dwell/preset planning;
|
||||
- calibration and uncertainty;
|
||||
- campaign reports.
|
||||
|
||||
## polisher-post
|
||||
|
||||
Owns translation and validation:
|
||||
- schema validation;
|
||||
- machine capability checks;
|
||||
- unit normalization;
|
||||
- segmentation;
|
||||
- packaging `controller-job.v1`;
|
||||
- run-log normalization back into analysis format.
|
||||
|
||||
## polisher-control
|
||||
|
||||
Owns machine execution:
|
||||
- state machine;
|
||||
- manual mode;
|
||||
- setpoint execution;
|
||||
- force loop interface;
|
||||
- telemetry;
|
||||
- alarms/interlocks;
|
||||
- pause/resume/abort;
|
||||
- run/manual-session logs;
|
||||
- operator workflow.
|
||||
|
||||
## Shared contracts
|
||||
|
||||
Own schemas and names that must not drift silently:
|
||||
- `controller-job.v1`;
|
||||
- `run-log.v1`;
|
||||
- `manual-session-log.v1`;
|
||||
- `machine-capabilities.v1`;
|
||||
- telemetry channel names;
|
||||
- event/alarm codes.
|
||||
|
||||
## Boundary rule
|
||||
|
||||
If you are about to add planning, optimization, calibration, or metrology interpretation to this repo, stop and ask Antoine. That likely belongs upstream.
|
||||
30
docs/02-v1-scope.md
Normal file
30
docs/02-v1-scope.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# v1 Scope
|
||||
|
||||
## In scope
|
||||
|
||||
- Manual mode with live operator setpoints.
|
||||
- Full safety/interlock handling.
|
||||
- Host↔Teensy command/telemetry protocol.
|
||||
- KWR75B-CAN force/torque sensor integration.
|
||||
- Table and arm encoder acquisition.
|
||||
- ODrive S1 + M8325s spindle control/telemetry path.
|
||||
- Force actuator command path and tool-weight compensation.
|
||||
- Synchronized telemetry at >=100 Hz.
|
||||
- Manual-session logs and raw telemetry CSV.
|
||||
- USB SSD `/data/` storage layout.
|
||||
- `status.json` machine summary.
|
||||
- Controller-job validation path for future use.
|
||||
|
||||
## Explicitly not in v1
|
||||
|
||||
- Figuring strategy.
|
||||
- Metrology import/interpretation.
|
||||
- Calibration updates.
|
||||
- Autonomous optimization.
|
||||
- Powered Zero-G/admittance manipulation mode.
|
||||
- Remote control of the machine.
|
||||
- Multi-pass orchestration inside the controller.
|
||||
|
||||
## First success condition
|
||||
|
||||
A real manual polishing session can be run safely, and the resulting telemetry/logs are coherent enough for Antoine to analyze downstream.
|
||||
64
docs/03-architecture.md
Normal file
64
docs/03-architecture.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Architecture
|
||||
|
||||
## Runtime split
|
||||
|
||||
### Host controller — Python on RPi/PC
|
||||
|
||||
Responsibilities:
|
||||
- operator workflow and manual-mode UI integration;
|
||||
- state machine orchestration;
|
||||
- controller-job intake validation;
|
||||
- geometry gate;
|
||||
- setpoint command stream;
|
||||
- telemetry capture;
|
||||
- logs, manifests, hashes, status file;
|
||||
- slow watchdogs and data export.
|
||||
|
||||
### Teensy firmware — C++ on Teensy 4.x
|
||||
|
||||
Responsibilities:
|
||||
- inner loop around ~1 kHz;
|
||||
- KWR75B-CAN F/T receive path;
|
||||
- encoder acquisition;
|
||||
- force PID output;
|
||||
- table/spindle/actuator command outputs;
|
||||
- fast safety checks;
|
||||
- heartbeat supervision;
|
||||
- telemetry frames at >=100 Hz;
|
||||
- ACK/NACK and event/fault frames.
|
||||
|
||||
## State machine
|
||||
|
||||
Current accepted state names:
|
||||
|
||||
```text
|
||||
IDLE
|
||||
JOB_LOADED
|
||||
READY
|
||||
RUNNING
|
||||
PAUSED
|
||||
ABORTING
|
||||
COMPLETED
|
||||
ABORTED
|
||||
FAULTED
|
||||
MANUAL
|
||||
```
|
||||
|
||||
Important rules:
|
||||
- `FAULTED` exits only through explicit operator reset.
|
||||
- `JOB_LOADED -> READY` requires operator acknowledge.
|
||||
- `IDLE -> MANUAL` is allowed only with no job loaded.
|
||||
- Illegal transitions are errors and events, never silent ignores.
|
||||
- Manual mode uses the same safety/interlocks as job mode.
|
||||
|
||||
## Manual mode sequence
|
||||
|
||||
1. Machine starts in `IDLE`.
|
||||
2. Operator opens Manual Mode.
|
||||
3. Host enforces the geometric gate.
|
||||
4. Host sends `MANUAL_START` to Teensy.
|
||||
5. Teensy ACKs or NACKs with a machine-readable reason.
|
||||
6. Host sends live `SETPOINT` updates.
|
||||
7. Teensy emits telemetry and events.
|
||||
8. Operator stops; host sends `MANUAL_STOP`.
|
||||
9. Host writes manual-session log and artifacts.
|
||||
60
docs/04-host-teensy-protocol-v1.md
Normal file
60
docs/04-host-teensy-protocol-v1.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Host ↔ Teensy Protocol v1 — Working Spec
|
||||
|
||||
Status: draft scaffold. Cédric should finalize the exact wire format before implementation locks.
|
||||
|
||||
## Philosophy
|
||||
|
||||
This is a setpoint/telemetry protocol, not G-code.
|
||||
|
||||
Requirements:
|
||||
- versioned frames;
|
||||
- length-delimited or COBS-delimited framing;
|
||||
- CRC-16 or CRC-32 on every frame;
|
||||
- deterministic parsing;
|
||||
- ACK/NACK for every command;
|
||||
- machine-readable NACK reason codes;
|
||||
- no text parsing in the control path.
|
||||
|
||||
## Host → Teensy messages
|
||||
|
||||
- `HEARTBEAT`
|
||||
- `MANUAL_START`
|
||||
- `SETPOINT`
|
||||
- `MANUAL_STOP`
|
||||
- `SEGMENT_START`
|
||||
- `PAUSE`
|
||||
- `RESUME`
|
||||
- `ABORT`
|
||||
- `ESTOP`
|
||||
|
||||
## Teensy → Host messages
|
||||
|
||||
- `ACK`
|
||||
- `NACK`
|
||||
- `TELEMETRY`
|
||||
- `EVENT`
|
||||
- `SEGMENT_DONE`
|
||||
- `ABORT_COMPLETE`
|
||||
|
||||
## v1 production subset
|
||||
|
||||
Manual v1 only needs:
|
||||
- `HEARTBEAT`
|
||||
- `MANUAL_START`
|
||||
- `SETPOINT`
|
||||
- `MANUAL_STOP`
|
||||
- `ACK` / `NACK`
|
||||
- `TELEMETRY`
|
||||
- `EVENT`
|
||||
|
||||
But define and parse the future segment messages now so Phase 3 does not require a protocol reset.
|
||||
|
||||
## Open protocol choices
|
||||
|
||||
- JSON-lines, MessagePack, or compact binary structs?
|
||||
- CRC-16 vs CRC-32?
|
||||
- COBS framing vs length-prefix + CRC?
|
||||
- Serial baud rate and reconnect behavior?
|
||||
- Whether heartbeat is host-only or bidirectional?
|
||||
|
||||
Recommendation: choose the simplest robust format Cédric is comfortable debugging on the bench.
|
||||
56
docs/05-telemetry-channel-spec-v1.md
Normal file
56
docs/05-telemetry-channel-spec-v1.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Telemetry Channel Spec v1
|
||||
|
||||
## Core channels — required at >=100 Hz
|
||||
|
||||
```text
|
||||
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
|
||||
force_setpoint_n
|
||||
```
|
||||
|
||||
## Strongly recommended channels
|
||||
|
||||
```text
|
||||
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
|
||||
table_rpm_setpoint
|
||||
spindle_rpm_setpoint
|
||||
force_actuator_cmd
|
||||
estop_active
|
||||
interlock_state
|
||||
mode
|
||||
fz_raw_n
|
||||
fz_contact_n
|
||||
```
|
||||
|
||||
## Principles
|
||||
|
||||
- One monotonic Teensy timestamp source.
|
||||
- Raw values, not only filtered values.
|
||||
- Commanded and actual values are both present.
|
||||
- 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.
|
||||
|
||||
## CSV baseline
|
||||
|
||||
First-write compatibility format is CSV. Later processing may produce Parquet/downsampled traces, but raw CSV must be preserved during commissioning.
|
||||
46
docs/06-event-alarm-codes-v1.md
Normal file
46
docs/06-event-alarm-codes-v1.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Event and Alarm Codes v1
|
||||
|
||||
Status: draft canonical table for implementation.
|
||||
|
||||
## Hard-stop faults — reset required
|
||||
|
||||
- `ESTOP_ACTIVATED`
|
||||
- `FORCE_OVER_LIMIT`
|
||||
- `ENCODER_LOST`
|
||||
- `DRIVE_FAULT`
|
||||
- `FT_SENSOR_INVALID`
|
||||
|
||||
## Recoverable pauses / warnings
|
||||
|
||||
- `FORCE_UNDER_LIMIT`
|
||||
- `SPINDLE_RPM_DEVIATION`
|
||||
- `TABLE_RPM_DEVIATION`
|
||||
- `HOST_COMMS_TIMEOUT`
|
||||
- `ARM_GEOMETRY_DIVERGENCE`
|
||||
|
||||
## Refused transitions / blocked starts
|
||||
|
||||
- `GEOMETRY_NOT_VALIDATED`
|
||||
- `ARM_HANDLING_INTERLOCK`
|
||||
- `WRONG_ARTIFACT_TYPE`
|
||||
- `SCHEMA_VALIDATION_FAILED`
|
||||
- `WRONG_MACHINE_ID`
|
||||
- `CONTROLLER_VERSION_MISMATCH`
|
||||
- `UNSUPPORTED_CAPABILITY`
|
||||
- `ILLEGAL_TRANSITION`
|
||||
|
||||
## Event fields
|
||||
|
||||
Every event should include:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "ISO-8601 or host monotonic mapping",
|
||||
"code": "GEOMETRY_NOT_VALIDATED",
|
||||
"severity": "info|warning|critical",
|
||||
"state": "IDLE|MANUAL|...",
|
||||
"detail": "short machine-readable or operator-readable detail"
|
||||
}
|
||||
```
|
||||
|
||||
Do not use free text as the only control signal. Human messages are for UI; codes are for software.
|
||||
32
docs/07-manual-mode-workflow.md
Normal file
32
docs/07-manual-mode-workflow.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Manual Mode Workflow
|
||||
|
||||
## Operator flow
|
||||
|
||||
1. Hardware HOA selector in Auto.
|
||||
2. Machine in `IDLE`.
|
||||
3. Operator selects Manual Mode.
|
||||
4. UI presents the geometric gate:
|
||||
- `r_menante`
|
||||
- `L_menee`
|
||||
- `R_tool`
|
||||
- configured arm amplitude
|
||||
- configured arm center
|
||||
5. Operator confirms values.
|
||||
6. Operator enters setpoints:
|
||||
- force N
|
||||
- table RPM
|
||||
- spindle RPM
|
||||
- optional force modulation harmonic/amplitude/phase
|
||||
7. Operator presses Start.
|
||||
8. Host sends `MANUAL_START`.
|
||||
9. Operator may adjust setpoints live.
|
||||
10. Operator presses Stop.
|
||||
11. Host writes the manual-session log, telemetry CSV, events, manifest, and status.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Manual mode cannot start with stale geometry.
|
||||
- Manual mode uses the same safety/interlocks as job mode.
|
||||
- Every setpoint change is logged.
|
||||
- Telemetry always runs while force/motion is active.
|
||||
- Tool removal uses the documented mechanical sequence; no powered Zero-G in v1.
|
||||
45
docs/08-commissioning-checklist.md
Normal file
45
docs/08-commissioning-checklist.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Commissioning Checklist
|
||||
|
||||
## Bench — no machine motion
|
||||
|
||||
- [ ] Host can open serial link to Teensy.
|
||||
- [ ] HEARTBEAT / ACK / NACK verified.
|
||||
- [ ] Bad CRC/frame rejected with NACK or ignored safely.
|
||||
- [ ] State machine illegal transitions logged.
|
||||
- [ ] Telemetry frames parse at expected rate.
|
||||
|
||||
## Sensor bring-up
|
||||
|
||||
- [ ] KWR75B-CAN isolated CAN wiring verified.
|
||||
- [ ] CAN bit rate confirmed.
|
||||
- [ ] Vendor frame map decoded into Fx/Fy/Fz/Mx/My/Mz/status.
|
||||
- [ ] F/T stale-frame watchdog tested.
|
||||
- [ ] Table encoder angle stable and monotonic.
|
||||
- [ ] Arm encoder angle stable.
|
||||
|
||||
## Drive bring-up
|
||||
|
||||
- [ ] ODrive runtime interface selected and documented.
|
||||
- [ ] ODrive enable/disable/fault reset path verified.
|
||||
- [ ] Spindle RPM command and actual feedback verified.
|
||||
- [ ] Z force actuator command path verified with safe limits.
|
||||
- [ ] Brake engaged feedback verified if installed.
|
||||
|
||||
## Safety
|
||||
|
||||
- [ ] E-stop hard circuit tested independent of software state.
|
||||
- [ ] Force over-limit response tested.
|
||||
- [ ] Encoder-loss response tested.
|
||||
- [ ] Drive-fault response tested.
|
||||
- [ ] Host timeout -> pause -> abort behavior tested.
|
||||
- [ ] FAULTED requires explicit reset.
|
||||
|
||||
## Manual run
|
||||
|
||||
- [ ] Geometric gate blocks stale/missing geometry.
|
||||
- [ ] Manual start succeeds after valid geometry.
|
||||
- [ ] Force/table/spindle setpoints ramp smoothly.
|
||||
- [ ] Live setpoint changes are logged.
|
||||
- [ ] Telemetry CSV has required header and >=100 Hz samples.
|
||||
- [ ] Manual-session log includes setpoint history and actual summary.
|
||||
- [ ] Artifacts are saved under `/data/manual/{session_id}/`.
|
||||
53
docs/09-acceptance-checklist.md
Normal file
53
docs/09-acceptance-checklist.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Acceptance Checklist
|
||||
|
||||
This checklist condenses the current P11 firmware/control spec for implementation tracking.
|
||||
|
||||
## Input contract
|
||||
|
||||
- [ ] Accepts `controller-job.v1` only.
|
||||
- [ ] Rejects `job.v1` and malformed input.
|
||||
- [ ] Runs 4-gate intake validation.
|
||||
- [ ] Rejects wrong `machine_id`.
|
||||
- [ ] Rejects incompatible `controller_version` or unsupported capability.
|
||||
|
||||
## Execution
|
||||
|
||||
- [ ] Sequential segment execution exists for future job mode.
|
||||
- [ ] Force PID tracks setpoint within agreed commissioning tolerance.
|
||||
- [ ] Force modulation uses live table encoder angle.
|
||||
- [ ] Table and spindle RPM follow commands.
|
||||
- [ ] Pause time excluded from segment polishing time.
|
||||
|
||||
## Telemetry
|
||||
|
||||
- [ ] Core channels logged at >=100 Hz.
|
||||
- [ ] Single monotonic timestamp source.
|
||||
- [ ] Commanded and actual values both present.
|
||||
- [ ] Sensor faults are detectable.
|
||||
- [ ] CSV parseable by standard tools.
|
||||
|
||||
## State machine and safety
|
||||
|
||||
- [ ] All accepted states/transitions implemented.
|
||||
- [ ] Illegal transitions logged.
|
||||
- [ ] Operator acknowledge cannot be bypassed.
|
||||
- [ ] E-stop all-stop independent of software state.
|
||||
- [ ] Interlocks implemented with documented thresholds.
|
||||
- [ ] Watchdog timeout causes pause, then abort.
|
||||
- [ ] `FAULTED` exits only through explicit reset.
|
||||
|
||||
## Manual mode
|
||||
|
||||
- [ ] `MANUAL` reachable from `IDLE`.
|
||||
- [ ] Live setpoint adjustment works.
|
||||
- [ ] Geometric gate blocks stale geometry.
|
||||
- [ ] Setpoint changes are timestamped events.
|
||||
- [ ] Manual-session log emitted on exit.
|
||||
- [ ] Safety behavior identical to job mode.
|
||||
|
||||
## Data
|
||||
|
||||
- [ ] `/data/manual/{session_id}/` and `/data/runs/{run_id}/` layouts respected.
|
||||
- [ ] `status.json` maintained.
|
||||
- [ ] Hashes/manifests emitted.
|
||||
- [ ] Raw telemetry preserved during commissioning.
|
||||
28
docs/10-open-questions-for-cedric.md
Normal file
28
docs/10-open-questions-for-cedric.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Open Questions for Cédric
|
||||
|
||||
## Table interface
|
||||
|
||||
- Confirm KBSI-240D opto-isolated interface or replacement.
|
||||
- Confirm table encoder transport and transceiver.
|
||||
- Confirm whether the table drive or command path changes electrically.
|
||||
|
||||
## Spindle / ODrive
|
||||
|
||||
- Define ODrive runtime command and telemetry path; CAN 2.0B at 1 Mbps is preferred unless replaced.
|
||||
- Define command scaling for velocity, enable/disable, fault reset, and safe stop.
|
||||
- Define telemetry subset: actual RPM, drive state/error, DC bus voltage, q-axis current, motor temperature if available.
|
||||
- Export final ODrive config with release.
|
||||
- Define spindle enable/fault I/O into HOA + safety chain.
|
||||
|
||||
## Z-axis / force actuator
|
||||
|
||||
- Confirm SV2A-2150 torque/current command mode and scaling.
|
||||
- Confirm enable/fault wiring and Iq/current monitor path.
|
||||
- Define Z limit-switch / hard-stop wiring into safety chain.
|
||||
- Define NC 24 VDC brake coil driver and brake-engaged diagnostic feedback.
|
||||
|
||||
## Cross-cutting
|
||||
|
||||
- Select safety relay model.
|
||||
- Confirm KWR75B-CAN frame map, byte order, scaling, status bits, and update rate.
|
||||
- Confirm final serial/protocol wire format.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Polisher-Control Repository Foundation
|
||||
status: draft
|
||||
requested_by: Antoine Letarte
|
||||
generated_by: Nick / Hermes
|
||||
project: P11-Polisher-Fullum
|
||||
repo: polisher-control
|
||||
source_truth: false
|
||||
created: 2026-05-26
|
||||
privacy: technical-only
|
||||
---
|
||||
|
||||
# Polisher-Control Repository Foundation
|
||||
|
||||
## Purpose
|
||||
|
||||
Give Cédric a clean starting point for the machine-side control implementation without mixing in upstream planning or private project context.
|
||||
|
||||
## What to build first
|
||||
|
||||
1. Host↔Teensy protocol and ACK/NACK behavior.
|
||||
2. State machine and illegal-transition tests.
|
||||
3. KWR75B-CAN receive path and F/T validity/stale-frame watchdog.
|
||||
4. Manual mode with geometric gate.
|
||||
5. Telemetry CSV and manual-session log.
|
||||
6. Safety interlock behavior.
|
||||
|
||||
## Key instruction
|
||||
|
||||
Manual mode is not a throwaway mode. Manual telemetry is the calibration dataset for the entire future digital-twin loop. Keep channel names, timestamps, setpoint history, and force/angle data stable from the start.
|
||||
|
||||
## Acceptance checks
|
||||
|
||||
- [ ] Normand can run manual mode safely.
|
||||
- [ ] Every session produces telemetry and a manual-session log.
|
||||
- [ ] Every fault or rejected command is visible as an event/alarm.
|
||||
- [ ] The controller never invents polishing strategy.
|
||||
15
docs/nick-generated/README.md
Normal file
15
docs/nick-generated/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Nick-Generated Notes
|
||||
|
||||
This folder is for source-grounded technical notes Nick generates for Cédric.
|
||||
|
||||
Rules:
|
||||
- technical-only;
|
||||
- no invoices/rates/private context/secrets;
|
||||
- mark generated notes `source_truth: false`;
|
||||
- if a note changes architecture, safety, telemetry contracts, protocol semantics, or v1 scope, ask Antoine before treating it as accepted.
|
||||
|
||||
Suggested filename:
|
||||
|
||||
```text
|
||||
YYYY-MM-DD-short-topic.md
|
||||
```
|
||||
17
docs/reference/source-map.md
Normal file
17
docs/reference/source-map.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Source Map
|
||||
|
||||
This repo foundation is derived from the current P11 / Fullum polisher-control planning context.
|
||||
|
||||
Primary source documents:
|
||||
|
||||
- Fullum Polisher — Machine Control & Firmware Specification v1
|
||||
- Controller / Bridge / Digital Twin Architecture Plan
|
||||
- polisher-control — System Definition
|
||||
- polisher-control — Roadmap
|
||||
- Polisher Software Suite index and shared schema bundle
|
||||
|
||||
Source-truth rule:
|
||||
|
||||
- These repo docs are implementation aids.
|
||||
- Approved P11 source specs and schemas outrank generated summaries.
|
||||
- If anything here conflicts with a source spec, ask Antoine before changing technical direction.
|
||||
12
firmware/README.md
Normal file
12
firmware/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Firmware
|
||||
|
||||
Teensy 4.x firmware owns the fast loop:
|
||||
|
||||
- sensor acquisition;
|
||||
- force PID;
|
||||
- command output;
|
||||
- fast safety/interlocks;
|
||||
- host heartbeat supervision;
|
||||
- telemetry/event emission.
|
||||
|
||||
Use `firmware/teensy/` as the first implementation target.
|
||||
17
firmware/teensy/README.md
Normal file
17
firmware/teensy/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Teensy Firmware Scaffold
|
||||
|
||||
Suggested starting point: Teensy 4.1 using PlatformIO/Arduino framework.
|
||||
|
||||
Core modules to implement:
|
||||
|
||||
- protocol framing + CRC;
|
||||
- message decode/encode;
|
||||
- state machine mirror / safety state;
|
||||
- KWR75B-CAN decoder via isolated CAN transceiver;
|
||||
- table and arm encoder readers;
|
||||
- force PID and actuator output;
|
||||
- ODrive command/telemetry path;
|
||||
- telemetry packet builder;
|
||||
- fast interlocks and watchdogs.
|
||||
|
||||
The included `src/main.cpp` is intentionally minimal. Replace with real modules as hardware decisions close.
|
||||
31
firmware/teensy/include/polisher_protocol.h
Normal file
31
firmware/teensy/include/polisher_protocol.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
HEARTBEAT = 1,
|
||||
MANUAL_START = 2,
|
||||
SETPOINT = 3,
|
||||
MANUAL_STOP = 4,
|
||||
SEGMENT_START = 5,
|
||||
PAUSE = 6,
|
||||
RESUME = 7,
|
||||
ABORT = 8,
|
||||
ESTOP = 9,
|
||||
ACK = 100,
|
||||
NACK = 101,
|
||||
TELEMETRY = 102,
|
||||
EVENT = 103,
|
||||
SEGMENT_DONE = 104,
|
||||
ABORT_COMPLETE = 105,
|
||||
};
|
||||
|
||||
enum class NackReason : uint16_t {
|
||||
NONE = 0,
|
||||
BAD_CRC = 1,
|
||||
BAD_VERSION = 2,
|
||||
ILLEGAL_TRANSITION = 3,
|
||||
GEOMETRY_NOT_VALIDATED = 4,
|
||||
SAFETY_INTERLOCK_ACTIVE = 5,
|
||||
UNSUPPORTED_COMMAND = 6,
|
||||
VALUE_OUT_OF_RANGE = 7,
|
||||
};
|
||||
28
firmware/teensy/include/polisher_telemetry.h
Normal file
28
firmware/teensy/include/polisher_telemetry.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
struct FT_LoadSample {
|
||||
float Fx;
|
||||
float Fy;
|
||||
float Fz;
|
||||
float Mx;
|
||||
float My;
|
||||
float Mz;
|
||||
uint32_t status;
|
||||
};
|
||||
|
||||
struct TelemetrySample {
|
||||
uint64_t timestamp_us;
|
||||
float table_angle_deg;
|
||||
float arm_angle_deg;
|
||||
float fz_n;
|
||||
float mx;
|
||||
float my;
|
||||
float mz;
|
||||
float spindle_rpm_actual;
|
||||
float table_rpm_actual;
|
||||
float arm_amplitude_deg_derived;
|
||||
float arm_center_deg_derived;
|
||||
uint8_t machine_state;
|
||||
float force_setpoint_n;
|
||||
};
|
||||
9
firmware/teensy/platformio.ini
Normal file
9
firmware/teensy/platformio.ini
Normal file
@@ -0,0 +1,9 @@
|
||||
[env:teensy41]
|
||||
platform = teensy
|
||||
board = teensy41
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
build_flags =
|
||||
-D POLISHER_CONTROL_PROTOCOL_VERSION=1
|
||||
lib_deps =
|
||||
tonton81/FlexCAN_T4
|
||||
30
firmware/teensy/src/main.cpp
Normal file
30
firmware/teensy/src/main.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include <Arduino.h>
|
||||
#include "polisher_protocol.h"
|
||||
#include "polisher_telemetry.h"
|
||||
|
||||
static constexpr uint32_t HEARTBEAT_LED_MS = 500;
|
||||
uint32_t lastLedToggleMs = 0;
|
||||
bool ledState = false;
|
||||
|
||||
void setup() {
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
Serial.begin(115200);
|
||||
// TODO: initialize protocol parser, CAN, encoders, drive I/O, watchdogs.
|
||||
}
|
||||
|
||||
void loop() {
|
||||
const uint32_t now = millis();
|
||||
if (now - lastLedToggleMs >= HEARTBEAT_LED_MS) {
|
||||
lastLedToggleMs = now;
|
||||
ledState = !ledState;
|
||||
digitalWrite(LED_BUILTIN, ledState ? HIGH : LOW);
|
||||
}
|
||||
|
||||
// TODO v1 loop order:
|
||||
// 1. Read host frames and validate CRC/version.
|
||||
// 2. Read KWR75B-CAN and encoder inputs.
|
||||
// 3. Update state/safety/watchdogs.
|
||||
// 4. Compute setpoint ramp and force PID.
|
||||
// 5. Apply actuator/drive outputs.
|
||||
// 6. Emit telemetry >=100 Hz and events on transitions/faults.
|
||||
}
|
||||
14
host/README.md
Normal file
14
host/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Host Controller
|
||||
|
||||
Python scaffold for the Raspberry Pi / host-side controller.
|
||||
|
||||
Host responsibilities:
|
||||
- state machine orchestration;
|
||||
- geometric gate;
|
||||
- manual-mode UI integration;
|
||||
- serial/protocol session with Teensy;
|
||||
- controller-job validation;
|
||||
- telemetry capture;
|
||||
- logs, manifests, hashes, and `/data/status.json`.
|
||||
|
||||
This package is intentionally small for now. Add real hardware adapters behind interfaces and keep state-machine/contract tests fast.
|
||||
3
host/polisher_control/__init__.py
Normal file
3
host/polisher_control/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Host-side scaffold for Fullum polisher-control."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
0
host/polisher_control/adapters/.gitkeep
Normal file
0
host/polisher_control/adapters/.gitkeep
Normal file
6
host/polisher_control/contracts.py
Normal file
6
host/polisher_control/contracts.py
Normal file
@@ -0,0 +1,6 @@
|
||||
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/"
|
||||
17
host/polisher_control/data_layout.py
Normal file
17
host/polisher_control/data_layout.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def manual_session_dir(data_root: Path, session_id: str) -> Path:
|
||||
return data_root / "manual" / session_id
|
||||
|
||||
|
||||
def run_dir(data_root: Path, run_id: str) -> Path:
|
||||
return data_root / "runs" / run_id
|
||||
|
||||
|
||||
def expected_manual_files(session_id: str) -> list[str]:
|
||||
return [
|
||||
"manual-session-log.v1.json",
|
||||
"telemetry.csv",
|
||||
"manifest.json",
|
||||
]
|
||||
30
host/polisher_control/protocol.py
Normal file
30
host/polisher_control/protocol.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class MessageType(IntEnum):
|
||||
HEARTBEAT = 1
|
||||
MANUAL_START = 2
|
||||
SETPOINT = 3
|
||||
MANUAL_STOP = 4
|
||||
SEGMENT_START = 5
|
||||
PAUSE = 6
|
||||
RESUME = 7
|
||||
ABORT = 8
|
||||
ESTOP = 9
|
||||
ACK = 100
|
||||
NACK = 101
|
||||
TELEMETRY = 102
|
||||
EVENT = 103
|
||||
SEGMENT_DONE = 104
|
||||
ABORT_COMPLETE = 105
|
||||
|
||||
|
||||
class NackReason(IntEnum):
|
||||
NONE = 0
|
||||
BAD_CRC = 1
|
||||
BAD_VERSION = 2
|
||||
ILLEGAL_TRANSITION = 3
|
||||
GEOMETRY_NOT_VALIDATED = 4
|
||||
SAFETY_INTERLOCK_ACTIVE = 5
|
||||
UNSUPPORTED_COMMAND = 6
|
||||
VALUE_OUT_OF_RANGE = 7
|
||||
65
host/polisher_control/state_machine.py
Normal file
65
host/polisher_control/state_machine.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class MachineState(StrEnum):
|
||||
IDLE = "IDLE"
|
||||
JOB_LOADED = "JOB_LOADED"
|
||||
READY = "READY"
|
||||
RUNNING = "RUNNING"
|
||||
PAUSED = "PAUSED"
|
||||
ABORTING = "ABORTING"
|
||||
COMPLETED = "COMPLETED"
|
||||
ABORTED = "ABORTED"
|
||||
FAULTED = "FAULTED"
|
||||
MANUAL = "MANUAL"
|
||||
|
||||
|
||||
TRANSITIONS: dict[tuple[MachineState, str], MachineState] = {
|
||||
(MachineState.IDLE, "load_job"): MachineState.JOB_LOADED,
|
||||
(MachineState.IDLE, "enter_manual"): MachineState.MANUAL,
|
||||
(MachineState.JOB_LOADED, "operator_acknowledge"): MachineState.READY,
|
||||
(MachineState.JOB_LOADED, "unload_job"): MachineState.IDLE,
|
||||
(MachineState.READY, "start"): MachineState.RUNNING,
|
||||
(MachineState.RUNNING, "pause"): MachineState.PAUSED,
|
||||
(MachineState.RUNNING, "segment_complete"): MachineState.RUNNING,
|
||||
(MachineState.RUNNING, "all_segments_complete"): MachineState.COMPLETED,
|
||||
(MachineState.RUNNING, "abort"): MachineState.ABORTING,
|
||||
(MachineState.RUNNING, "fault"): MachineState.FAULTED,
|
||||
(MachineState.PAUSED, "resume"): MachineState.RUNNING,
|
||||
(MachineState.PAUSED, "abort"): MachineState.ABORTING,
|
||||
(MachineState.MANUAL, "exit_manual"): MachineState.IDLE,
|
||||
(MachineState.MANUAL, "fault"): MachineState.FAULTED,
|
||||
(MachineState.ABORTING, "abort_complete"): MachineState.ABORTED,
|
||||
(MachineState.COMPLETED, "reset"): MachineState.IDLE,
|
||||
(MachineState.ABORTED, "reset"): MachineState.IDLE,
|
||||
(MachineState.FAULTED, "reset"): MachineState.IDLE,
|
||||
}
|
||||
|
||||
|
||||
class IllegalTransitionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransitionEvent:
|
||||
from_state: MachineState
|
||||
trigger: str
|
||||
to_state: MachineState
|
||||
|
||||
|
||||
class StateMachine:
|
||||
def __init__(self, initial: MachineState = MachineState.IDLE) -> None:
|
||||
self.state = initial
|
||||
self.events: list[TransitionEvent] = []
|
||||
|
||||
def trigger(self, name: str) -> MachineState:
|
||||
key = (self.state, name)
|
||||
if key not in TRANSITIONS:
|
||||
raise IllegalTransitionError(f"Illegal transition: {self.state} + {name}")
|
||||
previous = self.state
|
||||
self.state = TRANSITIONS[key]
|
||||
self.events.append(TransitionEvent(previous, name, self.state))
|
||||
return self.state
|
||||
39
host/polisher_control/telemetry_channels.py
Normal file
39
host/polisher_control/telemetry_channels.py
Normal file
@@ -0,0 +1,39 @@
|
||||
CORE_CHANNELS = [
|
||||
"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",
|
||||
"force_setpoint_n",
|
||||
]
|
||||
|
||||
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",
|
||||
"table_rpm_setpoint",
|
||||
"spindle_rpm_setpoint",
|
||||
"force_actuator_cmd",
|
||||
"estop_active",
|
||||
"interlock_state",
|
||||
"mode",
|
||||
"fz_raw_n",
|
||||
"fz_contact_n",
|
||||
]
|
||||
|
||||
CSV_HEADER = CORE_CHANNELS
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "polisher-control-host"
|
||||
version = "0.1.0"
|
||||
description = "Host-side scaffold for Fullum polisher-control"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8", "jsonschema>=4"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["host"]
|
||||
11
scripts/print_tree.py
Normal file
11
scripts/print_tree.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for path in sorted(ROOT.rglob("*")):
|
||||
if ".git" in path.parts or "__pycache__" in path.parts:
|
||||
continue
|
||||
rel = path.relative_to(ROOT)
|
||||
if len(rel.parts) > 4:
|
||||
continue
|
||||
print(rel)
|
||||
27
scripts/validate_json.py
Normal file
27
scripts/validate_json.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
errors = 0
|
||||
for path in sorted(ROOT.rglob("*.json")):
|
||||
if ".git" in path.parts:
|
||||
continue
|
||||
try:
|
||||
json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception as exc:
|
||||
print(f"JSON ERROR {path.relative_to(ROOT)}: {exc}")
|
||||
errors += 1
|
||||
if errors:
|
||||
return 1
|
||||
print("OK: all JSON files parse")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
130
shared/machine/fullum-alpha.capabilities.v1.json
Normal file
130
shared/machine/fullum-alpha.capabilities.v1.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"schema_version": "machine-capabilities.v1",
|
||||
"machine_id": "fullum-alpha",
|
||||
"machine_family": "swing-arm",
|
||||
"machine_name": "Fullum swing-arm polisher",
|
||||
"controller_version": "polisher-control/0.1.0",
|
||||
"supported_motion_families": [
|
||||
"swing-arm-rosette"
|
||||
],
|
||||
"force_range_n": [
|
||||
0,
|
||||
80
|
||||
],
|
||||
"table_rpm_range": [
|
||||
0,
|
||||
5.0
|
||||
],
|
||||
"spindle_rpm_range": [
|
||||
0,
|
||||
80
|
||||
],
|
||||
"cam_amplitude_range_deg": [
|
||||
0,
|
||||
45
|
||||
],
|
||||
"cam_offset_range_deg": [
|
||||
-30,
|
||||
30
|
||||
],
|
||||
"force_modulation": {
|
||||
"supported": true,
|
||||
"max_harmonics": 3,
|
||||
"max_amplitude_n": 20.0,
|
||||
"notes": "Angle-synchronized force modulation from live table encoder angle; validate during commissioning."
|
||||
},
|
||||
"supported_dither_profiles": [
|
||||
"none",
|
||||
"default"
|
||||
],
|
||||
"segment_duration_limits": {
|
||||
"min_s": 10,
|
||||
"max_s": 7200
|
||||
},
|
||||
"pause_resume_support": true,
|
||||
"telemetry_channels": [
|
||||
{
|
||||
"name": "timestamp_us",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "table_angle_deg",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "arm_angle_deg",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "fz_n",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "mx",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "my",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "mz",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "spindle_rpm_actual",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "table_rpm_actual",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "arm_amplitude_deg_derived",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "arm_center_deg_derived",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "machine_state",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
},
|
||||
{
|
||||
"name": "force_setpoint_n",
|
||||
"unit": "see docs/05-telemetry-channel-spec-v1.md",
|
||||
"sample_rate_hz": 100
|
||||
}
|
||||
],
|
||||
"safety_limits": {
|
||||
"max_force_n": 80,
|
||||
"max_table_rpm": 5.0,
|
||||
"max_spindle_rpm": 80,
|
||||
"notes": "Initial source-aligned limits; final hardware commissioning must verify."
|
||||
},
|
||||
"known_constraints": [
|
||||
"Manual mode is the first production target.",
|
||||
"No powered Zero-G/admittance mode in v1.",
|
||||
"Oscillation amplitude/center are operator-entered configuration in v1, not actuated cam commands.",
|
||||
"Controller rejects unknown controller-job fields rather than silently ignoring them."
|
||||
],
|
||||
"unknowns": [
|
||||
"Exact KWR75B-CAN frame map, scaling, status bits, and update rate.",
|
||||
"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."
|
||||
]
|
||||
}
|
||||
0
shared/protocol/.gitkeep
Normal file
0
shared/protocol/.gitkeep
Normal file
11
shared/schemas/README.md
Normal file
11
shared/schemas/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Shared Schemas
|
||||
|
||||
Mirrored from the current Polisher Software Suite schema bundle for implementation alignment.
|
||||
|
||||
Important:
|
||||
- `controller-job.v1` is what this controller accepts from `polisher-post`.
|
||||
- `job.v1` is upstream planning input and must be rejected by `polisher-control`.
|
||||
- `run-log.v1` is emitted by `polisher-control` after program execution.
|
||||
- `manual-session-log.v1` is currently documented in repo docs/spec; formal schema can be added when Antoine locks it.
|
||||
|
||||
Do not rename fields locally without coordinating with Antoine because downstream tools depend on stable names.
|
||||
117
shared/schemas/controller-job.schema.json
Normal file
117
shared/schemas/controller-job.schema.json
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://polisher-suite.local/schemas/controller-job.schema.json",
|
||||
"title": "Controller Job Package",
|
||||
"description": "Translated execution package produced by polisher-post for polisher-control. Represents translated truth.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"controller_job_id",
|
||||
"job_id",
|
||||
"machine_id",
|
||||
"machine_capabilities_ref",
|
||||
"controller_version",
|
||||
"translated_at",
|
||||
"translated_by",
|
||||
"segments",
|
||||
"translation_losses"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "controller-job.v1"
|
||||
},
|
||||
"controller_job_id": {
|
||||
"type": "string",
|
||||
"description": "Stable unique identifier for this translated package."
|
||||
},
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "Back-reference to the source planning job."
|
||||
},
|
||||
"machine_id": {
|
||||
"type": "string",
|
||||
"description": "Target machine identifier."
|
||||
},
|
||||
"machine_capabilities_ref": {
|
||||
"type": "string",
|
||||
"description": "Reference to the machine-capabilities document used for translation."
|
||||
},
|
||||
"controller_version": {
|
||||
"type": "string",
|
||||
"description": "Target controller software version."
|
||||
},
|
||||
"translated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"translated_by": {
|
||||
"type": "string",
|
||||
"description": "polisher-post version that performed the translation."
|
||||
},
|
||||
"translation_notes": {
|
||||
"type": "string",
|
||||
"description": "Summary of constraints applied during translation."
|
||||
},
|
||||
"segments": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/segment"
|
||||
}
|
||||
},
|
||||
"translation_losses": {
|
||||
"type": "array",
|
||||
"description": "Explicit record of every constraint, clip, or approximation applied.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["source_pass_id", "field", "description"],
|
||||
"properties": {
|
||||
"source_pass_id": { "type": "string" },
|
||||
"field": { "type": "string" },
|
||||
"planned_value": {},
|
||||
"translated_value": {},
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"segment": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"segment_id",
|
||||
"sequence_index",
|
||||
"source_pass_id",
|
||||
"duration_s",
|
||||
"commanded_force_n",
|
||||
"commanded_table_rpm",
|
||||
"commanded_spindle_rpm"
|
||||
],
|
||||
"properties": {
|
||||
"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" },
|
||||
"force_modulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"harmonic": { "type": "integer" },
|
||||
"amplitude_n": { "type": "number" },
|
||||
"phase_deg": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"dither_profile": { "type": "string" },
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
shared/schemas/examples/controller-job.example.json
Normal file
32
shared/schemas/examples/controller-job.example.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schema_version": "controller-job.v1",
|
||||
"controller_job_id": "cjob-2026-07-001",
|
||||
"job_id": "job-2026-07-001",
|
||||
"machine_id": "fullum-alpha",
|
||||
"machine_capabilities_ref": "machine-capabilities-fullum-alpha-v1",
|
||||
"controller_version": "polisher-control/0.1.0",
|
||||
"translated_at": "2026-07-15T10:15:00Z",
|
||||
"translated_by": "polisher-post/0.1.0",
|
||||
"translation_notes": "All setpoints within machine limits. No clipping required.",
|
||||
"segments": [
|
||||
{
|
||||
"segment_id": "seg-01",
|
||||
"sequence_index": 1,
|
||||
"source_pass_id": "pass-01",
|
||||
"duration_s": 1200,
|
||||
"commanded_force_n": 45.0,
|
||||
"commanded_table_rpm": 4.0,
|
||||
"commanded_spindle_rpm": 40.0,
|
||||
"commanded_cam_amplitude_deg": 31.3,
|
||||
"commanded_cam_offset_deg": 0.0,
|
||||
"force_modulation": {
|
||||
"harmonic": 2,
|
||||
"amplitude_n": 5.0,
|
||||
"phase_deg": 37.2
|
||||
},
|
||||
"dither_profile": "none",
|
||||
"notes": "Direct translation — no segmentation needed for 1200s duration."
|
||||
}
|
||||
],
|
||||
"translation_losses": []
|
||||
}
|
||||
71
shared/schemas/examples/job.example.json
Normal file
71
shared/schemas/examples/job.example.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"schema_version": "job.v1",
|
||||
"job_id": "job-2026-07-001",
|
||||
"mirror_id": "gigabit-m1",
|
||||
"machine_family": "swing-arm",
|
||||
"source_map_id": "ifg-2026-07-14-pre",
|
||||
"target_id": "target-m1-final-figure",
|
||||
"created_at": "2026-07-15T09:30:00Z",
|
||||
"created_by": "antoine",
|
||||
"planner_version": "polisher-sim/0.6.0",
|
||||
"calibration_state_ref": "cal-2026-06-fullum-alpha",
|
||||
"strategy_summary": {
|
||||
"intent": "Reduce dominant astigmatism using m=2 force modulation on 280mm tool",
|
||||
"confidence": 0.72,
|
||||
"notes": "Single-pass conservative correction. Dither disabled for baseline comparison."
|
||||
},
|
||||
"passes": [
|
||||
{
|
||||
"pass_id": "pass-01",
|
||||
"sequence_index": 1,
|
||||
"objective": "Astigmatism reduction via force modulation",
|
||||
"preset_name": "figuring_standard",
|
||||
"duration_s": 1200,
|
||||
"force_n": 45.0,
|
||||
"table_rpm": 4.0,
|
||||
"spindle_rpm": 40.0,
|
||||
"motion_family": "swing-arm-rosette",
|
||||
"motion_params": {
|
||||
"cam_amplitude_deg": 31.3,
|
||||
"cam_offset_deg": 0.0,
|
||||
"osc_hz": 0.25
|
||||
},
|
||||
"dither_profile": "none",
|
||||
"force_modulation": {
|
||||
"harmonic": 2,
|
||||
"amplitude_n": 5.0,
|
||||
"phase_deg": 37.2
|
||||
},
|
||||
"notes": "Phase derived from Zernike fit of pre-polish interferogram."
|
||||
}
|
||||
],
|
||||
"predicted_outcome": {
|
||||
"removal_rms_nm": 18.5,
|
||||
"removal_pv_nm": 62.0,
|
||||
"residual_zernikes": {
|
||||
"Z4": -2.1,
|
||||
"Z5": 12.3,
|
||||
"Z6": -8.7,
|
||||
"Z7": 15.1,
|
||||
"Z8": -3.2,
|
||||
"Z9": 4.5,
|
||||
"Z10": -2.8
|
||||
}
|
||||
},
|
||||
"uncertainty": {
|
||||
"removal_rms_nm_range": [14.0, 24.0],
|
||||
"notes": "Uncertainty dominated by Preston coefficient calibration spread."
|
||||
},
|
||||
"attachments": [
|
||||
{
|
||||
"filename": "predicted_removal.csv",
|
||||
"role": "predicted_removal_map",
|
||||
"description": "2D removal map in nm, 256x256 grid"
|
||||
},
|
||||
{
|
||||
"filename": "pre_surface.csv",
|
||||
"role": "input_surface_map",
|
||||
"description": "Pre-polish surface map used as planning input"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
shared/schemas/examples/machine-capabilities.example.json
Normal file
51
shared/schemas/examples/machine-capabilities.example.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schema_version": "machine-capabilities.v1",
|
||||
"machine_id": "fullum-alpha",
|
||||
"machine_family": "swing-arm",
|
||||
"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],
|
||||
"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"],
|
||||
"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 }
|
||||
],
|
||||
"safety_limits": {
|
||||
"max_force_n": 200,
|
||||
"max_table_rpm": 10.0,
|
||||
"max_spindle_rpm": 120,
|
||||
"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.",
|
||||
"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.",
|
||||
"Thermal drift effect on force sensor over runs longer than 1 hour.",
|
||||
"Arm pivot play magnitude under varying force loads."
|
||||
]
|
||||
}
|
||||
43
shared/schemas/examples/manifest.example.json
Normal file
43
shared/schemas/examples/manifest.example.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"schema_version": "manifest.v1",
|
||||
"manifest_id": "manifest-job-2026-07-001",
|
||||
"bundle_type": "job",
|
||||
"source_id": "job-2026-07-001",
|
||||
"created_at": "2026-07-15T09:31:00Z",
|
||||
"created_by": "polisher-sim/0.6.0",
|
||||
"entries": [
|
||||
{
|
||||
"filename": "job.json",
|
||||
"role": "primary",
|
||||
"hash_sha256": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
|
||||
"size_bytes": 2007,
|
||||
"content_type": "application/json",
|
||||
"description": "Frozen planning job package."
|
||||
},
|
||||
{
|
||||
"filename": "predicted_removal.csv",
|
||||
"role": "attachment",
|
||||
"hash_sha256": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3",
|
||||
"size_bytes": 524288,
|
||||
"content_type": "text/csv",
|
||||
"description": "Predicted removal map, 256x256 grid, nm."
|
||||
},
|
||||
{
|
||||
"filename": "pre_surface.csv",
|
||||
"role": "attachment",
|
||||
"hash_sha256": "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
|
||||
"size_bytes": 524288,
|
||||
"content_type": "text/csv",
|
||||
"description": "Pre-polish surface map used as planning input."
|
||||
},
|
||||
{
|
||||
"filename": "manifest.json",
|
||||
"role": "manifest",
|
||||
"hash_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"size_bytes": 0,
|
||||
"content_type": "application/json",
|
||||
"description": "This manifest (self-reference, hash zeroed by convention)."
|
||||
}
|
||||
],
|
||||
"notes": "July proving scenario — single-pass astigmatism correction job bundle."
|
||||
}
|
||||
64
shared/schemas/examples/run-log.example.json
Normal file
64
shared/schemas/examples/run-log.example.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"schema_version": "run-log.v1",
|
||||
"run_id": "run-2026-07-001",
|
||||
"job_id": "job-2026-07-001",
|
||||
"controller_job_id": "cjob-2026-07-001",
|
||||
"controller_version": "polisher-control/0.1.0",
|
||||
"machine_id": "fullum-alpha",
|
||||
"started_at": "2026-07-17T14:00:00Z",
|
||||
"ended_at": "2026-07-17T14:20:35Z",
|
||||
"result_state": "completed",
|
||||
"segments": [
|
||||
{
|
||||
"segment_id": "seg-01",
|
||||
"source_pass_id": "pass-01",
|
||||
"commanded_duration_s": 1200,
|
||||
"actual_duration_s": 1203.5,
|
||||
"result_state": "completed",
|
||||
"commanded": {
|
||||
"force_n": 45.0,
|
||||
"table_rpm": 4.0,
|
||||
"spindle_rpm": 40.0,
|
||||
"cam_amplitude_deg": 31.3,
|
||||
"cam_offset_deg": 0.0
|
||||
},
|
||||
"actual": {
|
||||
"force_n_mean": 44.7,
|
||||
"force_n_min": 41.2,
|
||||
"force_n_max": 49.8,
|
||||
"table_rpm_mean": 3.98,
|
||||
"spindle_rpm_mean": 39.9
|
||||
},
|
||||
"pause_windows": [],
|
||||
"anomaly_flags": [],
|
||||
"notes": "Clean run. Minor force excursion at arm reversal points."
|
||||
}
|
||||
],
|
||||
"commanded_summary": {
|
||||
"force_n": 45.0,
|
||||
"table_rpm": 4.0,
|
||||
"spindle_rpm": 40.0
|
||||
},
|
||||
"actual_summary": {
|
||||
"force_n_mean": 44.7,
|
||||
"force_n_min": 41.2,
|
||||
"force_n_max": 49.8,
|
||||
"table_rpm_mean": 3.98,
|
||||
"spindle_rpm_mean": 39.9
|
||||
},
|
||||
"alarms": [],
|
||||
"events": [
|
||||
{
|
||||
"timestamp": "2026-07-17T14:00:00Z",
|
||||
"type": "run_started",
|
||||
"detail": "Operator acknowledged start."
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-07-17T14:20:35Z",
|
||||
"type": "run_completed",
|
||||
"detail": "All segments completed normally."
|
||||
}
|
||||
],
|
||||
"telemetry_ref": "telemetry-run-2026-07-001.csv",
|
||||
"operator_notes": "Smooth run. No intervention needed."
|
||||
}
|
||||
158
shared/schemas/job.schema.json
Normal file
158
shared/schemas/job.schema.json
Normal file
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://polisher-suite.local/schemas/job.schema.json",
|
||||
"title": "Polisher Job Package",
|
||||
"description": "Frozen planning artifact emitted by polisher-sim. Represents planning truth.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"job_id",
|
||||
"mirror_id",
|
||||
"machine_family",
|
||||
"planner_version",
|
||||
"created_at",
|
||||
"strategy_summary",
|
||||
"passes"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "job.v1"
|
||||
},
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "Stable unique identifier for this job."
|
||||
},
|
||||
"mirror_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"machine_family": {
|
||||
"type": "string",
|
||||
"description": "Target machine family (e.g. swing-arm)."
|
||||
},
|
||||
"source_map_id": {
|
||||
"type": "string",
|
||||
"description": "Reference to the input surface measurement."
|
||||
},
|
||||
"target_id": {
|
||||
"type": "string",
|
||||
"description": "Reference to the target surface specification."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "string"
|
||||
},
|
||||
"planner_version": {
|
||||
"type": "string",
|
||||
"description": "Software version of polisher-sim that generated this job."
|
||||
},
|
||||
"calibration_state_ref": {
|
||||
"type": "string",
|
||||
"description": "Reference to the calibration state used during planning."
|
||||
},
|
||||
"strategy_summary": {
|
||||
"type": "object",
|
||||
"required": ["intent"],
|
||||
"properties": {
|
||||
"intent": { "type": "string" },
|
||||
"confidence": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"passes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/pass"
|
||||
}
|
||||
},
|
||||
"predicted_outcome": {
|
||||
"type": "object",
|
||||
"description": "Summary of predicted post-polish surface state.",
|
||||
"properties": {
|
||||
"removal_rms_nm": { "type": "number" },
|
||||
"removal_pv_nm": { "type": "number" },
|
||||
"residual_zernikes": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "number" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"uncertainty": {
|
||||
"type": "object",
|
||||
"description": "Planner's uncertainty estimates.",
|
||||
"properties": {
|
||||
"removal_rms_nm_range": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["filename", "role"],
|
||||
"properties": {
|
||||
"filename": { "type": "string" },
|
||||
"role": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"pass": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pass_id",
|
||||
"sequence_index",
|
||||
"duration_s",
|
||||
"force_n",
|
||||
"table_rpm",
|
||||
"spindle_rpm",
|
||||
"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" },
|
||||
"motion_params": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"zone": { "type": "string" },
|
||||
"dither_profile": { "type": "string" },
|
||||
"force_modulation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"harmonic": { "type": "integer" },
|
||||
"amplitude_n": { "type": "number" },
|
||||
"phase_deg": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"acceptance": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
shared/schemas/machine-capabilities.schema.json
Normal file
135
shared/schemas/machine-capabilities.schema.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://polisher-suite.local/schemas/machine-capabilities.schema.json",
|
||||
"title": "Machine Capabilities",
|
||||
"description": "Conservative declaration of what a machine+controller can safely execute. Unknown capabilities must be explicitly marked rather than assumed.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"machine_id",
|
||||
"machine_family",
|
||||
"controller_version",
|
||||
"supported_motion_families",
|
||||
"force_range_n",
|
||||
"table_rpm_range",
|
||||
"spindle_rpm_range"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "machine-capabilities.v1"
|
||||
},
|
||||
"machine_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"machine_family": {
|
||||
"type": "string"
|
||||
},
|
||||
"machine_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"controller_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_verified": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When these capabilities were last confirmed on the real machine."
|
||||
},
|
||||
"supported_motion_families": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1
|
||||
},
|
||||
"force_range_n": {
|
||||
"$ref": "#/$defs/range_or_unknown"
|
||||
},
|
||||
"table_rpm_range": {
|
||||
"$ref": "#/$defs/range_or_unknown"
|
||||
},
|
||||
"spindle_rpm_range": {
|
||||
"$ref": "#/$defs/range_or_unknown"
|
||||
},
|
||||
"cam_amplitude_range_deg": {
|
||||
"$ref": "#/$defs/range_or_unknown"
|
||||
},
|
||||
"cam_offset_range_deg": {
|
||||
"$ref": "#/$defs/range_or_unknown"
|
||||
},
|
||||
"force_modulation": {
|
||||
"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_dither_profiles": {
|
||||
"type": "array",
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"pause_resume_support": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"telemetry_channels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "unit"],
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"unit": { "type": "string" },
|
||||
"sample_rate_hz": { "type": "number" },
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"safety_limits": {
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"known_constraints": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Free-text list of known limitations, quirks, or warnings."
|
||||
},
|
||||
"unknowns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Capabilities that have NOT been verified. Explicit unknowns prevent fake certainty."
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"range_or_unknown": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"description": "[min, max] range."
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"const": "unknown"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
76
shared/schemas/manifest.schema.json
Normal file
76
shared/schemas/manifest.schema.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://polisher-suite.local/schemas/manifest.schema.json",
|
||||
"title": "Artifact Bundle Manifest",
|
||||
"description": "Manifest for a portable artifact bundle. Lists all files with hashes for integrity verification.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"manifest_id",
|
||||
"bundle_type",
|
||||
"created_at",
|
||||
"entries"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "manifest.v1"
|
||||
},
|
||||
"manifest_id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for this manifest."
|
||||
},
|
||||
"bundle_type": {
|
||||
"type": "string",
|
||||
"enum": ["job", "controller-job", "run-log"],
|
||||
"description": "What kind of artifact bundle this manifest describes."
|
||||
},
|
||||
"source_id": {
|
||||
"type": "string",
|
||||
"description": "The primary artifact ID (job_id, controller_job_id, or run_id)."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "string",
|
||||
"description": "Software/version that created this bundle."
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["filename", "role", "hash_sha256"],
|
||||
"properties": {
|
||||
"filename": {
|
||||
"type": "string",
|
||||
"description": "Relative path within the bundle."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"description": "Semantic role: primary, attachment, telemetry, report, etc."
|
||||
},
|
||||
"hash_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"size_bytes": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"content_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
171
shared/schemas/run-log.schema.json
Normal file
171
shared/schemas/run-log.schema.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://polisher-suite.local/schemas/run-log.schema.json",
|
||||
"title": "Run Log",
|
||||
"description": "Canonical execution record emitted by polisher-control. Represents executed truth.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"schema_version",
|
||||
"run_id",
|
||||
"job_id",
|
||||
"controller_job_id",
|
||||
"controller_version",
|
||||
"machine_id",
|
||||
"started_at",
|
||||
"ended_at",
|
||||
"result_state",
|
||||
"segments"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"type": "string",
|
||||
"const": "run-log.v1"
|
||||
},
|
||||
"run_id": {
|
||||
"type": "string",
|
||||
"description": "Stable unique identifier for this execution run."
|
||||
},
|
||||
"job_id": {
|
||||
"type": "string",
|
||||
"description": "Back-reference to the source planning job."
|
||||
},
|
||||
"controller_job_id": {
|
||||
"type": "string",
|
||||
"description": "Back-reference to the translated controller-job package."
|
||||
},
|
||||
"controller_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"machine_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"result_state": {
|
||||
"type": "string",
|
||||
"enum": ["completed", "completed_with_pause", "aborted", "faulted"]
|
||||
},
|
||||
"segments": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/executed_segment"
|
||||
}
|
||||
},
|
||||
"commanded_summary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"force_n": { "type": "number" },
|
||||
"table_rpm": { "type": "number" },
|
||||
"spindle_rpm": { "type": "number" }
|
||||
}
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"alarms": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["timestamp", "code", "message"],
|
||||
"properties": {
|
||||
"timestamp": { "type": "string", "format": "date-time" },
|
||||
"code": { "type": "string" },
|
||||
"message": { "type": "string" },
|
||||
"severity": { "type": "string", "enum": ["info", "warning", "critical"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["timestamp", "type"],
|
||||
"properties": {
|
||||
"timestamp": { "type": "string", "format": "date-time" },
|
||||
"type": { "type": "string" },
|
||||
"detail": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"telemetry_ref": {
|
||||
"type": "string",
|
||||
"description": "Filename or path to the raw telemetry data file."
|
||||
},
|
||||
"operator_notes": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"executed_segment": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"segment_id",
|
||||
"source_pass_id",
|
||||
"commanded_duration_s",
|
||||
"actual_duration_s",
|
||||
"result_state"
|
||||
],
|
||||
"properties": {
|
||||
"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"]
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"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" }
|
||||
}
|
||||
},
|
||||
"pause_windows": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["paused_at", "resumed_at"],
|
||||
"properties": {
|
||||
"paused_at": { "type": "string", "format": "date-time" },
|
||||
"resumed_at": { "type": "string", "format": "date-time" },
|
||||
"reason": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"anomaly_flags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"notes": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/test_contract_constants.py
Normal file
16
tests/test_contract_constants.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from polisher_control.telemetry_channels import CORE_CHANNELS
|
||||
from polisher_control.contracts import MACHINE_ID
|
||||
|
||||
|
||||
def test_machine_id_is_fullum_alpha():
|
||||
assert MACHINE_ID == "fullum-alpha"
|
||||
|
||||
|
||||
def test_core_telemetry_channels_are_stable():
|
||||
assert CORE_CHANNELS[:4] == [
|
||||
"timestamp_us",
|
||||
"table_angle_deg",
|
||||
"arm_angle_deg",
|
||||
"fz_n",
|
||||
]
|
||||
assert "force_setpoint_n" in CORE_CHANNELS
|
||||
26
tests/test_state_machine.py
Normal file
26
tests/test_state_machine.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
|
||||
from polisher_control.state_machine import IllegalTransitionError, MachineState, StateMachine
|
||||
|
||||
|
||||
def test_manual_mode_path_returns_to_idle():
|
||||
sm = StateMachine()
|
||||
assert sm.trigger("enter_manual") == MachineState.MANUAL
|
||||
assert sm.trigger("exit_manual") == MachineState.IDLE
|
||||
|
||||
|
||||
def test_faulted_requires_reset():
|
||||
sm = StateMachine(MachineState.MANUAL)
|
||||
assert sm.trigger("fault") == MachineState.FAULTED
|
||||
with pytest.raises(IllegalTransitionError):
|
||||
sm.trigger("enter_manual")
|
||||
assert sm.trigger("reset") == MachineState.IDLE
|
||||
|
||||
|
||||
def test_operator_acknowledge_required_before_running():
|
||||
sm = StateMachine()
|
||||
sm.trigger("load_job")
|
||||
with pytest.raises(IllegalTransitionError):
|
||||
sm.trigger("start")
|
||||
sm.trigger("operator_acknowledge")
|
||||
assert sm.trigger("start") == MachineState.RUNNING
|
||||
Reference in New Issue
Block a user