remoteRF-server 0.1.77__tar.gz → 0.1.79__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/PKG-INFO +5 -3
  2. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/README.md +4 -2
  3. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/pyproject.toml +1 -1
  4. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/host/host_tunnel_server.py +122 -73
  5. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/device_manager.py +71 -241
  6. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/PKG-INFO +5 -3
  7. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/setup.cfg +0 -0
  8. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/__init__.py +0 -0
  9. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/__init__.py +0 -0
  10. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  11. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  12. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  13. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  14. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  15. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/__init__.py +0 -0
  16. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  17. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/api_token.py +0 -0
  18. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  19. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/db_location.py +0 -0
  20. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/list_string.py +0 -0
  21. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  22. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/drivers/__init__.py +0 -0
  23. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  24. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  25. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/host/__init__.py +0 -0
  26. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/host/host_directory_store.py +0 -0
  27. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/__init__.py +0 -0
  28. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/acc_perms.py +0 -0
  29. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/cert_provider.py +0 -0
  30. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/grpc_server.py +0 -0
  31. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/reservation.py +0 -0
  32. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/rpc_manager.py +0 -0
  33. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/user_group_cli.py +0 -0
  34. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/server/user_group_handler.py +0 -0
  35. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/serverrf_cli.py +0 -0
  36. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/tools/__init__.py +0 -0
  37. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server/tools/gen_certs.py +0 -0
  38. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  39. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  40. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  41. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/requires.txt +0 -0
  42. {remoterf_server-0.1.77 → remoterf_server-0.1.79}/src/remoteRF_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.77
3
+ Version: 0.1.79
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -348,9 +348,11 @@ todo:
348
348
 
349
349
  <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
350
 
351
- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
351
+ <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
352
352
 
353
- ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
353
+ ServerRF display meta data as part of devices list / reservation
354
+
355
+ + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
354
356
 
355
357
  set_device token (virtual devices) + routing/relay to proper host (multiplexed)
356
358
 
@@ -337,9 +337,11 @@ todo:
337
337
 
338
338
  <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
339
339
 
340
- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
340
+ <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
341
341
 
342
- ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
342
+ ServerRF display meta data as part of devices list / reservation
343
+
344
+ + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
343
345
 
344
346
  set_device token (virtual devices) + routing/relay to proper host (multiplexed)
345
347
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.77"
7
+ version = "0.1.79"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -10,7 +10,7 @@ import secrets
10
10
  import traceback
11
11
  import hashlib
12
12
  import re
13
- import os
13
+ import uuid
14
14
 
15
15
  from dataclasses import dataclass
16
16
  from typing import Dict, Optional, Tuple, Any, List, Set, Iterator
