remoteRF-server 1.0.0__tar.gz → 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/PKG-INFO +1 -1
  2. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/pyproject.toml +1 -1
  3. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_pb2.py +7 -7
  4. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/schema.py +106 -24
  5. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/process_arg.py +67 -3
  6. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +10 -2
  7. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/grpc_server.py +3 -1
  8. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/rpc_manager.py +10 -0
  9. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/serverrf_cli.py +2 -0
  10. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  11. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/SOURCES.txt +4 -1
  12. remoterf_server-1.0.1/tests/test_idl_schema.py +81 -0
  13. remoterf_server-1.0.1/tests/test_process_arg.py +44 -0
  14. remoterf_server-1.0.1/tests/test_rpc_manager_calln.py +71 -0
  15. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/README.md +0 -0
  16. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/setup.cfg +0 -0
  17. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/__init__.py +0 -0
  18. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/__init__.py +0 -0
  19. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  20. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  21. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  22. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  23. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/__init__.py +0 -0
  24. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  25. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  26. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  27. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/__init__.py +0 -0
  28. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  29. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/api_token.py +0 -0
  30. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  31. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/db_location.py +0 -0
  32. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/list_string.py +0 -0
  33. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/__init__.py +0 -0
  34. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  35. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  36. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  37. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/host/__init__.py +0 -0
  38. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_auth_token.py +0 -0
  39. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_directory_store.py +0 -0
  40. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  41. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/__init__.py +0 -0
  42. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/acc_perms.py +0 -0
  43. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/cert_provider.py +0 -0
  44. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/device_manager.py +0 -0
  45. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/reservation.py +0 -0
  46. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/user_group_cli.py +0 -0
  47. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/server/user_group_handler.py +0 -0
  48. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/tools/__init__.py +0 -0
  49. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gen_certs.py +0 -0
  50. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gist_status.py +0 -0
  51. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  52. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  53. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  54. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/requires.txt +0 -0
  55. {remoterf_server-1.0.0 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: RemoteRF server-side control package
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.0.0"
7
+ version = "1.0.1"
8
8
  description = "RemoteRF server-side control package"
9
9
  requires-python = ">=3.8"
10
10
  dependencies = [
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: grpc.proto
5
- # Protobuf Python Version: 5.29.0
5
+ # Protobuf Python Version: 5.27.1
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -12,8 +12,8 @@ from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
14
  5,
15
- 29,
16
- 0,
15
+ 27,
16
+ 1,
17
17
  '',
18
18
  'grpc.proto'
19
19
  )
@@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default()
24
24
 
25
25
 
26
26
 
27
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngrpc.proto\x12\tremote_rf\"\xa2\x01\n\x11GenericRPCRequest\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x34\n\x04\x61rgs\x18\x02 \x03(\x0b\x32&.remote_rf.GenericRPCRequest.ArgsEntry\x1a@\n\tArgsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.remote_rf.Argument:\x02\x38\x01\"\x96\x01\n\x12GenericRPCResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.remote_rf.GenericRPCResponse.ResultsEntry\x1a\x43\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.remote_rf.Argument:\x02\x38\x01\"\x19\n\nArrayShape\x12\x0b\n\x03\x64im\x18\x01 \x03(\x05\"+\n\rComplexNumber\x12\x0c\n\x04real\x18\x01 \x01(\x02\x12\x0c\n\x04imag\x18\x02 \x01(\x02\"a\n\x11\x43omplexNumpyArray\x12$\n\x05shape\x18\x01 \x01(\x0b\x32\x15.remote_rf.ArrayShape\x12&\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x18.remote_rf.ComplexNumber\"D\n\x0eRealNumpyArray\x12$\n\x05shape\x18\x01 \x01(\x0b\x32\x15.remote_rf.ArrayShape\x12\x0c\n\x04\x64\x61ta\x18\x02 \x03(\x02\"\xd7\x01\n\x08\x41rgument\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x15\n\x0bint64_value\x18\x02 \x01(\x03H\x00\x12\x15\n\x0b\x66loat_value\x18\x03 \x01(\x02H\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12\x35\n\rcomplex_array\x18\x05 \x01(\x0b\x32\x1c.remote_rf.ComplexNumpyArrayH\x00\x12/\n\nreal_array\x18\x06 \x01(\x0b\x32\x19.remote_rf.RealNumpyArrayH\x00\x42\x07\n\x05value2Q\n\nGenericRPC\x12\x43\n\x04\x43\x61ll\x12\x1c.remote_rf.GenericRPCRequest\x1a\x1d.remote_rf.GenericRPCResponseB\x1e\n\x10\x63om.example.demoB\nDemoProtosb\x06proto3')
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ngrpc.proto\x12\tremote_rf\"\xa2\x01\n\x11GenericRPCRequest\x12\x15\n\rfunction_name\x18\x01 \x01(\t\x12\x34\n\x04\x61rgs\x18\x02 \x03(\x0b\x32&.remote_rf.GenericRPCRequest.ArgsEntry\x1a@\n\tArgsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.remote_rf.Argument:\x02\x38\x01\"\x96\x01\n\x12GenericRPCResponse\x12;\n\x07results\x18\x01 \x03(\x0b\x32*.remote_rf.GenericRPCResponse.ResultsEntry\x1a\x43\n\x0cResultsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.remote_rf.Argument:\x02\x38\x01\"\x19\n\nArrayShape\x12\x0b\n\x03\x64im\x18\x01 \x03(\x05\"+\n\rComplexNumber\x12\x0c\n\x04real\x18\x01 \x01(\x02\x12\x0c\n\x04imag\x18\x02 \x01(\x02\"a\n\x11\x43omplexNumpyArray\x12$\n\x05shape\x18\x01 \x01(\x0b\x32\x15.remote_rf.ArrayShape\x12&\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x18.remote_rf.ComplexNumber\"D\n\x0eRealNumpyArray\x12$\n\x05shape\x18\x01 \x01(\x0b\x32\x15.remote_rf.ArrayShape\x12\x0c\n\x04\x64\x61ta\x18\x02 \x03(\x02\"\xed\x01\n\x08\x41rgument\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x15\n\x0bint64_value\x18\x02 \x01(\x03H\x00\x12\x15\n\x0b\x66loat_value\x18\x03 \x01(\x02H\x00\x12\x14\n\nbool_value\x18\x04 \x01(\x08H\x00\x12\x35\n\rcomplex_array\x18\x05 \x01(\x0b\x32\x1c.remote_rf.ComplexNumpyArrayH\x00\x12/\n\nreal_array\x18\x06 \x01(\x0b\x32\x19.remote_rf.RealNumpyArrayH\x00\x12\x14\n\njson_value\x18\x07 \x01(\tH\x00\x42\x07\n\x05value2Q\n\nGenericRPC\x12\x43\n\x04\x43\x61ll\x12\x1c.remote_rf.GenericRPCRequest\x1a\x1d.remote_rf.GenericRPCResponseB\x1e\n\x10\x63om.example.demoB\nDemoProtosb\x06proto3')
28
28
 
29
29
  _globals = globals()
30
30
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -53,7 +53,7 @@ if not _descriptor._USE_C_DESCRIPTORS:
53
53
  _globals['_REALNUMPYARRAY']._serialized_start=514
54
54
  _globals['_REALNUMPYARRAY']._serialized_end=582
55
55
  _globals['_ARGUMENT']._serialized_start=585
56
- _globals['_ARGUMENT']._serialized_end=800
57
- _globals['_GENERICRPC']._serialized_start=802
58
- _globals['_GENERICRPC']._serialized_end=883
56
+ _globals['_ARGUMENT']._serialized_end=822
57
+ _globals['_GENERICRPC']._serialized_start=824
58
+ _globals['_GENERICRPC']._serialized_end=905
59
59
  # @@protoc_insertion_point(module_scope)
@@ -24,6 +24,7 @@ That's it. The server never needs to be modified.
24
24
  from __future__ import annotations
25
25
 
26
26
  import hashlib
27
+ import inspect
27
28
  import importlib.util
28
29
  import json
29
30
  import sys
@@ -259,8 +260,10 @@ class DeviceSchema:
259
260
  if ex.kind == "get":
260
261
  getters[name] = entry
261
262
  elif ex.kind == "set":
263
+ entry["args"] = self._args_metadata(ex, skip_self=True)
262
264
  setters[name] = entry
263
265
  elif ex.kind == "call":
266
+ entry["args"] = self._args_metadata(ex, skip_self=True)
264
267
  calls[name] = entry
265
268
 
266
269
  body = {
@@ -278,6 +281,98 @@ class DeviceSchema:
278
281
 
279
282
  return body
280
283
 
284
+ def _args_metadata(self, ex: "_Exposed", *, skip_self: bool) -> list[dict]:
285
+ sig = inspect.signature(ex.fn)
286
+ args = []
287
+ for pname, param in sig.parameters.items():
288
+ if skip_self and pname == "self":
289
+ continue
290
+ if param.kind is inspect.Parameter.POSITIONAL_ONLY:
291
+ raise TypeError(
292
+ f"{self.device_type}.{ex.name} cannot expose positional-only "
293
+ f"parameter {pname!r}"
294
+ )
295
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
296
+ raise TypeError(
297
+ f"{self.device_type}.{ex.name} cannot be exposed with "
298
+ f"{param.kind.description} parameter {pname!r}"
299
+ )
300
+
301
+ item = {
302
+ "name": pname,
303
+ "required": param.default is inspect.Parameter.empty,
304
+ "type": self._annotation_name(param.annotation),
305
+ "kind": (
306
+ "keyword_only"
307
+ if param.kind is inspect.Parameter.KEYWORD_ONLY
308
+ else "positional_or_keyword"
309
+ ),
310
+ }
311
+ if param.default is not inspect.Parameter.empty:
312
+ item["default"] = self._json_default(ex, pname, param.default)
313
+ args.append(item)
314
+ return args
315
+
316
+ @staticmethod
317
+ def _annotation_name(annotation: Any) -> str:
318
+ if annotation is inspect.Parameter.empty:
319
+ return "any"
320
+ if isinstance(annotation, str):
321
+ return annotation
322
+ name = getattr(annotation, "__name__", None)
323
+ if name:
324
+ return str(name)
325
+ return str(annotation).replace("typing.", "")
326
+
327
+ def _json_default(self, ex: "_Exposed", pname: str, value: Any) -> Any:
328
+ try:
329
+ json.dumps(value)
330
+ return value
331
+ except TypeError as exc:
332
+ raise TypeError(
333
+ f"{self.device_type}.{ex.name} default for {pname!r} is not JSON serializable"
334
+ ) from exc
335
+
336
+ def _bind_exposed_args(
337
+ self,
338
+ ex: "_Exposed",
339
+ args: dict,
340
+ *,
341
+ allow_value_alias: bool,
342
+ allow_first_arg: bool = False,
343
+ ) -> dict:
344
+ sig = inspect.signature(ex.fn)
345
+ bound = {}
346
+ params = [
347
+ (pname, param)
348
+ for pname, param in sig.parameters.items()
349
+ if pname != "self"
350
+ ]
351
+
352
+ for pname, param in params:
353
+ if param.kind is inspect.Parameter.POSITIONAL_ONLY:
354
+ raise TypeError(
355
+ f"{self.device_type}.{ex.name} cannot be dispatched with "
356
+ f"positional-only parameter {pname!r}"
357
+ )
358
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
359
+ raise TypeError(
360
+ f"{self.device_type}.{ex.name} cannot be dispatched with "
361
+ f"{param.kind.description} parameter {pname!r}"
362
+ )
363
+
364
+ if pname in args:
365
+ bound[pname] = args[pname]
366
+ elif allow_value_alias and len(params) == 1 and "value" in args:
367
+ bound[pname] = args["value"]
368
+ elif allow_first_arg and len(params) == 1 and args:
369
+ first_key = next(iter(args))
370
+ bound[pname] = args[first_key]
371
+ elif param.default is inspect.Parameter.empty:
372
+ raise TypeError(f"missing required argument {pname!r} for {ex.name}")
373
+
374
+ return bound
375
+
281
376
  # ── Dispatch (server RPC layer calls this) ────────────────────
282
377
 
283
378
  def dispatch(self, method_name: str, args: dict) -> Any:
@@ -303,32 +398,19 @@ class DeviceSchema:
303
398
  return ex.fn(self)
304
399
 
305
400
  elif ex.kind == "set":
306
- value = args.get("value")
307
- if value is None and "value" not in args:
308
- # Also check first positional-style arg key
309
- for k, v in args.items():
310
- value = v
311
- break
312
- return ex.fn(self, value)
401
+ call_args = self._bind_exposed_args(
402
+ ex,
403
+ args,
404
+ allow_value_alias=True,
405
+ allow_first_arg=True,
406
+ )
407
+ if not call_args:
408
+ raise TypeError(f"missing required setter value for {ex.name}")
409
+ return ex.fn(self, **call_args)
313
410
 
314
411
  elif ex.kind == "call":
315
- # Calls can have 0 or more args
316
- # Simple convention: if the function takes 'value', pass it
317
- import inspect
318
- sig = inspect.signature(ex.fn)
319
- params = [p for p in sig.parameters if p != "self"]
320
-
321
- if len(params) == 0:
322
- return ex.fn(self)
323
- elif len(params) == 1:
324
- # Single arg — pull from args dict by param name or "value"
325
- pname = params[0]
326
- val = args.get(pname, args.get("value"))
327
- return ex.fn(self, val)
328
- else:
329
- # Multi-arg — match by name
330
- call_args = {p: args.get(p) for p in params if p in args}
331
- return ex.fn(self, **call_args)
412
+ call_args = self._bind_exposed_args(ex, args, allow_value_alias=True)
413
+ return ex.fn(self, **call_args)
332
414
  else:
333
415
  raise ValueError(f"unknown kind '{ex.kind}' on {method_name}")
334
416
 
@@ -1,6 +1,61 @@
1
1
  from ..grpc import grpc_pb2, grpc_pb2_grpc
2
+ import json
2
3
  import numpy as np
3
4
 
5
+ _JSON_TYPE_KEY = "__remoterf_json_type__"
6
+
7
+ def _json_safe(value):
8
+ if isinstance(value, np.ndarray):
9
+ if value.dtype == object:
10
+ raise ValueError(f"Cannot JSON-map object-dtype array: {value!r}")
11
+ array = np.asarray(value)
12
+ if np.iscomplexobj(array):
13
+ data = [[float(num.real), float(num.imag)] for num in array.ravel()]
14
+ return {
15
+ _JSON_TYPE_KEY: "ndarray",
16
+ "dtype": str(array.dtype),
17
+ "shape": list(array.shape),
18
+ "complex": True,
19
+ "data": data,
20
+ }
21
+ return {
22
+ _JSON_TYPE_KEY: "ndarray",
23
+ "dtype": str(array.dtype),
24
+ "shape": list(array.shape),
25
+ "complex": False,
26
+ "data": array.tolist(),
27
+ }
28
+ if isinstance(value, (np.bool_,)):
29
+ return bool(value)
30
+ if isinstance(value, (np.integer,)):
31
+ return int(value)
32
+ if isinstance(value, (np.floating,)):
33
+ return float(value)
34
+ if isinstance(value, complex):
35
+ return {_JSON_TYPE_KEY: "complex", "real": float(value.real), "imag": float(value.imag)}
36
+ if isinstance(value, dict):
37
+ return {str(key): _json_safe(item) for key, item in value.items()}
38
+ if isinstance(value, (list, tuple)):
39
+ return [_json_safe(item) for item in value]
40
+ return value
41
+
42
+ def _json_restore(value):
43
+ if isinstance(value, dict):
44
+ kind = value.get(_JSON_TYPE_KEY)
45
+ if kind == "ndarray":
46
+ shape = tuple(value.get("shape", ()))
47
+ dtype = value.get("dtype")
48
+ if value.get("complex"):
49
+ data = [complex(real, imag) for real, imag in value.get("data", [])]
50
+ return np.array(data, dtype=dtype or np.complex64).reshape(shape)
51
+ return np.array(value.get("data", []), dtype=dtype).reshape(shape)
52
+ if kind == "complex":
53
+ return complex(value.get("real", 0.0), value.get("imag", 0.0))
54
+ return {key: _json_restore(item) for key, item in value.items()}
55
+ if isinstance(value, list):
56
+ return [_json_restore(item) for item in value]
57
+ return value
58
+
4
59
  def unmap_arg(arg):
5
60
  if arg.HasField('int64_value'):
6
61
  return arg.int64_value
@@ -17,6 +72,8 @@ def unmap_arg(arg):
17
72
  shape = tuple(arg.complex_array.shape.dim)
18
73
  data = [complex(c.real, c.imag) for c in arg.complex_array.data]
19
74
  return np.array(data, dtype=np.complex64).reshape(shape)
75
+ elif arg.HasField('json_value'):
76
+ return _json_restore(json.loads(arg.json_value))
20
77
  else:
21
78
  raise ValueError(f"Unknown argument type during unmapping: {arg}")
22
79
 
@@ -25,6 +82,8 @@ def map_arg(value):
25
82
 
26
83
  if isinstance(value, (bool, np.bool_)):
27
84
  arg.bool_value = bool(value)
85
+ elif value is None or isinstance(value, dict):
86
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
28
87
  elif isinstance(value, (int, np.integer)):
29
88
  arg.int64_value = int(value)
30
89
  elif isinstance(value, (float, np.floating)):
@@ -32,12 +91,17 @@ def map_arg(value):
32
91
  elif isinstance(value, str):
33
92
  arg.string_value = value
34
93
  elif isinstance(value, (list, tuple)):
35
- array = np.asarray(value)
94
+ try:
95
+ array = np.asarray(value)
96
+ except Exception:
97
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
98
+ return arg
36
99
  if array.dtype == object:
37
100
  try:
38
101
  array = np.stack(value)
39
- except Exception as exc:
40
- raise ValueError(f"Cannot map ragged/object array value: {value}") from exc
102
+ except Exception:
103
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
104
+ return arg
41
105
  return map_arg(array)
42
106
  elif isinstance(value, np.ndarray):
43
107
  if value.dtype == object:
@@ -18,7 +18,7 @@ class PlutoServer:
18
18
  elif function_type == "SET":
19
19
  PlutoServer.set(function_name=function_name, value=args[function_name], token=unmap_arg(args['a']))
20
20
 
21
- elif function_type in ("CALL0", "CALL1"):
21
+ elif function_type in ("CALL0", "CALL1", "CALLN"):
22
22
  return PlutoServer.call_func(
23
23
  function_name=function_name,
24
24
  function_type=function_type,
@@ -60,6 +60,14 @@ class PlutoServer:
60
60
  # print(f"unmapped values: {python_arg}")
61
61
  # print(f"{function_name} is called on {device}")
62
62
  result = method(python_arg)
63
+ elif function_type == "CALLN":
64
+ reserved = {"a", "g", "token", "device_id", "device_name"}
65
+ call_args = {
66
+ str(key): unmap_arg(value)
67
+ for key, value in dict(args).items()
68
+ if str(key) not in reserved
69
+ }
70
+ result = method(**call_args)
63
71
  else:
64
72
  raise ValueError(f"Unsupported call type: {function_type} (arg length not supported)")
65
73
 
@@ -102,4 +110,4 @@ class PlutoServer:
102
110
  return {}
103
111
  except Exception as e:
104
112
  # print(f"UE: Pluto_server ln:44: {e}")
105
- return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
113
+ return {function_name:map_arg('None'), "UE":map_arg(f'{e}')}
@@ -95,6 +95,7 @@ def _get_port(name: str, *, default: int | None = None) -> int:
95
95
  return n
96
96
 
97
97
  session = PromptSession()
98
+ BANNER_CREDIT = "Created at UCLA by E. Ge and I. Roberts."
98
99
 
99
100
  from dotenv import load_dotenv, find_dotenv
100
101
  dotenv_path = find_dotenv('.env.server')
@@ -247,6 +248,7 @@ start_time = datetime.now()
247
248
 
248
249
  def welcome():
249
250
  printf(f"Server running nominally.", Sty.BOLD)
251
+ printf(BANNER_CREDIT, Sty.GRAY)
250
252
  printf(f"Local IP: ", Sty.DEFAULT, f"{local_ip}", Sty.ITALIC, f" | Port: ", Sty.DEFAULT, f"{local_port}", Sty.ITALIC)
251
253
  # printf(f" -> Started at ", Sty.DEFAULT, f"{start_time}", Sty.DEFAULT)
252
254
  printf("Input ", Sty.DEFAULT, "'help'", Sty.BOLD," for help menu.", Sty.DEFAULT)
@@ -1020,4 +1022,4 @@ welcome()
1020
1022
 
1021
1023
  def main():
1022
1024
  while server_input(session.prompt(stylize('server@remoterf: ', Sty.BOLD))):
1023
- pass
1025
+ pass
@@ -180,6 +180,16 @@ class RpcManager:
180
180
  result = dev.dispatch(f"call_{prop}", {"value": value})
181
181
  return {prop: map_arg(result)} if result is not None else {prop: map_arg('None')}
182
182
 
183
+ if verb == "CALLN":
184
+ reserved = {"a", "g", "token", "device_id", "device_name"}
185
+ call_args = {
186
+ str(key): unmap_arg(value)
187
+ for key, value in dict(args).items()
188
+ if str(key) not in reserved
189
+ }
190
+ result = dev.dispatch(f"call_{prop}", call_args)
191
+ return {prop: map_arg(result)} if result is not None else {prop: map_arg('None')}
192
+
183
193
  raise ValueError(f"Unknown verb {verb!r} in {function_name!r}")
184
194
 
185
195
  def _resolve_idl_target(self, args: dict) -> tuple[int | None, str | None]:
@@ -161,6 +161,7 @@ def _tos_ack_path() -> Path:
161
161
 
162
162
  _TOS_VERSION = "2026"
163
163
  _TOS_URL = "https://remoterf.net/tos"
164
+ BANNER_CREDIT = "Created at UCLA by E. Ge and I. Roberts."
164
165
 
165
166
 
166
167
  def _tos_acknowledged() -> bool:
@@ -224,6 +225,7 @@ def _ensure_tos_acknowledged() -> bool:
224
225
  def print_help() -> None:
225
226
  print(
226
227
  "RemoteRF Server CLI Help\n"
228
+ f"{BANNER_CREDIT}\n"
227
229
  "\n"
228
230
  "Usage:\n"
229
231
  " serverrf -h | --help\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: RemoteRF server-side control package
5
5
  Requires-Python: >=3.8
6
6
  Description-Content-Type: text/markdown
@@ -47,4 +47,7 @@ src/remoteRF_server/server/user_group_handler.py
47
47
  src/remoteRF_server/tools/__init__.py
48
48
  src/remoteRF_server/tools/gen_certs.py
49
49
  src/remoteRF_server/tools/gist_status.py
50
- src/remoteRF_server/tools/gist_status_testing.py
50
+ src/remoteRF_server/tools/gist_status_testing.py
51
+ tests/test_idl_schema.py
52
+ tests/test_process_arg.py
53
+ tests/test_rpc_manager_calln.py
@@ -0,0 +1,81 @@
1
+ import unittest
2
+
3
+ from remoteRF_server.common.idl.schema import DeviceSchema, idl_expose
4
+
5
+
6
+ class FakeSchema(DeviceSchema):
7
+ device_type = "fake"
8
+ driver_version = "0.0.0"
9
+
10
+ @idl_expose(kind="set")
11
+ def set_level(self, value: int):
12
+ self.device["level"] = value
13
+
14
+ @idl_expose(kind="call")
15
+ def call_ping(self):
16
+ return "pong"
17
+
18
+ @idl_expose(kind="call")
19
+ def call_echo(self, value):
20
+ return value
21
+
22
+ @idl_expose(kind="call")
23
+ def call_configure(self, freq: float, gain: int = 0, options: dict = None):
24
+ return {"freq": freq, "gain": gain, "options": options}
25
+
26
+ @idl_expose(kind="call")
27
+ def call_keyword(self, first: int = 1, *, required: str):
28
+ return {"first": first, "required": required}
29
+
30
+
31
+ class DeviceSchemaTests(unittest.TestCase):
32
+ def test_schema_includes_arg_metadata(self):
33
+ schema = FakeSchema()
34
+ idl = schema.get_idl()
35
+
36
+ args = idl["calls"]["call_configure"]["args"]
37
+ self.assertEqual([arg["name"] for arg in args], ["freq", "gain", "options"])
38
+ self.assertEqual([arg["required"] for arg in args], [True, False, False])
39
+ self.assertEqual([arg["type"] for arg in args], ["float", "int", "dict"])
40
+ self.assertEqual(args[1]["default"], 0)
41
+ self.assertIsNone(args[2]["default"])
42
+ self.assertEqual(idl["setters"]["set_level"]["args"][0]["name"], "value")
43
+
44
+ keyword_args = idl["calls"]["call_keyword"]["args"]
45
+ self.assertEqual(
46
+ [(arg["name"], arg["kind"], arg["required"]) for arg in keyword_args],
47
+ [
48
+ ("first", "positional_or_keyword", False),
49
+ ("required", "keyword_only", True),
50
+ ],
51
+ )
52
+
53
+ def test_dispatch_supports_defaults_dicts_and_legacy_value_alias(self):
54
+ schema = FakeSchema()
55
+ schema.bind({})
56
+
57
+ self.assertEqual(schema.dispatch("call_ping", {}), "pong")
58
+ self.assertEqual(schema.dispatch("call_echo", {"value": "hello"}), "hello")
59
+ self.assertEqual(
60
+ schema.dispatch(
61
+ "call_configure",
62
+ {"freq": 2.4e9, "options": {"mode": "fast"}},
63
+ ),
64
+ {"freq": 2.4e9, "gain": 0, "options": {"mode": "fast"}},
65
+ )
66
+
67
+ schema.dispatch("set_level", {"value": 7})
68
+ self.assertEqual(schema.device["level"], 7)
69
+ schema.dispatch("set_level", {"level": 8})
70
+ self.assertEqual(schema.device["level"], 8)
71
+
72
+ def test_dispatch_missing_required_arg_raises(self):
73
+ schema = FakeSchema()
74
+ schema.bind({})
75
+
76
+ with self.assertRaisesRegex(TypeError, "missing required argument 'freq'"):
77
+ schema.dispatch("call_configure", {"gain": 3})
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main()
@@ -0,0 +1,44 @@
1
+ import unittest
2
+
3
+ import numpy as np
4
+
5
+ from remoteRF_server.common.utils.process_arg import map_arg, unmap_arg
6
+
7
+
8
+ class ProcessArgTests(unittest.TestCase):
9
+ def test_json_values_round_trip_dict_and_none(self):
10
+ value = {"nested": {"x": 1}, "flag": True, "nothing": None}
11
+
12
+ self.assertEqual(unmap_arg(map_arg(value)), value)
13
+ self.assertIsNone(unmap_arg(map_arg(None)))
14
+
15
+ def test_existing_scalar_values_still_round_trip(self):
16
+ self.assertIs(unmap_arg(map_arg(True)), True)
17
+ self.assertEqual(unmap_arg(map_arg(12)), 12)
18
+ self.assertAlmostEqual(unmap_arg(map_arg(1.5)), 1.5)
19
+ self.assertEqual(unmap_arg(map_arg("abc")), "abc")
20
+
21
+ def test_arrays_and_list_values_still_round_trip_as_arrays(self):
22
+ real = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
23
+ complex_value = np.array([1 + 2j, 3 + 4j], dtype=np.complex64)
24
+
25
+ np.testing.assert_allclose(unmap_arg(map_arg(real)), real)
26
+ np.testing.assert_allclose(unmap_arg(map_arg([1, 2, 3])), np.array([1, 2, 3]))
27
+ np.testing.assert_allclose(unmap_arg(map_arg(complex_value)), complex_value)
28
+
29
+ def test_json_values_can_contain_arrays_and_mixed_return_tuples(self):
30
+ samples = np.array([[1 + 2j, 3 + 4j]], dtype=np.complex64)
31
+ value = (2, samples, {"error_code": "none"})
32
+
33
+ count, restored_samples, metadata = unmap_arg(map_arg(value))
34
+
35
+ self.assertEqual(count, 2)
36
+ np.testing.assert_allclose(restored_samples, samples)
37
+ self.assertEqual(metadata, {"error_code": "none"})
38
+
39
+ nested = unmap_arg(map_arg({"samples": samples}))
40
+ np.testing.assert_allclose(nested["samples"], samples)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ unittest.main()
@@ -0,0 +1,71 @@
1
+ import unittest
2
+ import sys
3
+ import types
4
+
5
+ sys.modules.setdefault("adi", types.ModuleType("adi"))
6
+
7
+ from remoteRF_server.common.utils.process_arg import map_arg, unmap_arg
8
+ from remoteRF_server.server.rpc_manager import RpcManager
9
+
10
+
11
+ class FakeDevice:
12
+ def __init__(self, result=None):
13
+ self.result = result
14
+ self.seen = None
15
+
16
+ def dispatch(self, method_name, args):
17
+ self.seen = (method_name, args)
18
+ return self.result if self.result is not None else {"method": method_name, "args": args}
19
+
20
+
21
+ class RpcManagerCallNTests(unittest.TestCase):
22
+ def test_calln_unmaps_named_args_and_filters_transport_keys(self):
23
+ dev = FakeDevice()
24
+ result = RpcManager()._dispatch_local_schema(
25
+ function_name="Fake:configure:CALLN",
26
+ args={
27
+ "a": map_arg("token"),
28
+ "g": map_arg(123),
29
+ "device_id": map_arg(123),
30
+ "freq": map_arg(2.4e9),
31
+ "options": map_arg({"mode": "fast"}),
32
+ },
33
+ dev=dev,
34
+ )
35
+
36
+ self.assertEqual(
37
+ dev.seen,
38
+ ("call_configure", {"freq": 2.4e9, "options": {"mode": "fast"}}),
39
+ )
40
+ self.assertEqual(
41
+ unmap_arg(result["configure"]),
42
+ {"method": "call_configure", "args": {"freq": 2.4e9, "options": {"mode": "fast"}}},
43
+ )
44
+
45
+ def test_existing_call0_and_call1_paths_still_work(self):
46
+ dev = FakeDevice(result="ok")
47
+ manager = RpcManager()
48
+
49
+ self.assertEqual(
50
+ unmap_arg(manager._dispatch_local_schema(
51
+ function_name="Fake:ping:CALL0",
52
+ args={"a": map_arg("token")},
53
+ dev=dev,
54
+ )["ping"]),
55
+ "ok",
56
+ )
57
+ self.assertEqual(dev.seen, ("call_ping", {}))
58
+
59
+ self.assertEqual(
60
+ unmap_arg(manager._dispatch_local_schema(
61
+ function_name="Fake:echo:CALL1",
62
+ args={"a": map_arg("token"), "arg1": map_arg(42)},
63
+ dev=dev,
64
+ )["echo"]),
65
+ "ok",
66
+ )
67
+ self.assertEqual(dev.seen, ("call_echo", {"value": 42}))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()