styly-netsync-server 0.10.4__tar.gz → 0.11.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.
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/PKG-INFO +69 -17
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/README.md +68 -16
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/binary_serializer.py +16 -10
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/rest_bridge.py +67 -9
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/server.py +2 -2
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/PKG-INFO +69 -17
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_binary_serializer.py +54 -13
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_rest_bridge.py +153 -7
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/LICENSE +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/setup.cfg +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/client.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/client_simulator.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/SOURCES.txt +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_discovery_probe.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_object_sync.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_timing_monotonic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: styly-netsync-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences
|
|
5
5
|
Author-email: "STYLY, Inc." <info@styly.inc>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -81,12 +81,14 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
81
81
|
|
|
82
82
|
## Wire protocol compatibility
|
|
83
83
|
|
|
84
|
-
- Current transform wire protocol is `protocolVersion =
|
|
85
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
86
|
-
-
|
|
84
|
+
- Current transform wire protocol is `protocolVersion = 4`.
|
|
85
|
+
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V4 pose body.
|
|
86
|
+
- v4 vs. v3: `xrOriginDelta` carries a Y component as a 4th `int16` (`dx, dy, dz, dyaw` = 8 bytes vs. v3's 6), so receivers can reconstruct the sender's rig-Y motion (e.g. elevators).
|
|
87
|
+
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
87
88
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
88
|
-
- Protocol
|
|
89
|
-
- Absolute (`
|
|
89
|
+
- Protocol v4 position quantization ranges:
|
|
90
|
+
- Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
|
|
91
|
+
- XROrigin locomotion delta (`xrOriginDelta`, 4×`int16`: `dx, dy, dz, dyaw`): `0.01 m` per unit for translation, `0.1°` for yaw. Receivers reconstruct `physicalPos = invDeltaRot * (headPos − deltaPos)`; it is not on the wire as a separate absolute field.
|
|
90
92
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
91
93
|
- These are encoding limits, not a hard world-size cap. Worlds can be larger, but encoded axis values are clamped if they exceed the representable range.
|
|
92
94
|
|
|
@@ -94,18 +96,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
94
96
|
|
|
95
97
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
96
98
|
|
|
97
|
-
Assumed baseline (`protocolVersion=
|
|
98
|
-
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `
|
|
99
|
-
- Room per-client entry (`clientNo + poseTime + clientBody`): `
|
|
99
|
+
Assumed baseline (`protocolVersion=4`):
|
|
100
|
+
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
|
|
101
|
+
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
100
102
|
|
|
101
103
|
| Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
|
|
102
104
|
|---|---|---:|---:|---:|
|
|
103
|
-
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`
|
|
104
|
-
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`
|
|
105
|
-
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`
|
|
105
|
+
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
|
|
106
|
+
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
107
|
+
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
106
108
|
|
|
107
109
|
Notes:
|
|
108
|
-
-
|
|
110
|
+
- Only `headPosAbs` is on the wire as an absolute field; `physicalPos` is reconstructed from `headPosAbs + xrOriginDelta`. Option B/C deltas therefore apply to `headPosAbs` only.
|
|
109
111
|
- Option B/C can reduce average overhead if `cell` is transmitted only when changed, but that requires extra state and flags in the wire format.
|
|
110
112
|
|
|
111
113
|
## Configuration
|
|
@@ -160,7 +162,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
160
162
|
|
|
161
163
|
```json
|
|
162
164
|
{
|
|
163
|
-
"
|
|
165
|
+
"variables": {
|
|
164
166
|
"name": "Jack",
|
|
165
167
|
"lang": "EN"
|
|
166
168
|
}
|
|
@@ -179,11 +181,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
179
181
|
```bash
|
|
180
182
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
|
|
181
183
|
-H "Content-Type: application/json" \
|
|
182
|
-
-d '{"
|
|
184
|
+
-d '{"variables":{"name":"Jack","lang":"EN"}}'
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
|
|
186
188
|
|
|
189
|
+
- Read endpoints:
|
|
190
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
|
|
197
|
+
|
|
198
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{"clientNo": 7, "value": "Jack"}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Returns `404` if the device is unmapped or the variable is not set.
|
|
205
|
+
|
|
206
|
+
- Example:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
|
|
210
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
|
|
211
|
+
```
|
|
212
|
+
|
|
187
213
|
### Global variables
|
|
188
214
|
|
|
189
215
|
- Endpoint: `POST /v1/rooms/{roomId}/global-variables`
|
|
@@ -191,7 +217,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
191
217
|
|
|
192
218
|
```json
|
|
193
219
|
{
|
|
194
|
-
"
|
|
220
|
+
"variables": {
|
|
195
221
|
"score": "42",
|
|
196
222
|
"stage": "lobby"
|
|
197
223
|
}
|
|
@@ -210,7 +236,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
210
236
|
```bash
|
|
211
237
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
|
|
212
238
|
-H "Content-Type: application/json" \
|
|
213
|
-
-d '{"
|
|
239
|
+
-d '{"variables":{"score":"42","stage":"lobby"}}'
|
|
214
240
|
```
|
|
215
241
|
|
|
216
242
|
The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
|
|
243
|
+
|
|
244
|
+
- Read endpoints:
|
|
245
|
+
- `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{"variables": {"score": "42", "stage": "lobby"}}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{"value": "42"}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Returns `404` if the variable is not set.
|
|
258
|
+
|
|
259
|
+
- Example:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
|
|
263
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Read consistency
|
|
267
|
+
|
|
268
|
+
GET endpoints return a snapshot of the REST bridge's in-process cache, which is populated by PUB-SUB broadcasts from the server. The first request to a room lazily creates a bridge and may return an empty snapshot until the initial broadcasts arrive — retry after a short delay if needed.
|
|
@@ -42,12 +42,14 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
42
42
|
|
|
43
43
|
## Wire protocol compatibility
|
|
44
44
|
|
|
45
|
-
- Current transform wire protocol is `protocolVersion =
|
|
46
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
47
|
-
-
|
|
45
|
+
- Current transform wire protocol is `protocolVersion = 4`.
|
|
46
|
+
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V4 pose body.
|
|
47
|
+
- v4 vs. v3: `xrOriginDelta` carries a Y component as a 4th `int16` (`dx, dy, dz, dyaw` = 8 bytes vs. v3's 6), so receivers can reconstruct the sender's rig-Y motion (e.g. elevators).
|
|
48
|
+
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
48
49
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
49
|
-
- Protocol
|
|
50
|
-
- Absolute (`
|
|
50
|
+
- Protocol v4 position quantization ranges:
|
|
51
|
+
- Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
|
|
52
|
+
- XROrigin locomotion delta (`xrOriginDelta`, 4×`int16`: `dx, dy, dz, dyaw`): `0.01 m` per unit for translation, `0.1°` for yaw. Receivers reconstruct `physicalPos = invDeltaRot * (headPos − deltaPos)`; it is not on the wire as a separate absolute field.
|
|
51
53
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
52
54
|
- These are encoding limits, not a hard world-size cap. Worlds can be larger, but encoded axis values are clamped if they exceed the representable range.
|
|
53
55
|
|
|
@@ -55,18 +57,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
55
57
|
|
|
56
58
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
57
59
|
|
|
58
|
-
Assumed baseline (`protocolVersion=
|
|
59
|
-
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `
|
|
60
|
-
- Room per-client entry (`clientNo + poseTime + clientBody`): `
|
|
60
|
+
Assumed baseline (`protocolVersion=4`):
|
|
61
|
+
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
|
|
62
|
+
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
61
63
|
|
|
62
64
|
| Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
|
|
63
65
|
|---|---|---:|---:|---:|
|
|
64
|
-
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`
|
|
65
|
-
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`
|
|
66
|
-
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`
|
|
66
|
+
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
|
|
67
|
+
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
68
|
+
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
67
69
|
|
|
68
70
|
Notes:
|
|
69
|
-
-
|
|
71
|
+
- Only `headPosAbs` is on the wire as an absolute field; `physicalPos` is reconstructed from `headPosAbs + xrOriginDelta`. Option B/C deltas therefore apply to `headPosAbs` only.
|
|
70
72
|
- Option B/C can reduce average overhead if `cell` is transmitted only when changed, but that requires extra state and flags in the wire format.
|
|
71
73
|
|
|
72
74
|
## Configuration
|
|
@@ -121,7 +123,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
121
123
|
|
|
122
124
|
```json
|
|
123
125
|
{
|
|
124
|
-
"
|
|
126
|
+
"variables": {
|
|
125
127
|
"name": "Jack",
|
|
126
128
|
"lang": "EN"
|
|
127
129
|
}
|
|
@@ -140,11 +142,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
140
142
|
```bash
|
|
141
143
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
|
|
142
144
|
-H "Content-Type: application/json" \
|
|
143
|
-
-d '{"
|
|
145
|
+
-d '{"variables":{"name":"Jack","lang":"EN"}}'
|
|
144
146
|
```
|
|
145
147
|
|
|
146
148
|
The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
|
|
147
149
|
|
|
150
|
+
- Read endpoints:
|
|
151
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
|
|
158
|
+
|
|
159
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{"clientNo": 7, "value": "Jack"}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Returns `404` if the device is unmapped or the variable is not set.
|
|
166
|
+
|
|
167
|
+
- Example:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
|
|
171
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
|
|
172
|
+
```
|
|
173
|
+
|
|
148
174
|
### Global variables
|
|
149
175
|
|
|
150
176
|
- Endpoint: `POST /v1/rooms/{roomId}/global-variables`
|
|
@@ -152,7 +178,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
152
178
|
|
|
153
179
|
```json
|
|
154
180
|
{
|
|
155
|
-
"
|
|
181
|
+
"variables": {
|
|
156
182
|
"score": "42",
|
|
157
183
|
"stage": "lobby"
|
|
158
184
|
}
|
|
@@ -171,7 +197,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
171
197
|
```bash
|
|
172
198
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
|
|
173
199
|
-H "Content-Type: application/json" \
|
|
174
|
-
-d '{"
|
|
200
|
+
-d '{"variables":{"score":"42","stage":"lobby"}}'
|
|
175
201
|
```
|
|
176
202
|
|
|
177
203
|
The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
|
|
204
|
+
|
|
205
|
+
- Read endpoints:
|
|
206
|
+
- `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{"variables": {"score": "42", "stage": "lobby"}}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{"value": "42"}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Returns `404` if the variable is not set.
|
|
219
|
+
|
|
220
|
+
- Example:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
|
|
224
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Read consistency
|
|
228
|
+
|
|
229
|
+
GET endpoints return a snapshot of the REST bridge's in-process cache, which is populated by PUB-SUB broadcasts from the server. The first request to a room lazily creates a bridge and may return an empty snapshot until the initial broadcasts arrive — retry after a short delay if needed.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "styly-netsync-server"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.0"
|
|
8
8
|
description = "STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/binary_serializer.py
RENAMED
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
logger = logging.getLogger(__name__)
|
|
7
7
|
|
|
8
8
|
# Message type identifiers
|
|
9
|
-
PROTOCOL_VERSION =
|
|
9
|
+
PROTOCOL_VERSION = 4
|
|
10
10
|
MSG_CLIENT_TRANSFORM = 1
|
|
11
11
|
MSG_ROOM_TRANSFORM = 2 # Legacy room transform with short IDs only
|
|
12
12
|
MSG_RPC = 3 # Remote procedure call
|
|
@@ -32,7 +32,7 @@ MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request reject
|
|
|
32
32
|
_max_virtual_transforms = 50
|
|
33
33
|
MAX_VIRTUAL_TRANSFORMS = _max_virtual_transforms # Legacy alias for backward compat
|
|
34
34
|
|
|
35
|
-
# Protocol
|
|
35
|
+
# Protocol v4 transform encoding constants
|
|
36
36
|
ABS_POS_SCALE = 0.01
|
|
37
37
|
LOCO_POS_SCALE = 0.01
|
|
38
38
|
REL_POS_SCALE = 0.005
|
|
@@ -372,7 +372,7 @@ def _create_transform_dict(
|
|
|
372
372
|
|
|
373
373
|
|
|
374
374
|
def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
375
|
-
"""Serialize a client body in protocol
|
|
375
|
+
"""Serialize a client body in protocol v4 compact format."""
|
|
376
376
|
pose_seq = int(client.get("poseSeq", 0)) & 0xFFFF
|
|
377
377
|
head = client.get("head", {}) or {}
|
|
378
378
|
right = client.get("rightHand", {}) or {}
|
|
@@ -380,6 +380,7 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
380
380
|
virtuals = client.get("virtuals", []) or []
|
|
381
381
|
has_xr_origin_delta = (
|
|
382
382
|
"xrOriginDeltaX" in client
|
|
383
|
+
or "xrOriginDeltaY" in client
|
|
383
384
|
or "xrOriginDeltaZ" in client
|
|
384
385
|
or "xrOriginDeltaYaw" in client
|
|
385
386
|
)
|
|
@@ -421,6 +422,7 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
421
422
|
virtual_valid = head_valid and bool(flags & POSE_FLAG_VIRTUALS_VALID)
|
|
422
423
|
|
|
423
424
|
xr_origin_delta_x = float(client.get("xrOriginDeltaX", 0.0))
|
|
425
|
+
xr_origin_delta_y = float(client.get("xrOriginDeltaY", 0.0))
|
|
424
426
|
xr_origin_delta_z = float(client.get("xrOriginDeltaZ", 0.0))
|
|
425
427
|
xr_origin_delta_yaw = float(client.get("xrOriginDeltaYaw", 0.0))
|
|
426
428
|
head_pos = _transform_get_position(head)
|
|
@@ -430,8 +432,9 @@ def _serialize_client_body(buffer: bytearray, client: dict[str, Any]) -> None:
|
|
|
430
432
|
if physical_valid:
|
|
431
433
|
buffer.extend(
|
|
432
434
|
struct.pack(
|
|
433
|
-
"<
|
|
435
|
+
"<hhhh",
|
|
434
436
|
_quantize_signed(xr_origin_delta_x, LOCO_POS_SCALE),
|
|
437
|
+
_quantize_signed(xr_origin_delta_y, LOCO_POS_SCALE),
|
|
435
438
|
_quantize_signed(xr_origin_delta_z, LOCO_POS_SCALE),
|
|
436
439
|
_quantize_signed(xr_origin_delta_yaw, PHYSICAL_YAW_SCALE),
|
|
437
440
|
)
|
|
@@ -849,7 +852,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
849
852
|
|
|
850
853
|
|
|
851
854
|
def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any], int]:
|
|
852
|
-
"""Deserialize protocol
|
|
855
|
+
"""Deserialize protocol v4 compact pose body."""
|
|
853
856
|
result: dict[str, Any] = {}
|
|
854
857
|
result["poseSeq"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
855
858
|
offset += 2
|
|
@@ -874,6 +877,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
874
877
|
head_pos = (0.0, 0.0, 0.0)
|
|
875
878
|
head_rot = (0.0, 0.0, 0.0, 1.0)
|
|
876
879
|
xr_origin_delta_x = 0.0
|
|
880
|
+
xr_origin_delta_y = 0.0
|
|
877
881
|
xr_origin_delta_z = 0.0
|
|
878
882
|
xr_origin_delta_yaw = 0.0
|
|
879
883
|
|
|
@@ -882,11 +886,12 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
882
886
|
raise ValueError(
|
|
883
887
|
"PhysicalValid set but XROrigin delta encoding flag is missing"
|
|
884
888
|
)
|
|
885
|
-
dx_q, dz_q, dyaw_q = struct.unpack("<
|
|
889
|
+
dx_q, dy_q, dz_q, dyaw_q = struct.unpack("<hhhh", data[offset : offset + 8])
|
|
886
890
|
xr_origin_delta_x = _dequantize_signed(dx_q, LOCO_POS_SCALE)
|
|
891
|
+
xr_origin_delta_y = _dequantize_signed(dy_q, LOCO_POS_SCALE)
|
|
887
892
|
xr_origin_delta_z = _dequantize_signed(dz_q, LOCO_POS_SCALE)
|
|
888
893
|
xr_origin_delta_yaw = _dequantize_signed(dyaw_q, PHYSICAL_YAW_SCALE)
|
|
889
|
-
offset +=
|
|
894
|
+
offset += 8
|
|
890
895
|
|
|
891
896
|
if head_valid:
|
|
892
897
|
hx_q, offset = _unpack_int24_le(data, offset)
|
|
@@ -913,7 +918,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
913
918
|
|
|
914
919
|
if physical_valid and head_valid:
|
|
915
920
|
translated_x = head_pos[0] - xr_origin_delta_x
|
|
916
|
-
translated_y = head_pos[1]
|
|
921
|
+
translated_y = head_pos[1] - xr_origin_delta_y
|
|
917
922
|
translated_z = head_pos[2] - xr_origin_delta_z
|
|
918
923
|
physical_pos = _rotate_yaw_vector(
|
|
919
924
|
translated_x,
|
|
@@ -1041,6 +1046,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1041
1046
|
)
|
|
1042
1047
|
|
|
1043
1048
|
result["xrOriginDeltaX"] = xr_origin_delta_x
|
|
1049
|
+
result["xrOriginDeltaY"] = xr_origin_delta_y
|
|
1044
1050
|
result["xrOriginDeltaZ"] = xr_origin_delta_z
|
|
1045
1051
|
result["xrOriginDeltaYaw"] = xr_origin_delta_yaw
|
|
1046
1052
|
result["physical"] = physical
|
|
@@ -1052,7 +1058,7 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
1052
1058
|
|
|
1053
1059
|
|
|
1054
1060
|
def _deserialize_client_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1055
|
-
"""Deserialize client pose (
|
|
1061
|
+
"""Deserialize client pose (v4) from binary data."""
|
|
1056
1062
|
result: dict[str, Any] = {}
|
|
1057
1063
|
|
|
1058
1064
|
protocol_version = data[offset]
|
|
@@ -1090,7 +1096,7 @@ def _deserialize_rpc_message(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1090
1096
|
|
|
1091
1097
|
|
|
1092
1098
|
def _deserialize_room_transform(data: bytes, offset: int) -> dict[str, Any]:
|
|
1093
|
-
"""Deserialize room pose (
|
|
1099
|
+
"""Deserialize room pose (v4) with client numbers only."""
|
|
1094
1100
|
result: dict[str, Any] = {}
|
|
1095
1101
|
|
|
1096
1102
|
protocol_version = data[offset]
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/rest_bridge.py
RENAMED
|
@@ -29,7 +29,7 @@ VarValue = Annotated[str, StringConstraints(max_length=MAX_VALUE)]
|
|
|
29
29
|
class UpsertBody(BaseModel):
|
|
30
30
|
"""Request body for client or global variable upsert."""
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
variables: dict[VarName, VarValue] = Field(default_factory=dict)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class PreseedStore:
|
|
@@ -187,6 +187,25 @@ class RoomBridge:
|
|
|
187
187
|
logger.debug("Failed to lookup client number for %s: %s", device_id, exc)
|
|
188
188
|
return None
|
|
189
189
|
|
|
190
|
+
def get_global_variables(self) -> dict[str, str]:
|
|
191
|
+
"""Return a snapshot of cached global variables for this room."""
|
|
192
|
+
try:
|
|
193
|
+
return self._manager.get_all_global_variables()
|
|
194
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
195
|
+
logger.debug("get_all_global_variables failed: %s", exc)
|
|
196
|
+
return {}
|
|
197
|
+
|
|
198
|
+
def get_client_variables(self, device_id: str) -> tuple[int | None, dict[str, str]]:
|
|
199
|
+
"""Return (client_no, snapshot) for device; client_no is None if unmapped."""
|
|
200
|
+
client_no = self.get_client_no(device_id)
|
|
201
|
+
if client_no is None:
|
|
202
|
+
return None, {}
|
|
203
|
+
try:
|
|
204
|
+
return client_no, self._manager.get_all_client_variables(client_no)
|
|
205
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
206
|
+
logger.debug("get_all_client_variables failed: %s", exc)
|
|
207
|
+
return client_no, {}
|
|
208
|
+
|
|
190
209
|
def _apply_to_client(self, client_no: int, kvs: dict[str, str]) -> set[str]:
|
|
191
210
|
"""Apply stored variables to the target client via set_client_variable."""
|
|
192
211
|
applied: set[str] = set()
|
|
@@ -308,15 +327,15 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
308
327
|
|
|
309
328
|
@app.post("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
310
329
|
def upsert(room_id: str, device_id: str, body: UpsertBody) -> dict[str, object]:
|
|
311
|
-
if not body.
|
|
312
|
-
raise HTTPException(status_code=400, detail="
|
|
330
|
+
if not body.variables:
|
|
331
|
+
raise HTTPException(status_code=400, detail="variables must not be empty")
|
|
313
332
|
try:
|
|
314
|
-
store.upsert(room_id, device_id, body.
|
|
333
|
+
store.upsert(room_id, device_id, body.variables)
|
|
315
334
|
except ValueError as exc:
|
|
316
335
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
317
336
|
|
|
318
337
|
bridge = manager.get(room_id)
|
|
319
|
-
statuses = bridge.apply_now_or_queue(device_id, body.
|
|
338
|
+
statuses = bridge.apply_now_or_queue(device_id, body.variables)
|
|
320
339
|
client_no = bridge.get_client_no(device_id)
|
|
321
340
|
|
|
322
341
|
return {
|
|
@@ -328,15 +347,15 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
328
347
|
|
|
329
348
|
@app.post("/v1/rooms/{room_id}/global-variables")
|
|
330
349
|
def upsert_global(room_id: str, body: UpsertBody) -> dict[str, object]:
|
|
331
|
-
if not body.
|
|
332
|
-
raise HTTPException(status_code=400, detail="
|
|
350
|
+
if not body.variables:
|
|
351
|
+
raise HTTPException(status_code=400, detail="variables must not be empty")
|
|
333
352
|
|
|
334
353
|
bridge = manager.get(room_id)
|
|
335
|
-
statuses = bridge.apply_global_now_or_queue(body.
|
|
354
|
+
statuses = bridge.apply_global_now_or_queue(body.variables)
|
|
336
355
|
|
|
337
356
|
# Only store variables that were queued (not applied immediately)
|
|
338
357
|
queued = {
|
|
339
|
-
name: body.
|
|
358
|
+
name: body.variables[name]
|
|
340
359
|
for name, state in statuses.items()
|
|
341
360
|
if state != "applied"
|
|
342
361
|
}
|
|
@@ -351,6 +370,45 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
351
370
|
"result": {name: {"state": state} for name, state in statuses.items()},
|
|
352
371
|
}
|
|
353
372
|
|
|
373
|
+
@app.get("/v1/rooms/{room_id}/global-variables")
|
|
374
|
+
def get_global_variables(room_id: str) -> dict[str, object]:
|
|
375
|
+
bridge = manager.get(room_id)
|
|
376
|
+
return {"variables": bridge.get_global_variables()}
|
|
377
|
+
|
|
378
|
+
@app.get("/v1/rooms/{room_id}/global-variables/{name}")
|
|
379
|
+
def get_global_variable(room_id: str, name: VarName) -> dict[str, object]:
|
|
380
|
+
bridge = manager.get(room_id)
|
|
381
|
+
variables = bridge.get_global_variables()
|
|
382
|
+
if name not in variables:
|
|
383
|
+
raise HTTPException(
|
|
384
|
+
status_code=404, detail=f"Global variable '{name}' not found"
|
|
385
|
+
)
|
|
386
|
+
return {"value": variables[name]}
|
|
387
|
+
|
|
388
|
+
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
389
|
+
def get_client_variables(room_id: str, device_id: str) -> dict[str, object]:
|
|
390
|
+
bridge = manager.get(room_id)
|
|
391
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
392
|
+
return {"clientNo": client_no, "variables": variables}
|
|
393
|
+
|
|
394
|
+
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables/{name}")
|
|
395
|
+
def get_client_variable(
|
|
396
|
+
room_id: str, device_id: str, name: VarName
|
|
397
|
+
) -> dict[str, object]:
|
|
398
|
+
bridge = manager.get(room_id)
|
|
399
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
400
|
+
if client_no is None:
|
|
401
|
+
raise HTTPException(
|
|
402
|
+
status_code=404,
|
|
403
|
+
detail=f"Device '{device_id}' has no client mapping",
|
|
404
|
+
)
|
|
405
|
+
if name not in variables:
|
|
406
|
+
raise HTTPException(
|
|
407
|
+
status_code=404,
|
|
408
|
+
detail=f"Client variable '{name}' not found for device '{device_id}'",
|
|
409
|
+
)
|
|
410
|
+
return {"clientNo": client_no, "value": variables[name]}
|
|
411
|
+
|
|
354
412
|
return app
|
|
355
413
|
|
|
356
414
|
|
|
@@ -1056,7 +1056,7 @@ class NetSyncServer:
|
|
|
1056
1056
|
except UnicodeDecodeError as e:
|
|
1057
1057
|
logger.error(f"Failed to decode room ID: {e}")
|
|
1058
1058
|
continue
|
|
1059
|
-
# Protocol
|
|
1059
|
+
# Protocol v4 binary-only handling (no JSON fallback)
|
|
1060
1060
|
try:
|
|
1061
1061
|
msg_type, data, raw_payload = binary_serializer.deserialize(
|
|
1062
1062
|
message_bytes
|
|
@@ -1127,7 +1127,7 @@ class NetSyncServer:
|
|
|
1127
1127
|
logger.warning(f"Unknown binary msg_type: {msg_type}")
|
|
1128
1128
|
except Exception as e:
|
|
1129
1129
|
logger.warning(
|
|
1130
|
-
"Failed to decode protocol
|
|
1130
|
+
"Failed to decode protocol v4 message from room %s: %s",
|
|
1131
1131
|
room_id,
|
|
1132
1132
|
e,
|
|
1133
1133
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: styly-netsync-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: STYLY NetSync Server - Multiplayer framework for Location-Based Entertainment VR/MR experiences
|
|
5
5
|
Author-email: "STYLY, Inc." <info@styly.inc>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -81,12 +81,14 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
81
81
|
|
|
82
82
|
## Wire protocol compatibility
|
|
83
83
|
|
|
84
|
-
- Current transform wire protocol is `protocolVersion =
|
|
85
|
-
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact
|
|
86
|
-
-
|
|
84
|
+
- Current transform wire protocol is `protocolVersion = 4`.
|
|
85
|
+
- Transform messages use `MSG_CLIENT_POSE` (11) and `MSG_ROOM_POSE` (12) with the compact V4 pose body.
|
|
86
|
+
- v4 vs. v3: `xrOriginDelta` carries a Y component as a 4th `int16` (`dx, dy, dz, dyaw` = 8 bytes vs. v3's 6), so receivers can reconstruct the sender's rig-Y motion (e.g. elevators).
|
|
87
|
+
- Legacy transform protocols (v2/v3) and JSON transform fallback are not supported.
|
|
87
88
|
- Deploy Unity and Python updates together when changing transform protocol behavior.
|
|
88
|
-
- Protocol
|
|
89
|
-
- Absolute (`
|
|
89
|
+
- Protocol v4 position quantization ranges:
|
|
90
|
+
- Absolute (`headPosAbs` only): signed `int24` at `0.01 m` per unit, per-axis range `[-83,886.08 m, 83,886.07 m]`.
|
|
91
|
+
- XROrigin locomotion delta (`xrOriginDelta`, 4×`int16`: `dx, dy, dz, dyaw`): `0.01 m` per unit for translation, `0.1°` for yaw. Receivers reconstruct `physicalPos = invDeltaRot * (headPos − deltaPos)`; it is not on the wire as a separate absolute field.
|
|
90
92
|
- Head-relative (`right/left/virtual`): signed `int16` at `0.005 m` per unit, per-axis range `[-163.84 m, 163.835 m]`.
|
|
91
93
|
- These are encoding limits, not a hard world-size cap. Worlds can be larger, but encoded axis values are clamped if they exceed the representable range.
|
|
92
94
|
|
|
@@ -94,18 +96,18 @@ styly-netsync-simulator --server tcp://localhost --room my_room --clients 50
|
|
|
94
96
|
|
|
95
97
|
The following options summarize trade-offs when expanding absolute-position range.
|
|
96
98
|
|
|
97
|
-
Assumed baseline (`protocolVersion=
|
|
98
|
-
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `
|
|
99
|
-
- Room per-client entry (`clientNo + poseTime + clientBody`): `
|
|
99
|
+
Assumed baseline (`protocolVersion=4`):
|
|
100
|
+
- Client pose body with `Physical+Head+Right+Left` valid and `virtualCount=0`: `46 bytes` (matches `test_client_body_size_with_full_pose_no_virtuals`).
|
|
101
|
+
- Room per-client entry (`clientNo + poseTime + clientBody`): `56 bytes`.
|
|
100
102
|
|
|
101
103
|
| Option | Absolute Position Encoding | Per-axis Range | Client Body Delta | Room Per-client Delta |
|
|
102
104
|
|---|---|---:|---:|---:|
|
|
103
|
-
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`
|
|
104
|
-
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`
|
|
105
|
-
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`
|
|
105
|
+
| A. Coarser scale (current integer width) | `int24 @ 0.02m` | `[-167,772.16m, 167,772.14m]` | `+0B` (`46 -> 46`) | `+0B` (`56 -> 56`) |
|
|
106
|
+
| B. Cell + local | `cell(i16, 256m) + local(int24 @ 0.01m)` | `[-8,472,494.08m, 8,472,238.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
107
|
+
| C. Cell + local (large cell) | `cell(i16, 1024m) + local(int24 @ 0.01m)` | `[-33,638,318.08m, 33,637,294.07m]` | `+6B` (`46 -> 52`, `+13.0%`) | `+6B` (`56 -> 62`, `+10.7%`) |
|
|
106
108
|
|
|
107
109
|
Notes:
|
|
108
|
-
-
|
|
110
|
+
- Only `headPosAbs` is on the wire as an absolute field; `physicalPos` is reconstructed from `headPosAbs + xrOriginDelta`. Option B/C deltas therefore apply to `headPosAbs` only.
|
|
109
111
|
- Option B/C can reduce average overhead if `cell` is transmitted only when changed, but that requires extra state and flags in the wire format.
|
|
110
112
|
|
|
111
113
|
## Configuration
|
|
@@ -160,7 +162,7 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
160
162
|
|
|
161
163
|
```json
|
|
162
164
|
{
|
|
163
|
-
"
|
|
165
|
+
"variables": {
|
|
164
166
|
"name": "Jack",
|
|
165
167
|
"lang": "EN"
|
|
166
168
|
}
|
|
@@ -179,11 +181,35 @@ The server launches an embedded FastAPI application that exposes REST endpoints
|
|
|
179
181
|
```bash
|
|
180
182
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables" \
|
|
181
183
|
-H "Content-Type: application/json" \
|
|
182
|
-
-d '{"
|
|
184
|
+
-d '{"variables":{"name":"Jack","lang":"EN"}}'
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
The response includes the current mapping status (`clientNo` or `null`) and whether each key was `"applied"` or `"queued"`.
|
|
186
188
|
|
|
189
|
+
- Read endpoints:
|
|
190
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables` — returns all client variables for the device.
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{"clientNo": 7, "variables": {"name": "Jack", "lang": "EN"}}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
If the device has no `clientNo` mapping yet, returns `{"clientNo": null, "variables": {}}`.
|
|
197
|
+
|
|
198
|
+
- `GET /v1/rooms/{roomId}/devices/{deviceId}/client-variables/{name}` — returns a single variable.
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{"clientNo": 7, "value": "Jack"}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Returns `404` if the device is unmapped or the variable is not set.
|
|
205
|
+
|
|
206
|
+
- Example:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables"
|
|
210
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/devices/00000000-0000-0000-0000-000000000000/client-variables/name"
|
|
211
|
+
```
|
|
212
|
+
|
|
187
213
|
### Global variables
|
|
188
214
|
|
|
189
215
|
- Endpoint: `POST /v1/rooms/{roomId}/global-variables`
|
|
@@ -191,7 +217,7 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
191
217
|
|
|
192
218
|
```json
|
|
193
219
|
{
|
|
194
|
-
"
|
|
220
|
+
"variables": {
|
|
195
221
|
"score": "42",
|
|
196
222
|
"stage": "lobby"
|
|
197
223
|
}
|
|
@@ -210,7 +236,33 @@ The response includes the current mapping status (`clientNo` or `null`) and whet
|
|
|
210
236
|
```bash
|
|
211
237
|
curl -sS -X POST "http://127.0.0.1:8800/v1/rooms/default_room/global-variables" \
|
|
212
238
|
-H "Content-Type: application/json" \
|
|
213
|
-
-d '{"
|
|
239
|
+
-d '{"variables":{"score":"42","stage":"lobby"}}'
|
|
214
240
|
```
|
|
215
241
|
|
|
216
242
|
The response includes the room ID and whether each key was `"applied"`, `"queued"`, or `"failed"`.
|
|
243
|
+
|
|
244
|
+
- Read endpoints:
|
|
245
|
+
- `GET /v1/rooms/{roomId}/global-variables` — returns all global variables for the room.
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{"variables": {"score": "42", "stage": "lobby"}}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- `GET /v1/rooms/{roomId}/global-variables/{name}` — returns a single variable.
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{"value": "42"}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Returns `404` if the variable is not set.
|
|
258
|
+
|
|
259
|
+
- Example:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables"
|
|
263
|
+
curl -sS "http://127.0.0.1:8800/v1/rooms/default_room/global-variables/score"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Read consistency
|
|
267
|
+
|
|
268
|
+
GET endpoints return a snapshot of the REST bridge's in-process cache, which is populated by PUB-SUB broadcasts from the server. The first request to a room lazily creates a bridge and may return an empty snapshot until the initial broadcasts arrive — retry after a short delay if needed.
|
|
@@ -220,6 +220,7 @@ def _build_random_client_pose(
|
|
|
220
220
|
)
|
|
221
221
|
head_rot = _random_unit_quaternion(rng)
|
|
222
222
|
xr_origin_delta_x = rng.uniform(-20.0, 20.0)
|
|
223
|
+
xr_origin_delta_y = rng.uniform(-5.0, 5.0)
|
|
223
224
|
xr_origin_delta_z = rng.uniform(-20.0, 20.0)
|
|
224
225
|
xr_origin_delta_yaw = rng.uniform(-180.0, 180.0)
|
|
225
226
|
|
|
@@ -249,6 +250,7 @@ def _build_random_client_pose(
|
|
|
249
250
|
"poseSeq": rng.randint(0, 65535),
|
|
250
251
|
"flags": 0x3E, # Physical + Head + Right + Left + Virtuals
|
|
251
252
|
"xrOriginDeltaX": xr_origin_delta_x,
|
|
253
|
+
"xrOriginDeltaY": xr_origin_delta_y,
|
|
252
254
|
"xrOriginDeltaZ": xr_origin_delta_z,
|
|
253
255
|
"xrOriginDeltaYaw": xr_origin_delta_yaw,
|
|
254
256
|
"head": _build_transform(head_pos, head_rot),
|
|
@@ -259,11 +261,15 @@ def _build_random_client_pose(
|
|
|
259
261
|
|
|
260
262
|
|
|
261
263
|
def _reconstruct_physical_from_head_and_delta(
|
|
262
|
-
head: dict[str, float],
|
|
264
|
+
head: dict[str, float],
|
|
265
|
+
delta_x: float,
|
|
266
|
+
delta_y: float,
|
|
267
|
+
delta_z: float,
|
|
268
|
+
delta_yaw: float,
|
|
263
269
|
) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]:
|
|
264
270
|
"""Reconstruct yaw-only physical pose from head pose and XROrigin delta."""
|
|
265
271
|
tx = head["posX"] - delta_x
|
|
266
|
-
ty = head["posY"]
|
|
272
|
+
ty = head["posY"] - delta_y
|
|
267
273
|
tz = head["posZ"] - delta_z
|
|
268
274
|
|
|
269
275
|
inv_yaw_rad = math.radians(-delta_yaw)
|
|
@@ -282,12 +288,12 @@ def _reconstruct_physical_from_head_and_delta(
|
|
|
282
288
|
return (px, ty, pz), physical_rot
|
|
283
289
|
|
|
284
290
|
|
|
285
|
-
class
|
|
286
|
-
"""Tests for protocol
|
|
291
|
+
class TestTransformSerializationV4:
|
|
292
|
+
"""Tests for protocol v4 transform compact serialization."""
|
|
287
293
|
|
|
288
|
-
def
|
|
289
|
-
"""Protocol version constant should be
|
|
290
|
-
assert binary_serializer.PROTOCOL_VERSION ==
|
|
294
|
+
def test_protocol_version_is_v4(self) -> None:
|
|
295
|
+
"""Protocol version constant should be at v4."""
|
|
296
|
+
assert binary_serializer.PROTOCOL_VERSION == 4
|
|
291
297
|
|
|
292
298
|
def test_client_roundtrip_without_flags_infers_valid_bits(self) -> None:
|
|
293
299
|
"""Serializer should infer valid bits when flags are omitted."""
|
|
@@ -295,6 +301,7 @@ class TestTransformSerializationV3:
|
|
|
295
301
|
"deviceId": "infer-flags",
|
|
296
302
|
"poseSeq": 77,
|
|
297
303
|
"xrOriginDeltaX": 1.25,
|
|
304
|
+
"xrOriginDeltaY": 0.4,
|
|
298
305
|
"xrOriginDeltaZ": -2.5,
|
|
299
306
|
"xrOriginDeltaYaw": 33.3,
|
|
300
307
|
"head": _build_transform(
|
|
@@ -331,6 +338,7 @@ class TestTransformSerializationV3:
|
|
|
331
338
|
assert abs(decoded["head"]["posY"] - 1.6) <= 0.01
|
|
332
339
|
assert abs(decoded["head"]["posZ"] + 2.3) <= 0.01
|
|
333
340
|
assert abs(decoded["xrOriginDeltaX"] - 1.25) <= 0.01
|
|
341
|
+
assert abs(decoded["xrOriginDeltaY"] - 0.4) <= 0.01
|
|
334
342
|
assert abs(decoded["xrOriginDeltaZ"] + 2.5) <= 0.01
|
|
335
343
|
assert abs(decoded["xrOriginDeltaYaw"] - 33.3) <= 0.1
|
|
336
344
|
assert len(decoded["virtuals"]) == 1
|
|
@@ -349,6 +357,7 @@ class TestTransformSerializationV3:
|
|
|
349
357
|
| binary_serializer.POSE_FLAG_VIRTUALS_VALID
|
|
350
358
|
),
|
|
351
359
|
"xrOriginDeltaX": 9.0,
|
|
360
|
+
"xrOriginDeltaY": 9.0,
|
|
352
361
|
"xrOriginDeltaZ": 9.0,
|
|
353
362
|
"xrOriginDeltaYaw": 9.0,
|
|
354
363
|
"head": _build_transform(
|
|
@@ -409,7 +418,7 @@ class TestTransformSerializationV3:
|
|
|
409
418
|
|
|
410
419
|
assert msg_type == binary_serializer.MSG_CLIENT_POSE
|
|
411
420
|
assert decoded is not None
|
|
412
|
-
assert decoded["protocolVersion"] ==
|
|
421
|
+
assert decoded["protocolVersion"] == 4
|
|
413
422
|
assert len(raw) > 0
|
|
414
423
|
|
|
415
424
|
o_head = original["head"]
|
|
@@ -466,15 +475,18 @@ class TestTransformSerializationV3:
|
|
|
466
475
|
assert left_err <= 1.0
|
|
467
476
|
|
|
468
477
|
o_dx = original["xrOriginDeltaX"]
|
|
478
|
+
o_dy = original["xrOriginDeltaY"]
|
|
469
479
|
o_dz = original["xrOriginDeltaZ"]
|
|
470
480
|
o_dyaw = original["xrOriginDeltaYaw"]
|
|
471
481
|
assert abs(o_dx - decoded["xrOriginDeltaX"]) <= 0.01
|
|
482
|
+
assert abs(o_dy - decoded["xrOriginDeltaY"]) <= 0.01
|
|
472
483
|
assert abs(o_dz - decoded["xrOriginDeltaZ"]) <= 0.01
|
|
473
484
|
assert abs(o_dyaw - decoded["xrOriginDeltaYaw"]) <= 0.1
|
|
474
485
|
|
|
475
486
|
expected_pos, expected_rot = _reconstruct_physical_from_head_and_delta(
|
|
476
487
|
decoded["head"],
|
|
477
488
|
decoded["xrOriginDeltaX"],
|
|
489
|
+
decoded["xrOriginDeltaY"],
|
|
478
490
|
decoded["xrOriginDeltaZ"],
|
|
479
491
|
decoded["xrOriginDeltaYaw"],
|
|
480
492
|
)
|
|
@@ -518,6 +530,7 @@ class TestTransformSerializationV3:
|
|
|
518
530
|
"poseSeq": 10,
|
|
519
531
|
"flags": 0x3E,
|
|
520
532
|
"xrOriginDeltaX": 9999.0,
|
|
533
|
+
"xrOriginDeltaY": 9999.0,
|
|
521
534
|
"xrOriginDeltaZ": -9999.0,
|
|
522
535
|
"xrOriginDeltaYaw": 9999.0,
|
|
523
536
|
"head": _build_transform(
|
|
@@ -551,6 +564,7 @@ class TestTransformSerializationV3:
|
|
|
551
564
|
assert min_abs <= decoded["head"]["posY"] <= max_abs
|
|
552
565
|
assert min_abs <= decoded["head"]["posZ"] <= max_abs
|
|
553
566
|
assert min_loco <= decoded["xrOriginDeltaX"] <= max_loco
|
|
567
|
+
assert min_loco <= decoded["xrOriginDeltaY"] <= max_loco
|
|
554
568
|
assert min_loco <= decoded["xrOriginDeltaZ"] <= max_loco
|
|
555
569
|
assert (
|
|
556
570
|
binary_serializer.INT16_MIN * binary_serializer.PHYSICAL_YAW_SCALE
|
|
@@ -574,6 +588,7 @@ class TestTransformSerializationV3:
|
|
|
574
588
|
"poseSeq": 33,
|
|
575
589
|
"flags": 0x3E,
|
|
576
590
|
"xrOriginDeltaX": 250.12,
|
|
591
|
+
"xrOriginDeltaY": -42.5,
|
|
577
592
|
"xrOriginDeltaZ": -125.34,
|
|
578
593
|
"xrOriginDeltaYaw": 179.9,
|
|
579
594
|
"head": _build_transform(
|
|
@@ -595,16 +610,18 @@ class TestTransformSerializationV3:
|
|
|
595
610
|
assert abs(decoded["head"]["posY"] + 5000.9) <= 0.01
|
|
596
611
|
assert abs(decoded["head"]["posZ"] - 4321.01) <= 0.01
|
|
597
612
|
assert abs(decoded["xrOriginDeltaX"] - 250.12) <= 0.01
|
|
613
|
+
assert abs(decoded["xrOriginDeltaY"] + 42.5) <= 0.01
|
|
598
614
|
assert abs(decoded["xrOriginDeltaZ"] + 125.34) <= 0.01
|
|
599
615
|
assert abs(decoded["xrOriginDeltaYaw"] - 179.9) <= 0.1
|
|
600
616
|
|
|
601
617
|
def test_client_body_size_with_full_pose_no_virtuals(self) -> None:
|
|
602
|
-
"""Full pose body (no virtuals) should match current protocol
|
|
618
|
+
"""Full pose body (no virtuals) should match current protocol v4 byte size."""
|
|
603
619
|
payload = {
|
|
604
620
|
"deviceId": "size-check",
|
|
605
621
|
"poseSeq": 1,
|
|
606
622
|
"flags": 0x1E, # Physical + Head + Right + Left
|
|
607
623
|
"xrOriginDeltaX": 1.0,
|
|
624
|
+
"xrOriginDeltaY": 0.5,
|
|
608
625
|
"xrOriginDeltaZ": 2.0,
|
|
609
626
|
"xrOriginDeltaYaw": 10.0,
|
|
610
627
|
"head": _build_transform(
|
|
@@ -621,7 +638,8 @@ class TestTransformSerializationV3:
|
|
|
621
638
|
_, _, raw = binary_serializer.deserialize(
|
|
622
639
|
binary_serializer.serialize_client_transform(payload)
|
|
623
640
|
)
|
|
624
|
-
|
|
641
|
+
# v4 added a Y component to xrOriginDelta (+2 bytes vs. v3's 44).
|
|
642
|
+
assert len(raw) == 46
|
|
625
643
|
|
|
626
644
|
def test_physical_requires_delta_encoding_flag(self) -> None:
|
|
627
645
|
"""PhysicalValid frames must carry the XROrigin-delta encoding bit."""
|
|
@@ -633,6 +651,7 @@ class TestTransformSerializationV3:
|
|
|
633
651
|
| binary_serializer.POSE_FLAG_HEAD_VALID
|
|
634
652
|
),
|
|
635
653
|
"xrOriginDeltaX": 1.0,
|
|
654
|
+
"xrOriginDeltaY": 0.3,
|
|
636
655
|
"xrOriginDeltaZ": -2.0,
|
|
637
656
|
"xrOriginDeltaYaw": 30.0,
|
|
638
657
|
"head": _build_transform(
|
|
@@ -663,7 +682,7 @@ class TestTransformSerializationV3:
|
|
|
663
682
|
c2["poseTime"] = 223.456
|
|
664
683
|
|
|
665
684
|
room_payload = {
|
|
666
|
-
"roomId": "room-
|
|
685
|
+
"roomId": "room-v4",
|
|
667
686
|
"broadcastTime": 999.123,
|
|
668
687
|
"clients": [c1, c2],
|
|
669
688
|
}
|
|
@@ -672,8 +691,8 @@ class TestTransformSerializationV3:
|
|
|
672
691
|
|
|
673
692
|
assert msg_type == binary_serializer.MSG_ROOM_POSE
|
|
674
693
|
assert decoded is not None
|
|
675
|
-
assert decoded["protocolVersion"] ==
|
|
676
|
-
assert decoded["roomId"] == "room-
|
|
694
|
+
assert decoded["protocolVersion"] == 4
|
|
695
|
+
assert decoded["roomId"] == "room-v4"
|
|
677
696
|
assert len(decoded["clients"]) == 2
|
|
678
697
|
|
|
679
698
|
for src, dst in zip(room_payload["clients"], decoded["clients"], strict=True):
|
|
@@ -686,6 +705,28 @@ class TestTransformSerializationV3:
|
|
|
686
705
|
assert abs(src_head["posY"] - dst_head["posY"]) <= 0.01
|
|
687
706
|
assert abs(src_head["posZ"] - dst_head["posZ"]) <= 0.01
|
|
688
707
|
|
|
708
|
+
def test_foot_invariant_head_minus_physical_equals_delta_y(self) -> None:
|
|
709
|
+
"""head.y - reconstructed physical.y must equal xrOriginDeltaY.
|
|
710
|
+
|
|
711
|
+
Follows by construction from `_deserialize_client_body`
|
|
712
|
+
(`translated_y = head_pos[1] - xr_origin_delta_y`) and the yaw-only
|
|
713
|
+
rotation that preserves Y. Pinning this protocol-shape invariant means
|
|
714
|
+
any future change that decouples the two (e.g. dropping delta_y from
|
|
715
|
+
the reconstruction, or adding extra Y transforms) breaks loudly here
|
|
716
|
+
rather than silently on the wire.
|
|
717
|
+
"""
|
|
718
|
+
rng = random.Random(20260425)
|
|
719
|
+
for _ in range(200):
|
|
720
|
+
payload = _build_random_client_pose(rng, virtual_count=0)
|
|
721
|
+
_, decoded, _ = binary_serializer.deserialize(
|
|
722
|
+
binary_serializer.serialize_client_transform(payload)
|
|
723
|
+
)
|
|
724
|
+
assert decoded is not None
|
|
725
|
+
d_head = decoded["head"]
|
|
726
|
+
d_phys = decoded["physical"]
|
|
727
|
+
d_dy = decoded["xrOriginDeltaY"]
|
|
728
|
+
assert abs((d_head["posY"] - d_phys["posY"]) - d_dy) <= 1e-6
|
|
729
|
+
|
|
689
730
|
def test_denormalized_quaternion_roundtrip(self) -> None:
|
|
690
731
|
"""Non-unit quaternions should be normalized and survive roundtrip."""
|
|
691
732
|
# magnitude = 2.0
|
|
@@ -177,14 +177,14 @@ class TestGlobalVariablesEndpoint:
|
|
|
177
177
|
def test_post_returns_200(self, client: TestClient) -> None:
|
|
178
178
|
resp = client.post(
|
|
179
179
|
"/v1/rooms/room1/global-variables",
|
|
180
|
-
json={"
|
|
180
|
+
json={"variables": {"score": "42"}},
|
|
181
181
|
)
|
|
182
182
|
assert resp.status_code == 200
|
|
183
183
|
|
|
184
184
|
def test_post_response_structure(self, client: TestClient) -> None:
|
|
185
185
|
resp = client.post(
|
|
186
186
|
"/v1/rooms/room1/global-variables",
|
|
187
|
-
json={"
|
|
187
|
+
json={"variables": {"score": "42"}},
|
|
188
188
|
)
|
|
189
189
|
body = resp.json()
|
|
190
190
|
assert body["roomId"] == "room1"
|
|
@@ -194,7 +194,7 @@ class TestGlobalVariablesEndpoint:
|
|
|
194
194
|
def test_post_empty_vars_returns_400(self, client: TestClient) -> None:
|
|
195
195
|
resp = client.post(
|
|
196
196
|
"/v1/rooms/room1/global-variables",
|
|
197
|
-
json={"
|
|
197
|
+
json={"variables": {}},
|
|
198
198
|
)
|
|
199
199
|
assert resp.status_code == 400
|
|
200
200
|
|
|
@@ -206,14 +206,14 @@ class TestGlobalVariablesEndpoint:
|
|
|
206
206
|
long_name = "x" * 65
|
|
207
207
|
resp = client.post(
|
|
208
208
|
"/v1/rooms/room1/global-variables",
|
|
209
|
-
json={"
|
|
209
|
+
json={"variables": {long_name: "v"}},
|
|
210
210
|
)
|
|
211
211
|
assert resp.status_code == 422
|
|
212
212
|
|
|
213
213
|
def test_post_var_value_too_long_returns_422(self, client: TestClient) -> None:
|
|
214
214
|
resp = client.post(
|
|
215
215
|
"/v1/rooms/room1/global-variables",
|
|
216
|
-
json={"
|
|
216
|
+
json={"variables": {"k": "x" * 1025}},
|
|
217
217
|
)
|
|
218
218
|
assert resp.status_code == 422
|
|
219
219
|
|
|
@@ -240,7 +240,7 @@ class TestGlobalVariablesEndpoint:
|
|
|
240
240
|
tc = TestClient(app)
|
|
241
241
|
resp = tc.post(
|
|
242
242
|
"/v1/rooms/room_full/global-variables",
|
|
243
|
-
json={"
|
|
243
|
+
json={"variables": {"overflow": "x"}},
|
|
244
244
|
)
|
|
245
245
|
assert resp.status_code == 409
|
|
246
246
|
finally:
|
|
@@ -263,10 +263,156 @@ class TestGlobalVariablesEndpoint:
|
|
|
263
263
|
tc = TestClient(app)
|
|
264
264
|
resp = tc.post(
|
|
265
265
|
"/v1/rooms/room1/global-variables",
|
|
266
|
-
json={"
|
|
266
|
+
json={"variables": {"k": "v"}},
|
|
267
267
|
)
|
|
268
268
|
assert resp.status_code == 200
|
|
269
269
|
# Store should remain empty since the var was applied
|
|
270
270
|
assert rest_bridge.global_store.get("room1") == {}
|
|
271
271
|
finally:
|
|
272
272
|
rest_bridge.global_store = original_store
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# RoomBridge read method tests
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class TestRoomBridgeReadMethods:
|
|
281
|
+
def test_get_global_variables_delegates_to_manager(self) -> None:
|
|
282
|
+
bridge = _make_bridge(client_no=1)
|
|
283
|
+
bridge._manager.get_all_global_variables.return_value = {"a": "1", "b": "2"}
|
|
284
|
+
result = bridge.get_global_variables()
|
|
285
|
+
assert result == {"a": "1", "b": "2"}
|
|
286
|
+
bridge._manager.get_all_global_variables.assert_called_once_with()
|
|
287
|
+
|
|
288
|
+
def test_get_global_variables_swallows_exceptions(self) -> None:
|
|
289
|
+
bridge = _make_bridge(client_no=1)
|
|
290
|
+
bridge._manager.get_all_global_variables.side_effect = RuntimeError("boom")
|
|
291
|
+
assert bridge.get_global_variables() == {}
|
|
292
|
+
|
|
293
|
+
def test_get_client_variables_unmapped_returns_none_and_empty(self) -> None:
|
|
294
|
+
bridge = _make_bridge(client_no=1)
|
|
295
|
+
bridge._manager.get_client_no.return_value = None
|
|
296
|
+
client_no, variables = bridge.get_client_variables("device-x")
|
|
297
|
+
assert client_no is None
|
|
298
|
+
assert variables == {}
|
|
299
|
+
bridge._manager.get_all_client_variables.assert_not_called()
|
|
300
|
+
|
|
301
|
+
def test_get_client_variables_mapped_returns_snapshot(self) -> None:
|
|
302
|
+
bridge = _make_bridge(client_no=1)
|
|
303
|
+
bridge._manager.get_client_no.return_value = 7
|
|
304
|
+
bridge._manager.get_all_client_variables.return_value = {"name": "alice"}
|
|
305
|
+
client_no, variables = bridge.get_client_variables("device-x")
|
|
306
|
+
assert client_no == 7
|
|
307
|
+
assert variables == {"name": "alice"}
|
|
308
|
+
bridge._manager.get_all_client_variables.assert_called_once_with(7)
|
|
309
|
+
|
|
310
|
+
def test_get_client_variables_swallows_exceptions(self) -> None:
|
|
311
|
+
bridge = _make_bridge(client_no=1)
|
|
312
|
+
bridge._manager.get_client_no.return_value = 7
|
|
313
|
+
bridge._manager.get_all_client_variables.side_effect = RuntimeError("boom")
|
|
314
|
+
client_no, variables = bridge.get_client_variables("device-x")
|
|
315
|
+
assert client_no == 7
|
|
316
|
+
assert variables == {}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
# GET endpoint tests via TestClient
|
|
321
|
+
# ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _make_get_client(mock_bridge: MagicMock) -> TestClient:
|
|
325
|
+
"""Build a TestClient whose BridgeManager always returns the given bridge."""
|
|
326
|
+
with patch("styly_netsync.rest_bridge.BridgeManager") as MockBM:
|
|
327
|
+
MockBM.return_value.get.return_value = mock_bridge
|
|
328
|
+
app = create_app("localhost", 5555, 5556)
|
|
329
|
+
return TestClient(app)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TestGlobalVariablesGetEndpoint:
|
|
333
|
+
def test_get_all_returns_snapshot(self) -> None:
|
|
334
|
+
mock_bridge = MagicMock()
|
|
335
|
+
mock_bridge.get_global_variables.return_value = {"score": "10", "level": "3"}
|
|
336
|
+
tc = _make_get_client(mock_bridge)
|
|
337
|
+
resp = tc.get("/v1/rooms/room1/global-variables")
|
|
338
|
+
assert resp.status_code == 200
|
|
339
|
+
assert resp.json() == {"variables": {"score": "10", "level": "3"}}
|
|
340
|
+
|
|
341
|
+
def test_get_all_returns_empty_when_cache_empty(self) -> None:
|
|
342
|
+
mock_bridge = MagicMock()
|
|
343
|
+
mock_bridge.get_global_variables.return_value = {}
|
|
344
|
+
tc = _make_get_client(mock_bridge)
|
|
345
|
+
resp = tc.get("/v1/rooms/room1/global-variables")
|
|
346
|
+
assert resp.status_code == 200
|
|
347
|
+
assert resp.json() == {"variables": {}}
|
|
348
|
+
|
|
349
|
+
def test_get_single_returns_value(self) -> None:
|
|
350
|
+
mock_bridge = MagicMock()
|
|
351
|
+
mock_bridge.get_global_variables.return_value = {"score": "10"}
|
|
352
|
+
tc = _make_get_client(mock_bridge)
|
|
353
|
+
resp = tc.get("/v1/rooms/room1/global-variables/score")
|
|
354
|
+
assert resp.status_code == 200
|
|
355
|
+
assert resp.json() == {"value": "10"}
|
|
356
|
+
|
|
357
|
+
def test_get_single_missing_returns_404(self) -> None:
|
|
358
|
+
mock_bridge = MagicMock()
|
|
359
|
+
mock_bridge.get_global_variables.return_value = {}
|
|
360
|
+
tc = _make_get_client(mock_bridge)
|
|
361
|
+
resp = tc.get("/v1/rooms/room1/global-variables/missing")
|
|
362
|
+
assert resp.status_code == 404
|
|
363
|
+
|
|
364
|
+
def test_get_single_name_too_long_returns_422(self) -> None:
|
|
365
|
+
mock_bridge = MagicMock()
|
|
366
|
+
mock_bridge.get_global_variables.return_value = {}
|
|
367
|
+
tc = _make_get_client(mock_bridge)
|
|
368
|
+
long_name = "x" * 65
|
|
369
|
+
resp = tc.get(f"/v1/rooms/room1/global-variables/{long_name}")
|
|
370
|
+
assert resp.status_code == 422
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class TestClientVariablesGetEndpoint:
|
|
374
|
+
def test_get_all_unknown_device_returns_null_mapping_and_empty(self) -> None:
|
|
375
|
+
mock_bridge = MagicMock()
|
|
376
|
+
mock_bridge.get_client_variables.return_value = (None, {})
|
|
377
|
+
tc = _make_get_client(mock_bridge)
|
|
378
|
+
resp = tc.get("/v1/rooms/room1/devices/device-x/client-variables")
|
|
379
|
+
assert resp.status_code == 200
|
|
380
|
+
assert resp.json() == {"clientNo": None, "variables": {}}
|
|
381
|
+
|
|
382
|
+
def test_get_all_mapped_device_returns_snapshot(self) -> None:
|
|
383
|
+
mock_bridge = MagicMock()
|
|
384
|
+
mock_bridge.get_client_variables.return_value = (7, {"name": "alice"})
|
|
385
|
+
tc = _make_get_client(mock_bridge)
|
|
386
|
+
resp = tc.get("/v1/rooms/room1/devices/device-x/client-variables")
|
|
387
|
+
assert resp.status_code == 200
|
|
388
|
+
assert resp.json() == {"clientNo": 7, "variables": {"name": "alice"}}
|
|
389
|
+
|
|
390
|
+
def test_get_single_returns_value(self) -> None:
|
|
391
|
+
mock_bridge = MagicMock()
|
|
392
|
+
mock_bridge.get_client_variables.return_value = (7, {"name": "alice"})
|
|
393
|
+
tc = _make_get_client(mock_bridge)
|
|
394
|
+
resp = tc.get("/v1/rooms/room1/devices/device-x/client-variables/name")
|
|
395
|
+
assert resp.status_code == 200
|
|
396
|
+
assert resp.json() == {"clientNo": 7, "value": "alice"}
|
|
397
|
+
|
|
398
|
+
def test_get_single_unknown_device_returns_404(self) -> None:
|
|
399
|
+
mock_bridge = MagicMock()
|
|
400
|
+
mock_bridge.get_client_variables.return_value = (None, {})
|
|
401
|
+
tc = _make_get_client(mock_bridge)
|
|
402
|
+
resp = tc.get("/v1/rooms/room1/devices/device-x/client-variables/name")
|
|
403
|
+
assert resp.status_code == 404
|
|
404
|
+
|
|
405
|
+
def test_get_single_missing_variable_returns_404(self) -> None:
|
|
406
|
+
mock_bridge = MagicMock()
|
|
407
|
+
mock_bridge.get_client_variables.return_value = (7, {})
|
|
408
|
+
tc = _make_get_client(mock_bridge)
|
|
409
|
+
resp = tc.get("/v1/rooms/room1/devices/device-x/client-variables/name")
|
|
410
|
+
assert resp.status_code == 404
|
|
411
|
+
|
|
412
|
+
def test_get_single_name_too_long_returns_422(self) -> None:
|
|
413
|
+
mock_bridge = MagicMock()
|
|
414
|
+
mock_bridge.get_client_variables.return_value = (7, {})
|
|
415
|
+
tc = _make_get_client(mock_bridge)
|
|
416
|
+
long_name = "x" * 65
|
|
417
|
+
resp = tc.get(f"/v1/rooms/room1/devices/device-x/client-variables/{long_name}")
|
|
418
|
+
assert resp.status_code == 422
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/client_simulator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/logging_utils.py
RENAMED
|
File without changes
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/src/styly_netsync/network_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_port_error_message.py
RENAMED
|
File without changes
|
|
File without changes
|
{styly_netsync_server-0.10.4 → styly_netsync_server-0.11.0}/tests/test_reconnect_identity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|