Skip to content
SDK 最小可执行用例

SDK 最小可执行用例#

下面的 sdk_minimal_examples.pyAirbotClient 的 37 个公开入口分别提供一个命令。每次只运行 一个用例,接口名就是命令参数。完整代码默认折叠,展开后可以直接复制保存:

完整代码:sdk_minimal_examples.py
sdk_minimal_examples.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
#!/usr/bin/env python3
"""Run one minimal example for every public AirbotClient entry."""

from __future__ import annotations

import argparse
import math
import time
from collections.abc import Callable
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Any


class ExampleError(RuntimeError):
    """The example cannot continue or cannot confirm safe cleanup."""


LIFECYCLE_EXAMPLES = ("__init__", "__enter__", "__exit__", "close")
IDENTITY_EXAMPLES = (
    "get_client_id",
    "get_source_name",
    "get_client_instance_id",
    "get_client_session_id",
)
CONTROL_EXAMPLES = (
    "acquire_control",
    "handover_to_previous",
    "release_control",
)
MODE_EXAMPLES = (
    "set_arm_speed",
    "set_eef_speed",
    "switch_controller",
    "enter_gravity_compensation_mode",
    "get_eef_mode",
    "switch_eef_control_mode",
)
STATE_EXAMPLES = (
    "get_service_state",
    "get_arm_joint_state",
    "get_arm_motor_state",
    "get_eef_joint_state",
    "get_eef_motor_state",
    "get_imu_state",
    "get_end_pose",
    "get_firmware_info",
)
RECOVERY_EXAMPLES = (
    "set_arm_emergency_stop",
    "clear_error",
    "return_zero",
    "clear_eef_motor_err",
    "clear_arm_motor_err",
)
MOTION_EXAMPLES = (
    "move_joint",
    "move_eef",
    "move_end_pose",
    "move_end_pose_linear",
    "move_end_pose_circle",
    "move_joint_waypoints",
    "move_end_pose_waypoints",
)
PUBLIC_EXAMPLES = (
    LIFECYCLE_EXAMPLES
    + IDENTITY_EXAMPLES
    + CONTROL_EXAMPLES
    + MODE_EXAMPLES
    + STATE_EXAMPLES
    + RECOVERY_EXAMPLES
    + MOTION_EXAMPLES
)

CONFIRMATION_TOKENS = {
    "acquire_control": "CONTROL",
    "handover_to_previous": "CONTROL",
    "release_control": "CONTROL",
    "set_arm_speed": "CONFIG",
    "set_eef_speed": "CONFIG",
    "switch_controller": "CONTROL",
    "enter_gravity_compensation_mode": "MOVE",
    "switch_eef_control_mode": "CONTROL",
    "set_arm_emergency_stop": "STOP",
    "clear_error": "RECOVER",
    "return_zero": "MOVE",
    "clear_eef_motor_err": "RECOVER",
    "clear_arm_motor_err": "RECOVER",
    **{name: "MOVE" for name in MOTION_EXAMPLES},
}

CARTESIAN_STEP_EXAMPLES = {
    "move_end_pose_linear",
    "move_end_pose_circle",
    "move_end_pose_waypoints",
}


@dataclass(frozen=True)
class RunInputs:
    """Explicit task inputs that cannot be safely guessed by an example."""

    cartesian_step_m: float | None = None
    eef_target_mm: tuple[float, ...] | None = None
    eef_eff: tuple[float, ...] | None = None
    eef_speed_mm_s: float = 50.0


def _parse_finite_csv(name: str, raw: str | None) -> tuple[float, ...] | None:
    if raw is None:
        return None
    values = tuple(float(item.strip()) for item in raw.split(",") if item.strip())
    if not values or not all(math.isfinite(item) for item in values):
        raise ValueError(f"{name} must contain finite comma-separated numbers")
    return values


def inputs_from_args(args: argparse.Namespace) -> RunInputs:
    """Read optional, task-specific motion values from command-line arguments."""
    return RunInputs(
        cartesian_step_m=args.cartesian_step_m,
        eef_target_mm=_parse_finite_csv("--eef-target-mm", args.eef_target_mm),
        eef_eff=_parse_finite_csv("--eef-eff", args.eef_eff),
        eef_speed_mm_s=args.eef_speed_mm_s,
    )


