Skip to content

Commit 8d94ac3

Browse files
committed
bugfixes
1 parent 94034bc commit 8d94ac3

16 files changed

Lines changed: 403 additions & 103 deletions

‎parol6/PAROL6_ROBOT.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def apply_tool(tool_name: str) -> None:
131131
_joint_min_speed: Vec6i = np.array([100, 100, 100, 100, 100, 100], dtype=np.int32)
132132

133133
# Jog speeds (steps/s) - 80% of max for safety margin during jogging
134-
_joint_max_jog_speed: Vec6i = (_joint_max_speed * 0.5).astype(np.int32)
134+
_joint_max_jog_speed: Vec6i = (_joint_max_speed * 0.8).astype(np.int32)
135135
_joint_min_jog_speed: Vec6i = np.array([100, 100, 100, 100, 100, 100], dtype=np.int32)
136136

137137
# Joint accelerations (steps/s^2) per joint

‎parol6/client/async_client.py‎

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ async def is_robot_stopped(self, threshold_speed: float = 2.0) -> bool:
690690

691691
async def wait_motion_complete(
692692
self,
693-
timeout: float = 90.0,
693+
timeout: float = 10.0,
694694
settle_window: float = 0.25,
695695
speed_threshold: float = 2.0,
696696
angle_threshold: float = 0.5,
@@ -727,57 +727,53 @@ async def wait_motion_complete(
727727
if timeout_task.done():
728728
return False
729729

730-
# Check speeds from status
730+
# Check speeds and angles from status
731731
speeds = status.get("speeds")
732+
angles = status.get("angles")
733+
734+
max_speed = None
732735
if speeds and isinstance(speeds, Iterable):
733736
max_speed = max(abs(s) for s in speeds)
734737

735-
# Phase 1: Wait for motion to start
736-
if not motion_started:
737-
if max_speed >= speed_threshold:
738-
motion_started = True
739-
settle_start = None # Reset settle timer
740-
elif time.time() - start_time > motion_start_timeout:
741-
# Motion never started - consider complete
742-
# This handles cases where command was invalid or no-op
743-
motion_started = True
744-
745-
# Phase 2: Wait for motion to complete
746-
if motion_started:
747-
if max_speed < speed_threshold:
748-
if settle_start is None:
749-
settle_start = time.time()
750-
elif time.time() - settle_start > settle_window:
751-
return True
752-
else:
753-
settle_start = None
754-
755-
# Also check angles as fallback
756-
angles = status.get("angles")
757-
if angles and not speeds:
758-
if last_angles is not None:
759-
max_change = max(
760-
abs(a - b)
761-
for a, b in zip(angles, last_angles, strict=False)
762-
)
763-
764-
# Detect motion start from angle changes
765-
if not motion_started:
766-
if max_change >= angle_threshold:
767-
motion_started = True
768-
settle_start = None
769-
elif time.time() - start_time > motion_start_timeout:
770-
motion_started = True
771-
772-
if motion_started:
773-
if max_change < angle_threshold:
774-
if settle_start is None:
775-
settle_start = time.time()
776-
elif time.time() - settle_start > settle_window:
777-
return True
778-
else:
779-
settle_start = None
738+
max_angle_change = None
739+
if angles and last_angles is not None:
740+
max_angle_change = max(
741+
abs(a - b) for a, b in zip(angles, last_angles, strict=False)
742+
)
743+
if angles:
780744
last_angles = angles
745+
746+
# Phase 1: Wait for motion to start
747+
if not motion_started:
748+
started_by_speed = (
749+
max_speed is not None and max_speed >= speed_threshold
750+
)
751+
started_by_angle = (
752+
max_angle_change is not None
753+
and max_angle_change >= angle_threshold
754+
)
755+
if started_by_speed or started_by_angle:
756+
motion_started = True
757+
settle_start = None
758+
elif time.time() - start_time > motion_start_timeout:
759+
# Motion never started - consider complete
760+
motion_started = True
761+
762+
# Phase 2: Wait for motion to complete
763+
if motion_started:
764+
# Check if settled: require ALL available metrics to be below threshold
765+
speed_settled = max_speed is None or max_speed < speed_threshold
766+
angle_settled = (
767+
max_angle_change is None or max_angle_change < angle_threshold
768+
)
769+
770+
if speed_settled and angle_settled:
771+
if settle_start is None:
772+
settle_start = time.time()
773+
elif time.time() - settle_start > settle_window:
774+
return True
775+
else:
776+
settle_start = None
781777
finally:
782778
timeout_task.cancel()
783779
try:

‎parol6/client/status_subscriber.py‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,19 @@ async def subscribe_status(
191191
)
192192
continue
193193

194-
except asyncio.CancelledError:
195-
# Normal shutdown path when consumer task is cancelled
196-
logger.info("subscribe_status cancelled")
197-
raise
194+
except (asyncio.CancelledError, GeneratorExit):
195+
# Normal shutdown - don't log
196+
pass
198197
except Exception as e:
199-
logger.error(f"Error in subscribe_status: {e}", exc_info=True)
200-
raise
198+
# Log unexpected errors, but not "Event loop is closed" during shutdown
199+
if "Event loop is closed" not in str(e):
200+
logger.error(f"Error in subscribe_status: {e}")
201201
finally:
202-
if transport:
203-
logger.info("Closing transport...")
204-
transport.close()
202+
try:
203+
if transport:
204+
transport.close()
205+
except Exception:
206+
pass
205207
try:
206208
sock.close()
207209
except Exception:

‎parol6/client/sync_client.py‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,30 @@ def _loop_worker(loop: asyncio.AbstractEventLoop) -> None:
3131

3232
def _stop_sync_loop() -> None:
3333
global _SYNC_LOOP, _SYNC_THREAD
34-
if _SYNC_LOOP is not None:
35-
try:
36-
_SYNC_LOOP.call_soon_threadsafe(_SYNC_LOOP.stop)
37-
except Exception:
38-
pass
39-
_SYNC_LOOP = None
40-
_SYNC_THREAD = None
34+
if _SYNC_LOOP is None:
35+
return
36+
37+
loop = _SYNC_LOOP
38+
39+
async def _shutdown():
40+
# Cancel all pending tasks
41+
tasks = [t for t in asyncio.all_tasks(loop) if t is not asyncio.current_task()]
42+
for task in tasks:
43+
task.cancel()
44+
# Let cancelled tasks finalize
45+
if tasks:
46+
await asyncio.gather(*tasks, return_exceptions=True)
47+
loop.stop()
48+
49+
try:
50+
asyncio.run_coroutine_threadsafe(_shutdown(), loop)
51+
if _SYNC_THREAD is not None:
52+
_SYNC_THREAD.join(timeout=2.0)
53+
except Exception:
54+
pass
55+
56+
_SYNC_LOOP = None
57+
_SYNC_THREAD = None
4158

4259

4360
def _ensure_sync_loop() -> None:
@@ -382,7 +399,7 @@ def is_robot_stopped(self, threshold_speed: float = 2.0) -> bool:
382399

383400
def wait_motion_complete(
384401
self,
385-
timeout: float = 90.0,
402+
timeout: float = 10.0,
386403
settle_window: float = 0.25,
387404
speed_threshold: float = 2.0,
388405
angle_threshold: float = 0.5,

‎parol6/commands/base.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from typing import Any, ClassVar, overload
1212

1313
import numpy as np
14-
from numba import njit # type: ignore[import-untyped]
1514

1615
from parol6.config import INTERVAL_S, LIMITS, TRACE
1716
from parol6.protocol.wire import CommandCode
@@ -20,9 +19,6 @@
2019

2120
logger = logging.getLogger(__name__)
2221

23-
_JOINT_LIM_MIN = LIMITS.joint.position.steps[:, 0]
24-
_JOINT_LIM_MAX = LIMITS.joint.position.steps[:, 1]
25-
2622

2723
class ExecutionStatusCode(Enum):
2824
"""Enumeration for command execution status codes."""
@@ -488,10 +484,9 @@ def linmap_pct(pct: float, lo: float, hi: float) -> float:
488484
return lo + (hi - lo) * (pct / 100.0)
489485

490486
@staticmethod
491-
@njit
492487
def limit_hit_mask(pos_steps: np.ndarray, speeds: np.ndarray) -> np.ndarray:
493-
return ((speeds > 0) & (pos_steps >= _JOINT_LIM_MAX)) | (
494-
(speeds < 0) & (pos_steps <= _JOINT_LIM_MIN)
488+
return ((speeds > 0) & (pos_steps >= LIMITS.joint.position.steps[:, 1])) | (
489+
(speeds < 0) & (pos_steps <= LIMITS.joint.position.steps[:, 0])
495490
)
496491

497492
def fail_and_idle(self, state: ControllerState, message: str) -> None:

‎parol6/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _trace(self, msg, *args, **kwargs):
3535
logger = logging.getLogger(__name__)
3636

3737
# Default control/sample rates (Hz)
38-
CONTROL_RATE_HZ: float = float(os.getenv("PAROL6_CONTROL_RATE_HZ", "250"))
38+
CONTROL_RATE_HZ: float = float(os.getenv("PAROL6_CONTROL_RATE_HZ", "200"))
3939

4040
DEFAULT_ACCEL_PERCENT: float = 100.0
4141

‎parol6/motion/streaming_executors.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ def reset_limits(self) -> None:
330330
self._acc_scale = 1.0
331331
self._apply_limits()
332332

333+
def reset(self) -> None:
334+
"""Reset executor state."""
335+
with self._lock:
336+
self._vel_scale = 1.0
337+
self._acc_scale = 1.0
338+
self.active = False
339+
self._cart_vel_limit = None
340+
self._init_state()
341+
333342
@property
334343
def current_position(self) -> list[float]:
335344
"""Get current position state in radians."""
@@ -450,7 +459,9 @@ def sync_pose(self, current_pose: sp.SE3) -> None:
450459
current_pose: Current TCP pose as SE3
451460
"""
452461
with self._lock:
453-
self.reference_pose = current_pose
462+
self.reference_pose = (
463+
current_pose.copy()
464+
) # Copy to avoid aliasing with cached FK
454465
# Reset Ruckig state to origin (relative to reference)
455466
self.inp.current_position = [0.0] * 6
456467
self.inp.current_velocity = [0.0] * 6

‎parol6/motion/trajectory.py‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@
3636
if TYPE_CHECKING:
3737
import sophuspy as sp
3838

39-
# Silence toppra's verbose debug output
40-
logging.getLogger("toppra").setLevel(logging.INFO)
41-
4239
logger = logging.getLogger(__name__)
4340

4441

‎parol6/server/controller.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,16 @@ def main():
14681468
datefmt="%H:%M:%S",
14691469
)
14701470

1471+
third_party_log_level = log_level if log_level >= logging.INFO else logging.INFO
1472+
# Silence toppra's verbose debug output
1473+
logging.getLogger("toppra").setLevel(third_party_log_level)
1474+
logging.getLogger("numba").setLevel(third_party_log_level)
1475+
1476+
# Pre-compile numba JIT functions to avoid mid-loop compilation stalls
1477+
from parol6.utils.warmup import warmup_jit
1478+
1479+
warmup_jit()
1480+
14711481
# Create configuration (env vars may override defaults)
14721482
env_host = os.getenv("PAROL6_CONTROLLER_IP")
14731483
env_port = os.getenv("PAROL6_CONTROLLER_PORT")

‎parol6/server/state.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ def reset(self) -> None:
239239
self._fkine_last_pos_in.fill(0)
240240
self._fkine_last_tool = ""
241241

242+
# Reset streaming executors (clears reference_pose and Ruckig state)
243+
if self.streaming_executor is not None:
244+
self.streaming_executor.reset()
245+
if self.cartesian_streaming_executor is not None:
246+
self.cartesian_streaming_executor.reset()
247+
242248
logger.debug("Controller state reset (preserving connection)")
243249

244250
@property

0 commit comments

Comments
 (0)