import importlib.util from pathlib import Path import sys REPO_ROOT = Path(__file__).resolve().parent class SCurveError(RuntimeError): pass def _default_module_candidates(): search_roots = [ REPO_ROOT / "cpp" / "common" / "curve" / "build", REPO_ROOT / "cpp" / "common" / "curve" / "build" / "Release", ] patterns = [ "_s_curve_native*.pyd", "_s_curve_native*.so", ] candidates = [] for root in search_roots: if not root.exists(): continue for pattern in patterns: candidates.extend(sorted(root.glob(pattern))) return candidates def _resolve_module_path(module_path=None): if module_path: path = Path(module_path) if path.exists(): return path raise FileNotFoundError(f"SCurve native module not found: {path}") for candidate in _default_module_candidates(): if candidate.exists(): return candidate candidate_text = "\n".join(str(path) for path in _default_module_candidates()) raise FileNotFoundError( "SCurve native module not found. Build it first with:\n" "powershell -ExecutionPolicy Bypass -File .\\cpp\\common\\curve\\build_python.ps1 " "-PythonExecutable 'C:\\Users\\Administrator\\miniconda3\\envs\\cmvr-head-client\\python.exe'\n" f"Expected one of:\n{candidate_text}" ) class SCurve: def __init__(self, max_velocity=3.0, max_acceleration=10.0, max_jerk=50.0, module_path=None, dll_path=None): resolved_path = _resolve_module_path(module_path or dll_path) try: self._module = self._load_native_module(resolved_path) self._native = self._module.SCurve(max_velocity, max_acceleration, max_jerk) except Exception as exc: raise SCurveError(f"Failed to create pybind11 SCurve instance: {exc}") from exc @staticmethod def _load_native_module(module_path): module_name = "_s_curve_native" if module_name in sys.modules: return sys.modules[module_name] spec = importlib.util.spec_from_file_location(module_name, module_path) if spec is None or spec.loader is None: raise ImportError(f"Unable to load module from {module_path}") module = importlib.util.module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) return module @staticmethod def _wrap_native_call(action, fn, *args, **kwargs): try: return fn(*args, **kwargs) except Exception as exc: raise SCurveError(f"{action} failed: {exc}") from exc def setConstraints(self, max_velocity, max_acceleration, max_jerk): return self._wrap_native_call( "setConstraints", self._native.setConstraints, max_velocity, max_acceleration, max_jerk, ) def getConstraints(self): return self._wrap_native_call( "getConstraints", self._native.getConstraints, ) def calculateProfile(self, start_position, end_position, start_velocity=0.0, end_velocity=0.0): return self._wrap_native_call( "calculateProfile", self._native.calculateProfile, start_position, end_position, start_velocity, end_velocity, ) def getPositionAtTime(self, profile, t): return self._wrap_native_call("getPositionAtTime", self._native.getPositionAtTime, profile, t) def getVelocityAtTime(self, profile, t): return self._wrap_native_call("getVelocityAtTime", self._native.getVelocityAtTime, profile, t) def getAccelerationAtTime(self, profile, t): return self._wrap_native_call( "getAccelerationAtTime", self._native.getAccelerationAtTime, profile, t, ) def getJerkAtTime(self, profile, t): return self._wrap_native_call("getJerkAtTime", self._native.getJerkAtTime, profile, t) def sampleTrajectory(self, profile, dt): return self._wrap_native_call( "sampleTrajectory", self._native.sampleTrajectory, profile, dt, ) class SCurvePositionPlanner1D: def __init__(self, max_velocity=3.0, max_acceleration=10.0, max_jerk=50.0, module_path=None, dll_path=None): resolved_path = _resolve_module_path(module_path or dll_path) try: self._module = SCurve._load_native_module(resolved_path) self._native = self._module.SCurvePositionPlanner1D( max_velocity, max_acceleration, max_jerk, ) except Exception as exc: raise SCurveError(f"Failed to create pybind11 SCurvePositionPlanner1D instance: {exc}") from exc def setConstraints(self, max_velocity, max_acceleration, max_jerk): return SCurve._wrap_native_call( "setConstraints", self._native.setConstraints, max_velocity, max_acceleration, max_jerk, ) def getConstraints(self): return SCurve._wrap_native_call("getConstraints", self._native.getConstraints) def setPositionGain(self, position_gain): return SCurve._wrap_native_call("setPositionGain", self._native.setPositionGain, position_gain) def getPositionGain(self): return SCurve._wrap_native_call("getPositionGain", self._native.getPositionGain) def initialize(self, position, velocity=0.0, acceleration=0.0): return SCurve._wrap_native_call( "initialize", self._native.initialize, position, velocity, acceleration, ) def reset(self): return SCurve._wrap_native_call("reset", self._native.reset) def setTarget(self, target_position): return SCurve._wrap_native_call("setTarget", self._native.setTarget, target_position) def update(self, dt): return SCurve._wrap_native_call("update", self._native.update, dt) def getState(self): return SCurve._wrap_native_call("getState", self._native.getState) def sampleTrajectory( self, target_position, dt, duration, initial_position=0.0, initial_velocity=0.0, initial_acceleration=0.0, position_gain=None, ): if dt <= 0: raise ValueError("dt must be > 0") if duration < 0: raise ValueError("duration must be >= 0") self.initialize(initial_position, initial_velocity, initial_acceleration) if position_gain is not None: self.setPositionGain(position_gain) self.setTarget(target_position) times = [0.0] state = self.getState() positions = [state.position] velocities = [state.velocity] accelerations = [state.acceleration] jerks = [state.jerk] targets = [state.target_position] moving = [state.is_moving] t = 0.0 while t + dt <= duration + 1e-12: t = round(t + dt, 12) self.update(dt) state = self.getState() times.append(t) positions.append(state.position) velocities.append(state.velocity) accelerations.append(state.acceleration) jerks.append(state.jerk) targets.append(state.target_position) moving.append(state.is_moving) if abs(times[-1] - duration) > 1e-9: final_dt = duration - times[-1] if final_dt > 1e-12: self.update(final_dt) state = self.getState() times.append(duration) positions.append(state.position) velocities.append(state.velocity) accelerations.append(state.acceleration) jerks.append(state.jerk) targets.append(state.target_position) moving.append(state.is_moving) return { "times": times, "positions": positions, "velocities": velocities, "accelerations": accelerations, "jerks": jerks, "targets": targets, "moving": moving, }