def client_options_from_args(args: argparse.Namespace) -> dict[str, Any]:
    """Build explicit gRPC or DDS client options from command-line arguments."""
    backend = args.backend
    common = {"client_name": "docs-sdk-minimal-example"}
    if backend == "grpc":
        if not args.host:
            raise ValueError("--host is required for --backend grpc")
        return {
            **common,
            "backend": "grpc",
            "host": args.host,
            "port": args.port,
        }
    if backend == "dds":
        if args.domain_id is None:
            raise ValueError("--domain-id is required for --backend dds")
        return {
            **common,
            "backend": "dds",
            "domain_id": args.domain_id,
            "side": args.side,
        }
    raise ValueError("--backend must be 'grpc' or 'dds'")


def _require_confirmation(name: str, confirmation: str | None) -> None:
    expected = CONFIRMATION_TOKENS.get(name)
    if expected is not None and confirmation != expected:
        raise ExampleError(f"{name} requires --confirm {expected}")


def _validate_run_inputs(name: str, inputs: RunInputs) -> None:
    if name in CARTESIAN_STEP_EXAMPLES:
        step = inputs.cartesian_step_m
        if step is None:
            raise ExampleError(
                f"{name} requires --cartesian-step-m from an approved workcell path"
            )
        if not math.isfinite(step) or step == 0.0 or abs(step) > 0.005:
            raise ExampleError("--cartesian-step-m must be finite and within ±0.005 m")

    if name == "move_eef":
        target = inputs.eef_target_mm
        effort = inputs.eef_eff
        speed = inputs.eef_speed_mm_s
        if target is None or effort is None:
            raise ExampleError(
                "move_eef requires approved --eef-target-mm and --eef-eff values"
            )
        if len(target) != len(effort):
            raise ExampleError(
                "--eef-target-mm and --eef-eff must have equal lengths"
            )
        if not math.isfinite(speed) or not 10.0 <= speed <= 1000.0:
            raise ExampleError("--eef-speed-mm-s must be within [10, 1000] mm/s")


def _require_true(result: Any, action: str) -> None:
    if result is not True:
        raise ExampleError(f"{action} returned {result!r}")


def _require_idle_snapshot(client: Any) -> tuple[float, ...]:
    service = client.get_service_state()
    if service is None or not service.valid or not service.service_state:
        raise ExampleError("service state is unavailable or stale")
    if service.fsm_state != "IDLE":
        raise ExampleError(f"expected IDLE before the example, got {service.fsm_state}")

    joints = client.get_arm_joint_state()
    if joints is None or len(joints.angles) != 7:
        raise ExampleError("arm joint state must contain seven values")
    angles = tuple(float(value) for value in joints.angles)
    if not all(math.isfinite(value) for value in angles):
        raise ExampleError("arm joint state contains a non-finite value")

    motors = client.get_arm_motor_state()
    if motors is None or len(motors.error_ids) != 7:
        raise ExampleError("arm motor state must contain seven error codes")
    if any(code != 0 for code in motors.error_ids):
        raise ExampleError(f"arm motor errors are present: {motors.error_ids}")
    return angles


def _require_unknown_error(client: Any) -> None:
    service = client.get_service_state()
    if service is None or not service.valid or not service.service_state:
        raise ExampleError("service state is unavailable or stale")
    if service.fsm_state != "UNKNOWN_ERROR":
        raise ExampleError(f"expected UNKNOWN_ERROR, got {service.fsm_state}")


def _require_arm_motor_errors(client: Any) -> None:
    motors = client.get_arm_motor_state()
    if motors is None or len(motors.error_ids) != 7:
        raise ExampleError("arm motor state must contain seven error codes")
    if not any(code != 0 for code in motors.error_ids):
        raise ExampleError("no nonzero arm motor error code is available to clear")


def _require_pose(client: Any) -> Any:
    pose = client.get_end_pose()
    if pose is None or len(pose.position) != 3 or len(pose.orientation) != 4:
        raise ExampleError("current end pose is unavailable or malformed")
    values = tuple(pose.position) + tuple(pose.orientation)
    if not all(math.isfinite(value) for value in values):
        raise ExampleError("current end pose contains a non-finite value")
    quaternion_norm = math.sqrt(sum(value * value for value in pose.orientation))
    if not math.isclose(quaternion_norm, 1.0, rel_tol=0.0, abs_tol=1e-3):
        raise ExampleError("current end pose does not contain a unit quaternion")
    return pose


