loop-sdk 0.1.0__tar.gz → 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.
Files changed (86) hide show
  1. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/.github/workflows/publish.yml +8 -1
  2. loop_sdk-0.1.2/CONTRIBUTING.md +91 -0
  3. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/PKG-INFO +1 -1
  4. loop_sdk-0.1.2/README.md +195 -0
  5. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/proto/loop/foundation/source/v1/source_ingest.proto +2 -8
  6. loop_sdk-0.1.2/src/loop_sdk/gen/v1/source_ingest_pb2.py +112 -0
  7. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/v1/source_ingest_pb2.pyi +161 -79
  8. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/v1/source_ingest_pb2_grpc.py +13 -145
  9. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/schema.py +6 -1
  10. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/robot_step_sender.py +26 -18
  11. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/service/producer_session.py +4 -0
  12. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/domain/test_schema.py +7 -3
  13. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/test_robot_step_sender.py +22 -0
  14. loop_sdk-0.1.0/README.md +0 -152
  15. loop_sdk-0.1.0/src/loop_sdk/gen/v1/source_ingest_pb2.py +0 -132
  16. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/.gitignore +0 -0
  17. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/Makefile +0 -0
  18. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/README.md +0 -0
  19. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/build_descriptor.py +0 -0
  20. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/build_journey.py +0 -0
  21. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/build_sequence.py +0 -0
  22. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/build_visualizer.py +0 -0
  23. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/consumer_plan.html +0 -0
  24. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/descriptor.html +0 -0
  25. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/extract_real_examples.py +0 -0
  26. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/flow.html +0 -0
  27. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/flow_sequence.html +0 -0
  28. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/journey.html +0 -0
  29. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/real_examples.json +0 -0
  30. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/data_model/visualizer.html +0 -0
  31. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/dev_requirements.md +0 -0
  32. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/foundation_request_consumer_rpc.md +0 -0
  33. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/pm_questions.md +0 -0
  34. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/source_connection_spec.md +0 -0
  35. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/docs/spec_discrepancies.md +0 -0
  36. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/examples/README.md +0 -0
  37. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/examples/policy_action_consumer.py +0 -0
  38. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/examples/quickstart.py +0 -0
  39. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/examples/robot_step_producer.py +0 -0
  40. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/proto/buf.gen.yaml +0 -0
  41. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/proto/buf.yaml +0 -0
  42. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/pyproject.toml +0 -0
  43. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/__init__.py +0 -0
  44. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/__init__.py +0 -0
  45. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/__init__.pyi +0 -0
  46. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/v1/__init__.py +0 -0
  47. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/gen/v1/__init__.pyi +0 -0
  48. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/__init__.py +0 -0
  49. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/client.py +0 -0
  50. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/consumer.py +0 -0
  51. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/__init__.py +0 -0
  52. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/config.py +0 -0
  53. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/frame.py +0 -0
  54. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/source_exception.py +0 -0
  55. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/source_kind.py +0 -0
  56. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/domain/stats.py +0 -0
  57. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/outbound/__init__.py +0 -0
  58. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/outbound/frame_queue.py +0 -0
  59. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/outbound/grpc_source_client.py +0 -0
  60. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/outbound/grpc_source_reader.py +0 -0
  61. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/outbound/proto_mapping.py +0 -0
  62. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/port/__init__.py +0 -0
  63. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/port/source_client.py +0 -0
  64. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/port/source_reader.py +0 -0
  65. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/service/__init__.py +0 -0
  66. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/setup/__init__.py +0 -0
  67. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/src/loop_sdk/source/setup/config.py +0 -0
  68. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/__init__.py +0 -0
  69. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/source/__init__.py +0 -0
  70. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/source/outbound/__init__.py +0 -0
  71. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/source/outbound/test_consumer_lifecycle.py +0 -0
  72. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/source/outbound/test_consumer_roundtrip.py +0 -0
  73. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/integration/source/outbound/test_grpc_roundtrip.py +0 -0
  74. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/__init__.py +0 -0
  75. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/__init__.py +0 -0
  76. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/domain/__init__.py +0 -0
  77. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/__init__.py +0 -0
  78. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/test_frame_queue.py +0 -0
  79. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/test_policy_action_decode.py +0 -0
  80. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/test_proto_mapping.py +0 -0
  81. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/test_robot_config_negotiation.py +0 -0
  82. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/outbound/test_send_latency.py +0 -0
  83. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/service/__init__.py +0 -0
  84. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tests/unit/source/service/test_producer_session.py +0 -0
  85. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/tools/replay_fidelity_check.py +0 -0
  86. {loop_sdk-0.1.0 → loop_sdk-0.1.2}/uv.lock +0 -0
@@ -19,7 +19,10 @@ name: Publish to PyPI
19
19
  on:
20
20
  push:
21
21
  tags:
22
- - "v*"
22
+ # Only clean version tags trigger a release. `[0-9]+` = one or more digits
23
+ # (so 2- or 3-digit segments are fine), `.` is literal. No other tag fires.
24
+ - "v[0-9]+.[0-9]+.[0-9]+" # vX.Y.Z -> PyPI
25
+ - "v[0-9]+.[0-9]+.[0-9]+-test*" # vX.Y.Z-test… -> TestPyPI (rehearsal)
23
26
 
24
27
  permissions:
25
28
  contents: read
@@ -34,6 +37,8 @@ jobs:
34
37
  url: https://test.pypi.org/p/loop-sdk
35
38
  permissions:
36
39
  id-token: write # OIDC token for Trusted Publishing
40
+ contents: read # read the repo at checkout (required for PRIVATE repos;
41
+ # job-level permissions REPLACE the top-level block)
37
42
  steps:
38
43
  - uses: actions/checkout@v4
39
44
  with:
@@ -66,6 +71,8 @@ jobs:
66
71
  url: https://pypi.org/p/loop-sdk
67
72
  permissions:
68
73
  id-token: write # OIDC token for Trusted Publishing
74
+ contents: read # read the repo at checkout (required for PRIVATE repos;
75
+ # job-level permissions REPLACE the top-level block)
69
76
  steps:
70
77
  - uses: actions/checkout@v4
71
78
  with:
