docs: scaffold polisher-control foundation
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user