def _require_eef(client: Any) -> None:
    mode = client.get_eef_mode()
    if mode is None or mode.get("has_eef") is not True:
        raise ExampleError("the runtime does not report an available EEF")
    firmware = client.get_firmware_info()
    if firmware is None or not str(firmware.eef_type).strip():
        raise ExampleError("the EEF type is unavailable; approved parameters cannot be matched")


def _wait_for_fsm(client: Any, expected: str, timeout_s: float = 3.0) -> None:
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        state = client.get_service_state()
        if state is not None and state.service_state and state.fsm_state == expected:
            return
        time.sleep(0.1)
    raise ExampleError(f"FSM did not report {expected} before the timeout")


def _wait_for_eef_mode(client: Any, expected: str, timeout_s: float = 3.0) -> None:
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        mode = client.get_eef_mode()
        if mode is not None and mode.get("current_mode_name") == expected:
            return
        time.sleep(0.1)
    raise ExampleError(f"EEF did not report {expected} mode before the timeout")


def _joint_target_toward_zero(angles: tuple[float, ...]) -> list[float]:
    target = list(angles)
    step = min(abs(target[6]), 0.03)
    target[6] -= math.copysign(step, target[6]) if target[6] else 0.0
    return target


def _offset_pose(api: Any, pose: Any, dx: float, dy: float = 0.0) -> Any:
    x, y, z = pose.position
    return api.CartesianPose(
        position=(x + dx, y + dy, z),
        orientation=tuple(pose.orientation),
    )


def _run_with_control(
    client: Any,
    action: Callable[[], Any],
    *,
    cleanup_arm: bool = False,
    cleanup_eef: bool = False,
    api: Any,
) -> Any:
    if not client.acquire_control(lease_ms=15_000, renew_period_s=5.0):
        raise ExampleError("control lease was not acquired")

    result: Any = None
    action_error: BaseException | None = None
    release_error: BaseException | None = None
    arm_idle = True
    eef_idle = True
    try:
        result = action()
    except BaseException as error:  # preserve the operation error after cleanup
        action_error = error
    finally:
        if cleanup_eef:
            try:
                eef_idle = bool(
                    client.switch_eef_control_mode(api.EEFControlMode.idle, timeout_ms=6_000)
                )
                if eef_idle:
                    _wait_for_eef_mode(client, "idle")
            except Exception:
                eef_idle = False
        if cleanup_arm:
            try:
                arm_idle = bool(
                    client.switch_controller(api.Controller.idle, timeout_ms=5_000)
                )
                if arm_idle:
                    _wait_for_fsm(client, "IDLE")
            except Exception:
                arm_idle = False
        try:
            client.release_control()
        except BaseException as error:  # report release failures without hiding the action error
            release_error = error

    cleanup_failures = []
    if not arm_idle:
        cleanup_failures.append("arm idle was not confirmed")
    if not eef_idle:
        cleanup_failures.append("EEF idle was not confirmed")
    if release_error is not None:
        cleanup_failures.append(f"control release failed: {release_error}")
    if cleanup_failures:
        message = "; ".join(cleanup_failures)
        if action_error is not None and hasattr(action_error, "add_note"):
            action_error.add_note(message)
        else:
            raise ExampleError(message)
    if action_error is not None:
        raise action_error
    return result


