Skip to content

Commit 7297f98

Browse files
Merge pull request #17 from SCHUNK-SE-Co-KG/highlevel-api
Design highlevel API
2 parents 125bad3 + 3804d57 commit 7297f98

File tree

9 files changed

+239
-82
lines changed

9 files changed

+239
-82
lines changed

schunk_gripper_driver/launch/driver.launch.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
port = DeclareLaunchArgument(
2424
"port",
25-
default_value="/dev/ur-ttylink/ttyTool",
25+
default_value="/dev/ttyUSB0",
2626
description="The gripper's serial port",
2727
)
2828
device_id = DeclareLaunchArgument(

schunk_gripper_driver/package.xml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<depend>launch</depend>
1212
<depend>launch_ros</depend>
1313
<depend>schunk_gripper_library</depend>
14+
<depend>std_srvs</depend>
1415

1516
<test_depend>launch_pytest</test_depend>
1617
<test_depend>ament_copyright</test_depend>

schunk_gripper_driver/schunk_gripper_driver/driver.py

+3-47
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from rclpy.lifecycle import TransitionCallbackReturn
2323
from schunk_gripper_library.driver import Driver as GripperDriver
2424
from std_srvs.srv import Trigger
25-
import time
25+
import asyncio
2626

2727

2828
class Driver(Node):
@@ -87,62 +87,18 @@ def on_shutdown(self, state: State) -> TransitionCallbackReturn:
8787
return TransitionCallbackReturn.SUCCESS
8888

8989
def status_update(self):
90-
self.gripper.receive_plc_input()
9190
self.get_logger().info(f"---> Status update: {self.gripper.get_plc_input()}")
9291

9392
# Service callbacks
9493
def _acknowledge_cb(self, request: Trigger.Request, response: Trigger.Response):
9594
self.get_logger().info("---> Acknowledge")
96-
# Reset
97-
self.gripper.clear_plc_output()
98-
self.gripper.set_control_bit(bit=0, value=True)
99-
self.gripper.send_plc_output()
100-
time.sleep(0.5)
101-
# Acknowledge
102-
self.gripper.set_control_bit(bit=2, value=True)
103-
for bit in self.gripper.valid_control_bits:
104-
self.get_logger().info(
105-
f"---> Control bit {bit}: {self.gripper.get_control_bit(bit=bit)}"
106-
)
107-
cmd_before = self.gripper.get_status_bit(bit=5)
108-
self.gripper.send_plc_output()
109-
time.sleep(0.5)
110-
self.gripper.receive_plc_input()
111-
cmd_after = self.gripper.get_status_bit(bit=5)
112-
self.get_logger().info(f"---> PLC input: {self.gripper.get_plc_input()}")
113-
for bit in self.gripper.valid_status_bits:
114-
self.get_logger().info(
115-
f"---> Status bit {bit}: {self.gripper.get_status_bit(bit=bit)}"
116-
)
117-
118-
response.success = self.gripper.get_status_bit(bit=0) == 1 and (
119-
cmd_after != cmd_before
120-
)
95+
response.success = asyncio.run(self.gripper.acknowledge())
12196
response.message = self.gripper.get_status_diagnostics()
12297
return response
12398

12499
def _fast_stop_cb(self, request: Trigger.Request, response: Trigger.Response):
125100
self.get_logger().info("---> Fast stop")
126-
self.gripper.clear_plc_output()
127-
self.gripper.set_control_bit(bit=0, value=False)
128-
for bit in self.gripper.valid_control_bits:
129-
self.get_logger().info(
130-
f"---> Control bit {bit}: {self.gripper.get_control_bit(bit=bit)}"
131-
)
132-
cmd_before = self.gripper.get_status_bit(bit=5)
133-
self.gripper.send_plc_output()
134-
time.sleep(0.5)
135-
self.gripper.receive_plc_input()
136-
cmd_after = self.gripper.get_status_bit(bit=5)
137-
self.get_logger().info(f"---> PLC input: {self.gripper.get_plc_input()}")
138-
for bit in self.gripper.valid_status_bits:
139-
self.get_logger().info(
140-
f"---> Status bit {bit}: {self.gripper.get_status_bit(bit=bit)}"
141-
)
142-
143-
response.success = self.gripper.get_status_bit(bit=4) == 1 and (
144-
cmd_after != cmd_before
145-
)
101+
response.success = asyncio.run(self.gripper.fast_stop())
146102
response.message = self.gripper.get_status_diagnostics()
147103
return response
148104

schunk_gripper_driver/test/test_services.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ def test_driver_implements_acknowledge(lifecycle_interface):
4949
client.wait_for_service(timeout_sec=2)
5050
future = client.call_async(Trigger.Request())
5151
rclpy.spin_until_future_complete(node, future)
52+
lifecycle_interface.change_state(Transition.TRANSITION_CLEANUP)
5253

5354
assert future.result().success
54-
expected_msg = "error_code: 0, warning_code: 0, additional_code: 0" # everything ok
55+
expected_msg = (
56+
"error_code: 0x0, warning_code: 0x0, additional_code: 0x0" # everything ok
57+
)
5558
assert future.result().message == expected_msg
56-
lifecycle_interface.change_state(Transition.TRANSITION_CLEANUP)
5759

5860

5961
@skip_without_gripper
@@ -65,8 +67,8 @@ def test_driver_implements_fast_stop(lifecycle_interface):
6567
client.wait_for_service(timeout_sec=2)
6668
future = client.call_async(Trigger.Request())
6769
rclpy.spin_until_future_complete(node, future)
70+
lifecycle_interface.change_state(Transition.TRANSITION_CLEANUP)
6871

6972
assert future.result().success
70-
expected_msg = "error_code: 0, warning_code: 0, additional_code: 0" # everything ok
73+
expected_msg = "error_code: 0xD9, warning_code: 0x0, additional_code: 0x0"
7174
assert future.result().message == expected_msg
72-
lifecycle_interface.change_state(Transition.TRANSITION_CLEANUP)

schunk_gripper_library/schunk_gripper_library/driver.py

+83-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from pymodbus.client import ModbusSerialClient
44
from pymodbus.pdu import ModbusPDU
55
import re
6+
from threading import Thread
7+
import asyncio
8+
import time
69

710

811
class Driver(object):
@@ -31,8 +34,16 @@ def __init__(self) -> None:
3134
self.mb_client: ModbusSerialClient | None = None
3235
self.mb_device_id: int | None = None
3336
self.connected: bool = False
34-
35-
def connect(self, protocol: str, port: str, device_id: int | None = None) -> bool:
37+
self.polling_thread: Thread = Thread()
38+
self.update_cycle: float = 0.05 # sec
39+
40+
def connect(
41+
self,
42+
protocol: str,
43+
port: str,
44+
device_id: int | None = None,
45+
update_cycle: float = 0.05,
46+
) -> bool:
3647
if protocol not in ["modbus"]:
3748
return False
3849
if not isinstance(port, str):
@@ -43,6 +54,8 @@ def connect(self, protocol: str, port: str, device_id: int | None = None) -> boo
4354
return False
4455
if isinstance(device_id, int) and device_id < 0:
4556
return False
57+
if update_cycle < 0.001:
58+
return False
4659
if self.connected:
4760
return False
4861

@@ -58,14 +71,55 @@ def connect(self, protocol: str, port: str, device_id: int | None = None) -> boo
5871
trace_pdu=self._trace_pdu,
5972
)
6073
self.connected = self.mb_client.connect()
74+
75+
if self.connected:
76+
self.update_cycle = update_cycle
77+
self.polling_thread = Thread(
78+
target=asyncio.run,
79+
args=(self._module_update(self.update_cycle),),
80+
daemon=True,
81+
)
82+
self.polling_thread.start()
83+
6184
return self.connected
6285