@@ -47,12 +47,13 @@ from .host_directory_store import (
47
47
  # LOG_MODE = "ALL" -> show everything (INFO + WARN + ERROR)
48
48
  # =============================================================================
49
49
 
50
- LOG_MODE = "OFF" # <-- change this
50
+ LOG_MODE = "WARN" # <-- set to ALL while debugging session UUIDs
51
51
 
52
52
  _LOG_LOCK = threading.Lock()
53
53
 
54
54
  HOST_ID_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") # adjust if you want
55
55
 
56
+
56
57
  class HostIdConflictError(RuntimeError):
57
58
  def __init__(self, host_id: str, *, age_ms: int, replace_after_ms: int) -> None:
58
59
  super().__init__(
@@ -63,6 +64,7 @@ class HostIdConflictError(RuntimeError):
63
64
  self.age_ms = age_ms
64
65
  self.replace_after_ms = replace_after_ms
65
66
 
67
+
66
68
  def _validate_host_id(raw: str) -> str:
67
69
  hid = (raw or "").strip()
68
70
  if not hid:
@@ -76,6 +78,7 @@ def _validate_host_id(raw: str) -> str:
76
78
  )
77
79
  return hid
78
80
 
81
+
79
82
  def _ts() -> str:
80
83
  t = time.time()
81
84
  sec = int(t)
@@ -83,6 +86,7 @@ def _ts() -> str:
83
86
  lt = time.localtime(sec)
84
87
  return time.strftime("%H:%M:%S", lt) + f".{ms:03d}"
85
88
 
89
+
86
90
  def _should_log(level: str) -> bool:
87
91
  lvl = (level or "").upper()
88
92
  mode = (LOG_MODE or "").upper()
@@ -92,28 +96,34 @@ def _should_log(level: str) -> bool:
92
96
  return lvl in ("WARN", "ERROR")
93
97
  if mode == "ALL":
94
98
  return lvl in ("INFO", "WARN", "ERROR")
95
- # safe default if mis-set
96
99
  return lvl in ("WARN", "ERROR")
97
100
 
101
+
98
102
  def _log(level: str, msg: str) -> None:
99
103
  if not _should_log(level):
100
104
  return
101
105
  tn = threading.current_thread().name
102
- print(f"{_ts()} {level:<5} [{tn}] hostrf.host_tunnel_server: {msg}", file=sys.stderr, flush=True)
106
+ with _LOG_LOCK:
107
+ print(f"{_ts()} {level:<5} [{tn}] hostrf.host_tunnel_server: {msg}", file=sys.stderr, flush=True)
108
+
103
109
 
104
110
  def info(msg: str) -> None:
105
111
  _log("INFO", msg)
106
112
 
113
+
107
114
  def warn(msg: str) -> None:
108
115
  _log("WARN", msg)
109
116
 
117
+
110
118
  def error(msg: str) -> None:
111
119
  _log("ERROR", msg)
112
120
 
121
+
113
122
  def exception(msg: str) -> None:
114
123
  _log("ERROR", msg)
115
124
  traceback.print_exc(limit=80, file=sys.stderr)
116
125
 
126
+
117
127
  # =============================================================================
118
128
  # Helpers
119
129
  # =============================================================================
@@ -122,10 +132,12 @@ def host_key(host_id: str) -> str:
122
132
  h = hashlib.sha256((host_id or "").encode("utf-8")).hexdigest()
123
133
  return h[:16] # short stable key
124
134
 
135
+
125
136
  def _safe_thread_tag(s: str) -> str:
126
137
  s = s or "peer"
127
138
  return re.sub(r"[^A-Za-z0-9_.-]+", "_", s)[:64]
128
139
 
140
+
129
141
  def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
130
142
  if src_map is None:
131
143
  return
@@ -136,6 +148,7 @@ def _copy_argument_map(dst_map: Any, src_map: Any) -> None:
136
148
  for k, v in dict(src_map).items():
137
149
  dst_map[str(k)].CopyFrom(v)
138
150
 
151
+
139
152
  # =============================================================================
140
153
  # Types
141
154
  # =============================================================================
@@ -145,12 +158,14 @@ class DeviceRoute:
145
158
  host_id: str
146
159
  host_local_id: int
147
160
 
161
+
148
162
  @dataclass
149
163
  class HostStatus:
150
164
  host_id: str
151
165
  online: bool = False
152
166
  last_seen_ms: int = 0
153
167
 
168
+
154
169
  @dataclass
155
170
  class DeviceStatus:
156
171
  device_id: str
@@ -159,19 +174,17 @@ class DeviceStatus:
159
174
  online: bool = False
160
175
  last_seen_ms: int = 0
161
176
 
177
+
162
178
  # =============================================================================
163
179
  # Session
164
180
  # =============================================================================
165
181
 
166
182
  class HostSession:
167
- """
168
- Represents one connected host (one bidi stream).
169
- Owns:
170
- - outbound frame queue
171
- - inflight req_id -> Future
172
- """
173
- def __init__(self, host_id: str, *, outbound_max: int = 2048) -> None:
183
+ def __init__(self, host_id: str, *, peer: str = "<?>", session_uuid: str = "", outbound_max: int = 2048) -> None:
174
184
  self.host_id = host_id
185
+ self.peer = peer
186
+ self.session_uuid = (session_uuid or uuid.uuid4().hex[:10])
187
+
175
188
  self.out_q: "queue.Queue[Optional[host_tunnel_pb2.HostFrame]]" = queue.Queue(maxsize=int(outbound_max))
176
189
  self.inflight: Dict[str, Future] = {}
177
190
  self.inflight_lock = threading.Lock()
@@ -179,34 +192,40 @@ class HostSession:
179
192
  self.alive = True
180
193
  self.outbound_max = int(outbound_max)
181
194
 
182
- info(f"[session] created host_id={self.host_id!r} outbound_max={self.outbound_max}")
195
+ info(
196
+ f"[session] created host_id={self.host_id!r} sess={self.session_uuid} "
197
+ f"peer={self.peer} outbound_max={self.outbound_max}"
198
+ )
183
199
 
184
200
  def send(self, frame: host_tunnel_pb2.HostFrame, *, timeout: float = 2.0) -> bool:
185
201
  which = frame.WhichOneof("msg")
186
202
  if not self.alive:
187
- warn(f"[session] send refused (dead session) host_id={self.host_id!r} type={which}")
203
+ warn(f"[session] send refused (dead session) host_id={self.host_id!r} sess={self.session_uuid} type={which}")
188
204
  return False
189
205
  try:
190
206
  self.out_q.put(frame, timeout=timeout)
191
- info(f"[session] queued outbound host_id={self.host_id!r} type={which}")
207
+ info(f"[session] queued outbound host_id={self.host_id!r} sess={self.session_uuid} type={which}")
192
208
  return True
193
209
  except queue.Full:
194
- warn(f"[session] outbound queue FULL host_id={self.host_id!r} max={self.outbound_max} drop type={which}")
210
+ warn(
211
+ f"[session] outbound queue FULL host_id={self.host_id!r} sess={self.session_uuid} "
212
+ f"max={self.outbound_max} drop type={which}"
213
+ )
195
214
  return False
196
215
  except Exception:
197
- exception(f"[session] send exception host_id={self.host_id!r} type={which}")
216
+ exception(f"[session] send exception host_id={self.host_id!r} sess={self.session_uuid} type={which}")
198
217
  return False
199
218
 
200
219
  def close(self) -> None:
201
220
  if not self.alive:
202
221
  return
203
- info(f"[session] closing host_id={self.host_id!r}")
222
+ info(f"[session] closing host_id={self.host_id!r} sess={self.session_uuid} peer={self.peer}")
204
223
  self.alive = False
205
224
 
206
225
  try:
207
226
  self.out_q.put_nowait(None)
208
227
  except Exception:
209
- exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id!r}")
228
+ exception(f"[session] close: failed to enqueue sentinel host_id={self.host_id!r} sess={self.session_uuid}")
210
229
 
211
230
  with self.inflight_lock:
212
231
  for rid, fut in list(self.inflight.items()):