def _run_motion(client: Any, api: Any, name: str, inputs: RunInputs) -> Any:
    angles = _require_idle_snapshot(client)

    if name == "move_joint":
        target = _joint_target_toward_zero(angles)

        def action() -> bool:
            _require_true(
                client.switch_controller(api.Controller.servo_control, timeout_ms=5_000),
                "switch_controller",
            )
            _wait_for_fsm(client, "SERVO_CONTROL")
            _require_true(client.set_arm_speed([0.6] * 7), "set_arm_speed")
            return client.move_joint(
                target,
                api.JointMoveOptions(blocking=True),
                timeout_ms=35_000,
            )

        result = _run_with_control(client, action, cleanup_arm=True, api=api)

    elif name == "move_joint_waypoints":
        target = _joint_target_toward_zero(angles)

        def action() -> bool:
            _require_true(
                client.switch_controller(api.Controller.planning_control, timeout_ms=5_000),
                "switch_controller",
            )
            _wait_for_fsm(client, "PLANNING_CONTROL")
            return client.move_joint_waypoints(
                [list(angles), target, list(angles)],
                api.JointWaypointsMoveOptions(
                    velocity_scaling_factor=0.05,
                    acceleration_scaling_factor=0.05,
                    enable_blend=False,
                    blocking=True,
                ),
                timeout_ms=35_000,
            )

        result = _run_with_control(client, action, cleanup_arm=True, api=api)

    elif name == "move_eef":
        _require_eef(client)
        target = list(inputs.eef_target_mm or ())
        effort = list(inputs.eef_eff or ())

        def action() -> bool:
            _require_true(
                client.switch_eef_control_mode(api.EEFControlMode.csp, timeout_ms=6_000),
                "switch_eef_control_mode",
            )
            _wait_for_eef_mode(client, "csp")
            _require_true(client.set_eef_speed(inputs.eef_speed_mm_s), "set_eef_speed")
            return client.move_eef(
                target,
                api.EEFMoveOptions(eff=effort, blocking=True),
                timeout_ms=35_000,
            )

        result = _run_with_control(client, action, cleanup_eef=True, api=api)

    else:
        current = _require_pose(client)
        if name == "move_end_pose":

            def action() -> bool:
                _require_true(
                    client.switch_controller(api.Controller.servo_control, timeout_ms=5_000),
                    "switch_controller",
                )
                _wait_for_fsm(client, "SERVO_CONTROL")
                return client.move_end_pose(
                    current,
                    api.CartesianMoveOptions(blocking=True),
                    timeout_ms=35_000,
                )

        elif name == "move_end_pose_linear":
            target = _offset_pose(api, current, inputs.cartesian_step_m or 0.0)

            def action() -> bool:
                _require_true(
                    client.switch_controller(api.Controller.planning_control, timeout_ms=5_000),
                    "switch_controller",
                )
                _wait_for_fsm(client, "PLANNING_CONTROL")
                return client.move_end_pose_linear(
                    current,
                    target,
                    api.CartesianMoveOptions(
                        velocity_scaling_factor=0.05,
                        acceleration_scaling_factor=0.05,
                        blocking=True,
                    ),
                    timeout_ms=35_000,
                )

        elif name == "move_end_pose_circle":
            step = inputs.cartesian_step_m or 0.0
            path = _offset_pose(api, current, step / 2.0, abs(step) / 2.0)
            target = _offset_pose(api, current, step)

            def action() -> bool:
                _require_true(
                    client.switch_controller(api.Controller.planning_control, timeout_ms=5_000),
                    "switch_controller",
                )
                _wait_for_fsm(client, "PLANNING_CONTROL")
                return client.move_end_pose_circle(
                    current,
                    path,
                    target,
                    api.CartesianMoveOptions(
                        circ_is_center=False,
                        velocity_scaling_factor=0.05,
                        acceleration_scaling_factor=0.05,
                        blocking=True,
                    ),
                    timeout_ms=35_000,
                )

        elif name == "move_end_pose_waypoints":
            target = _offset_pose(api, current, inputs.cartesian_step_m or 0.0)

            def action() -> bool:
                _require_true(
                    client.switch_controller(api.Controller.planning_control, timeout_ms=5_000),
                    "switch_controller",
                )
                _wait_for_fsm(client, "PLANNING_CONTROL")
                return client.move_end_pose_waypoints(
                    [current, target, current],
                    api.CartesianWaypointsMoveOptions(
                        motion_type="lin",
                        velocity_scaling_factor=0.05,
                        acceleration_scaling_factor=0.05,
                        enable_blend=False,
                        blocking=True,
                    ),
                    timeout_ms=35_000,
                )

        else:  # protected by PUBLIC_EXAMPLES
            raise AssertionError(name)

        result = _run_with_control(client, action, cleanup_arm=True, api=api)

    _require_true(result, name)
    return result


