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.
- {remoterf-2.0.13/src/remoterf.egg-info → remoterf-2.0.15}/PKG-INFO +3 -15
- remoterf-2.0.15/pyproject.toml +36 -0
- remoterf-2.0.15/src/remoteRF/common/grpc/grpc.proto +237 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/v2_codec.py +1 -1
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/grpc_acc.py +1 -1
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_device.py +44 -28
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/dynamic_v2.py +25 -38
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/remoterf_cli.py +15 -1
- {remoterf-2.0.13 → remoterf-2.0.15/src/remoterf.egg-info}/PKG-INFO +3 -15
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/SOURCES.txt +2 -39
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/requires.txt +0 -1
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_dynamic_device_codegen.py +76 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_dynamic_v2.py +55 -7
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_remoterf_cli.py +26 -0
- remoterf-2.0.13/MANIFEST.in +0 -3
- remoterf-2.0.13/setup.py +0 -60
- {remoterf-2.0.13 → remoterf-2.0.15}/LICENSE +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/README.md +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/setup.cfg +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/tos_notice.txt +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/ansi_codes.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/api_token.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/banner.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/list_string.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/common/utils/process_arg.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/cert_fetcher.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/config/config.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/acc_login.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/app.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/banner_note.txt +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/grpc_client.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/core/v2_errors.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/_virtual.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/adalm_pluto/pluto_remote.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/__init__.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoteRF/version.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_banner.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.0.13 → remoterf-2.0.15}/tests/test_process_arg.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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"
|
|
696
|
-
f"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
513
|
-
class
|
|
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"] = "
|
|
516
|
+
result["uhd_version"] = "3.15.0.0"
|
|
517
517
|
return result
|
|
518
518
|
|
|
519
|
-
transport =
|
|
519
|
+
transport = OlderTransport()
|
|
520
520
|
_, MultiUSRP = build_uhd_bindings(
|
|
521
521
|
schema(), transport_factory=lambda: transport
|
|
522
522
|
)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
self.
|
|
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()
|
remoterf-2.0.13/MANIFEST.in
DELETED
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|