@@ -214,7 +233,8 @@ class HostSession:
214
233
  fut.set_exception(RuntimeError(f"Host '{self.host_id}' disconnected (req_id={rid})"))
215
234
  self.inflight.clear()
216
235
 
217
- info(f"[session] closed host_id={self.host_id!r}")
236
+ info(f"[session] closed host_id={self.host_id!r} sess={self.session_uuid}")
237
+
218
238
 
219
239
  # =============================================================================
220
240
  # Registry
@@ -352,7 +372,10 @@ class HostTunnelRegistry:
352
372
 
353
373
  self._invalidate_caches_locked()
354
374
 
355
- info(f"[registry] persisted directory loaded: routes={len(self._routes)} hosts={len(self._host_status)} devices={len(self._device_status)}")
375
+ info(
376
+ f"[registry] persisted directory loaded: routes={len(self._routes)} "
377
+ f"hosts={len(self._host_status)} devices={len(self._device_status)}"
378
+ )
356
379
 
357
380
  # ---------------------------
358
381
  # persistence write helpers
@@ -412,28 +435,33 @@ class HostTunnelRegistry:
412
435
  # directory/status mutators
413
436
  # ---------------------------
414
437
 
415
- def register_host(
416
- self,
417
- host_id: str,
418
- session: HostSession,
419
- *,
420
- replace_if_stale_ms: int = 15_000, # tweak: how long without heartbeat before we allow replacement
421
- ) -> None:
422
- info(f"[registry] register_host: host_id={host_id!r}")
438
+ def register_host(self, host_id: str, session: HostSession, *, replace_if_stale_ms: int = 15_000) -> None:
439
+ info(
440
+ f"[registry] register_host: host_id={host_id!r} incoming_sess={session.session_uuid} "
441
+ f"incoming_peer={session.peer}"
442
+ )
423
443
 
424
444
  with self._lock:
425
445
  old = self._hosts.get(host_id)
426
446
  if old is not None and old.alive:
427
447
  age_ms = int(now_ms() - int(getattr(old, "last_heartbeat_ms", 0) or 0))
448
+
428
449
  if age_ms < replace_if_stale_ms:
429
- # conflict: someone else is already using this host_id
450
+ warn(
451
+ f"[registry] HostIdConflict host_id={host_id!r} age_ms={age_ms} "
452
+ f"incoming_sess={session.session_uuid} incoming_peer={session.peer} "
453
+ f"existing_sess={getattr(old,'session_uuid','?')} existing_peer={getattr(old,'peer','?')}"
454
+ )
430
455
  raise HostIdConflictError(host_id, age_ms=age_ms, replace_after_ms=replace_if_stale_ms)
431
456
 
432
- # stale: allow replacement
433
- warn(f"[registry] register_host: replacing STALE session host_id={host_id!r} age_ms={age_ms}")
457
+ warn(
458
+ f"[registry] replacing STALE host_id={host_id!r} age_ms={age_ms} "
459
+ f"incoming_sess={session.session_uuid} incoming_peer={session.peer} "
460
+ f"existing_sess={getattr(old,'session_uuid','?')} existing_peer={getattr(old,'peer','?')}"
461
+ )
434
462
  old.close()
435
463
 
436
- self._hosts[host_id] = session
464
+ self._hosts[host_id] = session # (removed duplicate assignment)
437
465
 
438
466
  hs = self._host_status.get(host_id) or HostStatus(host_id=host_id)
439
467
  hs.online = True
@@ -444,7 +472,7 @@ class HostTunnelRegistry:
444
472
  self._persist_host(host_id)
445
473
  self._invalidate_caches_locked()
446
474
 
447
- info(f"[registry] register_host done: host_id={host_id!r} online=True")
475
+ info(f"[registry] register_host done: host_id={host_id!r} online=True incoming_sess={session.session_uuid}")
448
476
 
449
477
  def drop_host(self, host_id: str) -> None:
450
478
  info(f"[registry] drop_host: host_id={host_id!r}")
@@ -778,6 +806,7 @@ class HostTunnelRegistry:
778
806
  cancel_on_timeout=cancel_on_timeout,
779
807
  )
780
808
 
809
+
781
810
  # =============================================================================
782
811
  # Convenience forwarders
783
812
  # =============================================================================
@@ -804,6 +833,7 @@ def handle_host_device(
804
833
  )
805
834
  return dict(resp.results)
806
835
 
