RemoteRF 2.1.0.dev7__py3-none-any.whl → 2.1.1__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/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.1.dist-info}/METADATA +4 -16
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.1.dist-info}/RECORD +18 -18
- remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -291
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.1.dist-info}/WHEEL +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.1.dist-info}/entry_points.txt +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {remoterf-2.1.0.dev7.dist-info → remoterf-2.1.1.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/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.1
|
|
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
|
|
@@ -24,11 +25,11 @@ remoteRF/core/__init__.py,sha256=1E9ARnxVICp4uCpGNQT3Fb-jD9TvUYbJmctuUqPy8hU,120
|
|
|
24
25
|
remoteRF/core/acc_login.py,sha256=Qct7vEsc0OIdVwmikOv2mVv9-ehH4CMVw7QWHe2yOsg,1010
|
|
25
26
|
remoteRF/core/app.py,sha256=uh4UtLpN9Qx88EfeKMP4J9XU9mKS4HD942xh3zD1PJg,71823
|
|
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,11 +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/pluto_remote.py,sha256=6HRc94yZlM9FDt0_XcPIo_kVcG6ELgRahW0T25BIfb0,9134
|
|
45
|
+
remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py,sha256=yIf_VkgMsDPsz5W54MK0emRJz22UujydMpWGFV41jtU,12186
|
|
46
46
|
remoteRF/drivers/hackrf/__init__.py,sha256=BlpF0ZyAu8IcfpxyQlPXbI4PATdisFT1lR0cM8Gk6O0,821
|
|
47
47
|
remoteRF/drivers/hackrf/hackrf_remote.py,sha256=UMwBsKGTUjq6lp0s3axwbdel9YtUUMoik0VtpsjGN-Y,8956
|
|
48
48
|
remoteRF/drivers/rtl_sdr/__init__.py,sha256=sJ0oAIS4hwE3PdZtnfNHTWKiYuLUHpWM_WspiTEKMTI,823
|
|
@@ -53,15 +53,15 @@ remoteRF/drivers/support/__init__.py,sha256=-1h_xQjiE66oiWRO-vnLGJ4smpgUNhFuV-Zr
|
|
|
53
53
|
remoteRF/drivers/support/ti_mmwave.py,sha256=a7ThvnYjdGzoqJjoG1l7KOdOGAbhVWXBYFjW3UE2MDI,10085
|
|
54
54
|
remoteRF/drivers/support/uhd.py,sha256=t0X2wG_xHSSVYedGhKSN-GIuvmkmd346bM7hx1KHm6s,11722
|
|
55
55
|
remoteRF/drivers/support/uhd_dsp.py,sha256=s1_PsBYao16wv8Gq2mHBo8tTBfHzc_cyd1DmRATNhAg,3223
|
|
56
|
-
remoteRF/drivers/support/uhd_v2.py,sha256=
|
|
56
|
+
remoteRF/drivers/support/uhd_v2.py,sha256=A66MK7FqfyT8AeNqS4pnCIwBxTTszMmT02YoZF2kqGk,23028
|
|
57
57
|
remoteRF/drivers/ti_mmwave/__init__.py,sha256=-ZhvekQi9UCgtZUhezu17s-wUoxc_FZO1-5ATLRzBAI,1189
|
|
58
58
|
remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py,sha256=sQiRCstSoi73T7TkE2WXp1YYp3GhCIeJavq50qKsllU,5696
|
|
59
59
|
remoteRF/drivers/usrp/__init__.py,sha256=y79nl50_4dV9-AI3cfDptOAWeR58fAaihHObqJI9HJM,780
|
|
60
60
|
remoteRF/drivers/usrp/usrp_remote.py,sha256=hBh_CmGOAoEkkdQQ8uPFAsBA6LVeZsPSlpPGgo1c7sM,115018
|
|
61
61
|
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.
|
|
62
|
+
remoterf-2.1.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
63
|
+
remoterf-2.1.1.dist-info/METADATA,sha256=EuGx38QDsfkF6s6GkXWfj-m6l1Qrr3O46l6G2LWZbIY,12778
|
|
64
|
+
remoterf-2.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
65
|
+
remoterf-2.1.1.dist-info/entry_points.txt,sha256=La_TjAqCfqdcVTVjB2WrsJNn5rfgcuIYroe2v-CMwRQ,56
|
|
66
|
+
remoterf-2.1.1.dist-info/top_level.txt,sha256=XQJoVTmAOsHV9qtPSP_QSh0ma4jKxsDkxn4IjM_ZuZk,9
|
|
67
|
+
remoterf-2.1.1.dist-info/RECORD,,
|
|
@@ -1,291 +0,0 @@
|
|
|
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
|
-
from ...common.utils import *
|
|
17
|
-
from .._virtual import (
|
|
18
|
-
is_virtual_token,
|
|
19
|
-
make_virtual_token,
|
|
20
|
-
retag_virtual_token,
|
|
21
|
-
virtual_call,
|
|
22
|
-
virtual_get,
|
|
23
|
-
virtual_set,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _rpc_client(*, function_name, args):
|
|
28
|
-
# Keep transport/configuration initialization out of the virtual path.
|
|
29
|
-
from ...core.grpc_client import rpc_client
|
|
30
|
-
|
|
31
|
-
return rpc_client(function_name=function_name, args=args)
|
|
32
|
-
|
|
33
|
-
def try_get(function_name, token):
|
|
34
|
-
if is_virtual_token(token):
|
|
35
|
-
return virtual_get(token, function_name)
|
|
36
|
-
try:
|
|
37
|
-
return unmap_arg(_rpc_client(function_name=f"Pluto:{function_name}:GET", args={'a':map_arg(token)}).results[function_name])
|
|
38
|
-
except Exception as e:
|
|
39
|
-
input(f"Error: {e}\nHit enter to continue...")
|
|
40
|
-
return None
|
|
41
|
-
|
|
42
|
-
def try_set(function_name, value, token):
|
|
43
|
-
if is_virtual_token(token):
|
|
44
|
-
virtual_set(token, function_name, value)
|
|
45
|
-
return None
|
|
46
|
-
try:
|
|
47
|
-
_rpc_client(function_name=f"Pluto:{function_name}:SET", args={function_name: map_arg(value), 'a':map_arg(token)})
|
|
48
|
-
except Exception as e:
|
|
49
|
-
input(f"Error: {e}\nHit enter to continue...")
|
|
50
|
-
|
|
51
|
-
def try_call_0_arg(function_name, token): # 0 argument call
|
|
52
|
-
if is_virtual_token(token):
|
|
53
|
-
return virtual_call(token, function_name)
|
|
54
|
-
try:
|
|
55
|
-
response = _rpc_client(
|
|
56
|
-
function_name=f"Pluto:{function_name}:CALL0",
|
|
57
|
-
args={
|
|
58
|
-
'a': map_arg(token)
|
|
59
|
-
}
|
|
60
|
-
)
|
|
61
|
-
return unmap_arg(response.results[function_name])
|
|
62
|
-
except Exception as e:
|
|
63
|
-
input(f"RPC_0_call Error: {e}\nHit enter to continue...")
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
def try_call_1_arg(function_name, arg, token): # 1 argument call
|
|
67
|
-
if is_virtual_token(token):
|
|
68
|
-
return virtual_call(token, function_name, arg, has_arg=True)
|
|
69
|
-
try:
|
|
70
|
-
response = _rpc_client(
|
|
71
|
-
function_name=f"Pluto:{function_name}:CALL1",
|
|
72
|
-
args={
|
|
73
|
-
'a': map_arg(token),
|
|
74
|
-
'arg1': map_arg(arg)
|
|
75
|
-
}
|
|
76
|
-
)
|
|
77
|
-
# The server should return something like {function_name: <something>}
|
|
78
|
-
return unmap_arg(response.results[function_name])
|
|
79
|
-
except Exception as e:
|
|
80
|
-
input(f"RPC_1_call Error: {e}\nHit enter to continue...")
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
class rx_def:
|
|
84
|
-
pass
|
|
85
|
-
|
|
86
|
-
class tx_def:
|
|
87
|
-
pass
|
|
88
|
-
|
|
89
|
-
class rx_tx_def(rx_def, tx_def):
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
class ad9364(rx_tx_def):
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
class Pluto: # client
|
|
96
|
-
|
|
97
|
-
def __init__(self, token: str | None = None, debug=False, *, virtual=False):
|
|
98
|
-
self.virtual = bool(virtual)
|
|
99
|
-
if self.virtual:
|
|
100
|
-
self.token = make_virtual_token(token, device_type="adalm_pluto")
|
|
101
|
-
return
|
|
102
|
-
if token is None:
|
|
103
|
-
raise ValueError("A reservation token is required unless virtual=True")
|
|
104
|
-
self.token = token
|
|
105
|
-
try_call_0_arg(function_name="ip", token=token)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def api_token(self, token:str) -> None:
|
|
109
|
-
if self.virtual:
|
|
110
|
-
self.token = retag_virtual_token(self.token, token)
|
|
111
|
-
return
|
|
112
|
-
self.token = token
|
|
113
|
-
try_call_0_arg(function_name="ip", token=token)
|
|
114
|
-
|
|
115
|
-
# PlutoSDR
|
|
116
|
-
|
|
117
|
-
_device_name = "PlutoSDR"
|
|
118
|
-
_uri_auto = "ip:pluto.local"
|
|
119
|
-
|
|
120
|
-
def __repr__(self): # ! UNTESTED !
|
|
121
|
-
return try_get("__repr__", self.token)
|
|
122
|
-
|
|
123
|
-
#region ad9364
|
|
124
|
-
"""AD9364 Transceiver"""
|
|
125
|
-
|
|
126
|
-
@property
|
|
127
|
-
def filter(self):
|
|
128
|
-
return try_get("filter", self.token)
|
|
129
|
-
|
|
130
|
-
@filter.setter
|
|
131
|
-
def filter(self, value):
|
|
132
|
-
try_set("filter", value, self.token)
|
|
133
|
-
|
|
134
|
-
@property
|
|
135
|
-
def loopback(self):
|
|
136
|
-
"""loopback: Set loopback mode. Options are:
|
|
137
|
-
0 (Disable), 1 (Digital), 2 (RF)"""
|
|
138
|
-
return try_get("loopback", self.token)
|
|
139
|
-
|
|
140
|
-
@loopback.setter
|
|
141
|
-
def loopback(self, value):
|
|
142
|
-
try_set("loopback", value, self.token)
|
|
143
|
-
|
|
144
|
-
@property
|
|
145
|
-
def gain_control_mode_chan0(self):
|
|
146
|
-
"""gain_control_mode_chan0: Mode of receive path AGC. Options are:
|
|
147
|
-
slow_attack, fast_attack, manual"""
|
|
148
|
-
return try_get("gain_control_mode_chan0", self.token)
|
|
149
|
-
|
|
150
|
-
@gain_control_mode_chan0.setter
|
|
151
|
-
def gain_control_mode_chan0(self, value):
|
|
152
|
-
try_set("gain_control_mode_chan0", value, self.token)
|
|
153
|
-
|
|
154
|
-
@property
|
|
155
|
-
def rx_hardwaregain_chan0(self):
|
|
156
|
-
"""rx_hardwaregain_chan0: Gain applied to RX path. Only applicable when
|
|
157
|
-
gain_control_mode is set to 'manual'"""
|
|
158
|
-
return try_get("rx_hardwaregain_chan0", self.token)
|
|
159
|
-
|
|
160
|
-
@rx_hardwaregain_chan0.setter
|
|
161
|
-
def rx_hardwaregain_chan0(self, value):
|
|
162
|
-
try_set("rx_hardwaregain_chan0", value, self.token)
|
|
163
|
-
|
|
164
|
-
@property
|
|
165
|
-
def tx_hardwaregain_chan0(self):
|
|
166
|
-
"""tx_hardwaregain_chan0: Attenuation applied to TX path"""
|
|
167
|
-
return try_get("tx_hardwaregain_chan0", self.token)
|
|
168
|
-
|
|
169
|
-
@tx_hardwaregain_chan0.setter
|
|
170
|
-
def tx_hardwaregain_chan0(self, value):
|
|
171
|
-
try_set("tx_hardwaregain_chan0", value, self.token)
|
|
172
|
-
|
|
173
|
-
@property
|
|
174
|
-
def rx_rf_bandwidth(self):
|
|
175
|
-
"""rx_rf_bandwidth: Bandwidth of front-end analog filter of RX path"""
|
|
176
|
-
return try_get("rx_rf_bandwidth", self.token)
|
|
177
|
-
|
|
178
|
-
@rx_rf_bandwidth.setter
|
|
179
|
-
def rx_rf_bandwidth(self, value):
|
|
180
|
-
try_set("rx_rf_bandwidth", value, self.token)
|
|
181
|
-
|
|
182
|
-
@property
|
|
183
|
-
def tx_rf_bandwidth(self):
|
|
184
|
-
"""tx_rf_bandwidth: Bandwidth of front-end analog filter of TX path"""
|
|
185
|
-
return try_get("tx_rf_bandwidth", self.token)
|
|
186
|
-
|
|
187
|
-
@tx_rf_bandwidth.setter
|
|
188
|
-
def tx_rf_bandwidth(self, value):
|
|
189
|
-
try_set("tx_rf_bandwidth", value, self.token)
|
|
190
|
-
|
|
191
|
-
@property
|
|
192
|
-
def sample_rate(self): # ! UNTESTED !
|
|
193
|
-
"""sample_rate: Sample rate RX and TX paths in samples per second"""
|
|
194
|
-
return try_get("sample_rate", self.token)
|
|
195
|
-
|
|
196
|
-
@sample_rate.setter
|
|
197
|
-
def sample_rate(self, rate): # ! UNTESTED !
|
|
198
|
-
try_set("sample_rate", rate, self.token)
|
|
199
|
-
|
|
200
|
-
@property
|
|
201
|
-
def rx_lo(self):
|
|
202
|
-
"""rx_lo: Carrier frequency of RX path"""
|
|
203
|
-
return try_get("rx_lo", self.token)
|
|
204
|
-
|
|
205
|
-
@rx_lo.setter
|
|
206
|
-
def rx_lo(self, value):
|
|
207
|
-
try_set("rx_lo", value, self.token)
|
|
208
|
-
|
|
209
|
-
@property
|
|
210
|
-
def tx_lo(self):
|
|
211
|
-
"""tx_lo: Carrier frequency of TX path"""
|
|
212
|
-
return try_get("tx_lo", self.token)
|
|
213
|
-
|
|
214
|
-
@tx_lo.setter
|
|
215
|
-
def tx_lo(self, value):
|
|
216
|
-
try_set("tx_lo", value, self.token)
|
|
217
|
-
|
|
218
|
-
@property
|
|
219
|
-
def tx_cyclic_buffer(self):
|
|
220
|
-
"""tx_cyclic_buffer: Size of cyclic buffer"""
|
|
221
|
-
return try_get("tx_cyclic_buffer", self.token)
|
|
222
|
-
|
|
223
|
-
@tx_cyclic_buffer.setter
|
|
224
|
-
def tx_cyclic_buffer(self, value):
|
|
225
|
-
try_set("tx_cyclic_buffer", value, self.token)
|
|
226
|
-
|
|
227
|
-
def tx_destroy_buffer(self):
|
|
228
|
-
try_call_0_arg("tx_destroy_buffer", self.token)
|
|
229
|
-
|
|
230
|
-
def rx_destroy_buffer(self):
|
|
231
|
-
try_call_0_arg("rx_destroy_buffer", self.token)
|
|
232
|
-
|
|
233
|
-
#endregion
|
|
234
|
-
#region rx_def
|
|
235
|
-
|
|
236
|
-
def rx(self):
|
|
237
|
-
return try_get("rx", self.token)
|
|
238
|
-
|
|
239
|
-
@property
|
|
240
|
-
def rx_buffer_size(self):
|
|
241
|
-
return try_get("rx_buffer_size", self.token)
|
|
242
|
-
|
|
243
|
-
@rx_buffer_size.setter
|
|
244
|
-
def rx_buffer_size(self, value):
|
|
245
|
-
try_set("rx_buffer_size", value, self.token)
|
|
246
|
-
|
|
247
|
-
#endregion
|
|
248
|
-
#region tx_def
|
|
249
|
-
|
|
250
|
-
def tx(self, value):
|
|
251
|
-
return try_call_1_arg("tx", value, self.token)
|
|
252
|
-
|
|
253
|
-
# @tx.setter
|
|
254
|
-
# def tx(self, value):
|
|
255
|
-
# try_set("tx", value, self.token)
|
|
256
|
-
|
|
257
|
-
#endregion
|
|
258
|
-
|
|
259
|
-
#region tx
|
|
260
|
-
|
|
261
|
-
#endregion
|
|
262
|
-
|
|
263
|
-
#region _dec_int_fpga_filter
|
|
264
|
-
|
|
265
|
-
"""Decimator and interpolator fpga filter controls"""
|
|
266
|
-
|
|
267
|
-
def _get_rates(self, dev, output): # ! UNTESTED !
|
|
268
|
-
"""Get the decimation and interpolation rates"""
|
|
269
|
-
return try_get("rates", self.token)
|
|
270
|
-
|
|
271
|
-
@property
|
|
272
|
-
def rx_dec8_filter_en(self) -> bool: # ! UNTESTED !
|
|
273
|
-
"""rx_dec8_filter_en: Enable decimate by 8 filter in FPGA"""
|
|
274
|
-
return try_get("rx_dec8_filter_en", self.token)
|
|
275
|
-
|
|
276
|
-
@rx_dec8_filter_en.setter
|
|
277
|
-
def rx_dec8_filter_en(self, value: bool): # ! UNTESTED !
|
|
278
|
-
"""rx_dec8_filter_en: Enable decimate by 8 filter in FPGA"""
|
|
279
|
-
return try_set("rx_dec8_filter_en", value, self.token)
|
|
280
|
-
|
|
281
|
-
@property
|
|
282
|
-
def tx_int8_filter_en(self) -> bool: # ! UNTESTED !
|
|
283
|
-
"""tx_int8_filter_en: Enable interpolate by 8 filter in FPGA"""
|
|
284
|
-
return try_get("tx_int8_filter_en", self.token)
|
|
285
|
-
|
|
286
|
-
@tx_int8_filter_en.setter
|
|
287
|
-
def tx_int8_filter_en(self, value: bool): # ! UNTESTED !
|
|
288
|
-
"""tx_int8_filter_en: Enable interpolate by 8 filter in FPGA"""
|
|
289
|
-
return try_set("tx_int8_filter_en", value, self.token)
|
|
290
|
-
|
|
291
|
-
#endregion
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|