def _run_connected_example(
    client: Any,
    api: Any,
    name: str,
    inputs: RunInputs,
    emit: Callable[[Any], None],
) -> None:
    if name == "get_client_id":
        emit(client.get_client_id)
        return
    if name == "get_source_name":
        emit(client.get_source_name)
        return
    if name == "get_client_instance_id":
        emit(client.get_client_instance_id)
        return
    if name == "get_client_session_id":
        emit(client.get_client_session_id)
        return
    if name == "get_service_state":
        result = client.get_service_state()
    elif name == "get_arm_joint_state":
        result = client.get_arm_joint_state()
    elif name == "get_arm_motor_state":
        result = client.get_arm_motor_state()
    elif name == "get_eef_joint_state":
        result = client.get_eef_joint_state()
    elif name == "get_eef_motor_state":
        result = client.get_eef_motor_state()
    elif name == "get_imu_state":
        result = client.get_imu_state()
    elif name == "get_end_pose":
        result = client.get_end_pose()
    elif name == "get_firmware_info":
        result = client.get_firmware_info()
    elif name == "get_eef_mode":
        result = client.get_eef_mode()
    else:
        result = None

    if name in STATE_EXAMPLES or name == "get_eef_mode":
        if result is None:
            if name in {"get_eef_joint_state", "get_eef_motor_state"}:
                emit({"available": False, "value": None})
                return
            raise ExampleError(f"{name} returned None")
        emit(result)
        return
    if name == "acquire_control":
        if not client.acquire_control(lease_ms=15_000, renew_period_s=5.0):
            raise ExampleError("control lease was not acquired")
        try:
            emit("control acquired")
        finally:
            try:
                client.release_control()
            except Exception as error:
                raise ExampleError(f"control release failed: {error}") from error
        return
    if name == "release_control":
        if not client.acquire_control(lease_ms=15_000, renew_period_s=5.0):
            raise ExampleError("control lease was not acquired")
        try:
            client.release_control()
        except Exception as error:
            raise ExampleError(f"control release failed: {error}") from error
        emit("control released")
        return
    if name == "handover_to_previous":
        emit({"handover_succeeded": client.handover_to_previous()})
        return
    if name == "set_arm_speed":
        _require_true(client.set_arm_speed([0.6] * 7), name)
        emit("arm speed cache updated")
        return
    if name == "set_eef_speed":
        _require_true(client.set_eef_speed(50.0), name)
        emit("EEF speed cache updated")
        return
    if name == "switch_controller":
        def select_arm_idle() -> bool:
            selected = client.switch_controller(api.Controller.idle, timeout_ms=5_000)
            if selected:
                _wait_for_fsm(client, "IDLE")
            return selected

        result = _run_with_control(
            client,
            select_arm_idle,
            cleanup_arm=True,
            api=api,
        )
        _require_true(result, name)
        emit("arm idle confirmed")
        return
    if name == "enter_gravity_compensation_mode":
        _require_idle_snapshot(client)

        def select_gravity_compensation() -> bool:
            selected = client.enter_gravity_compensation_mode(timeout_ms=5_000)
            if selected:
                _wait_for_fsm(client, "GRAVITY_COMPENSATION")
            return selected

        result = _run_with_control(
            client,
            select_gravity_compensation,
            cleanup_arm=True,
            api=api,
        )
        _require_true(result, name)
        emit("gravity compensation exited; arm idle requested")
        return
    if name == "switch_eef_control_mode":
        def select_eef_idle() -> bool:
            selected = client.switch_eef_control_mode(
                api.EEFControlMode.idle,
                timeout_ms=6_000,
            )
            if selected:
                _wait_for_eef_mode(client, "idle")
            return selected

        result = _run_with_control(
            client,
            select_eef_idle,
            cleanup_eef=True,
            api=api,
        )
        _require_true(result, name)
        emit("EEF idle confirmed")
        return
    if name == "set_arm_emergency_stop":
        _require_true(client.set_arm_emergency_stop(True), name)
        emit("software emergency-stop request accepted; reset was not sent")
        return
    if name == "clear_error":
        _require_unknown_error(client)

        def clear_unknown_error() -> bool:
            cleared = client.clear_error()
            if cleared:
                _wait_for_fsm(client, "IDLE")
            return cleared

        result = _run_with_control(client, clear_unknown_error, api=api)
        _require_true(result, name)
        emit("error-clear request completed")
        return
    if name == "clear_arm_motor_err":
        _require_unknown_error(client)
        _require_arm_motor_errors(client)

        def clear_arm_errors() -> bool:
            cleared = client.clear_arm_motor_err(retry=False)
            if cleared:
                _wait_for_fsm(client, "IDLE")
            return cleared

        result = _run_with_control(
            client,
            clear_arm_errors,
            api=api,
        )
        _require_true(result, name)
        emit("arm motor error-clear request completed")
        return
    if name == "clear_eef_motor_err":
        try:
            result = _run_with_control(client, client.clear_eef_motor_err, api=api)
        except api.UnsupportedOperationError as error:
            emit(f"unsupported by the current backend: {error}")
            return
        _require_true(result, name)
        emit("EEF motor error-clear request completed")
        return
    if name == "return_zero":
        _require_idle_snapshot(client)
        result = _run_with_control(
            client,
            lambda: client.return_zero(timeout_ms=35_000),
            cleanup_arm=True,
            api=api,
        )
        _require_true(result, name)
        emit("zero-position move completed; arm idle requested")
        return
    if name in MOTION_EXAMPLES:
        emit({name: _run_motion(client, api, name, inputs)})
        return
    raise AssertionError(name)


