RemoteRF 2.0.14__tar.gz → 2.0.15__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.0.14/src/remoterf.egg-info → remoterf-2.0.15}/PKG-INFO +3 -15
- remoterf-2.0.15/pyproject.toml +36 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/grpc/v2_codec.py +1 -1
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/grpc_acc.py +1 -1
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_device.py +44 -28
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_v2.py +25 -38
- {remoterf-2.0.14 → remoterf-2.0.15/src/remoterf.egg-info}/PKG-INFO +3 -15
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoterf.egg-info/SOURCES.txt +1 -2
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoterf.egg-info/requires.txt +0 -1
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_dynamic_device_codegen.py +76 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_dynamic_v2.py +55 -7
- remoterf-2.0.14/MANIFEST.in +0 -3
- remoterf-2.0.14/setup.py +0 -60
- {remoterf-2.0.14 → remoterf-2.0.15}/LICENSE +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/README.md +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/setup.cfg +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc.proto +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/banner.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/app.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/grpc_client.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/remoterf_cli.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoteRF/version.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_banner.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_process_arg.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_remoterf_cli.py +0 -0
- {remoterf-2.0.14 → remoterf-2.0.15}/tests/test_ti_mmwave.py +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.15
|
|
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
|
|
@@ -16,17 +14,7 @@ Requires-Dist: protobuf<7.0.0,>=6.31.1
|
|
|
16
14
|
Requires-Dist: numpy
|
|
17
15
|
Requires-Dist: prompt_toolkit
|
|
18
16
|
Requires-Dist: python-dotenv
|
|
19
|
-
Requires-Dist: prompt-toolkit
|
|
20
|
-
Dynamic: author
|
|
21
|
-
Dynamic: author-email
|
|
22
|
-
Dynamic: classifier
|
|
23
|
-
Dynamic: description
|
|
24
|
-
Dynamic: description-content-type
|
|
25
|
-
Dynamic: license
|
|
26
17
|
Dynamic: license-file
|
|
27
|
-
Dynamic: requires-dist
|
|
28
|
-
Dynamic: requires-python
|
|
29
|
-
Dynamic: summary
|
|
30
18
|
|
|
31
19
|
# Remote RF
|
|
32
20
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "remoterf"
|
|
7
|
+
version = "2.0.15"
|
|
8
|
+
description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "GPL-3.0-or-later"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Ethan Ge", email = "ethoGalaxy@gmail.com" }]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"grpcio>=1.78.1,<2.0.0",
|
|
16
|
+
"protobuf>=6.31.1,<7.0.0",
|
|
17
|
+
"numpy",
|
|
18
|
+
"prompt_toolkit",
|
|
19
|
+
"python-dotenv",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
remoterf = "remoteRF.remoterf_cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
package-dir = { "" = "src" }
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
"*" = ["*.crt", "*.key", "*.pem", "*.txt", "*.pyi", "*.proto"]
|
|
@@ -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",
|
|
@@ -56,7 +56,7 @@ class RemoteRFAccount:
|
|
|
56
56
|
token = unmap_arg(response.results["Token"])
|
|
57
57
|
try:
|
|
58
58
|
from ..drivers.dynamic_device import install_driver
|
|
59
|
-
install_driver(device_id=device_id)
|
|
59
|
+
install_driver(token=token, device_id=device_id)
|
|
60
60
|
except Exception as e:
|
|
61
61
|
print(f"Warning: could not install driver for device {device_id}: {e}")
|
|
62
62
|
return token
|
|
@@ -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
|
|
@@ -89,29 +90,20 @@ def fetch_idl(
|
|
|
89
90
|
"schema_hash": "sha256:...",
|
|
90
91
|
}
|
|
91
92
|
"""
|
|
92
|
-
if token is not None and prefer_v2:
|
|
93
|
-
from .dynamic_v2 import fetch_schema_v2
|
|
94
|
-
from ..core.v2_errors import RemoteRFProtocolError, RemoteRFTransportError
|
|
95
|
-
|
|
96
|
-
try:
|
|
97
|
-
return fetch_schema_v2(str(token))
|
|
98
|
-
except RemoteRFProtocolError:
|
|
99
|
-
# The selected device may only publish v1 (for example Pluto).
|
|
100
|
-
pass
|
|
101
|
-
except RemoteRFTransportError as exc:
|
|
102
|
-
# Old servers report the additive v2 service as UNIMPLEMENTED.
|
|
103
|
-
if exc.details.get("grpc_code") != "UNIMPLEMENTED":
|
|
104
|
-
raise
|
|
105
|
-
|
|
106
93
|
args: dict = {}
|
|
107
|
-
if
|
|
108
|
-
args['token'] = map_arg(str(token))
|
|
109
|
-
elif device_id is not None:
|
|
94
|
+
if device_id is not None:
|
|
110
95
|
args['device_id'] = map_arg(int(device_id))
|
|
96
|
+
elif token is not None:
|
|
97
|
+
args['token'] = map_arg(str(token))
|
|
111
98
|
elif device_name is not None:
|
|
112
99
|
args['device_name'] = map_arg(str(device_name))
|
|
113
100
|
else:
|
|
114
101
|
raise ValueError("fetch_idl requires token, device_id, or device_name")
|
|
102
|
+
if prefer_v2:
|
|
103
|
+
# One RPC either way: servers that know v2 answer with it when the
|
|
104
|
+
# device publishes one (also by device_id, which needs no active
|
|
105
|
+
# reservation); v1-only devices and older servers answer v1.
|
|
106
|
+
args['schema_versions'] = map_arg(["2.0"])
|
|
115
107
|
|
|
116
108
|
from ..core.grpc_client import rpc_client
|
|
117
109
|
|
|
@@ -1381,8 +1373,14 @@ _DRIVERS_DIR = Path(__file__).parent
|
|
|
1381
1373
|
|
|
1382
1374
|
|
|
1383
1375
|
def _write_driver_files(schema: dict) -> Path:
|
|
1384
|
-
if str(schema.get("schema_version")) == "2.0"
|
|
1385
|
-
|
|
1376
|
+
pkg_dir = _write_v2_driver_files(schema) if str(schema.get("schema_version")) == "2.0" else _write_v1_driver_files(schema)
|
|
1377
|
+
stale = f"remoteRF.drivers.{pkg_dir.name}"
|
|
1378
|
+
for name in [m for m in sys.modules if m == stale or m.startswith(stale + ".")]:
|
|
1379
|
+
del sys.modules[name]
|
|
1380
|
+
return pkg_dir
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _write_v1_driver_files(schema: dict) -> Path:
|
|
1386
1384
|
|
|
1387
1385
|
(
|
|
1388
1386
|
device_type,
|
|
@@ -1397,8 +1395,13 @@ def _write_driver_files(schema: dict) -> Path:
|
|
|
1397
1395
|
_call_map,
|
|
1398
1396
|
) = _schema_maps(schema)
|
|
1399
1397
|
pkg_dir = _DRIVERS_DIR / device_type
|
|
1398
|
+
remote_path = pkg_dir / f"{device_type}_remote.py"
|
|
1399
|
+
if remote_path.exists() and _is_v2_driver(remote_path):
|
|
1400
|
+
# The server answered v1 (older server, or a device that no longer
|
|
1401
|
+
# publishes v2); don't trade an installed Dynamic v2 driver for it.
|
|
1402
|
+
return pkg_dir
|
|
1400
1403
|
pkg_dir.mkdir(exist_ok=True)
|
|
1401
|
-
|
|
1404
|
+
remote_path.write_text(_codegen(schema), encoding="utf-8")
|
|
1402
1405
|
init_lines = [
|
|
1403
1406
|
*_GPL_NOTICE_LINES,
|
|
1404
1407
|
"from importlib import import_module as _import_module",
|
|
@@ -1550,11 +1553,7 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1550
1553
|
device_name=device_name,
|
|
1551
1554
|
prefer_v2=True,
|
|
1552
1555
|
)
|
|
1553
|
-
device_type = (
|
|
1554
|
-
_require_package_name(schema.get("device_type"), field="device_type")
|
|
1555
|
-
if str(schema.get("schema_version")) == "2.0"
|
|
1556
|
-
else _schema_maps(schema)[0]
|
|
1557
|
-
)
|
|
1556
|
+
device_type = _schema_device_type(schema)
|
|
1558
1557
|
remote_path = _DRIVERS_DIR / device_type / f"{device_type}_remote.py"
|
|
1559
1558
|
|
|
1560
1559
|
if not remote_path.exists():
|
|
@@ -1566,6 +1565,8 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1566
1565
|
current_hash = _read_schema_hash(remote_path)
|
|
1567
1566
|
if current_hash != schema.get("schema_hash"):
|
|
1568
1567
|
_write_driver_files(schema)
|
|
1568
|
+
if _read_schema_hash(remote_path) == current_hash:
|
|
1569
|
+
return
|
|
1569
1570
|
print(
|
|
1570
1571
|
f"Driver for '{device_type}' updated "
|
|
1571
1572
|
f"(was {(current_hash or '?')[:16]}…, "
|
|
@@ -1574,6 +1575,19 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
|
|
|
1574
1575
|
)
|
|
1575
1576
|
|
|
1576
1577
|
|
|
1578
|
+
def _schema_device_type(schema: dict) -> str:
|
|
1579
|
+
if str(schema.get("schema_version")) == "2.0":
|
|
1580
|
+
return _require_package_name(schema.get("device_type"), field="device_type")
|
|
1581
|
+
return _schema_maps(schema)[0]
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
def _is_v2_driver(path: Path) -> bool:
|
|
1585
|
+
try:
|
|
1586
|
+
return "build_uhd_bindings" in path.read_text(encoding="utf-8")
|
|
1587
|
+
except OSError:
|
|
1588
|
+
return False
|
|
1589
|
+
|
|
1590
|
+
|
|
1577
1591
|
def _read_schema_hash(path: Path) -> str | None:
|
|
1578
1592
|
"""Extract _SCHEMA_HASH value from a generated driver file without importing it."""
|
|
1579
1593
|
try:
|
|
@@ -1601,12 +1615,14 @@ def install_driver_if_stale(*, token: str, current_hash: str) -> bool:
|
|
|
1601
1615
|
|
|
1602
1616
|
from remoteRF.drivers.pluto import *
|
|
1603
1617
|
"""
|
|
1604
|
-
schema = fetch_idl(token=token, prefer_v2=
|
|
1618
|
+
schema = fetch_idl(token=token, prefer_v2=True)
|
|
1605
1619
|
if schema.get("schema_hash") == current_hash:
|
|
1606
1620
|
return False
|
|
1607
1621
|
|
|
1608
|
-
device_type =
|
|
1609
|
-
_write_driver_files(schema)
|
|
1622
|
+
device_type = _schema_device_type(schema)
|
|
1623
|
+
pkg_dir = _write_driver_files(schema)
|
|
1624
|
+
if _read_schema_hash(pkg_dir / f"{device_type}_remote.py") == current_hash:
|
|
1625
|
+
return False
|
|
1610
1626
|
print(
|
|
1611
1627
|
f"Driver for '{device_type}' updated "
|
|
1612
1628
|
f"(was {current_hash[:16]}…, now {schema.get('schema_hash','?')[:16]}…).\n"
|
|
@@ -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
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.15
|
|
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
|
|
@@ -16,17 +14,7 @@ Requires-Dist: protobuf<7.0.0,>=6.31.1
|
|
|
16
14
|
Requires-Dist: numpy
|
|
17
15
|
Requires-Dist: prompt_toolkit
|
|
18
16
|
Requires-Dist: python-dotenv
|
|
19
|
-
Requires-Dist: prompt-toolkit
|
|
20
|
-
Dynamic: author
|
|
21
|
-
Dynamic: author-email
|
|
22
|
-
Dynamic: classifier
|
|
23
|
-
Dynamic: description
|
|
24
|
-
Dynamic: description-content-type
|
|
25
|
-
Dynamic: license
|
|
26
17
|
Dynamic: license-file
|
|
27
|
-
Dynamic: requires-dist
|
|
28
|
-
Dynamic: requires-python
|
|
29
|
-
Dynamic: summary
|
|
30
18
|
|
|
31
19
|
# Remote RF
|
|
32
20
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# You should have received a copy of the GNU General Public License
|
|
14
14
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
15
|
|
|
16
|
+
import json
|
|
16
17
|
import os
|
|
17
18
|
import unittest
|
|
18
19
|
import sys
|
|
@@ -1055,3 +1056,78 @@ class DynamicDeviceCodegenTests(unittest.TestCase):
|
|
|
1055
1056
|
|
|
1056
1057
|
if __name__ == "__main__":
|
|
1057
1058
|
unittest.main()
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
class DriverInstallPathTests(unittest.TestCase):
|
|
1062
|
+
"""The default USRP driver must land on Dynamic v2 and stay there."""
|
|
1063
|
+
|
|
1064
|
+
V2 = {
|
|
1065
|
+
"schema_version": "2.0", "device_type": "usrp", "client_class": "MultiUSRP", "driver_version": "t",
|
|
1066
|
+
"native_api": {}, "hardware_profiles": [], "objects": [], "methods": [], "errors": [],
|
|
1067
|
+
"capability_fields": [], "coverage": {}, "deprecations": [],
|
|
1068
|
+
}
|
|
1069
|
+
V1 = {"schema_version": "1.0", "device_type": "usrp", "client_class": "MultiUSRP", "driver_version": "t",
|
|
1070
|
+
"schema_hash": "sha256:v1", "getters": {}, "setters": {}, "calls": {}}
|
|
1071
|
+
|
|
1072
|
+
@classmethod
|
|
1073
|
+
def setUpClass(cls):
|
|
1074
|
+
import hashlib
|
|
1075
|
+
body = json.dumps(cls.V2, sort_keys=True, separators=(",", ":"))
|
|
1076
|
+
cls.V2 = dict(cls.V2, schema_hash="sha256:" + hashlib.sha256(body.encode()).hexdigest())
|
|
1077
|
+
|
|
1078
|
+
def _run(self, respond, call):
|
|
1079
|
+
seen = []
|
|
1080
|
+
|
|
1081
|
+
class Response:
|
|
1082
|
+
def __init__(self, results):
|
|
1083
|
+
self.results = results
|
|
1084
|
+
|
|
1085
|
+
def rpc_client(function_name, args):
|
|
1086
|
+
plain = {k: unmap_arg(v) for k, v in args.items()}
|
|
1087
|
+
seen.append(plain)
|
|
1088
|
+
return Response({"schema": map_arg(json.dumps(respond(plain)))})
|
|
1089
|
+
|
|
1090
|
+
old_rpc_client = fake_grpc_client.rpc_client
|
|
1091
|
+
old_drivers_dir = dynamic_device._DRIVERS_DIR
|
|
1092
|
+
fake_grpc_client.rpc_client = rpc_client
|
|
1093
|
+
try:
|
|
1094
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1095
|
+
dynamic_device._DRIVERS_DIR = Path(temp_dir)
|
|
1096
|
+
result = call()
|
|
1097
|
+
remote = Path(temp_dir) / "usrp" / "usrp_remote.py"
|
|
1098
|
+
text = remote.read_text() if remote.exists() else ""
|
|
1099
|
+
finally:
|
|
1100
|
+
fake_grpc_client.rpc_client = old_rpc_client
|
|
1101
|
+
dynamic_device._DRIVERS_DIR = old_drivers_dir
|
|
1102
|
+
return seen, text, result
|
|
1103
|
+
|
|
1104
|
+
def test_install_asks_the_legacy_endpoint_for_v2_and_writes_a_v2_driver(self):
|
|
1105
|
+
seen, text, _ = self._run(
|
|
1106
|
+
lambda plain: self.V2 if "2.0" in plain.get("schema_versions", []) else self.V1,
|
|
1107
|
+
lambda: dynamic_device.install_driver(token="future-token", device_id=4),
|
|
1108
|
+
)
|
|
1109
|
+
# device_id first: a not-yet-active reservation's token is inert.
|
|
1110
|
+
self.assertEqual(seen, [{"device_id": 4, "schema_versions": ["2.0"]}])
|
|
1111
|
+
self.assertIn("build_uhd_bindings", text)
|
|
1112
|
+
|
|
1113
|
+
def test_a_v1_answer_never_replaces_an_installed_v2_driver(self):
|
|
1114
|
+
def call():
|
|
1115
|
+
dynamic_device._write_driver_files(self.V2)
|
|
1116
|
+
dynamic_device.install_driver(device_id=4)
|
|
1117
|
+
self.assertFalse(dynamic_device.install_driver_if_stale(token="t", current_hash=self.V2["schema_hash"]))
|
|
1118
|
+
|
|
1119
|
+
seen, text, _ = self._run(lambda plain: self.V1, call)
|
|
1120
|
+
self.assertEqual(len(seen), 2)
|
|
1121
|
+
self.assertIn("build_uhd_bindings", text)
|
|
1122
|
+
|
|
1123
|
+
def test_rewriting_a_driver_drops_the_cached_package_so_reimport_works(self):
|
|
1124
|
+
sys.modules["remoteRF.drivers.usrp"] = types.ModuleType("remoteRF.drivers.usrp")
|
|
1125
|
+
sys.modules["remoteRF.drivers.usrp.usrp_remote"] = types.ModuleType("remoteRF.drivers.usrp.usrp_remote")
|
|
1126
|
+
try:
|
|
1127
|
+
self._run(lambda plain: self.V2,
|
|
1128
|
+
lambda: dynamic_device.install_driver_if_stale(token="t", current_hash="sha256:v1"))
|
|
1129
|
+
self.assertNotIn("remoteRF.drivers.usrp", sys.modules)
|
|
1130
|
+
self.assertNotIn("remoteRF.drivers.usrp.usrp_remote", sys.modules)
|
|
1131
|
+
finally:
|
|
1132
|
+
sys.modules.pop("remoteRF.drivers.usrp", None)
|
|
1133
|
+
sys.modules.pop("remoteRF.drivers.usrp.usrp_remote", None)
|
|
@@ -509,20 +509,68 @@ class DynamicV2Tests(unittest.TestCase):
|
|
|
509
509
|
self.assertEqual(encoded.WhichOneof("value"), "json_value")
|
|
510
510
|
self.assertEqual(decode_value(encoded), all_indexes)
|
|
511
511
|
|
|
512
|
-
def
|
|
513
|
-
class
|
|
512
|
+
def test_client_accepts_an_older_upstream_uhd_version(self):
|
|
513
|
+
class OlderTransport(FakeTransport):
|
|
514
514
|
def open_session(self, token, schema_hash):
|
|
515
515
|
result = super().open_session(token, schema_hash)
|
|
516
|
-
result["uhd_version"] = "
|
|
516
|
+
result["uhd_version"] = "3.15.0.0"
|
|
517
517
|
return result
|
|
518
518
|
|
|
519
|
-
transport =
|
|
519
|
+
transport = OlderTransport()
|
|
520
520
|
_, MultiUSRP = build_uhd_bindings(
|
|
521
521
|
schema(), transport_factory=lambda: transport
|
|
522
522
|
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
self.
|
|
523
|
+
device = MultiUSRP("token")
|
|
524
|
+
self.assertEqual(transport.closed_sessions, [])
|
|
525
|
+
self.assertTrue(device.close())
|
|
526
|
+
|
|
527
|
+
def test_binder_is_no_stricter_than_native_uhd(self):
|
|
528
|
+
dc_offset = descriptor(
|
|
529
|
+
"uhd.usrp.MultiUSRP",
|
|
530
|
+
"set_rx_dc_offset",
|
|
531
|
+
[
|
|
532
|
+
overload("offset", [param("offset", "complex"), param("chan", "int", required=False, default=0)]),
|
|
533
|
+
overload("enable", [param("enb", "bool"), param("chan", "int", required=False, default=0)]),
|
|
534
|
+
],
|
|
535
|
+
)
|
|
536
|
+
overload_id, bound = OverloadBinder(dc_offset).bind((0,), {})
|
|
537
|
+
self.assertEqual((overload_id, bound), ("offset", {"offset": 0, "chan": 0}))
|
|
538
|
+
overload_id, _ = OverloadBinder(dc_offset).bind((False,), {})
|
|
539
|
+
self.assertEqual(overload_id, "enable")
|
|
540
|
+
with self.assertRaisesRegex(TypeError, "incompatible arguments"):
|
|
541
|
+
OverloadBinder(dc_offset).bind(("0",), {})
|
|
542
|
+
|
|
543
|
+
optional = descriptor(
|
|
544
|
+
"uhd.usrp.MultiUSRP",
|
|
545
|
+
"get_rx_freq",
|
|
546
|
+
[overload("default", [param("chan", "int", required=False, default=0)], "float")],
|
|
547
|
+
)
|
|
548
|
+
_, bound = OverloadBinder(optional).bind((), {"chan": None})
|
|
549
|
+
self.assertEqual(bound, {"chan": None})
|
|
550
|
+
|
|
551
|
+
phases = descriptor(
|
|
552
|
+
"uhd.usrp.MultiUSRP",
|
|
553
|
+
"phaser_set_phases",
|
|
554
|
+
[overload("default", [param("phases_deg", "list[float]")], "list[float]")],
|
|
555
|
+
)
|
|
556
|
+
OverloadBinder(phases).bind((np.linspace(0, 315, 8),), {}) # numpy arrays are sequences
|
|
557
|
+
self.assertEqual(decode_value(encode_value(np.complex64(1 + 2j))), 1 + 2j)
|
|
558
|
+
|
|
559
|
+
def test_a_stale_v2_driver_refreshes_itself_and_says_so(self):
|
|
560
|
+
from unittest.mock import patch
|
|
561
|
+
|
|
562
|
+
class StaleTransport(FakeTransport):
|
|
563
|
+
def open_session(self, token, schema_hash):
|
|
564
|
+
raise RemoteRFProtocolError(
|
|
565
|
+
"client schema hash is stale",
|
|
566
|
+
details={"requested": schema_hash, "current": "sha256:newer"},
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
_, MultiUSRP = build_uhd_bindings(schema(), transport_factory=StaleTransport)
|
|
570
|
+
with patch("remoteRF.drivers.dynamic_device.install_driver_if_stale", return_value=True) as refresh:
|
|
571
|
+
with self.assertRaisesRegex(RemoteRFProtocolError, "reimport remoteRF.drivers.usrp"):
|
|
572
|
+
MultiUSRP("token")
|
|
573
|
+
refresh.assert_called_once_with(token="token", current_hash=schema()["schema_hash"])
|
|
526
574
|
|
|
527
575
|
def test_client_accepts_conda_forge_uhd_main_release_version(self):
|
|
528
576
|
class CondaForgeTransport(FakeTransport):
|
remoterf-2.0.14/MANIFEST.in
DELETED
remoterf-2.0.14/setup.py
DELETED
|
@@ -1,60 +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 setuptools import setup, find_packages
|
|
17
|
-
import os
|
|
18
|
-
|
|
19
|
-
with open("README.md", "r", encoding="utf-8") as fh:
|
|
20
|
-
long_description = fh.read()
|
|
21
|
-
|
|
22
|
-
setup(
|
|
23
|
-
name="remoterf",
|
|
24
|
-
version="2.0.14",
|
|
25
|
-
author="Ethan Ge",
|
|
26
|
-
author_email="ethoGalaxy@gmail.com",
|
|
27
|
-
description="A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.",
|
|
28
|
-
long_description=long_description, # Set the README content here
|
|
29
|
-
long_description_content_type="text/markdown", # Specify that it's Markdown
|
|
30
|
-
packages=find_packages(where="src"), # Automatically finds subpackages like core, deviceA, deviceB
|
|
31
|
-
package_dir={"": "src"},
|
|
32
|
-
license='GPL-3.0-or-later',
|
|
33
|
-
include_package_data=True, # Includes files specified in MANIFEST.in
|
|
34
|
-
install_requires=[
|
|
35
|
-
"grpcio>=1.78.1,<2.0.0", "protobuf>=6.31.1,<7.0.0", "numpy", "prompt_toolkit", "python-dotenv", "prompt-toolkit"
|
|
36
|
-
],
|
|
37
|
-
classifiers=[
|
|
38
|
-
"Programming Language :: Python :: 3",
|
|
39
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
40
|
-
"Operating System :: OS Independent",
|
|
41
|
-
],
|
|
42
|
-
python_requires='>=3.10',
|
|
43
|
-
# entry_points={
|
|
44
|
-
# 'console_scripts': [
|
|
45
|
-
# 'remoterf-login=remoteRF.core.acc_login:main',
|
|
46
|
-
# 'remoterf-v=remoteRF.core.version:main',
|
|
47
|
-
# 'remoterf-config=remoteRF.config.config_cli:main',
|
|
48
|
-
# ],
|
|
49
|
-
# },
|
|
50
|
-
|
|
51
|
-
entry_points={
|
|
52
|
-
"console_scripts": [
|
|
53
|
-
"remoterf=remoteRF.remoterf_cli:main",
|
|
54
|
-
# "remoterf-login=remoteRF.core.acc_login:main",
|
|
55
|
-
# "remoterf-v=remoteRF.core.version:main",
|
|
56
|
-
# "remoterf-config=remoteRF.config.config_cli:main",
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
)
|
|
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
|