docs: scaffold polisher-control foundation

This commit is contained in:
Nick Hermes
2026-05-26 16:23:04 +00:00
commit fa9c43fae8
52 changed files with 2224 additions and 0 deletions

26
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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/`.

View 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
View 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
View 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.

View 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.

View 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.

View 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.

View 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.

View 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}/`.

View 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.

View 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.

View File

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

View 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
```

View 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
View 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
View 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.

View 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,
};

View 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;
};

View 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

View 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
View 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.

View File

@@ -0,0 +1,3 @@
"""Host-side scaffold for Fullum polisher-control."""
__version__ = "0.1.0"

View File

View 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/"

View 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",
]

View 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

View 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

View 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
View 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
View 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
View 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())

View 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
View File

11
shared/schemas/README.md Normal file
View 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.

View 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" }
}
}
}
}

View 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": []
}

View 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"
}
]
}

View 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."
]
}

View 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."
}

View 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."
}

View 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" }
}
}
}
}

View 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"
}
]
}
}
}

View 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"
}
}
}

View 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" }
}
}
}
}

View 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

View 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