RemoteRF 2.5.1__tar.gz → 2.5.2__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.5.1/src/remoterf.egg-info → remoterf-2.5.2}/PKG-INFO +9 -1
- {remoterf-2.5.1 → remoterf-2.5.2}/README.md +8 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/pyproject.toml +3 -2
- remoterf-2.5.2/setup.py +16 -0
- remoterf-2.5.2/src/_remoterf_alias.py +60 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/direct_path.py +3 -1
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/dynamic_v2_transport.py +3 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/grpc_client.py +45 -6
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/dynamic_device.py +12 -2
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +41 -9
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +39 -7
- {remoterf-2.5.1 → remoterf-2.5.2/src/remoterf.egg-info}/PKG-INFO +9 -1
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf.egg-info/SOURCES.txt +5 -0
- remoterf-2.5.2/src/remoterf.egg-info/top_level.txt +2 -0
- remoterf-2.5.2/tests/test_bundled_drivers.py +57 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_direct_path_global.py +12 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_fetch_idl_fallback.py +19 -0
- remoterf-2.5.2/tests/test_old_import_name.py +31 -0
- remoterf-2.5.2/tests/test_relay_paging.py +63 -0
- remoterf-2.5.1/src/remoterf.egg-info/top_level.txt +0 -1
- {remoterf-2.5.1 → remoterf-2.5.2}/LICENSE +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/setup.cfg +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/grpc.proto +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/grpc/v2_codec.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/tos_notice.txt +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/ansi_codes.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/api_token.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/banner.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/list_string.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/common/utils/process_arg.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/config/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/config/cert_fetcher.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/config/config.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/acc_login.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/app.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/banner_note.txt +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/grpc_acc.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/secure_channel.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/core/v2_errors.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/backend.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/direct.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/homes.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/http.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/deployment/state.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/_virtual.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/dynamic_v2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/support/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/support/uhd.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/usrp/__init__.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/remoterf_cli.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf/version.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/src/remoterf.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_banner.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_batch.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_cli_login_fresh.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_config_address.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_device_routes.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_direct_path_race.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_federated_devices.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_finish_prompt.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_homes.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_native_devices.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_process_arg.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_remoterf_cli.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_rx_readahead.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.5.1 → remoterf-2.5.2}/tests/test_upgrade_rescue.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
5
|
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -52,6 +52,8 @@ source venv/bin/activate # Activate virtual environment
|
|
|
52
52
|
pip install remoterf # Install remoterf
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
The package was named `remoteRF` before 2.2.0. Old code that does `import remoteRF` or `from remoteRF.drivers... import ...` still runs, with a one-time warning; change it to `remoterf`.
|
|
56
|
+
|
|
55
57
|
## NI USRP-2901, Ettus B205-mini, B200-family, and USRP N210
|
|
56
58
|
|
|
57
59
|
The packaged client natively includes the shared USRP schema for the qualified
|
|
@@ -145,6 +147,12 @@ not close the physical radio. `read_samples` and `read_bytes` are bounded to a
|
|
|
145
147
|
4 MiB response. Call them repeatedly for longer captures. Native asynchronous
|
|
146
148
|
callbacks are not transported remotely.
|
|
147
149
|
|
|
150
|
+
Through the RemoteRF Global relay one reply is capped at 768 KiB. A lab on a
|
|
151
|
+
current Server keeps a larger reply (up to 4 MiB) and the client fetches it in
|
|
152
|
+
pages; an older lab refuses it with `response_too_large`, so keep reads there
|
|
153
|
+
to about 98k complex64 samples (`read_samples(65536)` is safe). LAN and direct
|
|
154
|
+
paths are not capped.
|
|
155
|
+
|
|
148
156
|
When a Tailscale address differs from the IP or DNS identity in the server
|
|
149
157
|
certificate, keep TLS verification enabled and set the expected identity:
|
|
150
158
|
|
|
@@ -27,6 +27,8 @@ source venv/bin/activate # Activate virtual environment
|
|
|
27
27
|
pip install remoterf # Install remoterf
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
The package was named `remoteRF` before 2.2.0. Old code that does `import remoteRF` or `from remoteRF.drivers... import ...` still runs, with a one-time warning; change it to `remoterf`.
|
|
31
|
+
|
|
30
32
|
## NI USRP-2901, Ettus B205-mini, B200-family, and USRP N210
|
|
31
33
|
|
|
32
34
|
The packaged client natively includes the shared USRP schema for the qualified
|
|
@@ -120,6 +122,12 @@ not close the physical radio. `read_samples` and `read_bytes` are bounded to a
|
|
|
120
122
|
4 MiB response. Call them repeatedly for longer captures. Native asynchronous
|
|
121
123
|
callbacks are not transported remotely.
|
|
122
124
|
|
|
125
|
+
Through the RemoteRF Global relay one reply is capped at 768 KiB. A lab on a
|
|
126
|
+
current Server keeps a larger reply (up to 4 MiB) and the client fetches it in
|
|
127
|
+
pages; an older lab refuses it with `response_too_large`, so keep reads there
|
|
128
|
+
to about 98k complex64 samples (`read_samples(65536)` is safe). LAN and direct
|
|
129
|
+
paths are not capped.
|
|
130
|
+
|
|
123
131
|
When a Tailscale address differs from the IP or DNS identity in the server
|
|
124
132
|
certificate, keep TLS verification enabled and set the expected identity:
|
|
125
133
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoterf"
|
|
7
|
-
version = "2.5.
|
|
7
|
+
version = "2.5.2"
|
|
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"
|
|
@@ -36,7 +36,8 @@ remoterf = "remoterf.remoterf_cli:main"
|
|
|
36
36
|
[tool.setuptools]
|
|
37
37
|
package-dir = { "" = "src" }
|
|
38
38
|
# src/remoterf.py: see its docstring (upgrades from 2.0.14-2.1.x).
|
|
39
|
-
py
|
|
39
|
+
# src/_remoterf_alias.py: `import remoteRF` (setup.py adds its .pth).
|
|
40
|
+
py-modules = ["remoterf", "_remoterf_alias"]
|
|
40
41
|
|
|
41
42
|
[tool.setuptools.packages.find]
|
|
42
43
|
where = ["src"]
|
remoterf-2.5.2/setup.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
from setuptools.command.build_py import build_py
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BuildPy(build_py):
|
|
8
|
+
"""The wheel also ships _remoterf_alias.pth, so `import remoteRF` works
|
|
9
|
+
(see src/_remoterf_alias.py)."""
|
|
10
|
+
|
|
11
|
+
def run(self):
|
|
12
|
+
super().run()
|
|
13
|
+
Path(self.build_lib, "_remoterf_alias.pth").write_text("import _remoterf_alias\n")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
setup(cmdclass={"build_py": BuildPy})
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
"""``import remoteRF``, the package's name before 2.2.0, still works.
|
|
17
|
+
|
|
18
|
+
_remoterf_alias.pth imports this module at startup. It maps ``remoteRF`` and
|
|
19
|
+
every ``remoteRF.*`` to the same module objects as ``remoterf``, warning once.
|
|
20
|
+
Until something imports ``remoteRF``, it imports nothing (no remoterf, grpc, numpy).
|
|
21
|
+
A ``remoteRF.py`` file cannot do this: on a case-insensitive disk it would
|
|
22
|
+
collide with the ``remoterf.py`` rescue module.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import importlib
|
|
26
|
+
import importlib.machinery
|
|
27
|
+
import sys
|
|
28
|
+
import warnings
|
|
29
|
+
|
|
30
|
+
_OLD, _NEW = "remoteRF", "remoterf"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Alias: # a meta path finder and its loader
|
|
34
|
+
warned = False
|
|
35
|
+
|
|
36
|
+
def find_spec(self, name, path=None, target=None):
|
|
37
|
+
if name == _OLD or name.startswith(_OLD + "."):
|
|
38
|
+
return importlib.machinery.ModuleSpec(name, self)
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def create_module(self, spec):
|
|
42
|
+
if not _Alias.warned:
|
|
43
|
+
_Alias.warned = True
|
|
44
|
+
# FutureWarning: shown by default (a DeprecationWarning is hidden outside __main__).
|
|
45
|
+
# stacklevel skips the import machinery, so it points at the importing line.
|
|
46
|
+
warnings.warn(
|
|
47
|
+
"'remoteRF' was renamed 'remoterf' in 2.2.0; import remoterf instead",
|
|
48
|
+
FutureWarning, stacklevel=2,
|
|
49
|
+
)
|
|
50
|
+
module = importlib.import_module(_NEW + spec.name[len(_OLD):])
|
|
51
|
+
spec.loader_state = module.__spec__
|
|
52
|
+
return module
|
|
53
|
+
|
|
54
|
+
def exec_module(self, module):
|
|
55
|
+
module.__spec__ = module.__spec__.loader_state # loading set it to the alias's spec
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# First: on an upgraded machine a surviving remoteRF folder would load a second copy.
|
|
59
|
+
if not any(isinstance(finder, _Alias) for finder in sys.meta_path):
|
|
60
|
+
sys.meta_path.insert(0, _Alias())
|
|
@@ -904,10 +904,12 @@ def describe() -> str:
|
|
|
904
904
|
return f"relay (direct path unavailable: {_BROKEN}; fix: pip install --force-reinstall remoterf)"
|
|
905
905
|
if not enabled():
|
|
906
906
|
return "relay (direct path disabled: REMOTERF_DIRECT=0)"
|
|
907
|
+
# A Global ref's path has no owner; its calls fall back to Global's relay.
|
|
907
908
|
partners = "; ".join(
|
|
908
909
|
f"partner {_partner_owners[key]}: "
|
|
909
910
|
+ (path.describe() if path.ready else path.describe().replace("relay", "via HOME", 1))
|
|
910
|
-
|
|
911
|
+
if key in _partner_owners else f"Global: {path.describe()}"
|
|
912
|
+
for key, path in list(_partners.items())
|
|
911
913
|
)
|
|
912
914
|
path = _path
|
|
913
915
|
if path is not None:
|
|
@@ -27,6 +27,7 @@ from ..common.grpc import grpc_pb2, grpc_pb2_grpc
|
|
|
27
27
|
from ..common.grpc.v2_codec import decode_value, encode_value
|
|
28
28
|
from .v2_errors import (
|
|
29
29
|
RemoteRFError,
|
|
30
|
+
RemoteRFPolicyError,
|
|
30
31
|
RemoteRFProtocolError,
|
|
31
32
|
RemoteRFReservationError,
|
|
32
33
|
RemoteRFTransportError,
|
|
@@ -96,6 +97,8 @@ class _DevicePlaneStub:
|
|
|
96
97
|
args={"a": map_arg(self._token), "wire": map_arg(request.SerializeToString())},
|
|
97
98
|
)
|
|
98
99
|
except RuntimeError as exc:
|
|
100
|
+
if "response_too_large" in str(exc):
|
|
101
|
+
raise RemoteRFPolicyError(f"{self._label}{exc}") from exc
|
|
99
102
|
# A refusal the HOME or the owner carried in "a": no live
|
|
100
103
|
# reservation, a revoked agreement, a device no longer visible.
|
|
101
104
|
raise RemoteRFReservationError(f"{self._label}{exc}") from exc
|
|
@@ -228,21 +228,32 @@ def _federated_call(route, function_name, args):
|
|
|
228
228
|
refused = "a" in response.results and unmap_arg(response.results["a"]) == "the reservation has ended"
|
|
229
229
|
if not refused:
|
|
230
230
|
return response
|
|
231
|
+
def send(request):
|
|
232
|
+
if ref.startswith("rfg_"):
|
|
233
|
+
return _token_backend().device_token_rpc(
|
|
234
|
+
ref, getattr(route, "deployment_id", None), request.SerializeToString()
|
|
235
|
+
)
|
|
236
|
+
return federated_backend().device_rpc(ref, request.SerializeToString())
|
|
237
|
+
|
|
231
238
|
request = grpc_pb2.GenericRPCRequest(function_name=function_name, args=args)
|
|
232
239
|
for retry in (function_name.endswith(":GET"), False):
|
|
233
240
|
try:
|
|
234
|
-
|
|
235
|
-
raw = _token_backend().device_token_rpc(
|
|
236
|
-
ref, getattr(route, "deployment_id", None), request.SerializeToString()
|
|
237
|
-
)
|
|
238
|
-
else:
|
|
239
|
-
raw = federated_backend().device_rpc(ref, request.SerializeToString())
|
|
241
|
+
raw = send(request)
|
|
240
242
|
break
|
|
241
243
|
except AccountBackendError as exc:
|
|
242
244
|
if retry and exc.code == "internal_error" and exc.provenance == "transport":
|
|
243
245
|
# An older lab drops ~0.1% of relayed results when a clock second
|
|
244
246
|
# ticks mid-call; a GET changes nothing, so it is sent once more.
|
|
245
247
|
continue
|
|
248
|
+
if exc.code == "response_too_large" and exc.provenance == "destination":
|
|
249
|
+
raw = _kept_reply(send, args)
|
|
250
|
+
if raw is not None:
|
|
251
|
+
break
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
"response_too_large [destination]: the reply is over the Global relay's "
|
|
254
|
+
"768 KiB limit for one call and this lab does not page it; read fewer "
|
|
255
|
+
"samples per call (about 98k complex64) or use a direct path"
|
|
256
|
+
) from None
|
|
246
257
|
if ref.startswith("rfg_") and str(exc) == "not_found":
|
|
247
258
|
raise RuntimeError(
|
|
248
259
|
"this RemoteRF Global does not accept device tokens yet; use the device reference"
|
|
@@ -660,6 +671,34 @@ def _rx_readahead(prefix, args, token):
|
|
|
660
671
|
return grpc_pb2.GenericRPCResponse(results={"rx": first})
|
|
661
672
|
|
|
662
673
|
|
|
674
|
+
# A newer lab keeps a reply too large for the relay (up to 4 MiB) for its
|
|
675
|
+
# caller, who fetches it in pages; an older lab refuses the first page unrun.
|
|
676
|
+
_PAGE_FUNCTION = "Federation:page:CALL2"
|
|
677
|
+
_MAX_KEPT_REPLY = 4 * 1024 * 1024 + 65536
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _kept_reply(send, args):
|
|
681
|
+
from ..deployment.http import AccountBackendError
|
|
682
|
+
data, nonce = b"", ""
|
|
683
|
+
while True:
|
|
684
|
+
page_args = {"arg1": map_arg(len(data)), "arg2": map_arg(nonce)}
|
|
685
|
+
if "a" in args:
|
|
686
|
+
page_args["a"] = args["a"]
|
|
687
|
+
try:
|
|
688
|
+
raw = send(grpc_pb2.GenericRPCRequest(function_name=_PAGE_FUNCTION, args=page_args))
|
|
689
|
+
except AccountBackendError:
|
|
690
|
+
return None
|
|
691
|
+
reply = grpc_pb2.GenericRPCResponse.FromString(raw)
|
|
692
|
+
if not {"page", "size", "nonce"} <= set(reply.results):
|
|
693
|
+
return None
|
|
694
|
+
page, size, nonce = (unmap_arg(reply.results[key]) for key in ("page", "size", "nonce"))
|
|
695
|
+
if not isinstance(page, bytes) or not page or type(size) is not int or size > _MAX_KEPT_REPLY:
|
|
696
|
+
return None
|
|
697
|
+
data += page
|
|
698
|
+
if len(data) >= size:
|
|
699
|
+
return data if len(data) == size else None
|
|
700
|
+
|
|
701
|
+
|
|
663
702
|
# Device tokens whose Server answered an rx in ci16: it decodes ci16 too (the
|
|
664
703
|
# same Gate I slice 3 change), so their tx may go out that way.
|
|
665
704
|
_ci16_tokens: set = set()
|
|
@@ -252,6 +252,9 @@ def _validate_client_modules(value) -> dict:
|
|
|
252
252
|
out = {}
|
|
253
253
|
for alias, module_path in value.items():
|
|
254
254
|
alias = _require_identifier(alias, field="client_modules alias")
|
|
255
|
+
module_path = str(module_path or "").strip()
|
|
256
|
+
if module_path.startswith("remoteRF."): # Servers still name the package as before 2.2.0
|
|
257
|
+
module_path = "remoterf." + module_path[len("remoteRF."):]
|
|
255
258
|
module_path = _require_dotted_identifier(
|
|
256
259
|
module_path,
|
|
257
260
|
field=f"client_modules[{alias!r}]",
|
|
@@ -1415,7 +1418,8 @@ _DRIVERS_DIR = Path(__file__).parent
|
|
|
1415
1418
|
def _write_driver_files(schema: dict) -> Path:
|
|
1416
1419
|
pkg_dir = _write_v2_driver_files(schema) if str(schema.get("schema_version")) == "2.0" else _write_v1_driver_files(schema)
|
|
1417
1420
|
stale = f"remoterf.drivers.{pkg_dir.name}"
|
|
1418
|
-
|
|
1421
|
+
old = "remoteRF" + stale[len("remoterf"):] # the `import remoteRF` alias keys
|
|
1422
|
+
for name in [m for m in sys.modules if m in (stale, old) or m.startswith((stale + ".", old + "."))]:
|
|
1419
1423
|
del sys.modules[name]
|
|
1420
1424
|
return pkg_dir
|
|
1421
1425
|
|
|
@@ -1685,7 +1689,13 @@ def install_driver_if_stale(*, token: str, current_hash: str) -> bool:
|
|
|
1685
1689
|
return False
|
|
1686
1690
|
|
|
1687
1691
|
device_type = _schema_device_type(schema)
|
|
1688
|
-
|
|
1692
|
+
try:
|
|
1693
|
+
pkg_dir = _write_driver_files(schema)
|
|
1694
|
+
except OSError as exc:
|
|
1695
|
+
# A read-only install (system site-packages): the installed driver keeps working.
|
|
1696
|
+
_verified.add(key)
|
|
1697
|
+
print(f"Driver for '{device_type}' is out of date and could not be updated ({exc}).")
|
|
1698
|
+
return False
|
|
1689
1699
|
if _read_schema_hash(pkg_dir / f"{device_type}_remote.py") == current_hash:
|
|
1690
1700
|
return False
|
|
1691
1701
|
print(
|
|
@@ -14,21 +14,35 @@
|
|
|
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: rtl_sdr driver_version: 0.1.1 schema_hash: sha256:
|
|
17
|
+
# device_type: rtl_sdr driver_version: 0.1.1 schema_hash: sha256:6603ec8e642c87073c35202980de9a3d9e4bb6eb88967686d3810c389fb4fb97
|
|
18
18
|
|
|
19
19
|
_PREFIX = "Rtl_sdr"
|
|
20
|
-
_SCHEMA_HASH = "sha256:
|
|
20
|
+
_SCHEMA_HASH = "sha256:6603ec8e642c87073c35202980de9a3d9e4bb6eb88967686d3810c389fb4fb97"
|
|
21
21
|
_CLIENT_MODULES = {}
|
|
22
22
|
_CLIENT_OBJECTS = {}
|
|
23
23
|
|
|
24
|
+
import os
|
|
25
|
+
|
|
24
26
|
from importlib import import_module as _import_module
|
|
25
27
|
|
|
26
|
-
from ...core.grpc_client import rpc_client
|
|
27
28
|
from ...common.utils import map_arg, unmap_arg
|
|
29
|
+
from .._virtual import (
|
|
30
|
+
is_virtual_token,
|
|
31
|
+
make_virtual_token,
|
|
32
|
+
virtual_call,
|
|
33
|
+
virtual_get,
|
|
34
|
+
virtual_set,
|
|
35
|
+
)
|
|
28
36
|
|
|
29
37
|
_NO_ARG = object()
|
|
30
38
|
|
|
31
39
|
|
|
40
|
+
def _rpc_client(*, function_name, args):
|
|
41
|
+
from ...core.grpc_client import rpc_client
|
|
42
|
+
|
|
43
|
+
return rpc_client(function_name=function_name, args=args)
|
|
44
|
+
|
|
45
|
+
|
|
32
46
|
for _alias, _module_path in _CLIENT_MODULES.items():
|
|
33
47
|
globals()[_alias] = _import_module(_module_path)
|
|
34
48
|
|
|
@@ -71,27 +85,34 @@ def _wrap_client_return(spec, self_obj, result):
|
|
|
71
85
|
|
|
72
86
|
|
|
73
87
|
def _try_get(prop, token):
|
|
74
|
-
|
|
88
|
+
if is_virtual_token(token):
|
|
89
|
+
return virtual_get(token, prop)
|
|
90
|
+
return unmap_arg(_rpc_client(
|
|
75
91
|
function_name=f"{_PREFIX}:{prop}:GET",
|
|
76
92
|
args={'a': map_arg(token)},
|
|
77
93
|
).results[prop])
|
|
78
94
|
|
|
79
95
|
|
|
80
96
|
def _try_set(prop, value, token):
|
|
81
|
-
|
|
97
|
+
if is_virtual_token(token):
|
|
98
|
+
virtual_set(token, prop, value)
|
|
99
|
+
return None
|
|
100
|
+
_rpc_client(
|
|
82
101
|
function_name=f"{_PREFIX}:{prop}:SET",
|
|
83
102
|
args={prop: map_arg(value), 'a': map_arg(token)},
|
|
84
103
|
)
|
|
85
104
|
|
|
86
105
|
|
|
87
106
|
def _try_call(prop, token, arg=_NO_ARG):
|
|
107
|
+
if is_virtual_token(token):
|
|
108
|
+
return virtual_call(token, prop, arg, has_arg=arg is not _NO_ARG)
|
|
88
109
|
if arg is _NO_ARG:
|
|
89
|
-
resp =
|
|
110
|
+
resp = _rpc_client(
|
|
90
111
|
function_name=f"{_PREFIX}:{prop}:CALL0",
|
|
91
112
|
args={'a': map_arg(token)},
|
|
92
113
|
)
|
|
93
114
|
else:
|
|
94
|
-
resp =
|
|
115
|
+
resp = _rpc_client(
|
|
95
116
|
function_name=f"{_PREFIX}:{prop}:CALL1",
|
|
96
117
|
args={'a': map_arg(token), 'arg1': map_arg(arg)},
|
|
97
118
|
)
|
|
@@ -100,12 +121,15 @@ def _try_call(prop, token, arg=_NO_ARG):
|
|
|
100
121
|
|
|
101
122
|
|
|
102
123
|
def _try_calln(prop, token, kwargs):
|
|
124
|
+
if is_virtual_token(token):
|
|
125
|
+
filtered = {key: value for key, value in dict(kwargs).items() if value is not _NO_ARG}
|
|
126
|
+
return virtual_call(token, prop, filtered, has_arg=True)
|
|
103
127
|
payload = {'a': map_arg(token)}
|
|
104
128
|
for key, value in dict(kwargs).items():
|
|
105
129
|
if value is _NO_ARG:
|
|
106
130
|
continue
|
|
107
131
|
payload[str(key)] = map_arg(value)
|
|
108
|
-
resp =
|
|
132
|
+
resp = _rpc_client(
|
|
109
133
|
function_name=f"{_PREFIX}:{prop}:CALLN",
|
|
110
134
|
args=payload,
|
|
111
135
|
)
|
|
@@ -114,7 +138,15 @@ def _try_calln(prop, token, kwargs):
|
|
|
114
138
|
|
|
115
139
|
class RtlSdr:
|
|
116
140
|
|
|
117
|
-
def __init__(self, token: str):
|
|
141
|
+
def __init__(self, token: str = None, *, virtual: bool = False):
|
|
142
|
+
self.virtual = bool(virtual)
|
|
143
|
+
if self.virtual:
|
|
144
|
+
self.token = make_virtual_token(token, device_type="rtl_sdr")
|
|
145
|
+
return
|
|
146
|
+
if token is None:
|
|
147
|
+
token = os.getenv("REMOTERF_TOKEN")
|
|
148
|
+
if not token:
|
|
149
|
+
raise ValueError("A reservation token is required; pass token= or set REMOTERF_TOKEN")
|
|
118
150
|
self.token = token
|
|
119
151
|
from ..dynamic_device import install_driver_if_stale
|
|
120
152
|
install_driver_if_stale(token=token, current_hash=_SCHEMA_HASH)
|
|
@@ -21,14 +21,28 @@ _SCHEMA_HASH = "sha256:320a89d95075a472d7899ff2f98b9fdee05c0be96fa5651bf5c4f0e5d
|
|
|
21
21
|
_CLIENT_MODULES = {'ti_mmwave': 'remoterf.drivers.support.ti_mmwave'}
|
|
22
22
|
_CLIENT_OBJECTS = {}
|
|
23
23
|
|
|
24
|
+
import os
|
|
25
|
+
|
|
24
26
|
from importlib import import_module as _import_module
|
|
25
27
|
|
|
26
|
-
from ...core.grpc_client import rpc_client
|
|
27
28
|
from ...common.utils import map_arg, unmap_arg
|
|
29
|
+
from .._virtual import (
|
|
30
|
+
is_virtual_token,
|
|
31
|
+
make_virtual_token,
|
|
32
|
+
virtual_call,
|
|
33
|
+
virtual_get,
|
|
34
|
+
virtual_set,
|
|
35
|
+
)
|
|
28
36
|
|
|
29
37
|
_NO_ARG = object()
|
|
30
38
|
|
|
31
39
|
|
|
40
|
+
def _rpc_client(*, function_name, args):
|
|
41
|
+
from ...core.grpc_client import rpc_client
|
|
42
|
+
|
|
43
|
+
return rpc_client(function_name=function_name, args=args)
|
|
44
|
+
|
|
45
|
+
|
|
32
46
|
for _alias, _module_path in _CLIENT_MODULES.items():
|
|
33
47
|
globals()[_alias] = _import_module(_module_path)
|
|
34
48
|
|
|
@@ -71,27 +85,34 @@ def _wrap_client_return(spec, self_obj, result):
|
|
|
71
85
|
|
|
72
86
|
|
|
73
87
|
def _try_get(prop, token):
|
|
74
|
-
|
|
88
|
+
if is_virtual_token(token):
|
|
89
|
+
return virtual_get(token, prop)
|
|
90
|
+
return unmap_arg(_rpc_client(
|
|
75
91
|
function_name=f"{_PREFIX}:{prop}:GET",
|
|
76
92
|
args={'a': map_arg(token)},
|
|
77
93
|
).results[prop])
|
|
78
94
|
|
|
79
95
|
|
|
80
96
|
def _try_set(prop, value, token):
|
|
81
|
-
|
|
97
|
+
if is_virtual_token(token):
|
|
98
|
+
virtual_set(token, prop, value)
|
|
99
|
+
return None
|
|
100
|
+
_rpc_client(
|
|
82
101
|
function_name=f"{_PREFIX}:{prop}:SET",
|
|
83
102
|
args={prop: map_arg(value), 'a': map_arg(token)},
|
|
84
103
|
)
|
|
85
104
|
|
|
86
105
|
|
|
87
106
|
def _try_call(prop, token, arg=_NO_ARG):
|
|
107
|
+
if is_virtual_token(token):
|
|
108
|
+
return virtual_call(token, prop, arg, has_arg=arg is not _NO_ARG)
|
|
88
109
|
if arg is _NO_ARG:
|
|
89
|
-
resp =
|
|
110
|
+
resp = _rpc_client(
|
|
90
111
|
function_name=f"{_PREFIX}:{prop}:CALL0",
|
|
91
112
|
args={'a': map_arg(token)},
|
|
92
113
|
)
|
|
93
114
|
else:
|
|
94
|
-
resp =
|
|
115
|
+
resp = _rpc_client(
|
|
95
116
|
function_name=f"{_PREFIX}:{prop}:CALL1",
|
|
96
117
|
args={'a': map_arg(token), 'arg1': map_arg(arg)},
|
|
97
118
|
)
|
|
@@ -100,12 +121,15 @@ def _try_call(prop, token, arg=_NO_ARG):
|
|
|
100
121
|
|
|
101
122
|
|
|
102
123
|
def _try_calln(prop, token, kwargs):
|
|
124
|
+
if is_virtual_token(token):
|
|
125
|
+
filtered = {key: value for key, value in dict(kwargs).items() if value is not _NO_ARG}
|
|
126
|
+
return virtual_call(token, prop, filtered, has_arg=True)
|
|
103
127
|
payload = {'a': map_arg(token)}
|
|
104
128
|
for key, value in dict(kwargs).items():
|
|
105
129
|
if value is _NO_ARG:
|
|
106
130
|
continue
|
|
107
131
|
payload[str(key)] = map_arg(value)
|
|
108
|
-
resp =
|
|
132
|
+
resp = _rpc_client(
|
|
109
133
|
function_name=f"{_PREFIX}:{prop}:CALLN",
|
|
110
134
|
args=payload,
|
|
111
135
|
)
|
|
@@ -114,7 +138,15 @@ def _try_calln(prop, token, kwargs):
|
|
|
114
138
|
|
|
115
139
|
class TiMmWave:
|
|
116
140
|
|
|
117
|
-
def __init__(self, token: str):
|
|
141
|
+
def __init__(self, token: str = None, *, virtual: bool = False):
|
|
142
|
+
self.virtual = bool(virtual)
|
|
143
|
+
if self.virtual:
|
|
144
|
+
self.token = make_virtual_token(token, device_type="ti_mmwave")
|
|
145
|
+
return
|
|
146
|
+
if token is None:
|
|
147
|
+
token = os.getenv("REMOTERF_TOKEN")
|
|
148
|
+
if not token:
|
|
149
|
+
raise ValueError("A reservation token is required; pass token= or set REMOTERF_TOKEN")
|
|
118
150
|
self.token = token
|
|
119
151
|
from ..dynamic_device import install_driver_if_stale
|
|
120
152
|
install_driver_if_stale(token=token, current_hash=_SCHEMA_HASH)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.2
|
|
4
4
|
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
5
|
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -52,6 +52,8 @@ source venv/bin/activate # Activate virtual environment
|
|
|
52
52
|
pip install remoterf # Install remoterf
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
The package was named `remoteRF` before 2.2.0. Old code that does `import remoteRF` or `from remoteRF.drivers... import ...` still runs, with a one-time warning; change it to `remoterf`.
|
|
56
|
+
|
|
55
57
|
## NI USRP-2901, Ettus B205-mini, B200-family, and USRP N210
|
|
56
58
|
|
|
57
59
|
The packaged client natively includes the shared USRP schema for the qualified
|
|
@@ -145,6 +147,12 @@ not close the physical radio. `read_samples` and `read_bytes` are bounded to a
|
|
|
145
147
|
4 MiB response. Call them repeatedly for longer captures. Native asynchronous
|
|
146
148
|
callbacks are not transported remotely.
|
|
147
149
|
|
|
150
|
+
Through the RemoteRF Global relay one reply is capped at 768 KiB. A lab on a
|
|
151
|
+
current Server keeps a larger reply (up to 4 MiB) and the client fetches it in
|
|
152
|
+
pages; an older lab refuses it with `response_too_large`, so keep reads there
|
|
153
|
+
to about 98k complex64 samples (`read_samples(65536)` is safe). LAN and direct
|
|
154
|
+
paths are not capped.
|
|
155
|
+
|
|
148
156
|
When a Tailscale address differs from the IP or DNS identity in the server
|
|
149
157
|
certificate, keep TLS verification enabled and set the expected identity:
|
|
150
158
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/_remoterf_alias.py
|
|
4
6
|
src/remoterf.py
|
|
5
7
|
src/remoterf/__init__.py
|
|
6
8
|
src/remoterf/remoterf_cli.py
|
|
@@ -70,6 +72,7 @@ src/remoterf/drivers/usrp/usrp_remote.py
|
|
|
70
72
|
src/remoterf/drivers/usrp/usrp_remote.pyi
|
|
71
73
|
tests/test_banner.py
|
|
72
74
|
tests/test_batch.py
|
|
75
|
+
tests/test_bundled_drivers.py
|
|
73
76
|
tests/test_cli_login_fresh.py
|
|
74
77
|
tests/test_config_address.py
|
|
75
78
|
tests/test_deployment_accounts.py
|
|
@@ -88,9 +91,11 @@ tests/test_grpc_client_target.py
|
|
|
88
91
|
tests/test_homes.py
|
|
89
92
|
tests/test_native_account_credentials.py
|
|
90
93
|
tests/test_native_devices.py
|
|
94
|
+
tests/test_old_import_name.py
|
|
91
95
|
tests/test_partner_dynamic_v2.py
|
|
92
96
|
tests/test_pluto_virtual.py
|
|
93
97
|
tests/test_process_arg.py
|
|
98
|
+
tests/test_relay_paging.py
|
|
94
99
|
tests/test_remoterf_cli.py
|
|
95
100
|
tests/test_rx_readahead.py
|
|
96
101
|
tests/test_ti_mmwave.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Each bundled driver is what the client would generate from the Server's
|
|
2
|
+
schema, so a first device call does not rewrite site-packages."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from remoterf.drivers import dynamic_device
|
|
15
|
+
|
|
16
|
+
SERVER_SRC = Path(
|
|
17
|
+
os.environ.get("REMOTERF_SERVER_SRC", Path(__file__).resolve().parents[2] / "RemoteRF-Server" / "src")
|
|
18
|
+
)
|
|
19
|
+
SCHEMAS = {
|
|
20
|
+
"adalm_pluto": "remoteRF_server.drivers.adalm_pluto.pluto_schema:PlutoSchema",
|
|
21
|
+
"hackrf": "remoteRF_server.drivers.hackrf.hackrf_schema:HackRFSchema",
|
|
22
|
+
"rtl_sdr": "remoteRF_server.drivers.rtl_sdr.rtl_sdr_schema:RtlSdrSchema",
|
|
23
|
+
"ti_mmwave": "remoteRF_server.drivers.ti_mmwave.ti_mmwave_schema:TiMmWaveSchema",
|
|
24
|
+
}
|
|
25
|
+
# A subprocess: the Server package stays out of this process's imports.
|
|
26
|
+
DUMP = """
|
|
27
|
+
import importlib, json, sys, types
|
|
28
|
+
try:
|
|
29
|
+
import adi
|
|
30
|
+
except ImportError:
|
|
31
|
+
sys.modules["adi"] = types.ModuleType("adi") # the Pluto schema imports pyadi at load
|
|
32
|
+
out = {}
|
|
33
|
+
for device_type, spec in json.loads(sys.argv[1]).items():
|
|
34
|
+
module, cls = spec.split(":")
|
|
35
|
+
out[device_type] = getattr(importlib.import_module(module), cls)().get_idl()
|
|
36
|
+
print(json.dumps(out))
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture(scope="module")
|
|
41
|
+
def server_schemas():
|
|
42
|
+
if not (SERVER_SRC / "remoteRF_server").is_dir():
|
|
43
|
+
pytest.skip(f"no Server source at {SERVER_SRC} (set REMOTERF_SERVER_SRC)")
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
[sys.executable, "-c", DUMP, json.dumps(SCHEMAS)],
|
|
46
|
+
env={**os.environ, "PYTHONPATH": str(SERVER_SRC)},
|
|
47
|
+
capture_output=True, text=True, check=True,
|
|
48
|
+
)
|
|
49
|
+
return json.loads(result.stdout.splitlines()[-1])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.parametrize("device_type", sorted(SCHEMAS))
|
|
53
|
+
def test_bundled_driver_matches_the_server_schema(server_schemas, device_type):
|
|
54
|
+
schema = server_schemas[device_type]
|
|
55
|
+
path = dynamic_device._DRIVERS_DIR / device_type / f"{device_type}_remote.py"
|
|
56
|
+
assert dynamic_device._read_schema_hash(path) == schema["schema_hash"]
|
|
57
|
+
assert path.read_text(encoding="utf-8") == dynamic_device._codegen(schema)
|
|
@@ -137,3 +137,15 @@ def test_the_login_backend_is_built_once_when_a_path_thread_asks_too(monkeypatch
|
|
|
137
137
|
for thread in threads:
|
|
138
138
|
thread.join(5)
|
|
139
139
|
assert len(built) == 1 and got == built * 2
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_describe_reports_a_global_path(fresh, monkeypatch):
|
|
143
|
+
monkeypatch.setattr(direct_path, "_from_stored_login", lambda: None)
|
|
144
|
+
monkeypatch.setattr(grpc_client, "_current_profile", lambda: None)
|
|
145
|
+
monkeypatch.setattr(direct_path, "_active_route", lambda profile: None)
|
|
146
|
+
path = direct_path.global_path(REF)
|
|
147
|
+
path.describe = lambda: "direct 192.0.2.7:4433 (48 calls)"
|
|
148
|
+
assert direct_path.describe() == "Global: direct 192.0.2.7:4433 (48 calls)"
|
|
149
|
+
path.state = "down"
|
|
150
|
+
path.describe = lambda: "relay (direct path down: timed out; retrying)"
|
|
151
|
+
assert direct_path.describe() == "Global: relay (direct path down: timed out; retrying)"
|
|
@@ -96,3 +96,22 @@ def test_a_v1_hash_that_differs_still_lets_v2_decide_and_reinstall(monkeypatch,
|
|
|
96
96
|
# A stale v1 driver: reinstalled from what v2 says, exactly as before.
|
|
97
97
|
assert dynamic_device.install_driver_if_stale(token="tok5", current_hash="sha256:old") is True
|
|
98
98
|
assert written == [v2]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_a_read_only_install_keeps_its_driver(monkeypatch, capsys):
|
|
102
|
+
import remoterf.drivers.dynamic_v2 as dynamic_v2
|
|
103
|
+
|
|
104
|
+
def v1_only(token):
|
|
105
|
+
raise v2_errors.RemoteRFProtocolError("partner devices use the v1 device plane")
|
|
106
|
+
|
|
107
|
+
monkeypatch.setattr(dynamic_v2, "fetch_schema_v2", v1_only)
|
|
108
|
+
calls = _schema_rpc(monkeypatch, "sha256:new")
|
|
109
|
+
|
|
110
|
+
def read_only(schema):
|
|
111
|
+
raise PermissionError(13, "Permission denied")
|
|
112
|
+
|
|
113
|
+
monkeypatch.setattr(dynamic_device, "_write_driver_files", read_only)
|
|
114
|
+
assert dynamic_device.install_driver_if_stale(token="tok5", current_hash="sha256:old") is False
|
|
115
|
+
assert "could not be updated" in capsys.readouterr().out
|
|
116
|
+
dynamic_device.install_driver_if_stale(token="tok5", current_hash="sha256:old")
|
|
117
|
+
assert len(calls) == 2 # fetched once (v1, then the v2 probe's fallback), not per device
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Code written before 2.2.0 imports remoteRF: it gets the remoterf modules
|
|
2
|
+
themselves, with one FutureWarning pointing at that code."""
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
SRC = Path(__file__).resolve().parents[1] / "src"
|
|
9
|
+
PROBE = """
|
|
10
|
+
import site, sys, warnings
|
|
11
|
+
sys.path.insert(0, sys.argv[1])
|
|
12
|
+
site.addsitedir(sys.argv[2]) # runs the .pth, as site-packages does at startup
|
|
13
|
+
assert not {"remoterf", "grpc", "numpy"} & set(sys.modules) # the hook alone stays light
|
|
14
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
15
|
+
warnings.simplefilter("always")
|
|
16
|
+
import remoteRF
|
|
17
|
+
from remoteRF.drivers.adalm_pluto import Pluto
|
|
18
|
+
import remoteRF.drivers.adalm_pluto as old
|
|
19
|
+
import remoterf
|
|
20
|
+
import remoterf.drivers.adalm_pluto as new
|
|
21
|
+
assert remoteRF is remoterf and old is new and Pluto is new.Pluto
|
|
22
|
+
assert remoterf.__spec__.name == "remoterf" and new.__spec__.name == "remoterf.drivers.adalm_pluto"
|
|
23
|
+
assert [(w.category, w.filename) for w in caught if "remoteRF" in str(w.message)] == [(FutureWarning, "<string>")], caught
|
|
24
|
+
print("ok")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_import_remoteRF_aliases_remoterf_with_one_warning(tmp_path):
|
|
29
|
+
(tmp_path / "_remoterf_alias.pth").write_text("import _remoterf_alias\n") # what setup.py ships
|
|
30
|
+
result = subprocess.run([sys.executable, "-c", PROBE, str(SRC), str(tmp_path)], capture_output=True, text=True)
|
|
31
|
+
assert result.stdout.strip() == "ok", result.stderr
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from remoterf.common.grpc import grpc_pb2
|
|
4
|
+
from remoterf.common.utils import map_arg, unmap_arg
|
|
5
|
+
from remoterf.core import direct_path, dynamic_v2_transport, grpc_client
|
|
6
|
+
from remoterf.core.v2_errors import RemoteRFPolicyError
|
|
7
|
+
from remoterf.deployment.http import AccountBackendError
|
|
8
|
+
|
|
9
|
+
REF = "00000000-0000-4000-8000-000000000001:7"
|
|
10
|
+
WHOLE = grpc_pb2.GenericRPCResponse(results={"rx": map_arg(bytes(range(256)) * 4000)}).SerializeToString()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Lab:
|
|
14
|
+
def __init__(self, pages=True):
|
|
15
|
+
self.pages = pages
|
|
16
|
+
self.requests = []
|
|
17
|
+
|
|
18
|
+
def device_rpc(self, ref, raw):
|
|
19
|
+
request = grpc_pb2.GenericRPCRequest.FromString(raw)
|
|
20
|
+
self.requests.append(request)
|
|
21
|
+
if request.function_name != "Federation:page:CALL2":
|
|
22
|
+
raise AccountBackendError("response_too_large", provenance="destination")
|
|
23
|
+
if not self.pages: # an older lab: not a device function
|
|
24
|
+
raise AccountBackendError("device_unavailable", provenance="destination")
|
|
25
|
+
offset = unmap_arg(request.args["arg1"])
|
|
26
|
+
return grpc_pb2.GenericRPCResponse(results={
|
|
27
|
+
"page": map_arg(WHOLE[offset:offset + 600_000]),
|
|
28
|
+
"size": map_arg(len(WHOLE)),
|
|
29
|
+
"nonce": map_arg("n1"),
|
|
30
|
+
}).SerializeToString()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def lab(monkeypatch):
|
|
35
|
+
lab = Lab()
|
|
36
|
+
monkeypatch.setattr(direct_path, "wants", lambda function_name: False)
|
|
37
|
+
monkeypatch.setattr(grpc_client, "federated_backend", lambda: lab)
|
|
38
|
+
return lab
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_an_oversized_reply_is_fetched_in_pages(lab):
|
|
42
|
+
response = grpc_client._federated_call(REF, "Rtl_sdr:read_samples:CALL1", {"a": map_arg(REF)})
|
|
43
|
+
assert response.SerializeToString() == WHOLE
|
|
44
|
+
pages = lab.requests[1:]
|
|
45
|
+
assert [unmap_arg(r.args["arg1"]) for r in pages] == [0, 600_000]
|
|
46
|
+
assert [unmap_arg(r.args["arg2"]) for r in pages] == ["", "n1"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_an_older_lab_gets_a_clear_error(lab):
|
|
50
|
+
lab.pages = False
|
|
51
|
+
with pytest.raises(RuntimeError, match=r"response_too_large \[destination\].*768 KiB"):
|
|
52
|
+
grpc_client._federated_call(REF, "Rtl_sdr:read_samples:CALL1", {"a": map_arg(REF)})
|
|
53
|
+
assert len(lab.requests) == 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_dynamic_v2_reports_it_as_a_policy_error(monkeypatch):
|
|
57
|
+
def refused(**_):
|
|
58
|
+
raise RuntimeError("response_too_large [destination]: over the relay limit")
|
|
59
|
+
|
|
60
|
+
monkeypatch.setattr(grpc_client, "rpc_client", refused)
|
|
61
|
+
stub = dynamic_v2_transport._DevicePlaneStub("token")
|
|
62
|
+
with pytest.raises(RemoteRFPolicyError, match="response_too_large"):
|
|
63
|
+
stub.Invoke(grpc_pb2.InvokeRequest())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
remoterf
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf-2.5.1 → remoterf-2.5.2}/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
|
|
File without changes
|
|
File without changes
|