RemoteRF 2.0.13__tar.gz → 2.0.15__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {remoterf-2.0.13/src/remoterf.egg-info → remoterf-2.0.15}/PKG-INFO +3 -15
  2. remoterf-2.0.15/pyproject.toml +36 -0
  3. remoterf-2.0.15/src/remoteRF/common/grpc/grpc.proto +237 -0
  4. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/v2_codec.py +1 -1
  5. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/grpc_acc.py +1 -1
  6. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_device.py +44 -28
  7. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_v2.py +25 -38
  8. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/remoterf_cli.py +15 -1
  9. {remoterf-2.0.13 → remoterf-2.0.15/src/remoterf.egg-info}/PKG-INFO +3 -15
  10. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/SOURCES.txt +2 -39
  11. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/requires.txt +0 -1
  12. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_dynamic_device_codegen.py +76 -0
  13. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_dynamic_v2.py +55 -7
  14. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_remoterf_cli.py +26 -0
  15. remoterf-2.0.13/MANIFEST.in +0 -3
  16. remoterf-2.0.13/setup.py +0 -60
  17. {remoterf-2.0.13 → remoterf-2.0.15}/LICENSE +0 -0
  18. {remoterf-2.0.13 → remoterf-2.0.15}/README.md +0 -0
  19. {remoterf-2.0.13 → remoterf-2.0.15}/setup.cfg +0 -0
  20. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/__init__.py +0 -0
  21. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/__init__.py +0 -0
  22. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/__init__.py +0 -0
  23. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
  24. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
  25. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/tos_notice.txt +0 -0
  26. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/__init__.py +0 -0
  27. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/ansi_codes.py +0 -0
  28. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/api_token.py +0 -0
  29. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/banner.py +0 -0
  30. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/list_string.py +0 -0
  31. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/process_arg.py +0 -0
  32. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/__init__.py +0 -0
  33. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/cert_fetcher.py +0 -0
  34. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/config.py +0 -0
  35. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/__init__.py +0 -0
  36. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/acc_login.py +0 -0
  37. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/app.py +0 -0
  38. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/banner_note.txt +0 -0
  39. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
  40. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/grpc_client.py +0 -0
  41. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/v2_errors.py +0 -0
  42. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/__init__.py +0 -0
  43. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/_virtual.py +0 -0
  44. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
  45. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
  46. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
  47. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
  48. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
  49. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
  50. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/__init__.py +0 -0
  51. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
  52. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd.py +0 -0
  53. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
  54. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
  55. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
  56. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
  57. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/__init__.py +0 -0
  58. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
  59. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
  60. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/version.py +0 -0
  61. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/dependency_links.txt +0 -0
  62. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/entry_points.txt +0 -0
  63. {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/top_level.txt +0 -0
  64. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_banner.py +0 -0
  65. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_pluto_virtual.py +0 -0
  66. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_process_arg.py +0 -0
  67. {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_ti_mmwave.py +0 -0
@@ -1,12 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.0.13
3
+ Version: 2.0.15
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
- Author: Ethan Ge
6
- Author-email: ethoGalaxy@gmail.com
7
- License: GPL-3.0-or-later
5
+ Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
8
7
  Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
8
  Classifier: Operating System :: OS Independent
11
9
  Requires-Python: >=3.10
12
10
  Description-Content-Type: text/markdown
@@ -16,17 +14,7 @@ Requires-Dist: protobuf<7.0.0,>=6.31.1
16
14
  Requires-Dist: numpy
17
15
  Requires-Dist: prompt_toolkit
18
16
  Requires-Dist: python-dotenv
19
- Requires-Dist: prompt-toolkit
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: license
26
17
  Dynamic: license-file
27
- Dynamic: requires-dist
28
- Dynamic: requires-python
29
- Dynamic: summary
30
18
 
31
19
  # Remote RF
32
20
 
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "remoterf"
7
+ version = "2.0.15"
8
+ description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
9
+ readme = "README.md"
10
+ license = "GPL-3.0-or-later"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Ethan Ge", email = "ethoGalaxy@gmail.com" }]
13
+ requires-python = ">=3.10"
14
+ dependencies = [
15
+ "grpcio>=1.78.1,<2.0.0",
16
+ "protobuf>=6.31.1,<7.0.0",
17
+ "numpy",
18
+ "prompt_toolkit",
19
+ "python-dotenv",
20
+ ]
21
+ classifiers = [
22
+ "Programming Language :: Python :: 3",
23
+ "Operating System :: OS Independent",
24
+ ]
25
+
26
+ [project.scripts]
27
+ remoterf = "remoteRF.remoterf_cli:main"
28
+
29
+ [tool.setuptools]
30
+ package-dir = { "" = "src" }
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [tool.setuptools.package-data]
36
+ "*" = ["*.crt", "*.key", "*.pem", "*.txt", "*.pyi", "*.proto"]
@@ -0,0 +1,237 @@
1
+ // Copyright (C) 2026 RemoteRF
2
+ //
3
+ // This program is free software: you can redistribute it and/or modify
4
+ // it under the terms of the GNU General Public License as published by
5
+ // the Free Software Foundation, either version 3 of the License, or
6
+ // (at your option) any later version.
7
+ //
8
+ // This program is distributed in the hope that it will be useful,
9
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ // GNU General Public License for more details.
12
+ //
13
+ // You should have received a copy of the GNU General Public License
14
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
15
+
16
+ syntax = "proto3";
17
+
18
+ option java_package = "com.example.demo";
19
+ option java_outer_classname = "DemoProtos";
20
+
21
+ package remote_rf;
22
+
23
+ service GenericRPC {
24
+ rpc Call (GenericRPCRequest) returns (GenericRPCResponse);
25
+ }
26
+
27
+ // Dynamic protocol v2 is deliberately additive. GenericRPC remains the v1
28
+ // compatibility surface for existing generated clients and device schemas.
29
+ service DynamicControlV2 {
30
+ rpc Negotiate (NegotiateRequest) returns (NegotiateResponse);
31
+ rpc GetSchema (GetSchemaRequest) returns (GetSchemaResponse);
32
+ rpc OpenSession (OpenSessionRequest) returns (OpenSessionResponse);
33
+ rpc Invoke (InvokeRequest) returns (InvokeResponse);
34
+ rpc CloseHandle (CloseHandleRequest) returns (CloseResponse);
35
+ rpc CloseSession (CloseSessionRequest) returns (CloseResponse);
36
+ }
37
+
38
+ service SampleDataV1 {
39
+ rpc SampleStream (stream SampleFrame) returns (stream SampleFrame);
40
+ }
41
+
42
+ message GenericRPCRequest {
43
+ string function_name = 1;
44
+ map<string, Argument> args = 2;
45
+ }
46
+
47
+ message GenericRPCResponse {
48
+ map<string, Argument> results = 1;
49
+ }
50
+
51
+ message ArrayShape {
52
+ repeated int32 dim = 1;
53
+ }
54
+
55
+ message ComplexNumber {
56
+ float real = 1;
57
+ float imag = 2;
58
+ }
59
+
60
+ message ComplexNumpyArray {
61
+ ArrayShape shape = 1;
62
+ repeated ComplexNumber data = 2;
63
+ }
64
+
65
+ message RealNumpyArray {
66
+ ArrayShape shape = 1;
67
+ repeated float data = 2;
68
+ }
69
+
70
+ message Argument {
71
+ oneof value {
72
+ string string_value = 1;
73
+ int64 int64_value = 2;
74
+ float float_value = 3;
75
+ bool bool_value = 4;
76
+ ComplexNumpyArray complex_array = 5;
77
+ RealNumpyArray real_array = 6;
78
+ string json_value = 7;
79
+ bytes bytes_value = 8;
80
+ // Contiguous C-order numeric data; fields 5 and 6 remain decode-compatible.
81
+ NDArrayValue ndarray_value = 9;
82
+ }
83
+ }
84
+
85
+ message NDArrayValue {
86
+ string dtype = 1;
87
+ repeated int64 shape = 2;
88
+ bytes data = 3;
89
+ }
90
+
91
+ message DynamicValue {
92
+ oneof value {
93
+ bool null_value = 1;
94
+ bool bool_value = 2;
95
+ sint64 int64_value = 3;
96
+ double double_value = 4;
97
+ string string_value = 5;
98
+ bytes bytes_value = 6;
99
+ string json_value = 7;
100
+ NDArrayValue ndarray_value = 8;
101
+ }
102
+ }
103
+
104
+ message NamedValue {
105
+ string name = 1;
106
+ DynamicValue value = 2;
107
+ }
108
+
109
+ message Mutation {
110
+ string target = 1;
111
+ DynamicValue value = 2;
112
+ }
113
+
114
+ message ErrorEnvelope {
115
+ string code = 1;
116
+ string message = 2;
117
+ string details_json = 3;
118
+ string method = 4;
119
+ string native_exception = 5;
120
+ string uhd_version = 6;
121
+ bool retryable = 7;
122
+ bool fatal_to_session = 8;
123
+ }
124
+
125
+ message NegotiateRequest {
126
+ repeated string schema_versions = 1;
127
+ repeated string control_protocol_versions = 2;
128
+ repeated string streaming_protocol_versions = 3;
129
+ }
130
+
131
+ message NegotiateResponse {
132
+ string schema_version = 1;
133
+ string control_protocol_version = 2;
134
+ string streaming_protocol_version = 3;
135
+ ErrorEnvelope error = 4;
136
+ }
137
+
138
+ message GetSchemaRequest {
139
+ string token = 1;
140
+ repeated string schema_versions = 2;
141
+ }
142
+
143
+ message GetSchemaResponse {
144
+ string schema_version = 1;
145
+ string schema_json = 2;
146
+ string schema_hash = 3;
147
+ ErrorEnvelope error = 4;
148
+ }
149
+
150
+ message OpenSessionRequest {
151
+ string token = 1;
152
+ string requested_schema_hash = 2;
153
+ string control_protocol_version = 3;
154
+ string streaming_protocol_version = 4;
155
+ }
156
+
157
+ message OpenSessionResponse {
158
+ string session_id = 1;
159
+ string device_handle = 2;
160
+ string schema_json = 3;
161
+ string schema_hash = 4;
162
+ string capabilities_json = 5;
163
+ string uhd_version = 6;
164
+ string uhd_abi = 7;
165
+ string hardware_profile = 8;
166
+ ErrorEnvelope error = 9;
167
+ }
168
+
169
+ message InvokeRequest {
170
+ string session_id = 1;
171
+ string handle = 2;
172
+ string method = 3;
173
+ string overload_id = 4;
174
+ repeated NamedValue args = 5;
175
+ }
176
+
177
+ message InvokeResponse {
178
+ DynamicValue result = 1;
179
+ repeated Mutation mutations = 2;
180
+ ErrorEnvelope error = 3;
181
+ }
182
+
183
+ message CloseHandleRequest {
184
+ string session_id = 1;
185
+ string handle = 2;
186
+ }
187
+
188
+ message CloseSessionRequest {
189
+ string session_id = 1;
190
+ }
191
+
192
+ message CloseResponse {
193
+ bool closed = 1;
194
+ ErrorEnvelope error = 2;
195
+ }
196
+
197
+ enum SampleDirection {
198
+ SAMPLE_DIRECTION_UNSPECIFIED = 0;
199
+ SAMPLE_DIRECTION_RX = 1;
200
+ SAMPLE_DIRECTION_TX = 2;
201
+ }
202
+
203
+ enum SampleFrameKind {
204
+ SAMPLE_FRAME_UNSPECIFIED = 0;
205
+ SAMPLE_FRAME_OPEN = 1;
206
+ SAMPLE_FRAME_REQUEST = 2;
207
+ SAMPLE_FRAME_DATA = 3;
208
+ SAMPLE_FRAME_RESULT = 4;
209
+ SAMPLE_FRAME_CREDIT = 5;
210
+ SAMPLE_FRAME_CANCEL = 6;
211
+ SAMPLE_FRAME_CLOSE = 7;
212
+ SAMPLE_FRAME_ERROR = 8;
213
+ }
214
+
215
+ message SampleFrame {
216
+ string session_id = 1;
217
+ string handle = 2;
218
+ uint64 generation = 3;
219
+ string operation_id = 4;
220
+ uint64 sequence = 5;
221
+ SampleDirection direction = 6;
222
+ SampleFrameKind kind = 7;
223
+ string dtype = 8;
224
+ repeated int64 shape = 9;
225
+ repeated uint32 channels = 10;
226
+ uint64 sample_count = 11;
227
+ bytes payload = 12;
228
+ string metadata_json = 13;
229
+ double device_time_secs = 14;
230
+ uint32 credits = 15;
231
+ double timeout_sec = 16;
232
+ bool one_packet = 17;
233
+ bool end_of_stream = 18;
234
+ bool cancelled = 19;
235
+ ErrorEnvelope error = 20;
236
+ uint64 flags = 21;
237
+ }
@@ -48,7 +48,7 @@ def _json_safe(value):
48
48
  if hasattr(value, "as_payload"):
49
49
  return _json_safe(value.as_payload())
50
50
  if isinstance(value, np.generic):
51
- return value.item()
51
+ return _json_safe(value.item())
52
52
  if isinstance(value, complex):
53
53
  return {
54
54
  _JSON_TYPE_KEY: "complex",
@@ -56,7 +56,7 @@ class RemoteRFAccount:
56
56
  token = unmap_arg(response.results["Token"])
57
57
  try:
58
58
  from ..drivers.dynamic_device import install_driver
59
- install_driver(device_id=device_id)
59
+ install_driver(token=token, device_id=device_id)
60
60
  except Exception as e:
61
61
  print(f"Warning: could not install driver for device {device_id}: {e}")
62
62
  return token
@@ -36,6 +36,7 @@ from __future__ import annotations
36
36
 
37
37
  import json
38
38
  import keyword
39
+ import sys
39
40
  from pathlib import Path
40
41
 
41
42
  from ..common.utils import map_arg, unmap_arg
@@ -89,29 +90,20 @@ def fetch_idl(
89
90
  "schema_hash": "sha256:...",
90
91
  }
91
92
  """
92
- if token is not None and prefer_v2:
93
- from .dynamic_v2 import fetch_schema_v2
94
- from ..core.v2_errors import RemoteRFProtocolError, RemoteRFTransportError
95
-
96
- try:
97
- return fetch_schema_v2(str(token))
98
- except RemoteRFProtocolError:
99
- # The selected device may only publish v1 (for example Pluto).
100
- pass
101
- except RemoteRFTransportError as exc:
102
- # Old servers report the additive v2 service as UNIMPLEMENTED.
103
- if exc.details.get("grpc_code") != "UNIMPLEMENTED":
104
- raise
105
-
106
93
  args: dict = {}
107
- if token is not None:
108
- args['token'] = map_arg(str(token))
109
- elif device_id is not None:
94
+ if device_id is not None:
110
95
  args['device_id'] = map_arg(int(device_id))
96
+ elif token is not None:
97
+ args['token'] = map_arg(str(token))
111
98
  elif device_name is not None:
112
99
  args['device_name'] = map_arg(str(device_name))
113
100
  else:
114
101
  raise ValueError("fetch_idl requires token, device_id, or device_name")
102
+ if prefer_v2:
103
+ # One RPC either way: servers that know v2 answer with it when the
104
+ # device publishes one (also by device_id, which needs no active
105
+ # reservation); v1-only devices and older servers answer v1.
106
+ args['schema_versions'] = map_arg(["2.0"])
115
107
 
116
108
  from ..core.grpc_client import rpc_client
117
109
 
@@ -1381,8 +1373,14 @@ _DRIVERS_DIR = Path(__file__).parent
1381
1373
 
1382
1374
 
1383
1375
  def _write_driver_files(schema: dict) -> Path:
1384
- if str(schema.get("schema_version")) == "2.0":
1385
- return _write_v2_driver_files(schema)
1376
+ pkg_dir = _write_v2_driver_files(schema) if str(schema.get("schema_version")) == "2.0" else _write_v1_driver_files(schema)
1377
+ stale = f"remoteRF.drivers.{pkg_dir.name}"
1378
+ for name in [m for m in sys.modules if m == stale or m.startswith(stale + ".")]:
1379
+ del sys.modules[name]
1380
+ return pkg_dir
1381
+
1382
+
1383
+ def _write_v1_driver_files(schema: dict) -> Path:
1386
1384
 
1387
1385
  (
1388
1386
  device_type,
@@ -1397,8 +1395,13 @@ def _write_driver_files(schema: dict) -> Path:
1397
1395
  _call_map,
1398
1396
  ) = _schema_maps(schema)
1399
1397
  pkg_dir = _DRIVERS_DIR / device_type
1398
+ remote_path = pkg_dir / f"{device_type}_remote.py"
1399
+ if remote_path.exists() and _is_v2_driver(remote_path):
1400
+ # The server answered v1 (older server, or a device that no longer
1401
+ # publishes v2); don't trade an installed Dynamic v2 driver for it.
1402
+ return pkg_dir
1400
1403
  pkg_dir.mkdir(exist_ok=True)
1401
- (pkg_dir / f"{device_type}_remote.py").write_text(_codegen(schema), encoding="utf-8")
1404
+ remote_path.write_text(_codegen(schema), encoding="utf-8")
1402
1405
  init_lines = [
1403
1406
  *_GPL_NOTICE_LINES,
1404
1407
  "from importlib import import_module as _import_module",
@@ -1550,11 +1553,7 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
1550
1553
  device_name=device_name,
1551
1554
  prefer_v2=True,
1552
1555
  )
1553
- device_type = (
1554
- _require_package_name(schema.get("device_type"), field="device_type")
1555
- if str(schema.get("schema_version")) == "2.0"
1556
- else _schema_maps(schema)[0]
1557
- )
1556
+ device_type = _schema_device_type(schema)
1558
1557
  remote_path = _DRIVERS_DIR / device_type / f"{device_type}_remote.py"
1559
1558
 
1560
1559
  if not remote_path.exists():
@@ -1566,6 +1565,8 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
1566
1565
  current_hash = _read_schema_hash(remote_path)
1567
1566
  if current_hash != schema.get("schema_hash"):
1568
1567
  _write_driver_files(schema)
1568
+ if _read_schema_hash(remote_path) == current_hash:
1569
+ return
1569
1570
  print(
1570
1571
  f"Driver for '{device_type}' updated "
1571
1572
  f"(was {(current_hash or '?')[:16]}…, "
@@ -1574,6 +1575,19 @@ def ensure_driver(*, token: str = None, device_id: int = None, device_name: str
1574
1575
  )
1575
1576
 
1576
1577
 
1578
+ def _schema_device_type(schema: dict) -> str:
1579
+ if str(schema.get("schema_version")) == "2.0":
1580
+ return _require_package_name(schema.get("device_type"), field="device_type")
1581
+ return _schema_maps(schema)[0]
1582
+
1583
+
1584
+ def _is_v2_driver(path: Path) -> bool:
1585
+ try:
1586
+ return "build_uhd_bindings" in path.read_text(encoding="utf-8")
1587
+ except OSError:
1588
+ return False
1589
+
1590
+
1577
1591
  def _read_schema_hash(path: Path) -> str | None:
1578
1592
  """Extract _SCHEMA_HASH value from a generated driver file without importing it."""
1579
1593
  try:
@@ -1601,12 +1615,14 @@ def install_driver_if_stale(*, token: str, current_hash: str) -> bool:
1601
1615
 
1602
1616
  from remoteRF.drivers.pluto import *
1603
1617
  """
1604
- schema = fetch_idl(token=token, prefer_v2=False)
1618
+ schema = fetch_idl(token=token, prefer_v2=True)
1605
1619
  if schema.get("schema_hash") == current_hash:
1606
1620
  return False
1607
1621
 
1608
- device_type = _schema_maps(schema)[0]
1609
- _write_driver_files(schema)
1622
+ device_type = _schema_device_type(schema)
1623
+ pkg_dir = _write_driver_files(schema)
1624
+ if _read_schema_hash(pkg_dir / f"{device_type}_remote.py") == current_hash:
1625
+ return False
1610
1626
  print(
1611
1627
  f"Driver for '{device_type}' updated "
1612
1628
  f"(was {current_hash[:16]}…, now {schema.get('schema_hash','?')[:16]}…).\n"
@@ -21,7 +21,6 @@ import hashlib
21
21
  import json
22
22
  import keyword
23
23
  import numbers
24
- import re
25
24
  import types
26
25
  import weakref
27
26
 
@@ -83,24 +82,6 @@ _PARAMETER_KINDS = {
83
82
  _PARAMETER_DIRECTIONS = {"in", "out", "inout"}
84
83
 
85
84
 
86
- def _canonical_native_version(value) -> str:
87
- """Strip distro/build metadata while retaining the pinned upstream API."""
88
- text = str(value or "").strip()
89
- if text.lower().startswith("uhd "):
90
- text = text[4:].strip()
91
- match = re.match(
92
- r"^(\d+)\.(\d+)\.(\d+)(?:\.(\d+|main|head))?",
93
- text,
94
- flags=re.IGNORECASE,
95
- )
96
- if match is None:
97
- return text.partition("-")[0].strip()
98
- major, minor, patch, build = match.groups()
99
- if build is None or build.lower() in {"main", "head"}:
100
- build = "0"
101
- return ".".join((major, minor, patch, build))
102
-
103
-
104
85
  def validate_schema_v2(schema: dict) -> dict:
105
86
  if not isinstance(schema, dict) or schema.get("schema_version") != "2.0":
106
87
  raise RemoteRFProtocolError("Dynamic schema_version must be '2.0'")
@@ -223,9 +204,13 @@ def _matches_type(value, wire_type: str) -> bool:
223
204
  return True
224
205
  if choice == "bytes" and isinstance(value, (bytes, bytearray, memoryview)):
225
206
  return True
226
- if choice == "complex" and isinstance(value, (complex, np.complexfloating)):
207
+ if (
208
+ choice == "complex"
209
+ and isinstance(value, (numbers.Complex, np.number))
210
+ and not isinstance(value, bool)
211
+ ):
227
212
  return True
228
- if choice.startswith("list") and isinstance(value, (list, tuple)):
213
+ if choice.startswith("list") and isinstance(value, (list, tuple, np.ndarray)):
229
214
  if "[" not in choice:
230
215
  return True
231
216
  item_type = choice.partition("[")[2].rpartition("]")[0]
@@ -295,7 +280,11 @@ class OverloadBinder:
295
280
  else:
296
281
  raise TypeError(f"missing required argument {name!r}")
297
282
 
298
- if supplied and not _matches_type(value, spec.get("type", "any")):
283
+ if (
284
+ supplied
285
+ and not (value is None and not spec.get("required", True))
286
+ and not _matches_type(value, spec.get("type", "any"))
287
+ ):
299
288
  raise TypeError(
300
289
  f"argument {name!r} must match {spec.get('type')}, "
301
290
  f"not {type(value).__name__}"
@@ -678,23 +667,21 @@ def build_uhd_bindings(schema: dict, *, transport_factory=DynamicV2Transport):
678
667
  self.token = str(token)
679
668
  self._transport = transport_factory()
680
669
  self._transport.negotiate()
681
- opened = self._transport.open_session(self.token, schema["schema_hash"])
682
- required_uhd = str(
683
- schema.get("native_api", {}).get("version") or ""
684
- )
685
- actual_uhd = str(opened.get("uhd_version") or "")
686
- if (
687
- required_uhd
688
- and _canonical_native_version(actual_uhd)
689
- != _canonical_native_version(required_uhd)
690
- ):
691
- try:
692
- self._transport.close_session(opened["session_id"])
693
- finally:
670
+ try:
671
+ opened = self._transport.open_session(self.token, schema["schema_hash"])
672
+ except RemoteRFProtocolError as exc:
673
+ if not exc.details.get("current"):
674
+ raise
675
+ from .dynamic_device import install_driver_if_stale
676
+
677
+ device_type = schema.get("device_type", "usrp")
678
+ if install_driver_if_stale(token=self.token, current_hash=schema["schema_hash"]):
694
679
  raise RemoteRFProtocolError(
695
- f"client requires UHD {required_uhd}; "
696
- f"server reports {actual_uhd or 'unknown'}"
697
- )
680
+ f"the server's {device_type} driver changed and the local copy "
681
+ f"was updated; reimport remoteRF.drivers.{device_type} and retry",
682
+ details=dict(exc.details),
683
+ ) from exc
684
+ raise
698
685
  self._session_id = opened["session_id"]
699
686
  self._handle = opened["device_handle"]
700
687
  self._generation = 0
@@ -134,6 +134,7 @@ def print_help() -> None:
134
134
  printf("Config:", (Sty.BOLD, Sty.MAGENTA))
135
135
  printf(" remoterf -c | --config [options]", Sty.CYAN)
136
136
  printf(" -a, --addr, -addr <host:port>", Sty.CYAN, " Set target server", Sty.DEFAULT)
137
+ printf(" --cert-port <port>", Sty.CYAN, " Cert server port (default: grpc port + 1)", Sty.DEFAULT)
137
138
  printf(" -w, --wipe, -wipe", Sty.CYAN, " Delete all local config", Sty.DEFAULT)
138
139
  printf(" -y, --yes, -yes", Sty.CYAN, " Skip wipe confirmation", Sty.DEFAULT)
139
140
  print()
@@ -182,6 +183,7 @@ def main() -> int:
182
183
  addr = None
183
184
  wipe = False
184
185
  yes = False
186
+ cert_port_arg = None
185
187
 
186
188
  i = 1
187
189
  while i < len(argv):
@@ -195,6 +197,18 @@ def main() -> int:
195
197
  i += 2
196
198
  continue
197
199
 
200
+ if tok in ("--cert-port", "-cert-port"):
201
+ if i + 1 >= len(argv):
202
+ print("ERROR: missing required argument after --cert-port")
203
+ return 2
204
+ try:
205
+ cert_port_arg = int(argv[i + 1].strip())
206
+ except Exception:
207
+ print("ERROR: --cert-port must be an integer")
208
+ return 2
209
+ i += 2
210
+ continue
211
+
198
212
  if tok in ("--wipe", "-w", "-wipe"):
199
213
  wipe = True
200
214
  i += 1
@@ -235,7 +249,7 @@ def main() -> int:
235
249
  return 2
236
250
 
237
251
  # configure returns the proper exit code
238
- cert_port = port + 1
252
+ cert_port = cert_port_arg if cert_port_arg is not None else port + 1
239
253
  configure(host, port, cert_port)
240
254
  return 0
241
255
 
@@ -1,12 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoterf
3
- Version: 2.0.13
3
+ Version: 2.0.15
4
4
  Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
5
- Author: Ethan Ge
6
- Author-email: ethoGalaxy@gmail.com
7
- License: GPL-3.0-or-later
5
+ Author-email: Ethan Ge <ethoGalaxy@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
8
7
  Classifier: Programming Language :: Python :: 3
9
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
8
  Classifier: Operating System :: OS Independent
11
9
  Requires-Python: >=3.10
12
10
  Description-Content-Type: text/markdown
@@ -16,17 +14,7 @@ Requires-Dist: protobuf<7.0.0,>=6.31.1
16
14
  Requires-Dist: numpy
17
15
  Requires-Dist: prompt_toolkit
18
16
  Requires-Dist: python-dotenv
19
- Requires-Dist: prompt-toolkit
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: license
26
17
  Dynamic: license-file
27
- Dynamic: requires-dist
28
- Dynamic: requires-python
29
- Dynamic: summary
30
18
 
31
19
  # Remote RF
32
20
 
@@ -1,13 +1,13 @@
1
1
  LICENSE
2
- MANIFEST.in
3
2
  README.md
4
- setup.py
3
+ pyproject.toml
5
4
  src/remoteRF/__init__.py
6
5
  src/remoteRF/remoterf_cli.py
7
6
  src/remoteRF/version.py
8
7
  src/remoteRF/common/__init__.py
9
8
  src/remoteRF/common/tos_notice.txt
10
9
  src/remoteRF/common/grpc/__init__.py
10
+ src/remoteRF/common/grpc/grpc.proto
11
11
  src/remoteRF/common/grpc/grpc_pb2.py
12
12
  src/remoteRF/common/grpc/grpc_pb2_grpc.py
13
13
  src/remoteRF/common/grpc/v2_codec.py
@@ -48,49 +48,12 @@ src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py
48
48
  src/remoteRF/drivers/usrp/__init__.py
49
49
  src/remoteRF/drivers/usrp/usrp_remote.py
50
50
  src/remoteRF/drivers/usrp/usrp_remote.pyi
51
- src/remoterf/__init__.py
52
- src/remoterf/remoterf_cli.py
53
- src/remoterf/version.py
54
51
  src/remoterf.egg-info/PKG-INFO
55
52
  src/remoterf.egg-info/SOURCES.txt
56
53
  src/remoterf.egg-info/dependency_links.txt
57
54
  src/remoterf.egg-info/entry_points.txt
58
55
  src/remoterf.egg-info/requires.txt
59
56
  src/remoterf.egg-info/top_level.txt
60
- src/remoterf/common/__init__.py
61
- src/remoterf/common/tos_notice.txt
62
- src/remoterf/common/grpc/__init__.py
63
- src/remoterf/common/grpc/grpc_pb2.py
64
- src/remoterf/common/grpc/grpc_pb2_grpc.py
65
- src/remoterf/common/grpc/v2_codec.py
66
- src/remoterf/common/utils/__init__.py
67
- src/remoterf/common/utils/ansi_codes.py
68
- src/remoterf/common/utils/api_token.py
69
- src/remoterf/common/utils/list_string.py
70
- src/remoterf/common/utils/process_arg.py
71
- src/remoterf/config/__init__.py
72
- src/remoterf/config/cert_fetcher.py
73
- src/remoterf/config/config.py
74
- src/remoterf/core/__init__.py
75
- src/remoterf/core/acc_login.py
76
- src/remoterf/core/app.py
77
- src/remoterf/core/banner_note.txt
78
- src/remoterf/core/dynamic_v2_transport.py
79
- src/remoterf/core/grpc_acc.py
80
- src/remoterf/core/grpc_client.py
81
- src/remoterf/core/v2_errors.py
82
- src/remoterf/drivers/__init__.py
83
- src/remoterf/drivers/dynamic_device.py
84
- src/remoterf/drivers/dynamic_v2.py
85
- src/remoterf/drivers/adalm_pluto/__init__.py
86
- src/remoterf/drivers/adalm_pluto/pluto_remote.py
87
- src/remoterf/drivers/support/__init__.py
88
- src/remoterf/drivers/support/uhd.py
89
- src/remoterf/drivers/support/uhd_dsp.py
90
- src/remoterf/drivers/support/uhd_v2.py
91
- src/remoterf/drivers/usrp/__init__.py
92
- src/remoterf/drivers/usrp/usrp_remote.py
93
- src/remoterf/drivers/usrp/usrp_remote.pyi
94
57
  tests/test_banner.py
95
58
  tests/test_dynamic_device_codegen.py
96
59
  tests/test_dynamic_v2.py
@@ -3,4 +3,3 @@ protobuf<7.0.0,>=6.31.1
3
3
  numpy
4
4
  prompt_toolkit
5
5
  python-dotenv
6
- prompt-toolkit
@@ -13,6 +13,7 @@
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
15
 
16
+ import json
16
17
  import os
17
18
  import unittest
18
19
  import sys
@@ -1055,3 +1056,78 @@ class DynamicDeviceCodegenTests(unittest.TestCase):
1055
1056
 
1056
1057
  if __name__ == "__main__":
1057
1058
  unittest.main()
1059
+
1060
+
1061
+ class DriverInstallPathTests(unittest.TestCase):
1062
+ """The default USRP driver must land on Dynamic v2 and stay there."""
1063
+
1064
+ V2 = {
1065
+ "schema_version": "2.0", "device_type": "usrp", "client_class": "MultiUSRP", "driver_version": "t",
1066
+ "native_api": {}, "hardware_profiles": [], "objects": [], "methods": [], "errors": [],
1067
+ "capability_fields": [], "coverage": {}, "deprecations": [],
1068
+ }
1069
+ V1 = {"schema_version": "1.0", "device_type": "usrp", "client_class": "MultiUSRP", "driver_version": "t",
1070
+ "schema_hash": "sha256:v1", "getters": {}, "setters": {}, "calls": {}}
1071
+
1072
+ @classmethod
1073
+ def setUpClass(cls):
1074
+ import hashlib
1075
+ body = json.dumps(cls.V2, sort_keys=True, separators=(",", ":"))
1076
+ cls.V2 = dict(cls.V2, schema_hash="sha256:" + hashlib.sha256(body.encode()).hexdigest())
1077
+
1078
+ def _run(self, respond, call):
1079
+ seen = []
1080
+
1081
+ class Response:
1082
+ def __init__(self, results):
1083
+ self.results = results
1084
+
1085
+ def rpc_client(function_name, args):
1086
+ plain = {k: unmap_arg(v) for k, v in args.items()}
1087
+ seen.append(plain)
1088
+ return Response({"schema": map_arg(json.dumps(respond(plain)))})
1089
+
1090
+ old_rpc_client = fake_grpc_client.rpc_client
1091
+ old_drivers_dir = dynamic_device._DRIVERS_DIR
1092
+ fake_grpc_client.rpc_client = rpc_client
1093
+ try:
1094
+ with tempfile.TemporaryDirectory() as temp_dir:
1095
+ dynamic_device._DRIVERS_DIR = Path(temp_dir)
1096
+ result = call()
1097
+ remote = Path(temp_dir) / "usrp" / "usrp_remote.py"
1098
+ text = remote.read_text() if remote.exists() else ""
1099
+ finally:
1100
+ fake_grpc_client.rpc_client = old_rpc_client
1101
+ dynamic_device._DRIVERS_DIR = old_drivers_dir
1102
+ return seen, text, result
1103
+
1104
+ def test_install_asks_the_legacy_endpoint_for_v2_and_writes_a_v2_driver(self):
1105
+ seen, text, _ = self._run(
1106
+ lambda plain: self.V2 if "2.0" in plain.get("schema_versions", []) else self.V1,
1107
+ lambda: dynamic_device.install_driver(token="future-token", device_id=4),
1108
+ )
1109
+ # device_id first: a not-yet-active reservation's token is inert.
1110
+ self.assertEqual(seen, [{"device_id": 4, "schema_versions": ["2.0"]}])
1111
+ self.assertIn("build_uhd_bindings", text)
1112
+
1113
+ def test_a_v1_answer_never_replaces_an_installed_v2_driver(self):
1114
+ def call():
1115
+ dynamic_device._write_driver_files(self.V2)
1116
+ dynamic_device.install_driver(device_id=4)
1117
+ self.assertFalse(dynamic_device.install_driver_if_stale(token="t", current_hash=self.V2["schema_hash"]))
1118
+
1119
+ seen, text, _ = self._run(lambda plain: self.V1, call)
1120
+ self.assertEqual(len(seen), 2)
1121
+ self.assertIn("build_uhd_bindings", text)
1122
+
1123
+ def test_rewriting_a_driver_drops_the_cached_package_so_reimport_works(self):
1124
+ sys.modules["remoteRF.drivers.usrp"] = types.ModuleType("remoteRF.drivers.usrp")
1125
+ sys.modules["remoteRF.drivers.usrp.usrp_remote"] = types.ModuleType("remoteRF.drivers.usrp.usrp_remote")
1126
+ try:
1127
+ self._run(lambda plain: self.V2,
1128
+ lambda: dynamic_device.install_driver_if_stale(token="t", current_hash="sha256:v1"))
1129
+ self.assertNotIn("remoteRF.drivers.usrp", sys.modules)
1130
+ self.assertNotIn("remoteRF.drivers.usrp.usrp_remote", sys.modules)
1131
+ finally:
1132
+ sys.modules.pop("remoteRF.drivers.usrp", None)
1133
+ sys.modules.pop("remoteRF.drivers.usrp.usrp_remote", None)
@@ -509,20 +509,68 @@ class DynamicV2Tests(unittest.TestCase):
509
509
  self.assertEqual(encoded.WhichOneof("value"), "json_value")
510
510
  self.assertEqual(decode_value(encoded), all_indexes)
511
511
 
512
- def test_client_rejects_a_different_upstream_uhd_version(self):
513
- class MismatchedTransport(FakeTransport):
512
+ def test_client_accepts_an_older_upstream_uhd_version(self):
513
+ class OlderTransport(FakeTransport):
514
514
  def open_session(self, token, schema_hash):
515
515
  result = super().open_session(token, schema_hash)
516
- result["uhd_version"] = "4.10.0.1-0ubuntu1"
516
+ result["uhd_version"] = "3.15.0.0"
517
517
  return result
518
518
 
519
- transport = MismatchedTransport()
519
+ transport = OlderTransport()
520
520
  _, MultiUSRP = build_uhd_bindings(
521
521
  schema(), transport_factory=lambda: transport
522
522
  )
523
- with self.assertRaisesRegex(Exception, "requires UHD 4.10.0.0"):
524
- MultiUSRP("token")
525
- self.assertEqual(transport.closed_sessions, ["session"])
523
+ device = MultiUSRP("token")
524
+ self.assertEqual(transport.closed_sessions, [])
525
+ self.assertTrue(device.close())
526
+
527
+ def test_binder_is_no_stricter_than_native_uhd(self):
528
+ dc_offset = descriptor(
529
+ "uhd.usrp.MultiUSRP",
530
+ "set_rx_dc_offset",
531
+ [
532
+ overload("offset", [param("offset", "complex"), param("chan", "int", required=False, default=0)]),
533
+ overload("enable", [param("enb", "bool"), param("chan", "int", required=False, default=0)]),
534
+ ],
535
+ )
536
+ overload_id, bound = OverloadBinder(dc_offset).bind((0,), {})
537
+ self.assertEqual((overload_id, bound), ("offset", {"offset": 0, "chan": 0}))
538
+ overload_id, _ = OverloadBinder(dc_offset).bind((False,), {})
539
+ self.assertEqual(overload_id, "enable")
540
+ with self.assertRaisesRegex(TypeError, "incompatible arguments"):
541
+ OverloadBinder(dc_offset).bind(("0",), {})
542
+
543
+ optional = descriptor(
544
+ "uhd.usrp.MultiUSRP",
545
+ "get_rx_freq",
546
+ [overload("default", [param("chan", "int", required=False, default=0)], "float")],
547
+ )
548
+ _, bound = OverloadBinder(optional).bind((), {"chan": None})
549
+ self.assertEqual(bound, {"chan": None})
550
+
551
+ phases = descriptor(
552
+ "uhd.usrp.MultiUSRP",
553
+ "phaser_set_phases",
554
+ [overload("default", [param("phases_deg", "list[float]")], "list[float]")],
555
+ )
556
+ OverloadBinder(phases).bind((np.linspace(0, 315, 8),), {}) # numpy arrays are sequences
557
+ self.assertEqual(decode_value(encode_value(np.complex64(1 + 2j))), 1 + 2j)
558
+
559
+ def test_a_stale_v2_driver_refreshes_itself_and_says_so(self):
560
+ from unittest.mock import patch
561
+
562
+ class StaleTransport(FakeTransport):
563
+ def open_session(self, token, schema_hash):
564
+ raise RemoteRFProtocolError(
565
+ "client schema hash is stale",
566
+ details={"requested": schema_hash, "current": "sha256:newer"},
567
+ )
568
+
569
+ _, MultiUSRP = build_uhd_bindings(schema(), transport_factory=StaleTransport)
570
+ with patch("remoteRF.drivers.dynamic_device.install_driver_if_stale", return_value=True) as refresh:
571
+ with self.assertRaisesRegex(RemoteRFProtocolError, "reimport remoteRF.drivers.usrp"):
572
+ MultiUSRP("token")
573
+ refresh.assert_called_once_with(token="token", current_hash=schema()["schema_hash"])
526
574
 
527
575
  def test_client_accepts_conda_forge_uhd_main_release_version(self):
528
576
  class CondaForgeTransport(FakeTransport):
@@ -48,6 +48,32 @@ class RemoteRFCliTests(unittest.TestCase):
48
48
  connected_server.assert_called_once_with()
49
49
  print_unavailable.assert_called_once_with()
50
50
 
51
+ @mock.patch("remoteRF.config.config.configure")
52
+ @mock.patch.object(
53
+ remoterf_cli.sys, "argv", ["remoterf", "--config", "--addr", "10.0.0.1:12321"]
54
+ )
55
+ def test_config_without_cert_port_defaults_to_grpc_port_plus_one(self, configure):
56
+ self.assertEqual(remoterf_cli.main(), 0)
57
+ configure.assert_called_once_with("10.0.0.1", 12321, 12322)
58
+
59
+ @mock.patch("remoteRF.config.config.configure")
60
+ @mock.patch.object(
61
+ remoterf_cli.sys,
62
+ "argv",
63
+ ["remoterf", "--config", "--addr", "10.0.0.1:12321", "--cert-port", "9999"],
64
+ )
65
+ def test_config_with_explicit_cert_port_overrides_default(self, configure):
66
+ self.assertEqual(remoterf_cli.main(), 0)
67
+ configure.assert_called_once_with("10.0.0.1", 12321, 9999)
68
+
69
+ @mock.patch("remoteRF.config.config.configure")
70
+ @mock.patch.object(
71
+ remoterf_cli.sys, "argv", ["remoterf", "-c", "-a", "10.0.0.1:12321"]
72
+ )
73
+ def test_existing_short_flag_usage_remains_compatible(self, configure):
74
+ self.assertEqual(remoterf_cli.main(), 0)
75
+ configure.assert_called_once_with("10.0.0.1", 12321, 12322)
76
+
51
77
 
52
78
  if __name__ == "__main__":
53
79
  unittest.main()
@@ -1,3 +0,0 @@
1
- # Include all .crt, .key, and .pem files
2
- recursive-include src/remoteRF *.crt *.key *.pem
3
- recursive-include src/remoteRF *.txt
remoterf-2.0.13/setup.py DELETED
@@ -1,60 +0,0 @@
1
- # Copyright (C) 2026 RemoteRF
2
- #
3
- # This program is free software: you can redistribute it and/or modify
4
- # it under the terms of the GNU General Public License as published by
5
- # the Free Software Foundation, either version 3 of the License, or
6
- # (at your option) any later version.
7
- #
8
- # This program is distributed in the hope that it will be useful,
9
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
- # GNU General Public License for more details.
12
- #
13
- # You should have received a copy of the GNU General Public License
14
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
-
16
- from setuptools import setup, find_packages
17
- import os
18
-
19
- with open("README.md", "r", encoding="utf-8") as fh:
20
- long_description = fh.read()
21
-
22
- setup(
23
- name="remoterf",
24
- version="2.0.13",
25
- author="Ethan Ge",
26
- author_email="ethoGalaxy@gmail.com",
27
- description="A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.",
28
- long_description=long_description, # Set the README content here
29
- long_description_content_type="text/markdown", # Specify that it's Markdown
30
- packages=find_packages(where="src"), # Automatically finds subpackages like core, deviceA, deviceB
31
- package_dir={"": "src"},
32
- license='GPL-3.0-or-later',
33
- include_package_data=True, # Includes files specified in MANIFEST.in
34
- install_requires=[
35
- "grpcio>=1.78.1,<2.0.0", "protobuf>=6.31.1,<7.0.0", "numpy", "prompt_toolkit", "python-dotenv", "prompt-toolkit"
36
- ],
37
- classifiers=[
38
- "Programming Language :: Python :: 3",
39
- "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
40
- "Operating System :: OS Independent",
41
- ],
42
- python_requires='>=3.10',
43
- # entry_points={
44
- # 'console_scripts': [
45
- # 'remoterf-login=remoteRF.core.acc_login:main',
46
- # 'remoterf-v=remoteRF.core.version:main',
47
- # 'remoterf-config=remoteRF.config.config_cli:main',
48
- # ],
49
- # },
50
-
51
- entry_points={
52
- "console_scripts": [
53
- "remoterf=remoteRF.remoterf_cli:main",
54
- # "remoterf-login=remoteRF.core.acc_login:main",
55
- # "remoterf-v=remoteRF.core.version:main",
56
- # "remoterf-config=remoteRF.config.config_cli:main",
57
- ],
58
- },
59
-
60
- )
File without changes
File without changes
File without changes