zeroArm-sdk 0.1.2__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) ForceEase Co.
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.
@@ -0,0 +1,9 @@
1
+ # MANIFEST.in
2
+ include LICENSE.txt
3
+ include README.md
4
+ include zeroArm_sdk/_core.pyx
5
+ recursive-include zeroArm_sdk/locale *.mo
6
+ exclude zeroArm_sdk/**/*.c
7
+ exclude zeroArm_sdk/**/*.html
8
+ global-exclude *.so
9
+ global-exclude *.pyd
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: zeroArm_sdk
3
+ Version: 0.1.2
4
+ Summary: Official SDK for ZeroArm robotic arm (proprietary)
5
+ Home-page: https://github.com/HANCKH/zeroArm_python_sdk
6
+ Author: ForceEase Co.
7
+ Author-email: "ForceEase Co." <support@forceease.tech>
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/HANCKH/zeroArm_python_sdk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Cython
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pyzmq>=25.0
16
+ Requires-Dist: websockets
17
+ Requires-Dist: zeroconf
18
+ Requires-Dist: tqdm
19
+ Dynamic: author
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: requires-python
23
+
24
+ # ZeroArm Python SDK
25
+
26
+ Official Python SDK for ForceEase ZeroArm robotic arms.
27
+
28
+ ## Connection Notes
29
+
30
+ - Use `RobotArm().auto_discover_and_connect()` for the default LAN workflow.
31
+ - Use `connect_with_short_sn()` with at least the first 8 serial-number characters in multi-arm environments.
32
+ - mDNS discovery only accepts connectable IPv4 addresses and ignores loopback or unspecified addresses such as `127.0.0.1` and `0.0.0.0`.
33
+ - The controller advertises WebSocket control port `8888` and bridge port `8889`; SDK mode uses the fixed ZMQ command/state ports `6666/6667`.
34
+
35
+ ## Current Protocol Coverage
36
+
37
+ - `set_tf(..., plan_mode=0)` uses joint-space planning; `plan_mode=1` uses Cartesian planning.
38
+ - `enable_hold_current()` asks the controller to hold all configured joints at their current feedback positions.
39
+ - `set_joints_batch()` sends a single `raw_msg` command through the normal `commands_<serial>` topic.
40
+
41
+ ## Build Notes
42
+
43
+ - `pyproject.toml` declares Cython as a build-system dependency for isolated builds.
44
+ - For direct `setup.py` builds, install `requirements-build.txt` first.
45
+ - `setuptools` is capped below 80 to stay compatible with ROS `colcon-core` in the shared development environment.
46
+
47
+ ## Release
48
+
49
+ - Publish Python SDK releases through `.github/workflows/build-wheels.yml`.
50
+ - A GitHub Release with action `published` builds the sdist plus Linux, Windows, and macOS wheels, then uploads all distributions to PyPI in one publish job.
51
+ - The workflow expects `PYPI_API_TOKEN` to be configured as a GitHub Actions secret.
52
+
53
+ ## Version
54
+
55
+ Current SDK source version: `0.1.2`.
@@ -0,0 +1,32 @@
1
+ # ZeroArm Python SDK
2
+
3
+ Official Python SDK for ForceEase ZeroArm robotic arms.
4
+
5
+ ## Connection Notes
6
+
7
+ - Use `RobotArm().auto_discover_and_connect()` for the default LAN workflow.
8
+ - Use `connect_with_short_sn()` with at least the first 8 serial-number characters in multi-arm environments.
9
+ - mDNS discovery only accepts connectable IPv4 addresses and ignores loopback or unspecified addresses such as `127.0.0.1` and `0.0.0.0`.
10
+ - The controller advertises WebSocket control port `8888` and bridge port `8889`; SDK mode uses the fixed ZMQ command/state ports `6666/6667`.
11
+
12
+ ## Current Protocol Coverage
13
+
14
+ - `set_tf(..., plan_mode=0)` uses joint-space planning; `plan_mode=1` uses Cartesian planning.
15
+ - `enable_hold_current()` asks the controller to hold all configured joints at their current feedback positions.
16
+ - `set_joints_batch()` sends a single `raw_msg` command through the normal `commands_<serial>` topic.
17
+
18
+ ## Build Notes
19
+
20
+ - `pyproject.toml` declares Cython as a build-system dependency for isolated builds.
21
+ - For direct `setup.py` builds, install `requirements-build.txt` first.
22
+ - `setuptools` is capped below 80 to stay compatible with ROS `colcon-core` in the shared development environment.
23
+
24
+ ## Release
25
+
26
+ - Publish Python SDK releases through `.github/workflows/build-wheels.yml`.
27
+ - A GitHub Release with action `published` builds the sdist plus Linux, Windows, and macOS wheels, then uploads all distributions to PyPI in one publish job.
28
+ - The workflow expects `PYPI_API_TOKEN` to be configured as a GitHub Actions secret.
29
+
30
+ ## Version
31
+
32
+ Current SDK source version: `0.1.2`.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0,<80", "wheel", "Cython>=3.0", "packaging>=24.2"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zeroArm_sdk"
7
+ version = "0.1.2"
8
+ description = "Official SDK for ZeroArm robotic arm (proprietary)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ authors = [
12
+ {name = "ForceEase Co.", email = "support@forceease.tech"},
13
+ ]
14
+ dynamic = ["license"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Cython",
18
+ "Operating System :: OS Independent",
19
+ ]
20
+ dependencies = [
21
+ "pyzmq >= 25.0",
22
+ "websockets",
23
+ "zeroconf",
24
+ "tqdm",
25
+ ]
26
+
27
+ [project.urls]
28
+ Source = "https://github.com/HANCKH/zeroArm_python_sdk"
29
+
30
+ [tool.cibuildwheel]
31
+ build = "cp39-* cp310-* cp311-* cp312-*"
32
+
33
+ manylinux-x86_64-image = "quay.io/pypa/manylinux2014_x86_64:latest"
34
+ manylinux-aarch64-image = "quay.io/pypa/manylinux2014_aarch64:latest"
35
+
36
+ archs = ["x86_64", "aarch64"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,76 @@
1
+ # setup.py(完整版 - 推荐使用此版本)
2
+
3
+ from setuptools import setup, Extension, Command
4
+ from Cython.Build import cythonize
5
+ import os
6
+ import shutil
7
+
8
+ class CleanCython(Command):
9
+ """自定义命令:清理 Cython 生成的 .c 和 .html 文件"""
10
+ user_options = []
11
+
12
+ def initialize_options(self):
13
+ pass
14
+
15
+ def finalize_options(self):
16
+ pass
17
+
18
+ def run(self):
19
+ for root, dirs, files in os.walk("zeroArm_sdk"):
20
+ for file in files:
21
+ if file.endswith((".c", ".html", ".so", ".pyd")):
22
+ file_path = os.path.join(root, file)
23
+ print(f"Removing {file_path}")
24
+ os.remove(file_path)
25
+
26
+ setup(
27
+ name="zeroArm_sdk",
28
+ version="0.1.2",
29
+ author="ForceEase Co.",
30
+ author_email="support@forceease.tech",
31
+ url="https://github.com/HANCKH/zeroArm_python_sdk",
32
+ description="Python SDK for ZeroArm robotic arms",
33
+ license="MIT",
34
+ license_files=[],
35
+ long_description=open("README.md", encoding="utf-8").read() if os.path.exists("README.md") else "",
36
+ long_description_content_type="text/markdown",
37
+ packages=["zeroArm_sdk"],
38
+ package_dir={"zeroArm_sdk": "zeroArm_sdk"},
39
+ ext_modules=cythonize([
40
+ Extension(
41
+ "zeroArm_sdk._core",
42
+ sources=["zeroArm_sdk/_core.pyx"],
43
+ )
44
+ ], compiler_directives={"language_level": "3"}),
45
+ package_data={
46
+ "zeroArm_sdk": [
47
+ "locale/*/LC_MESSAGES/*.mo",
48
+ ]
49
+ },
50
+ include_package_data=False,
51
+ exclude_package_data={
52
+ "zeroArm_sdk": [
53
+ "*.pyx",
54
+ "*.c",
55
+ "*.html",
56
+ "**/*.pyx",
57
+ "**/*.c",
58
+ "**/*.html",
59
+ ]
60
+ },
61
+ install_requires=[
62
+ "pyzmq>=25.0",
63
+ "websockets",
64
+ "zeroconf",
65
+ "tqdm",
66
+ ],
67
+ python_requires=">=3.8",
68
+ classifiers=[
69
+ "Programming Language :: Python :: 3",
70
+ "Programming Language :: Cython",
71
+ "Operating System :: OS Independent",
72
+ ],
73
+ cmdclass={
74
+ 'clean_cython': CleanCython,
75
+ },
76
+ )
@@ -0,0 +1,9 @@
1
+ # zeroArm_sdk/__init__.py
2
+ # Copyright (c) 2026 ForceEase Co. All Rights Reserved.
3
+
4
+ from .arm import RobotArm
5
+ from .arm_types import JointState, GripperState, Pose, FullArmState
6
+ from .exceptions import ArmNotFoundError, ArmConnectionError, ArmCommandError
7
+
8
+ __version__ = "0.1.2"
9
+ __author__ = "ForceEase Co."
@@ -0,0 +1,569 @@
1
+ # zeroArm_sdk/_core.pyx
2
+ # Copyright (c) 2026 Han / ForceEase Co. All Rights Reserved.
3
+ # Core implementation module - to be compiled into .so
4
+
5
+ import asyncio
6
+ import json
7
+ import ipaddress
8
+ import socket
9
+ import time
10
+ from typing import Dict, List, Optional, Callable
11
+ from tqdm import tqdm
12
+ import os
13
+ import gettext
14
+ import zmq
15
+ import zmq.asyncio
16
+ import websockets
17
+ from zeroconf import Zeroconf, ServiceBrowser, ServiceListener
18
+ from .arm_types import JointState, GripperState, Pose, FullArmState
19
+ from .exceptions import ArmNotFoundError, ArmConnectionError, ArmCommandError
20
+
21
+ cdef str localedir = os.path.join(os.path.dirname(__file__), "locale")
22
+ cdef object translation = gettext.translation(
23
+ "zeroArm_sdk",
24
+ localedir,
25
+ fallback=True
26
+ )
27
+ cdef str _(str message):
28
+ return translation.gettext(message)
29
+
30
+ # =============================================================================
31
+ # Discovery Logic
32
+ # =============================================================================
33
+ def _decode_property(value):
34
+ if isinstance(value, bytes):
35
+ return value.decode(errors="ignore")
36
+ return str(value)
37
+
38
+
39
+ def _is_connectable_ip(raw_ip) -> bool:
40
+ ip_text = str(raw_ip or "").strip()
41
+ if not ip_text:
42
+ return False
43
+ try:
44
+ parsed_ip = ipaddress.ip_address(ip_text)
45
+ except ValueError:
46
+ return False
47
+ return (
48
+ parsed_ip.version == 4
49
+ and not parsed_ip.is_loopback
50
+ and not parsed_ip.is_unspecified
51
+ )
52
+
53
+
54
+ def _append_connectable_ip(ips: List[str], raw_ip):
55
+ ip_text = str(raw_ip or "").strip()
56
+ if _is_connectable_ip(ip_text) and ip_text not in ips:
57
+ ips.append(ip_text)
58
+
59
+
60
+ def _extract_connectable_ips(info, props: Dict) -> List[str]:
61
+ ips: List[str] = []
62
+ for advertised_ip in str(props.get("ip", "")).split(","):
63
+ _append_connectable_ip(ips, advertised_ip)
64
+
65
+ for addr in getattr(info, "addresses", []) or []:
66
+ try:
67
+ if len(addr) == 4:
68
+ _append_connectable_ip(ips, socket.inet_ntoa(addr))
69
+ elif hasattr(socket, "inet_ntop"):
70
+ _append_connectable_ip(ips, socket.inet_ntop(socket.AF_INET6, addr))
71
+ except Exception:
72
+ continue
73
+ return ips
74
+
75
+
76
+ def _int_property(props: Dict, key: str, default: int) -> int:
77
+ try:
78
+ return int(props.get(key, default))
79
+ except (TypeError, ValueError):
80
+ return default
81
+
82
+
83
+ class ArmListener(ServiceListener):
84
+ def __init__(self):
85
+ self.arms = []
86
+
87
+ def add_service(self, zeroconf, type_str, name):
88
+ info = zeroconf.get_service_info(type_str, name)
89
+ if not info:
90
+ return
91
+ props = {_decode_property(k): _decode_property(v)
92
+ for k, v in info.properties.items()}
93
+ ips = _extract_connectable_ips(info, props)
94
+ try:
95
+ sns = json.loads(props.get("available_sns", "[]"))
96
+ except json.JSONDecodeError:
97
+ sns = []
98
+ if sns and ips:
99
+ self.arms = [arm for arm in self.arms if arm.get("name") != name]
100
+ self.arms.append({
101
+ "name": name,
102
+ "ips": ips,
103
+ "sns": sns,
104
+ "control_port": _int_property(props, "control_ws_port", 8888),
105
+ "zmq_cmd_port": _int_property(props, "zmq_cmd_port", 6666),
106
+ })
107
+
108
+ def remove_service(self, zeroconf, type_str, name):
109
+ pass
110
+
111
+ def update_service(self, zeroconf, type_str, name):
112
+ self.add_service(zeroconf, type_str, name)
113
+
114
+
115
+ async def discover_arms(timeout: float = 1.0) -> List[Dict]:
116
+ """Discover all available ZeroArm devices via mDNS."""
117
+ zeroconf = Zeroconf()
118
+ listener = ArmListener()
119
+ ServiceBrowser(zeroconf, "_armcontrol._tcp.local.", listener)
120
+ await asyncio.sleep(timeout)
121
+ zeroconf.close()
122
+ return listener.arms
123
+
124
+
125
+ # =============================================================================
126
+ # Communicator Class
127
+ # =============================================================================
128
+ cdef class ArmCommunicator:
129
+ cdef str ip
130
+ cdef str sn
131
+ cdef int control_port
132
+ cdef int zmq_cmd_port
133
+ cdef public object last_state
134
+ cdef public object state_callback
135
+ cdef object ctx
136
+ cdef object cmd_pub
137
+ cdef object state_sub
138
+ cdef public bint running
139
+ cdef object _recv_task
140
+
141
+ def __init__(self, str ip, str sn, int control_port=8888, int zmq_cmd_port=6666):
142
+ self.ip = ip
143
+ self.sn = sn
144
+ self.control_port = control_port
145
+ self.zmq_cmd_port = zmq_cmd_port
146
+ self.last_state = None
147
+ self.state_callback = None
148
+ self.ctx = zmq.asyncio.Context()
149
+ self.cmd_pub = self.ctx.socket(zmq.PUB)
150
+ self.state_sub = self.ctx.socket(zmq.SUB)
151
+ self.running = True
152
+ self._recv_task = None
153
+
154
+ async def select_sn(self) -> bool:
155
+ uri = f"ws://{self.ip}:{self.control_port}"
156
+ try:
157
+ async with websockets.connect(uri) as ws:
158
+ msg = json.dumps({"type": "select_sn", "sn": self.sn, "sdk": True})
159
+ await ws.send(msg)
160
+ response = await asyncio.wait_for(ws.recv(), timeout=20.0)
161
+ data = json.loads(response)
162
+ return data.get("type") == "select_ack"
163
+ except Exception:
164
+ return False
165
+
166
+ async def shutdown_control(self) -> bool:
167
+ uri = f"ws://{self.ip}:{self.control_port}"
168
+ try:
169
+ async with websockets.connect(uri) as ws:
170
+ msg = json.dumps({"type": "shutdown_control", "sn": self.sn})
171
+ await ws.send(msg)
172
+ response = await asyncio.wait_for(ws.recv(), timeout=1.0)
173
+ data = json.loads(response)
174
+ return data.get("type") == "shutdown_ack"
175
+ except Exception:
176
+ return False
177
+
178
+ async def connect_zmq(self) -> bool:
179
+ try:
180
+ self.cmd_pub.connect(f"tcp://{self.ip}:{self.zmq_cmd_port}")
181
+ self.state_sub.connect(f"tcp://{self.ip}:{self.zmq_cmd_port + 1}")
182
+ self.state_sub.setsockopt(zmq.SUBSCRIBE, f"states_{self.sn}".encode())
183
+ self._recv_task = asyncio.create_task(self._recv_states_loop())
184
+ return True
185
+ except Exception:
186
+ return False
187
+
188
+ async def _recv_states_loop(self):
189
+ poller = zmq.asyncio.Poller()
190
+ poller.register(self.state_sub, zmq.POLLIN)
191
+ while self.running:
192
+ events = await poller.poll(timeout=500)
193
+ if not events:
194
+ continue
195
+ topic, payload = await self.state_sub.recv_multipart()
196
+ if topic.decode() == f"states_{self.sn}":
197
+ try:
198
+ raw_state = json.loads(payload)
199
+ parsed_state = self._parse_state(raw_state)
200
+ self.last_state = parsed_state
201
+ if self.state_callback:
202
+ self.state_callback(parsed_state)
203
+ except Exception:
204
+ pass
205
+
206
+ def _parse_state(self, raw: Dict) -> FullArmState:
207
+ joints = []
208
+ for j in raw.get("joints", []):
209
+ joints.append(
210
+ JointState(
211
+ can_id=j.get("can_id", 0),
212
+ position=j.get("position", 0.0),
213
+ velocity=j.get("velocity", 0.0),
214
+ torque=j.get("torque", 0.0),
215
+ enable=j.get("enable", False),
216
+ mode=j.get("mode", 0),
217
+ )
218
+ )
219
+ gripper = None
220
+ if "gripper" in raw:
221
+ g = raw["gripper"]
222
+ gripper = GripperState(
223
+ position=g.get("position", 0.0),
224
+ velocity=g.get("velocity", 0.0),
225
+ torque=g.get("torque", 0.0),
226
+ enable=g.get("enable", False),
227
+ mode=g.get("mode", 0),
228
+ )
229
+ eef_pose = None
230
+ if "eef_pose" in raw:
231
+ p = raw["eef_pose"]
232
+ eef_pose = Pose(
233
+ x=p.get("x", 0.0),
234
+ y=p.get("y", 0.0),
235
+ z=p.get("z", 0.0),
236
+ qw=p.get("qw", 1.0),
237
+ qx=p.get("qx", 0.0),
238
+ qy=p.get("qy", 0.0),
239
+ qz=p.get("qz", 0.0),
240
+ )
241
+ return FullArmState(joints=joints, gripper=gripper, eef_pose=eef_pose)
242
+
243
+ async def send_command(self, cmd: Dict) -> bool:
244
+ try:
245
+ payload = json.dumps(cmd).encode()
246
+ await self.cmd_pub.send_multipart([f"commands_{self.sn}".encode(), payload])
247
+ return True
248
+ except Exception:
249
+ return False
250
+
251
+ async def send_raw(self, bytes topic, bytes payload) -> bool:
252
+ try:
253
+ await self.cmd_pub.send_multipart([topic, payload])
254
+ return True
255
+ except Exception:
256
+ return False
257
+
258
+ def set_state_callback(self, callback: Callable[[FullArmState], None]):
259
+ self.state_callback = callback
260
+
261
+ def is_connected(self) -> bool:
262
+ return self.cmd_pub is not None and not self.cmd_pub.closed
263
+
264
+ async def close(self):
265
+ self.running = False
266
+ if self._recv_task is not None:
267
+ try:
268
+ await asyncio.wait_for(self._recv_task, timeout=1.0)
269
+ except asyncio.TimeoutError:
270
+ self._recv_task.cancel()
271
+ except Exception:
272
+ pass
273
+ try:
274
+ self.cmd_pub.close()
275
+ self.state_sub.close()
276
+ self.ctx.term()
277
+ except Exception:
278
+ pass
279
+
280
+
281
+ # =============================================================================
282
+ # RobotArm Class - Main User Interface (compiled)
283
+ # =============================================================================
284
+ cdef class RobotArm:
285
+ cdef public str sn
286
+ cdef public str ip
287
+ cdef public int control_port
288
+ cdef public int zmq_cmd_port
289
+ cdef public object communicator
290
+ cdef public object _state_callback
291
+
292
+ def __init__(
293
+ self,
294
+ sn: Optional[str] = None,
295
+ ip: Optional[str] = None,
296
+ int control_port = 8888,
297
+ int zmq_cmd_port = 6666,
298
+ ):
299
+ self.sn = sn
300
+ self.ip = ip
301
+ self.control_port = control_port
302
+ self.zmq_cmd_port = zmq_cmd_port
303
+ self.communicator = None
304
+ self._state_callback = None
305
+
306
+ async def find_all_devices(self, timeout: float = 1.0) -> List[Dict]:
307
+ return await discover_arms(timeout)
308
+
309
+ async def auto_discover_and_connect(self, timeout: float = 1.0) -> bool:
310
+ total_steps = 5
311
+ progress = tqdm(total=total_steps, desc=_("Connecting to ZeroArm"), unit="step")
312
+ try:
313
+ progress.set_description(_("Step 1/5: Finding devices"))
314
+ arms = await discover_arms(timeout)
315
+ progress.update(1)
316
+ if not arms:
317
+ progress.close()
318
+ print(_("No ZeroArm found on the network"))
319
+ return False
320
+
321
+ progress.set_description(_("Step 2/5: Choosing first arm"))
322
+ arm_info = arms[0]
323
+ self.sn = arm_info["sns"][0]
324
+ self.ip = arm_info["ips"][0]
325
+ self.control_port = int(arm_info.get("control_port", 8888))
326
+ self.zmq_cmd_port = int(arm_info.get("zmq_cmd_port", 6666))
327
+ short_sn = self.sn[:6] + "..." if len(self.sn) > 6 else self.sn
328
+ progress.update(1)
329
+
330
+ progress.set_description(_("Step 3/5: Selecting arm"))
331
+ self.communicator = ArmCommunicator(
332
+ self.ip,
333
+ self.sn,
334
+ self.control_port,
335
+ self.zmq_cmd_port,
336
+ )
337
+ success = await self.communicator.select_sn()
338
+ progress.update(1)
339
+ if not success:
340
+ progress.close()
341
+ print(_("Failed to select arm"))
342
+ return False
343
+
344
+ progress.set_description(_("Step 4/5: Waiting for arm ready (1s)"))
345
+ await asyncio.sleep(1.0)
346
+ progress.update(1)
347
+
348
+ progress.set_description(_("Step 5/5: Final connection"))
349
+ await self.communicator.connect_zmq()
350
+ progress.update(1)
351
+
352
+ if self._state_callback:
353
+ self.communicator.set_state_callback(self._state_callback)
354
+
355
+ progress.set_description(_("Connected!"))
356
+ progress.close()
357
+ print(_("Connected successfully: Arm {}").format(short_sn))
358
+ return True
359
+ except Exception:
360
+ progress.close()
361
+ print(_("Connection failed"))
362
+ return False
363
+
364
+ async def connect_with_short_sn(self, short_sn: str, timeout: float = 1.0) -> bool:
365
+ arms = await discover_arms(timeout)
366
+ if not arms:
367
+ print(_("No ZeroArm found on the network"))
368
+ return False
369
+
370
+ for arm_info in arms:
371
+ for full_sn in arm_info["sns"]:
372
+ if full_sn.startswith(short_sn):
373
+ self.sn = full_sn
374
+ self.ip = arm_info["ips"][0]
375
+ self.control_port = int(arm_info.get("control_port", 8888))
376
+ self.zmq_cmd_port = int(arm_info.get("zmq_cmd_port", 6666))
377
+ short_display = short_sn + "..." if len(full_sn) > 6 else short_sn
378
+ print(_("Found arm: {}").format(short_display))
379
+ return await self.connect()
380
+
381
+ print(_("No arm found starting with '{}'").format(short_sn))
382
+ return False
383
+
384
+ async def connect(self) -> bool:
385
+ if not self.sn or not self.ip:
386
+ print(_("Serial number and IP must be set first"))
387
+ return False
388
+
389
+ self.communicator = ArmCommunicator(
390
+ self.ip,
391
+ self.sn,
392
+ self.control_port,
393
+ self.zmq_cmd_port,
394
+ )
395
+ success = await self.communicator.select_sn()
396
+ if not success:
397
+ print(_("Failed to select arm"))
398
+ return False
399
+
400
+ await asyncio.sleep(1.0)
401
+ await self.communicator.connect_zmq()
402
+
403
+ if self._state_callback:
404
+ self.communicator.set_state_callback(self._state_callback)
405
+
406
+ short_sn = self.sn[:6] + "..." if len(self.sn) > 6 else self.sn
407
+ print(_("Connected to arm: {}").format(short_sn))
408
+ return True
409
+
410
+ def is_connected(self) -> bool:
411
+ return self.communicator is not None and self.communicator.is_connected()
412
+
413
+ @property
414
+ def current_state(self) -> Optional[FullArmState]:
415
+ if self.communicator:
416
+ return self.communicator.last_state
417
+ return None
418
+
419
+ def on_state_update(self, callback: Callable[[FullArmState], None]):
420
+ self._state_callback = callback
421
+ if self.communicator:
422
+ self.communicator.set_state_callback(callback)
423
+
424
+ async def send_command(self, cmd: Dict):
425
+ if not self.communicator:
426
+ raise ArmConnectionError("Not connected yet")
427
+ await self.communicator.send_command(cmd)
428
+
429
+ async def send_raw_msg(self, raw_payload: str):
430
+ if not self.communicator:
431
+ raise ArmConnectionError("Not connected yet")
432
+ await self.send_command({"type": "raw_msg", "payload": raw_payload})
433
+
434
+ async def set_joint(self, int can_id, float target_pos,
435
+ float target_vel = 0.0, float target_tau = 0.0,
436
+ int mode = 0, bint enable = True, bint set_zero = False,
437
+ max_vel: Optional[float] = None, max_acc: Optional[float] = None,
438
+ max_jerk: Optional[float] = None):
439
+ cmd = {
440
+ "type": "set_joint",
441
+ "can_id": can_id,
442
+ "target_pos": target_pos,
443
+ "target_vel": target_vel,
444
+ "target_tau": target_tau,
445
+ "mode": mode,
446
+ "enable": enable,
447
+ "set_zero": set_zero,
448
+ }
449
+ if max_vel is not None: cmd["max_vel"] = max_vel
450
+ if max_acc is not None: cmd["max_acc"] = max_acc
451
+ if max_jerk is not None: cmd["max_jerk"] = max_jerk
452
+ await self.send_command(cmd)
453
+
454
+ async def set_gripper(self, float target_pos,
455
+ float target_vel = 0.0, float target_tau = 0.0,
456
+ bint set_zero = False):
457
+ cmd = {
458
+ "type": "set_gripper",
459
+ "target_pos": target_pos,
460
+ "target_vel": target_vel,
461
+ "target_tau": target_tau,
462
+ "set_zero": set_zero,
463
+ }
464
+ await self.send_command(cmd)
465
+
466
+ async def set_tf(self, float x, float y, float z,
467
+ float qw, float qx, float qy, float qz,
468
+ int plan_mode = 0):
469
+ cmd = {
470
+ "type": "set_tf",
471
+ "x": x, "y": y, "z": z,
472
+ "qw": qw, "qx": qx, "qy": qy, "qz": qz,
473
+ "plan_mode": plan_mode,
474
+ }
475
+ await self.send_command(cmd)
476
+
477
+ async def enable_hold_current(self, max_vel: Optional[float] = None,
478
+ max_acc: Optional[float] = None,
479
+ max_jerk: Optional[float] = None):
480
+ cmd = {"type": "enable_hold_current"}
481
+ if max_vel is not None: cmd["max_vel"] = max_vel
482
+ if max_acc is not None: cmd["max_acc"] = max_acc
483
+ if max_jerk is not None: cmd["max_jerk"] = max_jerk
484
+ await self.send_command(cmd)
485
+
486
+ async def set_joints_batch(
487
+ self,
488
+ joint_targets: List[tuple[int, float]],
489
+ target_vel: float = 0.0,
490
+ target_tau: float = 0.0,
491
+ mode: int = 0,
492
+ enable: bool = True,
493
+ set_zero: bool = False,
494
+ max_vel: int = 10,
495
+ max_acc: int = 30,
496
+ max_jerk: int = 50,
497
+ wait_for_completion: bool = False,
498
+ timeout: float = 8.0,
499
+ tolerance: float = 0.015
500
+ ) -> bool:
501
+ if not self.communicator:
502
+ raise ArmConnectionError("Not connected yet")
503
+ if not joint_targets:
504
+ raise ValueError("joint_targets list cannot be empty")
505
+
506
+ parts = [f"{time.time():.6f}"]
507
+ for can_id, target_pos in joint_targets:
508
+ if not isinstance(can_id, int) or not isinstance(target_pos, (int, float)):
509
+ raise ValueError(f"Invalid joint target: ({can_id}, {target_pos})")
510
+ part = f"{can_id}:{target_pos:.6f}:{target_vel:.6f}:{target_tau:.6f}:{mode}:{int(enable)}:{int(set_zero)}"
511
+ if max_vel is not None:
512
+ part += f":{max_vel:.6f}"
513
+ if max_acc is not None:
514
+ part += f":{max_acc:.6f}"
515
+ if max_jerk is not None:
516
+ part += f":{max_jerk:.6f}"
517
+ parts.append(part)
518
+
519
+ raw_payload = ";".join(parts)
520
+ await self.send_raw_msg(raw_payload)
521
+
522
+ if not wait_for_completion:
523
+ return True
524
+
525
+ start_time = time.time()
526
+ while True:
527
+ state = self.current_state
528
+ if not state:
529
+ await asyncio.sleep(0.02)
530
+ continue
531
+ all_reached = True
532
+ for can_id, target_pos in joint_targets:
533
+ js = next((j for j in state.joints if j.can_id == can_id), None)
534
+ if not js or abs(js.position - target_pos) > tolerance:
535
+ all_reached = False
536
+ break
537
+ if all_reached:
538
+ print(_("All joints reached targets successfully!"))
539
+ return True
540
+ if time.time() - start_time > timeout:
541
+ print(_("Timeout waiting for joints to reach targets (tolerance: {} rad)").format(tolerance))
542
+ return False
543
+ await asyncio.sleep(0.02)
544
+
545
+ async def zero_gravity_mode(self):
546
+ await self.send_command({"type": "zero_gravity_mode"})
547
+
548
+ async def back_to_zero(self):
549
+ await self.send_command({"type": "back_to_zero"})
550
+
551
+ async def back_to_initial(self):
552
+ await self.send_command({"type": "back_to_initial"})
553
+
554
+ async def gracefully_shutdown(self, wait_before_shutdown: float = 10.0):
555
+ await self.back_to_initial()
556
+ await asyncio.sleep(wait_before_shutdown)
557
+ if self.communicator:
558
+ await self.communicator.shutdown_control()
559
+ print(_("Graceful shutdown completed."))
560
+
561
+ async def emergency_shutdown(self):
562
+ if self.communicator:
563
+ await self.communicator.shutdown_control()
564
+ print(_("Emergency shutdown executed."))
565
+
566
+ async def close(self):
567
+ if self.communicator:
568
+ await self.communicator.close()
569
+ print(_("Connection closed."))
@@ -0,0 +1,374 @@
1
+ # zeroArm_sdk/arm.py
2
+ # Copyright (c) 2026 Han / ForceEase Co. All Rights Reserved.
3
+ # Public interface for ZeroArm SDK - only for licensed hardware owners
4
+
5
+ import gettext
6
+ import os
7
+ from typing import Dict, List, Optional, Callable, Tuple
8
+ from ._core import RobotArm as _RobotArmImpl
9
+ from .arm_types import FullArmState, JointState, GripperState, Pose
10
+ from .exceptions import ArmNotFoundError, ArmConnectionError, ArmCommandError
11
+
12
+
13
+ class RobotArm(_RobotArmImpl):
14
+ """
15
+ Main class for controlling ZeroArm robotic arm.
16
+
17
+ 机械臂控制主类。
18
+
19
+ Features / 主要功能:
20
+ - Automatically find and connect to the arm / 自动发现并连接机械臂
21
+ - Connect using short serial number prefix / 支持使用序列号前缀快速连接
22
+ - Control all joints, gripper, and end-effector pose / 控制所有关节、夹爪、末端位姿
23
+ - Switch control modes (zero gravity, etc.) / 切换控制模式(零重力等)
24
+ - Graceful and emergency shutdown / 优雅关机与紧急停止
25
+ - Real-time state updates / 实时状态更新
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ sn: Optional[str] = None,
31
+ ip: Optional[str] = None,
32
+ control_port: int = 8888,
33
+ zmq_cmd_port: int = 6666,
34
+ ):
35
+ """
36
+ Create a new RobotArm instance.
37
+
38
+ 创建 RobotArm 实例。
39
+
40
+ Args / 参数:
41
+ sn: Full serial number of the arm (optional) / 机械臂完整序列号(可选)
42
+ ip: IP address of the arm (optional) / 机械臂 IP 地址(可选)
43
+ control_port: WebSocket control port, default 8888 / 控制端口,默认 8888
44
+ zmq_cmd_port: ZMQ command port, default 6666 / 指令端口,默认 6666
45
+
46
+ Note / 注意:
47
+ - If both sn and ip are None, use auto_discover_and_connect() to find the arm automatically.
48
+ 如果 sn 和 ip 均未提供,可通过 auto_discover_and_connect() 自动发现机械臂。
49
+ """
50
+ super().__init__(sn, ip, control_port, zmq_cmd_port)
51
+
52
+ async def find_all_devices(self, timeout: float = 1.0) -> List[Dict]:
53
+ """
54
+ Find all available ZeroArm devices on the network.
55
+
56
+ 在局域网内发现所有可用的 ZeroArm 设备。
57
+
58
+ Args / 参数:
59
+ timeout: Maximum time to wait for discovery (seconds, default 1.0) / 发现最大等待时间(秒,默认 1.0)
60
+
61
+ Returns / 返回:
62
+ List[Dict]: List of device information dictionaries / 设备信息字典列表
63
+
64
+ Example / 示例:
65
+ devices = await arm.find_all_devices(timeout=2.0)
66
+ print(devices)
67
+ """
68
+ return await super().find_all_devices(timeout)
69
+
70
+ async def auto_discover_and_connect(self, timeout: float = 1.0) -> bool:
71
+ """
72
+ Automatically find the first available arm and connect to it.
73
+ Shows a progress bar during connection.
74
+
75
+ 自动发现局域网内第一个可用机械臂并连接。
76
+ 连接过程中显示进度条。
77
+
78
+ Args / 参数:
79
+ timeout: Maximum wait time for discovery (seconds, default 1.0) / 发现最大等待时间(秒,默认 1.0)
80
+
81
+ Returns / 返回:
82
+ bool: True if connected successfully, False otherwise / 连接成功返回 True,否则 False
83
+
84
+ Example / 示例:
85
+ success = await arm.auto_discover_and_connect(timeout=3.0)
86
+ if success:
87
+ print(_("Arm is ready!")) # 支持本地化:已连接机械臂!
88
+ """
89
+ return await super().auto_discover_and_connect(timeout)
90
+
91
+ async def connect_with_short_sn(self, short_sn: str, timeout: float = 1.0) -> bool:
92
+ """
93
+ Connect to an arm using a serial number prefix.
94
+
95
+ 使用序列号前缀连接机械臂,建议使用前 8 位以避免多臂环境中误匹配。
96
+
97
+ Args / 参数:
98
+ short_sn: Serial number prefix / 序列号前缀(建议至少 8 位)
99
+ timeout: Maximum wait time for discovery (seconds, default 1.0) / 发现最大等待时间(秒,默认 1.0)
100
+
101
+ Returns / 返回:
102
+ bool: True if connected successfully, False otherwise / 连接成功返回 True,否则 False
103
+
104
+ Example / 示例:
105
+ await arm.connect_with_short_sn("F82FCDC0")
106
+ """
107
+ return await super().connect_with_short_sn(short_sn, timeout)
108
+
109
+ async def connect(self) -> bool:
110
+ """
111
+ Connect to the arm using current serial number and IP.
112
+
113
+ 使用当前已设置的序列号和 IP 进行连接。
114
+
115
+ Returns / 返回:
116
+ bool: True if successful, False otherwise / 连接成功返回 True,否则 False
117
+
118
+ Note / 注意:
119
+ - Must set sn and ip first (via __init__ or manually).
120
+ 必须先设置 sn 和 ip。
121
+ """
122
+ return await super().connect()
123
+
124
+ def is_connected(self) -> bool:
125
+ """
126
+ Check if the arm is currently connected.
127
+
128
+ 检查当前是否已连接到机械臂。
129
+
130
+ Returns / 返回:
131
+ bool: True if connected, False otherwise / 已连接返回 True,否则 False
132
+ """
133
+ return super().is_connected()
134
+
135
+ @property
136
+ def current_state(self) -> Optional[FullArmState]:
137
+ """
138
+ Get the latest received arm state.
139
+
140
+ 获取机械臂的最新状态(实时更新)。
141
+
142
+ Returns / 返回:
143
+ FullArmState or None: Latest arm state / 最新机械臂状态;若无数据则返回 None
144
+
145
+ Example / 示例:
146
+ state = arm.current_state
147
+ if state:
148
+ print(f"Joint 1: {state.joints[0].position:.3f} rad")
149
+ """
150
+ return super().current_state
151
+
152
+ def on_state_update(self, callback: Callable[[FullArmState], None]):
153
+ """
154
+ Set a callback function to be called on every new state update.
155
+
156
+ 设置状态更新回调函数,每次接收到新状态时调用。
157
+
158
+ Args / 参数:
159
+ callback: Function that receives FullArmState / 接收 FullArmState 的回调函数
160
+
161
+ Example / 示例:
162
+ def handler(state):
163
+ print(f"End-effector pose: {state.eef_pose}")
164
+ arm.on_state_update(handler)
165
+ """
166
+ super().on_state_update(callback)
167
+
168
+ async def set_joint(
169
+ self,
170
+ can_id: int,
171
+ target_pos: float,
172
+ target_vel: float = 0.0,
173
+ target_tau: float = 0.0,
174
+ mode: int = 0,
175
+ enable: bool = True,
176
+ set_zero: bool = False,
177
+ max_vel: Optional[float] = None,
178
+ max_acc: Optional[float] = None,
179
+ max_jerk: Optional[float] = None,
180
+ ):
181
+ """
182
+ Move a single joint to target position.
183
+
184
+ 控制单个关节运动到目标位置。
185
+
186
+ Args / 参数:
187
+ can_id: Joint CAN ID (usually starts from 1) / 关节 CAN ID(通常从 1 开始)
188
+ target_pos: Target position in radians / 目标位置(弧度)
189
+ target_vel: Target velocity (rad/s), default 0.0 / 目标速度(弧度/秒,默认 0.0)
190
+ target_tau: Target torque (Nm), default 0.0 / 目标力矩(Nm,默认 0.0)
191
+ mode: Control mode (0 = position mode, see arm manual) / 控制模式(0=位置模式,参考臂说明书)
192
+ enable: Enable the joint, default True / 是否使能关节,默认 True
193
+ set_zero: Set current position as zero, default False / 是否将当前位置设为零位,默认 False
194
+ max_vel/max_acc/max_jerk: Optional speed/acceleration/jerk limits / 可选的最大速度/加速度/加加速度限制
195
+
196
+ Example / 示例:
197
+ await arm.set_joint(1, 0.785) # Move joint 1 to 45° / 将关节1移动到45°
198
+ """
199
+ await super().set_joint(
200
+ can_id,
201
+ target_pos,
202
+ target_vel,
203
+ target_tau,
204
+ mode,
205
+ enable,
206
+ set_zero,
207
+ max_vel,
208
+ max_acc,
209
+ max_jerk,
210
+ )
211
+
212
+ async def set_gripper(
213
+ self,
214
+ target_pos: float,
215
+ target_vel: float = 0.0,
216
+ target_tau: float = 0.0,
217
+ set_zero: bool = False,
218
+ ):
219
+ """
220
+ Control gripper opening/closing.
221
+
222
+ 控制夹爪开合。
223
+
224
+ Args / 参数:
225
+ target_pos: Target gripper position / 目标开合位置
226
+ target_vel: Gripper velocity, default 0.0 / 夹爪运动速度,默认 0.0
227
+ target_tau: Gripper torque, default 0.0 / 夹爪力矩,默认 0.0
228
+ set_zero: Set current position as zero, default False / 是否设为零位,默认 False
229
+
230
+ Example / 示例:
231
+ await arm.set_gripper(0.08) # Open gripper to 8cm / 将夹爪打开到8cm
232
+ """
233
+ await super().set_gripper(target_pos, target_vel, target_tau, set_zero)
234
+
235
+ async def set_tf(
236
+ self,
237
+ x: float,
238
+ y: float,
239
+ z: float,
240
+ qw: float,
241
+ qx: float,
242
+ qy: float,
243
+ qz: float,
244
+ plan_mode: int = 0,
245
+ ):
246
+ """
247
+ Set end-effector position and orientation (quaternion).
248
+
249
+ 设置末端执行器位姿(位置 + 四元数姿态)。
250
+
251
+ Args / 参数:
252
+ x, y, z: Position in meters / 位置(米)
253
+ qw, qx, qy, qz: Orientation quaternion / 姿态四元数
254
+ plan_mode: 0 = joint-space planning, 1 = Cartesian planning / 0=关节空间,1=笛卡尔
255
+
256
+ Example / 示例:
257
+ await arm.set_tf(0.5, 0.0, 0.3, 1.0, 0.0, 0.0, 0.0, plan_mode=1)
258
+ """
259
+ await super().set_tf(x, y, z, qw, qx, qy, qz, plan_mode)
260
+
261
+ async def enable_hold_current(
262
+ self,
263
+ max_vel: Optional[float] = None,
264
+ max_acc: Optional[float] = None,
265
+ max_jerk: Optional[float] = None,
266
+ ):
267
+ """
268
+ Hold all configured joints at their current feedback positions.
269
+
270
+ 以当前反馈位置重新保持所有已配置关节。
271
+ """
272
+ await super().enable_hold_current(max_vel, max_acc, max_jerk)
273
+
274
+ async def set_joints_batch(
275
+ self,
276
+ joint_targets: List[Tuple[int, float]],
277
+ target_vel: float = 0.0,
278
+ target_tau: float = 0.0,
279
+ mode: int = 0,
280
+ enable: bool = True,
281
+ set_zero: bool = False,
282
+ max_vel: int = 10,
283
+ max_acc: int = 30,
284
+ max_jerk: int = 50,
285
+ wait_for_completion: bool = False,
286
+ timeout: float = 8.0,
287
+ tolerance: float = 0.015,
288
+ ) -> bool:
289
+ """
290
+ Move multiple joints simultaneously (recommended for synchronized motion).
291
+
292
+ 同时控制多个关节运动(推荐用于同步轨迹规划)。
293
+
294
+ Args / 参数:
295
+ joint_targets: List of (can_id, target_pos) tuples / 关节目标列表:[(can_id, 目标位置), ...]
296
+ target_vel/target_tau: Reserved parameters (currently not used) / 保留参数(当前未使用)
297
+ mode/enable/set_zero: Global mode, enable, zero setting / 全局模式、使能、零位设置
298
+ max_vel/max_acc/max_jerk: Global limits (deg/s, deg/s², deg/s³) / 全局最大速度/加速度/加加速度限制
299
+ wait_for_completion: Wait until all joints reach targets, default False / 是否等待到达目标,默认 False
300
+ timeout: Timeout in seconds (when waiting) / 等待超时时间(秒)
301
+ tolerance: Position tolerance in radians, default 0.015 (~0.86°) / 位置容差(弧度,默认 0.015)
302
+
303
+ Returns / 返回:
304
+ bool: Command sent successfully (or reached targets if waiting) / 命令发送成功(或等待成功)
305
+
306
+ Example / 示例:
307
+ await arm.set_joints_batch([(1, 0.5), (2, -0.3), (5, 1.57)])
308
+ """
309
+ return await super().set_joints_batch(
310
+ joint_targets,
311
+ target_vel,
312
+ target_tau,
313
+ mode,
314
+ enable,
315
+ set_zero,
316
+ max_vel,
317
+ max_acc,
318
+ max_jerk,
319
+ wait_for_completion,
320
+ timeout,
321
+ tolerance,
322
+ )
323
+
324
+ async def zero_gravity_mode(self):
325
+ """
326
+ Switch to zero gravity mode (free manual movement).
327
+
328
+ 切换到零重力模式(可手动自由拖动机械臂)。
329
+ """
330
+ await super().zero_gravity_mode()
331
+
332
+ async def back_to_zero(self):
333
+ """
334
+ Move all joints back to zero position.
335
+
336
+ 将所有关节移动回零位。
337
+ """
338
+ await super().back_to_zero()
339
+
340
+ async def back_to_initial(self):
341
+ """
342
+ Move all joints to factory initial positions.
343
+
344
+ 将所有关节移动到出厂初始位置。
345
+ """
346
+ await super().back_to_initial()
347
+
348
+ async def gracefully_shutdown(self, wait_before_shutdown: float = 10.0):
349
+ """
350
+ Gracefully shutdown: move to initial pose → wait → power off.
351
+
352
+ 优雅关机:先移动到初始位置 → 等待 → 安全断电。
353
+
354
+ Args / 参数:
355
+ wait_before_shutdown: Wait time after movement (seconds, default 10.0) / 运动完成后等待时间(秒,默认 10.0)
356
+ """
357
+ await super().gracefully_shutdown(wait_before_shutdown)
358
+
359
+ async def emergency_shutdown(self):
360
+ """
361
+ Immediate emergency stop and shutdown.
362
+
363
+ 紧急停止并关机(立即停止所有运动并断电)。
364
+ Use only in emergency situations / 仅在紧急情况下使用。
365
+ """
366
+ await super().emergency_shutdown()
367
+
368
+ async def close(self):
369
+ """
370
+ Close connection and release resources.
371
+
372
+ 关闭连接并释放所有资源。
373
+ """
374
+ await super().close()
@@ -0,0 +1,40 @@
1
+ # zeroArm_sdk/arm_types.py
2
+ from dataclasses import dataclass
3
+ from typing import List, Optional
4
+
5
+
6
+ @dataclass
7
+ class JointState:
8
+ can_id: int
9
+ position: float # radians
10
+ velocity: float
11
+ torque: float
12
+ enable: bool
13
+ mode: int
14
+
15
+
16
+ @dataclass
17
+ class GripperState:
18
+ position: float
19
+ velocity: float
20
+ torque: float
21
+ enable: bool
22
+ mode: int
23
+
24
+
25
+ @dataclass
26
+ class Pose:
27
+ x: float
28
+ y: float
29
+ z: float
30
+ qw: float
31
+ qx: float
32
+ qy: float
33
+ qz: float
34
+
35
+
36
+ @dataclass
37
+ class FullArmState:
38
+ joints: List[JointState]
39
+ gripper: Optional[GripperState] = None
40
+ eef_pose: Optional[Pose] = None
@@ -0,0 +1,15 @@
1
+ # zeroArm_sdk/exceptions.py
2
+
3
+ class ArmNotFoundError(Exception):
4
+ """No ZeroArm device found on the network."""
5
+ pass
6
+
7
+
8
+ class ArmConnectionError(Exception):
9
+ """Failed to connect, select SN, or establish communication."""
10
+ pass
11
+
12
+
13
+ class ArmCommandError(Exception):
14
+ """Command execution failed or invalid response."""
15
+ pass
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: zeroArm_sdk
3
+ Version: 0.1.2
4
+ Summary: Official SDK for ZeroArm robotic arm (proprietary)
5
+ Home-page: https://github.com/HANCKH/zeroArm_python_sdk
6
+ Author: ForceEase Co.
7
+ Author-email: "ForceEase Co." <support@forceease.tech>
8
+ License: MIT
9
+ Project-URL: Source, https://github.com/HANCKH/zeroArm_python_sdk
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Cython
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: pyzmq>=25.0
16
+ Requires-Dist: websockets
17
+ Requires-Dist: zeroconf
18
+ Requires-Dist: tqdm
19
+ Dynamic: author
20
+ Dynamic: home-page
21
+ Dynamic: license
22
+ Dynamic: requires-python
23
+
24
+ # ZeroArm Python SDK
25
+
26
+ Official Python SDK for ForceEase ZeroArm robotic arms.
27
+
28
+ ## Connection Notes
29
+
30
+ - Use `RobotArm().auto_discover_and_connect()` for the default LAN workflow.
31
+ - Use `connect_with_short_sn()` with at least the first 8 serial-number characters in multi-arm environments.
32
+ - mDNS discovery only accepts connectable IPv4 addresses and ignores loopback or unspecified addresses such as `127.0.0.1` and `0.0.0.0`.
33
+ - The controller advertises WebSocket control port `8888` and bridge port `8889`; SDK mode uses the fixed ZMQ command/state ports `6666/6667`.
34
+
35
+ ## Current Protocol Coverage
36
+
37
+ - `set_tf(..., plan_mode=0)` uses joint-space planning; `plan_mode=1` uses Cartesian planning.
38
+ - `enable_hold_current()` asks the controller to hold all configured joints at their current feedback positions.
39
+ - `set_joints_batch()` sends a single `raw_msg` command through the normal `commands_<serial>` topic.
40
+
41
+ ## Build Notes
42
+
43
+ - `pyproject.toml` declares Cython as a build-system dependency for isolated builds.
44
+ - For direct `setup.py` builds, install `requirements-build.txt` first.
45
+ - `setuptools` is capped below 80 to stay compatible with ROS `colcon-core` in the shared development environment.
46
+
47
+ ## Release
48
+
49
+ - Publish Python SDK releases through `.github/workflows/build-wheels.yml`.
50
+ - A GitHub Release with action `published` builds the sdist plus Linux, Windows, and macOS wheels, then uploads all distributions to PyPI in one publish job.
51
+ - The workflow expects `PYPI_API_TOKEN` to be configured as a GitHub Actions secret.
52
+
53
+ ## Version
54
+
55
+ Current SDK source version: `0.1.2`.
@@ -0,0 +1,16 @@
1
+ LICENSE.txt
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ zeroArm_sdk/__init__.py
7
+ zeroArm_sdk/_core.pyx
8
+ zeroArm_sdk/arm.py
9
+ zeroArm_sdk/arm_types.py
10
+ zeroArm_sdk/exceptions.py
11
+ zeroArm_sdk.egg-info/PKG-INFO
12
+ zeroArm_sdk.egg-info/SOURCES.txt
13
+ zeroArm_sdk.egg-info/dependency_links.txt
14
+ zeroArm_sdk.egg-info/requires.txt
15
+ zeroArm_sdk.egg-info/top_level.txt
16
+ zeroArm_sdk/locale/zh_CN/LC_MESSAGES/zeroArm_sdk.mo
@@ -0,0 +1,4 @@
1
+ pyzmq>=25.0
2
+ websockets
3
+ zeroconf
4
+ tqdm
@@ -0,0 +1 @@
1
+ zeroArm_sdk