remoterf 0.1.0.3__py3-none-any.whl

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.
@@ -0,0 +1,320 @@
1
+ import socket
2
+ import getpass
3
+ from pathlib import Path
4
+ import os
5
+ from dotenv import load_dotenv
6
+
7
+ import grpc
8
+ from ..common.grpc import grpc_pb2
9
+ from ..common.grpc import grpc_pb2_grpc
10
+ from ..common.utils import *
11
+
12
+ load_dotenv(Path.home() / ".config" / "remoterf" / ".env")
13
+ addr = os.getenv("REMOTERF_ADDR") # "host:port"
14
+ ca_path = os.getenv("REMOTERF_CA_CERT") # path to saved CA cert
15
+
16
+ options = [
17
+ ('grpc.max_send_message_length', 100 * 1024 * 1024),
18
+ ('grpc.max_receive_message_length', 100 * 1024 * 1024),
19
+ ]
20
+
21
+ # Server.crt
22
+ certs_path = Path(ca_path).expanduser().resolve()
23
+ with certs_path.open('rb') as f:
24
+ trusted_certs = f.read()
25
+
26
+ credentials = grpc.ssl_channel_credentials(root_certificates=trusted_certs)
27
+ channel = grpc.secure_channel(addr, credentials, options=options)
28
+ stub = grpc_pb2_grpc.GenericRPCStub(channel)
29
+
30
+ tcp_calls = 0
31
+
32
+ def get_tcp_calls():
33
+ return tcp_calls
34
+
35
+ def rpc_client(*, function_name, args):
36
+ global tcp_calls
37
+ tcp_calls += 1
38
+ # print(tcp_calls)
39
+ # if not is_connected:
40
+ # response = rpc_client(function_name="UserLogin", args={"username": grpc_pb2.Argument(string_value=input("Username: ")), "password": grpc_pb2.Argument(string_value=getpass.getpass("Password: ")), "client_ip": grpc_pb2.Argument(string_value=local_ip)})
41
+ # if (response.results['status'].string_value == 'Success'):
42
+ # print("Login successful.")
43
+ # is_connected = True
44
+
45
+ # TODO: Handle user login
46
+
47
+ # print(f"Opening connection to {server_ip}:{server_port}")
48
+
49
+ # TODO: Handle Errors
50
+
51
+ # print(f"Calling function: {function_name}")
52
+ response = stub.Call(grpc_pb2.GenericRPCRequest(function_name=function_name, args=args))
53
+
54
+ if 'a' in response.results:
55
+ print(f"Error: {unmap_arg(response.results['a'])}")
56
+ exit()
57
+
58
+ if 'UE' in response.results:
59
+ print(f"UserError: {unmap_arg(response.results['UE'])}")
60
+ input("Hit enter to continue...")
61
+
62
+ if 'Message' in response.results:
63
+ print(f"{unmap_arg(response.results['Message'])}")
64
+
65
+
66
+ return response
67
+
68
+ #region Example Usage
69
+
70
+ # if __name__ == '__main__':
71
+ # args={
72
+ # "key1": grpc_pb2.Argument(string_value="Hello"),
73
+ # "key2": grpc_pb2.Argument(int32_value=123),
74
+ # "key3": grpc_pb2.Argument(float_value=4.56),
75
+ # "key4": grpc_pb2.Argument(bool_value=True),
76
+ # "client_ip": grpc_pb2.Argument(string_value=local_ip)
77
+ # }
78
+
79
+ # response = rpc_client(function_name="echo", args=args)
80
+
81
+ # if 'client_ip' in response.results:
82
+ # del response.results['client_ip']
83
+
84
+ # # Print results
85
+ # print("Received response:")
86
+ # for key, arg in response.results.items():
87
+ # # Decode the oneof fields
88
+ # if arg.HasField('string_value'):
89
+ # value = arg.string_value
90
+ # elif arg.HasField('int32_value'):
91
+ # value = arg.int32_value
92
+ # elif arg.HasField('float_value'):
93
+ # value = arg.float_value
94
+ # elif arg.HasField('bool_value'):
95
+ # value = arg.bool_value
96
+ # else:
97
+ # value = "Undefined"
98
+
99
+ # print(f"{key}: {value}")
100
+
101
+ #endregion
102
+
103
+ from typing import Any, Dict, Optional
104
+
105
+ def handle_admin_command(inpu: str):
106
+ """
107
+ Parses commands like:
108
+ admin help
109
+ admin printa | printr | printp | printd
110
+ admin rm aa | rm ar | rm a <username>
111
+ admin setacc <username> <U|P|A> [devices=1,2,3] [max_res=5] [max_time=1800]
112
+
113
+ Requires account.is_admin == True (client-side gate). Server still enforces.
114
+ """
115
+ if not getattr(account, "is_admin", False):
116
+ print("Access denied: you are not an Admin.")
117
+ return
118
+
119
+ tokens = (inpu or "").strip().split()
120
+ if len(tokens) < 2 or tokens[0].lower() != "admin":
121
+ print("Usage: admin <command>. Try: admin help")
122
+ return
123
+
124
+ cmd = tokens[1].lower()
125
+
126
+ def call_admin(fn: str, extra: dict | None = None):
127
+ return remote_admin_rpc_client(
128
+ function_name=fn,
129
+ auth_un=account.username,
130
+ auth_pw=account.password,
131
+ args=extra or {},
132
+ print_result=True,
133
+ )
134
+
135
+ if cmd in ("help", "h"):
136
+ print("Admin commands:")
137
+ print(" admin printa - Print all accounts")
138
+ print(" admin printr - Print all reservations")
139
+ print(" admin printp - Print all perms")
140
+ print(" admin printd - Print all devices")
141
+ print(" admin rm aa - Remove all accounts")
142
+ print(" admin rm ar - Remove all reservations")
143
+ print(" admin rm a <username> - Remove one account")
144
+ print(" admin setacc <u> <U|P|A> [devices=1,2] [max_res=3] [max_time=1800]")
145
+ return
146
+
147
+ # ----- print* -----
148
+ if cmd in ("printa", "print_accounts", "print_all_accounts"):
149
+ call_admin("print_all_accounts")
150
+ return
151
+
152
+ if cmd in ("printr", "print_res", "print_all_reservations"):
153
+ call_admin("print_all_reservations")
154
+ return
155
+
156
+ if cmd in ("printp", "print_perms", "print_all_perms"):
157
+ call_admin("print_all_perms")
158
+ return
159
+
160
+ if cmd in ("printd", "print_devices", "print_all_devices"):
161
+ call_admin("print_all_devices")
162
+ return
163
+
164
+ # ----- rm -----
165
+ if cmd == "rm":
166
+ if len(tokens) < 3:
167
+ print("Usage: admin rm <aa|ar|a> [username]")
168
+ return
169
+
170
+ sub = tokens[2].lower()
171
+
172
+ if sub == "aa":
173
+ call_admin("remove_all_users")
174
+ return
175
+
176
+ if sub == "ar":
177
+ call_admin("remove_all_reservations")
178
+ return
179
+
180
+ if sub == "a":
181
+ if len(tokens) < 4:
182
+ print("Usage: admin rm a <username>")
183
+ return
184
+ call_admin("remove_user", {"username": tokens[3]})
185
+ return
186
+
187
+ print("Unknown rm subcommand. Use: aa, ar, a <username>")
188
+ return
189
+
190
+ # ----- setacc -----
191
+ if cmd in ("setacc", "set_account", "setperm", "set"):
192
+ if len(tokens) < 4:
193
+ print("Usage: admin setacc <username> <U|P|A> [devices=1,2] [max_res=3] [max_time=1800]")
194
+ return
195
+
196
+ target_user = tokens[2]
197
+ perm = tokens[3].upper()
198
+
199
+ extra: dict = {"username": target_user, "permission": perm}
200
+
201
+ # Parse optional k=v args (very lightweight parser)
202
+ # Examples:
203
+ # devices=1,2,3
204
+ # max_res=5
205
+ # max_time=1800
206
+ for t in tokens[4:]:
207
+ if "=" not in t:
208
+ continue
209
+ k, v = t.split("=", 1)
210
+ k = k.strip().lower()
211
+ v = v.strip()
212
+
213
+ if k in ("devices", "device_list", "devices_allowed", "device_ids"):
214
+ # server-side remote_admin_call already supports "1,2,3" as string
215
+ extra["device_list"] = v
216
+ elif k in ("max_res", "max_reservations"):
217
+ extra["max_reservations"] = int(v)
218
+ elif k in ("max_time", "max_res_time", "max_reservation_time_sec"):
219
+ extra["max_reservation_time_sec"] = int(v)
220
+
221
+ call_admin("set_account", extra)
222
+ return
223
+
224
+ print(f"Unknown admin command: {cmd}. Try: admin help")
225
+
226
+
227
+ def remote_admin_rpc_client(
228
+ *,
229
+ function_name: str,
230
+ auth_un: str,
231
+ auth_pw: str,
232
+ args: Optional[Dict[str, Any]] = None,
233
+ raise_on_error: bool = False,
234
+ print_result: bool = False,
235
+ ) -> Dict[str, Any]:
236
+ """
237
+ Client helper for RemoteAdmin:<function> calls.
238
+
239
+ Builds and sends:
240
+ function_name = "RemoteAdmin:<function_name>"
241
+ args include:
242
+ auth_un, auth_pw (mapped via map_arg)
243
+ + any additional args (mapped via map_arg if not already grpc_pb2.Argument)
244
+
245
+ Returns:
246
+ {
247
+ "ok": bool,
248
+ "result": str | None,
249
+ "error": str | None,
250
+ "traceback": str | None,
251
+ "raw": grpc_pb2.GenericRPCResponse,
252
+ }
253
+ """
254
+ fn = (function_name or "").strip()
255
+ if fn.startswith("RemoteAdmin:"):
256
+ rpc_fn = fn
257
+ else:
258
+ rpc_fn = f"RemoteAdmin:{fn}"
259
+
260
+ payload: Dict[str, Any] = {
261
+ "auth_un": map_arg(auth_un),
262
+ "auth_pw": map_arg(auth_pw),
263
+ }
264
+
265
+ if args:
266
+ for k, v in args.items():
267
+ if v is None:
268
+ continue
269
+ # If caller already passed a grpc Argument, keep it.
270
+ if isinstance(v, grpc_pb2.Argument):
271
+ payload[str(k)] = v
272
+ else:
273
+ payload[str(k)] = map_arg(v)
274
+
275
+ resp = rpc_client(function_name=rpc_fn, args=payload)
276
+
277
+ # Parse the standardized RemoteAdmin response keys:
278
+ # Ok, Result, Error, Traceback (each typically map_arg-encoded)
279
+ results = getattr(resp, "results", {}) or {}
280
+
281
+ def _get_str(key: str) -> Optional[str]:
282
+ if key not in results:
283
+ return None
284
+ try:
285
+ return str(unmap_arg(results[key]))
286
+ except Exception:
287
+ try:
288
+ return str(results[key])
289
+ except Exception:
290
+ return None
291
+
292
+ ok_val = results.get("Ok", None)
293
+ ok = False
294
+ if ok_val is not None:
295
+ try:
296
+ ok = bool(unmap_arg(ok_val))
297
+ except Exception:
298
+ ok = False
299
+
300
+ out = {
301
+ "ok": ok,
302
+ "result": _get_str("Result"),
303
+ "error": _get_str("Error"),
304
+ "traceback": _get_str("Traceback"),
305
+ "raw": resp,
306
+ }
307
+
308
+ if print_result:
309
+ if out["ok"]:
310
+ print(out["result"] or "OK")
311
+ else:
312
+ print(out["error"] or "RemoteAdmin call failed.")
313
+ if out["traceback"]:
314
+ print(out["traceback"])
315
+
316
+ if raise_on_error and not out["ok"]:
317
+ msg = out["error"] or "RemoteAdmin call failed."
318
+ raise RuntimeError(msg)
319
+
320
+ return out
@@ -0,0 +1,8 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ def main():
4
+ try:
5
+ v = version("remoterf")
6
+ except PackageNotFoundError:
7
+ v = "unknown"
8
+ print(v)
File without changes
@@ -0,0 +1 @@
1
+ from . import pluto_remote as adi
@@ -0,0 +1,249 @@
1
+ from ...core.grpc_client import rpc_client
2
+ from ...common.utils import *
3
+ from ...common.grpc import grpc_pb2
4
+ from ...core.grpc_client import get_tcp_calls
5
+
6
+ def try_get(function_name, token):
7
+ try:
8
+ return unmap_arg(rpc_client(function_name=f"Pluto:{function_name}:GET", args={'a':map_arg(token)}).results[function_name])
9
+ except Exception as e:
10
+ input(f"Error: {e}\nHit enter to continue...")
11
+ return None
12
+
13
+ def try_set(function_name, value, token):
14
+ try:
15
+ rpc_client(function_name=f"Pluto:{function_name}:SET", args={function_name: map_arg(value), 'a':map_arg(token)})
16
+ except Exception as e:
17
+ input(f"Error: {e}\nHit enter to continue...")
18
+
19
+ def try_call_0_arg(function_name, token): # 0 argument call
20
+ try:
21
+ response = rpc_client(
22
+ function_name=f"Pluto:{function_name}:CALL0",
23
+ args={
24
+ 'a': map_arg(token)
25
+ }
26
+ )
27
+ return unmap_arg(response.results[function_name])
28
+ except Exception as e:
29
+ input(f"RPC_0_call Error: {e}\nHit enter to continue...")
30
+ return None
31
+
32
+ def try_call_1_arg(function_name, arg, token): # 1 argument call
33
+ try:
34
+ response = rpc_client(
35
+ function_name=f"Pluto:{function_name}:CALL1",
36
+ args={
37
+ 'a': map_arg(token),
38
+ 'arg1': map_arg(arg)
39
+ }
40
+ )
41
+ # The server should return something like {function_name: <something>}
42
+ return unmap_arg(response.results[function_name])
43
+ except Exception as e:
44
+ input(f"RPC_1_call Error: {e}\nHit enter to continue...")
45
+ return None
46
+
47
+ class rx_def:
48
+ pass
49
+
50
+ class tx_def:
51
+ pass
52
+
53
+ class rx_tx_def(rx_def, tx_def):
54
+ pass
55
+
56
+ class ad9364(rx_tx_def):
57
+ pass
58
+
59
+ class Pluto: # client
60
+
61
+ def __init__(self, token:str, debug=False):
62
+ self.token = token
63
+ response = try_call_0_arg(function_name="ip", token=token)
64
+
65
+
66
+ def api_token(self, token:str) -> None:
67
+ self.token = token
68
+ try_call_0_arg(function_name="ip", token=token)
69
+
70
+ # PlutoSDR
71
+
72
+ _device_name = "PlutoSDR"
73
+ _uri_auto = "ip:pluto.local"
74
+
75
+ def __repr__(self): # ! UNTESTED !
76
+ return try_get("__repr__", self.token)
77
+
78
+ #region ad9364
79
+ """AD9364 Transceiver"""
80
+
81
+ @property
82
+ def filter(self):
83
+ return try_get("filter", self.token)
84
+
85
+ @filter.setter
86
+ def filter(self, value):
87
+ try_set("filter", value, self.token)
88
+
89
+ @property
90
+ def loopback(self):
91
+ """loopback: Set loopback mode. Options are:
92
+ 0 (Disable), 1 (Digital), 2 (RF)"""
93
+ return try_get("loopback", self.token)
94
+
95
+ @loopback.setter
96
+ def loopback(self, value):
97
+ try_set("loopback", value, self.token)
98
+
99
+ @property
100
+ def gain_control_mode_chan0(self):
101
+ """gain_control_mode_chan0: Mode of receive path AGC. Options are:
102
+ slow_attack, fast_attack, manual"""
103
+ return try_get("gain_control_mode_chan0", self.token)
104
+
105
+ @gain_control_mode_chan0.setter
106
+ def gain_control_mode_chan0(self, value):
107
+ try_set("gain_control_mode_chan0", value, self.token)
108
+
109
+ @property
110
+ def rx_hardwaregain_chan0(self):
111
+ """rx_hardwaregain_chan0: Gain applied to RX path. Only applicable when
112
+ gain_control_mode is set to 'manual'"""
113
+ return try_get("rx_hardwaregain_chan0", self.token)
114
+
115
+ @rx_hardwaregain_chan0.setter
116
+ def rx_hardwaregain_chan0(self, value):
117
+ try_set("rx_hardwaregain_chan0", value, self.token)
118
+
119
+ @property
120
+ def tx_hardwaregain_chan0(self):
121
+ """tx_hardwaregain_chan0: Attenuation applied to TX path"""
122
+ return try_get("tx_hardwaregain_chan0", self.token)
123
+
124
+ @tx_hardwaregain_chan0.setter
125
+ def tx_hardwaregain_chan0(self, value):
126
+ try_set("tx_hardwaregain_chan0", value, self.token)
127
+
128
+ @property
129
+ def rx_rf_bandwidth(self):
130
+ """rx_rf_bandwidth: Bandwidth of front-end analog filter of RX path"""
131
+ return try_get("rx_rf_bandwidth", self.token)
132
+
133
+ @rx_rf_bandwidth.setter
134
+ def rx_rf_bandwidth(self, value):
135
+ try_set("rx_rf_bandwidth", value, self.token)
136
+
137
+ @property
138
+ def tx_rf_bandwidth(self):
139
+ """tx_rf_bandwidth: Bandwidth of front-end analog filter of TX path"""
140
+ return try_get("tx_rf_bandwidth", self.token)
141
+
142
+ @tx_rf_bandwidth.setter
143
+ def tx_rf_bandwidth(self, value):
144
+ try_set("tx_rf_bandwidth", value, self.token)
145
+
146
+ @property
147
+ def sample_rate(self): # ! UNTESTED !
148
+ """sample_rate: Sample rate RX and TX paths in samples per second"""
149
+ return try_get("sample_rate", self.token)
150
+
151
+ @sample_rate.setter
152
+ def sample_rate(self, rate): # ! UNTESTED !
153
+ try_set("sample_rate", rate, self.token)
154
+
155
+ @property
156
+ def rx_lo(self):
157
+ """rx_lo: Carrier frequency of RX path"""
158
+ return try_get("rx_lo", self.token)
159
+
160
+ @rx_lo.setter
161
+ def rx_lo(self, value):
162
+ try_set("rx_lo", value, self.token)
163
+
164
+ @property
165
+ def tx_lo(self):
166
+ """tx_lo: Carrier frequency of TX path"""
167
+ return try_get("tx_lo", self.token)
168
+
169
+ @tx_lo.setter
170
+ def tx_lo(self, value):
171
+ try_set("tx_lo", value, self.token)
172
+
173
+ @property
174
+ def tx_cyclic_buffer(self):
175
+ """tx_cyclic_buffer: Size of cyclic buffer"""
176
+ return try_get("tx_cyclic_buffer", self.token)
177
+
178
+ @tx_cyclic_buffer.setter
179
+ def tx_cyclic_buffer(self, value):
180
+ try_set("tx_cyclic_buffer", value, self.token)
181
+
182
+ def tx_destroy_buffer(self):
183
+ try_call_0_arg("tx_destroy_buffer", self.token)
184
+
185
+ def rx_destroy_buffer(self):
186
+ try_call_0_arg("rx_destroy_buffer", self.token)
187
+
188
+ #endregion
189
+
190
+ #region rx_def
191
+
192
+ def rx(self):
193
+ return try_get("rx", self.token)
194
+
195
+ @property
196
+ def rx_buffer_size(self):
197
+ return try_get("rx_buffer_size", self.token)
198
+
199
+ @rx_buffer_size.setter
200
+ def rx_buffer_size(self, value):
201
+ try_set("rx_buffer_size", value, self.token)
202
+
203
+ #endregion
204
+
205
+ #region tx_def
206
+
207
+ def tx(self, value):
208
+ return try_call_1_arg("tx", value, self.token)
209
+
210
+ # @tx.setter
211
+ # def tx(self, value):
212
+ # try_set("tx", value, self.token)
213
+
214
+ #endregion
215
+
216
+ #region tx
217
+
218
+ #endregion
219
+
220
+ #region _dec_int_fpga_filter
221
+
222
+ """Decimator and interpolator fpga filter controls"""
223
+
224
+ def _get_rates(self, dev, output): # ! UNTESTED !
225
+ """Get the decimation and interpolation rates"""
226
+ return try_get("rates", self.token)
227
+
228
+ @property
229
+ def rx_dec8_filter_en(self) -> bool: # ! UNTESTED !
230
+ """rx_dec8_filter_en: Enable decimate by 8 filter in FPGA"""
231
+ return try_get("rx_dec8_filter_en", self.token)
232
+
233
+ @rx_dec8_filter_en.setter
234
+ def rx_dec8_filter_en(self, value: bool): # ! UNTESTED !
235
+ """rx_dec8_filter_en: Enable decimate by 8 filter in FPGA"""
236
+ return try_set("rx_dec8_filter_en", value, self.token)
237
+
238
+ @property
239
+ def tx_int8_filter_en(self) -> bool: # ! UNTESTED !
240
+ """tx_int8_filter_en: Enable interpolate by 8 filter in FPGA"""
241
+ return try_get("tx_int8_filter_en", self.token)
242
+
243
+ @tx_int8_filter_en.setter
244
+ def tx_int8_filter_en(self, value: bool): # ! UNTESTED !
245
+ """tx_int8_filter_en: Enable interpolate by 8 filter in FPGA"""
246
+ return try_set("tx_int8_filter_en", value, self.token)
247
+
248
+ #endregion
249
+