RemoteRF 2.0.15__tar.gz → 2.1.0__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.15 → remoterf-2.1.0}/PKG-INFO +70 -1
- remoterf-2.0.15/src/remoterf.egg-info/PKG-INFO → remoterf-2.1.0/README.md +62 -18
- {remoterf-2.0.15 → remoterf-2.1.0}/pyproject.toml +10 -1
- {remoterf-2.0.15/src/remoteRF/drivers/adalm_pluto → remoterf-2.1.0/src/remoteRF/common/grpc}/__init__.py +1 -1
- remoterf-2.1.0/src/remoteRF/common/grpc/deployment_capabilities.proto +15 -0
- remoterf-2.1.0/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +41 -0
- remoterf-2.1.0/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +100 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/banner.py +13 -10
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/process_arg.py +31 -2
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/config/cert_fetcher.py +30 -5
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/config/config.py +84 -7
- remoterf-2.0.15/src/remoteRF/common/grpc/__init__.py → remoterf-2.1.0/src/remoteRF/core/acc_login.py +11 -1
- remoterf-2.1.0/src/remoteRF/core/app.py +1685 -0
- remoterf-2.1.0/src/remoteRF/core/direct_path.py +707 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/core/dynamic_v2_transport.py +91 -4
- remoterf-2.1.0/src/remoteRF/core/grpc_acc.py +159 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/core/grpc_client.py +200 -50
- remoterf-2.1.0/src/remoteRF/core/secure_channel.py +72 -0
- remoterf-2.1.0/src/remoteRF/deployment/__init__.py +1 -0
- remoterf-2.1.0/src/remoteRF/deployment/backend.py +416 -0
- remoterf-2.1.0/src/remoteRF/deployment/direct.py +38 -0
- remoterf-2.1.0/src/remoteRF/deployment/homes.py +383 -0
- remoterf-2.1.0/src/remoteRF/deployment/http.py +121 -0
- remoterf-2.1.0/src/remoteRF/deployment/state.py +187 -0
- remoterf-2.1.0/src/remoteRF/drivers/adalm_pluto/__init__.py +21 -0
- remoterf-2.1.0/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +400 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/dynamic_device.py +49 -9
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/dynamic_v2.py +2 -0
- remoterf-2.0.15/src/remoteRF/core/acc_login.py → remoterf-2.1.0/src/remoteRF/drivers/sim_sdr/__init__.py +5 -3
- remoterf-2.1.0/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +183 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/support/uhd_v2.py +9 -0
- remoterf-2.1.0/src/remoteRF/remoterf_cli.py +587 -0
- remoterf-2.1.0/src/remoteRF.py +39 -0
- remoterf-2.0.15/README.md → remoterf-2.1.0/src/remoterf.egg-info/PKG-INFO +87 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoterf.egg-info/SOURCES.txt +30 -1
- remoterf-2.1.0/src/remoterf.egg-info/requires.txt +12 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_banner.py +4 -4
- remoterf-2.1.0/tests/test_config_address.py +128 -0
- remoterf-2.1.0/tests/test_deployment_accounts.py +582 -0
- remoterf-2.1.0/tests/test_direct_path_fixed.py +95 -0
- remoterf-2.1.0/tests/test_direct_path_partner.py +146 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_dynamic_device_codegen.py +41 -5
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_dynamic_v2.py +29 -0
- remoterf-2.1.0/tests/test_federated_devices.py +115 -0
- remoterf-2.1.0/tests/test_fetch_idl_fallback.py +45 -0
- remoterf-2.1.0/tests/test_finish_prompt.py +15 -0
- remoterf-2.1.0/tests/test_gate_i_direct_path.py +464 -0
- remoterf-2.1.0/tests/test_grpc_client_target.py +80 -0
- remoterf-2.1.0/tests/test_homes.py +609 -0
- remoterf-2.1.0/tests/test_native_account_credentials.py +12 -0
- remoterf-2.1.0/tests/test_native_devices.py +57 -0
- remoterf-2.1.0/tests/test_partner_dynamic_v2.py +95 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_pluto_virtual.py +6 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_process_arg.py +21 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_remoterf_cli.py +9 -3
- remoterf-2.1.0/tests/test_upgrade_rescue.py +45 -0
- remoterf-2.0.15/src/remoteRF/core/app.py +0 -983
- remoterf-2.0.15/src/remoteRF/core/grpc_acc.py +0 -79
- remoterf-2.0.15/src/remoteRF/remoterf_cli.py +0 -271
- remoterf-2.0.15/src/remoterf.egg-info/requires.txt +0 -5
- {remoterf-2.0.15 → remoterf-2.1.0}/LICENSE +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/setup.cfg +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/grpc/grpc.proto +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/grpc/v2_codec.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoteRF/version.py +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.0.15 → remoterf-2.1.0}/tests/test_ti_mmwave.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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
|
|
@@ -9,11 +9,18 @@ Classifier: Operating System :: OS Independent
|
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
|
+
Requires-Dist: remoterf-federation-core==0.16.0
|
|
12
13
|
Requires-Dist: grpcio<2.0.0,>=1.78.1
|
|
13
14
|
Requires-Dist: protobuf<7.0.0,>=6.31.1
|
|
14
15
|
Requires-Dist: numpy
|
|
15
16
|
Requires-Dist: prompt_toolkit
|
|
16
17
|
Requires-Dist: python-dotenv
|
|
18
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
19
|
+
Requires-Dist: certifi
|
|
20
|
+
Requires-Dist: pydantic<3.0,>=2.6
|
|
21
|
+
Requires-Dist: keyring<26,>=24
|
|
22
|
+
Requires-Dist: aioice<0.11,>=0.10
|
|
23
|
+
Requires-Dist: aioquic<2.0,>=1.2
|
|
17
24
|
Dynamic: license-file
|
|
18
25
|
|
|
19
26
|
# Remote RF
|
|
@@ -181,6 +188,68 @@ health. Remote firmware flashing is deliberately unsupported.
|
|
|
181
188
|
|
|
182
189
|
If `pip install` doesn't work, you can clone the [source](https://github.com/WirelessLabAtUCLA/RemoteRF-Client) directly from github.
|
|
183
190
|
|
|
191
|
+
## RemoteRF Global v0 / Remote Internet Access
|
|
192
|
+
|
|
193
|
+
By default, RemoteRF assumes you're on the same network as the RemoteRF
|
|
194
|
+
Server, and you configure the client with that server's address:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
remoterf --config --addr 192.168.1.20:12321
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
For deployments exposed through the initial RemoteRF Global gateway, the
|
|
201
|
+
same workflow works with a DNS hostname instead of a LAN IP:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
remoterf --config --addr ucla.global.remoterf.net:12321
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The client does not know or care whether `ucla.global.remoterf.net` is a
|
|
208
|
+
directly reachable RemoteRF Server, a LAN server, or a RemoteRF Global v0
|
|
209
|
+
transparent TCP gateway forwarding to a RemoteRF Server elsewhere. From the
|
|
210
|
+
client's perspective, it's simply a RemoteRF Server endpoint identified by
|
|
211
|
+
`host:port` — an IPv4 address, an IPv6 address (bracketed, e.g.
|
|
212
|
+
`[2001:db8::1]:12321`), or a normal DNS hostname all work the same way.
|
|
213
|
+
Certificate bootstrap (`host:cert_port`) and the gRPC connection
|
|
214
|
+
(`host:port`) are both made against whatever hostname you configure — the
|
|
215
|
+
client stores and uses that hostname as-is and never resolves it to an IP
|
|
216
|
+
before saving your configuration, so DNS/IP changes on the server side don't
|
|
217
|
+
require reconfiguration.
|
|
218
|
+
|
|
219
|
+
**TLS note:** RemoteRF Global v0 does not weaken certificate verification.
|
|
220
|
+
The RemoteRF Server's certificate must include the hostname you connect
|
|
221
|
+
with (e.g. `ucla.global.remoterf.net`) in its Subject Alternative Name
|
|
222
|
+
(SAN) for standard TLS hostname validation to succeed — this is a
|
|
223
|
+
server-side certificate requirement, not a client setting. If you're
|
|
224
|
+
instead connecting through something like Tailscale, where the address you
|
|
225
|
+
dial differs from the identity in the certificate, use the existing
|
|
226
|
+
`REMOTERF_TLS_SERVER_NAME` override described above; it still requires that
|
|
227
|
+
name to be present in the certificate.
|
|
228
|
+
|
|
229
|
+
v0 is intentionally minimal — it does **not** include RemoteRF Global
|
|
230
|
+
accounts, deployment discovery/selection, federation, NAT traversal, direct
|
|
231
|
+
P2P, automatic routing, or global resource IDs. Those are future versions.
|
|
232
|
+
The client-side software behaves identically whether you point it at a LAN
|
|
233
|
+
server or at a RemoteRF Global v0 endpoint.
|
|
234
|
+
|
|
235
|
+
### RemoteRF Global v2 Phase-1 development boundary
|
|
236
|
+
|
|
237
|
+
The disposable Global v1 account-selection and deployment-session client was
|
|
238
|
+
removed at Gate C. The normal CLI supports direct/native gRPC accounts and
|
|
239
|
+
clean HTTPS deployment-home accounts; neither path treats a broker identity,
|
|
240
|
+
username, email, or home token as destination credentials.
|
|
241
|
+
|
|
242
|
+
Gate G adds permission visibility to the ordinary `perms` command. It renders
|
|
243
|
+
HOME-local groups plus current destination-signed federated groups with their
|
|
244
|
+
contract/version/state and retrieval time. An unreachable destination is
|
|
245
|
+
explicitly labeled as unsigned transport status, never as an authoritative
|
|
246
|
+
empty group result. This works when the HOME has zero devices.
|
|
247
|
+
|
|
248
|
+
Phase 1 still grants no federated device discovery, reservation, lease,
|
|
249
|
+
session, streaming, RX, TX, or radio access. Production remains unchanged.
|
|
250
|
+
See [deployment accounts](docs/deployment-accounts-v2.md) and
|
|
251
|
+
[identity boundary](docs/gate-c-identity-boundary.md).
|
|
252
|
+
|
|
184
253
|
<!-- 1. **Clone the repository:**
|
|
185
254
|
```bash
|
|
186
255
|
git clone https://github.com/WirelessLabAtUCLA/RemoteRF-Client
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: remoterf
|
|
3
|
-
Version: 2.0.15
|
|
4
|
-
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
|
-
Author-email: Ethan Ge <ethoGalaxy@gmail.com>
|
|
6
|
-
License-Expression: GPL-3.0-or-later
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Operating System :: OS Independent
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Dist: grpcio<2.0.0,>=1.78.1
|
|
13
|
-
Requires-Dist: protobuf<7.0.0,>=6.31.1
|
|
14
|
-
Requires-Dist: numpy
|
|
15
|
-
Requires-Dist: prompt_toolkit
|
|
16
|
-
Requires-Dist: python-dotenv
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
# Remote RF
|
|
20
2
|
|
|
21
3
|
A python API to remotely access signal centric hardware.
|
|
@@ -181,6 +163,68 @@ health. Remote firmware flashing is deliberately unsupported.
|
|
|
181
163
|
|
|
182
164
|
If `pip install` doesn't work, you can clone the [source](https://github.com/WirelessLabAtUCLA/RemoteRF-Client) directly from github.
|
|
183
165
|
|
|
166
|
+
## RemoteRF Global v0 / Remote Internet Access
|
|
167
|
+
|
|
168
|
+
By default, RemoteRF assumes you're on the same network as the RemoteRF
|
|
169
|
+
Server, and you configure the client with that server's address:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
remoterf --config --addr 192.168.1.20:12321
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For deployments exposed through the initial RemoteRF Global gateway, the
|
|
176
|
+
same workflow works with a DNS hostname instead of a LAN IP:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
remoterf --config --addr ucla.global.remoterf.net:12321
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The client does not know or care whether `ucla.global.remoterf.net` is a
|
|
183
|
+
directly reachable RemoteRF Server, a LAN server, or a RemoteRF Global v0
|
|
184
|
+
transparent TCP gateway forwarding to a RemoteRF Server elsewhere. From the
|
|
185
|
+
client's perspective, it's simply a RemoteRF Server endpoint identified by
|
|
186
|
+
`host:port` — an IPv4 address, an IPv6 address (bracketed, e.g.
|
|
187
|
+
`[2001:db8::1]:12321`), or a normal DNS hostname all work the same way.
|
|
188
|
+
Certificate bootstrap (`host:cert_port`) and the gRPC connection
|
|
189
|
+
(`host:port`) are both made against whatever hostname you configure — the
|
|
190
|
+
client stores and uses that hostname as-is and never resolves it to an IP
|
|
191
|
+
before saving your configuration, so DNS/IP changes on the server side don't
|
|
192
|
+
require reconfiguration.
|
|
193
|
+
|
|
194
|
+
**TLS note:** RemoteRF Global v0 does not weaken certificate verification.
|
|
195
|
+
The RemoteRF Server's certificate must include the hostname you connect
|
|
196
|
+
with (e.g. `ucla.global.remoterf.net`) in its Subject Alternative Name
|
|
197
|
+
(SAN) for standard TLS hostname validation to succeed — this is a
|
|
198
|
+
server-side certificate requirement, not a client setting. If you're
|
|
199
|
+
instead connecting through something like Tailscale, where the address you
|
|
200
|
+
dial differs from the identity in the certificate, use the existing
|
|
201
|
+
`REMOTERF_TLS_SERVER_NAME` override described above; it still requires that
|
|
202
|
+
name to be present in the certificate.
|
|
203
|
+
|
|
204
|
+
v0 is intentionally minimal — it does **not** include RemoteRF Global
|
|
205
|
+
accounts, deployment discovery/selection, federation, NAT traversal, direct
|
|
206
|
+
P2P, automatic routing, or global resource IDs. Those are future versions.
|
|
207
|
+
The client-side software behaves identically whether you point it at a LAN
|
|
208
|
+
server or at a RemoteRF Global v0 endpoint.
|
|
209
|
+
|
|
210
|
+
### RemoteRF Global v2 Phase-1 development boundary
|
|
211
|
+
|
|
212
|
+
The disposable Global v1 account-selection and deployment-session client was
|
|
213
|
+
removed at Gate C. The normal CLI supports direct/native gRPC accounts and
|
|
214
|
+
clean HTTPS deployment-home accounts; neither path treats a broker identity,
|
|
215
|
+
username, email, or home token as destination credentials.
|
|
216
|
+
|
|
217
|
+
Gate G adds permission visibility to the ordinary `perms` command. It renders
|
|
218
|
+
HOME-local groups plus current destination-signed federated groups with their
|
|
219
|
+
contract/version/state and retrieval time. An unreachable destination is
|
|
220
|
+
explicitly labeled as unsigned transport status, never as an authoritative
|
|
221
|
+
empty group result. This works when the HOME has zero devices.
|
|
222
|
+
|
|
223
|
+
Phase 1 still grants no federated device discovery, reservation, lease,
|
|
224
|
+
session, streaming, RX, TX, or radio access. Production remains unchanged.
|
|
225
|
+
See [deployment accounts](docs/deployment-accounts-v2.md) and
|
|
226
|
+
[identity boundary](docs/gate-c-identity-boundary.md).
|
|
227
|
+
|
|
184
228
|
<!-- 1. **Clone the repository:**
|
|
185
229
|
```bash
|
|
186
230
|
git clone https://github.com/WirelessLabAtUCLA/RemoteRF-Client
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoterf"
|
|
7
|
-
version = "2.0
|
|
7
|
+
version = "2.1.0"
|
|
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"
|
|
@@ -12,11 +12,18 @@ license-files = ["LICENSE"]
|
|
|
12
12
|
authors = [{ name = "Ethan Ge", email = "ethoGalaxy@gmail.com" }]
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
dependencies = [
|
|
15
|
+
"remoterf-federation-core==0.16.0",
|
|
15
16
|
"grpcio>=1.78.1,<2.0.0",
|
|
16
17
|
"protobuf>=6.31.1,<7.0.0",
|
|
17
18
|
"numpy",
|
|
18
19
|
"prompt_toolkit",
|
|
19
20
|
"python-dotenv",
|
|
21
|
+
"httpx>=0.27,<1.0",
|
|
22
|
+
"certifi",
|
|
23
|
+
"pydantic>=2.6,<3.0",
|
|
24
|
+
"keyring>=24,<26",
|
|
25
|
+
"aioice>=0.10,<0.11",
|
|
26
|
+
"aioquic>=1.2,<2.0",
|
|
20
27
|
]
|
|
21
28
|
classifiers = [
|
|
22
29
|
"Programming Language :: Python :: 3",
|
|
@@ -28,6 +35,8 @@ remoterf = "remoteRF.remoterf_cli:main"
|
|
|
28
35
|
|
|
29
36
|
[tool.setuptools]
|
|
30
37
|
package-dir = { "" = "src" }
|
|
38
|
+
# src/remoteRF.py: see its docstring (upgrades from 2.0.7-2.0.13).
|
|
39
|
+
py-modules = ["remoteRF"]
|
|
31
40
|
|
|
32
41
|
[tool.setuptools.packages.find]
|
|
33
42
|
where = ["src"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
package remote_rf;
|
|
3
|
+
import "grpc.proto";
|
|
4
|
+
|
|
5
|
+
// Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
6
|
+
service DeploymentCapabilitiesV1 {
|
|
7
|
+
rpc GetCapabilities (DeploymentCapabilitiesRequest) returns (DeploymentCapabilitiesResponse);
|
|
8
|
+
}
|
|
9
|
+
message DeploymentCapabilitiesRequest {
|
|
10
|
+
string protocol_version = 1;
|
|
11
|
+
}
|
|
12
|
+
message DeploymentCapabilitiesResponse {
|
|
13
|
+
string capabilities_json = 1;
|
|
14
|
+
ErrorEnvelope error = 2;
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: deployment_capabilities.proto
|
|
5
|
+
# Protobuf Python Version: 6.31.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
6,
|
|
15
|
+
31,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'deployment_capabilities.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from . import grpc_pb2 as grpc__pb2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x64\x65ployment_capabilities.proto\x12\tremote_rf\x1a\ngrpc.proto\"9\n\x1d\x44\x65ploymentCapabilitiesRequest\x12\x18\n\x10protocol_version\x18\x01 \x01(\t\"d\n\x1e\x44\x65ploymentCapabilitiesResponse\x12\x19\n\x11\x63\x61pabilities_json\x18\x01 \x01(\t\x12\'\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x18.remote_rf.ErrorEnvelope2\x82\x01\n\x18\x44\x65ploymentCapabilitiesV1\x12\x66\n\x0fGetCapabilities\x12(.remote_rf.DeploymentCapabilitiesRequest\x1a).remote_rf.DeploymentCapabilitiesResponseb\x06proto3')
|
|
29
|
+
|
|
30
|
+
_globals = globals()
|
|
31
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
32
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'deployment_capabilities_pb2', _globals)
|
|
33
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
34
|
+
DESCRIPTOR._loaded_options = None
|
|
35
|
+
_globals['_DEPLOYMENTCAPABILITIESREQUEST']._serialized_start=56
|
|
36
|
+
_globals['_DEPLOYMENTCAPABILITIESREQUEST']._serialized_end=113
|
|
37
|
+
_globals['_DEPLOYMENTCAPABILITIESRESPONSE']._serialized_start=115
|
|
38
|
+
_globals['_DEPLOYMENTCAPABILITIESRESPONSE']._serialized_end=215
|
|
39
|
+
_globals['_DEPLOYMENTCAPABILITIESV1']._serialized_start=218
|
|
40
|
+
_globals['_DEPLOYMENTCAPABILITIESV1']._serialized_end=348
|
|
41
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from . import deployment_capabilities_pb2 as deployment__capabilities__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.74.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ f' but the generated code in deployment_capabilities_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeploymentCapabilitiesV1Stub(object):
|
|
29
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, channel):
|
|
33
|
+
"""Constructor.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
channel: A grpc.Channel.
|
|
37
|
+
"""
|
|
38
|
+
self.GetCapabilities = channel.unary_unary(
|
|
39
|
+
'/remote_rf.DeploymentCapabilitiesV1/GetCapabilities',
|
|
40
|
+
request_serializer=deployment__capabilities__pb2.DeploymentCapabilitiesRequest.SerializeToString,
|
|
41
|
+
response_deserializer=deployment__capabilities__pb2.DeploymentCapabilitiesResponse.FromString,
|
|
42
|
+
_registered_method=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DeploymentCapabilitiesV1Servicer(object):
|
|
46
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def GetCapabilities(self, request, context):
|
|
50
|
+
"""Missing associated documentation comment in .proto file."""
|
|
51
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
52
|
+
context.set_details('Method not implemented!')
|
|
53
|
+
raise NotImplementedError('Method not implemented!')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_DeploymentCapabilitiesV1Servicer_to_server(servicer, server):
|
|
57
|
+
rpc_method_handlers = {
|
|
58
|
+
'GetCapabilities': grpc.unary_unary_rpc_method_handler(
|
|
59
|
+
servicer.GetCapabilities,
|
|
60
|
+
request_deserializer=deployment__capabilities__pb2.DeploymentCapabilitiesRequest.FromString,
|
|
61
|
+
response_serializer=deployment__capabilities__pb2.DeploymentCapabilitiesResponse.SerializeToString,
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
65
|
+
'remote_rf.DeploymentCapabilitiesV1', rpc_method_handlers)
|
|
66
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
67
|
+
server.add_registered_method_handlers('remote_rf.DeploymentCapabilitiesV1', rpc_method_handlers)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# This class is part of an EXPERIMENTAL API.
|
|
71
|
+
class DeploymentCapabilitiesV1(object):
|
|
72
|
+
"""Optional discovery. Existing GenericRPC/IDL/device protocols are unchanged.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def GetCapabilities(request,
|
|
77
|
+
target,
|
|
78
|
+
options=(),
|
|
79
|
+
channel_credentials=None,
|
|
80
|
+
call_credentials=None,
|
|
81
|
+
insecure=False,
|
|
82
|
+
compression=None,
|
|
83
|
+
wait_for_ready=None,
|
|
84
|
+
timeout=None,
|
|
85
|
+
metadata=None):
|
|
86
|
+
return grpc.experimental.unary_unary(
|
|
87
|
+
request,
|
|
88
|
+
target,
|
|
89
|
+
'/remote_rf.DeploymentCapabilitiesV1/GetCapabilities',
|
|
90
|
+
deployment__capabilities__pb2.DeploymentCapabilitiesRequest.SerializeToString,
|
|
91
|
+
deployment__capabilities__pb2.DeploymentCapabilitiesResponse.FromString,
|
|
92
|
+
options,
|
|
93
|
+
channel_credentials,
|
|
94
|
+
insecure,
|
|
95
|
+
call_credentials,
|
|
96
|
+
compression,
|
|
97
|
+
wait_for_ready,
|
|
98
|
+
timeout,
|
|
99
|
+
metadata,
|
|
100
|
+
_registered_method=True)
|
|
@@ -47,7 +47,7 @@ COMPACT_BANNER = "RemoteRF"
|
|
|
47
47
|
ASCII_WORDMARK_WIDTH = max(len(line) for line in ASCII_BANNER.splitlines())
|
|
48
48
|
ASCII_BANNER_WIDTH = max(len(line) for line in BRANDED_ASCII_BANNER.splitlines())
|
|
49
49
|
ASCII_BANNER_MIN_COLUMNS = ASCII_BANNER_WIDTH + 1
|
|
50
|
-
PANEL_CONTENT_WIDTH =
|
|
50
|
+
PANEL_CONTENT_WIDTH = 39
|
|
51
51
|
PANEL_INNER_WIDTH = PANEL_CONTENT_WIDTH + 2
|
|
52
52
|
PANEL_WIDTH = PANEL_INNER_WIDTH + 2
|
|
53
53
|
PANEL_GAP = 1
|
|
@@ -111,12 +111,15 @@ def _panel_lines(
|
|
|
111
111
|
marker = "◆" if unicode_supported else "*"
|
|
112
112
|
label = f"{horizontal} REMOTERF CLIENT "
|
|
113
113
|
top = f"{top_left}{label}{horizontal * (PANEL_INNER_WIDTH - len(label))}{top_right}"
|
|
114
|
-
content =
|
|
114
|
+
content = [
|
|
115
115
|
f"{marker} www.remoterf.net",
|
|
116
|
-
"Created by
|
|
116
|
+
"Created by https://wireless.ee.ucla.edu",
|
|
117
117
|
f"VERSION {version}",
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
]
|
|
119
|
+
# Before a target is chosen there is no server to name; an "NA" row would
|
|
120
|
+
# only say so twice.
|
|
121
|
+
if server:
|
|
122
|
+
content.append(f"SERVER {server}")
|
|
120
123
|
body = [
|
|
121
124
|
f"{vertical} {_fit_panel_text(line):<{PANEL_CONTENT_WIDTH}} {vertical}"
|
|
122
125
|
for line in content
|
|
@@ -169,12 +172,12 @@ def _compact_lines(
|
|
|
169
172
|
heading = COMPACT_BANNER[:width]
|
|
170
173
|
|
|
171
174
|
short_version = str(version).split(maxsplit=1)[0]
|
|
172
|
-
server = str(server or "
|
|
175
|
+
server = str(server or "")
|
|
173
176
|
detail_options = (
|
|
174
|
-
f"v{version} {separator} {server}",
|
|
175
|
-
|
|
176
|
-
f"v{
|
|
177
|
-
)
|
|
177
|
+
(f"v{version} {separator} {server}", f"v{short_version} {separator} {server}")
|
|
178
|
+
if server
|
|
179
|
+
else (f"v{version}",)
|
|
180
|
+
) + (f"v{short_version}",)
|
|
178
181
|
details = next((line for line in detail_options if len(line) <= width), "")
|
|
179
182
|
if not details:
|
|
180
183
|
details = detail_options[-1][:width]
|
|
@@ -30,6 +30,31 @@ _MAX_NDARRAY_DIMENSIONS = 8
|
|
|
30
30
|
_SUPPORTED_NDARRAY_KINDS = {"b", "i", "u", "f", "c"}
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
# Gate I slice 3: complex samples as interleaved int16 IQ, a quarter of the
|
|
34
|
+
# complex128 bytes, decoded to complex64. Only exact for integer-valued data
|
|
35
|
+
# within int16 range (a 12-bit ADC's, say); map_iq16 refuses anything else.
|
|
36
|
+
IQ16 = "ci16"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def fits_iq16(array: np.ndarray) -> bool:
|
|
40
|
+
if not (isinstance(array, np.ndarray) and np.iscomplexobj(array)):
|
|
41
|
+
return False
|
|
42
|
+
parts = np.stack((array.real, array.imag))
|
|
43
|
+
return bool(np.all(np.abs(parts) <= 32767) and np.array_equal(parts, np.round(parts)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def map_iq16(array: np.ndarray) -> grpc_pb2.Argument:
|
|
47
|
+
if not fits_iq16(array):
|
|
48
|
+
raise ValueError("samples are not integer-valued int16 IQ")
|
|
49
|
+
iq = np.empty(array.shape + (2,), dtype=np.int16)
|
|
50
|
+
iq[..., 0], iq[..., 1] = array.real, array.imag
|
|
51
|
+
arg = grpc_pb2.Argument()
|
|
52
|
+
arg.ndarray_value.dtype = IQ16
|
|
53
|
+
arg.ndarray_value.shape.extend(array.shape)
|
|
54
|
+
arg.ndarray_value.data = np.ascontiguousarray(iq).tobytes(order="C")
|
|
55
|
+
return arg
|
|
56
|
+
|
|
57
|
+
|
|
33
58
|
def _validate_ndarray(dtype: np.dtype, shape) -> int:
|
|
34
59
|
"""Return the expected payload length for a safe numeric ndarray."""
|
|
35
60
|
if dtype.hasobject or dtype.kind not in _SUPPORTED_NDARRAY_KINDS:
|
|
@@ -49,18 +74,22 @@ def _validate_ndarray(dtype: np.dtype, shape) -> int:
|
|
|
49
74
|
|
|
50
75
|
|
|
51
76
|
def _decode_ndarray(descriptor: grpc_pb2.NDArrayValue) -> np.ndarray:
|
|
77
|
+
iq16 = descriptor.dtype == IQ16
|
|
52
78
|
try:
|
|
53
|
-
dtype = np.dtype(descriptor.dtype)
|
|
79
|
+
dtype = np.dtype(np.int16) if iq16 else np.dtype(descriptor.dtype)
|
|
54
80
|
except (TypeError, ValueError) as exc:
|
|
55
81
|
raise ValueError(f"invalid ndarray dtype: {descriptor.dtype!r}") from exc
|
|
56
82
|
|
|
57
83
|
shape = tuple(int(item) for item in descriptor.shape)
|
|
58
|
-
expected = _validate_ndarray(dtype, shape)
|
|
84
|
+
expected = _validate_ndarray(dtype, shape + (2,) if iq16 else shape)
|
|
59
85
|
actual = len(descriptor.data)
|
|
60
86
|
if actual != expected:
|
|
61
87
|
raise ValueError(
|
|
62
88
|
f"ndarray byte length mismatch: expected {expected}, got {actual}"
|
|
63
89
|
)
|
|
90
|
+
if iq16:
|
|
91
|
+
iq = np.frombuffer(descriptor.data, dtype=dtype).reshape(shape + (2,))
|
|
92
|
+
return (iq[..., 0].astype(np.float32) + 1j * iq[..., 1].astype(np.float32)).astype(np.complex64)
|
|
64
93
|
return np.frombuffer(descriptor.data, dtype=dtype).reshape(shape).copy()
|
|
65
94
|
|
|
66
95
|
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import hashlib
|
|
21
21
|
import os
|
|
22
22
|
import socket
|
|
23
|
+
import urllib.error
|
|
23
24
|
import urllib.request
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import Optional
|
|
@@ -38,6 +39,26 @@ def _ensure_parent_dir(p: Path) -> None:
|
|
|
38
39
|
def _looks_like_pem_cert(data: bytes) -> bool:
|
|
39
40
|
return b"BEGIN CERTIFICATE" in data and b"END CERTIFICATE" in data
|
|
40
41
|
|
|
42
|
+
|
|
43
|
+
def looks_like_pem_cert(data: bytes) -> bool:
|
|
44
|
+
"""Public alias of `_looks_like_pem_cert` for generic callers."""
|
|
45
|
+
return _looks_like_pem_cert(data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_ca_bytes(host: str, port: int, *, timeout_sec: float = 3.0) -> bytes:
|
|
49
|
+
"""Fetch raw CA certificate bytes (HTTP first, then raw-TCP fallback),
|
|
50
|
+
without writing anything to disk. Shared by direct-mode's
|
|
51
|
+
`fetch_and_save_ca_cert` below.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
return _fetch_http(host, port, timeout_sec)
|
|
55
|
+
except urllib.error.URLError as exc:
|
|
56
|
+
if isinstance(exc.reason, (OSError, socket.timeout)):
|
|
57
|
+
raise # nothing answered: a raw retry would only wait the timeout again
|
|
58
|
+
return _fetch_raw_tcp(host, port, timeout_sec)
|
|
59
|
+
except Exception:
|
|
60
|
+
return _fetch_raw_tcp(host, port, timeout_sec)
|
|
61
|
+
|
|
41
62
|
def sha256_fingerprint_pem(pem_bytes: bytes) -> str:
|
|
42
63
|
h = hashlib.sha256(pem_bytes).hexdigest()
|
|
43
64
|
return ":".join(h[i:i+2] for i in range(0, len(h), 2))
|
|
@@ -48,9 +69,16 @@ def _fetch_http(host: str, port: int, timeout_sec: float) -> bytes:
|
|
|
48
69
|
with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
|
|
49
70
|
return resp.read()
|
|
50
71
|
|
|
72
|
+
def _bare_host(host: str) -> str:
|
|
73
|
+
"""Strip IPv6 literal brackets (e.g. '[::1]' -> '::1') for raw-socket use."""
|
|
74
|
+
host = host.strip()
|
|
75
|
+
if host.startswith("[") and host.endswith("]"):
|
|
76
|
+
return host[1:-1]
|
|
77
|
+
return host
|
|
78
|
+
|
|
51
79
|
def _fetch_raw_tcp(host: str, port: int, timeout_sec: float) -> bytes:
|
|
52
80
|
chunks: list[bytes] = []
|
|
53
|
-
with socket.create_connection((host, port), timeout=timeout_sec) as s:
|
|
81
|
+
with socket.create_connection((_bare_host(host), port), timeout=timeout_sec) as s:
|
|
54
82
|
s.settimeout(timeout_sec)
|
|
55
83
|
while True:
|
|
56
84
|
try:
|
|
@@ -164,10 +192,7 @@ def fetch_and_save_ca_cert(
|
|
|
164
192
|
return True
|
|
165
193
|
|
|
166
194
|
# Fetch (HTTP first, then raw TCP fallback)
|
|
167
|
-
|
|
168
|
-
data = _fetch_http(host, port, timeout_sec)
|
|
169
|
-
except Exception:
|
|
170
|
-
data = _fetch_raw_tcp(host, port, timeout_sec)
|
|
195
|
+
data = fetch_ca_bytes(host, port, timeout_sec=timeout_sec)
|
|
171
196
|
|
|
172
197
|
if not data or not _looks_like_pem_cert(data):
|
|
173
198
|
return False
|