RemoteRF 2.1.0.dev7__py3-none-any.whl → 2.1.2__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.
- remoteRF/common/grpc/v2_codec.py +1 -1
- remoteRF/core/app.py +20 -12
- remoteRF/core/direct_path.py +105 -40
- remoteRF/core/dynamic_v2_transport.py +91 -4
- remoteRF/core/grpc_acc.py +1 -1
- remoteRF/core/grpc_client.py +6 -8
- remoteRF/core/secure_channel.py +9 -0
- remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +14 -2
- remoteRF/drivers/dynamic_device.py +57 -21
- remoteRF/drivers/dynamic_v2.py +27 -38
- remoteRF/drivers/support/uhd_v2.py +9 -0
- remoteRF/remoterf_cli.py +17 -3
- remoteRF.py +39 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/METADATA +4 -16
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/RECORD +19 -18
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/WHEEL +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/entry_points.txt +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.2.dist-info}/top_level.txt +0 -0
remoteRF/common/grpc/v2_codec.py
CHANGED
|
@@ -48,7 +48,7 @@ def _json_safe(value):
|
|
|
48
48
|
if hasattr(value, "as_payload"):
|
|
49
49
|
return _json_safe(value.as_payload())
|
|
50
50
|
if isinstance(value, np.generic):
|
|
51
|
-
return value.item()
|
|
51
|
+
return _json_safe(value.item())
|
|
52
52
|
if isinstance(value, complex):
|
|
53
53
|
return {
|
|
54
54
|
_JSON_TYPE_KEY: "complex",
|
remoteRF/core/app.py
CHANGED
|
@@ -581,7 +581,8 @@ def _federated_reserve():
|
|
|
581
581
|
)
|
|
582
582
|
return
|
|
583
583
|
_pick_slot_and_reserve(dev["device_id"], dev["display_name"], dev["block_min"], _federated_reservations_for_range, _federated_reserve_slot,
|
|
584
|
-
device_label=f"{dev['display_name']} @{dev['deployment_name']}"
|
|
584
|
+
device_label=f"{dev['display_name']} @{dev['deployment_name']}",
|
|
585
|
+
max_days=-(-int(dev.get("max_horizon_sec") or 0) // 86400) or None)
|
|
585
586
|
|
|
586
587
|
|
|
587
588
|
def reservations():
|
|
@@ -985,7 +986,7 @@ def perms():
|
|
|
985
986
|
printf("Accessible Devices: ", (Sty.BOLD, Sty.BLUE), f"{devices}", Sty.MAGENTA)
|
|
986
987
|
|
|
987
988
|
# Build per-device caps and group identical limits together
|
|
988
|
-
buckets: dict[tuple[int, int], list[int]] = {} # (max_r, max_t_sec) -> [dev_ids]
|
|
989
|
+
buckets: dict[tuple[int, int, int], list[int]] = {} # (max_r, max_t_sec, horizon_sec) -> [dev_ids]
|
|
989
990
|
for d in devices:
|
|
990
991
|
try:
|
|
991
992
|
did = int(d)
|
|
@@ -994,7 +995,8 @@ def perms():
|
|
|
994
995
|
c = _cap_for(did)
|
|
995
996
|
max_t = int(c.get("max_reservation_time_sec", 0) or 0)
|
|
996
997
|
max_r = int(c.get("max_reservations", 0) or 0)
|
|
997
|
-
|
|
998
|
+
max_h = int(c.get("max_horizon_sec", 0) or 0)
|
|
999
|
+
buckets.setdefault((max_r, max_t, max_h), []).append(did)
|
|
998
1000
|
|
|
999
1001
|
if not buckets:
|
|
1000
1002
|
print("Limits per device: (none)")
|
|
@@ -1003,16 +1005,17 @@ def perms():
|
|
|
1003
1005
|
|
|
1004
1006
|
# If everything shares the same limits, print once
|
|
1005
1007
|
if len(buckets) == 1:
|
|
1006
|
-
(max_r, max_t), _devs = next(iter(buckets.items()))
|
|
1008
|
+
(max_r, max_t, max_h), _devs = next(iter(buckets.items()))
|
|
1007
1009
|
printf("Permissions:", (Sty.BOLD, Sty.BLUE))
|
|
1008
1010
|
printf(" Max Reservations: ", Sty.GRAY, f"{max_r}", Sty.CYAN)
|
|
1009
1011
|
printf(" Reservation Duration (min): ", Sty.GRAY, f"{max_t // 60}", Sty.CYAN)
|
|
1012
|
+
printf(" Days Ahead: ", Sty.GRAY, f"{max_h / 86400:g}" if max_h else "no limit", Sty.CYAN)
|
|
1010
1013
|
render_remote_permissions()
|
|
1011
1014
|
return
|
|
1012
1015
|
|
|
1013
1016
|
# Otherwise print grouped limits
|
|
1014
1017
|
printf("Permissions per device (grouped):", (Sty.BOLD, Sty.BLUE))
|
|
1015
|
-
for (max_r, max_t), devs in sorted(buckets.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[1])):
|
|
1018
|
+
for (max_r, max_t, max_h), devs in sorted(buckets.items(), key=lambda kv: (kv[0][0], kv[0][1], kv[0][2], kv[1])):
|
|
1016
1019
|
devs = sorted(devs)
|
|
1017
1020
|
|
|
1018
1021
|
# compress ranges like 0-3,5,7-9
|
|
@@ -1031,7 +1034,7 @@ def perms():
|
|
|
1031
1034
|
ranges.append(f"{start}-{prev}" if start != prev else f"{start}")
|
|
1032
1035
|
|
|
1033
1036
|
dev_str = ",".join(ranges)
|
|
1034
|
-
printf(" devices[", Sty.GRAY, f"{dev_str}", Sty.MAGENTA, "]: ", Sty.GRAY, f"max_reservations={max_r}, max_time_min={max_t // 60}", Sty.CYAN)
|
|
1037
|
+
printf(" devices[", Sty.GRAY, f"{dev_str}", Sty.MAGENTA, "]: ", Sty.GRAY, f"max_reservations={max_r}, max_time_min={max_t // 60}, days_ahead={f'{max_h / 86400:g}' if max_h else 'no limit'}", Sty.CYAN)
|
|
1035
1038
|
|
|
1036
1039
|
render_remote_permissions()
|
|
1037
1040
|
|
|
@@ -1306,16 +1309,18 @@ def _auto_block_minutes(max_t_sec: int) -> int:
|
|
|
1306
1309
|
break
|
|
1307
1310
|
return best
|
|
1308
1311
|
|
|
1309
|
-
def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, reservations_for_range, do_reserve, *, device_label=None):
|
|
1312
|
+
def _pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, reservations_for_range, do_reserve, *, device_label=None, max_days=None):
|
|
1310
1313
|
"""Shared `resdev` slot picker: days -> free slots -> pick -> confirm -> reserve.
|
|
1311
1314
|
Native and federated homes differ only in how reservations are fetched and
|
|
1312
|
-
how the reservation is made (token vs. HOME handle).
|
|
1315
|
+
how the reservation is made (token vs. HOME handle). `max_days` is the
|
|
1316
|
+
group's reservation horizon; the server refuses starts beyond it anyway."""
|
|
1313
1317
|
device_label = chosen_device_id if device_label is None else device_label
|
|
1314
|
-
|
|
1318
|
+
limit = f" (up to {max_days})" if max_days else ""
|
|
1319
|
+
num_days_s = session.prompt(stylize(f"Enter the number of days to check for available reservations (starting today){limit}: ", (Sty.BOLD, Sty.GREEN))).strip()
|
|
1315
1320
|
try:
|
|
1316
1321
|
num_days = int(num_days_s)
|
|
1317
|
-
if num_days <= 0:
|
|
1318
|
-
print("Invalid number of days.")
|
|
1322
|
+
if num_days <= 0 or (max_days and num_days > max_days):
|
|
1323
|
+
print("Invalid number of days." if num_days <= 0 else f"Your group may reserve at most {max_days} days ahead.")
|
|
1319
1324
|
return
|
|
1320
1325
|
except ValueError:
|
|
1321
1326
|
print("Invalid input. Please enter a number.")
|
|
@@ -1501,6 +1506,7 @@ def interactive_reserve_next_days_auto():
|
|
|
1501
1506
|
printf("Devices:", Sty.BOLD)
|
|
1502
1507
|
block_by_dev: dict[str, int] = {}
|
|
1503
1508
|
max_by_dev: dict[str, int] = {}
|
|
1509
|
+
days_by_dev: dict[str, int] = {}
|
|
1504
1510
|
|
|
1505
1511
|
for idx, dev_id in enumerate(sorted_device_ids):
|
|
1506
1512
|
dev_name = unmap_arg(dev_resp.results[dev_id])
|
|
@@ -1510,6 +1516,7 @@ def interactive_reserve_next_days_auto():
|
|
|
1510
1516
|
c = _cap_for(int(dev_id))
|
|
1511
1517
|
try:
|
|
1512
1518
|
max_t_sec = int(c.get("max_reservation_time_sec", 0) or 0)
|
|
1519
|
+
days_by_dev[str(dev_id)] = -(-int(c.get("max_horizon_sec", 0) or 0) // 86400)
|
|
1513
1520
|
except Exception:
|
|
1514
1521
|
max_t_sec = 0
|
|
1515
1522
|
elif perm_level == "Power User" and power_user_max_t_sec is not None:
|
|
@@ -1562,7 +1569,8 @@ def interactive_reserve_next_days_auto():
|
|
|
1562
1569
|
)
|
|
1563
1570
|
return
|
|
1564
1571
|
|
|
1565
|
-
_pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, fetch_reservations_for_range, _native_reserve_slot
|
|
1572
|
+
_pick_slot_and_reserve(chosen_device_id, chosen_device_name, block_minutes, fetch_reservations_for_range, _native_reserve_slot,
|
|
1573
|
+
max_days=days_by_dev.get(chosen_device_id) or None)
|
|
1566
1574
|
|
|
1567
1575
|
except Exception as e:
|
|
1568
1576
|
print(f"Error: {e}")
|
remoteRF/core/direct_path.py
CHANGED
|
@@ -32,7 +32,8 @@ path; everything else still takes the home's.
|
|
|
32
32
|
When the answer names a *fixed port* (a Server with a reachable address
|
|
33
33
|
listening for QUIC on one admitted UDP port), the path dials it like any QUIC
|
|
34
34
|
server -- no punch, so it works behind any NAT -- and claims the session with
|
|
35
|
-
the secret the answer carried.
|
|
35
|
+
the secret the answer carried. The dial and the punch run at once; the first
|
|
36
|
+
to connect wins.
|
|
36
37
|
"""
|
|
37
38
|
|
|
38
39
|
from __future__ import annotations
|
|
@@ -112,7 +113,10 @@ def quiet_aioice_errors(loop) -> None:
|
|
|
112
113
|
|
|
113
114
|
The retry fires from a TimerHandle named for aioice's Transaction and
|
|
114
115
|
dies inside asyncio's own transport code once the socket is gone (an
|
|
115
|
-
AttributeError on the closed transport or its dropped loop).
|
|
116
|
+
AttributeError on the closed transport or its dropped loop).
|
|
117
|
+
|
|
118
|
+
Likewise aioquic's shielded connect waiter: a handshake that ran out of
|
|
119
|
+
time leaves it to fail with a ConnectionError nobody reads."""
|
|
116
120
|
default = loop.default_exception_handler
|
|
117
121
|
|
|
118
122
|
def handler(loop, context):
|
|
@@ -120,6 +124,8 @@ def quiet_aioice_errors(loop) -> None:
|
|
|
120
124
|
where = repr(context.get("handle", "")) + str(context.get("message", ""))
|
|
121
125
|
if isinstance(exc, (AttributeError, OSError)) and "Transaction.__retry" in where:
|
|
122
126
|
return
|
|
127
|
+
if isinstance(exc, ConnectionError) and "exception was never retrieved" in where:
|
|
128
|
+
return
|
|
123
129
|
default(context)
|
|
124
130
|
|
|
125
131
|
loop.set_exception_handler(handler)
|
|
@@ -189,6 +195,10 @@ class PathError(Exception):
|
|
|
189
195
|
"""The direct path could not carry this call and is down now."""
|
|
190
196
|
|
|
191
197
|
|
|
198
|
+
def _why(exc: BaseException) -> str:
|
|
199
|
+
return "timed out" if isinstance(exc, asyncio.TimeoutError) else f"{type(exc).__name__}: {exc}"
|
|
200
|
+
|
|
201
|
+
|
|
192
202
|
class _IceTransport:
|
|
193
203
|
"""The sendto() surface aioquic's protocol needs, on a nominated ICE pair."""
|
|
194
204
|
|
|
@@ -354,10 +364,7 @@ class DirectPath:
|
|
|
354
364
|
try:
|
|
355
365
|
await asyncio.wait_for(self._establish(), SETTLE_SECONDS)
|
|
356
366
|
except Exception as exc: # noqa: BLE001 - a failed punch is expected on some networks
|
|
357
|
-
self.reason = (
|
|
358
|
-
"timed out" if isinstance(exc, asyncio.TimeoutError)
|
|
359
|
-
else f"{type(exc).__name__}: {exc}"
|
|
360
|
-
)
|
|
367
|
+
self.reason = str(exc) if isinstance(exc, PathError) else _why(exc)
|
|
361
368
|
self.state = "down"
|
|
362
369
|
if "No matching server function" in self.reason:
|
|
363
370
|
self.state = "unavailable"
|
|
@@ -397,6 +404,10 @@ class DirectPath:
|
|
|
397
404
|
"ufrag": ice.local_username,
|
|
398
405
|
"pwd": ice.local_password,
|
|
399
406
|
"candidates": [candidate.to_sdp() for candidate in ice.local_candidates],
|
|
407
|
+
# This client punches at once (no fixed-port head start), so the
|
|
408
|
+
# Server may send its first check sooner; a Server that predates
|
|
409
|
+
# the flag ignores it.
|
|
410
|
+
"race": True,
|
|
400
411
|
}
|
|
401
412
|
answer = await loop.run_in_executor(None, self._offer, offer)
|
|
402
413
|
|
|
@@ -409,35 +420,68 @@ class DirectPath:
|
|
|
409
420
|
)
|
|
410
421
|
configuration.load_verify_locations(cadata=ca_pem)
|
|
411
422
|
|
|
423
|
+
# Both at once. The Server starts its checks the moment it answers,
|
|
424
|
+
# and a NAT admits them only once this side has sent its own: a punch
|
|
425
|
+
# held back for the fixed dial (filtered from off campus) misses that
|
|
426
|
+
# window. On campus the fixed port still wins in milliseconds.
|
|
427
|
+
racers: dict[str, asyncio.Task] = {}
|
|
412
428
|
if answer.get("udp") and answer.get("session"):
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
else:
|
|
418
|
-
await close_ice(ice)
|
|
419
|
-
self._ice = None
|
|
420
|
-
return
|
|
421
|
-
ice.remote_username = str(answer["ufrag"])
|
|
422
|
-
ice.remote_password = str(answer["pwd"])
|
|
423
|
-
for sdp in answer["candidates"]:
|
|
424
|
-
await ice.add_remote_candidate(Candidate.from_sdp(str(sdp)))
|
|
425
|
-
await ice.add_remote_candidate(None)
|
|
426
|
-
await asyncio.wait_for(ice.connect(), ICE_SECONDS)
|
|
427
|
-
peer = peer_address(ice)
|
|
428
|
-
protocol = _Protocol(QuicConnection(configuration=configuration))
|
|
429
|
-
self._transport = _IceTransport(ice)
|
|
430
|
-
protocol.connection_made(self._transport)
|
|
431
|
-
self._tasks = [
|
|
432
|
-
asyncio.ensure_future(self._pump(ice, protocol, peer)),
|
|
433
|
-
asyncio.ensure_future(self._watch(protocol)),
|
|
434
|
-
asyncio.ensure_future(self._watch_sleep()),
|
|
435
|
-
]
|
|
436
|
-
protocol.connect(peer)
|
|
429
|
+
racers["fixed port"] = asyncio.ensure_future(self._dial_fixed(answer, configuration))
|
|
430
|
+
racers["punch"] = asyncio.ensure_future(self._punch(ice, answer, configuration))
|
|
431
|
+
failures: dict[asyncio.Task, Exception] = {}
|
|
432
|
+
pending = set(racers.values())
|
|
437
433
|
try:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
434
|
+
while pending:
|
|
435
|
+
done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
|
|
436
|
+
for task in done:
|
|
437
|
+
try:
|
|
438
|
+
task.result()
|
|
439
|
+
except Exception as exc: # noqa: BLE001 - filtered or refused: the other may still win
|
|
440
|
+
failures[task] = exc
|
|
441
|
+
else:
|
|
442
|
+
return
|
|
443
|
+
finally:
|
|
444
|
+
await cancel_tasks(pending)
|
|
445
|
+
if len(failures) == 1:
|
|
446
|
+
raise next(iter(failures.values()))
|
|
447
|
+
raise PathError("; ".join(f"{name}: {_why(failures[task])}" for name, task in racers.items()))
|
|
448
|
+
|
|
449
|
+
async def _punch(self, ice: Connection, answer: dict, configuration: QuicConfiguration) -> None:
|
|
450
|
+
"""QUIC over the pair ICE nominates; the ICE goes with it when this loses."""
|
|
451
|
+
tasks: list[asyncio.Task] = []
|
|
452
|
+
transport = None
|
|
453
|
+
try:
|
|
454
|
+
ice.remote_username = str(answer["ufrag"])
|
|
455
|
+
ice.remote_password = str(answer["pwd"])
|
|
456
|
+
for sdp in answer["candidates"]:
|
|
457
|
+
await ice.add_remote_candidate(Candidate.from_sdp(str(sdp)))
|
|
458
|
+
await ice.add_remote_candidate(None)
|
|
459
|
+
await asyncio.wait_for(ice.connect(), ICE_SECONDS)
|
|
460
|
+
peer = peer_address(ice)
|
|
461
|
+
protocol = _Protocol(QuicConnection(configuration=configuration))
|
|
462
|
+
transport = _IceTransport(ice)
|
|
463
|
+
protocol.connection_made(transport)
|
|
464
|
+
tasks = [
|
|
465
|
+
asyncio.ensure_future(self._pump(ice, protocol, peer)),
|
|
466
|
+
asyncio.ensure_future(self._watch(protocol)),
|
|
467
|
+
asyncio.ensure_future(self._watch_sleep()),
|
|
468
|
+
]
|
|
469
|
+
protocol.connect(peer)
|
|
470
|
+
try:
|
|
471
|
+
await asyncio.wait_for(protocol.wait_connected(), QUIC_SECONDS)
|
|
472
|
+
except ConnectionError:
|
|
473
|
+
raise ConnectionError(f"QUIC handshake refused: {protocol.terminated}") from None
|
|
474
|
+
if self._protocol is not None:
|
|
475
|
+
raise PathError("the fixed port connected first")
|
|
476
|
+
except BaseException:
|
|
477
|
+
await cancel_tasks(tasks) # before the close, or _watch reports it as a death
|
|
478
|
+
if transport is not None:
|
|
479
|
+
protocol.close()
|
|
480
|
+
transport.close()
|
|
481
|
+
await close_ice(ice)
|
|
482
|
+
self._ice = None
|
|
483
|
+
raise
|
|
484
|
+
self._transport, self._tasks = transport, tasks
|
|
441
485
|
self._protocol, self.remote = protocol, peer
|
|
442
486
|
|
|
443
487
|
async def _dial_fixed(self, answer: dict, configuration: QuicConfiguration) -> None:
|
|
@@ -457,6 +501,8 @@ class DirectPath:
|
|
|
457
501
|
reply = json.loads(await asyncio.wait_for(read_frame(reader), FIXED_SECONDS))
|
|
458
502
|
if not reply.get("ok"):
|
|
459
503
|
raise PathError(str(reply.get("a", "session refused")))
|
|
504
|
+
if self._protocol is not None:
|
|
505
|
+
raise PathError("the punch connected first")
|
|
460
506
|
except BaseException:
|
|
461
507
|
protocol.close()
|
|
462
508
|
transport.close()
|
|
@@ -595,21 +641,30 @@ def _stored_login() -> Optional[dict]:
|
|
|
595
641
|
return homes.recall_login(profile.home) if profile.home else None
|
|
596
642
|
|
|
597
643
|
|
|
598
|
-
def
|
|
599
|
-
"""
|
|
644
|
+
def _active_route(profile) -> Optional[dict]:
|
|
645
|
+
"""The active home's route this process is connected over, if it has one."""
|
|
600
646
|
from ..deployment import homes
|
|
601
|
-
from .grpc_client import _current_profile
|
|
602
647
|
|
|
603
|
-
profile = _current_profile()
|
|
604
648
|
if not profile.home:
|
|
605
649
|
return None
|
|
606
|
-
|
|
650
|
+
return next(
|
|
607
651
|
(r for r in homes.load_home(profile.home)["routes"]
|
|
608
652
|
if f"{r['host']}:{r['port']}" == profile.grpc_endpoint),
|
|
609
653
|
None,
|
|
610
654
|
)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _from_stored_login() -> Optional[DirectPath]:
|
|
658
|
+
"""A path for a process that never logged in (a script using a driver)."""
|
|
659
|
+
from ..deployment import homes
|
|
660
|
+
from .grpc_client import _current_profile
|
|
661
|
+
|
|
662
|
+
profile = _current_profile()
|
|
663
|
+
route = _active_route(profile)
|
|
664
|
+
if route is None or route["kind"] != "relay":
|
|
665
|
+
return None
|
|
611
666
|
login = homes.recall_login(profile.home)
|
|
612
|
-
if
|
|
667
|
+
if login is None:
|
|
613
668
|
return None
|
|
614
669
|
return DirectPath(
|
|
615
670
|
offer=_offer_rpc(login["username"], login["secret"]), server_name=route["host"],
|
|
@@ -690,7 +745,17 @@ def describe() -> str:
|
|
|
690
745
|
if not enabled():
|
|
691
746
|
return "relay (direct path disabled: REMOTERF_DIRECT=0)"
|
|
692
747
|
path = _path
|
|
693
|
-
|
|
748
|
+
if path is not None:
|
|
749
|
+
return path.describe()
|
|
750
|
+
try:
|
|
751
|
+
from .grpc_client import _current_profile
|
|
752
|
+
|
|
753
|
+
route = _active_route(_current_profile())
|
|
754
|
+
except Exception: # noqa: BLE001 - not configured, no home: nothing to add
|
|
755
|
+
route = None
|
|
756
|
+
if route is None or route["kind"] == "relay":
|
|
757
|
+
return "relay (direct path off)"
|
|
758
|
+
return "LAN (direct)" if route["kind"] == "lan" else str(route["kind"])
|
|
694
759
|
|
|
695
760
|
|
|
696
761
|
def stop() -> None:
|
|
@@ -28,6 +28,7 @@ from ..common.grpc.v2_codec import decode_value, encode_value
|
|
|
28
28
|
from .v2_errors import (
|
|
29
29
|
RemoteRFError,
|
|
30
30
|
RemoteRFProtocolError,
|
|
31
|
+
RemoteRFReservationError,
|
|
31
32
|
RemoteRFTransportError,
|
|
32
33
|
raise_for_envelope,
|
|
33
34
|
)
|
|
@@ -75,6 +76,55 @@ def _payload(value):
|
|
|
75
76
|
return value
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class _DevicePlaneStub:
|
|
80
|
+
"""DynamicControlV2 and SampleDataV1 for a partner lab's device: each
|
|
81
|
+
request is one ``DynamicV2:<Operation>:WIRE`` device RPC, routed like any
|
|
82
|
+
call on that token (the direct path to the owner when it is up, else the
|
|
83
|
+
HOME, which relays it). The owner runs it through its own servicers."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, token: str):
|
|
86
|
+
self._token = str(token)
|
|
87
|
+
|
|
88
|
+
def _rpc(self, operation, request, response_type):
|
|
89
|
+
from ..common.utils import map_arg, unmap_arg
|
|
90
|
+
from .grpc_client import rpc_client
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
response = rpc_client(
|
|
94
|
+
function_name=f"DynamicV2:{operation}:WIRE",
|
|
95
|
+
args={"a": map_arg(self._token), "wire": map_arg(request.SerializeToString())},
|
|
96
|
+
)
|
|
97
|
+
except RuntimeError as exc:
|
|
98
|
+
# A refusal the HOME or the owner carried in "a": no live
|
|
99
|
+
# reservation, a revoked agreement, a device no longer visible.
|
|
100
|
+
raise RemoteRFReservationError(f"partner device: {exc}") from exc
|
|
101
|
+
wire = unmap_arg(response.results["wire"]) if "wire" in response.results else None
|
|
102
|
+
if not isinstance(wire, bytes):
|
|
103
|
+
raise RemoteRFProtocolError("partner device plane returned no Dynamic v2 reply")
|
|
104
|
+
return response_type.FromString(wire)
|
|
105
|
+
|
|
106
|
+
def Negotiate(self, request, timeout=None):
|
|
107
|
+
return self._rpc("Negotiate", request, grpc_pb2.NegotiateResponse)
|
|
108
|
+
|
|
109
|
+
def GetSchema(self, request, timeout=None):
|
|
110
|
+
return self._rpc("GetSchema", request, grpc_pb2.GetSchemaResponse)
|
|
111
|
+
|
|
112
|
+
def OpenSession(self, request, timeout=None):
|
|
113
|
+
return self._rpc("OpenSession", request, grpc_pb2.OpenSessionResponse)
|
|
114
|
+
|
|
115
|
+
def Invoke(self, request, timeout=None):
|
|
116
|
+
return self._rpc("Invoke", request, grpc_pb2.InvokeResponse)
|
|
117
|
+
|
|
118
|
+
def CloseHandle(self, request, timeout=None):
|
|
119
|
+
return self._rpc("CloseHandle", request, grpc_pb2.CloseResponse)
|
|
120
|
+
|
|
121
|
+
def CloseSession(self, request, timeout=None):
|
|
122
|
+
return self._rpc("CloseSession", request, grpc_pb2.CloseResponse)
|
|
123
|
+
|
|
124
|
+
def SampleStream(self, frames, timeout=None):
|
|
125
|
+
return [self._rpc("SampleFrame", frame, grpc_pb2.SampleFrame) for frame in frames]
|
|
126
|
+
|
|
127
|
+
|
|
78
128
|
class DynamicV2Transport:
|
|
79
129
|
def __init__(
|
|
80
130
|
self,
|
|
@@ -82,15 +132,21 @@ class DynamicV2Transport:
|
|
|
82
132
|
control_stub=None,
|
|
83
133
|
sample_stub=None,
|
|
84
134
|
control_timeout_sec: float = 30.0,
|
|
135
|
+
open_timeout_sec: float = 120.0,
|
|
85
136
|
stream_timeout_margin_sec: float = 5.0,
|
|
86
137
|
):
|
|
87
138
|
self.control_timeout_sec = float(control_timeout_sec)
|
|
139
|
+
# A cold USRP open loads firmware/FPGA (B2xx re-enumerates on USB),
|
|
140
|
+
# which can outlast the normal control deadline.
|
|
141
|
+
self.open_timeout_sec = float(open_timeout_sec)
|
|
88
142
|
self.stream_timeout_margin_sec = float(stream_timeout_margin_sec)
|
|
89
143
|
if (
|
|
90
144
|
not math.isfinite(self.control_timeout_sec)
|
|
91
145
|
or self.control_timeout_sec <= 0
|
|
92
146
|
):
|
|
93
147
|
raise ValueError("control_timeout_sec must be finite and positive")
|
|
148
|
+
if not math.isfinite(self.open_timeout_sec) or self.open_timeout_sec <= 0:
|
|
149
|
+
raise ValueError("open_timeout_sec must be finite and positive")
|
|
94
150
|
if (
|
|
95
151
|
not math.isfinite(self.stream_timeout_margin_sec)
|
|
96
152
|
or self.stream_timeout_margin_sec <= 0
|
|
@@ -107,9 +163,35 @@ class DynamicV2Transport:
|
|
|
107
163
|
self.control = control_stub
|
|
108
164
|
self.samples = sample_stub
|
|
109
165
|
|
|
110
|
-
def
|
|
166
|
+
def _to_owner(self, token: str, owner: str | None = None) -> bool:
|
|
167
|
+
"""Carry this transport over the device plane when ``token`` drives a
|
|
168
|
+
partner lab's device (named by the HOME's refusal, or already known)."""
|
|
169
|
+
from . import direct_path
|
|
170
|
+
|
|
171
|
+
if isinstance(self.control, _DevicePlaneStub):
|
|
172
|
+
return False
|
|
173
|
+
owner = owner or direct_path.partner_of(str(token))
|
|
174
|
+
if not owner:
|
|
175
|
+
return False
|
|
176
|
+
# Marked, the token's device calls take the direct path to the owner.
|
|
177
|
+
direct_path.mark_partner(str(token), str(owner))
|
|
178
|
+
self.control = self.samples = _DevicePlaneStub(token)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def _checked(self, response, token: str) -> bool:
|
|
182
|
+
"""raise_for_envelope, except for the refusal of a partner's token:
|
|
183
|
+
True when the call should be repeated over the device plane."""
|
|
184
|
+
try:
|
|
185
|
+
raise_for_envelope(response.error)
|
|
186
|
+
except RemoteRFProtocolError as exc:
|
|
187
|
+
if self._to_owner(token, exc.details.get("federated")):
|
|
188
|
+
return True
|
|
189
|
+
raise
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def _call(self, fn, request, *, timeout: float | None = None):
|
|
111
193
|
try:
|
|
112
|
-
return fn(request, timeout=self.control_timeout_sec)
|
|
194
|
+
return fn(request, timeout=timeout or self.control_timeout_sec)
|
|
113
195
|
except grpc.RpcError as exc:
|
|
114
196
|
raise RemoteRFTransportError(
|
|
115
197
|
f"Dynamic v2 RPC failed: {exc.code().name}: {exc.details()}",
|
|
@@ -141,6 +223,7 @@ class DynamicV2Transport:
|
|
|
141
223
|
return response
|
|
142
224
|
|
|
143
225
|
def get_schema(self, token: str) -> dict:
|
|
226
|
+
self._to_owner(token)
|
|
144
227
|
self.negotiate()
|
|
145
228
|
response = self._call(
|
|
146
229
|
self.control.GetSchema,
|
|
@@ -149,7 +232,8 @@ class DynamicV2Transport:
|
|
|
149
232
|
schema_versions=[SCHEMA_VERSION],
|
|
150
233
|
),
|
|
151
234
|
)
|
|
152
|
-
|
|
235
|
+
if self._checked(response, token):
|
|
236
|
+
return self.get_schema(token)
|
|
153
237
|
try:
|
|
154
238
|
schema = json.loads(response.schema_json)
|
|
155
239
|
except (TypeError, json.JSONDecodeError) as exc:
|
|
@@ -165,6 +249,7 @@ class DynamicV2Transport:
|
|
|
165
249
|
return schema
|
|
166
250
|
|
|
167
251
|
def open_session(self, token: str, schema_hash: str):
|
|
252
|
+
self._to_owner(token)
|
|
168
253
|
response = self._call(
|
|
169
254
|
self.control.OpenSession,
|
|
170
255
|
grpc_pb2.OpenSessionRequest(
|
|
@@ -173,8 +258,10 @@ class DynamicV2Transport:
|
|
|
173
258
|
control_protocol_version=CONTROL_PROTOCOL_VERSION,
|
|
174
259
|
streaming_protocol_version=STREAMING_PROTOCOL_VERSION,
|
|
175
260
|
),
|
|
261
|
+
timeout=self.open_timeout_sec,
|
|
176
262
|
)
|
|
177
|
-
|
|
263
|
+
if self._checked(response, token):
|
|
264
|
+
return self.open_session(token, schema_hash)
|
|
178
265
|
try:
|
|
179
266
|
schema = json.loads(response.schema_json)
|
|
180
267
|
capabilities = json.loads(response.capabilities_json or "{}")
|
remoteRF/core/grpc_acc.py
CHANGED
|
@@ -110,7 +110,7 @@ class RemoteRFAccount:
|
|
|
110
110
|
token = unmap_arg(response.results["Token"])
|
|
111
111
|
try:
|
|
112
112
|
from ..drivers.dynamic_device import install_driver
|
|
113
|
-
install_driver(device_id=device_id)
|
|
113
|
+
install_driver(token=token, device_id=device_id)
|
|
114
114
|
except Exception as e:
|
|
115
115
|
print(f"Warning: could not install driver for device {device_id}: {e}")
|
|
116
116
|
return token
|
remoteRF/core/grpc_client.py
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
16
|
import os
|
|
17
|
-
import sys
|
|
18
17
|
from dataclasses import dataclass
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
from typing import Any, Dict, Optional
|
|
@@ -31,7 +30,7 @@ _CONFIG_HELP = (
|
|
|
31
30
|
"Run:\n"
|
|
32
31
|
" remoterf --config --addr <host:port>\n"
|
|
33
32
|
"Example:\n"
|
|
34
|
-
" remoterf --config --addr
|
|
33
|
+
" remoterf --config --addr 203.0.113.10:12321"
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
|
|
@@ -229,10 +228,11 @@ def rpc_client(*, function_name, args, connection=None):
|
|
|
229
228
|
ref = _federated_ref(function_name, args) if connection is None else None
|
|
230
229
|
if ref is not None:
|
|
231
230
|
return _finish(_federated_call(ref, function_name, args))
|
|
232
|
-
wire = os.getenv("REMOTERF_RX_WIRE", "").strip().lower()
|
|
231
|
+
wire = os.getenv("REMOTERF_RX_WIRE", "i16").strip().lower()
|
|
233
232
|
if wire and function_name.endswith(":rx:CALL0"):
|
|
234
|
-
# Gate I slice 3
|
|
235
|
-
# them when they are integer IQ; the driver still sees a complex
|
|
233
|
+
# Gate I slice 3: c64 halves the samples on the wire, i16 (the default)
|
|
234
|
+
# quarters them when they are integer IQ; the driver still sees a complex
|
|
235
|
+
# array. REMOTERF_RX_WIRE="" or "c128" opts out (servers ignore unknown).
|
|
236
236
|
args = {**args, "wire": map_arg(wire)}
|
|
237
237
|
if connection is None and direct_path.wants(function_name):
|
|
238
238
|
path = direct_path.current(token=_device_token(function_name, args))
|
|
@@ -265,9 +265,7 @@ def _finish(response):
|
|
|
265
265
|
|
|
266
266
|
if 'UE' in response.results:
|
|
267
267
|
print(f"UserError: {unmap_arg(response.results['UE'])}")
|
|
268
|
-
|
|
269
|
-
input("Hit enter to continue...")
|
|
270
|
-
|
|
268
|
+
|
|
271
269
|
if 'Message' in response.results:
|
|
272
270
|
print(f"{unmap_arg(response.results['Message'])}")
|
|
273
271
|
|
remoteRF/core/secure_channel.py
CHANGED
|
@@ -36,6 +36,15 @@ import grpc
|
|
|
36
36
|
DEFAULT_OPTIONS: Tuple[Tuple[str, int], ...] = (
|
|
37
37
|
("grpc.max_send_message_length", 100 * 1024 * 1024),
|
|
38
38
|
("grpc.max_receive_message_length", 100 * 1024 * 1024),
|
|
39
|
+
# A call in flight when the network changes under it (a laptop roaming to
|
|
40
|
+
# another access point) fails within ~30 s instead of waiting out TCP's
|
|
41
|
+
# own retransmission timeout, ~9 minutes on macOS. Pings go out only while
|
|
42
|
+
# a call is open, at most two per quiet stretch (gRPC's default), which
|
|
43
|
+
# even a Server that never raised its ping allowance accepts.
|
|
44
|
+
("grpc.keepalive_time_ms", 20_000),
|
|
45
|
+
("grpc.keepalive_timeout_ms", 10_000),
|
|
46
|
+
# The ack deadline a keepalive ping actually waits on (default 60 s).
|
|
47
|
+
("grpc.http2.ping_timeout_ms", 10_000),
|
|
39
48
|
)
|
|
40
49
|
|
|
41
50
|
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
16
|
# Auto-generated from IDL schema — do not edit by hand.
|
|
17
|
-
# device_type: adalm_pluto driver_version: 0.0.
|
|
17
|
+
# device_type: adalm_pluto driver_version: 0.0.3 schema_hash: sha256:35b66a1e4383a344046be27ff80d40aff3ebd147e7aa5dd0513303fd3c0cfab4
|
|
18
18
|
|
|
19
19
|
_PREFIX = "Adalm_pluto"
|
|
20
|
-
_SCHEMA_HASH = "sha256:
|
|
20
|
+
_SCHEMA_HASH = "sha256:35b66a1e4383a344046be27ff80d40aff3ebd147e7aa5dd0513303fd3c0cfab4"
|
|
21
21
|
_CLIENT_MODULES = {}
|
|
22
22
|
_CLIENT_OBJECTS = {}
|
|
23
23
|
|
|
@@ -375,6 +375,18 @@ class Pluto:
|
|
|
375
375
|
'Return a connection identifier for this device (ping / verify).'
|
|
376
376
|
return _try_call("ip", self.token)
|
|
377
377
|
|
|
378
|
+
def power_cycle(self):
|
|
379
|
+
'PoE Plutos: power off, then on, and wait for boot. No-op on other Plutos.'
|
|
380
|
+
return _try_call("power_cycle", self.token)
|
|
381
|
+
|
|
382
|
+
def power_down(self):
|
|
383
|
+
'PoE Plutos: cut power; the next command powers it back up. No-op on other Plutos.'
|
|
384
|
+
return _try_call("power_down", self.token)
|
|
385
|
+
|
|
386
|
+
def power_up(self):
|
|
387
|
+
'PoE Plutos: power on and wait for boot (any command does this when needed). No-op on other Plutos.'
|
|
388
|
+
return _try_call("power_up", self.token)
|
|
389
|
+
|
|
378
390
|
def rx(self):
|
|
379
391
|
return _try_call("rx", self.token)
|
|
380
392
|
|
|
@@ -36,6 +36,7 @@ from __future__ import annotations
|
|
|
36
36
|
|
|
37
37
|
import json
|
|
38
38
|
import keyword
|
|
39
|
+
import sys
|
|
39
40
|
from pathlib import Path
|
|
40
41
|
|
|
41
42
|
from ..common.utils import map_arg, unmap_arg
|
|
@@ -94,7 +95,7 @@ def fetch_idl(
|
|
|
94
95
|
# A HOME-minted device reference has no direct v2 control channel; the
|
|
95
96
|
# schema is fetched through the same relayed IDL RPC every driver uses.
|
|
96
97
|
federated = is_global_ref(token) or is_global_ref(device_id)
|
|
97
|
-
if token is not None and prefer_v2 and not federated:
|
|
98
|
+
if token is not None and device_id is None and prefer_v2 and not federated:
|
|
98
99
|
from .dynamic_v2 import fetch_schema_v2
|
|
99
100
|
from ..core.v2_errors import RemoteRFProtocolError, RemoteRFReservationError, RemoteRFTransportError
|
|
100
101
|
|
|
@@ -111,14 +112,19 @@ def fetch_idl(
|
|
|
111
112
|
raise
|
|
112
113
|
|
|
113
114
|
args: dict = {}
|
|
114
|
-
if
|
|
115
|
-
args['token'] = map_arg(str(token))
|
|
116
|
-
elif device_id is not None:
|
|
115
|
+
if device_id is not None:
|
|
117
116
|
args['device_id'] = map_arg(device_id if is_global_ref(device_id) else int(device_id))
|
|
117
|
+
elif token is not None:
|
|
118
|
+
args['token'] = map_arg(str(token))
|
|
118
119
|
elif device_name is not None:
|
|
119
120
|
args['device_name'] = map_arg(str(device_name))
|
|
120
121
|
else:
|
|
121
122
|
raise ValueError("fetch_idl requires token, device_id, or device_name")
|
|
123
|
+
if prefer_v2 and 'token' not in args and not federated:
|
|
124
|
+
# Servers that know v2 answer with it when the device publishes one
|
|
125
|
+
# (by device_id, which needs no active reservation); v1-only devices
|
|
126
|
+
# and older servers answer v1.
|
|
127
|
+
args['schema_versions'] = map_arg(["2.0"])
|
|
122
128
|
|
|
123
129
|
from ..core.grpc_client import rpc_client
|
|
124
130
|
|
|
@@ -126,13 +132,18 @@ def fetch_idl(
|
|
|
126
132
|
|
|
127
133
|
if 'error' in resp.results:
|
|
128
134
|
raise RuntimeError(f"IDL fetch failed: {unmap_arg(resp.results['error'])}")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
schema = json.loads(unmap_arg(resp.results['schema']))
|
|
136
|
+
if 'federated' in resp.results:
|
|
137
|
+
if token is not None:
|
|
138
|
+
# A partner lab answered: device calls with this token can punch to it.
|
|
139
|
+
from ..core import direct_path
|
|
134
140
|
|
|
135
|
-
|
|
141
|
+
direct_path.mark_partner(str(token), str(unmap_arg(resp.results['federated'])))
|
|
142
|
+
if str(schema.get("schema_version")) != "2.0":
|
|
143
|
+
# A partner's v1 answer may replace an installed v2 driver (see
|
|
144
|
+
# _write_v1_driver_files); a v2 schema is hashed, so left as is.
|
|
145
|
+
schema["federated"] = True
|
|
146
|
+
return schema
|
|
136
147
|
|
|
137
148
|
|
|
138
149
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1393,8 +1404,14 @@ _DRIVERS_DIR = Path(__file__).parent
|
|
|
1393
1404
|
|
|
1394
1405
|
|
|
1395
1406
|
def _write_driver_files(schema: dict) -> Path:
|
|
1396
|
-
if str(schema.get("schema_version")) == "2.0"
|
|
1397
|
-
|
|
1407
|
+
pkg_dir = _write_v2_driver_files(schema) if str(schema.get("schema_version")) == "2.0" else _write_v1_driver_files(schema)
|
|
1408
|
+
stale = f"remoteRF.drivers.{pkg_dir.name}"
|
|
1409
|
+
for name in [m for m in sys.modules if m == stale or m.startswith(stale + ".")]:
|
|
1410
|
+
del sys.modules[name]
|
|
1411
|
+
return pkg_dir
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def _write_v1_driver_files(schema: dict) -> Path:
|
|
1398
1415
|
|
|
1399
1416
|
(
|
|
1400
1417
|
device_type,
|
|
@@ -1409,8 +1426,14 @@ def _write_driver_files(schema: dict) -> Path:
|
|
|
1409
1426
|
_call_map,
|
|
1410
1427
|
) = _schema_maps(schema)
|
|
1411
1428
|
pkg_dir = _DRIVERS_DIR / device_type
|
|
1429
|
+
remote_path = pkg_dir / f"{device_type}_remote.py"
|
|
1430
|
+
if remote_path.exists() and _is_v2_driver(remote_path) and not schema.get("federated"):
|
|
1431
|
+
# The server answered v1 (older server, or a device that no longer
|
|
1432
|
+
# publishes v2); don't trade an installed Dynamic v2 driver for it.
|
|
1433
|
+
# A partner's device has only the v1 plane, so its answer does.
|
|
1434
|
+
return pkg_dir
|
|
1412
1435
|
pkg_dir.mkdir(exist_ok=True)
|
|
1413
|
-
|
|
1436
|
+
remote_path.write_text(_codegen(schema), encoding="utf-8")
|
|
1414
1437
|
init_lines = [
|
|
1415
1438
|
*_GPL_NOTICE_LINES,
|
|
1416
1439
|
"from importlib import import_module as _import_module",
|
|
@@ -1562,11 +1585,7 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1562
1585
|
device_name=device_name,
|
|
1563
1586
|
prefer_v2=True,
|
|
1564
1587
|
)
|
|
1565
|
-
device_type = (
|
|
1566
|
-
_require_package_name(schema.get("device_type"), field="device_type")
|
|
1567
|
-
if str(schema.get("schema_version")) == "2.0"
|
|
1568
|
-
else _schema_maps(schema)[0]
|
|
1569
|
-
)
|
|
1588
|
+
device_type = _schema_device_type(schema)
|
|
1570
1589
|
remote_path = _DRIVERS_DIR / device_type / f"{device_type}_remote.py"
|
|
1571
1590
|
init_path = remote_path.with_name("__init__.py")
|
|
1572
1591
|
|
|
@@ -1586,6 +1605,8 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1586
1605
|
current_hash = _read_schema_hash(remote_path)
|
|
1587
1606
|
if current_hash != schema.get("schema_hash"):
|
|
1588
1607
|
_write_driver_files(schema)
|
|
1608
|
+
if _read_schema_hash(remote_path) == current_hash:
|
|
1609
|
+
return
|
|
1589
1610
|
print(
|
|
1590
1611
|
f"Driver for '{device_type}' updated "
|
|
1591
1612
|
f"(was {(current_hash or '?')[:16]}…, "
|
|
@@ -1594,6 +1615,19 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1594
1615
|
)
|
|
1595
1616
|
|
|
1596
1617
|
|
|
1618
|
+
def _schema_device_type(schema: dict) -> str:
|
|
1619
|
+
if str(schema.get("schema_version")) == "2.0":
|
|
1620
|
+
return _require_package_name(schema.get("device_type"), field="device_type")
|
|
1621
|
+
return _schema_maps(schema)[0]
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def _is_v2_driver(path: Path) -> bool:
|
|
1625
|
+
try:
|
|
1626
|
+
return "build_uhd_bindings" in path.read_text(encoding="utf-8")
|
|
1627
|
+
except OSError:
|
|
1628
|
+
return False
|
|
1629
|
+
|
|
1630
|
+
|
|
1597
1631
|
def _read_schema_hash(path: Path) -> str | None:
|
|
1598
1632
|
"""Extract _SCHEMA_HASH value from a generated driver file without importing it."""
|
|
1599
1633
|
try:
|
|
@@ -1621,12 +1655,14 @@ def install_driver_if_stale(*, token: str, current_hash: str) -> bool:
|
|
|
1621
1655
|
|
|
1622
1656
|
from remoteRF.drivers.pluto import *
|
|
1623
1657
|
"""
|
|
1624
|
-
schema = fetch_idl(token=token, prefer_v2=
|
|
1658
|
+
schema = fetch_idl(token=token, prefer_v2=True)
|
|
1625
1659
|
if schema.get("schema_hash") == current_hash:
|
|
1626
1660
|
return False
|
|
1627
1661
|
|
|
1628
|
-
device_type =
|
|
1629
|
-
_write_driver_files(schema)
|
|
1662
|
+
device_type = _schema_device_type(schema)
|
|
1663
|
+
pkg_dir = _write_driver_files(schema)
|
|
1664
|
+
if _read_schema_hash(pkg_dir / f"{device_type}_remote.py") == current_hash:
|
|
1665
|
+
return False
|
|
1630
1666
|
print(
|
|
1631
1667
|
f"Driver for '{device_type}' updated "
|
|
1632
1668
|
f"(was {current_hash[:16]}…, now {schema.get('schema_hash','?')[:16]}…).\n"
|
remoteRF/drivers/dynamic_v2.py
CHANGED
|
@@ -21,7 +21,6 @@ import hashlib
|
|
|
21
21
|
import json
|
|
22
22
|
import keyword
|
|
23
23
|
import numbers
|
|
24
|
-
import re
|
|
25
24
|
import types
|
|
26
25
|
import weakref
|
|
27
26
|
|
|
@@ -83,24 +82,6 @@ _PARAMETER_KINDS = {
|
|
|
83
82
|
_PARAMETER_DIRECTIONS = {"in", "out", "inout"}
|
|
84
83
|
|
|
85
84
|
|
|
86
|
-
def _canonical_native_version(value) -> str:
|
|
87
|
-
"""Strip distro/build metadata while retaining the pinned upstream API."""
|
|
88
|
-
text = str(value or "").strip()
|
|
89
|
-
if text.lower().startswith("uhd "):
|
|
90
|
-
text = text[4:].strip()
|
|
91
|
-
match = re.match(
|
|
92
|
-
r"^(\d+)\.(\d+)\.(\d+)(?:\.(\d+|main|head))?",
|
|
93
|
-
text,
|
|
94
|
-
flags=re.IGNORECASE,
|
|
95
|
-
)
|
|
96
|
-
if match is None:
|
|
97
|
-
return text.partition("-")[0].strip()
|
|
98
|
-
major, minor, patch, build = match.groups()
|
|
99
|
-
if build is None or build.lower() in {"main", "head"}:
|
|
100
|
-
build = "0"
|
|
101
|
-
return ".".join((major, minor, patch, build))
|
|
102
|
-
|
|
103
|
-
|
|
104
85
|
def validate_schema_v2(schema: dict) -> dict:
|
|
105
86
|
if not isinstance(schema, dict) or schema.get("schema_version") != "2.0":
|
|
106
87
|
raise RemoteRFProtocolError("Dynamic schema_version must be '2.0'")
|
|
@@ -223,9 +204,13 @@ def _matches_type(value, wire_type: str) -> bool:
|
|
|
223
204
|
return True
|
|
224
205
|
if choice == "bytes" and isinstance(value, (bytes, bytearray, memoryview)):
|
|
225
206
|
return True
|
|
226
|
-
if
|
|
207
|
+
if (
|
|
208
|
+
choice == "complex"
|
|
209
|
+
and isinstance(value, (numbers.Complex, np.number))
|
|
210
|
+
and not isinstance(value, bool)
|
|
211
|
+
):
|
|
227
212
|
return True
|
|
228
|
-
if choice.startswith("list") and isinstance(value, (list, tuple)):
|
|
213
|
+
if choice.startswith("list") and isinstance(value, (list, tuple, np.ndarray)):
|
|
229
214
|
if "[" not in choice:
|
|
230
215
|
return True
|
|
231
216
|
item_type = choice.partition("[")[2].rpartition("]")[0]
|
|
@@ -295,7 +280,11 @@ class OverloadBinder:
|
|
|
295
280
|
else:
|
|
296
281
|
raise TypeError(f"missing required argument {name!r}")
|
|
297
282
|
|
|
298
|
-
if
|
|
283
|
+
if (
|
|
284
|
+
supplied
|
|
285
|
+
and not (value is None and not spec.get("required", True))
|
|
286
|
+
and not _matches_type(value, spec.get("type", "any"))
|
|
287
|
+
):
|
|
299
288
|
raise TypeError(
|
|
300
289
|
f"argument {name!r} must match {spec.get('type')}, "
|
|
301
290
|
f"not {type(value).__name__}"
|
|
@@ -678,23 +667,21 @@ def build_uhd_bindings(schema: dict, *, transport_factory=DynamicV2Transport):
|
|
|
678
667
|
self.token = str(token)
|
|
679
668
|
self._transport = transport_factory()
|
|
680
669
|
self._transport.negotiate()
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
):
|
|
691
|
-
try:
|
|
692
|
-
self._transport.close_session(opened["session_id"])
|
|
693
|
-
finally:
|
|
670
|
+
try:
|
|
671
|
+
opened = self._transport.open_session(self.token, schema["schema_hash"])
|
|
672
|
+
except RemoteRFProtocolError as exc:
|
|
673
|
+
if not exc.details.get("current"):
|
|
674
|
+
raise
|
|
675
|
+
from .dynamic_device import install_driver_if_stale
|
|
676
|
+
|
|
677
|
+
device_type = schema.get("device_type", "usrp")
|
|
678
|
+
if install_driver_if_stale(token=self.token, current_hash=schema["schema_hash"]):
|
|
694
679
|
raise RemoteRFProtocolError(
|
|
695
|
-
f"
|
|
696
|
-
f"
|
|
697
|
-
|
|
680
|
+
f"the server's {device_type} driver changed and the local copy "
|
|
681
|
+
f"was updated; reimport remoteRF.drivers.{device_type} and retry",
|
|
682
|
+
details=dict(exc.details),
|
|
683
|
+
) from exc
|
|
684
|
+
raise
|
|
698
685
|
self._session_id = opened["session_id"]
|
|
699
686
|
self._handle = opened["device_handle"]
|
|
700
687
|
self._generation = 0
|
|
@@ -833,6 +820,7 @@ def build_uhd_bindings(schema: dict, *, transport_factory=DynamicV2Transport):
|
|
|
833
820
|
Range=uhd_v2.Range,
|
|
834
821
|
MetaRange=uhd_v2.MetaRange,
|
|
835
822
|
TuneRequest=uhd_v2.TuneRequest,
|
|
823
|
+
TuneRequestPolicy=uhd_v2.TuneRequestPolicy,
|
|
836
824
|
TuneResult=uhd_v2.TuneResult,
|
|
837
825
|
StreamCMD=uhd_v2.StreamCMD,
|
|
838
826
|
StreamMode=uhd_v2.StreamMode,
|
|
@@ -864,6 +852,7 @@ def build_uhd_bindings(schema: dict, *, transport_factory=DynamicV2Transport):
|
|
|
864
852
|
time_spec=uhd_v2.TimeSpec,
|
|
865
853
|
device_addr=uhd_v2.DeviceAddr,
|
|
866
854
|
tune_request=uhd_v2.TuneRequest,
|
|
855
|
+
tune_request_policy=uhd_v2.TuneRequestPolicy,
|
|
867
856
|
tune_result=uhd_v2.TuneResult,
|
|
868
857
|
)
|
|
869
858
|
)
|
|
@@ -230,6 +230,10 @@ class TuneRequestPolicy(str, Enum):
|
|
|
230
230
|
NONE = "NONE"
|
|
231
231
|
AUTO = "AUTO"
|
|
232
232
|
MANUAL = "MANUAL"
|
|
233
|
+
# UHD's own spellings: uhd.types.TuneRequestPolicy.manual
|
|
234
|
+
none = "NONE"
|
|
235
|
+
auto = "AUTO"
|
|
236
|
+
manual = "MANUAL"
|
|
233
237
|
|
|
234
238
|
|
|
235
239
|
class TuneRequest(_ValueObject):
|
|
@@ -246,6 +250,11 @@ class TuneRequest(_ValueObject):
|
|
|
246
250
|
self.dsp_freq = 0.0
|
|
247
251
|
self.rf_freq_policy = TuneRequestPolicy.AUTO
|
|
248
252
|
self.dsp_freq_policy = TuneRequestPolicy.AUTO
|
|
253
|
+
if self.lo_offset is not None:
|
|
254
|
+
# As UHD's tune_request_t(target, lo_off): the RF LO sits manually
|
|
255
|
+
# at target + offset and the DSP takes up the difference.
|
|
256
|
+
self.rf_freq_policy = TuneRequestPolicy.MANUAL
|
|
257
|
+
self.rf_freq = self.target_freq + self.lo_offset
|
|
249
258
|
self.args = DeviceAddr()
|
|
250
259
|
|
|
251
260
|
def as_payload(self):
|
remoteRF/remoterf_cli.py
CHANGED
|
@@ -114,7 +114,7 @@ def _ensure_config_present() -> tuple[bool, str]:
|
|
|
114
114
|
"Run:\n"
|
|
115
115
|
" remoterf --config --addr <host:port>\n"
|
|
116
116
|
"Example:\n"
|
|
117
|
-
" remoterf --config --addr
|
|
117
|
+
" remoterf --config --addr 203.0.113.10:12321\n",
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
kv = _read_dotenv_kv(env_file)
|
|
@@ -158,6 +158,7 @@ def print_help() -> None:
|
|
|
158
158
|
printf("Config:", (Sty.BOLD, Sty.MAGENTA))
|
|
159
159
|
printf(" remoterf -c | --config [options]", Sty.CYAN)
|
|
160
160
|
printf(" -a, --addr, -addr <host:port>", Sty.CYAN, " Set target server", Sty.DEFAULT)
|
|
161
|
+
printf(" --cert-port <port>", Sty.CYAN, " Cert server port (default: grpc port + 1)", Sty.DEFAULT)
|
|
161
162
|
printf(" -w, --wipe, -wipe", Sty.CYAN, " Delete all local config", Sty.DEFAULT)
|
|
162
163
|
printf(" -y, --yes, -yes", Sty.CYAN, " Skip wipe confirmation", Sty.DEFAULT)
|
|
163
164
|
printf(" -r, --register", Sty.CYAN, " Register right after configuring", Sty.DEFAULT)
|
|
@@ -172,7 +173,7 @@ def print_help() -> None:
|
|
|
172
173
|
printf(" remoterf --login global", Sty.GREEN)
|
|
173
174
|
printf(" remoterf homes", Sty.GREEN)
|
|
174
175
|
printf(" remoterf --version", Sty.GREEN)
|
|
175
|
-
printf(" remoterf --config --addr
|
|
176
|
+
printf(" remoterf --config --addr 203.0.113.10:12321", Sty.GREEN)
|
|
176
177
|
printf(" remoterf --config --addr global.remoterf.net --register", Sty.GREEN)
|
|
177
178
|
printf(" remoterf --config --wipe", Sty.GREEN)
|
|
178
179
|
printf(" remoterf --config --wipe --yes", Sty.GREEN)
|
|
@@ -460,6 +461,7 @@ def main() -> int:
|
|
|
460
461
|
yes = False
|
|
461
462
|
register = False
|
|
462
463
|
account_transport = None
|
|
464
|
+
cert_port_arg = None
|
|
463
465
|
|
|
464
466
|
i = 1
|
|
465
467
|
while i < len(argv):
|
|
@@ -481,6 +483,18 @@ def main() -> int:
|
|
|
481
483
|
i += 2
|
|
482
484
|
continue
|
|
483
485
|
|
|
486
|
+
if tok in ("--cert-port", "-cert-port"):
|
|
487
|
+
if i + 1 >= len(argv):
|
|
488
|
+
print("ERROR: missing required argument after --cert-port")
|
|
489
|
+
return 2
|
|
490
|
+
try:
|
|
491
|
+
cert_port_arg = int(argv[i + 1].strip())
|
|
492
|
+
except Exception:
|
|
493
|
+
print("ERROR: --cert-port must be an integer")
|
|
494
|
+
return 2
|
|
495
|
+
i += 2
|
|
496
|
+
continue
|
|
497
|
+
|
|
484
498
|
if tok in ("--wipe", "-w", "-wipe"):
|
|
485
499
|
wipe = True
|
|
486
500
|
i += 1
|
|
@@ -549,7 +563,7 @@ def main() -> int:
|
|
|
549
563
|
return 2
|
|
550
564
|
|
|
551
565
|
# configure returns the proper exit code
|
|
552
|
-
cert_port = port + 1
|
|
566
|
+
cert_port = cert_port_arg if cert_port_arg is not None else port + 1
|
|
553
567
|
result = configure(host, port, cert_port)
|
|
554
568
|
if result not in (None, 0):
|
|
555
569
|
return int(result)
|
remoteRF.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (C) 2026 RemoteRF
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""Rescue for an upgrade from remoterf 2.0.7-2.0.13 on Windows and macOS.
|
|
17
|
+
|
|
18
|
+
Those releases installed the package in a lowercase ``remoterf`` folder. When
|
|
19
|
+
that folder survives an upgrade (it holds the generated drivers pip does not
|
|
20
|
+
know about), the new files land inside it, and on a case-insensitive disk
|
|
21
|
+
``import remoteRF`` no longer finds the package. Python only reaches this
|
|
22
|
+
module then: a correctly named ``remoteRF`` folder always takes precedence.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import importlib.util
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "remoterf")
|
|
30
|
+
_SPEC = importlib.util.spec_from_file_location(
|
|
31
|
+
__name__, os.path.join(_FOLDER, "__init__.py"), submodule_search_locations=[_FOLDER]
|
|
32
|
+
)
|
|
33
|
+
if _SPEC is None or not os.path.isfile(os.path.join(_FOLDER, "__init__.py")):
|
|
34
|
+
raise ImportError("the remoteRF package folder is missing; reinstall with: pip install --force-reinstall remoterf")
|
|
35
|
+
_MODULE = importlib.util.module_from_spec(_SPEC)
|
|
36
|
+
# The import system returns whatever this name maps to once this module has
|
|
37
|
+
# run: the package itself, so ``remoteRF.<submodule>`` resolves inside it.
|
|
38
|
+
sys.modules[__name__] = _MODULE
|
|
39
|
+
_SPEC.loader.exec_module(_MODULE)
|
|
@@ -1,39 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
|
-
Author: Ethan Ge
|
|
6
|
-
|
|
7
|
-
License: GPL-3.0-or-later
|
|
5
|
+
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
8
7
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
10
8
|
Classifier: Operating System :: OS Independent
|
|
11
9
|
Requires-Python: >=3.10
|
|
12
10
|
Description-Content-Type: text/markdown
|
|
13
11
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: remoterf-federation-core==0.
|
|
12
|
+
Requires-Dist: remoterf-federation-core==0.16.0
|
|
15
13
|
Requires-Dist: grpcio<2.0.0,>=1.78.1
|
|
16
14
|
Requires-Dist: protobuf<7.0.0,>=6.31.1
|
|
17
15
|
Requires-Dist: numpy
|
|
18
16
|
Requires-Dist: prompt_toolkit
|
|
19
17
|
Requires-Dist: python-dotenv
|
|
20
|
-
Requires-Dist: prompt-toolkit
|
|
21
18
|
Requires-Dist: httpx<1.0,>=0.27
|
|
22
19
|
Requires-Dist: certifi
|
|
23
20
|
Requires-Dist: pydantic<3.0,>=2.6
|
|
24
21
|
Requires-Dist: keyring<26,>=24
|
|
25
22
|
Requires-Dist: aioice<0.11,>=0.10
|
|
26
23
|
Requires-Dist: aioquic<2.0,>=1.2
|
|
27
|
-
Dynamic: author
|
|
28
|
-
Dynamic: author-email
|
|
29
|
-
Dynamic: classifier
|
|
30
|
-
Dynamic: description
|
|
31
|
-
Dynamic: description-content-type
|
|
32
|
-
Dynamic: license
|
|
33
24
|
Dynamic: license-file
|
|
34
|
-
Dynamic: requires-dist
|
|
35
|
-
Dynamic: requires-python
|
|
36
|
-
Dynamic: summary
|
|
37
25
|
|
|
38
26
|
# Remote RF
|
|
39
27
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
remoteRF.py,sha256=ANpEl1Hg7HpGji6UZP6SzHORqE1v4S7SzsZzWFiG6Es,1853
|
|
1
2
|
remoteRF/__init__.py,sha256=-1h_xQjiE66oiWRO-vnLGJ4smpgUNhFuV-ZreW9_tD4,667
|
|
2
|
-
remoteRF/remoterf_cli.py,sha256=
|
|
3
|
+
remoteRF/remoterf_cli.py,sha256=c09oAUXQp5N65d0riN35eUfkX6Lg-TlP2N2MZZ6Iywk,21403
|
|
3
4
|
remoteRF/version.py,sha256=NWzcPOAdd567IvC8ei1yyPdeS7w0kt99oKwL6oWAQSc,850
|
|
4
5
|
remoteRF/common/__init__.py,sha256=ey1IjkAaXFL1sZBnBwuWVlHhA_n0bu_uxgkDV_KOxYg,708
|
|
5
6
|
remoteRF/common/tos_notice.txt,sha256=fUHXV6U9uhZDvVs8BcRi4kWbU1PeU9mkxML0n5DDQp8,334
|
|
@@ -10,7 +11,7 @@ remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py,sha256=Vy_7zGsMNy_UwbGR
|
|
|
10
11
|
remoteRF/common/grpc/grpc.proto,sha256=X85kGh3DgypPsb_sDjtARCkdb91XXNOr6GiTr2axEcY,5853
|
|
11
12
|
remoteRF/common/grpc/grpc_pb2.py,sha256=8aVMR9vsLGWtdpvK1iAMspgGQN1_Jn7EdHzdz_YcJ3M,12736
|
|
12
13
|
remoteRF/common/grpc/grpc_pb2_grpc.py,sha256=1vbqOxzDXt0bi8mjTJLAS742CsSUQcaZTw0N4cxRVyE,17731
|
|
13
|
-
remoteRF/common/grpc/v2_codec.py,sha256=
|
|
14
|
+
remoteRF/common/grpc/v2_codec.py,sha256=kgj_WXBOKw--J7ZgE7TBfrUbF5AmjMg_VNMx9tDNHTQ,5191
|
|
14
15
|
remoteRF/common/utils/__init__.py,sha256=mRw79YhvGlA5m1wsejx6n4uNcGwusUizDquSkoU1nFY,1128
|
|
15
16
|
remoteRF/common/utils/ansi_codes.py,sha256=jzSri_BqezN5yMBBLT9cjU2LoJZiQ4qBxhk5Y0KQPSI,5275
|
|
16
17
|
remoteRF/common/utils/api_token.py,sha256=G0kratdJk1PUs4svF6qjRC0do8osV9mntj7UgqzRySI,1928
|
|
@@ -22,13 +23,13 @@ remoteRF/config/cert_fetcher.py,sha256=_m4CiYxJPTckDrdynwUjwnehD_gd6yZoly5etfT83
|
|
|
22
23
|
remoteRF/config/config.py,sha256=jItSEI557nTor1BW2iOgebmMfl24TfIY7juRTjjICrc,9353
|
|
23
24
|
remoteRF/core/__init__.py,sha256=1E9ARnxVICp4uCpGNQT3Fb-jD9TvUYbJmctuUqPy8hU,1206
|
|
24
25
|
remoteRF/core/acc_login.py,sha256=Qct7vEsc0OIdVwmikOv2mVv9-ehH4CMVw7QWHe2yOsg,1010
|
|
25
|
-
remoteRF/core/app.py,sha256=
|
|
26
|
+
remoteRF/core/app.py,sha256=kLDTUMChZfriuUuZOGo-uCzwwjtBNiRuuzZlR-iQ6pQ,72701
|
|
26
27
|
remoteRF/core/banner_note.txt,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
remoteRF/core/direct_path.py,sha256=
|
|
28
|
-
remoteRF/core/dynamic_v2_transport.py,sha256=
|
|
29
|
-
remoteRF/core/grpc_acc.py,sha256
|
|
30
|
-
remoteRF/core/grpc_client.py,sha256=
|
|
31
|
-
remoteRF/core/secure_channel.py,sha256=
|
|
28
|
+
remoteRF/core/direct_path.py,sha256=5-75osvC2vgEyfk4zopX-wYy8XJpFvE1EcYTVKjDygM,31789
|
|
29
|
+
remoteRF/core/dynamic_v2_transport.py,sha256=sOXrgvo020u_Mu8lGp1v0SI-MvMY3A43ezTbJRzY5aY,21998
|
|
30
|
+
remoteRF/core/grpc_acc.py,sha256=XzE4uTSQ6ZX_4jcgK9pbS96_9_7oDqWEqDTTi07Z6P0,7636
|
|
31
|
+
remoteRF/core/grpc_client.py,sha256=SAKdDQUm92zAYyzgG1N0gE9Lq53Z9CulRjv_49XILw8,17973
|
|
32
|
+
remoteRF/core/secure_channel.py,sha256=_avJBcvtLv0_9MgT6dpM-6bNH8DGph2Gls6DJZMkIXo,2859
|
|
32
33
|
remoteRF/core/v2_errors.py,sha256=Xmuta24cfQEy4FGG7HZk7B9hD5g4pGT5Hz8Eguy42lU,2956
|
|
33
34
|
remoteRF/deployment/__init__.py,sha256=aEl9z32KMUBfRRm6Ho0-fM-VUtiKIRq4bQBU2_u29-c,69
|
|
34
35
|
remoteRF/deployment/backend.py,sha256=ULrHn31w-v8tN5BU4HktuPKG4vEGIteqPYo0hUWVjfI,16135
|
|
@@ -38,10 +39,10 @@ remoteRF/deployment/http.py,sha256=8Ar1-GOIyCJrPWURZyOl969e7FZqjdQuOkA5Km2IMGw,4
|
|
|
38
39
|
remoteRF/deployment/state.py,sha256=rXHEMf-kJP1sYCJn-VD84ZNalkYUaNAiYaoaVu4uo3U,6541
|
|
39
40
|
remoteRF/drivers/__init__.py,sha256=DXjon8E0R1X27fDQDy6IrwzqTFwrT6-suC5VSDXUP8Y,1250
|
|
40
41
|
remoteRF/drivers/_virtual.py,sha256=J6uy0G1VCpr3AeTKWiPUZienNTlO9eBC3TVb2uBdcvc,4402
|
|
41
|
-
remoteRF/drivers/dynamic_device.py,sha256=
|
|
42
|
-
remoteRF/drivers/dynamic_v2.py,sha256=
|
|
42
|
+
remoteRF/drivers/dynamic_device.py,sha256=lSdDgGl6022G2rUyo2WD0zMImwi9Q-uIzqX-T60MYeY,62180
|
|
43
|
+
remoteRF/drivers/dynamic_v2.py,sha256=CIJ6SW0jptT0Kp93Rly7WlYbeDoTdZVoP2CfHCaSiAo,33666
|
|
43
44
|
remoteRF/drivers/adalm_pluto/__init__.py,sha256=QmFIGRC9MG9GMOc277Hkwd2CG-3wI59UmuV0k2XsvY8,829
|
|
44
|
-
remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py,sha256=
|
|
45
|
+
remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py,sha256=yIf_VkgMsDPsz5W54MK0emRJz22UujydMpWGFV41jtU,12186
|
|
45
46
|
remoteRF/drivers/adalm_pluto/pluto_remote.py,sha256=6HRc94yZlM9FDt0_XcPIo_kVcG6ELgRahW0T25BIfb0,9134
|
|
46
47
|
remoteRF/drivers/hackrf/__init__.py,sha256=BlpF0ZyAu8IcfpxyQlPXbI4PATdisFT1lR0cM8Gk6O0,821
|
|
47
48
|
remoteRF/drivers/hackrf/hackrf_remote.py,sha256=UMwBsKGTUjq6lp0s3axwbdel9YtUUMoik0VtpsjGN-Y,8956
|
|
@@ -53,15 +54,15 @@ remoteRF/drivers/support/__init__.py,sha256=-1h_xQjiE66oiWRO-vnLGJ4smpgUNhFuV-Zr
|
|
|
53
54
|
remoteRF/drivers/support/ti_mmwave.py,sha256=a7ThvnYjdGzoqJjoG1l7KOdOGAbhVWXBYFjW3UE2MDI,10085
|
|
54
55
|
remoteRF/drivers/support/uhd.py,sha256=t0X2wG_xHSSVYedGhKSN-GIuvmkmd346bM7hx1KHm6s,11722
|
|
55
56
|
remoteRF/drivers/support/uhd_dsp.py,sha256=s1_PsBYao16wv8Gq2mHBo8tTBfHzc_cyd1DmRATNhAg,3223
|
|
56
|
-
remoteRF/drivers/support/uhd_v2.py,sha256=
|
|
57
|
+
remoteRF/drivers/support/uhd_v2.py,sha256=A66MK7FqfyT8AeNqS4pnCIwBxTTszMmT02YoZF2kqGk,23028
|
|
57
58
|
remoteRF/drivers/ti_mmwave/__init__.py,sha256=-ZhvekQi9UCgtZUhezu17s-wUoxc_FZO1-5ATLRzBAI,1189
|
|
58
59
|
remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py,sha256=sQiRCstSoi73T7TkE2WXp1YYp3GhCIeJavq50qKsllU,5696
|
|
59
60
|
remoteRF/drivers/usrp/__init__.py,sha256=y79nl50_4dV9-AI3cfDptOAWeR58fAaihHObqJI9HJM,780
|
|
60
61
|
remoteRF/drivers/usrp/usrp_remote.py,sha256=hBh_CmGOAoEkkdQQ8uPFAsBA6LVeZsPSlpPGgo1c7sM,115018
|
|
61
62
|
remoteRF/drivers/usrp/usrp_remote.pyi,sha256=MG7rMJdjyT0hLLoJmnS1emDQjgepWz695GbFED3v-T4,11810
|
|
62
|
-
remoterf-2.1.
|
|
63
|
-
remoterf-2.1.
|
|
64
|
-
remoterf-2.1.
|
|
65
|
-
remoterf-2.1.
|
|
66
|
-
remoterf-2.1.
|
|
67
|
-
remoterf-2.1.
|
|
63
|
+
remoterf-2.1.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
64
|
+
remoterf-2.1.2.dist-info/METADATA,sha256=xQLoKP0HEbmSxVt2I52TGq6bCcpxb9vmlj_Tmt436r0,12778
|
|
65
|
+
remoterf-2.1.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
66
|
+
remoterf-2.1.2.dist-info/entry_points.txt,sha256=La_TjAqCfqdcVTVjB2WrsJNn5rfgcuIYroe2v-CMwRQ,56
|
|
67
|
+
remoterf-2.1.2.dist-info/top_level.txt,sha256=XQJoVTmAOsHV9qtPSP_QSh0ma4jKxsDkxn4IjM_ZuZk,9
|
|
68
|
+
remoterf-2.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|