world state
Browse files- xros_world.py +123 -0
xros_world.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""xros_world.py — the core of the DiOS/XROS world-state service.
|
| 2 |
+
|
| 3 |
+
This implements the central claim of the DiOS paper (arXiv:2201.03256):
|
| 4 |
+
the physical-digital world is a *single shared resource* among applications.
|
| 5 |
+
Instead of every app reconstructing its own view of the world, apps read and
|
| 6 |
+
mutate one shared world model.
|
| 7 |
+
|
| 8 |
+
The module is dependency-free (stdlib only) so the store can be unit-tested
|
| 9 |
+
and reused anywhere. Networking lives in xros_server.py.
|
| 10 |
+
|
| 11 |
+
Concepts:
|
| 12 |
+
- WorldObject: a node in the scene graph (an entity in the shared world).
|
| 13 |
+
- WorldState: the registry of objects + an event bus that publishes
|
| 14 |
+
spawn/move/remove events to every subscriber.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import asyncio
|
| 20 |
+
import time
|
| 21 |
+
import uuid
|
| 22 |
+
from dataclasses import dataclass, field, asdict
|
| 23 |
+
from typing import Any, Dict, List, Optional
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class Pose:
|
| 28 |
+
"""Position (meters), rotation (quaternion x,y,z,w), scale (unitless)."""
|
| 29 |
+
position: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0])
|
| 30 |
+
rotation: List[float] = field(default_factory=lambda: [0.0, 0.0, 0.0, 1.0])
|
| 31 |
+
scale: List[float] = field(default_factory=lambda: [1.0, 1.0, 1.0])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class WorldObject:
|
| 36 |
+
"""A single entity in the shared world (a scene-graph node)."""
|
| 37 |
+
id: str
|
| 38 |
+
type: str
|
| 39 |
+
pose: Pose
|
| 40 |
+
properties: Dict[str, Any] = field(default_factory=dict)
|
| 41 |
+
owner: Optional[str] = None
|
| 42 |
+
created_at: float = field(default_factory=time.time)
|
| 43 |
+
updated_at: float = field(default_factory=time.time)
|
| 44 |
+
|
| 45 |
+
def to_dict(self) -> Dict[str, Any]:
|
| 46 |
+
d = asdict(self)
|
| 47 |
+
d["pose"] = asdict(self.pose)
|
| 48 |
+
return d
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class WorldState:
|
| 52 |
+
"""The shared world: an object registry plus a fan-out event bus.
|
| 53 |
+
|
| 54 |
+
Thread-safety: all mutations are serialized through an asyncio.Lock, and
|
| 55 |
+
events are delivered to per-subscriber asyncio.Queues so a slow client
|
| 56 |
+
never blocks a fast one.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(self) -> None:
|
| 60 |
+
self._objects: Dict[str, WorldObject] = {}
|
| 61 |
+
self._subscribers: List[asyncio.Queue] = []
|
| 62 |
+
self._lock = asyncio.Lock()
|
| 63 |
+
|
| 64 |
+
# ---- mutations -----------------------------------------------------
|
| 65 |
+
|
| 66 |
+
async def spawn(
|
| 67 |
+
self,
|
| 68 |
+
type: str,
|
| 69 |
+
pose: Optional[Pose] = None,
|
| 70 |
+
properties: Optional[Dict[str, Any]] = None,
|
| 71 |
+
owner: Optional[str] = None,
|
| 72 |
+
) -> WorldObject:
|
| 73 |
+
obj = WorldObject(
|
| 74 |
+
id=uuid.uuid4().hex,
|
| 75 |
+
type=type,
|
| 76 |
+
pose=pose or Pose(),
|
| 77 |
+
properties=properties or {},
|
| 78 |
+
owner=owner,
|
| 79 |
+
)
|
| 80 |
+
async with self._lock:
|
| 81 |
+
self._objects[obj.id] = obj
|
| 82 |
+
await self._publish({"op": "spawn", "object": obj.to_dict()})
|
| 83 |
+
return obj
|
| 84 |
+
|
| 85 |
+
async def move(self, obj_id: str, pose: Pose) -> WorldObject:
|
| 86 |
+
async with self._lock:
|
| 87 |
+
obj = self._objects[obj_id] # KeyError if unknown -> caller handles
|
| 88 |
+
obj.pose = pose
|
| 89 |
+
obj.updated_at = time.time()
|
| 90 |
+
await self._publish({"op": "move", "object": obj.to_dict()})
|
| 91 |
+
return obj
|
| 92 |
+
|
| 93 |
+
async def remove(self, obj_id: str) -> WorldObject:
|
| 94 |
+
async with self._lock:
|
| 95 |
+
obj = self._objects.pop(obj_id) # KeyError if unknown
|
| 96 |
+
await self._publish({"op": "remove", "id": obj_id})
|
| 97 |
+
return obj
|
| 98 |
+
|
| 99 |
+
# ---- reads ---------------------------------------------------------
|
| 100 |
+
|
| 101 |
+
async def snapshot(self) -> List[Dict[str, Any]]:
|
| 102 |
+
async with self._lock:
|
| 103 |
+
return [o.to_dict() for o in self._objects.values()]
|
| 104 |
+
|
| 105 |
+
async def get(self, obj_id: str) -> Optional[WorldObject]:
|
| 106 |
+
async with self._lock:
|
| 107 |
+
return self._objects.get(obj_id)
|
| 108 |
+
|
| 109 |
+
# ---- pub/sub -------------------------------------------------------
|
| 110 |
+
|
| 111 |
+
def subscribe(self) -> asyncio.Queue:
|
| 112 |
+
"""Return a queue that will receive every future world event."""
|
| 113 |
+
q: asyncio.Queue = asyncio.Queue()
|
| 114 |
+
self._subscribers.append(q)
|
| 115 |
+
return q
|
| 116 |
+
|
| 117 |
+
def unsubscribe(self, q: asyncio.Queue) -> None:
|
| 118 |
+
if q in self._subscribers:
|
| 119 |
+
self._subscribers.remove(q)
|
| 120 |
+
|
| 121 |
+
async def _publish(self, event: Dict[str, Any]) -> None:
|
| 122 |
+
for q in list(self._subscribers):
|
| 123 |
+
q.put_nowait(event)
|