Metadata-Version: 2.4
Name: zeroArm_sdk
Version: 0.3.2
Summary: Official SDK for ZeroArm robotic arm (proprietary)
Home-page: https://github.com/HANCKH/zeroArm_python_sdk
Author: ForceEase Co.
Author-email: "ForceEase Co." <support@forceease.tech>
License: MIT
Project-URL: Source, https://github.com/HANCKH/zeroArm_python_sdk
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Cython
Classifier: Operating System :: OS Independent
Requires-Python: >=3.9,<3.13
Description-Content-Type: text/markdown
Requires-Dist: websockets>=15.0.1
Requires-Dist: zeroconf
Provides-Extra: camera
Requires-Dist: numpy>=1.21; extra == "camera"
Dynamic: author
Dynamic: home-page
Dynamic: license
Dynamic: requires-python

# ZeroArm Python SDK

Current SDK source version: `0.3.2`.

Maintenance boundary: [`docs/zeroarm-framework-integration.md`](docs/zeroarm-framework-integration.md).

Version 0.3 uses the Cmd protocol-v3 device WebSocket bridge as the GUI and
ROS SDK. Before any motion command is enabled, `connect()` verifies both the
firmware version and the advertised SDK protocol capability, waits for a fresh
operational state, and opens a Motion-confirmed Cmd session. Requests carry a
32-hex runtime/session/request identity and a frozen device time anchor.
`resume()` reattaches a dropped bridge to the existing fenced session; a fresh
`connect()` opens a new session.

Requirements:

- ZeroArm firmware `1.1.0` or newer
- `sdk_protocol_version = 3`
- Python 3.9–3.12

Controller WebSocket and camera HTTP traffic always connects directly to the
device and doesn't use operating-system or environment HTTP proxies.

```bash
pip install zeroArm_sdk
```

```python
import asyncio
from zeroArm_sdk import RobotArm


async def main():
    arm = RobotArm(slot="left", mount="upright")
    await arm.auto_discover_and_connect()
    print(arm.firmware_version, arm.sdk_protocol_version)
    try:
        await arm.enable_hold()
        await arm.move_once(
            {"type": "JOINT", "q_rad": [0.2, -0.3, 0, 0, 0, 0]},
            {"path": "JOINT", "speed_scale": 1.0},
        )
        await arm.set_gripper(0.03)
    finally:
        await arm.close()


asyncio.run(main())
```

Use `end_effector="none"`, a gripper configuration, or a screwdriver
configuration such as `{"type": "screwdriver", "default_vel": 2,
"max_vel": 6}`. If it is omitted, the SDK leaves the device's existing
end-effector configuration unchanged.

Normal-control APIs intentionally do not expose motor zeroing. Calibration and
zero writes belong to the maintenance workflow.

Main methods:

- `move_once()`
- `enable_hold()` / `start_gravity_compensation()`
- `set_gripper()` / `set_gripper_torque()`
- `set_screwdriver()`
- `start_stream()` / `stream_update()` / `cancel_operation()`
- `get_status()` / `get_operation()` / `get_request_outcome()`
- `gracefully_shutdown()` / `emergency_shutdown()`

Command rejection, stale state, Cmd session loss, incompatible firmware, and
transport failures raise the typed exceptions exported by `zeroArm_sdk`.

The device publishes its authoritative model, tool and wire capabilities. Normal
connect reads that prepared configuration and does not upload URDF or guess an
arm model. Install and maintenance tools own provisioning changes.

## 相机画面与深度

使用包含 camera frame protocol v1 的控制器固件和 Python SDK 0.3.0 或更高版本。
相机图像依赖 NumPy，请安装 `camera` extra：

```bash
pip install 'zeroArm_sdk[camera]>=0.3.0'
```

```python
import asyncio
from zeroArm_sdk import CameraClient

async def main():
    camera = CameraClient("192.168.1.10")  # 只有一台已配置相机时自动选择
    print(await camera.list_cameras())  # 只查询，不启动视频服务
    await camera.start()  # 经控制器启动共享相机，等待首帧；不申请机械臂控制权
    frame = await camera.read_frame()
    color = frame.color_bgr       # H×W×3，numpy.uint8，BGR 顺序
    depth = frame.depth_mm       # H×W，numpy.uint16，毫米；普通 UVC 为 None
    print(frame.frame_id, frame.timestamp_ms, frame.intrinsics)
    if depth is not None:
        h, w = depth.shape
        print(frame.depth_at(w // 2, h // 2))  # 光轴 Z 深度，单位米；无效值返回 None
    # await camera.stop()         # 显式停流会影响其他 GUI/SDK 读者

asyncio.run(main())
```

- `list_cameras()` 从控制端口查询 Product Profile，返回 `camera_id`、`provider`、`attached_to`、
  `capabilities` 和 `available`（缺失时为 `None`）。这是已配置相机列表，`available` 仅为控制器提示，
  不能当作物理相机在线或首帧就绪证明；无需先启动相机服务。仍须提供控制器 host。
- 省略 `camera_id` 时，首次 `start()` / `health()` / `read_frame()` / `stop()` 自动解析唯一相机。
  也可 `await camera.select_camera()` 只完成选择。零台会报错，多台会报出全部候选 ID；用
  `CameraClient(host, camera_id="left_hand_camera")` 指定，不按列表顺序或 `available` 猜测。
  选定后 `camera.camera_id` 保持绑定；`list_cameras()` 可刷新列表，但不会重新绑定或启停相机。
