RemoteRF 2.0.7__tar.gz → 2.0.8__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.7/src/remoterf.egg-info → remoterf-2.0.8}/PKG-INFO +78 -3
- {remoterf-2.0.7 → remoterf-2.0.8}/README.md +77 -2
- {remoterf-2.0.7 → remoterf-2.0.8}/setup.py +1 -1
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/grpc_client.py +12 -0
- remoterf-2.0.8/src/remoteRF/drivers/hackrf/__init__.py +6 -0
- remoterf-2.0.8/src/remoteRF/drivers/hackrf/hackrf_remote.py +216 -0
- remoterf-2.0.8/src/remoteRF/drivers/rtl_sdr/__init__.py +6 -0
- remoterf-2.0.8/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +244 -0
- remoterf-2.0.8/src/remoteRF/drivers/support/ti_mmwave.py +295 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/support/uhd_v2.py +17 -1
- remoterf-2.0.8/src/remoteRF/drivers/ti_mmwave/__init__.py +15 -0
- remoterf-2.0.8/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +163 -0
- remoterf-2.0.8/src/remoteRF/drivers/usrp/usrp_remote.py +8 -0
- {remoterf-2.0.7 → remoterf-2.0.8/src/remoterf.egg-info}/PKG-INFO +78 -3
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoterf.egg-info/SOURCES.txt +44 -1
- remoterf-2.0.8/src/remoterf.egg-info/top_level.txt +1 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/tests/test_dynamic_device_codegen.py +65 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/tests/test_dynamic_v2.py +58 -0
- remoterf-2.0.8/tests/test_ti_mmwave.py +211 -0
- remoterf-2.0.7/src/remoteRF/drivers/usrp/usrp_remote.py +0 -8
- remoterf-2.0.7/src/remoterf.egg-info/top_level.txt +0 -1
- {remoterf-2.0.7 → remoterf-2.0.8}/MANIFEST.in +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/setup.cfg +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/app.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/grpc_acc.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/dynamic_device.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/dynamic_v2.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/remoterf_cli.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoteRF/version.py +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/src/remoterf.egg-info/requires.txt +0 -0
- {remoterf-2.0.7 → remoterf-2.0.8}/tests/test_process_arg.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.8
|
|
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: Ethan Ge
|
|
6
6
|
Author-email: ethoGalaxy@gmail.com
|
|
@@ -53,9 +53,15 @@ source venv/bin/activate # Activate virtual environment
|
|
|
53
53
|
pip install remoterf # Install remoteRF
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
## NI USRP-2901
|
|
56
|
+
## NI USRP-2901, Ettus B205-mini, B200-family, and USRP N210
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
The packaged client natively includes the shared USRP schema for the qualified
|
|
59
|
+
NI USRP-2901, Ettus Research USRP B205-mini, the broader USB/B200 family, and
|
|
60
|
+
the Ethernet-connected N210. No custom client driver is required. The B205-mini
|
|
61
|
+
uses the same native-style API and reports its live 1 RX/1 TX channel topology
|
|
62
|
+
through `remoterf_capabilities`.
|
|
63
|
+
|
|
64
|
+
After reserving a server-local `usrp` inventory entry, fetch or refresh its
|
|
59
65
|
Dynamic v2 package with the reservation token:
|
|
60
66
|
|
|
61
67
|
```python
|
|
@@ -112,6 +118,75 @@ generic native dispatch, but are not release-qualified as exact UHD overload
|
|
|
112
118
|
parity until the server's UHD 4.10 target-introspection and hardware
|
|
113
119
|
differential gates pass.
|
|
114
120
|
|
|
121
|
+
## RTL-SDR
|
|
122
|
+
|
|
123
|
+
RTL-SDR clients are generated from the server's `rtl_sdr` schema. The machine
|
|
124
|
+
running client code does not need `librtlsdr` or PyRtlSdr:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from remoteRF.drivers import ensure_driver
|
|
128
|
+
|
|
129
|
+
token = "reservation-token"
|
|
130
|
+
ensure_driver(token=token)
|
|
131
|
+
|
|
132
|
+
from remoteRF.drivers.rtl_sdr import RtlSdr
|
|
133
|
+
|
|
134
|
+
sdr = RtlSdr(token)
|
|
135
|
+
sdr.sample_rate = 2_048_000
|
|
136
|
+
sdr.center_freq = 100_000_000
|
|
137
|
+
sdr.gain = "auto"
|
|
138
|
+
sdr.agc_mode = True
|
|
139
|
+
|
|
140
|
+
samples = sdr.read_samples(16_384) # NumPy complex64
|
|
141
|
+
raw_iq = sdr.read_bytes(4096) # packed uint8 I/Q bytes
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The server owns the USB handle for its lifetime, so the generated client does
|
|
145
|
+
not close the physical radio. `read_samples` and `read_bytes` are bounded to a
|
|
146
|
+
4 MiB response. Call them repeatedly for longer captures. Native asynchronous
|
|
147
|
+
callbacks are not transported remotely.
|
|
148
|
+
|
|
149
|
+
When a Tailscale address differs from the IP or DNS identity in the server
|
|
150
|
+
certificate, keep TLS verification enabled and set the expected identity:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
export REMOTERF_ADDR=rrf2.example.ts.net:61005
|
|
154
|
+
export REMOTERF_TLS_SERVER_NAME=certificate-name.example
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## TI mmWave radar
|
|
158
|
+
|
|
159
|
+
TI radar clients are generated from the server's `ti_mmwave` schema. Only the
|
|
160
|
+
machine physically connected to the radar needs `pyserial` or a CP210x driver.
|
|
161
|
+
The initial parser profile supports xWR68xx mmWave SDK 3 out-of-box TLV output.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from remoteRF.drivers import ensure_driver
|
|
165
|
+
|
|
166
|
+
token = "reservation-token"
|
|
167
|
+
ensure_driver(token=token)
|
|
168
|
+
|
|
169
|
+
from remoteRF.drivers.ti_mmwave import TiMmWave, decode_frame
|
|
170
|
+
|
|
171
|
+
radar = TiMmWave(token)
|
|
172
|
+
print(radar.device_info)
|
|
173
|
+
print(radar.query_version())
|
|
174
|
+
|
|
175
|
+
radar.apply_config(open("profile.cfg").read(), start=True)
|
|
176
|
+
try:
|
|
177
|
+
packet = radar.read_frame(timeout=1.0)
|
|
178
|
+
frame = decode_frame(packet)
|
|
179
|
+
print(frame.header.frame_number, frame.points, frame.side_info)
|
|
180
|
+
finally:
|
|
181
|
+
radar.stop()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`read_frame()` returns one complete binary UART packet. Decoding is client-local
|
|
185
|
+
and retains unknown TLVs as raw bytes, allowing custom firmware to be captured
|
|
186
|
+
before a dedicated parser profile is added. `stream_stats` reports dropped
|
|
187
|
+
frames, queued frames, resynchronization bytes, invalid headers, and reader
|
|
188
|
+
health. Remote firmware flashing is deliberately unsupported.
|
|
189
|
+
|
|
115
190
|
If `pip install` doesn't work, you can clone the [source](https://github.com/WirelessLabAtUCLA/RemoteRF-Client) directly from github.
|
|
116
191
|
|
|
117
192
|
<!-- 1. **Clone the repository:**
|
|
@@ -27,9 +27,15 @@ source venv/bin/activate # Activate virtual environment
|
|
|
27
27
|
pip install remoterf # Install remoteRF
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
## NI USRP-2901
|
|
30
|
+
## NI USRP-2901, Ettus B205-mini, B200-family, and USRP N210
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The packaged client natively includes the shared USRP schema for the qualified
|
|
33
|
+
NI USRP-2901, Ettus Research USRP B205-mini, the broader USB/B200 family, and
|
|
34
|
+
the Ethernet-connected N210. No custom client driver is required. The B205-mini
|
|
35
|
+
uses the same native-style API and reports its live 1 RX/1 TX channel topology
|
|
36
|
+
through `remoterf_capabilities`.
|
|
37
|
+
|
|
38
|
+
After reserving a server-local `usrp` inventory entry, fetch or refresh its
|
|
33
39
|
Dynamic v2 package with the reservation token:
|
|
34
40
|
|
|
35
41
|
```python
|
|
@@ -86,6 +92,75 @@ generic native dispatch, but are not release-qualified as exact UHD overload
|
|
|
86
92
|
parity until the server's UHD 4.10 target-introspection and hardware
|
|
87
93
|
differential gates pass.
|
|
88
94
|
|
|
95
|
+
## RTL-SDR
|
|
96
|
+
|
|
97
|
+
RTL-SDR clients are generated from the server's `rtl_sdr` schema. The machine
|
|
98
|
+
running client code does not need `librtlsdr` or PyRtlSdr:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from remoteRF.drivers import ensure_driver
|
|
102
|
+
|
|
103
|
+
token = "reservation-token"
|
|
104
|
+
ensure_driver(token=token)
|
|
105
|
+
|
|
106
|
+
from remoteRF.drivers.rtl_sdr import RtlSdr
|
|
107
|
+
|
|
108
|
+
sdr = RtlSdr(token)
|
|
109
|
+
sdr.sample_rate = 2_048_000
|
|
110
|
+
sdr.center_freq = 100_000_000
|
|
111
|
+
sdr.gain = "auto"
|
|
112
|
+
sdr.agc_mode = True
|
|
113
|
+
|
|
114
|
+
samples = sdr.read_samples(16_384) # NumPy complex64
|
|
115
|
+
raw_iq = sdr.read_bytes(4096) # packed uint8 I/Q bytes
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The server owns the USB handle for its lifetime, so the generated client does
|
|
119
|
+
not close the physical radio. `read_samples` and `read_bytes` are bounded to a
|
|
120
|
+
4 MiB response. Call them repeatedly for longer captures. Native asynchronous
|
|
121
|
+
callbacks are not transported remotely.
|
|
122
|
+
|
|
123
|
+
When a Tailscale address differs from the IP or DNS identity in the server
|
|
124
|
+
certificate, keep TLS verification enabled and set the expected identity:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export REMOTERF_ADDR=rrf2.example.ts.net:61005
|
|
128
|
+
export REMOTERF_TLS_SERVER_NAME=certificate-name.example
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## TI mmWave radar
|
|
132
|
+
|
|
133
|
+
TI radar clients are generated from the server's `ti_mmwave` schema. Only the
|
|
134
|
+
machine physically connected to the radar needs `pyserial` or a CP210x driver.
|
|
135
|
+
The initial parser profile supports xWR68xx mmWave SDK 3 out-of-box TLV output.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from remoteRF.drivers import ensure_driver
|
|
139
|
+
|
|
140
|
+
token = "reservation-token"
|
|
141
|
+
ensure_driver(token=token)
|
|
142
|
+
|
|
143
|
+
from remoteRF.drivers.ti_mmwave import TiMmWave, decode_frame
|
|
144
|
+
|
|
145
|
+
radar = TiMmWave(token)
|
|
146
|
+
print(radar.device_info)
|
|
147
|
+
print(radar.query_version())
|
|
148
|
+
|
|
149
|
+
radar.apply_config(open("profile.cfg").read(), start=True)
|
|
150
|
+
try:
|
|
151
|
+
packet = radar.read_frame(timeout=1.0)
|
|
152
|
+
frame = decode_frame(packet)
|
|
153
|
+
print(frame.header.frame_number, frame.points, frame.side_info)
|
|
154
|
+
finally:
|
|
155
|
+
radar.stop()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`read_frame()` returns one complete binary UART packet. Decoding is client-local
|
|
159
|
+
and retains unknown TLVs as raw bytes, allowing custom firmware to be captured
|
|
160
|
+
before a dedicated parser profile is added. `stream_stats` reports dropped
|
|
161
|
+
frames, queued frames, resynchronization bytes, invalid headers, and reader
|
|
162
|
+
health. Remote firmware flashing is deliberately unsupported.
|
|
163
|
+
|
|
89
164
|
If `pip install` doesn't work, you can clone the [source](https://github.com/WirelessLabAtUCLA/RemoteRF-Client) directly from github.
|
|
90
165
|
|
|
91
166
|
<!-- 1. **Clone the repository:**
|
|
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
6
6
|
|
|
7
7
|
setup(
|
|
8
8
|
name="remoterf",
|
|
9
|
-
version="2.0.
|
|
9
|
+
version="2.0.8",
|
|
10
10
|
author="Ethan Ge",
|
|
11
11
|
author_email="ethoGalaxy@gmail.com",
|
|
12
12
|
description="A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.",
|
|
@@ -49,6 +49,18 @@ options = [
|
|
|
49
49
|
('grpc.max_receive_message_length', 100 * 1024 * 1024),
|
|
50
50
|
]
|
|
51
51
|
|
|
52
|
+
# A server reached over Tailscale may present the same certificate it uses on
|
|
53
|
+
# its public or LAN address. Keep certificate verification enabled while
|
|
54
|
+
# allowing the configured certificate identity to differ from REMOTERF_ADDR.
|
|
55
|
+
tls_server_name = (os.getenv("REMOTERF_TLS_SERVER_NAME") or "").strip()
|
|
56
|
+
if tls_server_name:
|
|
57
|
+
options.extend(
|
|
58
|
+
[
|
|
59
|
+
("grpc.ssl_target_name_override", tls_server_name),
|
|
60
|
+
("grpc.default_authority", tls_server_name),
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
52
64
|
# Server.crt
|
|
53
65
|
certs_path = Path(ca_path).expanduser().resolve()
|
|
54
66
|
with certs_path.open('rb') as f:
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Auto-generated from IDL schema — do not edit by hand.
|
|
2
|
+
# device_type: hackrf driver_version: 0.1.1 schema_hash: sha256:1d6e935e2e33959e753b89d943a1a5696d814ec28aac52a7c39142b3fce89693
|
|
3
|
+
|
|
4
|
+
_PREFIX = "Hackrf"
|
|
5
|
+
_SCHEMA_HASH = "sha256:1d6e935e2e33959e753b89d943a1a5696d814ec28aac52a7c39142b3fce89693"
|
|
6
|
+
_CLIENT_MODULES = {}
|
|
7
|
+
_CLIENT_OBJECTS = {}
|
|
8
|
+
|
|
9
|
+
from importlib import import_module as _import_module
|
|
10
|
+
|
|
11
|
+
from ...core.grpc_client import rpc_client
|
|
12
|
+
from ...common.utils import map_arg, unmap_arg
|
|
13
|
+
|
|
14
|
+
_NO_ARG = object()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
for _alias, _module_path in _CLIENT_MODULES.items():
|
|
18
|
+
globals()[_alias] = _import_module(_module_path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_client_target(target):
|
|
22
|
+
parts = str(target or "").split(".")
|
|
23
|
+
if not parts or not parts[0]:
|
|
24
|
+
raise ValueError(f"Invalid client constructor target: {target!r}")
|
|
25
|
+
if parts[0] not in globals():
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Client helper module {parts[0]!r} is not declared in _CLIENT_MODULES"
|
|
28
|
+
)
|
|
29
|
+
obj = globals()[parts[0]]
|
|
30
|
+
for part in parts[1:]:
|
|
31
|
+
obj = getattr(obj, part)
|
|
32
|
+
return obj
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _client_ctor_arg(spec, self_obj, result):
|
|
36
|
+
if spec == "$self":
|
|
37
|
+
return self_obj
|
|
38
|
+
if spec == "$result":
|
|
39
|
+
return result
|
|
40
|
+
if isinstance(spec, dict) and "const" in spec:
|
|
41
|
+
return spec["const"]
|
|
42
|
+
return spec
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _wrap_client_return(spec, self_obj, result):
|
|
46
|
+
if not spec:
|
|
47
|
+
return result
|
|
48
|
+
if spec.get("kind") != "constructor":
|
|
49
|
+
raise ValueError(f"Unsupported client_return metadata: {spec!r}")
|
|
50
|
+
ctor = _resolve_client_target(spec.get("target"))
|
|
51
|
+
args = [
|
|
52
|
+
_client_ctor_arg(item, self_obj, result)
|
|
53
|
+
for item in spec.get("args", [])
|
|
54
|
+
]
|
|
55
|
+
return ctor(*args)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _try_get(prop, token):
|
|
59
|
+
return unmap_arg(rpc_client(
|
|
60
|
+
function_name=f"{_PREFIX}:{prop}:GET",
|
|
61
|
+
args={'a': map_arg(token)},
|
|
62
|
+
).results[prop])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _try_set(prop, value, token):
|
|
66
|
+
rpc_client(
|
|
67
|
+
function_name=f"{_PREFIX}:{prop}:SET",
|
|
68
|
+
args={prop: map_arg(value), 'a': map_arg(token)},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _try_call(prop, token, arg=_NO_ARG):
|
|
73
|
+
if arg is _NO_ARG:
|
|
74
|
+
resp = rpc_client(
|
|
75
|
+
function_name=f"{_PREFIX}:{prop}:CALL0",
|
|
76
|
+
args={'a': map_arg(token)},
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
resp = rpc_client(
|
|
80
|
+
function_name=f"{_PREFIX}:{prop}:CALL1",
|
|
81
|
+
args={'a': map_arg(token), 'arg1': map_arg(arg)},
|
|
82
|
+
)
|
|
83
|
+
result = resp.results.get(prop)
|
|
84
|
+
return unmap_arg(result) if result is not None else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _try_calln(prop, token, kwargs):
|
|
88
|
+
payload = {'a': map_arg(token)}
|
|
89
|
+
for key, value in dict(kwargs).items():
|
|
90
|
+
if value is _NO_ARG:
|
|
91
|
+
continue
|
|
92
|
+
payload[str(key)] = map_arg(value)
|
|
93
|
+
resp = rpc_client(
|
|
94
|
+
function_name=f"{_PREFIX}:{prop}:CALLN",
|
|
95
|
+
args=payload,
|
|
96
|
+
)
|
|
97
|
+
result = resp.results.get(prop)
|
|
98
|
+
return unmap_arg(result) if result is not None else None
|
|
99
|
+
|
|
100
|
+
class HackRF:
|
|
101
|
+
|
|
102
|
+
def __init__(self, token: str):
|
|
103
|
+
self.token = token
|
|
104
|
+
from ..dynamic_device import install_driver_if_stale
|
|
105
|
+
install_driver_if_stale(token=token, current_hash=_SCHEMA_HASH)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def amplifier_on(self):
|
|
109
|
+
'Whether the 14 dB RF front-end amplifier is enabled.'
|
|
110
|
+
return _try_get("amplifier_on", self.token)
|
|
111
|
+
|
|
112
|
+
@amplifier_on.setter
|
|
113
|
+
def amplifier_on(self, value):
|
|
114
|
+
_try_set("amplifier_on", value, self.token)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def bias_tee_on(self):
|
|
118
|
+
'Whether 3.3 V antenna bias (50 mA maximum) is enabled.'
|
|
119
|
+
return _try_get("bias_tee_on", self.token)
|
|
120
|
+
|
|
121
|
+
@bias_tee_on.setter
|
|
122
|
+
def bias_tee_on(self, value):
|
|
123
|
+
_try_set("bias_tee_on", value, self.token)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def center_freq(self):
|
|
127
|
+
'Center frequency in Hz.'
|
|
128
|
+
return _try_get("center_freq", self.token)
|
|
129
|
+
|
|
130
|
+
@center_freq.setter
|
|
131
|
+
def center_freq(self, value):
|
|
132
|
+
_try_set("center_freq", value, self.token)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def filter_bandwidth(self):
|
|
136
|
+
'Baseband filter bandwidth in Hz.'
|
|
137
|
+
return _try_get("filter_bandwidth", self.token)
|
|
138
|
+
|
|
139
|
+
@filter_bandwidth.setter
|
|
140
|
+
def filter_bandwidth(self, value):
|
|
141
|
+
_try_set("filter_bandwidth", value, self.token)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def lna_gain(self):
|
|
145
|
+
'Receive IF LNA gain in dB (0 to 40, in 8 dB steps).'
|
|
146
|
+
return _try_get("lna_gain", self.token)
|
|
147
|
+
|
|
148
|
+
@lna_gain.setter
|
|
149
|
+
def lna_gain(self, value):
|
|
150
|
+
_try_set("lna_gain", value, self.token)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def sample_count_limit(self):
|
|
154
|
+
'Asynchronous receive limit in raw IQ bytes; zero means unlimited.'
|
|
155
|
+
return _try_get("sample_count_limit", self.token)
|
|
156
|
+
|
|
157
|
+
@sample_count_limit.setter
|
|
158
|
+
def sample_count_limit(self, value):
|
|
159
|
+
_try_set("sample_count_limit", value, self.token)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def sample_rate(self):
|
|
163
|
+
'Complex sample rate in samples per second.'
|
|
164
|
+
return _try_get("sample_rate", self.token)
|
|
165
|
+
|
|
166
|
+
@sample_rate.setter
|
|
167
|
+
def sample_rate(self, value):
|
|
168
|
+
_try_set("sample_rate", value, self.token)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def txvga_gain(self):
|
|
172
|
+
'Transmit VGA gain in dB (0 to 47).'
|
|
173
|
+
return _try_get("txvga_gain", self.token)
|
|
174
|
+
|
|
175
|
+
@txvga_gain.setter
|
|
176
|
+
def txvga_gain(self, value):
|
|
177
|
+
_try_set("txvga_gain", value, self.token)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def vga_gain(self):
|
|
181
|
+
'Receive baseband VGA gain in dB (0 to 62, in 2 dB steps).'
|
|
182
|
+
return _try_get("vga_gain", self.token)
|
|
183
|
+
|
|
184
|
+
@vga_gain.setter
|
|
185
|
+
def vga_gain(self, value):
|
|
186
|
+
_try_set("vga_gain", value, self.token)
|
|
187
|
+
|
|
188
|
+
def clear_buffer(self):
|
|
189
|
+
'Clear any captured or queued IQ bytes while the radio is idle.'
|
|
190
|
+
return _try_call("clear_buffer", self.token)
|
|
191
|
+
|
|
192
|
+
def enumerate(self):
|
|
193
|
+
'Return attached serials as a JSON array for broad client compatibility.'
|
|
194
|
+
return _try_call("enumerate", self.token)
|
|
195
|
+
|
|
196
|
+
def get_serial_no(self):
|
|
197
|
+
'Read the serial number from the open device.'
|
|
198
|
+
return _try_call("get_serial_no", self.token)
|
|
199
|
+
|
|
200
|
+
def load_tx_iq(self, samples):
|
|
201
|
+
'Convert normalized IQ samples and load the native transmit buffer.'
|
|
202
|
+
return _try_call("load_tx_iq", self.token, samples)
|
|
203
|
+
|
|
204
|
+
def read_samples(self, num_samples=_NO_ARG):
|
|
205
|
+
'Synchronously read a bounded complex64 IQ array.'
|
|
206
|
+
return _try_calln("read_samples", self.token, {
|
|
207
|
+
"num_samples": num_samples,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
def start_tx(self):
|
|
211
|
+
'Start transmitting the IQ bytes previously loaded with load_tx_iq().'
|
|
212
|
+
return _try_call("start_tx", self.token)
|
|
213
|
+
|
|
214
|
+
def stop_tx(self):
|
|
215
|
+
'Stop an active HackRF transmission.'
|
|
216
|
+
return _try_call("stop_tx", self.token)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Auto-generated from IDL schema — do not edit by hand.
|
|
2
|
+
# device_type: rtl_sdr driver_version: 0.1.1 schema_hash: sha256:1c84effc2c21be734398a87d24bb9da52ab5b7ea0def955c503da484b8d818c0
|
|
3
|
+
|
|
4
|
+
_PREFIX = "Rtl_sdr"
|
|
5
|
+
_SCHEMA_HASH = "sha256:1c84effc2c21be734398a87d24bb9da52ab5b7ea0def955c503da484b8d818c0"
|
|
6
|
+
_CLIENT_MODULES = {}
|
|
7
|
+
_CLIENT_OBJECTS = {}
|
|
8
|
+
|
|
9
|
+
from importlib import import_module as _import_module
|
|
10
|
+
|
|
11
|
+
from ...core.grpc_client import rpc_client
|
|
12
|
+
from ...common.utils import map_arg, unmap_arg
|
|
13
|
+
|
|
14
|
+
_NO_ARG = object()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
for _alias, _module_path in _CLIENT_MODULES.items():
|
|
18
|
+
globals()[_alias] = _import_module(_module_path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_client_target(target):
|
|
22
|
+
parts = str(target or "").split(".")
|
|
23
|
+
if not parts or not parts[0]:
|
|
24
|
+
raise ValueError(f"Invalid client constructor target: {target!r}")
|
|
25
|
+
if parts[0] not in globals():
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Client helper module {parts[0]!r} is not declared in _CLIENT_MODULES"
|
|
28
|
+
)
|
|
29
|
+
obj = globals()[parts[0]]
|
|
30
|
+
for part in parts[1:]:
|
|
31
|
+
obj = getattr(obj, part)
|
|
32
|
+
return obj
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _client_ctor_arg(spec, self_obj, result):
|
|
36
|
+
if spec == "$self":
|
|
37
|
+
return self_obj
|
|
38
|
+
if spec == "$result":
|
|
39
|
+
return result
|
|
40
|
+
if isinstance(spec, dict) and "const" in spec:
|
|
41
|
+
return spec["const"]
|
|
42
|
+
return spec
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _wrap_client_return(spec, self_obj, result):
|
|
46
|
+
if not spec:
|
|
47
|
+
return result
|
|
48
|
+
if spec.get("kind") != "constructor":
|
|
49
|
+
raise ValueError(f"Unsupported client_return metadata: {spec!r}")
|
|
50
|
+
ctor = _resolve_client_target(spec.get("target"))
|
|
51
|
+
args = [
|
|
52
|
+
_client_ctor_arg(item, self_obj, result)
|
|
53
|
+
for item in spec.get("args", [])
|
|
54
|
+
]
|
|
55
|
+
return ctor(*args)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _try_get(prop, token):
|
|
59
|
+
return unmap_arg(rpc_client(
|
|
60
|
+
function_name=f"{_PREFIX}:{prop}:GET",
|
|
61
|
+
args={'a': map_arg(token)},
|
|
62
|
+
).results[prop])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _try_set(prop, value, token):
|
|
66
|
+
rpc_client(
|
|
67
|
+
function_name=f"{_PREFIX}:{prop}:SET",
|
|
68
|
+
args={prop: map_arg(value), 'a': map_arg(token)},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _try_call(prop, token, arg=_NO_ARG):
|
|
73
|
+
if arg is _NO_ARG:
|
|
74
|
+
resp = rpc_client(
|
|
75
|
+
function_name=f"{_PREFIX}:{prop}:CALL0",
|
|
76
|
+
args={'a': map_arg(token)},
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
resp = rpc_client(
|
|
80
|
+
function_name=f"{_PREFIX}:{prop}:CALL1",
|
|
81
|
+
args={'a': map_arg(token), 'arg1': map_arg(arg)},
|
|
82
|
+
)
|
|
83
|
+
result = resp.results.get(prop)
|
|
84
|
+
return unmap_arg(result) if result is not None else None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _try_calln(prop, token, kwargs):
|
|
88
|
+
payload = {'a': map_arg(token)}
|
|
89
|
+
for key, value in dict(kwargs).items():
|
|
90
|
+
if value is _NO_ARG:
|
|
91
|
+
continue
|
|
92
|
+
payload[str(key)] = map_arg(value)
|
|
93
|
+
resp = rpc_client(
|
|
94
|
+
function_name=f"{_PREFIX}:{prop}:CALLN",
|
|
95
|
+
args=payload,
|
|
96
|
+
)
|
|
97
|
+
result = resp.results.get(prop)
|
|
98
|
+
return unmap_arg(result) if result is not None else None
|
|
99
|
+
|
|
100
|
+
class RtlSdr:
|
|
101
|
+
|
|
102
|
+
def __init__(self, token: str):
|
|
103
|
+
self.token = token
|
|
104
|
+
from ..dynamic_device import install_driver_if_stale
|
|
105
|
+
install_driver_if_stale(token=token, current_hash=_SCHEMA_HASH)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def agc_mode(self):
|
|
109
|
+
return _try_get("agc_mode", self.token)
|
|
110
|
+
|
|
111
|
+
@agc_mode.setter
|
|
112
|
+
def agc_mode(self, value):
|
|
113
|
+
_try_set("agc_mode", value, self.token)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def bandwidth(self):
|
|
117
|
+
'Tuner bandwidth in Hz; zero requests automatic selection.'
|
|
118
|
+
return _try_get("bandwidth", self.token)
|
|
119
|
+
|
|
120
|
+
@bandwidth.setter
|
|
121
|
+
def bandwidth(self, value):
|
|
122
|
+
_try_set("bandwidth", value, self.token)
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def bias_tee(self):
|
|
126
|
+
return _try_get("bias_tee", self.token)
|
|
127
|
+
|
|
128
|
+
@bias_tee.setter
|
|
129
|
+
def bias_tee(self, value):
|
|
130
|
+
_try_set("bias_tee", value, self.token)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def center_freq(self):
|
|
134
|
+
'Tuner center frequency in Hz.'
|
|
135
|
+
return _try_get("center_freq", self.token)
|
|
136
|
+
|
|
137
|
+
@center_freq.setter
|
|
138
|
+
def center_freq(self, value):
|
|
139
|
+
_try_set("center_freq", value, self.token)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def device_index(self):
|
|
143
|
+
return _try_get("device_index", self.token)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def direct_sampling(self):
|
|
147
|
+
return _try_get("direct_sampling", self.token)
|
|
148
|
+
|
|
149
|
+
@direct_sampling.setter
|
|
150
|
+
def direct_sampling(self, value):
|
|
151
|
+
_try_set("direct_sampling", value, self.token)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def dithering(self):
|
|
155
|
+
return _try_get("dithering", self.token)
|
|
156
|
+
|
|
157
|
+
@dithering.setter
|
|
158
|
+
def dithering(self, value):
|
|
159
|
+
_try_set("dithering", value, self.token)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def fc(self):
|
|
163
|
+
'Alias for center_freq, matching PyRtlSdr.'
|
|
164
|
+
return _try_get("fc", self.token)
|
|
165
|
+
|
|
166
|
+
@fc.setter
|
|
167
|
+
def fc(self, value):
|
|
168
|
+
_try_set("fc", value, self.token)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def freq_correction(self):
|
|
172
|
+
'Frequency correction in parts per million.'
|
|
173
|
+
return _try_get("freq_correction", self.token)
|
|
174
|
+
|
|
175
|
+
@freq_correction.setter
|
|
176
|
+
def freq_correction(self, value):
|
|
177
|
+
_try_set("freq_correction", value, self.token)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def gain(self):
|
|
181
|
+
'Tuner gain in dB, or auto when automatic gain is enabled.'
|
|
182
|
+
return _try_get("gain", self.token)
|
|
183
|
+
|
|
184
|
+
@gain.setter
|
|
185
|
+
def gain(self, value):
|
|
186
|
+
_try_set("gain", value, self.token)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def offset_tuning(self):
|
|
190
|
+
return _try_get("offset_tuning", self.token)
|
|
191
|
+
|
|
192
|
+
@offset_tuning.setter
|
|
193
|
+
def offset_tuning(self, value):
|
|
194
|
+
_try_set("offset_tuning", value, self.token)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def rs(self):
|
|
198
|
+
'Alias for sample_rate, matching PyRtlSdr.'
|
|
199
|
+
return _try_get("rs", self.token)
|
|
200
|
+
|
|
201
|
+
@rs.setter
|
|
202
|
+
def rs(self, value):
|
|
203
|
+
_try_set("rs", value, self.token)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def sample_rate(self):
|
|
207
|
+
'Complex sample rate in samples per second.'
|
|
208
|
+
return _try_get("sample_rate", self.token)
|
|
209
|
+
|
|
210
|
+
@sample_rate.setter
|
|
211
|
+
def sample_rate(self, value):
|
|
212
|
+
_try_set("sample_rate", value, self.token)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def serial_number(self):
|
|
216
|
+
return _try_get("serial_number", self.token)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def tuner_type(self):
|
|
220
|
+
return _try_get("tuner_type", self.token)
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def usb_strings(self):
|
|
224
|
+
return _try_get("usb_strings", self.token)
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def valid_gains_db(self):
|
|
228
|
+
return _try_get("valid_gains_db", self.token)
|
|
229
|
+
|
|
230
|
+
def read_bytes(self, num_bytes=_NO_ARG):
|
|
231
|
+
'Read packed unsigned 8-bit interleaved IQ bytes.'
|
|
232
|
+
return _try_calln("read_bytes", self.token, {
|
|
233
|
+
"num_bytes": num_bytes,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
def read_samples(self, num_samples=_NO_ARG):
|
|
237
|
+
'Read normalized complex64 IQ samples.'
|
|
238
|
+
return _try_calln("read_samples", self.token, {
|
|
239
|
+
"num_samples": num_samples,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
def reset_buffer(self):
|
|
243
|
+
'Discard pending USB samples before a new capture.'
|
|
244
|
+
return _try_call("reset_buffer", self.token)
|