resonite-io 0.1.0__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 (99) hide show
  1. resonite_io-0.1.0/.gitignore +227 -0
  2. resonite_io-0.1.0/PKG-INFO +69 -0
  3. resonite_io-0.1.0/README.md +44 -0
  4. resonite_io-0.1.0/examples/README.md +119 -0
  5. resonite_io-0.1.0/examples/camera_view.py +86 -0
  6. resonite_io-0.1.0/examples/connection_ping.py +66 -0
  7. resonite_io-0.1.0/examples/context_menu_interact.py +88 -0
  8. resonite_io-0.1.0/examples/cursor_move.py +71 -0
  9. resonite_io-0.1.0/examples/dash_navigate.py +97 -0
  10. resonite_io-0.1.0/examples/display_config.py +96 -0
  11. resonite_io-0.1.0/examples/inventory_manage.py +89 -0
  12. resonite_io-0.1.0/examples/locomotion_drive.py +111 -0
  13. resonite_io-0.1.0/examples/manipulation_grab.py +75 -0
  14. resonite_io-0.1.0/examples/microphone_send.py +98 -0
  15. resonite_io-0.1.0/examples/speaker_record.py +84 -0
  16. resonite_io-0.1.0/examples/world_browse.py +99 -0
  17. resonite_io-0.1.0/pyproject.toml +195 -0
  18. resonite_io-0.1.0/src/resoio/__init__.py +124 -0
  19. resonite_io-0.1.0/src/resoio/_client.py +96 -0
  20. resonite_io-0.1.0/src/resoio/_generated/__init__.py +0 -0
  21. resonite_io-0.1.0/src/resoio/_generated/message_pool.py +3 -0
  22. resonite_io-0.1.0/src/resoio/_generated/py.typed +0 -0
  23. resonite_io-0.1.0/src/resoio/_generated/resonite_io/__init__.py +0 -0
  24. resonite_io-0.1.0/src/resoio/_generated/resonite_io/v1/__init__.py +4069 -0
  25. resonite_io-0.1.0/src/resoio/_socket.py +66 -0
  26. resonite_io-0.1.0/src/resoio/camera.py +87 -0
  27. resonite_io-0.1.0/src/resoio/cli/__init__.py +118 -0
  28. resonite_io-0.1.0/src/resoio/cli/context_menu.py +110 -0
  29. resonite_io-0.1.0/src/resoio/cli/cursor.py +95 -0
  30. resonite_io-0.1.0/src/resoio/cli/dash.py +155 -0
  31. resonite_io-0.1.0/src/resoio/cli/display.py +90 -0
  32. resonite_io-0.1.0/src/resoio/cli/inventory.py +327 -0
  33. resonite_io-0.1.0/src/resoio/cli/locomotion.py +518 -0
  34. resonite_io-0.1.0/src/resoio/cli/manipulate.py +194 -0
  35. resonite_io-0.1.0/src/resoio/cli/mic.py +333 -0
  36. resonite_io-0.1.0/src/resoio/cli/ping.py +78 -0
  37. resonite_io-0.1.0/src/resoio/cli/record.py +934 -0
  38. resonite_io-0.1.0/src/resoio/cli/world.py +567 -0
  39. resonite_io-0.1.0/src/resoio/connection.py +47 -0
  40. resonite_io-0.1.0/src/resoio/context_menu.py +176 -0
  41. resonite_io-0.1.0/src/resoio/cursor.py +91 -0
  42. resonite_io-0.1.0/src/resoio/dash.py +376 -0
  43. resonite_io-0.1.0/src/resoio/display.py +86 -0
  44. resonite_io-0.1.0/src/resoio/inventory.py +197 -0
  45. resonite_io-0.1.0/src/resoio/locomotion.py +183 -0
  46. resonite_io-0.1.0/src/resoio/manipulation.py +184 -0
  47. resonite_io-0.1.0/src/resoio/microphone.py +154 -0
  48. resonite_io-0.1.0/src/resoio/py.typed +0 -0
  49. resonite_io-0.1.0/src/resoio/speaker.py +83 -0
  50. resonite_io-0.1.0/src/resoio/world.py +403 -0
  51. resonite_io-0.1.0/tests/__init__.py +0 -0
  52. resonite_io-0.1.0/tests/conftest.py +55 -0
  53. resonite_io-0.1.0/tests/e2e/README.md +55 -0
  54. resonite_io-0.1.0/tests/e2e/__init__.py +0 -0
  55. resonite_io-0.1.0/tests/e2e/camera_stream.py +130 -0
  56. resonite_io-0.1.0/tests/e2e/conftest.py +94 -0
  57. resonite_io-0.1.0/tests/e2e/connection.py +19 -0
  58. resonite_io-0.1.0/tests/e2e/context_menu.py +192 -0
  59. resonite_io-0.1.0/tests/e2e/cursor.py +127 -0
  60. resonite_io-0.1.0/tests/e2e/dash.py +442 -0
  61. resonite_io-0.1.0/tests/e2e/display_resolution.py +199 -0
  62. resonite_io-0.1.0/tests/e2e/fixtures/generate_sine.py +104 -0
  63. resonite_io-0.1.0/tests/e2e/fixtures/sine_440hz_5s_mono_48k.wav +0 -0
  64. resonite_io-0.1.0/tests/e2e/inventory.py +356 -0
  65. resonite_io-0.1.0/tests/e2e/locomotion.py +335 -0
  66. resonite_io-0.1.0/tests/e2e/manipulation.py +224 -0
  67. resonite_io-0.1.0/tests/e2e/mic_auto_default.py +83 -0
  68. resonite_io-0.1.0/tests/e2e/mic_send.py +157 -0
  69. resonite_io-0.1.0/tests/e2e/speaker_record.py +214 -0
  70. resonite_io-0.1.0/tests/e2e/world.py +406 -0
  71. resonite_io-0.1.0/tests/helpers.py +5 -0
  72. resonite_io-0.1.0/tests/resoio/__init__.py +0 -0
  73. resonite_io-0.1.0/tests/resoio/cli/__init__.py +0 -0
  74. resonite_io-0.1.0/tests/resoio/cli/test_context_menu.py +260 -0
  75. resonite_io-0.1.0/tests/resoio/cli/test_cursor.py +154 -0
  76. resonite_io-0.1.0/tests/resoio/cli/test_dash.py +409 -0
  77. resonite_io-0.1.0/tests/resoio/cli/test_display.py +167 -0
  78. resonite_io-0.1.0/tests/resoio/cli/test_inventory.py +261 -0
  79. resonite_io-0.1.0/tests/resoio/cli/test_locomotion.py +950 -0
  80. resonite_io-0.1.0/tests/resoio/cli/test_manipulate.py +224 -0
  81. resonite_io-0.1.0/tests/resoio/cli/test_mic.py +411 -0
  82. resonite_io-0.1.0/tests/resoio/cli/test_ping.py +66 -0
  83. resonite_io-0.1.0/tests/resoio/cli/test_record.py +1219 -0
  84. resonite_io-0.1.0/tests/resoio/cli/test_world.py +1037 -0
  85. resonite_io-0.1.0/tests/resoio/test_api_contract.py +333 -0
  86. resonite_io-0.1.0/tests/resoio/test_camera.py +87 -0
  87. resonite_io-0.1.0/tests/resoio/test_connection.py +60 -0
  88. resonite_io-0.1.0/tests/resoio/test_context_menu.py +250 -0
  89. resonite_io-0.1.0/tests/resoio/test_cursor.py +109 -0
  90. resonite_io-0.1.0/tests/resoio/test_dash.py +585 -0
  91. resonite_io-0.1.0/tests/resoio/test_display.py +89 -0
  92. resonite_io-0.1.0/tests/resoio/test_inventory.py +183 -0
  93. resonite_io-0.1.0/tests/resoio/test_locomotion.py +199 -0
  94. resonite_io-0.1.0/tests/resoio/test_manipulation.py +298 -0
  95. resonite_io-0.1.0/tests/resoio/test_microphone.py +184 -0
  96. resonite_io-0.1.0/tests/resoio/test_proto_contract.py +615 -0
  97. resonite_io-0.1.0/tests/resoio/test_speaker.py +82 -0
  98. resonite_io-0.1.0/tests/resoio/test_world.py +711 -0
  99. resonite_io-0.1.0/uv.lock +1167 -0
