RemoteRF 2.0.15__tar.gz → 2.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {remoterf-2.0.15 → remoterf-2.1.1}/PKG-INFO +70 -1
  2. remoterf-2.0.15/src/remoterf.egg-info/PKG-INFO → remoterf-2.1.1/README.md +62 -18
  3. {remoterf-2.0.15 → remoterf-2.1.1}/pyproject.toml +10 -1
  4. {remoterf-2.0.15/src/remoteRF/drivers/adalm_pluto → remoterf-2.1.1/src/remoteRF/common/grpc}/__init__.py +1 -1
  5. remoterf-2.1.1/src/remoteRF/common/grpc/deployment_capabilities.proto +15 -0
  6. remoterf-2.1.1/src/remoteRF/common/grpc/deployment_capabilities_pb2.py +41 -0
  7. remoterf-2.1.1/src/remoteRF/common/grpc/deployment_capabilities_pb2_grpc.py +100 -0
  8. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/banner.py +13 -10
  9. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/process_arg.py +31 -2
  10. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/config/cert_fetcher.py +30 -5
  11. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/config/config.py +84 -7
  12. remoterf-2.0.15/src/remoteRF/common/grpc/__init__.py → remoterf-2.1.1/src/remoteRF/core/acc_login.py +11 -1
  13. remoterf-2.1.1/src/remoteRF/core/app.py +1685 -0
  14. remoterf-2.1.1/src/remoteRF/core/direct_path.py +772 -0
  15. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/core/dynamic_v2_transport.py +91 -4
  16. remoterf-2.1.1/src/remoteRF/core/grpc_acc.py +159 -0
  17. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/core/grpc_client.py +200 -52
  18. remoterf-2.1.1/src/remoteRF/core/secure_channel.py +72 -0
  19. remoterf-2.1.1/src/remoteRF/deployment/__init__.py +1 -0
  20. remoterf-2.1.1/src/remoteRF/deployment/backend.py +416 -0
  21. remoterf-2.1.1/src/remoteRF/deployment/direct.py +38 -0
  22. remoterf-2.1.1/src/remoteRF/deployment/homes.py +383 -0
  23. remoterf-2.1.1/src/remoteRF/deployment/http.py +121 -0
  24. remoterf-2.1.1/src/remoteRF/deployment/state.py +187 -0
  25. remoterf-2.1.1/src/remoteRF/drivers/adalm_pluto/__init__.py +21 -0
  26. remoterf-2.1.1/src/remoteRF/drivers/adalm_pluto/adalm_pluto_remote.py +400 -0
  27. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/dynamic_device.py +49 -9
  28. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/dynamic_v2.py +2 -0
  29. remoterf-2.0.15/src/remoteRF/core/acc_login.py → remoterf-2.1.1/src/remoteRF/drivers/sim_sdr/__init__.py +5 -3
  30. remoterf-2.1.1/src/remoteRF/drivers/sim_sdr/sim_sdr_remote.py +183 -0
  31. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd_v2.py +9 -0
  32. remoterf-2.1.1/src/remoteRF/remoterf_cli.py +587 -0
  33. remoterf-2.1.1/src/remoteRF.py +39 -0
  34. remoterf-2.0.15/README.md → remoterf-2.1.1/src/remoterf.egg-info/PKG-INFO +87 -0
  35. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoterf.egg-info/SOURCES.txt +31 -2
  36. remoterf-2.1.1/src/remoterf.egg-info/requires.txt +12 -0
  37. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_banner.py +4 -4
  38. remoterf-2.1.1/tests/test_config_address.py +128 -0
  39. remoterf-2.1.1/tests/test_deployment_accounts.py +582 -0
  40. remoterf-2.1.1/tests/test_direct_path_fixed.py +95 -0
  41. remoterf-2.1.1/tests/test_direct_path_partner.py +146 -0
  42. remoterf-2.1.1/tests/test_direct_path_race.py +129 -0
  43. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_dynamic_device_codegen.py +41 -5
  44. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_dynamic_v2.py +29 -0
  45. remoterf-2.1.1/tests/test_federated_devices.py +115 -0
  46. remoterf-2.1.1/tests/test_fetch_idl_fallback.py +45 -0
  47. remoterf-2.1.1/tests/test_finish_prompt.py +20 -0
  48. remoterf-2.1.1/tests/test_gate_i_direct_path.py +470 -0
  49. remoterf-2.1.1/tests/test_grpc_client_target.py +80 -0
  50. remoterf-2.1.1/tests/test_homes.py +609 -0
  51. remoterf-2.1.1/tests/test_native_account_credentials.py +12 -0
  52. remoterf-2.1.1/tests/test_native_devices.py +57 -0
  53. remoterf-2.1.1/tests/test_partner_dynamic_v2.py +95 -0
  54. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_pluto_virtual.py +6 -0
  55. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_process_arg.py +21 -0
  56. {remoterf-2.0.15 → remoterf-2.1.1}/tests/test_remoterf_cli.py +9 -3
  57. remoterf-2.1.1/tests/test_upgrade_rescue.py +45 -0
  58. remoterf-2.0.15/src/remoteRF/core/app.py +0 -983
  59. remoterf-2.0.15/src/remoteRF/core/grpc_acc.py +0 -79
  60. remoterf-2.0.15/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -291
  61. remoterf-2.0.15/src/remoteRF/remoterf_cli.py +0 -271
  62. remoterf-2.0.15/src/remoterf.egg-info/requires.txt +0 -5
  63. {remoterf-2.0.15 → remoterf-2.1.1}/LICENSE +0 -0
  64. {remoterf-2.0.15 → remoterf-2.1.1}/setup.cfg +0 -0
  65. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/__init__.py +0 -0
  66. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/__init__.py +0 -0
  67. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc.proto +0 -0
  68. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
  69. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
  70. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/grpc/v2_codec.py +0 -0
  71. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/tos_notice.txt +0 -0
  72. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/__init__.py +0 -0
  73. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/ansi_codes.py +0 -0
  74. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/api_token.py +0 -0
  75. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/common/utils/list_string.py +0 -0
  76. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/config/__init__.py +0 -0
  77. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/core/__init__.py +0 -0
  78. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/core/banner_note.txt +0 -0
  79. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/core/v2_errors.py +0 -0
  80. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/__init__.py +0 -0
  81. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/_virtual.py +0 -0
  82. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
  83. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
  84. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
  85. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  86. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/support/__init__.py +0 -0
  87. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
  88. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd.py +0 -0
  89. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
  90. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
  91. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  92. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/__init__.py +0 -0
  93. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
  94. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
  95. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoteRF/version.py +0 -0
  96. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoterf.egg-info/dependency_links.txt +0 -0
  97. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoterf.egg-info/entry_points.txt +0 -0
  98. {remoterf-2.0.15 → remoterf-2.1.1}/src/remoterf.egg-info/top_level.txt +0 -0
  99. {remoterf-2.0.15 → remoterf-2.1.1}/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.15
3
+ Version: 2.1.1
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
5
  Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -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.15"
7
+ version = "2.1.1"
8
8
  description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
@@ -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"]
@@ -13,4 +13,4 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
15
 
16
- from . import pluto_remote as adi
16
+ from . import grpc_pb2, grpc_pb2_grpc
@@ -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 = 35
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 the Wireless Lab at UCLA",
116
+ "Created by https://wireless.ee.ucla.edu",
117
117
  f"VERSION {version}",
118
- f"SERVER {server or 'NA'}",
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 "NA")
175
+ server = str(server or "")
173
176
  detail_options = (
174
- f"v{version} {separator} {server}",
175
- f"v{short_version} {separator} {server}",
176
- f"v{short_version}",
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
- try:
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