def run_example(
    name: str,
    *,
    client_factory: Callable[..., Any],
    api: Any,
    client_kwargs: dict[str, Any],
    confirmation: str | None = None,
    inputs: RunInputs = RunInputs(),
    emit: Callable[[Any], None] = print,
) -> None:
    """Run one named public-interface example."""
    if name not in PUBLIC_EXAMPLES:
        raise ValueError(f"unknown example {name!r}")
    _require_confirmation(name, confirmation)
    _validate_run_inputs(name, inputs)

    if name == "__init__":
        client = client_factory(**client_kwargs)
        try:
            emit(client)
        finally:
            client.close()
        return
    if name == "__enter__":
        client = client_factory(**client_kwargs)
        with client as entered:
            emit(entered)
        return
    if name == "__exit__":
        client = client_factory(**client_kwargs)
        client.__enter__()
        result = client.__exit__(None, None, None)
        emit({"suppressed_exception": bool(result)})
        return
    if name == "close":
        client = client_factory(**client_kwargs)
        client.close()
        emit("client closed")
        return

    with client_factory(**client_kwargs) as client:
        _run_connected_example(client, api, name, inputs, emit)


def _public_api() -> Any:
    from arm_p7_sdk import (
        AirbotClient,
        CartesianMoveOptions,
        CartesianPose,
        CartesianWaypointsMoveOptions,
        Controller,
        EEFControlMode,
        EEFMoveOptions,
        JointMoveOptions,
        JointWaypointsMoveOptions,
        UnsupportedOperationError,
    )

    return SimpleNamespace(
        AirbotClient=AirbotClient,
        CartesianMoveOptions=CartesianMoveOptions,
        CartesianPose=CartesianPose,
        CartesianWaypointsMoveOptions=CartesianWaypointsMoveOptions,
        Controller=Controller,
        EEFControlMode=EEFControlMode,
        EEFMoveOptions=EEFMoveOptions,
        JointMoveOptions=JointMoveOptions,
        JointWaypointsMoveOptions=JointWaypointsMoveOptions,
        UnsupportedOperationError=UnsupportedOperationError,
    )


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("example", nargs="?", choices=PUBLIC_EXAMPLES)
    parser.add_argument("--confirm", help="required token for state-changing examples")
    parser.add_argument("--list", action="store_true", help="list every example and token")
    parser.add_argument("--backend", choices=("grpc", "dds"), default="grpc")
    parser.add_argument("--host", help="P7 gRPC host; required for --backend grpc")
    parser.add_argument("--port", type=int, default=50071)
    parser.add_argument("--domain-id", type=int, help="required for --backend dds")
    parser.add_argument("--side", choices=("none", "left", "right"), default="none")
    parser.add_argument("--cartesian-step-m", type=float)
    parser.add_argument("--eef-target-mm")
    parser.add_argument("--eef-eff")
    parser.add_argument("--eef-speed-mm-s", type=float, default=50.0)
    args = parser.parse_args()

    if args.list:
        for name in PUBLIC_EXAMPLES:
            token = CONFIRMATION_TOKENS.get(name, "-")
            print(f"{name:34} confirm={token}")
        return
    if args.example is None:
        parser.error("choose an example or use --list")

    try:
        api = _public_api()
        run_example(
            args.example,
            client_factory=api.AirbotClient,
            api=api,
            client_kwargs=client_options_from_args(args),
            confirmation=args.confirm,
            inputs=inputs_from_args(args),
        )
    except (ExampleError, ValueError, ConnectionError) as error:
        raise SystemExit(f"example failed: {error}") from error


if __name__ == "__main__":
    main()

列出全部入口不需要连接参数;读取服务状态时传入 gRPC 地址和端口:

python sdk_minimal_examples.py --list
python sdk_minimal_examples.py get_service_state \
  --backend grpc --host P7_IP_ADDRESS --port 50071

第一条命令列出全部入口和确认口令,不连接机械臂。第二条命令创建客户端、读取一次服务状态并关闭 连接。