@@ -0,0 +1,227 @@
1
+ # =============================================================================
2
+ # Python
3
+ # =============================================================================
4
+
5
+ # Byte-compiled / optimized
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # uv
32
+ # `python/uv.lock` は commit するので除外しない (lock を commit する方針)
33
+ .venv/
34
+ venv/
35
+ env/
36
+ ENV/
37
+
38
+ # Type checkers / linters / test runners
39
+ .pytest_cache/
40
+ .mypy_cache/
41
+ .pyright/
42
+ .ruff_cache/
43
+ .tox/
44
+ .nox/
45
+ .cache/
46
+ .hypothesis/
47
+
48
+ # Coverage
49
+ .coverage
50
+ .coverage.*
51
+ htmlcov/
52
+ coverage.xml
53
+ *.cover
54
+
55
+ # Jupyter / IPython
56
+ .ipynb_checkpoints
57
+ profile_default/
58
+ ipython_config.py
59
+
60
+ # pyenv
61
+ .python-version
62
+
63
+ # =============================================================================
64
+ # C# / .NET
65
+ # =============================================================================
66
+
67
+ # Build outputs
68
+ [Bb]in/
69
+ [Oo]bj/
70
+ [Oo]ut/
71
+ [Ll]og/
72
+ [Ll]ogs/
73
+ [Dd]ebug/
74
+ [Dd]ebugPublic/
75
+ [Rr]elease/
76
+ [Rr]eleases/
77
+ x64/
78
+ x86/
79
+ [Ww][Ii][Nn]32/
80
+ [Aa][Rr][Mm]/
81
+ [Aa][Rr][Mm]64/
82
+ bld/
83
+
84
+ # Visual Studio / Rider / VSCode user state
85
+ .vs/
86
+ .vscode/*
87
+ !.vscode/settings.json
88
+ !.vscode/tasks.json
89
+ !.vscode/launch.json
90
+ !.vscode/extensions.json
91
+ .idea/
92
+ *.user
93
+ *.suo
94
+ *.userprefs
95
+ *.userosscache
96
+ *.sln.docstates
97
+
98
+ # .NET tooling
99
+ .dotnet/
100
+ project.lock.json
101
+ project.fragment.lock.json
102
+ artifacts/
103
+
104
+ # Test results
105
+ [Tt]est[Rr]esult*/
106
+ [Bb]uild[Ll]og.*
107
+ *.VisualState.xml
108
+ TestResult.xml
109
+ nunit-*.xml
110
+
111
+ # Roslyn cache / build artifacts
112
+ *.tlog
113
+ *.lastbuildstate
114
+ *.cache
115
+ *.ilk
116
+ *.iobj
117
+ *.ipdb
118
+ *.meta
119
+ *_i.c
120
+ *_p.c
121
+ *_h.h
122
+
123
+ # NuGet
124
+ *.nupkg
125
+ *.snupkg
126
+ **/[Pp]ackages/*
127
+ !**/[Pp]ackages/build/
128
+ *.nuget.props
129
+ *.nuget.targets
130
+
131
+ # Compiled binaries (mod artifacts deploy 経由で配布する想定)
132
+ *.dll
133
+ *.pdb
134
+ *.exe
135
+
136
+ # =============================================================================
137
+ # Protobuf
138
+ # =============================================================================
139
+
140
+ # Python 側の protoc 出力は意図的に commit する (plan §3.C)
141
+ # `python/src/resoio/_generated/` は ignore せず
142
+ #
143
+ # C# 側は csproj の <Protobuf> ItemGroup から build 時生成される (obj/ に出る) ため
144
+ # obj/ の ignore でカバーされる
145
+
146
+ # =============================================================================
147
+ # Environment / secrets
148
+ # =============================================================================
149
+
150
+ .env
151
+ .env.*
152
+ !.env.example
153
+
154
+ # =============================================================================
155
+ # OS noise
156
+ # =============================================================================
157
+
158
+ .DS_Store
159
+ Thumbs.db
160
+ ehthumbs.db
161
+ Desktop.ini
162
+ $RECYCLE.BIN/
163
+
164
+ # =============================================================================
165
+ # Local artifacts
166
+ # =============================================================================
167
+
168
+ *.log
169
+ *.tmp
170
+ *.swp
171
+ *~
172
+
173
+ # =============================================================================
174
+ # Docs site (mkdocs)
175
+ # =============================================================================
176
+
177
+ # `just docs-build` の出力先 (repo root の mkdocs.yml の既定 site_dir)。
178
+ site/
179
+
180
+ # =============================================================================
181
+ # Host-side scripts venv
182
+ # =============================================================================
183
+
184
+ # `just host-agent` が起動時に `uv venv` で作る host 専用 venv。
185
+ # 既に `.venv/` で吸えるが、scripts/ 配下の意図を明示するため明記する。
186
+ scripts/.venv/
187
+
188
+ # =============================================================================
189
+ # E2E artifacts
190
+ # =============================================================================
191
+
192
+ # `just e2e-test camera_stream` 等が録画した MP4 / PNG の出力先。
193
+ # 動画は数 MB/run で commit する意味がないため ignore。
194
+ python/tests/e2e/e2e_artifacts/
195
+
196
+ # `just e2e-camera-v2` / `just resonite-screenshot` の出力先。
197
+ # repo-relative path で書き出すため repo root 直下に `tmp/` を作る。
198
+ tmp/
199
+
200
+ # =============================================================================
201
+ # Decompiled sources
202
+ # =============================================================================
203
+
204
+ # `just decompile` の出力先。Resonite の decompile 結果は配布物・改変対象では
205
+ # なく開発者個人の参照用なので commit しない。
206
+ decompiled/
207
+
208
+ # =============================================================================
209
+ # Gale profile
210
+ # =============================================================================
211
+
212
+ # Gale (Resonite mod manager) のカスタムプロファイル展開先。
213
+ # `just deploy-mod` の配置先 (gale/BepInEx/plugins/ResoniteIO/) も含めて
214
+ # host 側で Gale が管理するため、リポジトリには持ち込まない。
215
+ gale/
216
+
217
+ # =============================================================================
218
+ # Claude Code
219
+ # =============================================================================
220
+
221
+ # 並列 implementer が一時的に作る git worktree のルート。
222
+ .claude/worktrees/
223
+ .claude/settings.local.json
224
+ .claude/scheduled_tasks.lock
225
+
226
+ # agent memory runtime mirror (canonical tracked copy lives under memory/agents/)
227
+ .claude/agent-memory/
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: resonite-io
3
+ Version: 0.1.0
4
+ Summary: Python client for Resonite IO
5
+ Project-URL: Homepage, https://github.com/MLShukai/ResoniteIO
6
+ Project-URL: Repository, https://github.com/MLShukai/ResoniteIO
7
+ Author: mlshukai
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: argcomplete>=3.5
20
+ Requires-Dist: av>=14
21
+ Requires-Dist: betterproto2[grpclib]
22
+ Requires-Dist: numpy>=2.0
23
+ Requires-Dist: prompt-toolkit>=3.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # resonite-io
27
+
28
+ Python client for [ResoniteIO](https://github.com/MLShukai/ResoniteIO) — a bidirectional IPC
29
+ bridge that turns [Resonite](https://resonite.com/) into a runtime environment for AI agents.
30
+ The `resonite-io` distribution imports as `resoio` and wraps the `resonite_io.v1` gRPC schema
31
+ (Unix Domain Socket transport, async via `grpclib`) into a friendly, fully typed client
32
+ library and a `resoio` CLI.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install resonite-io
38
+ ```
39
+
40
+ ## Requires
41
+
42
+ A Resonite client running the **ResoniteIO mod** on the same host (the two halves connect
43
+ over a Unix Domain Socket). See the documentation for installing the mod.
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ from resoio import ConnectionClient
51
+
52
+
53
+ async def main() -> None:
54
+ async with ConnectionClient() as client:
55
+ response = await client.ping("hello")
56
+ print(response.message)
57
+
58
+
59
+ asyncio.run(main())
60
+ ```
61
+
62
+ ## Documentation
63
+
64
+ - **Docs:** <https://mlshukai.github.io/ResoniteIO/>
65
+ - **Source:** <https://github.com/MLShukai/ResoniteIO>
66
+
67
+ ## License
68
+
69
+ [MIT](https://github.com/MLShukai/ResoniteIO/blob/main/LICENSE)
@@ -0,0 +1,44 @@
1
+ # resonite-io
2
+
3
+ Python client for [ResoniteIO](https://github.com/MLShukai/ResoniteIO) — a bidirectional IPC
4
+ bridge that turns [Resonite](https://resonite.com/) into a runtime environment for AI agents.
5
+ The `resonite-io` distribution imports as `resoio` and wraps the `resonite_io.v1` gRPC schema
6
+ (Unix Domain Socket transport, async via `grpclib`) into a friendly, fully typed client
7
+ library and a `resoio` CLI.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install resonite-io
13
+ ```
14
+
15
+ ## Requires
16
+
17
+ A Resonite client running the **ResoniteIO mod** on the same host (the two halves connect
18
+ over a Unix Domain Socket). See the documentation for installing the mod.
19
+
20
+ ## Quick start
21
+
22
+ ```python
23
+ import asyncio
24
+
25
+ from resoio import ConnectionClient
26
+
27
+
28
+ async def main() -> None:
29
+ async with ConnectionClient() as client:
30
+ response = await client.ping("hello")
31
+ print(response.message)
32
+
33
+
34
+ asyncio.run(main())
35
+ ```
36
+
37
+ ## Documentation
38
+
39
+ - **Docs:** <https://mlshukai.github.io/ResoniteIO/>
40
+ - **Source:** <https://github.com/MLShukai/ResoniteIO>
41
+
42
+ ## License
43
+
44
+ [MIT](https://github.com/MLShukai/ResoniteIO/blob/main/LICENSE)
@@ -0,0 +1,119 @@
1
+ # resoio examples
2
+
3
+ 各モダリティの **最小 API 呼び出しサンプル** を 1 ファイル = 1 モダリティで
4
+ 収録する。 `python/src/resoio/cli/` の production CLI や `python/tests/e2e/` の
5
+ 回帰テストは "完全形" で重いため、ライブラリの呼び方を最短で把握したい場合は
6
+ こちらを読む。
7
+
8
+ ## 前提
9
+
10
+ - ホスト側で Resonite が起動しており、`ResoniteIO` mod が load 済み
11
+ - mod が `~/.resonite-io/` 配下に UDS を bind 済み (初回環境構築は
12
+ [`.claude/skills/setup-resonite-env/SKILL.md`](../../.claude/skills/setup-resonite-env/SKILL.md))
13
+ - 依存は dev container 内にすべて閉じている (`uv sync` 済み)
14
+
15
+ モダリティ固有の追加前提:
16
+
17
+ - `microphone_send.py` — Resonite Settings → Audio Input で "ResoniteIO" の
18
+ virtual mic デバイスを選択しないと音は鳴らない (mod 起動後 1 回だけ)
19
+ - `locomotion_drive.py` — `SmoothLocomotionBase` が active な world (default
20
+ Home Cloud は OK。Teleport / NoClip / NoLocomotion world は不可)
21
+ - `display_config.py` — Resonite が desktop mode で起動していること (VR mode
22
+ では `ResolutionSettings` が異なる経路を通る)
23
+ - `world_browse.py` — cloud に login 済みで join 可能な公開セッションが見える
24
+ こと (session list が空 = signed out / 空 cloud の場合は notice を print して
25
+ 安全に終了する)
26
+ - `context_menu_interact.py` — desktop の T-key radial menu が出せる状態
27
+ (LocalUser / InteractionHandler が attach 済みの world にいること)
28
+ - `dash_navigate.py` — userspace の Esc dash が開ける状態 (engine boot 済み)。
29
+ screen が少ない logged-out 状態でも開閉は可能だが navigation 先が減る
30
+ - `inventory_manage.py` — cloud に login 済み (inventory ops は実 cloud
31
+ inventory を叩く)。書込先は自分で mkdir する `/Inventory/__resoio_example__`
32
+ 配下のみで、最後に rm -r で後片付けする
33
+ - `cursor_move.py` — Resonite が desktop mode で起動していること (cursor は
34
+ desktop window 座標を操作する。カーソル自体は screenshot に写りにくいので、
35
+ 動いたことの可視確認は context menu を開く `tests/e2e/cursor.py` を参照)
36
+
37
+ ## 実行
38
+
39
+ すべて引数なし。dev container 内で:
40
+
41
+ ```bash
42
+ uv run python python/examples/connection_ping.py
43
+ uv run python python/examples/camera_view.py
44
+ uv run python python/examples/speaker_record.py
45
+ uv run python python/examples/microphone_send.py
46
+ uv run python python/examples/locomotion_drive.py
47
+ uv run python python/examples/display_config.py
48
+ uv run python python/examples/manipulation_grab.py
49
+ uv run python python/examples/world_browse.py
50
+ uv run python python/examples/context_menu_interact.py
51
+ uv run python python/examples/dash_navigate.py
52
+ uv run python python/examples/inventory_manage.py
53
+ uv run python python/examples/cursor_move.py
54
+ ```
55
+
56
+ 各 example の内容:
57
+
58
+ | File | やること |
59
+ | -------------------------- | ------------------------------------------------------------------------------------------- |
60
+ | `connection_ping.py` | `Connection.Ping` を 1 回呼んで RTT と server timestamp を print |
61
+ | `camera_view.py` | 5 秒 streaming して fps と最終フレームの輝度統計を print |
62
+ | `speaker_record.py` | 5 秒 streaming して peak amplitude を print + `speaker_output.raw` に raw float32 LE で保存 |
63
+ | `microphone_send.py` | 440 Hz / 3 秒 mono sine wave を生成し virtual mic に送信 |
64
+ | `locomotion_drive.py` | 6 秒 scripted シナリオで forward → strafe → yaw → jump → neutral を流し、reset() で締める |
65
+ | `display_config.py` | 現在解像度 → 1024x768 apply → 元解像度に restore |
66
+ | `manipulation_grab.py` | primary hand で get_state → grab → release の最小サイクル (空き home では grabbed=False) |
67
+ | `world_browse.py` | session list → join → list_open_worlds → focus → leave (空 cloud は notice して終了) |
68
+ | `context_menu_interact.py` | T-key radial を open → get_state → highlight(0) → invoke(first enabled) → close |
69
+ | `dash_navigate.py` | Esc dash を open → list_screens → set_screen(key) → get_tree → invoke(first) → close |
70
+ | `inventory_manage.py` | 一時 dir を mkdir → cp -r → mv → list で確認 → finally で rm -r 後片付け |
71
+ | `cursor_move.py` | get_position → center(0.5,0.5) → move(0.25,0.25) → 元位置に restore |
72
+
73
+ ## FAILED_PRECONDITION について
74
+
75
+ Resonite cold-boot 中 (UDS は bound 済みだが engine の `LocalUser` /
76
+ `FocusedWorld` がまだ attach されていない期間) は、各 bridge が
77
+ `grpclib.exceptions.GRPCError(status=Status.FAILED_PRECONDITION)` を返す。
78
+
79
+ 各 example は `wait_for_ready()` という小さな inline retry helper を持ち、
80
+ 1〜2 秒間隔で 60〜120 秒間 retry する。production レベルで同じことを
81
+ やりたい場合は、より厳密な実装例として `python/tests/e2e/*.py` の
82
+ `wait_for_*_ready()` 系を参照。
83
+
84
+ ## 出力 artifact
85
+
86
+ - `speaker_output.raw` — `speaker_record.py` が生成する raw float32 LE stereo
87
+ (48 kHz)。再生は `ffplay`:
88
+
89
+ ```bash
90
+ ffplay -f f32le -ar 48000 -ac 2 speaker_output.raw
91
+ ```
92
+
93
+ ## production reference
94
+
95
+ examples では「最短コード」を優先しているため、以下は意図的に削っている:
96
+
97
+ - argparse / CLI flag 群 (引数は module-level constant で固定)
98
+ - Signal handler (Ctrl+C は asyncio の default 挙動に委ねる)
99
+ - Per-frame 詳細ログ
100
+ - WAV / MP4 header / muxing (raw float32 / 統計 print のみ)
101
+ - TTY 制御 (Locomotion は scripted シナリオのみ、対話操作なし)
102
+ - 厳密な error 分類 (FAILED_PRECONDITION 以外の status はそのまま投げる)
103
+
104
+ 完全形が必要な場合は対応する CLI / e2e を参照:
105
+
106
+ | Example | CLI | E2E |
107
+ | -------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
108
+ | `connection_ping.py` | [`cli/ping.py`](../src/resoio/cli/ping.py) | [`tests/e2e/connection.py`](../tests/e2e/connection.py) |
109
+ | `camera_view.py` | [`cli/record.py`](../src/resoio/cli/record.py) (video 経路) | [`tests/e2e/camera_stream.py`](../tests/e2e/camera_stream.py) |
110
+ | `speaker_record.py` | [`cli/record.py`](../src/resoio/cli/record.py) (audio 経路) | [`tests/e2e/speaker_record.py`](../tests/e2e/speaker_record.py) |
111
+ | `microphone_send.py` | [`cli/mic.py`](../src/resoio/cli/mic.py) | [`tests/e2e/mic_send.py`](../tests/e2e/mic_send.py) |
112
+ | `locomotion_drive.py` | [`cli/locomotion.py`](../src/resoio/cli/locomotion.py) | [`tests/e2e/locomotion.py`](../tests/e2e/locomotion.py) |
113
+ | `display_config.py` | [`cli/display.py`](../src/resoio/cli/display.py) | [`tests/e2e/display_resolution.py`](../tests/e2e/display_resolution.py) |
114
+ | `manipulation_grab.py` | [`cli/manipulate.py`](../src/resoio/cli/manipulate.py) | [`tests/e2e/manipulation.py`](../tests/e2e/manipulation.py) |
115
+ | `world_browse.py` | [`cli/world.py`](../src/resoio/cli/world.py) | [`tests/e2e/world.py`](../tests/e2e/world.py) |
116
+ | `context_menu_interact.py` | [`cli/context_menu.py`](../src/resoio/cli/context_menu.py) | [`tests/e2e/context_menu.py`](../tests/e2e/context_menu.py) |
117
+ | `dash_navigate.py` | [`cli/dash.py`](../src/resoio/cli/dash.py) | [`tests/e2e/dash.py`](../tests/e2e/dash.py) |
118
+ | `inventory_manage.py` | [`cli/inventory.py`](../src/resoio/cli/inventory.py) | [`tests/e2e/inventory.py`](../tests/e2e/inventory.py) |
119
+ | `cursor_move.py` | [`cli/cursor.py`](../src/resoio/cli/cursor.py) | [`tests/e2e/cursor.py`](../tests/e2e/cursor.py) |
@@ -0,0 +1,86 @@
1
+ """Minimal Camera stream example.
2
+
3
+ Streams RGBA frames for DURATION_S seconds, then prints the achieved
4
+ fps and basic luminance statistics of the final frame. No frames are
5
+ written to disk; numpy is the only non-stdlib dependency. Assumes a
6
+ Resonite client with the ResoniteIO mod loaded is running on the host.
7
+
8
+ Run from inside the dev container:
9
+
10
+ uv run python python/examples/camera_view.py
11
+ """
12
+
13
+ import asyncio
14
+ import time
15
+
16
+ import grpclib.exceptions
17
+ import numpy as np
18
+ from grpclib.const import Status
19
+
20
+ from resoio import CameraClient, Frame
21
+
22
+ SOCKET_PATH: str | None = None
23
+ DURATION_S = 5.0
24
+ WIDTH = 640
25
+ HEIGHT = 480
26
+ # fps_limit caps server emission so this demo does not burn CPU
27
+ # rendering at the engine's native framerate.
28
+ FPS_LIMIT = 30.0
29
+ READY_TIMEOUT_S = 120.0
30
+ READY_INTERVAL_S = 2.0
31
+
32
+
33
+ async def wait_for_ready() -> None:
34
+ """Block until the Camera bridge yields a frame.
35
+
36
+ Cold-boot gap between UDS bind and LocalUser/FocusedWorld attach
37
+ surfaces as FAILED_PRECONDITION; retry until ``READY_TIMEOUT_S``.
38
+ """
39
+ deadline = time.monotonic() + READY_TIMEOUT_S
40
+ while True:
41
+ try:
42
+ async with CameraClient(SOCKET_PATH) as cam:
43
+ async for _ in cam.stream(1, 1, 1.0):
44
+ return
45
+ except grpclib.exceptions.GRPCError as e:
46
+ if e.status != Status.FAILED_PRECONDITION:
47
+ raise
48
+ if time.monotonic() > deadline:
49
+ raise TimeoutError(
50
+ f"Camera did not become ready in {READY_TIMEOUT_S:.0f}s"
51
+ ) from e
52
+ await asyncio.sleep(READY_INTERVAL_S)
53
+
54
+
55
+ async def main() -> None:
56
+ await wait_for_ready()
57
+ count = 0
58
+ last: Frame | None = None
59
+ t0 = 0.0
60
+ elapsed = 0.0
61
+ async with CameraClient(SOCKET_PATH) as client:
62
+ async for frame in client.stream(WIDTH, HEIGHT, FPS_LIMIT):
63
+ if count == 0:
64
+ t0 = time.monotonic()
65
+ last = frame
66
+ count += 1
67
+ elapsed = time.monotonic() - t0
68
+ if elapsed >= DURATION_S:
69
+ break
70
+
71
+ fps = count / elapsed if elapsed > 0 else 0.0
72
+ print(f"frames={count} elapsed_s={elapsed:.3f} fps={fps:.2f}")
73
+ if last is not None:
74
+ # frame.pixels is a read-only RGBA8 view; row 0 is the image
75
+ # top. astype(float32) makes a writable copy for arithmetic.
76
+ rgb = last.pixels[..., :3].astype(np.float32)
77
+ lum = 0.299 * rgb[..., 0] + 0.587 * rgb[..., 1] + 0.114 * rgb[..., 2]
78
+ print(
79
+ f"shape={last.pixels.shape} dtype={last.pixels.dtype} "
80
+ f"lum_min={lum.min():.2f} lum_max={lum.max():.2f} "
81
+ f"lum_mean={lum.mean():.2f}"
82
+ )
83
+
84
+
85
+ if __name__ == "__main__":
86
+ asyncio.run(main())
@@ -0,0 +1,66 @@
1
+ """Minimal Connection.Ping example.
2
+
3
+ Sends a single ping over the Resonite IO UDS and prints the server
4
+ timestamp plus the measured round-trip time. Assumes a Resonite client
5
+ with the ResoniteIO mod loaded is running on the host.
6
+
7
+ Run from inside the dev container:
8
+
9
+ uv run python python/examples/connection_ping.py
10
+ """
11
+
12
+ import asyncio
13
+ import time
14
+
15
+ import grpclib.exceptions
16
+ from grpclib.const import Status
17
+
18
+ from resoio import ConnectionClient
19
+
20
+ SOCKET_PATH: str | None = None
21
+ MESSAGE = "hello"
22
+ READY_TIMEOUT_S = 60.0
23
+ READY_INTERVAL_S = 2.0
24
+
25
+
26
+ async def wait_for_ready() -> None:
27
+ """Block until Connection.Ping returns OK.
28
+
29
+ During cold boot the UDS may be bound before the engine is fully
30
+ ready, in which case the server replies with FAILED_PRECONDITION.
31
+ Retry until ``READY_TIMEOUT_S`` elapses.
32
+ """
33
+ deadline = time.monotonic() + READY_TIMEOUT_S
34
+ while True:
35
+ try:
36
+ async with ConnectionClient(SOCKET_PATH) as client:
37
+ await client.ping("ready?")
38
+ return
39
+ except grpclib.exceptions.GRPCError as e:
40
+ if e.status != Status.FAILED_PRECONDITION:
41
+ raise
42
+ if time.monotonic() > deadline:
43
+ raise TimeoutError(
44
+ f"Connection did not become ready in {READY_TIMEOUT_S:.0f}s"
45
+ ) from e
46
+ await asyncio.sleep(READY_INTERVAL_S)
47
+
48
+
49
+ async def main() -> None:
50
+ await wait_for_ready()
51
+ async with ConnectionClient(SOCKET_PATH) as client:
52
+ # monotonic_ns is immune to wall-clock jumps (NTP step / DST)
53
+ # that would otherwise produce negative or inflated RTTs.
54
+ t0 = time.monotonic_ns()
55
+ resp = await client.ping(MESSAGE)
56
+ t1 = time.monotonic_ns()
57
+ rtt_ms = (t1 - t0) / 1e6
58
+ print(
59
+ f"message={resp.message} "
60
+ f"server_unix_nanos={resp.server_unix_nanos} "
61
+ f"rtt_ms={rtt_ms:.3f}"
62
+ )
63
+
64
+
65
+ if __name__ == "__main__":
66
+ asyncio.run(main())