@@ -0,0 +1,91 @@
1
+ # Contributing to loop-sdk
2
+
3
+ Maintainer notes: how to test the package and how to release it. (User-facing
4
+ usage lives in the [README](README.md).)
5
+
6
+ The project uses **[uv](https://docs.astral.sh/uv/)**. The dev tools (pytest,
7
+ ruff, pyright) aren't pinned as dependencies, so the commands below pull them on
8
+ demand with `uv run --with` / `uvx`.
9
+
10
+ ## Testing
11
+
12
+ ```bash
13
+ # Unit + integration tests
14
+ uv run --with pytest python -m pytest
15
+
16
+ # Unit only (skip the integration-marked tests that touch sockets/threads)
17
+ uv run --with pytest python -m pytest tests/unit
18
+
19
+ # Lint and format
20
+ uv run --with ruff ruff check src tests
21
+ uv run --with ruff ruff format --check src tests
22
+
23
+ # Type-check
24
+ uvx pyright src tests
25
+ ```
26
+
27
+ The generated gRPC stubs under `src/loop_sdk/gen/` are excluded from lint and
28
+ type-checking — don't hand-edit them (see below).
29
+
30
+ ## Regenerating the gRPC stubs
31
+
32
+ The proto contract is vendored at `proto/loop/foundation/source/v1/` (owned
33
+ upstream by the loop foundation module). After editing it, regenerate:
34
+
35
+ ```bash
36
+ make proto # buf lint + buf generate + protoletariat (needs buf on PATH + uv)
37
+ ```
38
+
39
+ This rewrites `src/loop_sdk/gen/`. Never edit those files by hand; change the
40
+ `.proto` and regenerate. Field numbers must stay in sync with the foundation
41
+ proto, or producer/consumer wire compatibility breaks.
42
+
43
+ ## Releasing to PyPI
44
+
45
+ Versioning is **tag-driven** (`hatch-vcs`): a tag `vX.Y.Z` becomes package version
46
+ `X.Y.Z` — there is no version to bump in `pyproject.toml`. A push of a version tag
47
+ triggers `.github/workflows/publish.yml`, which builds the sdist + wheel and
48
+ publishes via **PyPI Trusted Publishing (OIDC)** — no API tokens.
49
+
50
+ Only clean version tags trigger a release:
51
+
52
+ | Tag | Publishes to | Version |
53
+ |---|---|---|
54
+ | `vX.Y.Z` (e.g. `v0.1.0`, `v1.20.300`) | **PyPI** | `X.Y.Z` |
55
+ | `vX.Y.Z-test` (or `-test2`, …) | **TestPyPI** (rehearsal) | `X.Y.Z.dev<run>` (unique per run) |
56
+ | anything else (`v1`, `v1.2`, `nightly`, …) | nothing | — |
57
+
58
+ ### Cut a release
59
+
60
+ ```bash
61
+ # (optional) rehearse on TestPyPI first
62
+ git tag v0.1.0-test && git push origin v0.1.0-test
63
+
64
+ # the real release
65
+ git checkout main && git pull
66
+ git tag v0.1.0 && git push origin v0.1.0
67
+ ```
68
+
69
+ A tag-triggered run uses the workflow file **as it exists at the tagged commit**,
70
+ so if you change the workflow, tag on top of that change (or re-tag).
71
+
72
+ ### One-time Trusted Publishing setup
73
+
74
+ Register a trusted publisher on **each** index (they're separate sites with
75
+ separate accounts). Project `loop-sdk`, owner `configinc`, repo `loop-sdk`,
76
+ workflow `publish.yml`:
77
+
78
+ - **pypi.org** → environment `pypi`
79
+ - **test.pypi.org** → environment `testpypi`
80
+
81
+ Before the project exists on an index, add it as a **pending publisher**. A
82
+ mismatch surfaces as an `invalid-publisher` error on the publish step.
83
+
84
+ ### Notes
85
+
86
+ - **Private repo is fine.** Trusted Publishing works regardless of repo
87
+ visibility; the published package stays public. The publish jobs grant
88
+ `contents: read` (needed for `actions/checkout` on a private repo) alongside
89
+ `id-token: write`.
90
+ - The published wheel contains the `loop_sdk` package (including the generated
91
+ stubs); `proto/`, `tests/`, and `examples/` are not shipped.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-sdk
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Customer-side gRPC client SDK for the Loop Source Bus.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: grpcio>=1.81.1
@@ -0,0 +1,195 @@
1
+ # loop-sdk
2
+
3
+ [English](#loop-sdk) · [한국어](#한국어)
4
+
5
+ ```bash
6
+ pip install loop-sdk # Python ≥ 3.10
7
+ ```
8
+
9
+ The Python client that connects your robot to **Loop**.
10
+
11
+ Loop records your robot's runs as episodes and deploys trained policies back onto
12
+ it. This SDK is the robot's side of that link — a single gRPC connection it manages
13
+ for you (handshake, retries, reconnection, backpressure) — and it does two things:
14
+
15
+ - **Record your robot.** Stream its per-tick state and actions to Loop as named
16
+ channels. An operator presses record in the Loop UI and the run is saved as an
17
+ episode; you just keep streaming — you never manage recording yourself.
18
+ - **Run a Loop-hosted policy.** Subscribe to a deployment's action stream and
19
+ execute the actions it sends, instead of standing up your own inference server.
20
+
21
+ Most people start with recording. It's one class — `RobotStepSender` — and three
22
+ calls: `connect`, `send` each control tick, `disconnect`.
23
+
24
+ ## Stream your robot in
25
+
26
+ ```python
27
+ import time
28
+
29
+ from loop_sdk import RobotStepSender, RobotConfig, RobotConfigOptions
30
+
31
+ control_hz = 20 # the control rate; updated when Loop negotiates one below
32
+
33
+ # At Open, Loop picks a config from `options`; apply it to your robot and confirm.
34
+ # (robot-control-interface assigns this to env.control_hz.)
35
+ def apply_robot_config(cfg: RobotConfig) -> RobotConfig | None:
36
+ global control_hz
37
+ control_hz = cfg.control_hz
38
+ return cfg
39
+
40
+ # loop_addr defaults to the LOOP_ADDR env var (else localhost:50051),
41
+ # source_id to "robot-step". Advertise the configs you can open with.
42
+ sender = RobotStepSender(
43
+ options=RobotConfigOptions(control_hz=(10, 20, 30), action_space=("target_cartesian_delta",)),
44
+ apply_config=apply_robot_config,
45
+ )
46
+
47
+ sender.connect()
48
+ while running: # your control loop
49
+ t_capture_us = time.time_ns() // 1_000 # capture time, epoch microseconds
50
+ # A nested dict — the SDK flattens it to dotted channel keys. The FIRST send
51
+ # fixes the layout, so this first frame must be complete. A numeric list is ONE
52
+ # vector channel (robot0.action.joint_position); build key[0], key[1], … yourself
53
+ # for per-index scalar channels.
54
+ sender.send(t_capture_us, {
55
+ "robot0": {
56
+ "observation": {"robot_state": {"joint_positions": [0.12, -0.34, 1.57], "gripper": 0.80}},
57
+ "action": {"joint_position": [0.13, -0.33, 1.55]},
58
+ "policy_action": [0.01, -0.02, 0.00], # or None when teleop is driving
59
+ },
60
+ })
61
+ wait_for_next_tick(control_hz) # your loop's rate control — send does not pace
62
+ sender.disconnect() # on shutdown (not for pauses)
63
+ ```
64
+
65
+ A few things worth knowing:
66
+
67
+ - **You name the channels.** Send a nested dict; the SDK flattens it to dotted keys
68
+ (`robot0.observation...`). The **first `send` fixes the layout**, so include every
69
+ channel you'll ever send in that first frame — give "no reading" ones `None`.
70
+ - **The operator runs recording, not you.** Keep streaming; Loop captures when told
71
+ to. `send` never blocks and doesn't pace your loop — your loop owns its own rate.
72
+ - **Config negotiation (when your robot can open in more than one config).**
73
+ Advertise the choices via `options`; at connect Loop picks one and the SDK calls
74
+ your `apply_config` to apply it (e.g. the control rate), after validating it
75
+ against what you advertised. Nothing to negotiate? Drop `options`/`apply_config`.
76
+ Need the config settled *before* the first send? Call `sender.declare(channels)`
77
+ once after `connect()`.
78
+
79
+ ## Run a Loop-hosted policy
80
+
81
+ The mirror of streaming in: instead of calling an inference server, subscribe to a
82
+ deployment's policy-action source and execute the actions it produces. See
83
+ [`examples/policy_action_consumer.py`](examples/policy_action_consumer.py).
84
+
85
+ ## Examples
86
+
87
+ [`examples/`](examples/): `quickstart.py` (the smallest version of the above) ·
88
+ `robot_step_producer.py` (a full dual-arm producer with config negotiation) ·
89
+ `policy_action_consumer.py` (reading a policy back out). Run with
90
+ `LOOP_ADDR=host:port python examples/<file>.py`.
91
+
92
+ ## Lower level
93
+
94
+ `RobotStepSender` is robot-only and opinionated. For per-channel metadata
95
+ (`role` / `unit` / `rot_type`) or non-robot source kinds (tactile, marker), use
96
+ `loop_sdk.SourceProducer` / `SourceConsumer` directly — the high-level sender is
97
+ built on them. **Developing & releasing:** [`CONTRIBUTING.md`](CONTRIBUTING.md).
98
+ Internals & specs: [`docs/`](docs/).
99
+
100
+ ---
101
+
102
+ <a id="한국어"></a>
103
+
104
+ # loop-sdk (한국어)
105
+
106
+ [English](#loop-sdk) · [한국어](#한국어)
107
+
108
+ ```bash
109
+ pip install loop-sdk # Python ≥ 3.10
110
+ ```
111
+
112
+ 로봇을 **Loop**에 연결하는 Python 클라이언트입니다.
113
+
114
+ Loop는 로봇의 실행을 에피소드로 **녹화**하고, 학습된 정책을 로봇에 **배포**하는 플랫폼입니다.
115
+ 이 SDK는 그 연결의 로봇 쪽 — SDK가 알아서 관리하는 단일 gRPC 연결(핸드셰이크·재시도·재연결·
116
+ 백프레셔) — 이고, 두 가지를 합니다:
117
+
118
+ - **로봇 녹화.** 매 tick의 상태·액션을 named 채널로 Loop에 스트리밍합니다. operator가 Loop
119
+ UI에서 녹화를 누르면 그 실행이 에피소드로 저장됩니다 — 너는 계속 스트리밍만 하면 되고,
120
+ 녹화 자체를 관리하지 않습니다.
121
+ - **Loop가 호스팅한 정책 실행.** 자체 추론 서버를 띄우는 대신, 배포(deployment)의 액션
122
+ 스트림을 구독해 그 액션을 실행합니다.
123
+
124
+ 대부분 녹화부터 시작합니다. 클래스 하나 — `RobotStepSender` — 와 세 번의 호출:
125
+ `connect`, 매 제어 tick `send`, `disconnect`.
126
+
127
+ ## 로봇 스트리밍
128
+
129
+ ```python
130
+ import time
131
+
132
+ from loop_sdk import RobotStepSender, RobotConfig, RobotConfigOptions
133
+
134
+ control_hz = 20 # 제어율; 아래에서 Loop가 협상하면 갱신됨
135
+
136
+ # Open 시 Loop가 `options` 중 하나를 고름 → 로봇에 적용하고 확인 반환.
137
+ # (robot-control-interface는 이를 env.control_hz에 대입.)
138
+ def apply_robot_config(cfg: RobotConfig) -> RobotConfig | None:
139
+ global control_hz
140
+ control_hz = cfg.control_hz
141
+ return cfg
142
+
143
+ # loop_addr 기본값 = LOOP_ADDR 환경변수(없으면 localhost:50051), source_id 기본값 = "robot-step".
144
+ # 열 수 있는 config들을 광고.
145
+ sender = RobotStepSender(
146
+ options=RobotConfigOptions(control_hz=(10, 20, 30), action_space=("target_cartesian_delta",)),
147
+ apply_config=apply_robot_config,
148
+ )
149
+
150
+ sender.connect()
151
+ while running: # 네 제어 루프
152
+ t_capture_us = time.time_ns() // 1_000 # 캡처 시각, epoch 마이크로초
153
+ # 중첩 dict → SDK가 점-키로 평탄화. 첫 send가 레이아웃을 고정하므로 첫 프레임은 완전해야.
154
+ # 숫자 리스트는 벡터 채널 하나(robot0.action.joint_position); per-index 스칼라 채널을
155
+ # 원하면 key[0], key[1], … 를 직접 만들어 넣을 것.
156
+ sender.send(t_capture_us, {
157
+ "robot0": {
158
+ "observation": {"robot_state": {"joint_positions": [0.12, -0.34, 1.57], "gripper": 0.80}},
159
+ "action": {"joint_position": [0.13, -0.33, 1.55]},
160
+ "policy_action": [0.01, -0.02, 0.00], # teleop이 몰면 None
161
+ },
162
+ })
163
+ wait_for_next_tick(control_hz) # 네 루프의 rate control — send는 페이싱 안 함
164
+ sender.disconnect() # 종료 시에만 (일시정지엔 금지)
165
+ ```
166
+
167
+ 알아두면 좋은 것:
168
+
169
+ - **채널 이름은 네가 짓는다.** 중첩 dict을 보내면 SDK가 점-키(`robot0.observation...`)로
170
+ 평탄화합니다. **첫 `send`가 레이아웃을 고정**하므로, 앞으로 보낼 모든 채널을 첫 프레임에
171
+ 넣으세요 — 이번엔 값이 없는 채널은 `None`으로.
172
+ - **녹화는 operator가 한다.** 너는 계속 스트리밍만, Loop가 지시받을 때 캡처합니다. `send`는
173
+ 블로킹하지 않고 루프를 페이싱하지도 않습니다 — 케이던스는 네 루프 몫.
174
+ - **Config 협상 (로봇이 여러 config로 열릴 수 있을 때).** 가능한 선택지를 `options`로 광고하면,
175
+ 연결 시 Loop가 하나를 고르고 SDK가 그걸 — 네가 광고한 것에 대해 검증한 뒤 — `apply_config`로
176
+ 적용합니다(예: 제어율). 협상할 게 없으면 `options`/`apply_config`를 빼면 됩니다. 첫 send
177
+ *전에* config를 확정하고 싶으면 `connect()` 뒤에 `sender.declare(channels)`를 한 번 호출하세요.
178
+
179
+ ## Loop 정책 실행
180
+
181
+ 스트리밍의 반대 방향: 추론 서버를 호출하는 대신, 배포의 policy-action 소스를 구독해 그 액션을
182
+ 실행합니다. [`examples/policy_action_consumer.py`](examples/policy_action_consumer.py) 참고.
183
+
184
+ ## 예제
185
+
186
+ [`examples/`](examples/): `quickstart.py`(위 코드의 최소 버전) · `robot_step_producer.py`(전체
187
+ dual-arm 생산자 + config 협상) · `policy_action_consumer.py`(정책 되읽기). 실행:
188
+ `LOOP_ADDR=host:port python examples/<file>.py`.
189
+
190
+ ## 저수준
191
+
192
+ `RobotStepSender`는 로봇 전용이고 의견이 강합니다. 채널별 메타(`role`/`unit`/`rot_type`)나
193
+ 비로봇 소스 종류(tactile, marker)가 필요하면 `loop_sdk.SourceProducer` / `SourceConsumer`를
194
+ 직접 쓰세요 — 고수준 sender가 그 위에 얹혀 있습니다. **개발·배포:** [`CONTRIBUTING.md`](CONTRIBUTING.md).
195
+ 내부 구조·스펙: [`docs/`](docs/).
@@ -260,32 +260,26 @@ enum CameraPixelFormat {
260
260
  // compatibility-gate fields. proto3 has no `required`; the SDK and foundation
261
261
  // enforce presence of the required fields.
262
262
  message RobotConfig {
263
- reserved 7, 8;
264
- reserved "robot_server_version", "rci_version";
265
-
266
263
  int32 control_hz = 1;
267
264
  string action_space = 2;
268
265
  optional string gripper_type = 3;
269
266
  optional string finger_type = 4;
270
267
  optional string robot_type = 5;
271
268
  optional string robot_firmware_version = 6;
272
- optional string teleoperation_version = 9;
269
+ optional string teleoperation_version = 7;
273
270
  }
274
271
 
275
272
  // Per-axis candidate lists a robot source advertises at Describe time. The
276
273
  // server picks one value per axis. control_hz and action_space must carry at
277
274
  // least one candidate; the rest may be empty (that axis is not negotiated).
278
275
  message RobotConfigOptions {
279
- reserved 7, 8;
280
- reserved "robot_server_version", "rci_version";
281
-
282
276
  repeated int32 control_hz = 1;
283
277
  repeated string action_space = 2;
284
278
  repeated string gripper_type = 3;
285
279
  repeated string finger_type = 4;
286
280
  repeated string robot_type = 5;
287
281
  repeated string robot_firmware_version = 6;
288
- repeated string teleoperation_version = 9;
282
+ repeated string teleoperation_version = 7;
289
283
  }
290
284
 
291
285
  // ---------------------------------------------------------------------------
@@ -0,0 +1,112 @@
1
+ """Generated protocol buffer code."""
2
+ from google.protobuf import descriptor as _descriptor
3
+ from google.protobuf import descriptor_pool as _descriptor_pool
4
+ from google.protobuf import runtime_version as _runtime_version
5
+ from google.protobuf import symbol_database as _symbol_database
6
+ from google.protobuf.internal import builder as _builder
7
+ _runtime_version.ValidateProtobufRuntimeVersion(_runtime_version.Domain.PUBLIC, 7, 35, 1, '', 'v1/source_ingest.proto')
8
+ _sym_db = _symbol_database.Default()
9
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16v1/source_ingest.proto\x12\x19loop.foundation.source.v1"\xaa\x04\n\x0bClientEvent\x12\x1b\n\tclient_id\x18\x01 \x01(\tR\x08clientId\x12\x1d\n\ncommand_id\x18\x02 \x01(\tR\tcommandId\x12>\n\x05ready\x18\n \x01(\x0b2&.loop.foundation.source.v1.ClientReadyH\x00R\x05ready\x12Z\n\x11source_discovered\x18\x0b \x01(\x0b2+.loop.foundation.source.v1.SourceDiscoveredH\x00R\x10sourceDiscovered\x12Q\n\x0escan_completed\x18\x0c \x01(\x0b2(.loop.foundation.source.v1.ScanCompletedH\x00R\rscanCompleted\x12a\n\x14stream_state_changed\x18\r \x01(\x0b2-.loop.foundation.source.v1.StreamStateChangedH\x00R\x12streamStateChanged\x12D\n\theartbeat\x18\x0e \x01(\x0b2$.loop.foundation.source.v1.HeartbeatH\x00R\theartbeat\x12>\n\x05error\x18\x0f \x01(\x0b2&.loop.foundation.source.v1.ClientErrorH\x00R\x05errorB\x07\n\x05event"\xb9\x02\n\x0fRecorderCommand\x12\x1d\n\ncommand_id\x18\x01 \x01(\tR\tcommandId\x12A\n\x08describe\x18\n \x01(\x0b2#.loop.foundation.source.v1.DescribeH\x00R\x08describe\x12<\n\x04open\x18\x0b \x01(\x0b2&.loop.foundation.source.v1.OpenCommandH\x00R\x04open\x128\n\x05close\x18\x0c \x01(\x0b2 .loop.foundation.source.v1.CloseH\x00R\x05close\x12A\n\x08shutdown\x18\r \x01(\x0b2#.loop.foundation.source.v1.ShutdownH\x00R\x08shutdownB\t\n\x07command"\'\n\x0bClientReady\x12\x18\n\x07version\x18\x01 \x01(\tR\x07version"\n\n\x08Describe"\x8c\x03\n\x0bOpenCommand\x12\x1d\n\nsession_id\x18\x01 \x01(\tR\tsessionId\x12\x19\n\x08start_us\x18\x02 \x01(\x03R\x07startUs\x12\x1d\n\nsource_ids\x18\x03 \x03(\tR\tsourceIds\x12H\n\x07tactile\x18\n \x01(\x0b2,.loop.foundation.source.v1.TactileOpenParamsH\x00R\x07tactile\x12B\n\x05robot\x18\x0b \x01(\x0b2*.loop.foundation.source.v1.RobotOpenParamsH\x00R\x05robot\x12E\n\x06marker\x18\x0c \x01(\x0b2+.loop.foundation.source.v1.MarkerOpenParamsH\x00R\x06marker\x12E\n\x06camera\x18\r \x01(\x0b2+.loop.foundation.source.v1.CameraOpenParamsH\x00R\x06cameraB\x08\n\x06params"^\n\x05Close\x12\x1d\n\nsession_id\x18\x01 \x01(\tR\tsessionId\x12\x17\n\x07stop_us\x18\x02 \x01(\x03R\x06stopUs\x12\x1d\n\nsource_ids\x18\x03 \x03(\tR\tsourceIds""\n\x08Shutdown\x12\x16\n\x06reason\x18\x01 \x01(\tR\x06reason"z\n\rScanCompleted\x12)\n\x10discovered_count\x18\x01 \x01(\x05R\x0fdiscoveredCount\x12>\n\x06errors\x18\x02 \x03(\x0b2&.loop.foundation.source.v1.ClientErrorR\x06errors".\n\tHeartbeat\x12!\n\x0cmonotonic_us\x18\x01 \x01(\x03R\x0bmonotonicUs"X\n\x0bClientError\x12\x12\n\x04code\x18\x01 \x01(\tR\x04code\x12\x18\n\x07message\x18\x02 \x01(\tR\x07message\x12\x1b\n\tsource_id\x18\x03 \x01(\tR\x08sourceId"\xf1\x01\n\x12StreamStateChanged\x12\x1d\n\nsession_id\x18\x01 \x01(\tR\tsessionId\x12\x1b\n\tsource_id\x18\x02 \x01(\tR\x08sourceId\x12<\n\x05state\x18\x03 \x01(\x0e2&.loop.foundation.source.v1.StreamStateR\x05state\x12\x16\n\x06detail\x18\x04 \x01(\tR\x06detail\x12>\n\x05robot\x18\x05 \x01(\x0b2&.loop.foundation.source.v1.RobotConfigH\x00R\x05robotB\t\n\x07applied"\x99\x03\n\x10SourceDiscovered\x12\x1b\n\tsource_id\x18\x01 \x01(\tR\x08sourceId\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n\x04path\x18\x03 \x01(\tR\x04path\x12N\n\x07tactile\x18\n \x01(\x0b22.loop.foundation.source.v1.TactileSourceDescriptorH\x00R\x07tactile\x12H\n\x05robot\x18\x0b \x01(\x0b20.loop.foundation.source.v1.RobotSourceDescriptorH\x00R\x05robot\x12K\n\x06marker\x18\x0c \x01(\x0b21.loop.foundation.source.v1.MarkerSourceDescriptorH\x00R\x06marker\x12K\n\x06camera\x18\r \x01(\x0b21.loop.foundation.source.v1.CameraSourceDescriptorH\x00R\x06cameraB\x0c\n\ndescriptor"j\n\x17TactileSourceDescriptor\x12O\n\x08channels\x18\x01 \x03(\x0b23.loop.foundation.source.v1.TactileChannelDescriptorR\x08channels"\x8a\x01\n\x18TactileChannelDescriptor\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05label\x18\x02 \x01(\tR\x05label\x12#\n\rsample_format\x18\x03 \x01(\tR\x0csampleFormat\x12!\n\x0csample_count\x18\x04 \x01(\x05R\x0bsampleCount"\xc2\x01\n\x15RobotSourceDescriptor\x12M\n\x08channels\x18\x01 \x03(\x0b21.loop.foundation.source.v1.RobotChannelDescriptorR\x08channels\x12Z\n\x11available_options\x18\x02 \x01(\x0b2-.loop.foundation.source.v1.RobotConfigOptionsR\x10availableOptions"\x85\x02\n\x16RobotChannelDescriptor\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05label\x18\x02 \x01(\tR\x05label\x12:\n\x04role\x18\x03 \x01(\x0e2&.loop.foundation.source.v1.ChannelRoleR\x04role\x12\x12\n\x04unit\x18\x04 \x01(\tR\x04unit\x12G\n\x08rot_type\x18\x05 \x01(\x0e2,.loop.foundation.source.v1.RobotRotationTypeR\x07rotType\x12\x14\n\x05group\x18\x06 \x01(\tR\x05group\x12\x14\n\x05range\x18\x07 \x03(\x01R\x05range"\x18\n\x16MarkerSourceDescriptor"\xb4\x01\n\x16CameraSourceDescriptor\x12A\n\x04rtsp\x18\x01 \x01(\x0b2-.loop.foundation.source.v1.RtspCameraEndpointR\x04rtsp\x12W\n\x12available_settings\x18\x02 \x03(\x0b2(.loop.foundation.source.v1.CameraSettingR\x11availableSettings"n\n\x12RtspCameraEndpoint\x12\x10\n\x03uri\x18\x01 \x01(\tR\x03uri\x12F\n\ttransport\x18\x02 \x01(\x0e2(.loop.foundation.source.v1.RtspTransportR\ttransport"\x8d\x02\n\rCameraSetting\x12K\n\nresolution\x18\x01 \x01(\x0b2+.loop.foundation.source.v1.CameraResolutionR\nresolution\x12\x10\n\x03fps\x18\x02 \x01(\x01R\x03fps\x12O\n\x0cpixel_format\x18\x03 \x01(\x0e2,.loop.foundation.source.v1.CameraPixelFormatR\x0bpixelFormat\x12\'\n\x0fhorizontal_flip\x18\x04 \x01(\x08R\x0ehorizontalFlip\x12#\n\rvertical_flip\x18\x05 \x01(\x08R\x0cverticalFlip"@\n\x10CameraResolution\x12\x14\n\x05width\x18\x01 \x01(\x05R\x05width\x12\x16\n\x06height\x18\x02 \x01(\x05R\x06height"\x9b\x03\n\x0bRobotConfig\x12\x1d\n\ncontrol_hz\x18\x01 \x01(\x05R\tcontrolHz\x12!\n\x0caction_space\x18\x02 \x01(\tR\x0bactionSpace\x12&\n\x0cgripper_type\x18\x03 \x01(\tH\x00R\x0bgripperType\x88\x01\x01\x12$\n\x0bfinger_type\x18\x04 \x01(\tH\x01R\nfingerType\x88\x01\x01\x12"\n\nrobot_type\x18\x05 \x01(\tH\x02R\trobotType\x88\x01\x01\x129\n\x16robot_firmware_version\x18\x06 \x01(\tH\x03R\x14robotFirmwareVersion\x88\x01\x01\x128\n\x15teleoperation_version\x18\x07 \x01(\tH\x04R\x14teleoperationVersion\x88\x01\x01B\x0f\n\r_gripper_typeB\x0e\n\x0c_finger_typeB\r\n\x0b_robot_typeB\x19\n\x17_robot_firmware_versionB\x18\n\x16_teleoperation_version"\xa4\x02\n\x12RobotConfigOptions\x12\x1d\n\ncontrol_hz\x18\x01 \x03(\x05R\tcontrolHz\x12!\n\x0caction_space\x18\x02 \x03(\tR\x0bactionSpace\x12!\n\x0cgripper_type\x18\x03 \x03(\tR\x0bgripperType\x12\x1f\n\x0bfinger_type\x18\x04 \x03(\tR\nfingerType\x12\x1d\n\nrobot_type\x18\x05 \x03(\tR\trobotType\x124\n\x16robot_firmware_version\x18\x06 \x03(\tR\x14robotFirmwareVersion\x123\n\x15teleoperation_version\x18\x07 \x03(\tR\x14teleoperationVersion"\x13\n\x11TactileOpenParams"d\n\x0fRobotOpenParams\x12Q\n\x10requested_config\x18\x01 \x01(\x0b2&.loop.foundation.source.v1.RobotConfigR\x0frequestedConfig"\x12\n\x10MarkerOpenParams"i\n\x10CameraOpenParams\x12U\n\x11requested_setting\x18\x01 \x01(\x0b2(.loop.foundation.source.v1.CameraSettingR\x10requestedSetting"\x86\x01\n\x0bSampleBatch\x12\x1d\n\nsession_id\x18\x01 \x01(\tR\tsessionId\x12\x1b\n\tsource_id\x18\x02 \x01(\tR\x08sourceId\x12;\n\x07samples\x18\x03 \x03(\x0b2!.loop.foundation.source.v1.SampleR\x07samples"\xf5\x02\n\x06Sample\x12!\n\x0ctimestamp_us\x18\x01 \x01(\x03R\x0btimestampUs\x12\x1a\n\x08sequence\x18\x02 \x01(\x04R\x08sequence\x12E\n\x07tactile\x18\n \x01(\x0b2).loop.foundation.source.v1.TactilePayloadH\x00R\x07tactile\x12?\n\x05robot\x18\x0b \x01(\x0b2\'.loop.foundation.source.v1.RobotPayloadH\x00R\x05robot\x12B\n\x06marker\x18\x0c \x01(\x0b2(.loop.foundation.source.v1.MarkerPayloadH\x00R\x06marker\x12U\n\rpolicy_action\x18\r \x01(\x0b2..loop.foundation.source.v1.PolicyActionPayloadH\x00R\x0cpolicyActionB\t\n\x07payload"]\n\x0eTactilePayload\x12K\n\x08channels\x18\x01 \x03(\x0b2/.loop.foundation.source.v1.TactileChannelSampleR\x08channels"Y\n\x14TactileChannelSample\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x10\n\x03raw\x18\x02 \x01(\x0cR\x03raw\x12\x1d\n\nvalues_i16\x18\x03 \x03(\x05R\tvaluesI16"\xc7\x01\n\x0cRobotPayload\x12H\n\x05state\x18\x02 \x03(\x0b22.loop.foundation.source.v1.RobotPayload.StateEntryR\x05state\x1a_\n\nStateEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12;\n\x05value\x18\x02 \x01(\x0b2%.loop.foundation.source.v1.RobotValueR\x05value:\x028\x01J\x04\x08\x01\x10\x02R\x06values"\xd4\x01\n\nRobotValue\x12\x18\n\x06scalar\x18\x01 \x01(\x01H\x00R\x06scalar\x12>\n\x05array\x18\x02 \x01(\x0b2&.loop.foundation.source.v1.DoubleArrayH\x00R\x05array\x12\x1d\n\tint_value\x18\x03 \x01(\x03H\x00R\x08intValue\x12\x1f\n\nbool_value\x18\x04 \x01(\x08H\x00R\tboolValue\x12#\n\x0cstring_value\x18\x05 \x01(\tH\x00R\x0bstringValueB\x07\n\x05value"%\n\x0bDoubleArray\x12\x16\n\x06values\x18\x01 \x03(\x01R\x06values"\x9c\x02\n\x13PolicyActionPayload\x12G\n\x07actions\x18\x01 \x03(\x0b2-.loop.foundation.source.v1.PolicyActionVectorR\x07actions\x12?\n\x1ainput_sample_set_source_id\x18\x02 \x01(\tH\x00R\x16inputSampleSetSourceId\x88\x01\x01\x12>\n\x19input_sample_set_sequence\x18\x03 \x01(\x03H\x01R\x16inputSampleSetSequence\x88\x01\x01B\x1d\n\x1b_input_sample_set_source_idB\x1c\n\x1a_input_sample_set_sequence",\n\x12PolicyActionVector\x12\x16\n\x06values\x18\x01 \x03(\x01R\x06values"A\n\rMarkerPayload\x12\x14\n\x05label\x18\x01 \x01(\tR\x05label\x12\x1a\n\x08category\x18\x02 \x01(\tR\x08category"a\n\x15StreamSamplesResponse\x12\x1d\n\nsession_id\x18\x01 \x01(\tR\tsessionId\x12)\n\x10accepted_samples\x18\x02 \x01(\x04R\x0facceptedSamples"N\n\x10SubscribeRequest\x12\x1b\n\tsource_id\x18\x01 \x01(\tR\x08sourceId\x12\x1d\n\nsession_id\x18\x02 \x01(\tR\tsessionId"\x9d\x01\n\x0eSubscribeEvent\x12;\n\x06sample\x18\x01 \x01(\x0b2!.loop.foundation.source.v1.SampleH\x00R\x06sample\x12E\n\x05state\x18\x02 \x01(\x0b2-.loop.foundation.source.v1.StreamStateChangedH\x00R\x05stateB\x07\n\x05event*x\n\x0bStreamState\x12\x1c\n\x18STREAM_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14STREAM_STATE_STARTED\x10\x01\x12\x18\n\x14STREAM_STATE_STOPPED\x10\x02\x12\x17\n\x13STREAM_STATE_FAILED\x10\x03*X\n\x0bChannelRole\x12\x1c\n\x18CHANNEL_ROLE_UNSPECIFIED\x10\x00\x12\x15\n\x11CHANNEL_ROLE_CORE\x10\x01\x12\x14\n\x10CHANNEL_ROLE_AUX\x10\x02*\xa0\x01\n\x11RobotRotationType\x12#\n\x1fROBOT_ROTATION_TYPE_UNSPECIFIED\x10\x00\x12"\n\x1eROBOT_ROTATION_TYPE_QUATERNION\x10\x01\x12\x1d\n\x19ROBOT_ROTATION_TYPE_EULER\x10\x02\x12#\n\x1fROBOT_ROTATION_TYPE_ROTATION_6D\x10\x03*_\n\rRtspTransport\x12\x1e\n\x1aRTSP_TRANSPORT_UNSPECIFIED\x10\x00\x12\x16\n\x12RTSP_TRANSPORT_TCP\x10\x01\x12\x16\n\x12RTSP_TRANSPORT_UDP\x10\x02*\x90\x02\n\x11CameraPixelFormat\x12#\n\x1fCAMERA_PIXEL_FORMAT_UNSPECIFIED\x10\x00\x12\x1d\n\x19CAMERA_PIXEL_FORMAT_MJPEG\x10\x01\x12\x1c\n\x18CAMERA_PIXEL_FORMAT_YUYV\x10\x02\x12\x1c\n\x18CAMERA_PIXEL_FORMAT_UYVY\x10\x03\x12\x1d\n\x19CAMERA_PIXEL_FORMAT_RGB24\x10\x04\x12\x1d\n\x19CAMERA_PIXEL_FORMAT_BGR24\x10\x05\x12\x1c\n\x18CAMERA_PIXEL_FORMAT_H264\x10\x06\x12\x1f\n\x1bCAMERA_PIXEL_FORMAT_UNKNOWN\x10\x072\xe5\x01\n\x13SourceIngestService\x12a\n\x07Connect\x12&.loop.foundation.source.v1.ClientEvent\x1a*.loop.foundation.source.v1.RecorderCommand(\x010\x01\x12k\n\rStreamSamples\x12&.loop.foundation.source.v1.SampleBatch\x1a0.loop.foundation.source.v1.StreamSamplesResponse(\x012z\n\x11SourceReadService\x12e\n\tSubscribe\x12+.loop.foundation.source.v1.SubscribeRequest\x1a).loop.foundation.source.v1.SubscribeEvent0\x01b\x06proto3')
10
+ _globals = globals()
11
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
12
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'v1.source_ingest_pb2', _globals)
13
+ if not _descriptor._USE_C_DESCRIPTORS:
14
+ DESCRIPTOR._loaded_options = None
15
+ _globals['_ROBOTPAYLOAD_STATEENTRY']._loaded_options = None
16
+ _globals['_ROBOTPAYLOAD_STATEENTRY']._serialized_options = b'8\x01'
17
+ _globals['_STREAMSTATE']._serialized_start = 6650
18
+ _globals['_STREAMSTATE']._serialized_end = 6770
19
+ _globals['_CHANNELROLE']._serialized_start = 6772
20
+ _globals['_CHANNELROLE']._serialized_end = 6860
21
+ _globals['_ROBOTROTATIONTYPE']._serialized_start = 6863
22
+ _globals['_ROBOTROTATIONTYPE']._serialized_end = 7023
23
+ _globals['_RTSPTRANSPORT']._serialized_start = 7025
24
+ _globals['_RTSPTRANSPORT']._serialized_end = 7120
25
+ _globals['_CAMERAPIXELFORMAT']._serialized_start = 7123
26
+ _globals['_CAMERAPIXELFORMAT']._serialized_end = 7395
27
+ _globals['_CLIENTEVENT']._serialized_start = 54
28
+ _globals['_CLIENTEVENT']._serialized_end = 608
29
+ _globals['_RECORDERCOMMAND']._serialized_start = 611
30
+ _globals['_RECORDERCOMMAND']._serialized_end = 924
31
+ _globals['_CLIENTREADY']._serialized_start = 926
32
+ _globals['_CLIENTREADY']._serialized_end = 965
33
+ _globals['_DESCRIBE']._serialized_start = 967
34
+ _globals['_DESCRIBE']._serialized_end = 977
35
+ _globals['_OPENCOMMAND']._serialized_start = 980
36
+ _globals['_OPENCOMMAND']._serialized_end = 1376
37
+ _globals['_CLOSE']._serialized_start = 1378
38
+ _globals['_CLOSE']._serialized_end = 1472
39
+ _globals['_SHUTDOWN']._serialized_start = 1474
40
+ _globals['_SHUTDOWN']._serialized_end = 1508
41
+ _globals['_SCANCOMPLETED']._serialized_start = 1510
42
+ _globals['_SCANCOMPLETED']._serialized_end = 1632
43
+ _globals['_HEARTBEAT']._serialized_start = 1634
44
+ _globals['_HEARTBEAT']._serialized_end = 1680
45
+ _globals['_CLIENTERROR']._serialized_start = 1682
46
+ _globals['_CLIENTERROR']._serialized_end = 1770
47
+ _globals['_STREAMSTATECHANGED']._serialized_start = 1773
48
+ _globals['_STREAMSTATECHANGED']._serialized_end = 2014
49
+ _globals['_SOURCEDISCOVERED']._serialized_start = 2017
50
+ _globals['_SOURCEDISCOVERED']._serialized_end = 2426
51
+ _globals['_TACTILESOURCEDESCRIPTOR']._serialized_start = 2428
52
+ _globals['_TACTILESOURCEDESCRIPTOR']._serialized_end = 2534
53
+ _globals['_TACTILECHANNELDESCRIPTOR']._serialized_start = 2537
54
+ _globals['_TACTILECHANNELDESCRIPTOR']._serialized_end = 2675
55
+ _globals['_ROBOTSOURCEDESCRIPTOR']._serialized_start = 2678
56
+ _globals['_ROBOTSOURCEDESCRIPTOR']._serialized_end = 2872
57
+ _globals['_ROBOTCHANNELDESCRIPTOR']._serialized_start = 2875
58
+ _globals['_ROBOTCHANNELDESCRIPTOR']._serialized_end = 3136
59
+ _globals['_MARKERSOURCEDESCRIPTOR']._serialized_start = 3138
60
+ _globals['_MARKERSOURCEDESCRIPTOR']._serialized_end = 3162
61
+ _globals['_CAMERASOURCEDESCRIPTOR']._serialized_start = 3165
62
+ _globals['_CAMERASOURCEDESCRIPTOR']._serialized_end = 3345
63
+ _globals['_RTSPCAMERAENDPOINT']._serialized_start = 3347
64
+ _globals['_RTSPCAMERAENDPOINT']._serialized_end = 3457
65
+ _globals['_CAMERASETTING']._serialized_start = 3460
66
+ _globals['_CAMERASETTING']._serialized_end = 3729
67
+ _globals['_CAMERARESOLUTION']._serialized_start = 3731
68
+ _globals['_CAMERARESOLUTION']._serialized_end = 3795
69
+ _globals['_ROBOTCONFIG']._serialized_start = 3798
70
+ _globals['_ROBOTCONFIG']._serialized_end = 4209
71
+ _globals['_ROBOTCONFIGOPTIONS']._serialized_start = 4212
72
+ _globals['_ROBOTCONFIGOPTIONS']._serialized_end = 4504
73
+ _globals['_TACTILEOPENPARAMS']._serialized_start = 4506
74
+ _globals['_TACTILEOPENPARAMS']._serialized_end = 4525
75
+ _globals['_ROBOTOPENPARAMS']._serialized_start = 4527
76
+ _globals['_ROBOTOPENPARAMS']._serialized_end = 4627
77
+ _globals['_MARKEROPENPARAMS']._serialized_start = 4629
78
+ _globals['_MARKEROPENPARAMS']._serialized_end = 4647
79
+ _globals['_CAMERAOPENPARAMS']._serialized_start = 4649
80
+ _globals['_CAMERAOPENPARAMS']._serialized_end = 4754
81
+ _globals['_SAMPLEBATCH']._serialized_start = 4757
82
+ _globals['_SAMPLEBATCH']._serialized_end = 4891
83
+ _globals['_SAMPLE']._serialized_start = 4894
84
+ _globals['_SAMPLE']._serialized_end = 5267
85
+ _globals['_TACTILEPAYLOAD']._serialized_start = 5269
86
+ _globals['_TACTILEPAYLOAD']._serialized_end = 5362
87
+ _globals['_TACTILECHANNELSAMPLE']._serialized_start = 5364
88
+ _globals['_TACTILECHANNELSAMPLE']._serialized_end = 5453
89
+ _globals['_ROBOTPAYLOAD']._serialized_start = 5456
90
+ _globals['_ROBOTPAYLOAD']._serialized_end = 5655
91
+ _globals['_ROBOTPAYLOAD_STATEENTRY']._serialized_start = 5546
92
+ _globals['_ROBOTPAYLOAD_STATEENTRY']._serialized_end = 5641
93
+ _globals['_ROBOTVALUE']._serialized_start = 5658
94
+ _globals['_ROBOTVALUE']._serialized_end = 5870
95
+ _globals['_DOUBLEARRAY']._serialized_start = 5872
96
+ _globals['_DOUBLEARRAY']._serialized_end = 5909
97
+ _globals['_POLICYACTIONPAYLOAD']._serialized_start = 5912
98
+ _globals['_POLICYACTIONPAYLOAD']._serialized_end = 6196
99
+ _globals['_POLICYACTIONVECTOR']._serialized_start = 6198
100
+ _globals['_POLICYACTIONVECTOR']._serialized_end = 6242
101
+ _globals['_MARKERPAYLOAD']._serialized_start = 6244
102
+ _globals['_MARKERPAYLOAD']._serialized_end = 6309
103
+ _globals['_STREAMSAMPLESRESPONSE']._serialized_start = 6311
104
+ _globals['_STREAMSAMPLESRESPONSE']._serialized_end = 6408
105
+ _globals['_SUBSCRIBEREQUEST']._serialized_start = 6410
106
+ _globals['_SUBSCRIBEREQUEST']._serialized_end = 6488
107
+ _globals['_SUBSCRIBEEVENT']._serialized_start = 6491
108
+ _globals['_SUBSCRIBEEVENT']._serialized_end = 6648
109
+ _globals['_SOURCEINGESTSERVICE']._serialized_start = 7398
110
+ _globals['_SOURCEINGESTSERVICE']._serialized_end = 7627
111
+ _globals['_SOURCEREADSERVICE']._serialized_start = 7629
112
+ _globals['_SOURCEREADSERVICE']._serialized_end = 7751