836
+
807
837
  def handle_host_device_request(
808
838
  registry: HostTunnelRegistry,
809
839
  *,
@@ -824,6 +854,7 @@ def handle_host_device_request(
824
854
  cancel_on_timeout=True,
825
855
  )
826
856
 
857
+
827
858
  # =============================================================================
828
859
  # Servicer
829
860
  # =============================================================================
@@ -839,7 +870,8 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
839
870
  peer = "unknown-peer"
840
871
 
841
872
  peer_tag = _safe_thread_tag(peer)
842
- info(f"[Connect] new stream peer={peer}")
873
+ stream_uuid = uuid.uuid4().hex[:10]
874
+ info(f"[Connect] new stream peer={peer} stream_sess={stream_uuid}")
843
875
 
844
876
  host_id: Optional[str] = None
845
877
  session: Optional[HostSession] = None
@@ -848,46 +880,50 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
848
880
 
849
881
  def inbound_loop():
850
882
  nonlocal host_id, session
851
- info(f"[inbound] start peer={peer}")
883
+ info(f"[inbound] start peer={peer} stream_sess={stream_uuid}")
852
884
 
853
885
  try:
854
886
  for frame in request_iterator:
855
887
  which = frame.WhichOneof("msg")
856
888
  if which is None:
857
- warn(f"[inbound] frame with no msg peer={peer}")
889
+ warn(f"[inbound] frame with no msg peer={peer} stream_sess={stream_uuid}")
858
890
  continue
859
891
 
860
- info(f"[inbound] IN type={which} peer={peer} host_id={host_id or '?'}")
892
+ info(f"[inbound] IN type={which} peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
861
893
 
862
894
  if which == "hello":
863
895
  hello = frame.hello
864
896
  raw = str(getattr(hello, "host_id", "") or "")
897
+
865
898
  try:
866
899
  host_id = _validate_host_id(raw)
867
900
  except ValueError as e:
868
- error(f"[inbound] invalid hello.host_id peer={peer} err={e}")
901
+ error(f"[inbound] invalid hello.host_id peer={peer} stream_sess={stream_uuid} err={e}")
869
902
  fatal["code"] = "INVALID_ARGUMENT"
870
903
  fatal["details"] = str(e)
871
904
  stop.set()
872
905
  return
873
906
 
874
907
  ver = str(getattr(hello, "version", "") or "")
875
- info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
908
+ info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer} stream_sess={stream_uuid}")
876
909
 
877
- session = HostSession(host_id)
910
+ # ONE session per stream
911
+ session = HostSession(host_id, peer=peer, session_uuid=stream_uuid)
912
+
913
+ # ONE register per stream
878
914
  try:
879
915
  self.registry.register_host(host_id, session, replace_if_stale_ms=15_000)
880
916
  except HostIdConflictError as e:
881
- error(f"[inbound] HostIdConflictError host_id={host_id!r} err={e}")
917
+ error(f"[inbound] HostIdConflictError host_id={host_id!r} stream_sess={stream_uuid} err={e}")
882
918
  fatal["code"] = "ALREADY_EXISTS"
883
919
  fatal["details"] = str(e)
920
+ try:
921
+ session.close()
922
+ except Exception:
923
+ pass
884
924
  stop.set()
885
925
  return
886
- ver = str(getattr(hello, "version", "") or "")
887
- info(f"[inbound] HELLO host_id={host_id!r} version={ver!r} peer={peer}")
888
926
 
889
- session = HostSession(host_id)
890
- self.registry.register_host(host_id, session)
891
927
  self.registry.persist_host_meta_from_hello(hello)
892
928
 
893
929
  # Ask host for metadata immediately on join
@@ -903,26 +939,30 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
903
939
  )
904
940
  )
905
941
  )
906
- info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id!r}")
942
+ info(f"[inbound] sent MetaRequest req_id={mrid} ok={ok} host_id={host_id!r} stream_sess={stream_uuid}")
907
943
  except Exception:
908
944
  exception("[inbound] FAILED sending MetaRequest")
909
945
 
910
946
  # heartbeat ack
911
947
  try:
912
- ok = session.send(host_tunnel_pb2.HostFrame(heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())))
913
- info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id!r}")
948
+ ok = session.send(
949
+ host_tunnel_pb2.HostFrame(
950
+ heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
951
+ )
952
+ )
953
+ info(f"[inbound] sent heartbeat ack ok={ok} host_id={host_id!r} stream_sess={stream_uuid}")
914
954
  except Exception:
915
955
  exception("[inbound] FAILED sending heartbeat ack")
916
956
 
917
957
  elif which == "device_announce":
918
958
  if session is None or host_id is None:
919
- warn(f"[inbound] device_announce before hello/session: ignoring peer={peer}")
959
+ warn(f"[inbound] device_announce before hello/session: ignoring peer={peer} stream_sess={stream_uuid}")
920
960
  continue
921
961
 
922
962
  ann = frame.device_announce
923
963
  n = len(getattr(ann, "devices", []))
924
964
  info(
925
- f"[inbound] DEVICE_ANNOUNCE host_id={host_id!r} n={n} "
965
+ f"[inbound] DEVICE_ANNOUNCE host_id={host_id!r} n={n} stream_sess={stream_uuid} "
926
966
  f"unix_ms={int(getattr(ann,'unix_ms',0) or 0)} full_snapshot={bool(getattr(ann,'full_snapshot',False))}"
927
967
  )
928
968
 
@@ -937,49 +977,53 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
937
977
 
938
978
  try:
939
979
  self.registry.announce_devices(host_id, list(ann.devices))
940
- info(f"[inbound] registry announce applied host_id={host_id!r}")
980
+ info(f"[inbound] registry announce applied host_id={host_id!r} stream_sess={stream_uuid}")
941
981
  except DeviceIdConflictError as e:
942
- error(f"[inbound] DeviceIdConflictError host_id={host_id!r} err={e}")
982
+ error(f"[inbound] DeviceIdConflictError host_id={host_id!r} stream_sess={stream_uuid} err={e}")
943
983
  fatal["code"] = "ALREADY_EXISTS"
944
984
  fatal["details"] = str(e)
945
985
  stop.set()
946
986
  return
947
987
  except Exception:
948
- exception(f"[inbound] announce_devices crashed host_id={host_id!r}")
988
+ exception(f"[inbound] announce_devices crashed host_id={host_id!r} stream_sess={stream_uuid}")
949
989
 