6386
def disconnect(self) -> bool:
87+
self.connected = False
88+
if self.polling_thread.is_alive():
89+
self.polling_thread.join()
90+
6491
if self.mb_client and self.mb_client.connected:
6592
self.mb_client.close()
66-
self.connected = False
6793
return True
6894

95+
async def acknowledge(self) -> bool:
96+
if not self.connected:
97+
return False
98+
self.clear_plc_output()
99+
self.send_plc_output()
100+
101+
cmd_toggle_before = self.get_status_bit(bit=5)
102+
self.set_control_bit(bit=2, value=True)
103+
self.send_plc_output()
104+
105+
desired_bits = {"0": 1, "5": cmd_toggle_before ^ 1}
106+
return await self.wait_for_status(bits=desired_bits)
107+
108+
async def fast_stop(self) -> bool:
109+
if not self.connected:
110+
return False
111+
self.clear_plc_output()
112+
self.send_plc_output()
113+
114+
cmd_toggle_before = self.get_status_bit(bit=5)
115+
self.set_control_bit(
116+
bit=0, value=False
117+
) # activate fast stop (inverted behavior)
118+
self.send_plc_output()
119+
desired_bits = {"5": cmd_toggle_before ^ 1, "7": 1}
120+
print(f"hello stefan: {self.get_plc_input()}")
121+
return await self.wait_for_status(bits=desired_bits)
122+
69123
def send_plc_output(self) -> bool:
70124
if self.mb_client and self.mb_client.connected:
71125
with self.output_buffer_lock:
@@ -102,6 +156,23 @@ def receive_plc_input(self) -> bool:
102156
return True
103157
return False
104158