连接配置#

将上面的完整代码保存为 sdk_minimal_examples.py。Python 环境需要安装与 P7 软件组合匹配的 arm-p7-sdk;版本选择和安装方法见 兼容性与安装 SDK。

python sdk_minimal_examples.py get_firmware_info \
  --backend grpc --host P7_IP_ADDRESS --port 50071
python sdk_minimal_examples.py get_firmware_info \
  --backend dds --domain-id DOMAIN_FROM_P7_CONFIG --side none

gRPC 是默认后端。DDS 还要求当前 Python 环境已经安装匹配的 CORA SDK;双臂系统应把 --side none 改为经过配置确认的 --side left--side right

读取类命令不申请控制权。输出为 SDK 数据模型、字典或一个简短结果;如果当前整机没有 EEF,两个 EEF 状态命令会输出 available: false,不会伪造一份状态继续运行。其他 getter 返回 None 时会作为失败 报告。

确认口令#

会改变客户端配置、控制权或机械臂状态的用例必须在命令中写出对应口令:

口令 操作 运行前确认
CONFIG 修改当前客户端保存的速度值 数值来自当前任务配置,后续命令不会误用旧值
CONTROL 获取/交还控制权或切换到 idle 已与同一机械臂的其他控制客户端协调
STOP 发送软件急停 明确需要停止,且知道软件急停不能替代实体急停
RECOVER 清错 故障原因已经排除,原始状态和错误码已经保存
MOVE 进入重力补偿、回零或发送运动目标 工位清空、实体急停可用,工具、负载和整条路径已经复核

例如,只验证控制租约的获取和释放:

python sdk_minimal_examples.py acquire_control \
  --backend grpc --host P7_IP_ADDRESS --port 50071 \
  --confirm CONTROL

确认口令只防止误运行,不代替风险评估、路径验证或现场批准。命令返回失败、超时或无法确认 idle 时, 保持工位隔离,停止发送新目标,并按停止和异常处理检查实际状态。

生命周期和身份#

接口/属性 最小结果 参考
__init__ 构造客户端,打印对象后显式 close() 构造参数
__enter__ 进入 context manager 并打印客户端 context manager
__exit__ 正常退出 context manager,打印是否抑制异常 context manager
close 显式关闭一次并报告完成 close()
get_client_id 打印当前连接分配的客户端 ID 身份属性
get_source_name 打印控制权来源类别 身份属性
get_client_instance_id 打印进程实例 ID 身份属性
get_client_session_id 打印注册 session;后端不提供时可能为 None 身份属性

运行方式为 python sdk_minimal_examples.py <接口名> <连接参数>。这些用例不发送运动命令。

控制权、速度和模式#

接口 口令和最小动作 参考
acquire_control CONTROL;获取租约后立即释放 acquire_control()
handover_to_previous CONTROL;请求交还给上一客户端,输出 True/False handover_to_previous()
release_control CONTROL;先获取一份租约,再显式释放 release_control()
set_arm_speed CONFIG;写入 7 个 0.6 rad/s 的格式示例 set_arm_speed()
set_eef_speed CONFIG;写入 50 mm/s 的格式示例 set_eef_speed()
switch_controller CONTROL;显式切换到 Controller.idle 并释放租约 switch_controller()
enter_gravity_compensation_mode MOVE;进入重力补偿后请求切回 arm idle 重力补偿
get_eef_mode 无口令;读取一次 EEF 模式字典 get_eef_mode()
switch_eef_control_mode CONTROL;显式切换到 EEFControlMode.idle EEF 模式切换

set_arm_speedset_eef_speed 只修改当前客户端缓存,但这些值会影响后续命令。表中的数值用于验证 数组长度、单位和调用形式,不是任意机械臂或 EEF 的推荐速度。

重力补偿会让机械臂在外力或重力作用下移动。脚本退出前会请求 arm idle 并释放租约;idle 清理失败 会让命令以错误结束,不能把进程退出当作机械臂已经停止。

状态读取#

接口 最小结果 参考
get_service_state 一份服务与 FSM 状态 服务状态
get_arm_joint_state 7 轴位置、速度和 effort 关节状态
get_arm_motor_state 电机温度和错误码 机械臂电机
get_eef_joint_state EEF 关节状态 EEF 关节
get_eef_motor_state EEF 电机状态 EEF 电机
get_imu_state 角速度和线加速度 IMU
get_end_pose base_link 下的末端位姿 末端位姿
get_firmware_info 序列号、硬件和固件信息 固件信息

