pymyarm 0.0.3__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.
- pymyarm-0.0.3/LICENSE +21 -0
- pymyarm-0.0.3/PKG-INFO +96 -0
- pymyarm-0.0.3/README.md +74 -0
- pymyarm-0.0.3/pymyarm/__init__.py +36 -0
- pymyarm-0.0.3/pymyarm/constants.py +89 -0
- pymyarm-0.0.3/pymyarm/conversion.py +56 -0
- pymyarm-0.0.3/pymyarm/exceptions.py +44 -0
- pymyarm-0.0.3/pymyarm/joint_config.py +98 -0
- pymyarm-0.0.3/pymyarm/myarm_sts.py +583 -0
- pymyarm-0.0.3/pymyarm/port.py +156 -0
- pymyarm-0.0.3/pymyarm/protocol.py +208 -0
- pymyarm-0.0.3/pymyarm/servo_controller.py +340 -0
- pymyarm-0.0.3/pymyarm/sts.py +120 -0
- pymyarm-0.0.3/pymyarm.egg-info/PKG-INFO +96 -0
- pymyarm-0.0.3/pymyarm.egg-info/SOURCES.txt +23 -0
- pymyarm-0.0.3/pymyarm.egg-info/dependency_links.txt +1 -0
- pymyarm-0.0.3/pymyarm.egg-info/requires.txt +7 -0
- pymyarm-0.0.3/pymyarm.egg-info/top_level.txt +1 -0
- pymyarm-0.0.3/pyproject.toml +61 -0
- pymyarm-0.0.3/setup.cfg +4 -0
- pymyarm-0.0.3/tests/test_conversion.py +13 -0
- pymyarm-0.0.3/tests/test_joint_config.py +17 -0
- pymyarm-0.0.3/tests/test_myarm_sts.py +240 -0
- pymyarm-0.0.3/tests/test_no_response_error.py +102 -0
- pymyarm-0.0.3/tests/test_protocol.py +86 -0
pymyarm-0.0.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yunzhe Xue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pymyarm-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymyarm
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: pymycobot-compatible API for Feetech STS servo-based robotic arms
|
|
5
|
+
Author-email: Yunzhe Xue <yunzhexue@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: robot,servo,feetech,robotic-arm,sts3215
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pyserial>=3.5
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
20
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pymyarm
|
|
24
|
+
|
|
25
|
+
pymycobot-compatible Python API for Feetech STS servo-based robotic arms.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pymyarm
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pymyarm import MyArmSTS
|
|
37
|
+
|
|
38
|
+
arm = MyArmSTS(port="/dev/ttyACM0", baudrate=1000000)
|
|
39
|
+
arm.connect()
|
|
40
|
+
|
|
41
|
+
# Read all joint angles
|
|
42
|
+
angles = arm.get_angles()
|
|
43
|
+
print(angles)
|
|
44
|
+
|
|
45
|
+
# Move joints
|
|
46
|
+
arm.set_angles([0, 0, 0, 0, 0, 0], speed=200)
|
|
47
|
+
|
|
48
|
+
# Read encoder values
|
|
49
|
+
encoders = arm.get_servos_encoder()
|
|
50
|
+
|
|
51
|
+
# Control gripper
|
|
52
|
+
arm.set_gripper_value(50, speed=200)
|
|
53
|
+
|
|
54
|
+
arm.disconnect()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Architecture
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
┌─────────────────────────────────────────┐
|
|
61
|
+
│ MyArmSTS (myarm_sts.py) │ User API: angles, encoders, gripper
|
|
62
|
+
├─────────────────────────────────────────┤
|
|
63
|
+
│ STS (sts.py) conversion.py │ Register-level ops + unit conversion
|
|
64
|
+
├─────────────────────────────────────────┤
|
|
65
|
+
│ ServoController │ Thread-safe TX/RX, retries, error cache
|
|
66
|
+
│ (servo_controller.py) │
|
|
67
|
+
├─────────────────────────────────────────┤
|
|
68
|
+
│ protocol.py port.py │ Frame encoding/decoding + serial I/O
|
|
69
|
+
├─────────────────────────────────────────┤
|
|
70
|
+
│ constants.py joint_config.py │ Protocol constants + arm mapping
|
|
71
|
+
└─────────────────────────────────────────┘
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
|
|
76
|
+
- **pymycobot-compatible API** — drop-in replacement for pymycobot with Feetech STS servos
|
|
77
|
+
- **Thread-safe** — all serial communication is locked and safe for multi-threaded use
|
|
78
|
+
- **Joint calibration** — offset calibration, encoder centering
|
|
79
|
+
- **Dual-motor joints** — supports joints driven by two servos (e.g. shoulder)
|
|
80
|
+
- **Sync read/write** — efficient batch operations for all servos
|
|
81
|
+
- **Auto port detection** — finds servo controllers automatically
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
- Python >= 3.8
|
|
86
|
+
- pyserial >= 3.5
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
- [API Reference (English)](API_REFERENCE_EN.md)
|
|
91
|
+
- [API Reference (Chinese)](API_REFERENCE.md)
|
|
92
|
+
- [Changelog](CHANGELOG.md)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
pymyarm-0.0.3/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# pymyarm
|
|
2
|
+
|
|
3
|
+
pymycobot-compatible Python API for Feetech STS servo-based robotic arms.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pymyarm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from pymyarm import MyArmSTS
|
|
15
|
+
|
|
16
|
+
arm = MyArmSTS(port="/dev/ttyACM0", baudrate=1000000)
|
|
17
|
+
arm.connect()
|
|
18
|
+
|
|
19
|
+
# Read all joint angles
|
|
20
|
+
angles = arm.get_angles()
|
|
21
|
+
print(angles)
|
|
22
|
+
|
|
23
|
+
# Move joints
|
|
24
|
+
arm.set_angles([0, 0, 0, 0, 0, 0], speed=200)
|
|
25
|
+
|
|
26
|
+
# Read encoder values
|
|
27
|
+
encoders = arm.get_servos_encoder()
|
|
28
|
+
|
|
29
|
+
# Control gripper
|
|
30
|
+
arm.set_gripper_value(50, speed=200)
|
|
31
|
+
|
|
32
|
+
arm.disconnect()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────────────────────────────────┐
|
|
39
|
+
│ MyArmSTS (myarm_sts.py) │ User API: angles, encoders, gripper
|
|
40
|
+
├─────────────────────────────────────────┤
|
|
41
|
+
│ STS (sts.py) conversion.py │ Register-level ops + unit conversion
|
|
42
|
+
├─────────────────────────────────────────┤
|
|
43
|
+
│ ServoController │ Thread-safe TX/RX, retries, error cache
|
|
44
|
+
│ (servo_controller.py) │
|
|
45
|
+
├─────────────────────────────────────────┤
|
|
46
|
+
│ protocol.py port.py │ Frame encoding/decoding + serial I/O
|
|
47
|
+
├─────────────────────────────────────────┤
|
|
48
|
+
│ constants.py joint_config.py │ Protocol constants + arm mapping
|
|
49
|
+
└─────────────────────────────────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- **pymycobot-compatible API** — drop-in replacement for pymycobot with Feetech STS servos
|
|
55
|
+
- **Thread-safe** — all serial communication is locked and safe for multi-threaded use
|
|
56
|
+
- **Joint calibration** — offset calibration, encoder centering
|
|
57
|
+
- **Dual-motor joints** — supports joints driven by two servos (e.g. shoulder)
|
|
58
|
+
- **Sync read/write** — efficient batch operations for all servos
|
|
59
|
+
- **Auto port detection** — finds servo controllers automatically
|
|
60
|
+
|
|
61
|
+
## Requirements
|
|
62
|
+
|
|
63
|
+
- Python >= 3.8
|
|
64
|
+
- pyserial >= 3.5
|
|
65
|
+
|
|
66
|
+
## Documentation
|
|
67
|
+
|
|
68
|
+
- [API Reference (English)](API_REFERENCE_EN.md)
|
|
69
|
+
- [API Reference (Chinese)](API_REFERENCE.md)
|
|
70
|
+
- [Changelog](CHANGELOG.md)
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""pymyarm public API."""
|
|
2
|
+
|
|
3
|
+
from .conversion import angle_to_encoder, encoder_to_angle
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
CommunicationError,
|
|
6
|
+
ConnectionError,
|
|
7
|
+
InvalidParameterError,
|
|
8
|
+
NoResponseError,
|
|
9
|
+
NotImplementedFeatureError,
|
|
10
|
+
PyMyArmError,
|
|
11
|
+
ServoError,
|
|
12
|
+
)
|
|
13
|
+
from .joint_config import DEFAULT_ARM_CONFIG, ArmConfig, GripperConfig, JointConfig
|
|
14
|
+
from .myarm_sts import MyArmSTS, ServoSnapshot
|
|
15
|
+
from .port import find_servo_port
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MyArmSTS",
|
|
19
|
+
"ServoSnapshot",
|
|
20
|
+
"find_servo_port",
|
|
21
|
+
"ArmConfig",
|
|
22
|
+
"JointConfig",
|
|
23
|
+
"GripperConfig",
|
|
24
|
+
"DEFAULT_ARM_CONFIG",
|
|
25
|
+
"angle_to_encoder",
|
|
26
|
+
"encoder_to_angle",
|
|
27
|
+
"PyMyArmError",
|
|
28
|
+
"ConnectionError",
|
|
29
|
+
"CommunicationError",
|
|
30
|
+
"NoResponseError",
|
|
31
|
+
"InvalidParameterError",
|
|
32
|
+
"NotImplementedFeatureError",
|
|
33
|
+
"ServoError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.0.3"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Constants for Feetech STS protocol and arm defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
HEADER = 0xFF
|
|
6
|
+
BROADCAST_ID = 0xFE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Instruction:
|
|
10
|
+
"""STS/SCS protocol instruction set."""
|
|
11
|
+
|
|
12
|
+
PING = 0x01
|
|
13
|
+
READ_DATA = 0x02
|
|
14
|
+
WRITE_DATA = 0x03
|
|
15
|
+
REG_WRITE = 0x04
|
|
16
|
+
ACTION = 0x05
|
|
17
|
+
RESET = 0x0A
|
|
18
|
+
POSITION_CALIBRATE = 0x0B
|
|
19
|
+
SYNC_READ = 0x82
|
|
20
|
+
SYNC_WRITE = 0x83
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MemAddr:
|
|
24
|
+
"""Common STS register addresses."""
|
|
25
|
+
|
|
26
|
+
ID = 0x05
|
|
27
|
+
BAUD_RATE = 0x06
|
|
28
|
+
PHASE = 0x12
|
|
29
|
+
OFFSET_POSITION = 0x1F
|
|
30
|
+
OPERATING_MODE = 0x21
|
|
31
|
+
|
|
32
|
+
TORQUE_ENABLE = 0x28
|
|
33
|
+
ACCELERATION = 0x29
|
|
34
|
+
GOAL_POSITION = 0x2A
|
|
35
|
+
GOAL_SPEED = 0x2E
|
|
36
|
+
LOCK_FLAG = 0x37
|
|
37
|
+
|
|
38
|
+
PRESENT_POSITION = 0x38
|
|
39
|
+
PRESENT_SPEED = 0x3A
|
|
40
|
+
PRESENT_LOAD = 0x3C
|
|
41
|
+
PRESENT_VOLTAGE = 0x3E
|
|
42
|
+
PRESENT_TEMPERATURE = 0x3F
|
|
43
|
+
STATUS = 0x41
|
|
44
|
+
MOVING_FLAG = 0x42
|
|
45
|
+
PRESENT_CURRENT = 0x45
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TorqueEnable:
|
|
49
|
+
"""Torque enable values."""
|
|
50
|
+
|
|
51
|
+
OFF = 0
|
|
52
|
+
ON = 1
|
|
53
|
+
DAMPING = 2
|
|
54
|
+
CALIBRATE_CENTER = 128
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StatusBit:
|
|
58
|
+
"""STS status bits."""
|
|
59
|
+
|
|
60
|
+
VOLTAGE_ERROR = 0x01
|
|
61
|
+
ENCODER_ERROR = 0x02
|
|
62
|
+
TEMPERATURE_ERROR = 0x04
|
|
63
|
+
CURRENT_ERROR = 0x08
|
|
64
|
+
OVERLOAD_ERROR = 0x20
|
|
65
|
+
|
|
66
|
+
# Errors that invalidate position read data.
|
|
67
|
+
# Only ENCODER_ERROR means the encoder value itself is unreliable.
|
|
68
|
+
# Other errors (voltage, temperature, current, overload) are status
|
|
69
|
+
# warnings — the encoder hardware still returns valid readings.
|
|
70
|
+
READ_INVALIDATING_ERRORS = ENCODER_ERROR
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ServoParams:
|
|
74
|
+
"""STS conversion constants."""
|
|
75
|
+
|
|
76
|
+
POSITION_MIN = 0
|
|
77
|
+
POSITION_MAX = 4095
|
|
78
|
+
POSITION_CENTER = 2048
|
|
79
|
+
POSITION_RESOLUTION_DEG = 0.087
|
|
80
|
+
|
|
81
|
+
SPEED_RESOLUTION_RPM = 0.732
|
|
82
|
+
VOLTAGE_RESOLUTION_V = 0.1
|
|
83
|
+
CURRENT_RESOLUTION_MA = 6.5
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
DEFAULT_BAUDRATE = 1_000_000
|
|
87
|
+
DEFAULT_TIMEOUT = 0.1
|
|
88
|
+
DEFAULT_RETRIES = 2
|
|
89
|
+
SYNC_READ_FRAME_BUDGET = 16
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Conversion helpers for encoder/angle/speed units."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .constants import ServoParams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def clamp(value: float, min_value: float, max_value: float) -> float:
|
|
9
|
+
"""Clamp numeric value to [min_value, max_value]."""
|
|
10
|
+
return max(min_value, min(value, max_value))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encoder_to_angle(
|
|
14
|
+
encoder: int, offset: int = ServoParams.POSITION_CENTER, direction: int = 1
|
|
15
|
+
) -> float:
|
|
16
|
+
"""Convert encoder value to joint angle in degrees."""
|
|
17
|
+
delta = encoder - offset
|
|
18
|
+
return round(delta * ServoParams.POSITION_RESOLUTION_DEG * direction, 3)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def angle_to_encoder(
|
|
22
|
+
angle: float, offset: int = ServoParams.POSITION_CENTER, direction: int = 1
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Convert joint angle in degrees to encoder value (0-4095)."""
|
|
25
|
+
raw = int(offset + angle / ServoParams.POSITION_RESOLUTION_DEG / direction)
|
|
26
|
+
return int(clamp(raw, ServoParams.POSITION_MIN, ServoParams.POSITION_MAX))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def angles_to_encoders(
|
|
30
|
+
angles: list[float], offsets: list[int], directions: list[int]
|
|
31
|
+
) -> list[int]:
|
|
32
|
+
"""Batch angle->encoder conversion."""
|
|
33
|
+
return [
|
|
34
|
+
angle_to_encoder(angle, offset, direction)
|
|
35
|
+
for angle, offset, direction in zip(angles, offsets, directions)
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def encoders_to_angles(
|
|
40
|
+
encoders: list[int], offsets: list[int], directions: list[int]
|
|
41
|
+
) -> list[float]:
|
|
42
|
+
"""Batch encoder->angle conversion."""
|
|
43
|
+
return [
|
|
44
|
+
encoder_to_angle(encoder, offset, direction)
|
|
45
|
+
for encoder, offset, direction in zip(encoders, offsets, directions)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def voltage_raw_to_v(raw: int) -> float:
|
|
50
|
+
"""Convert raw voltage register to volts."""
|
|
51
|
+
return round(raw * ServoParams.VOLTAGE_RESOLUTION_V, 3)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def current_raw_to_ma(raw: int) -> float:
|
|
55
|
+
"""Convert raw current register to mA."""
|
|
56
|
+
return round(raw * ServoParams.CURRENT_RESOLUTION_MA, 3)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Custom exceptions for pymyarm."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PyMyArmError(Exception):
|
|
5
|
+
"""Base exception for pymyarm."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionError(PyMyArmError):
|
|
9
|
+
"""Raised when serial connection is not available."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CommunicationError(PyMyArmError):
|
|
13
|
+
"""Raised when protocol communication fails."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NoResponseError(CommunicationError):
|
|
17
|
+
"""Raised when a servo does not respond at all."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TimeoutError(PyMyArmError):
|
|
21
|
+
"""Raised when response timeout occurs."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InvalidParameterError(PyMyArmError):
|
|
25
|
+
"""Raised when user input parameter is invalid."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NotImplementedFeatureError(PyMyArmError):
|
|
29
|
+
"""Raised when feature is intentionally unsupported on this hardware."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, feature: str) -> None:
|
|
32
|
+
super().__init__(
|
|
33
|
+
f"Feature '{feature}' is not supported on current STS implementation."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ServoError(PyMyArmError):
|
|
38
|
+
"""Raised when servo status/error byte indicates failure."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, servo_id: int, error_code: int, message: str = "") -> None:
|
|
41
|
+
detail = f"Servo {servo_id} error 0x{error_code:02X}"
|
|
42
|
+
if message:
|
|
43
|
+
detail = f"{detail}: {message}"
|
|
44
|
+
super().__init__(detail)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Arm joint and motor mapping configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class JointConfig:
|
|
10
|
+
"""Single joint mapping config."""
|
|
11
|
+
|
|
12
|
+
joint_id: int
|
|
13
|
+
motor_ids: list[int]
|
|
14
|
+
is_dual_motor: bool = False
|
|
15
|
+
encoder_offset: int = 2048
|
|
16
|
+
direction: int = 1
|
|
17
|
+
min_angle: float = -180.0
|
|
18
|
+
max_angle: float = 180.0
|
|
19
|
+
|
|
20
|
+
def primary_motor_id(self) -> int:
|
|
21
|
+
"""Return primary motor ID."""
|
|
22
|
+
return self.motor_ids[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class GripperConfig:
|
|
27
|
+
"""Gripper mapping config."""
|
|
28
|
+
|
|
29
|
+
motor_id: int = 8
|
|
30
|
+
open_encoder: int = 0
|
|
31
|
+
close_encoder: int = 4095
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ArmConfig:
|
|
36
|
+
"""Arm-level mapping config."""
|
|
37
|
+
|
|
38
|
+
dof: int = 6
|
|
39
|
+
joints: list[JointConfig] = field(default_factory=list)
|
|
40
|
+
gripper: GripperConfig | None = None
|
|
41
|
+
|
|
42
|
+
def get_joint(self, joint_id: int) -> JointConfig | None:
|
|
43
|
+
"""Get joint config by ID."""
|
|
44
|
+
for joint in self.joints:
|
|
45
|
+
if joint.joint_id == joint_id:
|
|
46
|
+
return joint
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def all_servo_ids(self, include_gripper: bool = True) -> list[int]:
|
|
50
|
+
"""Get all servo IDs in arm order."""
|
|
51
|
+
ids: list[int] = []
|
|
52
|
+
for joint in self.joints:
|
|
53
|
+
ids.extend(joint.motor_ids)
|
|
54
|
+
if include_gripper and self.gripper is not None:
|
|
55
|
+
ids.append(self.gripper.motor_id)
|
|
56
|
+
return ids
|
|
57
|
+
|
|
58
|
+
def joint_to_motor_pairs(
|
|
59
|
+
self, joint_id: int, encoder: int
|
|
60
|
+
) -> list[tuple[int, int]]:
|
|
61
|
+
"""Map joint target encoder to one/two motor target pairs."""
|
|
62
|
+
joint = self.get_joint(joint_id)
|
|
63
|
+
if joint is None:
|
|
64
|
+
raise ValueError(f"Invalid joint_id: {joint_id}")
|
|
65
|
+
|
|
66
|
+
if joint.is_dual_motor:
|
|
67
|
+
motor_a, motor_b = joint.motor_ids
|
|
68
|
+
return [(motor_a, encoder), (motor_b, 4096 - encoder)]
|
|
69
|
+
return [(joint.primary_motor_id(), encoder)]
|
|
70
|
+
|
|
71
|
+
def motor_offsets(self) -> list[int]:
|
|
72
|
+
"""Return offsets for 1..dof joints."""
|
|
73
|
+
return [self.joints[i].encoder_offset for i in range(self.dof)]
|
|
74
|
+
|
|
75
|
+
def motor_directions(self) -> list[int]:
|
|
76
|
+
"""Return directions for 1..dof joints."""
|
|
77
|
+
return [self.joints[i].direction for i in range(self.dof)]
|
|
78
|
+
|
|
79
|
+
def motor_id_for_joint_angle_read(self, joint_id: int) -> int:
|
|
80
|
+
"""Read joint angle by primary motor ID."""
|
|
81
|
+
joint = self.get_joint(joint_id)
|
|
82
|
+
if joint is None:
|
|
83
|
+
raise ValueError(f"Invalid joint_id: {joint_id}")
|
|
84
|
+
return joint.primary_motor_id()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
DEFAULT_ARM_CONFIG = ArmConfig(
|
|
88
|
+
dof=6,
|
|
89
|
+
joints=[
|
|
90
|
+
JointConfig(1, [1], False, 2048, 1, -180.0, 180.0),
|
|
91
|
+
JointConfig(2, [2, 3], True, 2048, 1, -90.0, 90.0),
|
|
92
|
+
JointConfig(3, [4], False, 2048, 1, -150.0, 150.0),
|
|
93
|
+
JointConfig(4, [5], False, 2048, 1, -180.0, 180.0),
|
|
94
|
+
JointConfig(5, [6], False, 2048, 1, -180.0, 180.0),
|
|
95
|
+
JointConfig(6, [7], False, 2048, 1, -180.0, 180.0),
|
|
96
|
+
],
|
|
97
|
+
gripper=GripperConfig(motor_id=8, open_encoder=0, close_encoder=4095),
|
|
98
|
+
)
|