phystack-hub-client 0.2.0__py3-none-any.whl
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.
- phystack/hub_client/__init__.py +90 -0
- phystack/hub_client/client.py +701 -0
- phystack/hub_client/instances/__init__.py +11 -0
- phystack/hub_client/instances/base_instance.py +225 -0
- phystack/hub_client/instances/edge_instance.py +212 -0
- phystack/hub_client/instances/peripheral_instance.py +186 -0
- phystack/hub_client/services/__init__.py +5 -0
- phystack/hub_client/services/phyhub_connection.py +272 -0
- phystack/hub_client/services/webrtc/__init__.py +23 -0
- phystack/hub_client/services/webrtc/data_channel.py +527 -0
- phystack/hub_client/services/webrtc/media_stream.py +571 -0
- phystack/hub_client/twin_messaging.py +287 -0
- phystack/hub_client/twin_registry.py +158 -0
- phystack/hub_client/types/__init__.py +15 -0
- phystack/hub_client/types/twin_types.py +136 -0
- phystack_hub_client-0.2.0.dist-info/METADATA +174 -0
- phystack_hub_client-0.2.0.dist-info/RECORD +18 -0
- phystack_hub_client-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PhyHub Direct Connection Service
|
|
3
|
+
|
|
4
|
+
Allows direct connection to PhyHub for testing purposes,
|
|
5
|
+
bypassing the phyos layer that normally runs on devices.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
- Set PHYHUB_DIRECT=true to enable direct connection
|
|
9
|
+
- Provide DEVICE_ID and ACCESS_KEY environment variables
|
|
10
|
+
- Optionally set PHYHUB_URL (defaults to EU region)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Callable, Optional
|
|
17
|
+
|
|
18
|
+
import socketio
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_PHYHUB_URLS: dict[str, str] = {
|
|
22
|
+
"eu": "https://phyhub.eu.omborigrid.net",
|
|
23
|
+
"us": "https://phyhub.us.omborigrid.net",
|
|
24
|
+
"uae": "https://phyhub.uae.omborigrid.net",
|
|
25
|
+
"au": "https://phyhub.au.omborigrid.net",
|
|
26
|
+
"in": "https://phyhub.in.omborigrid.net",
|
|
27
|
+
"qa": "https://phyhub.qa.omborigrid.net",
|
|
28
|
+
"dev": "https://phyhub.dev.omborigrid.net",
|
|
29
|
+
"local": "http://localhost:3000",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_direct_mode() -> bool:
|
|
34
|
+
"""Check if running in direct connection mode (vs PhyOS mode)."""
|
|
35
|
+
return os.environ.get("PHYHUB_DIRECT", "").lower() in ("true", "1")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DirectConnectionConfig:
|
|
40
|
+
"""Configuration for direct PhyHub connection."""
|
|
41
|
+
|
|
42
|
+
device_id: str
|
|
43
|
+
access_key: str
|
|
44
|
+
phyhub_url: str = field(default_factory=lambda: DEFAULT_PHYHUB_URLS["eu"])
|
|
45
|
+
instance_id: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PhyHubConnection:
|
|
49
|
+
"""Direct connection to PhyHub using Socket.IO."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: DirectConnectionConfig) -> None:
|
|
52
|
+
self.config = config
|
|
53
|
+
self._sio: Optional[socketio.AsyncClient] = None
|
|
54
|
+
self._connected = False
|
|
55
|
+
self._connect_event: Optional[asyncio.Event] = None
|
|
56
|
+
self._listeners: dict[str, list[Callable[..., Any]]] = {}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_env(cls) -> "PhyHubConnection":
|
|
60
|
+
"""Create a direct connection from environment variables.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If required environment variables are missing.
|
|
64
|
+
"""
|
|
65
|
+
device_id = os.environ.get("DEVICE_ID") or os.environ.get("PHYGRID_DEVICE_ID")
|
|
66
|
+
access_key = os.environ.get("ACCESS_KEY") or os.environ.get("PHYGRID_DEVICE_KEY")
|
|
67
|
+
|
|
68
|
+
if not device_id:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"[PhyHubConnection] DEVICE_ID or PHYGRID_DEVICE_ID env var required"
|
|
71
|
+
)
|
|
72
|
+
if not access_key:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"[PhyHubConnection] ACCESS_KEY or PHYGRID_DEVICE_KEY env var required"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
region = os.environ.get("PHYHUB_REGION", "").lower()
|
|
78
|
+
phyhub_url = os.environ.get("PHYHUB_URL") or DEFAULT_PHYHUB_URLS.get(region)
|
|
79
|
+
|
|
80
|
+
if not phyhub_url:
|
|
81
|
+
valid_regions = ", ".join(DEFAULT_PHYHUB_URLS.keys())
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"[PhyHubConnection] PHYHUB_URL or valid PHYHUB_REGION required. "
|
|
84
|
+
f"Valid regions: {valid_regions}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return cls(
|
|
88
|
+
DirectConnectionConfig(
|
|
89
|
+
device_id=device_id,
|
|
90
|
+
access_key=access_key,
|
|
91
|
+
phyhub_url=phyhub_url,
|
|
92
|
+
instance_id=os.environ.get("TWIN_ID") or os.environ.get("INSTANCE_ID"),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def is_enabled() -> bool:
|
|
98
|
+
"""Check if direct connection mode is enabled."""
|
|
99
|
+
return os.environ.get("PHYHUB_DIRECT", "").lower() in ("true", "1")
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def device_id(self) -> str:
|
|
103
|
+
"""Get the device ID (also used as the primary twin ID)."""
|
|
104
|
+
return self.config.device_id
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def instance_id(self) -> str:
|
|
108
|
+
"""Get the instance/twin ID to use."""
|
|
109
|
+
return self.config.instance_id or self.config.device_id
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_connected(self) -> bool:
|
|
113
|
+
"""Check if currently connected."""
|
|
114
|
+
return self._connected and self._sio is not None
|
|
115
|
+
|
|
116
|
+
async def connect(self) -> socketio.AsyncClient:
|
|
117
|
+
"""Connect directly to PhyHub.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The connected Socket.IO client.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ConnectionError: If connection fails.
|
|
124
|
+
"""
|
|
125
|
+
if self._sio and self._connected:
|
|
126
|
+
return self._sio
|
|
127
|
+
|
|
128
|
+
print(f"[PhyHubConnection] Connecting to {self.config.phyhub_url}")
|
|
129
|
+
print(f"[PhyHubConnection] Device ID: {self.config.device_id}")
|
|
130
|
+
|
|
131
|
+
self._sio = socketio.AsyncClient(
|
|
132
|
+
reconnection=True,
|
|
133
|
+
reconnection_attempts=5,
|
|
134
|
+
reconnection_delay=1,
|
|
135
|
+
logger=False,
|
|
136
|
+
engineio_logger=False,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self._connect_event = asyncio.Event()
|
|
140
|
+
|
|
141
|
+
@self._sio.event
|
|
142
|
+
async def connect() -> None:
|
|
143
|
+
self._connected = True
|
|
144
|
+
print("[PhyHubConnection] Connected successfully")
|
|
145
|
+
if self._connect_event:
|
|
146
|
+
self._connect_event.set()
|
|
147
|
+
|
|
148
|
+
@self._sio.event
|
|
149
|
+
async def disconnect() -> None:
|
|
150
|
+
self._connected = False
|
|
151
|
+
print("[PhyHubConnection] Disconnected")
|
|
152
|
+
|
|
153
|
+
@self._sio.event
|
|
154
|
+
async def connect_error(data: Any) -> None:
|
|
155
|
+
print(f"[PhyHubConnection] Connection error: {data}")
|
|
156
|
+
if self._connect_event:
|
|
157
|
+
self._connect_event.set()
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
await self._sio.connect(
|
|
161
|
+
self.config.phyhub_url,
|
|
162
|
+
auth={
|
|
163
|
+
"deviceId": self.config.device_id,
|
|
164
|
+
"accessKey": self.config.access_key,
|
|
165
|
+
},
|
|
166
|
+
wait_timeout=15,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Wait for connection confirmation
|
|
170
|
+
await asyncio.wait_for(self._connect_event.wait(), timeout=15.0)
|
|
171
|
+
|
|
172
|
+
if not self._connected:
|
|
173
|
+
raise ConnectionError("Failed to connect to PhyHub")
|
|
174
|
+
|
|
175
|
+
return self._sio
|
|
176
|
+
|
|
177
|
+
except asyncio.TimeoutError:
|
|
178
|
+
raise ConnectionError("Connection timeout")
|
|
179
|
+
|
|
180
|
+
async def disconnect(self) -> None:
|
|
181
|
+
"""Disconnect from PhyHub."""
|
|
182
|
+
if self._sio:
|
|
183
|
+
await self._sio.disconnect()
|
|
184
|
+
self._sio = None
|
|
185
|
+
self._connected = False
|
|
186
|
+
|
|
187
|
+
def get_socket(self) -> Optional[socketio.AsyncClient]:
|
|
188
|
+
"""Get the socket instance."""
|
|
189
|
+
return self._sio
|
|
190
|
+
|
|
191
|
+
async def emit(
|
|
192
|
+
self,
|
|
193
|
+
event: str,
|
|
194
|
+
data: Any = None,
|
|
195
|
+
callback: Optional[Callable[..., Any]] = None,
|
|
196
|
+
response_event: Optional[str] = None,
|
|
197
|
+
) -> Any:
|
|
198
|
+
"""Emit an event to the server.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
event: Event name.
|
|
202
|
+
data: Data to send.
|
|
203
|
+
callback: Optional callback for acknowledgement.
|
|
204
|
+
response_event: Optional event name to wait for response.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Response from server if callback pattern is used.
|
|
208
|
+
"""
|
|
209
|
+
if not self._sio or not self._connected:
|
|
210
|
+
raise ConnectionError("Not connected to PhyHub")
|
|
211
|
+
|
|
212
|
+
if response_event:
|
|
213
|
+
# Wait for a specific response event
|
|
214
|
+
response_future: asyncio.Future[Any] = asyncio.get_event_loop().create_future()
|
|
215
|
+
|
|
216
|
+
def on_response(data: Any) -> None:
|
|
217
|
+
if not response_future.done():
|
|
218
|
+
response_future.set_result(data)
|
|
219
|
+
|
|
220
|
+
self._sio.on(response_event, on_response)
|
|
221
|
+
await self._sio.emit(event, data)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
result = await asyncio.wait_for(response_future, timeout=20)
|
|
225
|
+
return result
|
|
226
|
+
finally:
|
|
227
|
+
# Clean up the listener - use handlers dict directly
|
|
228
|
+
if response_event in self._sio.handlers.get("/", {}):
|
|
229
|
+
del self._sio.handlers["/"][response_event]
|
|
230
|
+
elif callback:
|
|
231
|
+
# Use callback pattern (ack-based)
|
|
232
|
+
return await self._sio.call(event, data, timeout=20)
|
|
233
|
+
else:
|
|
234
|
+
await self._sio.emit(event, data)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def on(self, event: str, handler: Callable[..., Any]) -> None:
|
|
238
|
+
"""Register an event handler.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
event: Event name to listen for.
|
|
242
|
+
handler: Handler function.
|
|
243
|
+
"""
|
|
244
|
+
if self._sio:
|
|
245
|
+
self._sio.on(event, handler)
|
|
246
|
+
|
|
247
|
+
# Also store for reconnection
|
|
248
|
+
if event not in self._listeners:
|
|
249
|
+
self._listeners[event] = []
|
|
250
|
+
self._listeners[event].append(handler)
|
|
251
|
+
|
|
252
|
+
def off(self, event: str, handler: Optional[Callable[..., Any]] = None) -> None:
|
|
253
|
+
"""Remove an event handler.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
event: Event name.
|
|
257
|
+
handler: Specific handler to remove, or None to remove all.
|
|
258
|
+
"""
|
|
259
|
+
if event in self._listeners:
|
|
260
|
+
if handler:
|
|
261
|
+
self._listeners[event] = [h for h in self._listeners[event] if h != handler]
|
|
262
|
+
else:
|
|
263
|
+
self._listeners[event] = []
|
|
264
|
+
|
|
265
|
+
async def __aenter__(self) -> "PhyHubConnection":
|
|
266
|
+
"""Async context manager entry."""
|
|
267
|
+
await self.connect()
|
|
268
|
+
return self
|
|
269
|
+
|
|
270
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
271
|
+
"""Async context manager exit."""
|
|
272
|
+
await self.disconnect()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""WebRTC services for DataChannel and MediaStream."""
|
|
2
|
+
|
|
3
|
+
from phystack.hub_client.services.webrtc.data_channel import (
|
|
4
|
+
DataChannelHandler,
|
|
5
|
+
DataChannelOptions,
|
|
6
|
+
PhygridDataChannel,
|
|
7
|
+
)
|
|
8
|
+
from phystack.hub_client.services.webrtc.media_stream import (
|
|
9
|
+
MediaStreamHandler,
|
|
10
|
+
MediaStreamOptions,
|
|
11
|
+
PhygridMediaStream,
|
|
12
|
+
WebcamVideoTrack,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DataChannelHandler",
|
|
17
|
+
"DataChannelOptions",
|
|
18
|
+
"PhygridDataChannel",
|
|
19
|
+
"MediaStreamHandler",
|
|
20
|
+
"MediaStreamOptions",
|
|
21
|
+
"PhygridMediaStream",
|
|
22
|
+
"WebcamVideoTrack",
|
|
23
|
+
]
|