950
990
  # heartbeat ack after announce
951
991
  try:
952
- ok = session.send(host_tunnel_pb2.HostFrame(heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())))
953
- info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id!r}")
992
+ ok = session.send(
993
+ host_tunnel_pb2.HostFrame(
994
+ heartbeat=host_tunnel_pb2.Heartbeat(unix_ms=now_ms())
995
+ )
996
+ )
997
+ info(f"[inbound] sent heartbeat ack after announce ok={ok} host_id={host_id!r} stream_sess={stream_uuid}")
954
998
  except Exception:
955
999
  exception("[inbound] FAILED sending heartbeat ack after announce")
956
1000
 
957
1001
  elif which == "rpc_response":
958
1002
  if session is None:
959
- warn(f"[inbound] rpc_response before session: ignoring peer={peer}")
1003
+ warn(f"[inbound] rpc_response before session: ignoring peer={peer} stream_sess={stream_uuid}")
960
1004
  continue
961
1005
 
962
1006
  resp = frame.rpc_response
963
1007
  rid = str(getattr(resp, "req_id", "") or "")
964
1008
  ok = bool(getattr(resp, "ok", False))
965
1009
  err_s = str(getattr(resp, "error", "") or "")
966
- info(f"[inbound] RPC_RESPONSE req_id={rid} ok={ok} error={err_s!r}")
1010
+ info(f"[inbound] RPC_RESPONSE req_id={rid} ok={ok} error={err_s!r} stream_sess={stream_uuid}")
967
1011
 
968
1012
  with session.inflight_lock:
969
1013
  fut = session.inflight.get(rid)
970
1014
 
971
1015
  if fut is not None and not fut.done():
972
1016
  fut.set_result(resp)
973
- info(f"[inbound] delivered rpc_response to waiter req_id={rid}")
1017
+ info(f"[inbound] delivered rpc_response to waiter req_id={rid} stream_sess={stream_uuid}")
974
1018
  else:
975
- warn(f"[inbound] rpc_response has no inflight waiter req_id={rid}")
1019
+ warn(f"[inbound] rpc_response has no inflight waiter req_id={rid} stream_sess={stream_uuid}")
976
1020
 
977
1021
  elif which == "meta_response":
978
1022
  try:
979
1023
  mr = frame.meta_response
980
1024
  ok = bool(getattr(mr, "ok", False))
981
1025
  err_s = str(getattr(mr, "error", "") or "")
982
- info(f"[inbound] META_RESPONSE ok={ok} error={err_s!r}")
1026
+ info(f"[inbound] META_RESPONSE ok={ok} error={err_s!r} stream_sess={stream_uuid}")
983
1027
  try:
984
1028
  info(f"[inbound] META_RESPONSE proto:\n{mr}")
985
1029
  except Exception:
@@ -992,24 +1036,27 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
992
1036
  ms = int(getattr(frame.heartbeat, "unix_ms", 0) or 0) or now_ms()
993
1037
  session.last_heartbeat_ms = ms
994
1038
  self.registry.heartbeat(host_id, ms)
995
- info(f"[inbound] HEARTBEAT host_id={host_id!r} unix_ms={ms}")
1039
+ info(f"[inbound] HEARTBEAT host_id={host_id!r} unix_ms={ms} stream_sess={stream_uuid}")
996
1040
  else:
997
- warn(f"[inbound] heartbeat before hello/session: ignoring peer={peer} host_id={(host_id or '?')!r}")
1041
+ warn(
1042
+ f"[inbound] heartbeat before hello/session: ignoring peer={peer} stream_sess={stream_uuid} "
1043
+ f"host_id={(host_id or '?')!r}"
1044
+ )
998
1045
 
999
1046
  elif which == "cancel":
1000
1047
  try:
1001
1048
  rid = str(getattr(frame.cancel, "req_id", "") or "")
1002
- warn(f"[inbound] CANCEL received from host?? req_id={rid} peer={peer}")
1049
+ warn(f"[inbound] CANCEL received from host?? req_id={rid} peer={peer} stream_sess={stream_uuid}")
1003
1050
  except Exception:
1004
1051
  exception("[inbound] cancel frame parse failed")
1005
1052
 
1006
1053
  else:
1007
- warn(f"[inbound] unknown frame type={which} peer={peer}")
1054
+ warn(f"[inbound] unknown frame type={which} peer={peer} stream_sess={stream_uuid}")
1008
1055
 
1009
1056
  except Exception:
1010
- exception(f"[inbound] loop crashed peer={peer} host_id={host_id or '?'}")
1057
+ exception(f"[inbound] loop crashed peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1011
1058
  finally:
1012
- info(f"[inbound] exiting peer={peer} host_id={host_id or '?'}")
1059
+ info(f"[inbound] exiting peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1013
1060
  stop.set()
1014
1061
  if host_id is not None:
1015
1062
  try:
@@ -1025,14 +1072,14 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
1025
1072
  t = threading.Thread(target=inbound_loop, daemon=True, name=f"inbound_{peer_tag}")
1026
1073
  t.start()
1027
1074
 
1028
- info(f"[Connect] outbound pump starting peer={peer}")
1075
+ info(f"[Connect] outbound pump starting peer={peer} stream_sess={stream_uuid}")
1029
1076
 
1030
1077
  printed_waiting = False
1031
1078
  while not stop.is_set():
1032
1079
  if fatal:
1033
1080
  code = fatal.get("code", "")
1034
1081
  details = fatal.get("details", "fatal")
1035
- error(f"[Connect] aborting stream peer={peer} code={code} details={details}")
1082
+ error(f"[Connect] aborting stream peer={peer} stream_sess={stream_uuid} code={code} details={details}")
1036
1083
  if code == "ALREADY_EXISTS":
1037
1084
  context.abort(grpc.StatusCode.ALREADY_EXISTS, details)
1038
1085
  if code == "INVALID_ARGUMENT":
@@ -1041,7 +1088,7 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
1041
1088
 
1042
1089
  if session is None:
1043
1090
  if not printed_waiting:
1044
- info(f"[Connect] waiting for HELLO peer={peer}")
1091
+ info(f"[Connect] waiting for HELLO peer={peer} stream_sess={stream_uuid}")
1045
1092
  printed_waiting = True
1046
1093
  time.sleep(0.05)
1047
1094
  continue
@@ -1051,18 +1098,19 @@ class HostTunnelServicer(host_tunnel_pb2_grpc.HostTunnelServicer):
1051
1098
  except queue.Empty:
1052
1099
  continue
1053
1100
  except Exception:
1054
- exception(f"[Connect] outbound queue get failed peer={peer} host_id={host_id or '?'}")
1101
+ exception(f"[Connect] outbound queue get failed peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1055
1102
  continue
1056
1103
 
1057
1104
  if out is None:
1058
- info(f"[Connect] outbound sentinel -> end stream peer={peer} host_id={host_id or '?'}")
1105
+ info(f"[Connect] outbound sentinel -> end stream peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1059
1106
  break
1060
1107
 
1061
1108
  out_type = out.WhichOneof("msg")
1062
- info(f"[Connect] OUT type={out_type} peer={peer} host_id={host_id or '?'}")
1109
+ info(f"[Connect] OUT type={out_type} peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1063
1110
  yield out
1064
1111
 
1065
- info(f"[Connect] stream ended peer={peer} host_id={host_id or '?'}")
1112
+ info(f"[Connect] stream ended peer={peer} stream_sess={stream_uuid} host_id={host_id or '?'}")
1113
+
1066
1114
 
1067
1115
  # =============================================================================
1068
1116
  # Credentials + server start
@@ -1084,6 +1132,7 @@ def build_server_credentials(
1084
1132
  )
1085
1133
  return grpc.ssl_server_credentials(((private_key_pem, certificate_chain_pem),))
1086
1134
 
1135
+
1087
1136
  def start_host_tunnel_server(
1088
1137
  *,
1089
1138
  host: str,
@@ -1,243 +1,10 @@
1
- # # device_manager.py
2
-
3
- # import adi
4
- # # import uhd
5
- # # from hackrf import *
6
- # from ..common.utils import *
7
- # import subprocess
8
-
9
- # import time
10
- # import gc
11
-
12
- # #* Device manager *
13
- # '''
14
- # Responsible for starting and managing queues for devices.
15
- # '''
16
-
17
- # # TODO: Implement multiple device support
18
- # #region PlutoSDR
19
-
20
- # def connect_pluto(*, ip='', usb=''):
21
- # try:
22
- # if ip == '':
23
- # device = adi.Pluto(f'usb:{usb}')
24
- # print(f"Connected to Pluto usb:{usb}")
25
- # else:
26
- # device = adi.Pluto(f'ip:{ip}')
27
- # print(f"Connected to Pluto ip:{ip}")
28
- # return device
29
- # except Exception as e:
30
- # print(f"Pluto {ip}: {e}")
31
- # return None
32
-
33
- # import subprocess
34
- # import re
35
-
36
- # def get_usb_port_from_serial(serial):
37
- # try:
38
- # # Run iio_info -s and get the output
39
- # output = subprocess.check_output(["iio_info", "-s"]).decode("utf-8")
40
- # except Exception as e:
41
- # print(f"Error running iio_info: {e}")
42
- # return None
43
-
44
- # # Regex to find the USB port associated with the given serial number
45
- # pattern = re.compile(rf"\d+:.*serial={serial}\s+\[usb:(\S+)\]")
46
-
47
- # for line in output.splitlines():
48
- # match = pattern.search(line)
49
- # if match:
50
- # return match.group(1) # Return the USB port
51
-
52
- # print(f"No device found with serial {serial}")
53
- # return None
54
-
55
- # # def connect_hackrf(device_index):
56
- # # try:
57
- # # device = HackRF(device_index=device_index)
58
- # # print(f"Connected to HackRF {device_index}")
59
- # # return device
60
- # # except Exception as e:
61
- # # print(f"HackRF {device_index}: {e}")
62
- # # return None
63
-
64
- # # def connect_usrp2901():
65
- # # usrp = uhd.usrp.MultiUSRP()
66
- # # print(f"Connected to USRP 2901")
67
- # # return usrp
68
-
69
- # device_serialization = {
70
- # 0: '10447392da11001009001500f02af8a678',
71
- # 1: '10447392da110010f9ff1500612ff8ecb0',
72
- # # 2: '104473b04a06000ffbff3900f5de83267e', # GONE?
73
- # 2: '10447318ac0f0003050036008365171a2d',
74
- # 3: '10447376de0b00130b00200023119a89c6',
75
- # 4: '1044732a98110002faff1500f92ae517bf',
76
-
77
- # 5: '10447318ac0f001915003200073317bbca',
78
- # 6: '104473e6a60f00020d003900e21716bf22',
79
- # 7: '104473e6a60f000cf3ff17000b3917d48f',
80
- # 8: '10447318ac0f0016faff2500998717eacf',
81
- # }
82
-
83
- # master_token = 'SuperCoolTokenForIan'
84
-
85
- # # print("New Version")
86
-
87
- # devices = { # device, salt, hash
88
-
89
- # # iio_info -s
90
- # # ^ find usb serial ports
91
-
92
- # # 0: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[0])),'',''),
93
- # # 1: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[1])),'',''),
94
- # # 2: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[2])),'',''),
95
- # # 3: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[3])),'',''),
96
- # # 4: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[4])),'',''),
97
-
98
-
99
- # # 5: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[5])),'',''),
100
- # # 6: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[6])),'',''),
101
- # # 7: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[7])),'',''),
102
- # # 8: (connect_pluto(usb=get_usb_port_from_serial(device_serialization[8])),'',''),
103
-
104
- # # 0: (connect_pluto(usb='1.8.5'),'',''),
105
- # # 1: (connect_pluto(usb='1.18.5'),'',''),
106
- # # 2: (connect_pluto(usb='1.8.5'),'',''),
107
- # # 1: (connect_pluto(usb='1.8.5'),'',''),
108
-
109
- # # hackrf
110
- # # 10: (connect_hackrf(0),'',''),
111
-
112
- # # 20: (connect_usrp2901(),'',''),
113
-
114
- # }
115
-
116
- # devices_info = { # device, info
117
- # 0: "Pluto SDR (Loopback)",
118
- # 1: "Pluto SDR (Loopback)",
119
- # 2: "Pluto SDR (OTA + BPF)",
120
- # 3: "Pluto SDR (OTA + BPF)",
121
- # 4: "Pluto SDR (OTA + BPF)",
122
- # 5: "Pluto SDR (OTA)",
123
- # 6: "Pluto SDR (OTA)",
124
- # 7: "Pluto SDR (OTA)",
125
- # 8: "Pluto SDR (OTA)",
126
- # }
127
-
128
- # # transmitter = subprocess.Popen(['python3', 'transmitter.py'])
129
- # # transmitter.terminate()
130
-
131
- # def start_transmitter():
132
- # pass
133
- # # global transmitter
134
- # # transmitter = subprocess.Popen(['python3', 'transmitter.py'])
135
-
136
- # def terminate_transmitter():
137
- # pass
138
- # # global transmitter
139
- # # transmitter.terminate()
140
-
141
- # def get_transmitter_state():
142
- # global transmitter
143
- # return transmitter.poll() == None
144
-
145
- # def get_device(device_id):
146
- # return devices[device_id]
147
-
148
- # def get_all_devices() -> dict:
149
- # return {k: v for k, v in devices.items() if v is not None}
150
-
151
- # def get_all_devices_str() -> dict:
152
- # return devices_info
153
-
154
- # def parse_mastertoken(token: str):
155
- # """
156
- # Matches:
157
- # {master_token}_0
158
- # {master_token}-1
159
- # {master_token}_0_force
160
-
161
- # Returns (device_id:int, force:bool) or None.
162
- # """
163
- # if not token:
164
- # return None
165
-
166
- # prefix = re.escape(master_token)
167
- # pattern = re.compile(rf"^{prefix}[_-](\d+)(?:_force)?$")
168
-
169
- # m = pattern.match(token)
170
- # if not m:
171
- # return None
172
-
173
- # device_id = int(m.group(1))
174
- # force = token.endswith("_force")
175
- # return (device_id, force)
176
- # def device_exists(device_id: int) -> bool:
177
- # if device_id not in devices:
178
- # return False
179
- # dev, _, _ = devices[device_id]
180
- # return dev is not None
181
-
182
- # def device_is_available(device_id: int) -> bool:
183
- # if device_id not in devices:
184
- # return False
185
- # dev, salt, hsh = devices[device_id]
186
- # return (dev is not None) and (salt == '') and (hsh == '')
187
-
188
- # def get_device_by_id(device_id: int):
189
- # return devices[device_id]
190
-
191
- # def get_device(*, api_token):
192
-
193
- # if api_token == master_token:
194
- # # Check for the first empty device
195
- # for key, value in devices.items():
196
- # if value[1] == '' and value[2] == '':
197
- # return value[0]
198
-
199
- # return None
200
-
201
- # parsed = parse_mastertoken(api_token) # Special args for specific devices
202
- # if parsed:
203
- # device_id, force = parsed
204
-
205
- # if not device_exists(device_id):
206
- # return None
207
-
208
- # if force:
209
- # # Force access if the device exists (ignores reservation state)
210
- # return devices[device_id][0]
211
-
212
- # # Non-force: only if device exists AND is not reserved
213
- # if device_is_available(device_id):
214
- # return devices[device_id][0]
215
-
216
- # return None
217
-
218
- # # Regular Behavior
219
- # for key, value in devices.items():
220
- # if validate_token(value[1], value[2], api_token):
221
- # return value[0]
222
- # return None
223
-
224
- # def set_device(device_id, salt, hash):
225
- # devices[device_id] = (devices[device_id][0], salt, hash)
226
-
227
- # def set_pluto(ip='192.168.2.1'):
228
- # # try:
229
- # # global device_pluto
230
- # # del device_pluto
231
- # # device_pluto = adi.Pluto(f'ip:{ip}')
232
- # # except Exception as e:
233
- # # print(f"PlutoError: {e}")
234
- # # NO IMPLEMENTATION SHOUlD BE GIVEN, AS THIS IS FOR DEVICE SETUP. Keeping it just to support common Pluto code
235
- # pass
236
- # device_manager.py
237
- # device_manager.py
1
+ # src/remoteRF_server/server/device_manager.py
238
2
 
239
3
  from __future__ import annotations
240
4
 
5
+ import hashlib
6
+ from typing import Dict, Tuple, Optional, Any
7
+
241
8
  import os
242
9
  import re
243
10
  import subprocess
@@ -250,7 +17,7 @@ from contextlib import contextmanager
250
17
  import adi
251
18
 
252
19
  from ..common.utils import validate_token # your salt/hash validator
253
-
20
+ from ..host import host_tunnel_server as hts # gives access to hts.TUNNEL_REGISTRY
254
21
 
255
22
  # -----------------------------
256
23
  # Config paths (server-side)
@@ -266,6 +33,54 @@ def _cfg_dir() -> Path:
266
33
  def _devices_env_path() -> Path:
267
34
  return _cfg_dir() / "devices.env"
268
35
 
36
+ # -----------------------------
37
+ # Host stuff
38
+ # -----------------------------
39
+
40
+ def _get_host_registry():
41
+ # host_tunnel_server sets this when the tunnel server starts
42
+ return getattr(hts, "TUNNEL_REGISTRY", None)
43
+
44
+
45
+ def _host_devices_str_snapshot() -> Dict[int, str]:
46
+ """
47
+ Pull a FRESH host-device directory snapshot every call.
48
+ Assumes host device_ids are numeric strings that should be treated as gids.
49
+ """
50
+ reg = _get_host_registry()
51
+ if reg is None:
52
+ return {}
53
+
54
+ # ttl_ms=0 => force refresh (no caching)
55
+ snap = reg.device_directory_cached(ttl_ms=0) # device_id -> (host_id, info_obj, is_active)
56
+
57
+ out: Dict[int, str] = {}
58
+ for device_id, (host_id, info_obj, is_active) in snap.items():
59
+ # You said host device IDs are already globally unique => treat as gid.
60
+ try:
61
+ gid = int(str(device_id))
62
+ except Exception:
63
+ # If something non-numeric ever sneaks in, just skip it.
64
+ continue
65
+
66
+ local_id = int(getattr(info_obj, "local_id", 0) or 0)
67
+ label = str(getattr(info_obj, "label", "") or "").strip()
68
+ serial = str(getattr(info_obj, "serial", "") or "").strip()
69
+ kind = str(getattr(info_obj, "kind", "") or "").strip()
70
+
71
+ status = "online" if bool(is_active) else "offline"
72
+ name = label or f"host-device-{gid}"
73
+
74
+ # keep it human-readable for CLI/UI
75
+ out[gid] = (
76
+ f"{name} "
77
+ f"[host={host_id}, local_id={local_id}"
78
+ f"{', kind=' + kind if kind else ''}"
79
+ f"{', serial=' + serial if serial else ''}"
80
+ f"] ({status})"
81
+ )
82
+
83
+ return out
269
84
 
270
85
  # -----------------------------
271
86
  # Pluto helpers
@@ -536,8 +351,13 @@ def get_all_devices() -> Dict[int, Tuple[object, str, str]]:
536
351
  return out
537
352
 
538
353
  def get_all_devices_str() -> Dict[int, str]:
354
+ # local snapshot (fast)
539
355
  with _state_lock:
540
- return dict(devices_info)
356
+ out = dict(devices_info)
357
+
358
+ # host snapshot (fresh pull each call)
359
+ out.update(_host_devices_str_snapshot())
360
+ return out
541
361
 
542
362
  def set_device(device_id: int, salt: str, hash: str):
543
363
  # update ONLY reservation/auth state
@@ -549,9 +369,19 @@ def set_device(device_id: int, salt: str, hash: str):
549
369
  st.hsh = str(hash or "")
550
370
 
551
371
  def device_exists(device_id: int) -> bool:
372
+ did = int(device_id)
373
+
374
+ # local
552
375
  with _state_lock:
553
- st = _devices.get(int(device_id))
554
- return bool(st and st.dev is not None)
376
+ st = _devices.get(did)
377
+ if st and st.dev is not None:
378
+ return True
379
+
380
+ # host
381
+ reg = _get_host_registry()
382
+ if reg is None:
383
+ return False
384
+ return bool(reg.is_host_device(str(did)))
555
385
 
556
386
  def device_is_available(device_id: int) -> bool:
557
387
  with _state_lock:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.77
3
+ Version: 0.1.79
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -348,9 +348,11 @@ todo:
348
348
 
349
349
  <!-- HostRF meta data -> push upon connection + retry connection on host side. -->
350
350
 
351
- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.)
351
+ <!-- Host ID (NEEDS TO BE UNIQUE AND TELL THE HOST THAT) (or if device also does it as well.) -->
352
352
 
353
- ServerRF display meta data as part of devices list / reservation + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
353
+ ServerRF display meta data as part of devices list / reservation
354
+
355
+ + online/activity status (appended into device name) + (plus host side warning if the device id is NOT globally unique!)
354
356
 
355
357
  set_device token (virtual devices) + routing/relay to proper host (multiplexed)
356
358