Metadata-Version: 2.4
Name: zeroArm_sdk
Version: 0.3.0
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
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.0`

Version 0.3 uses the same device WebSocket protocol-v2 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 acquires an exclusive renewable control lease.

Requirements:

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

```bash
pip install zeroArm_sdk
```

```python
import asyncio
from zeroArm_sdk import RobotArm


async def main():
    arm = RobotArm(
        arm_model="v4",
        slot="left",
        mount="upright",
        end_effector={
            "type": "gripper",
            "model": "standardParallel",
            "ff_kp": 4,
        },
    )
    await arm.auto_discover_and_connect()
    print(arm.firmware_version, arm.sdk_protocol_version)
    try:
        await arm.set_joints_batch(
            [(1, 0.2), (2, -0.3)],
            max_vel=3.0,
            max_acc=5.0,
            max_jerk=5.0,
            wait_for_completion=True,
        )
        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:

- `set_joint()` / `set_joints_batch()`
- `set_tf()`
- `set_gripper()` / `set_gripper_torque()`
- `set_screwdriver()`
- `enable_hold_current()` / `zero_gravity_mode()`
- `back_to_zero()` / `back_to_initial()`
- `gracefully_shutdown()` / `emergency_shutdown()`

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

Firmware 1.1.0+ requires the provisioned Product Profile to match the requested SN, SDK slot, model and mount. The SDK checks the local model first, downloads and verifies a missing URDF on the client, and uploads it before starting the runtime. Cached models work offline. A missing unpublished model or any identity/integrity failure stops connection.

## 相机画面与深度

使用包含 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 本身无深度流 |
