styly-netsync-server 0.12.0__tar.gz → 0.13.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.12.0 → styly_netsync_server-0.13.0}/PKG-INFO +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/pyproject.toml +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/binary_serializer.py +40 -10
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client.py +89 -19
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client_simulator.py +2 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/rest_bridge.py +74 -64
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/server.py +237 -31
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/PKG-INFO +1 -1
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/SOURCES.txt +1 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_binary_serializer.py +14 -0
- styly_netsync_server-0.13.0/tests/test_client_variable_device_store.py +468 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/LICENSE +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/README.md +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/setup.cfg +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/__init__.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/__main__.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/adapters.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/cli.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/config.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/default.toml +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/events.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/logging_utils.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/network_utils.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/nv_sync.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/types.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/dependency_links.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/entry_points.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/requires.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync_server.egg-info/top_level.txt +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_all_run_methods.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_config.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_discovery_probe.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_logging_cli.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_multi_nic.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_nv_protocol.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_object_sync.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_port_error_message.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_python_client.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_reconnect_identity.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_rest_bridge.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_room_expiry.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/tests/test_stealth_heartbeat.py +0 -0
- {styly_netsync_server-0.12.0 → styly_netsync_server-0.13.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.13.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
|
|
@@ -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.13.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.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/binary_serializer.py
RENAMED
|
@@ -24,6 +24,7 @@ MSG_ROOM_OBJECTS = 14 # Server → Clients (PUB): room object states
|
|
|
24
24
|
MSG_OBJECT_OWNERSHIP_REQUEST = 15 # Client → Server: RequestOwnership/ReleaseOwnership
|
|
25
25
|
MSG_OBJECT_OWNERSHIP_CHANGED = 16 # Server → Clients (ROUTER): ownership changed
|
|
26
26
|
MSG_OBJECT_OWNERSHIP_REJECTED = 17 # Server → Client (ROUTER): request rejected
|
|
27
|
+
MSG_CLIENT_VAR_CLEAR = 18 # Clear all client variables for the sender
|
|
27
28
|
|
|
28
29
|
# Transform data type identifiers (deprecated - kept for reference)
|
|
29
30
|
|
|
@@ -765,6 +766,26 @@ def serialize_client_var_set(data: dict[str, Any]) -> bytes:
|
|
|
765
766
|
return bytes(buffer)
|
|
766
767
|
|
|
767
768
|
|
|
769
|
+
def serialize_client_var_clear(data: dict[str, Any]) -> bytes:
|
|
770
|
+
"""Serialize client variable clear message.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
data: Dictionary with senderClientNo and timestamp.
|
|
774
|
+
"""
|
|
775
|
+
buffer = bytearray()
|
|
776
|
+
|
|
777
|
+
# Message type
|
|
778
|
+
buffer.append(MSG_CLIENT_VAR_CLEAR)
|
|
779
|
+
|
|
780
|
+
# Sender client number (2 bytes)
|
|
781
|
+
buffer.extend(struct.pack("<H", data.get("senderClientNo", 0)))
|
|
782
|
+
|
|
783
|
+
# Timestamp (8 bytes double)
|
|
784
|
+
buffer.extend(struct.pack("<d", data.get("timestamp", 0.0)))
|
|
785
|
+
|
|
786
|
+
return bytes(buffer)
|
|
787
|
+
|
|
788
|
+
|
|
768
789
|
def serialize_client_var_sync(data: dict[str, Any]) -> bytes:
|
|
769
790
|
"""Serialize client variable sync message
|
|
770
791
|
|
|
@@ -811,10 +832,7 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
811
832
|
offset += 1
|
|
812
833
|
|
|
813
834
|
# Validate message type is within valid range
|
|
814
|
-
if
|
|
815
|
-
message_type < MSG_CLIENT_TRANSFORM
|
|
816
|
-
or message_type > MSG_OBJECT_OWNERSHIP_REJECTED
|
|
817
|
-
):
|
|
835
|
+
if message_type < MSG_CLIENT_TRANSFORM or message_type > MSG_CLIENT_VAR_CLEAR:
|
|
818
836
|
# Return invalid message type with None data instead of raising exception
|
|
819
837
|
return message_type, None, b""
|
|
820
838
|
|
|
@@ -843,6 +861,8 @@ def deserialize(data: bytes) -> tuple[int, dict[str, Any] | None, bytes]:
|
|
|
843
861
|
return message_type, _deserialize_client_var_set(data, offset), b""
|
|
844
862
|
elif message_type == MSG_CLIENT_VAR_SYNC:
|
|
845
863
|
return message_type, _deserialize_client_var_sync(data, offset), b""
|
|
864
|
+
elif message_type == MSG_CLIENT_VAR_CLEAR:
|
|
865
|
+
return message_type, _deserialize_client_var_clear(data, offset), b""
|
|
846
866
|
elif message_type == MSG_OBJECT_POSE:
|
|
847
867
|
return message_type, _deserialize_object_pose(data, offset), b""
|
|
848
868
|
elif message_type == MSG_ROOM_OBJECTS:
|
|
@@ -897,15 +917,11 @@ def _deserialize_client_body(data: bytes, offset: int) -> tuple[dict[str, Any],
|
|
|
897
917
|
moving_floor_local = bool(flags & POSE_FLAG_MOVING_FLOOR_LOCAL)
|
|
898
918
|
|
|
899
919
|
physical = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, True)
|
|
900
|
-
head = _create_transform_dict(
|
|
901
|
-
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
902
|
-
)
|
|
920
|
+
head = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
903
921
|
right = _create_transform_dict(
|
|
904
922
|
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
905
923
|
)
|
|
906
|
-
left = _create_transform_dict(
|
|
907
|
-
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local
|
|
908
|
-
)
|
|
924
|
+
left = _create_transform_dict(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, moving_floor_local)
|
|
909
925
|
|
|
910
926
|
head_pos = (0.0, 0.0, 0.0)
|
|
911
927
|
head_rot = (0.0, 0.0, 0.0, 1.0)
|
|
@@ -1285,6 +1301,20 @@ def _deserialize_client_var_set(data: bytes, offset: int) -> dict[str, Any]:
|
|
|
1285
1301
|
return result
|
|
1286
1302
|
|
|
1287
1303
|
|
|
1304
|
+
def _deserialize_client_var_clear(data: bytes, offset: int) -> dict[str, Any]:
|
|
1305
|
+
"""Deserialize client variable clear message."""
|
|
1306
|
+
result: dict[str, Any] = {}
|
|
1307
|
+
|
|
1308
|
+
# Sender client number (2 bytes)
|
|
1309
|
+
result["senderClientNo"] = struct.unpack("<H", data[offset : offset + 2])[0]
|
|
1310
|
+
offset += 2
|
|
1311
|
+
|
|
1312
|
+
# Timestamp (8 bytes double)
|
|
1313
|
+
result["timestamp"] = struct.unpack("<d", data[offset : offset + 8])[0]
|
|
1314
|
+
|
|
1315
|
+
return result
|
|
1316
|
+
|
|
1317
|
+
|
|
1288
1318
|
def _deserialize_client_var_sync(data: bytes, offset: int) -> dict[str, Any]:
|
|
1289
1319
|
"""Deserialize client variable sync message"""
|
|
1290
1320
|
result: dict[str, Any] = {"clientVariables": {}}
|
|
@@ -1027,7 +1027,11 @@ class net_sync_manager:
|
|
|
1027
1027
|
logger.error(f"Error processing global var sync: {e}")
|
|
1028
1028
|
|
|
1029
1029
|
def _process_client_var_sync(self, msg_data: dict[str, Any]) -> None:
|
|
1030
|
-
"""Process client variable sync.
|
|
1030
|
+
"""Process client variable sync.
|
|
1031
|
+
|
|
1032
|
+
Each included client number is a full authoritative snapshot. Missing
|
|
1033
|
+
keys are removed from the local cache for that client.
|
|
1034
|
+
"""
|
|
1031
1035
|
try:
|
|
1032
1036
|
client_variables = msg_data.get("clientVariables", {})
|
|
1033
1037
|
|
|
@@ -1037,34 +1041,50 @@ class net_sync_manager:
|
|
|
1037
1041
|
except ValueError:
|
|
1038
1042
|
continue
|
|
1039
1043
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1044
|
+
old_vars = self._client_variables.get(client_no, {})
|
|
1045
|
+
new_vars: dict[str, str] = {}
|
|
1042
1046
|
|
|
1043
1047
|
for var in variables:
|
|
1044
1048
|
name = var.get("name", "")
|
|
1045
1049
|
value = var.get("value", "")
|
|
1046
|
-
|
|
1050
|
+
if name:
|
|
1051
|
+
new_vars[name] = value
|
|
1047
1052
|
|
|
1048
|
-
|
|
1053
|
+
changed_events: list[tuple[int, str, str | None, str | None]] = []
|
|
1054
|
+
for name in set(old_vars) - set(new_vars):
|
|
1055
|
+
changed_events.append((client_no, name, old_vars.get(name), None))
|
|
1049
1056
|
|
|
1057
|
+
for name, value in new_vars.items():
|
|
1058
|
+
old_value = old_vars.get(name)
|
|
1050
1059
|
if old_value != value:
|
|
1051
|
-
|
|
1052
|
-
self._stats["nv_updates"] += 1
|
|
1060
|
+
changed_events.append((client_no, name, old_value, value))
|
|
1053
1061
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1062
|
+
self._client_variables[client_no] = new_vars
|
|
1063
|
+
|
|
1064
|
+
for event_client_no, name, old_value, new_value in changed_events:
|
|
1065
|
+
with self._lock:
|
|
1066
|
+
self._stats["nv_updates"] += 1
|
|
1067
|
+
|
|
1068
|
+
event: tuple[str, int, str, str | None, str | None] = (
|
|
1069
|
+
"client",
|
|
1070
|
+
event_client_no,
|
|
1071
|
+
name,
|
|
1072
|
+
old_value,
|
|
1073
|
+
new_value,
|
|
1074
|
+
)
|
|
1075
|
+
if self._auto_dispatch:
|
|
1076
|
+
self.on_client_variable_changed.invoke(
|
|
1077
|
+
event_client_no, name, old_value, new_value
|
|
1078
|
+
)
|
|
1079
|
+
else:
|
|
1080
|
+
try:
|
|
1081
|
+
self._nv_queue.put_nowait(event)
|
|
1082
|
+
except Full:
|
|
1060
1083
|
try:
|
|
1084
|
+
self._nv_queue.get_nowait()
|
|
1061
1085
|
self._nv_queue.put_nowait(event)
|
|
1062
|
-
except
|
|
1063
|
-
|
|
1064
|
-
self._nv_queue.get_nowait()
|
|
1065
|
-
self._nv_queue.put_nowait(event)
|
|
1066
|
-
except Empty:
|
|
1067
|
-
pass
|
|
1086
|
+
except Empty:
|
|
1087
|
+
pass
|
|
1068
1088
|
|
|
1069
1089
|
except Exception as e:
|
|
1070
1090
|
logger.error(f"Error processing client var sync: {e}")
|
|
@@ -1235,6 +1255,28 @@ class net_sync_manager:
|
|
|
1235
1255
|
logger.error(f"Error queueing client variable: {e}")
|
|
1236
1256
|
return False
|
|
1237
1257
|
|
|
1258
|
+
def clear_my_client_variables(self) -> bool:
|
|
1259
|
+
"""Queue a request to clear this client's variables on the server."""
|
|
1260
|
+
if not self._running or not self._dealer_socket or self._client_no is None:
|
|
1261
|
+
return False
|
|
1262
|
+
|
|
1263
|
+
try:
|
|
1264
|
+
clear_data = {
|
|
1265
|
+
"senderClientNo": self._client_no,
|
|
1266
|
+
"timestamp": time.time(),
|
|
1267
|
+
}
|
|
1268
|
+
message = binary_serializer.serialize_client_var_clear(clear_data)
|
|
1269
|
+
sent = self._enqueue_control(
|
|
1270
|
+
self._room, message, msg_type="client_variable_clear"
|
|
1271
|
+
)
|
|
1272
|
+
if sent:
|
|
1273
|
+
self._clear_local_client_variables(self._client_no)
|
|
1274
|
+
return sent
|
|
1275
|
+
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
logger.error(f"Error queueing client variable clear: {e}")
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1238
1280
|
def _enqueue_control(
|
|
1239
1281
|
self, room_id: str, payload: bytes, msg_type: str = "control"
|
|
1240
1282
|
) -> bool:
|
|
@@ -1294,6 +1336,34 @@ class net_sync_manager:
|
|
|
1294
1336
|
"""Return a copy of all variables for the given client."""
|
|
1295
1337
|
return self._client_variables.get(client_no, {}).copy()
|
|
1296
1338
|
|
|
1339
|
+
def _clear_local_client_variables(self, client_no: int) -> None:
|
|
1340
|
+
"""Clear local client-variable cache for a client and emit events."""
|
|
1341
|
+
old_vars = self._client_variables.get(client_no, {}).copy()
|
|
1342
|
+
self._client_variables[client_no] = {}
|
|
1343
|
+
|
|
1344
|
+
for name, old_value in old_vars.items():
|
|
1345
|
+
with self._lock:
|
|
1346
|
+
self._stats["nv_updates"] += 1
|
|
1347
|
+
|
|
1348
|
+
event: tuple[str, int, str, str | None, str | None] = (
|
|
1349
|
+
"client",
|
|
1350
|
+
client_no,
|
|
1351
|
+
name,
|
|
1352
|
+
old_value,
|
|
1353
|
+
None,
|
|
1354
|
+
)
|
|
1355
|
+
if self._auto_dispatch:
|
|
1356
|
+
self.on_client_variable_changed.invoke(client_no, name, old_value, None)
|
|
1357
|
+
else:
|
|
1358
|
+
try:
|
|
1359
|
+
self._nv_queue.put_nowait(event)
|
|
1360
|
+
except Full:
|
|
1361
|
+
try:
|
|
1362
|
+
self._nv_queue.get_nowait()
|
|
1363
|
+
self._nv_queue.put_nowait(event)
|
|
1364
|
+
except Empty:
|
|
1365
|
+
pass
|
|
1366
|
+
|
|
1297
1367
|
def is_client_stealth_mode(self, client_no: int) -> bool:
|
|
1298
1368
|
"""Check if the client is in stealth mode."""
|
|
1299
1369
|
return self._client_stealth_flags.get(client_no, False)
|
{styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/client_simulator.py
RENAMED
|
@@ -46,6 +46,7 @@ import zmq
|
|
|
46
46
|
# Import public APIs from styly_netsync module
|
|
47
47
|
from styly_netsync.binary_serializer import (
|
|
48
48
|
MSG_CLIENT_POSE,
|
|
49
|
+
MSG_CLIENT_VAR_CLEAR,
|
|
49
50
|
MSG_CLIENT_VAR_SET,
|
|
50
51
|
MSG_CLIENT_VAR_SYNC,
|
|
51
52
|
MSG_DEVICE_ID_MAPPING,
|
|
@@ -95,6 +96,7 @@ MESSAGE_TYPE_NAMES: dict[int, str] = {
|
|
|
95
96
|
MSG_GLOBAL_VAR_SYNC: "GLOBAL_VAR_SYNC",
|
|
96
97
|
MSG_CLIENT_VAR_SET: "CLIENT_VAR_SET",
|
|
97
98
|
MSG_CLIENT_VAR_SYNC: "CLIENT_VAR_SYNC",
|
|
99
|
+
MSG_CLIENT_VAR_CLEAR: "CLIENT_VAR_CLEAR",
|
|
98
100
|
MSG_OBJECT_POSE: "OBJECT_POSE",
|
|
99
101
|
MSG_ROOM_OBJECTS: "ROOM_OBJECTS",
|
|
100
102
|
MSG_OBJECT_OWNERSHIP_REQUEST: "OBJECT_OWNERSHIP_REQUEST",
|
{styly_netsync_server-0.12.0 → styly_netsync_server-0.13.0}/src/styly_netsync/rest_bridge.py
RENAMED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import logging
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
|
-
from typing import TYPE_CHECKING, Annotated
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Protocol
|
|
7
7
|
|
|
8
8
|
from fastapi import FastAPI, HTTPException
|
|
9
9
|
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -18,7 +18,6 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
|
|
19
19
|
MAX_NAME = 64
|
|
20
20
|
MAX_VALUE = 1024
|
|
21
|
-
MAX_CLIENT_VARS = 20
|
|
22
21
|
MAX_GLOBAL_VARS = 100
|
|
23
22
|
|
|
24
23
|
# Constrained string types for variable names and values
|
|
@@ -32,45 +31,26 @@ class UpsertBody(BaseModel):
|
|
|
32
31
|
variables: dict[VarName, VarValue] = Field(default_factory=dict)
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
class
|
|
36
|
-
"""
|
|
34
|
+
class ClientVariableServer(Protocol):
|
|
35
|
+
"""Server-side operations used by REST client-variable endpoints."""
|
|
37
36
|
|
|
38
|
-
def
|
|
39
|
-
self
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self, room_id: str, device_id: str, kvs: dict[str, str]
|
|
44
|
-
) -> dict[str, str]:
|
|
45
|
-
"""Merge incoming key-values for a device within a room."""
|
|
46
|
-
with self._lock:
|
|
47
|
-
key = (room_id, device_id)
|
|
48
|
-
current = self._data.get(key, {})
|
|
49
|
-
new_keys = [name for name in kvs if name not in current]
|
|
50
|
-
if len(current) + len(new_keys) > MAX_CLIENT_VARS:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"Too many client variables (> {MAX_CLIENT_VARS}) for device {device_id}"
|
|
53
|
-
)
|
|
54
|
-
current.update(kvs)
|
|
55
|
-
self._data[key] = current
|
|
56
|
-
return dict(current)
|
|
57
|
-
|
|
58
|
-
def get(self, room_id: str, device_id: str) -> dict[str, str]:
|
|
59
|
-
"""Return a copy of stored variables for a device."""
|
|
60
|
-
with self._lock:
|
|
61
|
-
return dict(self._data.get((room_id, device_id), {}))
|
|
62
|
-
|
|
63
|
-
def all_for_room(self, room_id: str) -> dict[str, dict[str, str]]:
|
|
64
|
-
"""Return all stored variables for the given room."""
|
|
65
|
-
with self._lock:
|
|
66
|
-
result: dict[str, dict[str, str]] = {}
|
|
67
|
-
for (stored_room, device_id), variables in self._data.items():
|
|
68
|
-
if stored_room == room_id:
|
|
69
|
-
result[device_id] = dict(variables)
|
|
70
|
-
return result
|
|
37
|
+
def upsert_client_variables_for_device(
|
|
38
|
+
self, room_id: str, device_id: str, variables: dict[str, str]
|
|
39
|
+
) -> tuple[int | None, dict[str, str]]:
|
|
40
|
+
"""Upsert client variables and return mapping plus per-key states."""
|
|
41
|
+
...
|
|
71
42
|
|
|
43
|
+
def get_client_variables_for_device(
|
|
44
|
+
self, room_id: str, device_id: str
|
|
45
|
+
) -> tuple[int | None, dict[str, str]]:
|
|
46
|
+
"""Return the mapped client number and current variables."""
|
|
47
|
+
...
|
|
72
48
|
|
|
73
|
-
|
|
49
|
+
def delete_client_variables_for_device(
|
|
50
|
+
self, room_id: str, device_id: str, name: str | None = None
|
|
51
|
+
) -> tuple[int | None, int]:
|
|
52
|
+
"""Delete client variables and return mapping plus deletion count."""
|
|
53
|
+
...
|
|
74
54
|
|
|
75
55
|
|
|
76
56
|
class GlobalVarStore:
|
|
@@ -157,7 +137,6 @@ class RoomBridge:
|
|
|
157
137
|
next_handshake_at = now + 0.5
|
|
158
138
|
else:
|
|
159
139
|
try:
|
|
160
|
-
self.flush_all_known_mappings()
|
|
161
140
|
self.flush_global_vars()
|
|
162
141
|
except Exception as exc:
|
|
163
142
|
logger.debug("Flush failed: %s", exc)
|
|
@@ -261,14 +240,6 @@ class RoomBridge:
|
|
|
261
240
|
applied.add(name)
|
|
262
241
|
return applied
|
|
263
242
|
|
|
264
|
-
def flush_all_known_mappings(self) -> None:
|
|
265
|
-
"""Flush queued variables for devices whose client numbers are known."""
|
|
266
|
-
pending = store.all_for_room(self.room_id)
|
|
267
|
-
for device_id, kvs in pending.items():
|
|
268
|
-
client_no = self.get_client_no(device_id)
|
|
269
|
-
if client_no:
|
|
270
|
-
self._apply_to_client(client_no, kvs)
|
|
271
|
-
|
|
272
243
|
def flush_global_vars(self) -> None:
|
|
273
244
|
"""Flush queued global variables for this room."""
|
|
274
245
|
pending = global_store.pop(self.room_id)
|
|
@@ -304,7 +275,12 @@ class BridgeManager:
|
|
|
304
275
|
return bridge
|
|
305
276
|
|
|
306
277
|
|
|
307
|
-
def create_app(
|
|
278
|
+
def create_app(
|
|
279
|
+
server_addr: str,
|
|
280
|
+
dealer_port: int,
|
|
281
|
+
sub_port: int,
|
|
282
|
+
server: ClientVariableServer | None = None,
|
|
283
|
+
) -> FastAPI:
|
|
308
284
|
"""Create the FastAPI application hosting the REST bridge."""
|
|
309
285
|
app = FastAPI(title="NetSync REST Bridge", version="1.0.0")
|
|
310
286
|
|
|
@@ -329,14 +305,17 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
329
305
|
def upsert(room_id: str, device_id: str, body: UpsertBody) -> dict[str, object]:
|
|
330
306
|
if not body.variables:
|
|
331
307
|
raise HTTPException(status_code=400, detail="variables must not be empty")
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
308
|
+
if server is not None:
|
|
309
|
+
try:
|
|
310
|
+
client_no, statuses = server.upsert_client_variables_for_device(
|
|
311
|
+
room_id, device_id, body.variables
|
|
312
|
+
)
|
|
313
|
+
except ValueError as exc:
|
|
314
|
+
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
315
|
+
else:
|
|
316
|
+
bridge = manager.get(room_id)
|
|
317
|
+
statuses = bridge.apply_now_or_queue(device_id, body.variables)
|
|
318
|
+
client_no = bridge.get_client_no(device_id)
|
|
340
319
|
|
|
341
320
|
return {
|
|
342
321
|
"roomId": room_id,
|
|
@@ -387,21 +366,26 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
387
366
|
|
|
388
367
|
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
389
368
|
def get_client_variables(room_id: str, device_id: str) -> dict[str, object]:
|
|
390
|
-
|
|
391
|
-
|
|
369
|
+
if server is not None:
|
|
370
|
+
client_no, variables = server.get_client_variables_for_device(
|
|
371
|
+
room_id, device_id
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
bridge = manager.get(room_id)
|
|
375
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
392
376
|
return {"clientNo": client_no, "variables": variables}
|
|
393
377
|
|
|
394
378
|
@app.get("/v1/rooms/{room_id}/devices/{device_id}/client-variables/{name}")
|
|
395
379
|
def get_client_variable(
|
|
396
380
|
room_id: str, device_id: str, name: VarName
|
|
397
381
|
) -> dict[str, object]:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
raise HTTPException(
|
|
402
|
-
status_code=404,
|
|
403
|
-
detail=f"Device '{device_id}' has no client mapping",
|
|
382
|
+
if server is not None:
|
|
383
|
+
client_no, variables = server.get_client_variables_for_device(
|
|
384
|
+
room_id, device_id
|
|
404
385
|
)
|
|
386
|
+
else:
|
|
387
|
+
bridge = manager.get(room_id)
|
|
388
|
+
client_no, variables = bridge.get_client_variables(device_id)
|
|
405
389
|
if name not in variables:
|
|
406
390
|
raise HTTPException(
|
|
407
391
|
status_code=404,
|
|
@@ -409,6 +393,32 @@ def create_app(server_addr: str, dealer_port: int, sub_port: int) -> FastAPI:
|
|
|
409
393
|
)
|
|
410
394
|
return {"clientNo": client_no, "value": variables[name]}
|
|
411
395
|
|
|
396
|
+
@app.delete("/v1/rooms/{room_id}/devices/{device_id}/client-variables")
|
|
397
|
+
def delete_client_variables(room_id: str, device_id: str) -> dict[str, object]:
|
|
398
|
+
if server is None:
|
|
399
|
+
raise HTTPException(
|
|
400
|
+
status_code=501,
|
|
401
|
+
detail="Client variable deletion requires an injected server",
|
|
402
|
+
)
|
|
403
|
+
client_no, deleted_count = server.delete_client_variables_for_device(
|
|
404
|
+
room_id, device_id
|
|
405
|
+
)
|
|
406
|
+
return {"clientNo": client_no, "deletedCount": deleted_count}
|
|
407
|
+
|
|
408
|
+
@app.delete("/v1/rooms/{room_id}/devices/{device_id}/client-variables/{name}")
|
|
409
|
+
def delete_client_variable(
|
|
410
|
+
room_id: str, device_id: str, name: VarName
|
|
411
|
+
) -> dict[str, object]:
|
|
412
|
+
if server is None:
|
|
413
|
+
raise HTTPException(
|
|
414
|
+
status_code=501,
|
|
415
|
+
detail="Client variable deletion requires an injected server",
|
|
416
|
+
)
|
|
417
|
+
client_no, deleted_count = server.delete_client_variables_for_device(
|
|
418
|
+
room_id, device_id, name
|
|
419
|
+
)
|
|
420
|
+
return {"clientNo": client_no, "deletedCount": deleted_count}
|
|
421
|
+
|
|
412
422
|
return app
|
|
413
423
|
|
|
414
424
|
|