Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean rewrite of the driver in Python #13

Draft
wants to merge 72 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
8434bfa
Launch the driver under the `/schunk` namespace
stefanscherzinger Feb 4, 2025
ebf522e
Begin a complete rework of this driver in Python
stefanscherzinger Feb 12, 2025
e48ee58
Add boilerplate code for lifecycle behavior
stefanscherzinger Feb 13, 2025
ace17bd
Restart the daemon inside the test setup fixture
stefanscherzinger Feb 13, 2025
e059d52
Deactivate the C++ driver sub packages for now
stefanscherzinger Feb 13, 2025
af5c05a
Fix the CI for the new driver
stefanscherzinger Feb 13, 2025
007a9c0
Drop ignores for mypy's undefined attributes
stefanscherzinger Feb 13, 2025
e688055
Adapt `conftest` include for `test_launch`
stefanscherzinger Feb 17, 2025
ca8e8f1
Add a test for primary lifecycle states
stefanscherzinger Feb 17, 2025
798ce4b
Add a test for repeated configure and activate transitions
stefanscherzinger Feb 17, 2025
deda88a
Remove the driver's entry point from `setup.py`
stefanscherzinger Feb 17, 2025
c03469e
Connect to the modbus device on configure
stefanscherzinger Feb 18, 2025
4bd6292
Use a parameter for the serial port
stefanscherzinger Feb 18, 2025
a7028f3
Add a parameter for the gripper's device id
stefanscherzinger Feb 19, 2025
4e90414
Start developing a low-level library
stefanscherzinger Feb 21, 2025
b47b9a8
Remove `Rolling` CI for now
stefanscherzinger Feb 21, 2025
4443a32
Make input output buffer access thread-safe
stefanscherzinger Feb 22, 2025
3556d77
Move threading-related tests into a separate module
stefanscherzinger Feb 22, 2025
173000c
Ignore the interfaces package for now
stefanscherzinger Feb 22, 2025
819620a
Increase test coverage for bit operations
stefanscherzinger Feb 22, 2025
91aedbc
Add support for making Modbus connections
stefanscherzinger Feb 22, 2025
f939635
Add more Modbus connection tests
stefanscherzinger Feb 22, 2025
9709e95
Rename test for connection
stefanscherzinger Feb 22, 2025
0c2faf8
Type all members in init
stefanscherzinger Feb 23, 2025
65f90d4
Ignore `E203` globally with flake8
stefanscherzinger Feb 24, 2025
8e004e3
Add test for non-existing Modbus port
stefanscherzinger Feb 25, 2025
d763923
Start implementing the `send_plc_output` method
stefanscherzinger Feb 25, 2025
2b5dcd2
Ignore everything but the new driver library
stefanscherzinger Feb 25, 2025
cfec375
Update dependencies
stefanscherzinger Feb 25, 2025
2ad8308
Update flake8 formatting rules
stefanscherzinger Feb 25, 2025
137cf2d
Deactivate the test for `send_plc_output`
stefanscherzinger Feb 25, 2025
b8c7cc9
Merge pull request #15 from SCHUNK-SE-Co-KG/fix-github-ci
stefanscherzinger Feb 25, 2025
b9065dc
Change units of `get_target_speed`
stefanscherzinger Feb 25, 2025
28ca6d4
Add server and client script for debugging
stefanscherzinger Feb 27, 2025
30a978d
Add debug output for the test server
stefanscherzinger Feb 27, 2025
32c4e66
Add a test skip fixture when no gripper is available
stefanscherzinger Feb 27, 2025
74c4948
Implement receiving plc input buffers
stefanscherzinger Feb 27, 2025
90e6dd3
Merge pull request #14 from SCHUNK-SE-Co-KG/add-low-level-library
stefanscherzinger Feb 27, 2025
8d7b9e8
Create new test-specific readme
stefanscherzinger Feb 27, 2025
a8a0e6f
Re-activate the `schunk_gripper_driver` package
stefanscherzinger Feb 28, 2025
75ed7f6
Simplify the driver's test infrastructure
stefanscherzinger Feb 28, 2025
ac8a58e
Fix the driver library's installation methods
stefanscherzinger Mar 1, 2025
58a24ef
Fix reconnection in the driver library
stefanscherzinger Mar 1, 2025
8235171
Reformulate the test for the driver's startup
stefanscherzinger Mar 1, 2025
1644a63
Implement the driver's `configure` and `cleanup`
stefanscherzinger Mar 2, 2025
efdcfc0
Add `get_status_warning` to driver library
stefanscherzinger Mar 2, 2025
9a9f4b4
Add a getter for the gripper's full diagnostics
stefanscherzinger Mar 2, 2025
9deec24
Implement `acknowledge`
stefanscherzinger Mar 2, 2025
b1b9544
Implement the `fast_stop` service
stefanscherzinger Mar 2, 2025
27e8e79
Fix the driver library's threading test
stefanscherzinger Mar 2, 2025
0e2dcf4
Add a test for concurrent receive calls
stefanscherzinger Mar 2, 2025
9078cc2
Use explicit hex strings for the status codes
stefanscherzinger Mar 2, 2025
3bb6e7f
Fix endianness in send and receive methods
stefanscherzinger Mar 2, 2025
c017cc1
Add a method to clear the plc output buffer
stefanscherzinger Mar 3, 2025
a4b4636
Try fixing acknowledge and fast stop
stefanscherzinger Mar 4, 2025
87084f3
Fix `acknowledge` and `fast_stop`
stefanscherzinger Mar 4, 2025
39de3f1
Merge pull request #16 from SCHUNK-SE-Co-KG/proof-of-concept
stefanscherzinger Mar 6, 2025
a626a43
fix modbus server
mathisallweyer Mar 6, 2025
1b18137
Add dependencies
mathisallweyer Mar 6, 2025
b45dcf1
Remove pytest-asyncio
mathisallweyer Mar 6, 2025
4ee006b
remove comments and obsolet code
mathisallweyer Mar 10, 2025
125bad3
Merge pull request #18 from SCHUNK-SE-Co-KG/modbus-server
stefanscherzinger Mar 10, 2025
0c9685e
Use a background thread for continuous polling of updates
stefanscherzinger Mar 5, 2025
2855aad
Add a connection argument for update cycle
stefanscherzinger Mar 5, 2025
409ddfd
Make the module update async
stefanscherzinger Mar 6, 2025
0f4bb72
Automatically invert fast stop bit in `clear_plc_output`
stefanscherzinger Mar 6, 2025
1cd8cd2
Add a waiting mechanism for desired states
stefanscherzinger Mar 6, 2025
d51a3da
Clean interface of the `wait_for_status` method
stefanscherzinger Mar 6, 2025
0546260
Implement an `acknowledge` coroutine
stefanscherzinger Mar 6, 2025
7821ef7
Implement fast_stop
stefanscherzinger Mar 6, 2025
3804d57
Use the library's coroutines in the ROS2 driver
stefanscherzinger Mar 7, 2025
7297f98
Merge pull request #17 from SCHUNK-SE-Co-KG/highlevel-api
stefanscherzinger Mar 10, 2025
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
4 changes: 2 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
# Use black's line length (default 88) instead of the flake8 default of 79:
max-line-length = 88