这些命令均不要求确认口令,也不会调用 acquire_control()。连续监控和新鲜度处理的完整、可复制代码 见连续只读诊断,页面默认折叠长代码但不省略实现。

安全与恢复#

接口 口令和最小动作 参考
set_arm_emergency_stop STOP;只发送 True,不会自动复位 软件急停
clear_error RECOVER;取得控制权后发出通用清错 clear_error()
clear_arm_motor_err RECOVER;使用 retry=False 清除一次 clear_arm_motor_err()
clear_eef_motor_err RECOVER;当前两种后端预期抛出 UnsupportedOperationError EEF 清错
return_zero MOVE;检查状态后执行阻塞回零,再请求 arm idle 回到关节零位

软件急停用例故意不发送 set_arm_emergency_stop(False)。复位需要确认急停原因已经排除,并按当前 FSM 选择后续恢复步骤。return_zero 会让全部关节移动到零位;它不是只读检查,也不是标定流程。

运动接口#

接口 最小调用 参考
move_joint 当前 J7 向零位最多移动 0.03 rad,阻塞 Servo 关节运动
move_joint_waypoints 当前关节位→小幅 J7 目标→当前关节位,禁用 blend 关节路点
move_end_pose 在 Servo 下发送当前实测位姿,验证单帧调用和清理 单个位姿
move_end_pose_linear 当前位姿到显式 X 偏移,阻塞 LIN 直线运动
move_end_pose_circle 当前位姿、显式中间点和目标组成小圆弧 圆弧运动
move_end_pose_waypoints 当前位姿→显式 X 偏移→当前位姿,禁用 blend 笛卡尔路点
move_eef 使用显式提供的目标、effort 和速度执行 CSP 末端运动

所有运动命令都要求 --confirm MOVE。脚本会检查 ServiceState、7 轴有限关节反馈和电机错误,取得 控制权,切换对应模式,并采用阻塞调用。正常或异常退出都会释放租约;进入 arm 或 EEF 控制模式的 用例还会请求对应的 idle。

关节运动#

python sdk_minimal_examples.py move_joint \
  --backend grpc --host P7_IP_ADDRESS --port 50071 \
  --confirm MOVE

J7 目标根据当前反馈生成,并朝零位变化最多 0.03 rad。即使变化很小,也必须确认整条机械臂、工具 和线缆不会与现场设备接触。

笛卡尔 LIN、CIRCLE 和路点#

三个 planning 用例要求显式提供沿 base_link X 轴的有符号偏移,绝对值不能超过 0.005 m

python sdk_minimal_examples.py move_end_pose_linear \
  --backend grpc --host P7_IP_ADDRESS --port 50071 \
  --cartesian-step-m APPROVED_SIGNED_STEP_IN_METERS \
  --confirm MOVE

把占位符替换为当前工位已经评估的非零值。脚本限制偏移幅度,但不会验证 IK、碰撞、奇异、工具、 负载或外部障碍;0.005 m 是脚本允许的最大格式范围,不是默认值或安全保证。

EEF#

EEF 的位置、effort 和速度必须来自对应型号及软件组合的交付参数:

python sdk_minimal_examples.py move_eef \
  --backend grpc --host P7_IP_ADDRESS --port 50071 \
  --eef-target-mm APPROVED_COMMA_SEPARATED_TARGET \
  --eef-eff APPROVED_COMMA_SEPARATED_EFFORT \
  --eef-speed-mm-s APPROVED_SPEED \
  --confirm MOVE

目标和 effort 数量必须相同,并与运行时 EEF 自由度一致。线性 EEF 的目标位置和速度使用 mmmm/seffort 仍是驱动原生量,不应解释为 SI 物理单位。脚本不会把状态值未经范围检查直接回灌 目标,也不会填入猜测值;仍只能使用已批准且经过受控环境验证的型号、行程和控制参数。

自动验证范围#

仓库测试使用 fake client 逐一运行 37 个名字,检查目标入口、参数、确认口令和清理路径。该测试能 发现示例拼写、调用签名、漏释放和分支错误,但不能证明网络、运行时、规划结果、EEF 标定或真实运动 符合预期。

真机运行前至少完成安全检查表。真机测试的整机、SDK、Arm App、 CORA、固件、工具和 EEF 版本应随测试结果一并记录。