diff --git a/.gitignore b/.gitignore index 0bb42e9..742005b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ *.pyc build/ myactuator_rmd_py.egg-info/ - +dist/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 477df40..56281df 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.20) project(myactuator_rmd VERSION 0.0.1) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + option(PYTHON_BINDINGS "Building Python bindings" OFF) option(BUILD_TESTING "Build unit and integration tests" OFF) option(SETUP_TEST_IFNAME "Set-up the test VCAN interface automatically" OFF) @@ -39,7 +42,26 @@ target_include_directories(myactuator_rmd PUBLIC $ ) set(MYACTUATOR_RMD_LIBRARIES "") -target_link_libraries(myactuator_rmd PUBLIC + +# --- Check for CAN frame member name --- +include(CheckCXXSourceCompiles) +check_cxx_source_compiles(" +#include +int main() { + struct can_frame frame; + frame.can_dlc = 0; + return 0; +}" HAVE_CAN_DLC) + +if(HAVE_CAN_DLC) + message(STATUS "Detected can_frame.can_dlc member - using this field name.") + target_compile_definitions(myactuator_rmd PUBLIC HAVE_CAN_DLC) +else() + message(STATUS "Using can_frame.len member for newer Linux CAN versions.") +endif() +# -------------------------------------- + +target_link_libraries(myactuator_rmd PUBLIC ${MYACTUATOR_RMD_LIBRARIES} ) install(DIRECTORY include/ @@ -57,7 +79,24 @@ if(PYTHON_BINDINGS) Development Interpreter ) - find_package(pybind11 CONFIG REQUIRED) + + # --- Dynamically find pybind11 CMake directory --- + execute_process( + COMMAND "${Python3_EXECUTABLE}" -m pybind11 --cmakedir + OUTPUT_VARIABLE PYBIND11_CMAKE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE PYBIND11_RESULT + ERROR_QUIET + ) + if(PYBIND11_RESULT EQUAL 0 AND IS_DIRECTORY "${PYBIND11_CMAKE_DIR}") + set(pybind11_DIR "${PYBIND11_CMAKE_DIR}") + message(STATUS "Found pybind11 CMake dir via Python: ${pybind11_DIR}") + else() + message(WARNING "Could not find pybind11 CMake dir via 'python3 -m pybind11 --cmakedir'. Falling back to standard find_package.") + endif() + # ------------------------------------------------- + + find_package(pybind11 REQUIRED) pybind11_add_module(myactuator_rmd_py bindings/myactuator_rmd.cpp @@ -65,6 +104,10 @@ if(PYTHON_BINDINGS) target_compile_features(myactuator_rmd_py PUBLIC cxx_std_17 ) + # Add HAVE_CAN_DLC definition if needed by bindings (unlikely, but for completeness) + if(HAVE_CAN_DLC) + target_compile_definitions(myactuator_rmd_py PUBLIC HAVE_CAN_DLC) + endif() target_link_libraries(myactuator_rmd_py PUBLIC myactuator_rmd ) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..06c760f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include myactuator_rmd_py.pyi diff --git a/myactuator_rmd_py.pyi b/myactuator_rmd_py.pyi new file mode 100644 index 0000000..456b760 --- /dev/null +++ b/myactuator_rmd_py.pyi @@ -0,0 +1,275 @@ +"""Python type stubs for the myactuator_rmd_py module. +Generated based on the pybind11 bindings. +""" + +import enum +from datetime import timedelta +from typing import Any, Type, Final, List, TypeVar, ClassVar + +# === Base Exceptions === + +class ActuatorException(Exception): ... + +class ProtocolException(ActuatorException): ... + +class ValueRangeException(ActuatorException): ... + +# === Base Driver Classes === + +class Driver: + # Base class, structure inferred from usage + ... + +class CanDriver(Driver): + def __init__(self, can_interface_name: str) -> None: ... + +# === Actuator State Types === + +class PiGains: + kp: int + ki: int + def __init__(self, kp: int, ki: int) -> None: ... + +class Gains: + current: PiGains + speed: PiGains + position: PiGains + def __init__(self, current: PiGains, speed: PiGains, position: PiGains) -> None: ... + # Overload for direct int initialization + # def __init__(self, current_kp: int, current_ki: int, speed_kp: int, speed_ki: int, position_kp: int, position_ki: int) -> None: ... + +class AccelerationType(enum.Enum): + POSITION_PLANNING_ACCELERATION: ClassVar[AccelerationType] + POSITION_PLANNING_DECELERATION: ClassVar[AccelerationType] + VELOCITY_PLANNING_ACCELERATION: ClassVar[AccelerationType] + VELOCITY_PLANNING_DECELERATION: ClassVar[AccelerationType] + +class CanBaudRate(enum.Enum): + KBPS500: ClassVar[CanBaudRate] + MBPS1: ClassVar[CanBaudRate] + +class ControlMode(enum.Enum): + NONE: ClassVar[ControlMode] + CURRENT: ClassVar[ControlMode] + VELOCITY: ClassVar[ControlMode] + POSITION: ClassVar[ControlMode] + +class ErrorCode(enum.Enum): + NO_ERROR: ClassVar[ErrorCode] + MOTOR_STALL: ClassVar[ErrorCode] + LOW_VOLTAGE: ClassVar[ErrorCode] + OVERVOLTAGE: ClassVar[ErrorCode] + OVERCURRENT: ClassVar[ErrorCode] + POWER_OVERRUN: ClassVar[ErrorCode] + SPEEDING: ClassVar[ErrorCode] + UNSPECIFIED_1: ClassVar[ErrorCode] + UNSPECIFIED_2: ClassVar[ErrorCode] + UNSPECIFIED_3: ClassVar[ErrorCode] + OVERTEMPERATURE: ClassVar[ErrorCode] + ENCODER_CALIBRATION_ERROR: ClassVar[ErrorCode] + +class MotorStatus1: + temperature: Final[int] + is_brake_released: Final[bool] + voltage: Final[float] + error_code: Final[ErrorCode] + def __init__(self, temperature: int, is_brake_released: bool, voltage: float, error_code: ErrorCode) -> None: ... + +class MotorStatus2: + temperature: Final[int] + current: Final[float] + shaft_speed: Final[float] + shaft_angle: Final[float] + def __init__(self, temperature: int, current: float, shaft_speed: float, shaft_angle: float) -> None: ... + +class MotorStatus3: + temperature: Final[int] + current_phase_a: Final[float] + current_phase_b: Final[float] + current_phase_c: Final[float] + def __init__(self, temperature: int, current_phase_a: float, current_phase_b: float, current_phase_c: float) -> None: ... + +# Feedback type returned by send* methods, structure mirrors MotorStatus2 +class Feedback: + temperature: Final[int] + current: Final[float] + shaft_speed: Final[float] + shaft_angle: Final[float] + # Actual __init__ might differ or not be exposed directly + def __init__(self, temperature: int, current: float, shaft_speed: float, shaft_angle: float) -> None: ... + +# === CAN Types === + +class Frame: + # Assuming std::array maps to list[int] + def __init__(self, id: int, data: List[int]) -> None: ... + def getId(self) -> int: ... + def getData(self) -> List[int]: ... + +class Node: + def __init__(self, interface_name: str) -> None: ... + # setRecvFilter takes struct can_filter*, difficult to type precisely + def setRecvFilter(self, filter: Any) -> None: ... + def read(self) -> Frame: ... + def write(self, frame: Frame) -> None: ... + +# CAN Exceptions +class SocketException(Exception): ... + +class CanException(SocketException): ... + +class TxTimeoutError(CanException): ... + +class LostArbitrationError(CanException): ... + +class ControllerProblemError(CanException): ... + +class ProtocolViolationError(CanException): ... + +class TransceiverStatusError(CanException): ... + +class NoAcknowledgeError(CanException): ... + +class BusOffError(CanException): ... + +class BusError(CanException): ... + +class ControllerRestartedError(CanException): ... + +# === Actuator Interface === + +class ActuatorInterface: + def __init__(self, driver: Driver, actuator_id: int) -> None: ... + + def getAcceleration(self) -> int: ... + def getCanId(self) -> int: ... + def getControllerGains(self) -> Gains: ... + def getControlMode(self) -> ControlMode: ... + def getMotorModel(self) -> str: ... + def getMotorPower(self) -> float: ... + def getMotorStatus1(self) -> MotorStatus1: ... + def getMotorStatus2(self) -> MotorStatus2: ... + def getMotorStatus3(self) -> MotorStatus3: ... + def getMultiTurnAngle(self) -> float: ... + def getMultiTurnEncoderPosition(self) -> int: ... + def getMultiTurnEncoderOriginalPosition(self) -> int: ... + def getMultiTurnEncoderZeroOffset(self) -> int: ... + def getRuntime(self) -> timedelta: ... + def getSingleTurnAngle(self) -> float: ... + def getSingleTurnEncoderPosition(self) -> int: ... + def getVersionDate(self) -> int: ... + + def lockBrake(self) -> None: ... + def releaseBrake(self) -> None: ... + def reset(self) -> None: ... + + def sendCurrentSetpoint(self, current: float) -> Feedback: ... + def sendPositionAbsoluteSetpoint(self, position: float, max_speed: float = 500.0) -> Feedback: ... + def sendTorqueSetpoint(self, torque: float, torque_constant: float) -> Feedback: ... + def sendVelocitySetpoint(self, speed: float) -> Feedback: ... + + def setAcceleration(self, acceleration: int, mode: AccelerationType) -> None: ... + def setCanBaudRate(self, baud_rate: CanBaudRate) -> None: ... + def setCanId(self, can_id: int) -> None: ... + def setControllerGains(self, gains: Gains, is_persistent: bool = False) -> Gains: ... + def setCurrentPositionAsEncoderZero(self) -> int: ... + def setEncoderZero(self, encoder_offset: int) -> None: ... + def setTimeout(self, timeout: timedelta) -> None: ... + def shutdownMotor(self) -> None: ... + def stopMotor(self) -> None: ... + +# === Actuator Constants === + +# Define a base class for actuator constants structure +class ActuatorConstantsBase: + reducer_ratio: Final[float] + rated_speed: Final[float] + rated_current: Final[float] + rated_power: Final[float] + rated_torque: Final[float] + torque_constant: Final[float] + rotor_inertia: Final[float] + +class X4V2(ActuatorConstantsBase): ... +class X4V3(ActuatorConstantsBase): ... +class X4_3(ActuatorConstantsBase): ... +class X4_24(ActuatorConstantsBase): ... +class X6V2(ActuatorConstantsBase): ... +class X6S2V2(ActuatorConstantsBase): ... +class X6V3(ActuatorConstantsBase): ... +class X6_7(ActuatorConstantsBase): ... +class X6_8(ActuatorConstantsBase): ... +class X6_40(ActuatorConstantsBase): ... +class X8V2(ActuatorConstantsBase): ... +class X8ProV2(ActuatorConstantsBase): ... +class X8S2V3(ActuatorConstantsBase): ... +class X8HV3(ActuatorConstantsBase): ... +class X8ProHV3(ActuatorConstantsBase): ... +class X8_20(ActuatorConstantsBase): ... +class X8_25(ActuatorConstantsBase): ... +class X8_60(ActuatorConstantsBase): ... +class X8_90(ActuatorConstantsBase): ... +class X10V3(ActuatorConstantsBase): ... +class X10S2V3(ActuatorConstantsBase): ... +class X10_40(ActuatorConstantsBase): ... +class X10_100(ActuatorConstantsBase): ... +class X12_150(ActuatorConstantsBase): ... +class X15_400(ActuatorConstantsBase): ... + + +# === Submodule Definitions === + +# Define modules that will be exposed at the top-level +class actuator_state: + AccelerationType: Type[AccelerationType] + CanBaudRate: Type[CanBaudRate] + ControlMode: Type[ControlMode] + ErrorCode: Type[ErrorCode] + PiGains: Type[PiGains] + Gains: Type[Gains] + MotorStatus1: Type[MotorStatus1] + MotorStatus2: Type[MotorStatus2] + MotorStatus3: Type[MotorStatus3] + Feedback: Type[Feedback] + +class can: + Frame: Type[Frame] + Node: Type[Node] + SocketException: Type[SocketException] + CanException: Type[CanException] + TxTimeoutError: Type[TxTimeoutError] + LostArbitrationError: Type[LostArbitrationError] + ControllerProblemError: Type[ControllerProblemError] + ProtocolViolationError: Type[ProtocolViolationError] + TransceiverStatusError: Type[TransceiverStatusError] + NoAcknowledgeError: Type[NoAcknowledgeError] + BusOffError: Type[BusOffError] + BusError: Type[BusError] + ControllerRestartedError: Type[ControllerRestartedError] + +class actuator_constants: + X4V2: Type[X4V2] + X4V3: Type[X4V3] + X4_3: Type[X4_3] + X4_24: Type[X4_24] + X6V2: Type[X6V2] + X6S2V2: Type[X6S2V2] + X6V3: Type[X6V3] + X6_7: Type[X6_7] + X6_8: Type[X6_8] + X6_40: Type[X6_40] + X8V2: Type[X8V2] + X8ProV2: Type[X8ProV2] + X8S2V3: Type[X8S2V3] + X8HV3: Type[X8HV3] + X8ProHV3: Type[X8ProHV3] + X8_20: Type[X8_20] + X8_25: Type[X8_25] + X8_60: Type[X8_60] + X8_90: Type[X8_90] + X10V3: Type[X10V3] + X10S2V3: Type[X10S2V3] + X10_40: Type[X10_40] + X10_100: Type[X10_100] + X12_150: Type[X12_150] + X15_400: Type[X15_400] diff --git a/setup.py b/setup.py index c7fffa6..0d97fe3 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ from pathlib import Path import subprocess import sys +import shutil from setuptools import Extension, setup from setuptools.command.build_ext import build_ext @@ -32,7 +33,8 @@ def build_extension(self, ext: CMakeExtension) -> None: f"-D CMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}", f"-D Python3_EXECUTABLE={sys.executable}", f"-D CMAKE_BUILD_TYPE={cfg}", - f"-D PYTHON_BINDINGS=on" + f"-D PYTHON_BINDINGS=on", + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" ] build_args = [] if "CMAKE_ARGS" in os.environ: @@ -53,7 +55,24 @@ def build_extension(self, ext: CMakeExtension) -> None: ["cmake", "--build", ".", *build_args], cwd=build_temp, check=True ) + # Copy the .pyi file to the build directory + src_pyi = Path('myactuator_rmd_py.pyi') + dst_pyi = Path(extdir) / 'myactuator_rmd_py.pyi' + shutil.copy(src_pyi, dst_pyi) + setup( + name="myactuator_rmd_py", + version="0.2.0", + author="Tobit Flatscher", + author_email="", + description="Python bindings for MyActuator RMD actuators", + long_description="Python bindings for the MyActuator RMD actuator series using CAN communication", + url="https://github.com/2b-t/myactuator_rmd", ext_modules=[CMakeExtension("myactuator_rmd_py")], - cmdclass={"build_ext": CMakeBuild} + cmdclass={"build_ext": CMakeBuild}, + # Include .pyi files for use with type checking + package_data={"": ["*.pyi"]}, + include_package_data=True, + zip_safe=False, + python_requires=">=3.6", ) diff --git a/src/can/node.cpp b/src/can/node.cpp index 0a4ec4b..9b33fb4 100644 --- a/src/can/node.cpp +++ b/src/can/node.cpp @@ -147,8 +147,13 @@ namespace myactuator_rmd { void Node::write(std::uint32_t const can_id, std::array const& data) { struct ::can_frame frame {}; frame.can_id = can_id; +#ifdef HAVE_CAN_DLC + frame.can_dlc = 8; +#else frame.len = 8; - std::copy(std::begin(data), std::end(data), std::begin(frame.data)); +#endif + memset(frame.data, 0, sizeof(frame.data)); + memcpy(frame.data, data.data(), data.size()); if (::write(socket_, &frame, sizeof(struct ::can_frame)) != sizeof(struct ::can_frame)) { std::ostringstream ss {}; ss << frame; diff --git a/src/can/utilities.cpp b/src/can/utilities.cpp index 4128230..1573ea7 100644 --- a/src/can/utilities.cpp +++ b/src/can/utilities.cpp @@ -2,15 +2,23 @@ #include #include +#include +#include #include std::ostream& operator << (std::ostream& os, struct ::can_frame const& frame) noexcept { os << "id: " << "0x" << std::hex << std::setfill('0') << std::setw(3) << frame.can_id << ", data: "; +#ifdef HAVE_CAN_DLC + for (int i = 0; i < frame.can_dlc; i++) { + os << std::hex << std::setfill('0') << std::setw(2) << static_cast(frame.data[i]) << " "; + } +#else for (int i = 0; i < frame.len; i++) { os << std::hex << std::setfill('0') << std::setw(2) << static_cast(frame.data[i]) << " "; } +#endif os << std::dec; return os; }