Skip to content

Commit 829c2c7

Browse files
authored
Merge pull request PCrnjak#27 from PCrnjak/feat/mcp-server
Unhomed motion gate, fast-path home, stop/estop/reset
2 parents 650fa08 + 0b2ff61 commit 829c2c7

37 files changed

Lines changed: 619 additions & 122 deletions

parol6/ack_policy.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
# System command types (always require ACK)
66
SYSTEM_CMD_TYPES: set[CmdType] = {
7-
CmdType.RESUME,
8-
CmdType.HALT,
7+
CmdType.RESET,
8+
CmdType.ESTOP,
9+
CmdType.STOP,
910
CmdType.CONNECT_HARDWARE,
1011
CmdType.SIMULATOR,
1112
CmdType.SELECT_PROFILE,
12-
CmdType.RESET,
13+
CmdType.RESET_STATE,
1314
CmdType.WRITE_IO,
1415
CmdType.SET_TCP_OFFSET,
1516
CmdType.SET_SHAPES,

parol6/client/async_client.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
IOResultStruct,
4242
JointSpeedsCmd,
4343
LoopStatsCmd,
44-
HaltCmd,
44+
EstopCmd,
4545
HomeCmd,
4646
JogJCmd,
4747
JogLCmd,
@@ -64,8 +64,9 @@
6464
ReachableCmd,
6565
ResetCmd,
6666
ResetLoopStatsCmd,
67-
ResumeCmd,
67+
ResetStateCmd,
6868
Response,
69+
StopCmd,
6970
ResponseMsg,
7071
SelectProfileCmd,
7172
SelectToolCmd,
@@ -660,6 +661,10 @@ async def home(
660661
) -> int:
661662
"""Home the robot to its home position.
662663
664+
Unhomed, this runs the full referencing sequence (each joint seeks
665+
its limit switch, then moves to standby). Already homed, it returns
666+
to standby with a normal planned, collision-checked joint move.
667+
663668
Returns the command index (≥ 0) on success, -1 on failure.
664669
665670
Category: Motion
@@ -679,31 +684,48 @@ async def home(
679684
raise TimeoutError(f"home() timed out after {timeout}s")
680685
return index
681686

682-
async def resume(self) -> int:
683-
"""Re-enable the robot controller, allowing motion commands.
687+
async def stop(self) -> int:
688+
"""Stop all motion — cancel the active move and clear the queue.
689+
690+
The controller stays enabled and holding position; the next motion
691+
command is accepted immediately.
684692
685693
Category: Control
686694
687695
Example:
688-
rbt.resume()
696+
rbt.stop()
689697
690698
Returns:
691699
1 if acknowledged, 0 on failure.
692700
"""
693-
return await self._send(ResumeCmd())
701+
return await self._send(StopCmd())
694702

695-
async def halt(self) -> int:
696-
"""Halt the robot — stop all motion and disable.
703+
async def estop(self) -> int:
704+
"""Protective stop: stop all motion and latch the controller
705+
disabled until ``reset()``.
697706
698707
Category: Control
699708
700709
Example:
701-
rbt.halt()
710+
rbt.estop()
702711
703712
Returns:
704713
1 if acknowledged, 0 on failure.
705714
"""
706-
return await self._send(HaltCmd())
715+
return await self._send(EstopCmd())
716+
717+
async def reset(self) -> int:
718+
"""Clear a latched protective stop, re-enabling motion.
719+
720+
Category: Control
721+
722+
Example:
723+
rbt.reset()
724+
725+
Returns:
726+
1 if acknowledged, 0 on failure.
727+
"""
728+
return await self._send(ResetCmd())
707729

708730
async def simulator(self, enabled: bool) -> int:
709731
"""Enable or disable simulator mode.
@@ -764,7 +786,7 @@ async def connect_hardware(self, port_str: str) -> int:
764786
raise ValueError("No port provided")
765787
return await self._send(ConnectHardwareCmd(port_str=port_str))
766788

767-
async def reset(self) -> int:
789+
async def reset_state(self) -> int:
768790
"""Reset controller state to initial values.
769791
770792
Instantly resets positions to home, clears queues, resets tool/errors.
@@ -773,9 +795,9 @@ async def reset(self) -> int:
773795
Category: Control
774796
775797
Example:
776-
rbt.reset()
798+
rbt.reset_state()
777799
"""
778-
return await self._send(ResetCmd())
800+
return await self._send(ResetStateCmd())
779801

780802
# --------------- Status / Queries ---------------
781803
async def ping(self) -> PingResult | None:

parol6/client/dry_run_client.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def __init__(
167167
self,
168168
initial_joints_deg: list[float] | None = None,
169169
max_snapshot_points: int = 200,
170+
initial_homed: bool = True,
170171
) -> None:
171172
# Reset tool transform — process pool workers persist across
172173
# invocations, so a previous run's select_tool() leaves a stale
@@ -190,6 +191,10 @@ def __init__(
190191

191192
self._planner = TrajectoryPlanner(diagnostic=True)
192193
self._planner.state.Position_in[:] = self._state.Position_in
194+
# Mirror the live gate: seeded from an unhomed robot, planned moves
195+
# are refused until the script homes (home()/teleport() establish
196+
# references — see _snap_to_angles).
197+
self._planner.state.Homed_in.fill(1 if initial_homed else 0)
193198

194199
self._registry = CommandRegistry()
195200
self._q_rad_buf = np.zeros(6, dtype=np.float64)
@@ -230,18 +235,25 @@ def flush(self) -> list[DryRunResult]:
230235
return results
231236

232237
def _snap_to_angles(self, angles_deg: list[float]) -> DryRunResult:
233-
"""Snap to angles instantly (no trajectory) — used by Home and Teleport."""
238+
"""Snap to angles instantly (no trajectory) — used by Home and Teleport.
239+
240+
Both establish position references, so subsequent planned moves pass
241+
the homed gate."""
234242
self._planner.flush()
235243
deg = np.asarray(angles_deg, dtype=np.float64)
236244
deg_to_steps(deg, self._state.Position_in)
237245
self._planner.state.Position_in[:] = self._state.Position_in
246+
self._planner.state.Homed_in.fill(1)
238247
rad = np.radians(deg).reshape(1, -1)
239248
return _build_result(rad, duration=0.0)
240249

241250
def _dispatch(self, params: Any) -> DryRunResult | None:
242251
"""Route a command struct through the trajectory planner."""
243252
if isinstance(params, HomeCmd):
244-
return self._snap_to_angles(HOME_ANGLES_DEG)
253+
if not self._planner.state.Homed_in[:6].all():
254+
return self._snap_to_angles(HOME_ANGLES_DEG)
255+
# Already referenced → fall through: the planner fast-paths HOME
256+
# into a planned return move, so the preview renders the path.
245257
if isinstance(params, TeleportCmd):
246258
return self._snap_to_angles(params.angles)
247259
if isinstance(params, SelectToolCmd):

parol6/client/sync_client.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class RobotClient:
112112
Can be used as a context manager to ensure proper cleanup:
113113
114114
with RobotClient() as client:
115-
client.resume()
115+
client.home()
116116
...
117117
"""
118118

@@ -178,6 +178,10 @@ def port(self) -> int:
178178
def home(self, wait: bool = False, timeout: float = 60.0) -> int:
179179
"""Home the robot to its home position.
180180
181+
Unhomed, this runs the full referencing sequence (each joint seeks
182+
its limit switch, then moves to standby). Already homed, it returns
183+
to standby with a normal planned, collision-checked joint move.
184+
181185
Returns the command index (≥ 0) on success, -1 on failure.
182186
183187
Args:
@@ -194,21 +198,33 @@ def teleport(
194198
"""Instantly set joint angles and optional tool positions (simulator only)."""
195199
return _run(self._inner.teleport(angles_deg, tool_positions=tool_positions))
196200

197-
def resume(self) -> int:
198-
"""Re-enable the robot controller, allowing motion commands.
201+
def stop(self) -> int:
202+
"""Stop all motion — cancel the active move and clear the queue.
203+
204+
The controller stays enabled and holding position; the next motion
205+
command is accepted immediately.
199206
200207
Returns:
201208
1 if acknowledged, 0 on failure.
202209
"""
203-
return _run(self._inner.resume())
210+
return _run(self._inner.stop())
204211

205-
def halt(self) -> int:
206-
"""Halt the robot — stop all motion and disable.
212+
def estop(self) -> int:
213+
"""Protective stop: stop all motion and latch the controller
214+
disabled until ``reset()``.
207215
208216
Returns:
209217
1 if acknowledged, 0 on failure.
210218
"""
211-
return _run(self._inner.halt())
219+
return _run(self._inner.estop())
220+
221+
def reset(self) -> int:
222+
"""Clear a latched protective stop, re-enabling motion.
223+
224+
Returns:
225+
1 if acknowledged, 0 on failure.
226+
"""
227+
return _run(self._inner.reset())
212228

213229
def simulator(self, enabled: bool) -> int:
214230
"""Enable or disable simulator mode."""
@@ -229,9 +245,9 @@ def connect_hardware(self, port_str: str) -> int:
229245
"""
230246
return _run(self._inner.connect_hardware(port_str))
231247

232-
def reset(self) -> int:
248+
def reset_state(self) -> int:
233249
"""Reset controller state to initial values."""
234-
return _run(self._inner.reset())
250+
return _run(self._inner.reset_state())
235251

236252
# ---------- status / queries ----------
237253
def ping(self) -> PingResult | None:

parol6/commands/base.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,28 @@
1313
from parol6.config import TRACE
1414
from parol6.protocol.wire import CmdType, Command, CommandCode, QueryType
1515
from parol6.server.state import ControllerState
16-
from parol6.utils.error_catalog import RobotError, extract_robot_error
16+
from parol6.utils.error_catalog import RobotError, extract_robot_error, make_error
1717
from parol6.utils.error_codes import ErrorCode
18+
from parol6.utils.errors import TrajectoryPlanningError
1819

1920
logger = logging.getLogger(__name__)
2021

2122

23+
def guard_homed(state: ControllerState) -> None:
24+
"""Refuse planned motion while the robot is not homed.
25+
26+
Reported joint positions are unreferenced until homing (the boot state is
27+
all-zeros steps — outside J2/J3's limits), so building or collision-checking
28+
a trajectory from them is meaningless. Called at the top of every planned
29+
command's ``do_setup``, like ``guard_joint_path``. Jog/servo/home are
30+
deliberately not gated: they don't plan a path from the reported pose, and
31+
an unhomed arm may need to be jogged clear of an obstruction before homing.
32+
"""
33+
for i in range(6):
34+
if not state.Homed_in[i]:
35+
raise TrajectoryPlanningError(make_error(ErrorCode.MOTN_NOT_HOMED))
36+
37+
2238
class ExecutionStatusCode(Enum):
2339
"""Enumeration for command execution status codes."""
2440

parol6/commands/basic_commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ class HomeState(Enum):
7777
class HomeCommand(MotionCommand[HomeCmd]):
7878
"""
7979
A non-blocking command that tells the robot to perform its internal homing sequence.
80-
This version uses a state machine to allow re-homing even if the robot is already homed.
80+
Reached only while the robot is unhomed — the planner routes HOME from an
81+
already-referenced robot to a planned return move instead.
8182
"""
8283

8384
PARAMS_TYPE = HomeCmd

parol6/commands/cartesian_commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
ExecutionStatusCode,
3939
MotionCommand,
4040
TrajectoryMoveCommandBase,
41+
guard_homed,
4142
)
4243

4344
logger = logging.getLogger(__name__)
@@ -293,6 +294,7 @@ def __init__(self, p: MoveLCmd):
293294

294295
def do_setup(self, state: "ControllerState") -> None:
295296
"""Set up the move - compute target pose and pre-compute trajectory."""
297+
guard_homed(state)
296298
self.initial_pose = get_fkine_se3(state)
297299
self._compute_target_pose(state)
298300
self._precompute_trajectory(state)
@@ -408,6 +410,7 @@ def do_setup_with_blend(
408410
next_cmds: "list[TrajectoryMoveCommandBase]",
409411
) -> int:
410412
"""Build composite Cartesian trajectory with blend zones."""
413+
guard_homed(state)
411414
if self.blend_radius <= 0 or not next_cmds:
412415
self.do_setup(state)
413416
return 0

parol6/commands/curved_commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import numpy as np
1313

1414
from parol6.commands._collision_guard import guard_joint_path
15-
from parol6.commands.base import TrajectoryMoveCommandBase
15+
from parol6.commands.base import TrajectoryMoveCommandBase, guard_homed
1616
from parol6.config import INTERVAL_S, LIMITS, steps_to_rad
1717
from parol6.motion import CircularMotion, JointPath, SplineMotion, TrajectoryBuilder
1818
from parol6.protocol.wire import (
@@ -115,6 +115,7 @@ def get_current_pose(self, state: "ControllerState") -> np.ndarray:
115115

116116
def do_setup(self, state: "ControllerState") -> None:
117117
"""Pre-compute trajectory from current position."""
118+
guard_homed(state)
118119
self.log_debug(" -> Preparing %s...", self.name)
119120

120121
current_pose = self.get_current_pose(state)

parol6/commands/joint_commands.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import parol6.PAROL6_ROBOT as PAROL6_ROBOT
1919
from parol6.commands._collision_guard import guard_joint_path
20-
from parol6.commands.base import TrajectoryMoveCommandBase
20+
from parol6.commands.base import TrajectoryMoveCommandBase, guard_homed
2121
from parol6.config import (
2222
INTERVAL_S,
2323
MAX_BLEND_LOOKAHEAD,
@@ -79,6 +79,7 @@ def _get_target_rad(
7979

8080
def do_setup(self, state: ControllerState) -> None:
8181
"""Build trajectory from current position to target using unified motion pipeline."""
82+
guard_homed(state)
8283
steps_to_rad(state.Position_in, self._q_rad_buf)
8384
target_rad = self._get_target_rad(state, self._q_rad_buf)
8485
current_rad = self._q_rad_buf
@@ -116,6 +117,7 @@ def do_setup_with_blend(
116117
next_cmds: "list[TrajectoryMoveCommandBase]",
117118
) -> int:
118119
"""Build composite joint-space trajectory with blend zones."""
120+
guard_homed(state)
119121
if self.blend_radius <= 0 or not next_cmds:
120122
self.do_setup(state)
121123
return 0

0 commit comments

Comments
 (0)