159+
async def wait_for_status(
160+
self, bits: dict[str, int] = {}, timeout_sec: float = 1.0
161+
) -> bool:
162+
if not timeout_sec > 0.0:
163+
return False
164+
if not bits:
165+
return False
166+
max_duration = time.time() + timeout_sec
167+
while not all(
168+
[self.get_status_bit(int(bit)) == value for bit, value in bits.items()]
169+
):
170+
await asyncio.sleep(0.001)
171+
self.receive_plc_input()
172+
if time.time() > max_duration:
173+
return False
174+
return True
175+
105176
def contains_non_hex_chars(self, buffer: str) -> bool:
106177
return bool(re.search(r"[^0-9a-fA-F]", buffer))
107178

@@ -132,8 +203,10 @@ def get_plc_output(self) -> str:
132203
return self.plc_output_buffer.hex().upper()
133204

134205
def clear_plc_output(self) -> None:
135-
with self.output_buffer_lock:
136-
self.plc_output_buffer = bytearray(bytes.fromhex("00" * 16))
206+
self.set_plc_output("00" * 16)
207+
self.set_control_bit(
208+
bit=0, value=True
209+
) # deactivate fast stop (inverted behavior)
137210

138211
def set_control_bit(self, bit: int, value: bool) -> bool:
139212
with self.output_buffer_lock:
@@ -251,6 +324,11 @@ def _set_status_bit(self, bit: int, value: bool) -> bool:
251324
self.plc_input_buffer[byte_index] &= ~(1 << bit_index)
252325
return True
253326

327+
async def _module_update(self, update_cycle: float) -> None:
328+
while self.connected:
329+
self.receive_plc_input()
330+
await asyncio.sleep(update_cycle)
331+
254332
def _trace_packet(self, sending: bool, data: bytes) -> bytes:
255333
txt = "REQUEST stream" if sending else "RESPONSE stream"
256334
print(f"---> {txt}: {data!r}")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from ..schunk_gripper_library.driver import Driver
2+
from ..tests.conftest import skip_without_gripper
3+
import asyncio
4+
5+
6+
@skip_without_gripper
7+
def test_acknowledge():
8+
driver = Driver()
9+
10+
# Not connected
11+
assert not asyncio.run(driver.acknowledge())
12+
13+
# Connected
14+
driver.connect("modbus", "/dev/ttyUSB0", 12)
15+
assert asyncio.run(driver.acknowledge())
16+
17+
# Repetitive
18+
for _ in range(5):
19+
assert asyncio.run(driver.acknowledge())
20+
21+
driver.disconnect()
22+
23+
24+
@skip_without_gripper
25+
def test_fast_stop():
26+
driver = Driver()
27+
28+
# Not connected
29+
assert not asyncio.run(driver.fast_stop())
30+
31+
# After fresh start
32+
driver.connect("modbus", "/dev/ttyUSB0", 12)
33+
assert asyncio.run(driver.fast_stop())
34+
35+
# From operational
36+
assert asyncio.run(driver.acknowledge())
37+
assert asyncio.run(driver.fast_stop())
38+
39+
# Repetitive
40+
for _ in range(5):
41+
assert asyncio.run(driver.fast_stop())
42+
43+
driver.disconnect()

0 commit comments

Comments
 (0)