per-file-ignores =
schunk_egu_egk_gripper_dummy/*.py:E203
ignore = E203, W503
# W503: Change in best practices, see here: https://www.flake8rules.com/rules/W503.html
4 changes: 2 additions & 2 deletions .github/script/install_dependencies.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#/usr/bin/bash
cd $HOME
apt-get install -y curl libcurl4-openssl-dev
apt-get install -y socat

# Python dependencies
python_deps="fastapi uvicorn httpx requests coverage python-multipart"
python_deps="fastapi uvicorn httpx requests coverage python-multipart pymodbus pyserial"
os_name=$(lsb_release -cs)

case $os_name in
Expand Down
24 changes: 0 additions & 24 deletions .github/workflows/industrial_ci_iron_action.yml

This file was deleted.

25 changes: 0 additions & 25 deletions .github/workflows/industrial_ci_rolling_action.yml

This file was deleted.

Empty file.
2 changes: 1 addition & 1 deletion schunk_egu_egk_gripper_driver/launch/schunk.launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def generate_launch_description():
package="schunk_egu_egk_gripper_driver",
plugin="SchunkGripperNode",
name="schunk_gripper_driver",
namespace="",
namespace="schunk",
parameters=[
{"IP": LaunchConfiguration("IP")},
{"port": LaunchConfiguration("port")},
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
22 changes: 22 additions & 0 deletions schunk_gripper_driver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SCHUNK Gripper Driver


## Lifecycle behavior

The driver implements a [lifecycle node](https://github.com/ros2/demos/tree/humble/lifecycle) with defined state transitions.
This gives us advanced control over configuration and resetting processes.

### Command line
With built-in features

```bash
ros2 lifecycle get /schunk/driver
ros2 lifecycle set /schunk/driver configure # activate | deactivate | cleanup | shutdown
```

And with ROS2 services

```bash
ros2 service call /schunk/driver/get_state lifecycle_msgs/srv/GetState '{}'
ros2 service call /schunk/driver/change_state lifecycle_msgs/srv/ChangeState '{transition: {label: configure}}' # activate | deactivate etc.
```
Empty file.
52 changes: 52 additions & 0 deletions schunk_gripper_driver/launch/driver.launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2025 SCHUNK SE & Co. KG
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.
# --------------------------------------------------------------------------------

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration


port = DeclareLaunchArgument(
"port",
default_value="/dev/ttyUSB0",
description="The gripper's serial port",
)
device_id = DeclareLaunchArgument(
"device_id",
default_value="12",
description="The gripper's Modbus device id",
)
args = [port, device_id]


def generate_launch_description():
return LaunchDescription(
args
+ [
Node(
package="schunk_gripper_driver",
namespace="schunk",
executable="driver.py",
name="driver",
parameters=[
{"port": LaunchConfiguration("port")},
{"device_id": LaunchConfiguration("device_id")},
],
output="both",
)
]
)
23 changes: 23 additions & 0 deletions schunk_gripper_driver/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>schunk_gripper_driver</name>
<version>0.0.0</version>
<description>ROS2 driver for SCHUNK`s EGU, EGK, and EZU mechatronic grippers</description>
<maintainer email="[email protected]">stefan</maintainer>
<license>GPL-3.0-or-later</license>

<depend>rclpy</depend>
<depend>launch</depend>
<depend>launch_ros</depend>
<depend>schunk_gripper_library</depend>
<depend>std_srvs</depend>

<test_depend>launch_pytest</test_depend>
<test_depend>ament_copyright</test_depend>
<test_depend>python3-pytest</test_depend>

<export>
<build_type>ament_python</build_type>
</export>
</package>
Empty file.
Empty file.
118 changes: 118 additions & 0 deletions schunk_gripper_driver/schunk_gripper_driver/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# Copyright 2025 SCHUNK SE & Co. KG
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.
# --------------------------------------------------------------------------------

import rclpy

from rclpy.lifecycle import Node
from rclpy.lifecycle import State
from rclpy.lifecycle import TransitionCallbackReturn
from schunk_gripper_library.driver import Driver as GripperDriver
from std_srvs.srv import Trigger
import asyncio


class Driver(Node):

def __init__(self, node_name: str, **kwargs):
super().__init__(node_name, **kwargs)
self.declare_parameter("port", rclpy.Parameter.Type.STRING)
self.declare_parameter("device_id", rclpy.Parameter.Type.INTEGER)
self.port = self.get_parameter_or("port", "/dev/ttyUSB0").value
self.gripper = GripperDriver()

def on_configure(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("on_configure() is called.")
self.get_logger().info(f"Connecting on port {self.port}")
if not self.gripper.connect(
protocol="modbus",
port=self.get_parameter_or("port", "/dev/ttyUSB0").value,
device_id=self.get_parameter_or("device_id", 12).value,
):
self.get_logger().warn("Gripper connect failed")
return TransitionCallbackReturn.FAILURE

# Services
self.acknowledge_srv = self.create_service(
Trigger, "~/acknowledge", self._acknowledge_cb
)
self.fast_stop_srv = self.create_service(
Trigger, "~/fast_stop", self._fast_stop_cb
)

# State update
self.timer = self.create_timer(0.5, self.status_update)

# Clear control bits
self.gripper.clear_plc_output()
self.gripper.set_control_bit(bit=0, value=True)
self.gripper.send_plc_output()
return TransitionCallbackReturn.SUCCESS

def on_activate(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("on_activate() is called.")
return super().on_activate(state)

def on_deactivate(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("on_deactivate() is called.")
return super().on_deactivate(state)

def on_cleanup(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("on_cleanup() is called.")
self.gripper.disconnect()

# Release services
if not self.destroy_service(self.acknowledge_srv):
return TransitionCallbackReturn.FAILURE
if not self.destroy_service(self.fast_stop_srv):
return TransitionCallbackReturn.FAILURE

return TransitionCallbackReturn.SUCCESS

def on_shutdown(self, state: State) -> TransitionCallbackReturn:
self.get_logger().info("on_shutdown() is called.")
return TransitionCallbackReturn.SUCCESS

def status_update(self):
self.get_logger().info(f"---> Status update: {self.gripper.get_plc_input()}")

# Service callbacks
def _acknowledge_cb(self, request: Trigger.Request, response: Trigger.Response):
self.get_logger().info("---> Acknowledge")
response.success = asyncio.run(self.gripper.acknowledge())
response.message = self.gripper.get_status_diagnostics()
return response

def _fast_stop_cb(self, request: Trigger.Request, response: Trigger.Response):
self.get_logger().info("---> Fast stop")
response.success = asyncio.run(self.gripper.fast_stop())
response.message = self.gripper.get_status_diagnostics()
return response


def main():
rclpy.init()
executor = rclpy.executors.SingleThreadedExecutor()
driver = Driver("driver")
executor.add_node(driver)
try:
executor.spin()
except (KeyboardInterrupt, rclpy.executors.ExternalShutdownException):
driver.destroy_node()


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions schunk_gripper_driver/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[develop]
script_dir=$base/lib/schunk_gripper_driver
[install]
install_scripts=$base/lib/schunk_gripper_driver
27 changes: 27 additions & 0 deletions schunk_gripper_driver/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from setuptools import find_packages, setup
from glob import glob
import os

package_name = "schunk_gripper_driver"

setup(
name=package_name,
version="0.0.0",
packages=find_packages(exclude=["test"]),
data_files=[
("share/ament_index/resource_index/packages", ["resource/" + package_name]),
("share/" + package_name, ["package.xml"]),
(os.path.join("lib", package_name), [package_name + "/driver.py"]),
(os.path.join("share", package_name, "launch"), glob("launch/*.launch.py")),
],
install_requires=["setuptools", "pymodbus"],
zip_safe=True,
maintainer="stefan",
maintainer_email="[email protected]",
description="ROS2 driver for SCHUNK`s EGU, EGK, and EZU mechatronic grippers",
license="GPL-3.0-or-later",
tests_require=["pytest"],
entry_points={
"console_scripts": [],
},
)
11 changes: 11 additions & 0 deletions schunk_gripper_driver/test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Run tests locally
Inside the driver's package

```bash
pip install --user pytest coverage
```

```bash
coverage run -m pytest test/
coverage report
```
Empty file.
Loading