Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions parol6/protocol/wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
Wire format uses msgpack arrays with integer type codes:
- OK: [MsgType.OK, req_id, index?]
- ERROR: [MsgType.ERROR, req_id, message]
- STATUS: [MsgType.STATUS, proto_version, pose, angles, speeds, io, action_current, action_state, joint_en, cart_en_wrf, cart_en_trf, executing_index, completed_index, last_checkpoint, error, queued_segments, queued_duration, action_params, tool_status, tcp_speed, simulator_active, collision_active, collision_pairs, scene_epoch, accepted_index, homed, enabled, homing_step, joints_homed, loop_health, drive_faults]
- STATUS: [MsgType.STATUS, proto_version, pose, angles, speeds, io, action_current, action_state, joint_en, cart_en_wrf, cart_en_trf, executing_index, completed_index, last_checkpoint, error, queued_segments, queued_duration, action_params, tool_status, tcp_speed, simulator_active, collision_active, collision_pairs, scene_epoch, accepted_index, homed, enabled, homing_step, joints_homed, loop_health, drive_faults, session_id, seq, mono_time_ns, motion_profile]
- RESPONSE: [MsgType.RESPONSE, req_id, [query_type, ...fields]]
- COMMAND: [CmdType.XXX, ...params]
"""
Expand Down Expand Up @@ -1692,6 +1692,10 @@ def pack_response(result: Response, req_id: int) -> bytes:

_NO_JOINTS_HOMED: tuple[int, ...] = (0, 0, 0, 0, 0, 0)

# Append-only so existing profile IDs remain stable, without importing the planner.
_MOTION_PROFILES = ("TOPPRA", "RUCKIG", "QUINTIC", "TRAPEZOID", "LINEAR", "SEPTIC")
_MOTION_PROFILE_IDS = {name: index for index, name in enumerate(_MOTION_PROFILES)}


def pack_status(
pose: np.ndarray,
Expand Down Expand Up @@ -1727,6 +1731,7 @@ def pack_status(
session_id: int = 0,
seq: int = 0,
mono_time_ns: int = 0,
motion_profile: str | None = None,
) -> bytes:
"""Pack a status broadcast message.

Expand Down Expand Up @@ -1781,6 +1786,7 @@ def pack_status(
session_id,
seq,
mono_time_ns,
_MOTION_PROFILE_IDS[motion_profile] if motion_profile is not None else None,
),
option=ormsgpack.OPT_SERIALIZE_NUMPY,
)
Expand Down Expand Up @@ -1834,6 +1840,7 @@ class StatusBuffer:
# Whether the controller accepts motion (False while disabled, e.g. the
# e-stop latch). True from producers that predate the field.
enabled: bool = True
motion_profile: str | None = None
# Remaining waldoctl StatusBuffer Protocol members. parol6 has no torque
# sensing, fieldbus, or warning source, so these hold their empty values.
torques: np.ndarray = field(default_factory=lambda: np.zeros(6, dtype=np.float64))
Expand Down Expand Up @@ -1920,6 +1927,7 @@ def copy(self) -> "StatusBuffer":
accepted_index=self.accepted_index,
homed=self.homed,
enabled=self.enabled,
motion_profile=self.motion_profile,
torques=self.torques.copy(),
torques_ext=self.torques_ext.copy(),
warnings=list(self.warnings),
Expand Down Expand Up @@ -1973,7 +1981,8 @@ def decode_status_bin_into(data: bytes, buf: StatusBuffer) -> bool:
tool_status_tuple, tcp_speed, simulator_active,
collision_active, collision_pairs, scene_epoch,
accepted_index, homed, enabled, homing_step, joints_homed,
loop_health, drive_faults, session_id, seq, mono_time_ns]
loop_health, drive_faults, session_id, seq, mono_time_ns,
motion_profile]

Args:
data: Raw msgpack bytes
Expand Down Expand Up @@ -2068,6 +2077,12 @@ def decode_status_bin_into(data: bytes, buf: StatusBuffer) -> bool:
buf.accepted_index = int(msg[24]) if len(msg) > 24 else -1
buf.homed = bool(msg[25]) if len(msg) > 25 else True
buf.enabled = bool(msg[26]) if len(msg) > 26 else True
profile_id = msg[34] if len(msg) > 34 else None
buf.motion_profile = (
_MOTION_PROFILES[profile_id]
if type(profile_id) is int and 0 <= profile_id < len(_MOTION_PROFILES)
else None
)
if len(msg) > 28:
_apply_homing_progress(buf, int(msg[27]), msg[28])
if len(msg) > 29:
Expand Down
3 changes: 3 additions & 0 deletions parol6/server/status_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def __init__(self) -> None:
self._p99_period_s: float = 0.0
self._overruns: int = 0
self._enabled: bool = True
self._motion_profile: str | None = None
self._homing_step: int = 0
self._joints_homed: list[int] = [0] * 6

Expand Down Expand Up @@ -570,6 +571,7 @@ def update_from_state(self, state: ControllerState) -> None:
enabled_changed = self._enabled != state.enabled
if enabled_changed:
self._enabled = state.enabled
self._motion_profile = state.motion_profile

for i in range(6):
bits = (1 if state.Temperature_error_in[i] else 0) | (
Expand Down Expand Up @@ -642,6 +644,7 @@ def to_binary(
accepted_index=self._accepted_index,
homed=self._homed,
enabled=self._enabled,
motion_profile=self._motion_profile,
homing_step=self._homing_step,
joints_homed=self._joints_homed,
p99_period_s=self._p99_period_s,
Expand Down
31 changes: 31 additions & 0 deletions tests/integration/test_profile_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
Tests SETPROFILE/GETPROFILE through the client API with a running server.
"""

import asyncio
import threading
import time

import numpy as np
import pytest

from parol6 import AsyncRobotClient
from parol6.motion.trajectory import ProfileType
from parol6.protocol.wire import StatusBuffer


@pytest.mark.integration
class TestProfileCommands:
Expand All @@ -36,6 +41,32 @@ def test_select_profile_case_insensitive(self, client, server_proc):
assert client.select_profile("Quintic") > 0
assert client.profile() == "QUINTIC"

@pytest.mark.asyncio
async def test_status_tracks_profile_selection_and_reset(self, server_proc, ports):
async with (
AsyncRobotClient(host=ports.server_ip, port=ports.server_port) as writer,
AsyncRobotClient(host=ports.server_ip, port=ports.server_port) as observer,
):

async def reported_profile(name: str) -> StatusBuffer:
async with asyncio.timeout(5.0):
async for status in observer.stream_status():
if status.motion_profile == name:
return status
raise AssertionError(
"Status stream closed before reporting the profile"
)

for profile in ProfileType:
if profile is ProfileType.TOPPRA:
continue
assert await writer.select_profile(profile.name) > 0
selected = await reported_profile(profile.name)
assert selected.motion_profile == profile.name
assert await writer.reset_state() > 0
reset = await reported_profile("TOPPRA")
assert reset.motion_profile == "TOPPRA"


@pytest.mark.integration
class TestProfileMotionBehavior:
Expand Down
Loading