pyrobotiqtactile 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrobotiqtactile-0.1.0/LICENCE +28 -0
- pyrobotiqtactile-0.1.0/PKG-INFO +55 -0
- pyrobotiqtactile-0.1.0/README.md +32 -0
- pyrobotiqtactile-0.1.0/pyproject.toml +40 -0
- pyrobotiqtactile-0.1.0/setup.cfg +4 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile/__init__.py +35 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile/data.py +54 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile/exceptions.py +14 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile/protocol.py +224 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile/sensor.py +204 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile.egg-info/PKG-INFO +55 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile.egg-info/SOURCES.txt +14 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile.egg-info/dependency_links.txt +1 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile.egg-info/requires.txt +2 -0
- pyrobotiqtactile-0.1.0/src/pyrobotiqtactile.egg-info/top_level.txt +1 -0
- pyrobotiqtactile-0.1.0/tests/test_sensor.py +75 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Robotiq
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrobotiqtactile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python support for the Robotiq TSF-85 tactile sensor
|
|
5
|
+
Author: Benoit CASTETS
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/bcastets-robotiq/pyRobotiqTactile
|
|
8
|
+
Project-URL: Repository, https://github.com/bcastets-robotiq/pyRobotiqTactile
|
|
9
|
+
Project-URL: Issues, https://github.com/bcastets-robotiq/pyRobotiqTactile/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENCE
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: pyserial>=3.5
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# pyRobotiqTactile
|
|
25
|
+
|
|
26
|
+
A Python package for communicating with the Robotiq TSF-85 tactile sensor.
|
|
27
|
+
|
|
28
|
+
The project focuses on a public `RobotiqTactileSensor` class that provides:
|
|
29
|
+
|
|
30
|
+
- serial communication setup
|
|
31
|
+
- sensor configuration and autosend control
|
|
32
|
+
- pressure-frame decoding as NumPy arrays
|
|
33
|
+
- accelerometer and gyroscope access
|
|
34
|
+
- a structured `SensorReading` container for the decoded data
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pyrobotiqtactile import RobotiqTactileSensor
|
|
40
|
+
|
|
41
|
+
sensor = RobotiqTactileSensor()
|
|
42
|
+
sensor.connect("COM3")
|
|
43
|
+
sensor.configure(update_period_ms=50, synchronous=True)
|
|
44
|
+
|
|
45
|
+
reading = sensor.read_frame()
|
|
46
|
+
print(reading.pressure.shape)
|
|
47
|
+
print(sensor.get_imu())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -e .
|
|
54
|
+
python -m unittest discover -s tests -v
|
|
55
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pyRobotiqTactile
|
|
2
|
+
|
|
3
|
+
A Python package for communicating with the Robotiq TSF-85 tactile sensor.
|
|
4
|
+
|
|
5
|
+
The project focuses on a public `RobotiqTactileSensor` class that provides:
|
|
6
|
+
|
|
7
|
+
- serial communication setup
|
|
8
|
+
- sensor configuration and autosend control
|
|
9
|
+
- pressure-frame decoding as NumPy arrays
|
|
10
|
+
- accelerometer and gyroscope access
|
|
11
|
+
- a structured `SensorReading` container for the decoded data
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from pyrobotiqtactile import RobotiqTactileSensor
|
|
17
|
+
|
|
18
|
+
sensor = RobotiqTactileSensor()
|
|
19
|
+
sensor.connect("COM3")
|
|
20
|
+
sensor.configure(update_period_ms=50, synchronous=True)
|
|
21
|
+
|
|
22
|
+
reading = sensor.read_frame()
|
|
23
|
+
print(reading.pressure.shape)
|
|
24
|
+
print(sensor.get_imu())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .
|
|
31
|
+
python -m unittest discover -s tests -v
|
|
32
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyrobotiqtactile"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python support for the Robotiq TSF-85 tactile sensor"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy>=1.26",
|
|
13
|
+
"pyserial>=3.5",
|
|
14
|
+
]
|
|
15
|
+
authors = [
|
|
16
|
+
{ name = "Benoit CASTETS" },
|
|
17
|
+
]
|
|
18
|
+
license = "BSD-3-Clause"
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
]
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/bcastets-robotiq/pyRobotiqTactile"
|
|
30
|
+
Repository = "https://github.com/bcastets-robotiq/pyRobotiqTactile"
|
|
31
|
+
Issues = "https://github.com/bcastets-robotiq/pyRobotiqTactile/issues"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
package-dir = {"" = "src"}
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.uv]
|
|
40
|
+
package = true
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Python package for the Robotiq TSF-85 tactile sensor."""
|
|
2
|
+
|
|
3
|
+
from .data import SensorReading
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
RobotiqTactileError,
|
|
6
|
+
SensorConnectionError,
|
|
7
|
+
SensorNotFoundError,
|
|
8
|
+
SensorTimeoutError,
|
|
9
|
+
)
|
|
10
|
+
from .protocol import (
|
|
11
|
+
BAUD_RATE,
|
|
12
|
+
COMMAND_AUTOSEND_ASYNC,
|
|
13
|
+
COMMAND_AUTOSEND_SYNC,
|
|
14
|
+
COMMAND_ENTER_BOOTLOADER,
|
|
15
|
+
COMMAND_GET_VERSION,
|
|
16
|
+
COMMAND_READ_SENSORS,
|
|
17
|
+
NUM_FINGERS,
|
|
18
|
+
)
|
|
19
|
+
from .sensor import RobotiqTactileSensor
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BAUD_RATE",
|
|
23
|
+
"COMMAND_AUTOSEND_ASYNC",
|
|
24
|
+
"COMMAND_AUTOSEND_SYNC",
|
|
25
|
+
"COMMAND_ENTER_BOOTLOADER",
|
|
26
|
+
"COMMAND_GET_VERSION",
|
|
27
|
+
"COMMAND_READ_SENSORS",
|
|
28
|
+
"NUM_FINGERS",
|
|
29
|
+
"RobotiqTactileError",
|
|
30
|
+
"RobotiqTactileSensor",
|
|
31
|
+
"SensorConnectionError",
|
|
32
|
+
"SensorNotFoundError",
|
|
33
|
+
"SensorReading",
|
|
34
|
+
"SensorTimeoutError",
|
|
35
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""numpy-friendly snapshot of a decoded sensor frame."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Sequence
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .protocol import FingerFrame, STATIC_TACTILE_COLS, STATIC_TACTILE_ROWS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SensorReading:
|
|
15
|
+
"""A snapshot of all fingers' sensor values, as numpy arrays.
|
|
16
|
+
|
|
17
|
+
Array shapes are (num_fingers, ...): index by finger id first, e.g.
|
|
18
|
+
``reading.pressure[0]`` is finger 0's 7x4 pressure grid.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pressure: np.ndarray # (F, 7, 4) uint16 — static tactile grid
|
|
22
|
+
dynamic_tactile: np.ndarray # (F,) int16
|
|
23
|
+
accelerometer: np.ndarray # (F, 3) int16
|
|
24
|
+
gyroscope: np.ndarray # (F, 3) int16
|
|
25
|
+
magnetometer: np.ndarray # (F, 3) int16
|
|
26
|
+
temperature: np.ndarray # (F,) int16
|
|
27
|
+
timestamp: np.ndarray # (F,) uint64 — sensor-side timestamp per finger
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_fingers(cls, fingers: Sequence[FingerFrame]) -> "SensorReading":
|
|
31
|
+
num_fingers = len(fingers)
|
|
32
|
+
pressure = np.array(
|
|
33
|
+
[f.static_tactile for f in fingers], dtype=np.uint16
|
|
34
|
+
).reshape(num_fingers, STATIC_TACTILE_ROWS, STATIC_TACTILE_COLS)
|
|
35
|
+
return cls(
|
|
36
|
+
pressure=pressure,
|
|
37
|
+
dynamic_tactile=np.array([f.dynamic_tactile for f in fingers], dtype=np.int16),
|
|
38
|
+
accelerometer=np.array([f.accelerometer for f in fingers], dtype=np.int16),
|
|
39
|
+
gyroscope=np.array([f.gyroscope for f in fingers], dtype=np.int16),
|
|
40
|
+
magnetometer=np.array([f.magnetometer for f in fingers], dtype=np.int16),
|
|
41
|
+
temperature=np.array([f.temperature for f in fingers], dtype=np.int16),
|
|
42
|
+
timestamp=np.array([f.timestamp for f in fingers], dtype=np.uint64),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def copy(self) -> "SensorReading":
|
|
46
|
+
return SensorReading(
|
|
47
|
+
pressure=self.pressure.copy(),
|
|
48
|
+
dynamic_tactile=self.dynamic_tactile.copy(),
|
|
49
|
+
accelerometer=self.accelerometer.copy(),
|
|
50
|
+
gyroscope=self.gyroscope.copy(),
|
|
51
|
+
magnetometer=self.magnetometer.copy(),
|
|
52
|
+
temperature=self.temperature.copy(),
|
|
53
|
+
timestamp=self.timestamp.copy(),
|
|
54
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class RobotiqTactileError(Exception):
|
|
2
|
+
"""Base exception for all pyRobotiqTactile errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SensorNotFoundError(RobotiqTactileError):
|
|
6
|
+
"""Raised when no TSF-85 device could be located on any serial port."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SensorConnectionError(RobotiqTactileError):
|
|
10
|
+
"""Raised when the serial connection to the sensor fails or drops."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SensorTimeoutError(RobotiqTactileError):
|
|
14
|
+
"""Raised when a request to the sensor (e.g. firmware version) times out."""
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level USB packet framing and parsing for the Robotiq TSF-85 tactile sensor.
|
|
3
|
+
|
|
4
|
+
Wire format reverse-ported from Robotiq/tactile_sensors (communicator.cpp /
|
|
5
|
+
sensor_quickstart/protocol.py): each packet is a 4-byte header (start byte,
|
|
6
|
+
CRC8 placeholder, command, data length) followed by a sequence of
|
|
7
|
+
[type+finger byte, big-endian payload] fields.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List, Tuple
|
|
14
|
+
|
|
15
|
+
# Serial link parameters (fixed by the sensor's hardware/firmware)
|
|
16
|
+
BAUD_RATE = 115200
|
|
17
|
+
DATA_BITS = 8
|
|
18
|
+
PARITY = "N"
|
|
19
|
+
STOP_BITS = 1
|
|
20
|
+
|
|
21
|
+
# USB VID:PID pairs used to auto-discover the sensor's serial port
|
|
22
|
+
MASTER_HUB_VID_PID_PAIRS: Tuple[Tuple[int, int], ...] = (
|
|
23
|
+
(0x16D0, 0x14CC), # Robotiq master hub
|
|
24
|
+
(0x04B4, 0xF232), # Cypress USB UART (older units)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Packet framing
|
|
28
|
+
START_BYTE = 0x9A
|
|
29
|
+
HEADER_SIZE = 4
|
|
30
|
+
MAX_DATA_SIZE = 256
|
|
31
|
+
|
|
32
|
+
# Commands
|
|
33
|
+
COMMAND_READ_SENSORS = 0x61
|
|
34
|
+
COMMAND_AUTOSEND_SYNC = 0x58
|
|
35
|
+
COMMAND_AUTOSEND_ASYNC = 0x59
|
|
36
|
+
COMMAND_ENTER_BOOTLOADER = 0xE2
|
|
37
|
+
COMMAND_GET_VERSION = 0xE3
|
|
38
|
+
|
|
39
|
+
# Sensor type identifiers (high nibble of the per-field type byte)
|
|
40
|
+
SENSOR_STATIC_TACTILE = 0x10
|
|
41
|
+
SENSOR_DYNAMIC_TACTILE = 0x20
|
|
42
|
+
SENSOR_ACCELEROMETER = 0x30
|
|
43
|
+
SENSOR_GYROSCOPE = 0x40
|
|
44
|
+
SENSOR_MAGNETOMETER = 0x50
|
|
45
|
+
SENSOR_TEMPERATURE = 0x60
|
|
46
|
+
SENSOR_TIMESTAMP = 0x70
|
|
47
|
+
|
|
48
|
+
# Fixed field sizes (element counts, not bytes)
|
|
49
|
+
STATIC_TACTILE_ROWS = 7
|
|
50
|
+
STATIC_TACTILE_COLS = 4
|
|
51
|
+
STATIC_TACTILE_SIZE = STATIC_TACTILE_ROWS * STATIC_TACTILE_COLS # 28
|
|
52
|
+
DYNAMIC_TACTILE_SIZE = 1
|
|
53
|
+
IMU_AXES = 3
|
|
54
|
+
NUM_FINGERS = 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class FingerFrame:
|
|
59
|
+
"""Raw (non-numpy) values decoded for a single finger from one or more packets."""
|
|
60
|
+
|
|
61
|
+
updated: bool = False
|
|
62
|
+
static_tactile: List[int] = field(default_factory=lambda: [0] * STATIC_TACTILE_SIZE)
|
|
63
|
+
dynamic_tactile: int = 0
|
|
64
|
+
accelerometer: List[int] = field(default_factory=lambda: [0, 0, 0])
|
|
65
|
+
gyroscope: List[int] = field(default_factory=lambda: [0, 0, 0])
|
|
66
|
+
magnetometer: List[int] = field(default_factory=lambda: [0, 0, 0])
|
|
67
|
+
temperature: int = 0
|
|
68
|
+
timestamp: int = 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _extract_uint16(data: bytes, count: int) -> Tuple[List[int], int]:
|
|
72
|
+
values = []
|
|
73
|
+
n = min(len(data), count * 2)
|
|
74
|
+
for i in range(0, n - (n % 2), 2):
|
|
75
|
+
values.append((data[i] << 8) | data[i + 1])
|
|
76
|
+
return values, n - (n % 2)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _extract_int16(data: bytes, count: int) -> Tuple[List[int], int]:
|
|
80
|
+
values, consumed = _extract_uint16(data, count)
|
|
81
|
+
return [v - 65536 if v >= 32768 else v for v in values], consumed
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _extract_uint64(data: bytes, count: int) -> Tuple[List[int], int]:
|
|
85
|
+
values = []
|
|
86
|
+
n = min((len(data) // 8) * 8, count * 8)
|
|
87
|
+
for i in range(0, n, 8):
|
|
88
|
+
value = 0
|
|
89
|
+
for b in data[i : i + 8]:
|
|
90
|
+
value = (value << 8) | b
|
|
91
|
+
values.append(value)
|
|
92
|
+
return values, n
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PacketParser:
|
|
96
|
+
"""Buffers raw serial bytes into complete packets and decodes sensor fields."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, num_fingers: int = NUM_FINGERS):
|
|
99
|
+
self._buffer = bytearray()
|
|
100
|
+
self.num_fingers = num_fingers
|
|
101
|
+
self.fingers: List[FingerFrame] = [FingerFrame() for _ in range(num_fingers)]
|
|
102
|
+
self.packet_count = 0
|
|
103
|
+
|
|
104
|
+
# -- outgoing commands ------------------------------------------------
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def build_command(command: int, data: bytes = b"") -> bytes:
|
|
108
|
+
if len(data) > MAX_DATA_SIZE:
|
|
109
|
+
raise ValueError(f"Data too large: {len(data)} > {MAX_DATA_SIZE}")
|
|
110
|
+
packet = bytearray([START_BYTE, 0x00, command, len(data)])
|
|
111
|
+
packet.extend(data)
|
|
112
|
+
return bytes(packet)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def build_get_version_command(cls) -> bytes:
|
|
116
|
+
return cls.build_command(COMMAND_GET_VERSION)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def build_autosend_command(cls, period_ms: int, synchronous: bool = True) -> bytes:
|
|
120
|
+
if not 0 <= period_ms <= 255:
|
|
121
|
+
raise ValueError(f"period_ms must be in [0, 255], got {period_ms}")
|
|
122
|
+
command = COMMAND_AUTOSEND_SYNC if synchronous else COMMAND_AUTOSEND_ASYNC
|
|
123
|
+
return cls.build_command(command, bytes([period_ms]))
|
|
124
|
+
|
|
125
|
+
# -- incoming bytes -----------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def feed(self, data: bytes) -> List[bytes]:
|
|
128
|
+
"""Feed raw serial bytes; returns any complete packets found (header+payload)."""
|
|
129
|
+
self._buffer.extend(data)
|
|
130
|
+
packets: List[bytes] = []
|
|
131
|
+
|
|
132
|
+
while len(self._buffer) >= HEADER_SIZE:
|
|
133
|
+
start = self._buffer.find(START_BYTE)
|
|
134
|
+
if start == -1:
|
|
135
|
+
self._buffer.clear()
|
|
136
|
+
break
|
|
137
|
+
if start > 0:
|
|
138
|
+
del self._buffer[:start]
|
|
139
|
+
if len(self._buffer) < HEADER_SIZE:
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
data_length = self._buffer[3]
|
|
143
|
+
packet_size = HEADER_SIZE + data_length
|
|
144
|
+
if len(self._buffer) < packet_size:
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
packets.append(bytes(self._buffer[:packet_size]))
|
|
148
|
+
del self._buffer[:packet_size]
|
|
149
|
+
self.packet_count += 1
|
|
150
|
+
|
|
151
|
+
return packets
|
|
152
|
+
|
|
153
|
+
def reset_buffer(self) -> None:
|
|
154
|
+
self._buffer.clear()
|
|
155
|
+
|
|
156
|
+
def parse_sensor_packet(self, packet: bytes) -> List[bool]:
|
|
157
|
+
"""Decode a sensor-data packet, updating self.fingers in place.
|
|
158
|
+
|
|
159
|
+
Returns one bool per finger: True if that finger received a field in
|
|
160
|
+
this packet (mirrors upstream's `newDataAvailable` semantics).
|
|
161
|
+
"""
|
|
162
|
+
if len(packet) < HEADER_SIZE:
|
|
163
|
+
return [False] * self.num_fingers
|
|
164
|
+
|
|
165
|
+
data = packet[HEADER_SIZE:]
|
|
166
|
+
idx = 0
|
|
167
|
+
n = len(data)
|
|
168
|
+
|
|
169
|
+
while idx < n:
|
|
170
|
+
sensor_byte = data[idx]
|
|
171
|
+
idx += 1
|
|
172
|
+
sensor_type = sensor_byte & 0xF0
|
|
173
|
+
finger_id = (sensor_byte >> 2) & 0x03
|
|
174
|
+
if finger_id >= self.num_fingers:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
finger = self.fingers[finger_id]
|
|
178
|
+
finger.updated = True
|
|
179
|
+
remaining = data[idx:]
|
|
180
|
+
|
|
181
|
+
if sensor_type == SENSOR_STATIC_TACTILE:
|
|
182
|
+
values, consumed = _extract_uint16(remaining, STATIC_TACTILE_SIZE)
|
|
183
|
+
if len(values) == STATIC_TACTILE_SIZE:
|
|
184
|
+
finger.static_tactile = values
|
|
185
|
+
idx += consumed
|
|
186
|
+
elif sensor_type == SENSOR_DYNAMIC_TACTILE:
|
|
187
|
+
values, consumed = _extract_int16(remaining, DYNAMIC_TACTILE_SIZE)
|
|
188
|
+
if values:
|
|
189
|
+
finger.dynamic_tactile = values[0]
|
|
190
|
+
idx += consumed
|
|
191
|
+
elif sensor_type == SENSOR_ACCELEROMETER:
|
|
192
|
+
values, consumed = _extract_int16(remaining, IMU_AXES)
|
|
193
|
+
if len(values) == IMU_AXES:
|
|
194
|
+
finger.accelerometer = values
|
|
195
|
+
idx += consumed
|
|
196
|
+
elif sensor_type == SENSOR_GYROSCOPE:
|
|
197
|
+
values, consumed = _extract_int16(remaining, IMU_AXES)
|
|
198
|
+
if len(values) == IMU_AXES:
|
|
199
|
+
finger.gyroscope = values
|
|
200
|
+
idx += consumed
|
|
201
|
+
elif sensor_type == SENSOR_MAGNETOMETER:
|
|
202
|
+
values, consumed = _extract_int16(remaining, IMU_AXES)
|
|
203
|
+
if len(values) == IMU_AXES:
|
|
204
|
+
finger.magnetometer = values
|
|
205
|
+
idx += consumed
|
|
206
|
+
elif sensor_type == SENSOR_TEMPERATURE:
|
|
207
|
+
values, consumed = _extract_int16(remaining, 1)
|
|
208
|
+
if values:
|
|
209
|
+
finger.temperature = values[0]
|
|
210
|
+
idx += consumed
|
|
211
|
+
elif sensor_type == SENSOR_TIMESTAMP:
|
|
212
|
+
values, consumed = _extract_uint64(remaining, 1)
|
|
213
|
+
if values:
|
|
214
|
+
finger.timestamp = values[0]
|
|
215
|
+
idx += consumed
|
|
216
|
+
else:
|
|
217
|
+
# Unknown field type: stop, we can't know its size.
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
return [f.updated for f in self.fingers]
|
|
221
|
+
|
|
222
|
+
def clear_updated_flags(self) -> None:
|
|
223
|
+
for f in self.fingers:
|
|
224
|
+
f.updated = False
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""High-level Python interface for the Robotiq TSF-85 tactile sensor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .data import SensorReading
|
|
11
|
+
from .exceptions import SensorConnectionError, SensorNotFoundError
|
|
12
|
+
from .protocol import BAUD_RATE, PacketParser, NUM_FINGERS
|
|
13
|
+
|
|
14
|
+
try: # pragma: no cover - optional dependency
|
|
15
|
+
import serial # type: ignore
|
|
16
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
17
|
+
serial = None # type: ignore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["RobotiqTactileSensor"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RobotiqTactileSensor:
|
|
24
|
+
"""Convenience wrapper around the low-level TSF-85 packet protocol.
|
|
25
|
+
|
|
26
|
+
The class exposes the main operations requested for a Python package:
|
|
27
|
+
- connect/setup communication,
|
|
28
|
+
- configure the sensor's autosend loop,
|
|
29
|
+
- retrieve pressure as a NumPy array,
|
|
30
|
+
- retrieve accelerometer and gyroscope data,
|
|
31
|
+
- expose the full decoded frame as a :class:`SensorReading`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
serial_port: Optional[object] = None,
|
|
37
|
+
port: Optional[str] = None,
|
|
38
|
+
baud_rate: int = BAUD_RATE,
|
|
39
|
+
timeout: float = 0.5,
|
|
40
|
+
num_fingers: int = NUM_FINGERS,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.serial_port = serial_port
|
|
43
|
+
self.port = port
|
|
44
|
+
self.baud_rate = baud_rate
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
self.parser = PacketParser(num_fingers=num_fingers)
|
|
47
|
+
self._latest_reading: Optional[SensorReading] = None
|
|
48
|
+
self._connected = False
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> "RobotiqTactileSensor":
|
|
51
|
+
self.connect()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
def discover_ports(self) -> list[str]:
|
|
58
|
+
"""Return a list of currently available serial ports."""
|
|
59
|
+
if serial is None:
|
|
60
|
+
return []
|
|
61
|
+
return [port.device for port in serial.tools.list_ports.comports()]
|
|
62
|
+
|
|
63
|
+
def connect(self, port: Optional[str] = None, baud_rate: Optional[int] = None) -> "RobotiqTactileSensor":
|
|
64
|
+
"""Open the serial connection to the sensor.
|
|
65
|
+
|
|
66
|
+
When ``serial_port`` was passed in at construction time, it is reused.
|
|
67
|
+
Otherwise this will try to auto-detect a port and use :mod:`pyserial`.
|
|
68
|
+
"""
|
|
69
|
+
if self.serial_port is not None:
|
|
70
|
+
if not getattr(self.serial_port, "is_open", True):
|
|
71
|
+
if hasattr(self.serial_port, "open"):
|
|
72
|
+
self.serial_port.open()
|
|
73
|
+
self._connected = True
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
if serial is None:
|
|
77
|
+
raise ImportError("pyserial is required to open a hardware connection")
|
|
78
|
+
|
|
79
|
+
if port is None:
|
|
80
|
+
port = self.port
|
|
81
|
+
if port is None:
|
|
82
|
+
candidates = self.discover_ports()
|
|
83
|
+
if not candidates:
|
|
84
|
+
raise SensorNotFoundError("No serial port discovered for the TSF-85 sensor")
|
|
85
|
+
port = candidates[0]
|
|
86
|
+
|
|
87
|
+
if baud_rate is None:
|
|
88
|
+
baud_rate = self.baud_rate
|
|
89
|
+
|
|
90
|
+
self.serial_port = serial.Serial(port=port, baudrate=baud_rate, timeout=self.timeout)
|
|
91
|
+
self._connected = True
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def setup_communication(self, port: Optional[str] = None, baud_rate: Optional[int] = None) -> "RobotiqTactileSensor":
|
|
95
|
+
"""Alias for :meth:`connect` that matches the package's public API."""
|
|
96
|
+
return self.connect(port=port, baud_rate=baud_rate)
|
|
97
|
+
|
|
98
|
+
def close(self) -> None:
|
|
99
|
+
"""Close the underlying serial connection if it exists."""
|
|
100
|
+
if self.serial_port is not None and hasattr(self.serial_port, "close"):
|
|
101
|
+
self.serial_port.close()
|
|
102
|
+
self._connected = False
|
|
103
|
+
|
|
104
|
+
def configure(self, update_period_ms: int = 50, synchronous: bool = True) -> "RobotiqTactileSensor":
|
|
105
|
+
"""Configure the sensor to stream data using the upstream autosend command.
|
|
106
|
+
|
|
107
|
+
The Robotiq firmware exposes several sensor settings; this wrapper
|
|
108
|
+
currently exposes the most useful general-purpose configuration option:
|
|
109
|
+
the autosend update period.
|
|
110
|
+
"""
|
|
111
|
+
self._ensure_connected()
|
|
112
|
+
command = PacketParser.build_autosend_command(update_period_ms, synchronous=synchronous)
|
|
113
|
+
self._write(command)
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def get_version(self) -> Optional[bytes]:
|
|
117
|
+
"""Request the firmware version and return the raw response payload."""
|
|
118
|
+
self._ensure_connected()
|
|
119
|
+
self._write(PacketParser.build_get_version_command())
|
|
120
|
+
return self._read_response(timeout=self.timeout)
|
|
121
|
+
|
|
122
|
+
def read_frame(self, timeout: Optional[float] = None) -> SensorReading:
|
|
123
|
+
"""Read one decoded sensor frame and return it as :class:`SensorReading`."""
|
|
124
|
+
self._ensure_connected()
|
|
125
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
126
|
+
|
|
127
|
+
while True:
|
|
128
|
+
data = self._read_bytes()
|
|
129
|
+
if not data:
|
|
130
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
131
|
+
break
|
|
132
|
+
time.sleep(0.001)
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
for packet in self.parser.feed(data):
|
|
136
|
+
self.parser.parse_sensor_packet(packet)
|
|
137
|
+
|
|
138
|
+
if self.parser.packet_count:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
self.parser.clear_updated_flags()
|
|
145
|
+
reading = SensorReading.from_fingers(self.parser.fingers)
|
|
146
|
+
self._latest_reading = reading
|
|
147
|
+
return reading
|
|
148
|
+
|
|
149
|
+
def get_pressure(self) -> np.ndarray:
|
|
150
|
+
"""Return the latest static pressure grid as a NumPy array."""
|
|
151
|
+
return self.read_frame().pressure
|
|
152
|
+
|
|
153
|
+
def get_accelerometer(self) -> np.ndarray:
|
|
154
|
+
"""Return the latest accelerometer values as a NumPy array."""
|
|
155
|
+
return self.read_frame().accelerometer
|
|
156
|
+
|
|
157
|
+
def get_gyroscope(self) -> np.ndarray:
|
|
158
|
+
"""Return the latest gyroscope values as a NumPy array."""
|
|
159
|
+
return self.read_frame().gyroscope
|
|
160
|
+
|
|
161
|
+
def get_imu(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
162
|
+
"""Return the latest accelerometer and gyroscope values as a tuple."""
|
|
163
|
+
reading = self.read_frame()
|
|
164
|
+
return reading.accelerometer, reading.gyroscope
|
|
165
|
+
|
|
166
|
+
def get_latest_reading(self) -> Optional[SensorReading]:
|
|
167
|
+
"""Return the most recently decoded frame, if any."""
|
|
168
|
+
return self._latest_reading
|
|
169
|
+
|
|
170
|
+
def _ensure_connected(self) -> None:
|
|
171
|
+
if not self._connected:
|
|
172
|
+
self.connect()
|
|
173
|
+
|
|
174
|
+
def _write(self, payload: bytes) -> None:
|
|
175
|
+
if self.serial_port is None:
|
|
176
|
+
raise SensorConnectionError("The sensor is not connected")
|
|
177
|
+
if getattr(self.serial_port, "is_open", True) is False:
|
|
178
|
+
raise SensorConnectionError("The sensor serial connection is closed")
|
|
179
|
+
|
|
180
|
+
written = self.serial_port.write(payload)
|
|
181
|
+
if written != len(payload):
|
|
182
|
+
raise SensorConnectionError("Failed to write the full payload to the sensor")
|
|
183
|
+
|
|
184
|
+
def _read_bytes(self) -> bytes:
|
|
185
|
+
if self.serial_port is None:
|
|
186
|
+
raise SensorConnectionError("The sensor is not connected")
|
|
187
|
+
|
|
188
|
+
if getattr(self.serial_port, "is_open", True) is False:
|
|
189
|
+
raise SensorConnectionError("The sensor serial connection is closed")
|
|
190
|
+
|
|
191
|
+
if hasattr(self.serial_port, "in_waiting"):
|
|
192
|
+
if self.serial_port.in_waiting > 0:
|
|
193
|
+
return self.serial_port.read(self.serial_port.in_waiting)
|
|
194
|
+
|
|
195
|
+
return self.serial_port.read(1)
|
|
196
|
+
|
|
197
|
+
def _read_response(self, timeout: float) -> Optional[bytes]:
|
|
198
|
+
deadline = time.monotonic() + timeout
|
|
199
|
+
while time.monotonic() < deadline:
|
|
200
|
+
data = self._read_bytes()
|
|
201
|
+
if data:
|
|
202
|
+
return data
|
|
203
|
+
time.sleep(0.001)
|
|
204
|
+
return None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrobotiqtactile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python support for the Robotiq TSF-85 tactile sensor
|
|
5
|
+
Author: Benoit CASTETS
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/bcastets-robotiq/pyRobotiqTactile
|
|
8
|
+
Project-URL: Repository, https://github.com/bcastets-robotiq/pyRobotiqTactile
|
|
9
|
+
Project-URL: Issues, https://github.com/bcastets-robotiq/pyRobotiqTactile/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENCE
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: pyserial>=3.5
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# pyRobotiqTactile
|
|
25
|
+
|
|
26
|
+
A Python package for communicating with the Robotiq TSF-85 tactile sensor.
|
|
27
|
+
|
|
28
|
+
The project focuses on a public `RobotiqTactileSensor` class that provides:
|
|
29
|
+
|
|
30
|
+
- serial communication setup
|
|
31
|
+
- sensor configuration and autosend control
|
|
32
|
+
- pressure-frame decoding as NumPy arrays
|
|
33
|
+
- accelerometer and gyroscope access
|
|
34
|
+
- a structured `SensorReading` container for the decoded data
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from pyrobotiqtactile import RobotiqTactileSensor
|
|
40
|
+
|
|
41
|
+
sensor = RobotiqTactileSensor()
|
|
42
|
+
sensor.connect("COM3")
|
|
43
|
+
sensor.configure(update_period_ms=50, synchronous=True)
|
|
44
|
+
|
|
45
|
+
reading = sensor.read_frame()
|
|
46
|
+
print(reading.pressure.shape)
|
|
47
|
+
print(sensor.get_imu())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -e .
|
|
54
|
+
python -m unittest discover -s tests -v
|
|
55
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENCE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pyrobotiqtactile/__init__.py
|
|
5
|
+
src/pyrobotiqtactile/data.py
|
|
6
|
+
src/pyrobotiqtactile/exceptions.py
|
|
7
|
+
src/pyrobotiqtactile/protocol.py
|
|
8
|
+
src/pyrobotiqtactile/sensor.py
|
|
9
|
+
src/pyrobotiqtactile.egg-info/PKG-INFO
|
|
10
|
+
src/pyrobotiqtactile.egg-info/SOURCES.txt
|
|
11
|
+
src/pyrobotiqtactile.egg-info/dependency_links.txt
|
|
12
|
+
src/pyrobotiqtactile.egg-info/requires.txt
|
|
13
|
+
src/pyrobotiqtactile.egg-info/top_level.txt
|
|
14
|
+
tests/test_sensor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyrobotiqtactile
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from pyrobotiqtactile import RobotiqTactileSensor
|
|
4
|
+
from pyrobotiqtactile.protocol import (
|
|
5
|
+
IMU_AXES,
|
|
6
|
+
SENSOR_ACCELEROMETER,
|
|
7
|
+
SENSOR_STATIC_TACTILE,
|
|
8
|
+
STATIC_TACTILE_COLS,
|
|
9
|
+
STATIC_TACTILE_ROWS,
|
|
10
|
+
STATIC_TACTILE_SIZE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeSerial:
|
|
15
|
+
def __init__(self, packets):
|
|
16
|
+
self._packets = list(packets)
|
|
17
|
+
self.writes = []
|
|
18
|
+
self._is_open = True
|
|
19
|
+
|
|
20
|
+
def write(self, data):
|
|
21
|
+
self.writes.append(data)
|
|
22
|
+
return len(data)
|
|
23
|
+
|
|
24
|
+
def read(self, size=1):
|
|
25
|
+
if self._packets:
|
|
26
|
+
return self._packets.pop(0)
|
|
27
|
+
return b""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def in_waiting(self):
|
|
31
|
+
return len(self._packets)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_open(self):
|
|
35
|
+
return self._is_open
|
|
36
|
+
|
|
37
|
+
def close(self):
|
|
38
|
+
self._is_open = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RobotiqTactileSensorTests(unittest.TestCase):
|
|
42
|
+
def test_read_frame_returns_pressure_and_imu_arrays(self):
|
|
43
|
+
static_payload = bytearray()
|
|
44
|
+
for value in range(STATIC_TACTILE_SIZE):
|
|
45
|
+
static_payload.append(((value + 1) & 0xFF00) >> 8)
|
|
46
|
+
static_payload.append((value + 1) & 0xFF)
|
|
47
|
+
|
|
48
|
+
accel_payload = bytearray()
|
|
49
|
+
for value in (1, -2, 3):
|
|
50
|
+
accel_payload.append(((value & 0xFFFF) >> 8) & 0xFF)
|
|
51
|
+
accel_payload.append(value & 0xFF)
|
|
52
|
+
|
|
53
|
+
payload = bytes([SENSOR_STATIC_TACTILE | 0x00]) + bytes(static_payload) + bytes([SENSOR_ACCELEROMETER | 0x00]) + bytes(accel_payload)
|
|
54
|
+
packet = bytes([0x9A, 0x00, 0x61, len(payload)]) + payload
|
|
55
|
+
|
|
56
|
+
sensor = RobotiqTactileSensor(serial_port=FakeSerial([packet]))
|
|
57
|
+
reading = sensor.read_frame()
|
|
58
|
+
|
|
59
|
+
self.assertEqual(reading.pressure.shape, (2, STATIC_TACTILE_ROWS, STATIC_TACTILE_COLS))
|
|
60
|
+
self.assertEqual(reading.accelerometer.shape, (2, IMU_AXES))
|
|
61
|
+
self.assertEqual(reading.gyroscope.shape, (2, IMU_AXES))
|
|
62
|
+
self.assertEqual(reading.pressure[0, 0, 0], 1)
|
|
63
|
+
self.assertEqual(reading.accelerometer[0, 0], 1)
|
|
64
|
+
self.assertEqual(reading.accelerometer[0, 1], -2)
|
|
65
|
+
|
|
66
|
+
def test_configure_sends_autosend_command(self):
|
|
67
|
+
sensor = RobotiqTactileSensor(serial_port=FakeSerial([]))
|
|
68
|
+
sensor.configure(update_period_ms=50, synchronous=True)
|
|
69
|
+
|
|
70
|
+
self.assertTrue(sensor.serial_port.writes)
|
|
71
|
+
self.assertEqual(sensor.serial_port.writes[-1][2], 0x58)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
unittest.main()
|