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 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
@@ -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
+ )