- `camera_id` 使用 Product Profile 的 `peripheral_id`，例如 `left_hand_camera`，不是 USB
  下标或机械臂序列号。多台 D405/D435 应在控制器配置中使用完整相机 serial 明确绑定。
- `read_color()`、`read_depth()` 可分别读取图像；需要匹配的 RGBD 时使用一次 `read_frame()`，
  两次独立读取可能来自不同帧。深度已对齐到彩色像素，0 表示无效，数组可直接交给 OpenCV/NumPy。
- D405/D435 返回 RGBD；UVC 只返回彩色，调用 `read_depth()` 会抛出 `CameraError`。
- `timestamp_ms` 来自相机时钟，不是 Unix 时间；轮询可能读到相同 `frame_id`，读取上限不是相机帧率承诺。
  设备端拒绝超过 2 秒的旧帧。原始 BGR/深度数据较大，按需要控制客户端读取频率。
- 默认控制端口 8888、相机 HTTP 端口 8892，可用 `control_port` / `camera_port` 和 `timeout` 调整。
- `start()` 遵守控制器已有相机占用门禁；标定/抓取正在占用时会明确报错。构造、读帧、对象销毁都不抢占/停流。
- 不需要连接 `RobotArm`、手眼标定、机械臂位姿或运动 lease；需要基座坐标系点云时仍应使用完整标定流程。

完整示例：[examples/camera_rgbd.py](examples/camera_rgbd.py)。

### 相机接口速查

```text
CameraClient(host, camera_id=None, *, control_port=8888, camera_port=8892, timeout=5.0)
```

`host` 为控制器 IP 或主机名，不包含 `http://`；不是运行脚本的电脑或相机 USB 地址。
端口范围为 1–65535，`timeout` 为正数秒。查询配置只需要控制端口，读帧还需要相机 HTTP 端口。
构造参数错误抛出 `ValueError`；通信、配置发现、启动和帧协议错误抛出 `CameraError`。

| 调用（在协程中使用 `await`） | 返回值与行为 |
|---|---|
| `list_cameras()` | 相机字典列表；不会选择或启动相机 |
| `select_camera()` | 选定的 ID 字符串；不会启动，显式 ID 会直接保留 |
| `start()` | 首帧就绪后的 health 字典；启动共享相机 |
| `health()` | 包含 `ok`、`camera_frame_ready`、`diagnostics` 的状态字典；不启动相机服务 |
| `read_frame()` | `CameraFrame`，包含同帧彩色、可选深度和元数据 |
| `read_color()` / `read_depth()` | 单独返回 BGR 数组 / 毫米深度数组 |
| `stop()` | `None`；停止共享相机，影响其他读者 |

`CameraFrame.metadata` 包含实际设备 `serial`、`provider`、尺寸、帧龄和内参等信息。
`frame.depth_at(x, y)` 是同步方法：坐标对应彩色像素，无深度时抛出 `CameraError`，
坐标越界抛出 `IndexError`。它返回光轴 Z 距离，不是点到相机原点的径向距离。

### 多相机：先列出，再指定

```python
import asyncio
from zeroArm_sdk import CameraClient, CameraError

async def main():
    host = "192.168.1.10"
    discovery = CameraClient(host)
    try:
        for item in await discovery.list_cameras():
            print(item["camera_id"], item["provider"], item["attached_to"], item["capabilities"])
        # 将下方 ID 替换为列表中实际需要的相机，不自动取第一项。
        camera = CameraClient(host, camera_id="left_hand_camera")
        await camera.start()
        frame = await camera.read_frame()
        print(frame.color_bgr.shape, frame.metadata["serial"])
    except CameraError as error:
        print(f"相机操作失败：{error}")

asyncio.run(main())
```

返回条目示例（配置提示不代表已经出图）：

```json
{
  "camera_id": "left_hand_camera",
  "provider": "d435",
  "attached_to": "left_arm",
  "capabilities": ["rgb_preview", "depth", "hand_eye_calibration", "point_cloud_capture"],
  "available": true
}
```

### 示例命令与排查

在 SDK 仓库根目录运行：

```bash
python examples/camera_rgbd.py --host 192.168.1.10 --list
python examples/camera_rgbd.py --host 192.168.1.10
python examples/camera_rgbd.py --host 192.168.1.10 --camera-id left_hand_camera
```

读取示例保存 `camera_frame.npz`；`--list` 只打印配置。退出示例不会停止其他读者的相机。

| 错误或现象 | 处理方式 |
|---|---|
| `No cameras configured` / `profile not ready` | 检查控制器 Product Profile 是否配置相机 |
| `Multiple cameras configured` | 从列表选择 ID，重新构造带 `camera_id` 的客户端 |
| 缺少 `CameraClient` / `list_cameras` 或帧协议 v1 | 安装 `zeroArm_sdk[camera]>=0.3.0` 并升级到配套固件 |
| `camera_not_started` / HTTP 连接失败 | 显式 `start()`；检查控制器地址、8888/8892 或自定义端口 |
| `busy_by_calibration` / `busy_by_capture` | 等占用任务结束后重试，不绕过占用门禁 |
| `stale_frame` / `depth_not_ready` / 首帧超时 | 检查物理连接、固件日志、型号与 serial 配置；UVC 本身无深度流 |
