RemoteRF 2.1.0__tar.gz → 2.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {remoterf-2.1.0 → remoterf-2.1.1}/PKG-INFO +1 -1
- {remoterf-2.1.0 → remoterf-2.1.1}/pyproject.toml +1 -1
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/direct_path.py +105 -40
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/grpc_client.py +6 -8
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/remoterf_cli.py +2 -2
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/PKG-INFO +1 -1
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/SOURCES.txt +1 -1
- remoterf-2.1.1/tests/test_direct_path_race.py +129 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_finish_prompt.py +8 -3
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_gate_i_direct_path.py +10 -4
- remoterf-2.1.0/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -291
- {remoterf-2.1.0 → remoterf-2.1.1}/LICENSE +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/README.md +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/setup.cfg +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc.proto +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/banner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/app.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/grpc_acc.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/secure_channel.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/backend.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/direct.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/homes.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/http.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/deployment/state.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/dynamic_device.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/version.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_banner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_config_address.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_federated_devices.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_fetch_idl_fallback.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_homes.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_native_devices.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_process_arg.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_remoterf_cli.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.1.0 → remoterf-2.1.1}/tests/test_upgrade_rescue.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoterf"
|
|
7
|
-
version = "2.1.
|
|
7
|
+
version = "2.1.1"
|
|
8
8
|
description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -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:
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -173,7 +173,7 @@ def print_help() -> None:
|
|
|
173
173
|
printf(" remoterf --login global", Sty.GREEN)
|
|
174
174
|
printf(" remoterf homes", Sty.GREEN)
|
|
175
175
|
printf(" remoterf --version", Sty.GREEN)
|
|
176
|
-
printf(" remoterf --config --addr
|
|
176
|
+
printf(" remoterf --config --addr 203.0.113.10:12321", Sty.GREEN)
|
|
177
177
|
printf(" remoterf --config --addr global.remoterf.net --register", Sty.GREEN)
|
|
178
178
|
printf(" remoterf --config --wipe", Sty.GREEN)
|
|
179
179
|
printf(" remoterf --config --wipe --yes", Sty.GREEN)
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -46,7 +46,6 @@ src/remoteRF/drivers/dynamic_device.py
|
|
|
46
46
|
src/remoteRF/drivers/dynamic_v2.py
|
|
47
47
|
src/remoteRF/drivers/adalm_pluto/__init__.py
|
|
48
48
|
src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py
|
|
49
|
-
src/remoteRF/drivers/adalm_pluto/pluto_remote.py
|
|
50
49
|
src/remoteRF/drivers/hackrf/__init__.py
|
|
51
50
|
src/remoteRF/drivers/hackrf/hackrf_remote.py
|
|
52
51
|
src/remoteRF/drivers/rtl_sdr/__init__.py
|
|
@@ -74,6 +73,7 @@ tests/test_config_address.py
|
|
|
74
73
|
tests/test_deployment_accounts.py
|
|
75
74
|
tests/test_direct_path_fixed.py
|
|
76
75
|
tests/test_direct_path_partner.py
|
|
76
|
+
tests/test_direct_path_race.py
|
|
77
77
|
tests/test_dynamic_device_codegen.py
|
|
78
78
|
tests/test_dynamic_v2.py
|
|
79
79
|
tests/test_federated_devices.py
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The fixed dial and the punch run at once: whichever connects first is the
|
|
2
|
+
path, the other is cancelled, and when both fail the reason names both."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
from remoteRF.core import direct_path
|
|
13
|
+
|
|
14
|
+
from test_gate_i_direct_path import SERVER_NAME, FakeServer, write_certs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RaceTests(unittest.TestCase):
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
20
|
+
self.addCleanup(self._tmp.cleanup)
|
|
21
|
+
self.ca_pem = write_certs(Path(self._tmp.name))
|
|
22
|
+
|
|
23
|
+
def _path(self, offer):
|
|
24
|
+
path = direct_path.DirectPath(offer=offer, server_name=SERVER_NAME, ca_pem=self.ca_pem, stun=None)
|
|
25
|
+
self.addCleanup(path.stop)
|
|
26
|
+
path.start()
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
def test_the_punch_wins_while_the_fixed_dial_still_hangs(self):
|
|
30
|
+
server = FakeServer(Path(self._tmp.name), lambda request: {})
|
|
31
|
+
self.addCleanup(server.close)
|
|
32
|
+
outcome = []
|
|
33
|
+
|
|
34
|
+
async def hang(self, answer, configuration):
|
|
35
|
+
try:
|
|
36
|
+
await asyncio.Event().wait()
|
|
37
|
+
except asyncio.CancelledError:
|
|
38
|
+
outcome.append("cancelled")
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
with mock.patch.object(direct_path.DirectPath, "_dial_fixed", hang):
|
|
42
|
+
path = self._path(lambda offer: {**server.offer(offer), "udp": "127.0.0.1:9", "session": "s"})
|
|
43
|
+
self.assertTrue(path.wait(15), path.describe())
|
|
44
|
+
self.assertFalse(path.fixed)
|
|
45
|
+
self.assertTrue(path.describe().startswith("direct "), path.describe())
|
|
46
|
+
self.assertEqual(outcome, ["cancelled"])
|
|
47
|
+
self.assertEqual(len(path._tasks), 3)
|
|
48
|
+
|
|
49
|
+
def test_both_failing_names_both(self):
|
|
50
|
+
async def fixed(self, answer, configuration):
|
|
51
|
+
raise asyncio.TimeoutError
|
|
52
|
+
|
|
53
|
+
async def punch(self, ice, answer, configuration):
|
|
54
|
+
raise ConnectionError("ICE negotiation failed")
|
|
55
|
+
|
|
56
|
+
with mock.patch.object(direct_path.DirectPath, "_dial_fixed", fixed), \
|
|
57
|
+
mock.patch.object(direct_path.DirectPath, "_punch", punch):
|
|
58
|
+
path = self._path(lambda offer: {"ufrag": "a", "pwd": "b", "candidates": [], "udp": "h:1", "session": "s"})
|
|
59
|
+
self.assertFalse(path.wait(15))
|
|
60
|
+
self.assertEqual(path.state, "down")
|
|
61
|
+
self.assertEqual(path.reason, "fixed port: timed out; punch: ConnectionError: ICE negotiation failed")
|
|
62
|
+
|
|
63
|
+
def test_a_punch_alone_keeps_its_own_reason(self):
|
|
64
|
+
async def punch(self, ice, answer, configuration):
|
|
65
|
+
raise asyncio.TimeoutError
|
|
66
|
+
|
|
67
|
+
with mock.patch.object(direct_path.DirectPath, "_punch", punch):
|
|
68
|
+
path = self._path(lambda offer: {"ufrag": "a", "pwd": "b", "candidates": []})
|
|
69
|
+
self.assertFalse(path.wait(15))
|
|
70
|
+
self.assertEqual(path.reason, "timed out")
|
|
71
|
+
|
|
72
|
+
def test_the_offer_says_this_client_punches_at_once(self):
|
|
73
|
+
seen = []
|
|
74
|
+
|
|
75
|
+
def offer(request):
|
|
76
|
+
seen.append(request)
|
|
77
|
+
return {"ufrag": "a", "pwd": "b", "candidates": []}
|
|
78
|
+
|
|
79
|
+
path = self._path(offer)
|
|
80
|
+
path.wait(15)
|
|
81
|
+
self.assertEqual(len(seen), 1)
|
|
82
|
+
self.assertIs(seen[0]["race"], True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DescribeTests(unittest.TestCase):
|
|
89
|
+
def setUp(self):
|
|
90
|
+
self.addCleanup(setattr, direct_path, "_path", None)
|
|
91
|
+
direct_path._path = None
|
|
92
|
+
|
|
93
|
+
def _describe(self, routes, endpoint="10.0.0.5:12321"):
|
|
94
|
+
profile = mock.Mock(home="lab", grpc_endpoint=endpoint)
|
|
95
|
+
with mock.patch("remoteRF.core.grpc_client._current_profile", return_value=profile), \
|
|
96
|
+
mock.patch("remoteRF.deployment.homes.load_home", return_value={"routes": routes}):
|
|
97
|
+
return direct_path.describe()
|
|
98
|
+
|
|
99
|
+
def test_a_lan_route_is_not_the_relay(self):
|
|
100
|
+
self.assertEqual(self._describe([{"kind": "lan", "host": "10.0.0.5", "port": 12321}]), "LAN (direct)")
|
|
101
|
+
self.assertEqual(self._describe([{"kind": "direct", "host": "10.0.0.5", "port": 12321}]), "direct")
|
|
102
|
+
|
|
103
|
+
def test_a_relay_route_and_a_failed_lookup_keep_the_old_wording(self):
|
|
104
|
+
self.assertEqual(self._describe([{"kind": "relay", "host": "10.0.0.5", "port": 12321}]), "relay (direct path off)")
|
|
105
|
+
with mock.patch("remoteRF.core.grpc_client._current_profile", side_effect=RuntimeError("not configured")):
|
|
106
|
+
self.assertEqual(direct_path.describe(), "relay (direct path off)")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class QuietWaiterTests(unittest.TestCase):
|
|
110
|
+
def _run(self, exc):
|
|
111
|
+
loop = asyncio.new_event_loop()
|
|
112
|
+
seen = []
|
|
113
|
+
try:
|
|
114
|
+
loop.default_exception_handler = lambda context: seen.append(context)
|
|
115
|
+
direct_path.quiet_aioice_errors(loop)
|
|
116
|
+
loop.call_exception_handler({"message": "Future exception was never retrieved", "exception": exc})
|
|
117
|
+
finally:
|
|
118
|
+
loop.close()
|
|
119
|
+
return seen
|
|
120
|
+
|
|
121
|
+
def test_aioquic_orphaned_connect_waiter_is_silent(self):
|
|
122
|
+
self.assertEqual(self._run(ConnectionError()), [])
|
|
123
|
+
|
|
124
|
+
def test_other_unretrieved_exceptions_still_reach_the_default_handler(self):
|
|
125
|
+
self.assertEqual(len(self._run(ValueError("not ours"))), 1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
unittest.main()
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
"""A user-error reply never blocks
|
|
1
|
+
"""A user-error reply never blocks on stdin, terminal or not."""
|
|
2
2
|
|
|
3
3
|
import io
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
5
7
|
from remoteRF.common.grpc import grpc_pb2
|
|
6
8
|
from remoteRF.common.utils import map_arg
|
|
7
9
|
from remoteRF.core import grpc_client
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
@pytest.mark.parametrize("isatty", [False, True])
|
|
13
|
+
def test_a_user_error_never_prompts(monkeypatch, capsys, isatty):
|
|
11
14
|
response = grpc_pb2.GenericRPCResponse(results={"UE": map_arg("Username already exists.")})
|
|
12
|
-
|
|
15
|
+
stdin = io.StringIO("")
|
|
16
|
+
monkeypatch.setattr(stdin, "isatty", lambda: isatty)
|
|
17
|
+
monkeypatch.setattr("sys.stdin", stdin)
|
|
13
18
|
monkeypatch.setattr("builtins.input", lambda *a: (_ for _ in ()).throw(AssertionError("prompted")))
|
|
14
19
|
assert grpc_client._finish(response) is response
|
|
15
20
|
assert "Username already exists" in capsys.readouterr().out
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import datetime
|
|
8
|
+
import os
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
import unittest
|
|
@@ -235,12 +236,17 @@ class RoutingTests(unittest.TestCase):
|
|
|
235
236
|
|
|
236
237
|
seen = []
|
|
237
238
|
self._path(lambda name, args: seen.append((name, dict(args))) or grpc_pb2.GenericRPCResponse())
|
|
238
|
-
with mock.patch.dict("os.environ", {"REMOTERF_RX_WIRE": "
|
|
239
|
+
with mock.patch.dict("os.environ", {"REMOTERF_RX_WIRE": "c64"}):
|
|
239
240
|
rpc_client(function_name="adalm_pluto:rx:CALL0", args={"a": map_arg("t")})
|
|
240
241
|
rpc_client(function_name="adalm_pluto:rx_lo:GET", args={"a": map_arg("t")})
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
with mock.patch.dict("os.environ", {"REMOTERF_RX_WIRE": ""}): # opt out
|
|
243
|
+
rpc_client(function_name="adalm_pluto:rx:CALL0", args={"a": map_arg("t")})
|
|
244
|
+
with mock.patch.dict("os.environ"):
|
|
245
|
+
os.environ.pop("REMOTERF_RX_WIRE", None) # unset: i16 by default
|
|
246
|
+
rpc_client(function_name="adalm_pluto:rx:CALL0", args={"a": map_arg("t")})
|
|
247
|
+
self.assertEqual([sorted(args) for _, args in seen], [["a", "wire"], ["a"], ["a"], ["a", "wire"]])
|
|
248
|
+
self.assertEqual(unmap_arg(seen[0][1]["wire"]), "c64")
|
|
249
|
+
self.assertEqual(unmap_arg(seen[3][1]["wire"]), "i16")
|
|
244
250
|
|
|
245
251
|
def test_a_path_that_is_not_ready_is_skipped(self):
|
|
246
252
|
from remoteRF.core.grpc_client import rpc_client
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf-2.1.0 → remoterf-2.1.1}/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|