remoteRF-server 0.2.4__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 (57) hide show
  1. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/PKG-INFO +1 -1
  2. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/pyproject.toml +1 -1
  3. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_pb2.py +7 -7
  4. remoterf_server-1.0.1/src/remoteRF_server/common/idl/pluto_mimo_schema.py +6 -0
  5. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/pluto_schema.py +66 -11
  6. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/schema.py +114 -26
  7. remoterf_server-1.0.1/src/remoteRF_server/common/utils/process_arg.py +153 -0
  8. remoterf_server-1.0.1/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +181 -0
  9. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +10 -2
  10. remoterf_server-1.0.1/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +229 -0
  11. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_tunnel_server.py +45 -1
  12. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/device_manager.py +266 -82
  13. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/grpc_server.py +4 -2
  14. remoterf_server-1.0.1/src/remoteRF_server/server/rpc_manager.py +298 -0
  15. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/user_group_cli.py +3 -3
  16. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/user_group_handler.py +13 -12
  17. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/serverrf_cli.py +161 -26
  18. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gist_status.py +14 -22
  19. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/PKG-INFO +1 -1
  20. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/SOURCES.txt +7 -1
  21. remoterf_server-1.0.1/tests/test_idl_schema.py +81 -0
  22. remoterf_server-1.0.1/tests/test_process_arg.py +44 -0
  23. remoterf_server-1.0.1/tests/test_rpc_manager_calln.py +71 -0
  24. remoterf_server-0.2.4/src/remoteRF_server/common/utils/process_arg.py +0 -80
  25. remoterf_server-0.2.4/src/remoteRF_server/server/rpc_manager.py +0 -104
  26. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/README.md +0 -0
  27. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/setup.cfg +0 -0
  28. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/__init__.py +0 -0
  29. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/__init__.py +0 -0
  30. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  31. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  32. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  33. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  34. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/__init__.py +0 -0
  35. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  36. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/__init__.py +0 -0
  37. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  38. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/api_token.py +0 -0
  39. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  40. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/db_location.py +0 -0
  41. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/common/utils/list_string.py +0 -0
  42. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/__init__.py +0 -0
  43. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  44. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/host/__init__.py +0 -0
  45. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_auth_token.py +0 -0
  46. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/host/host_directory_store.py +0 -0
  47. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/__init__.py +0 -0
  48. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/acc_perms.py +0 -0
  49. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/cert_provider.py +0 -0
  50. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/server/reservation.py +0 -0
  51. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/tools/__init__.py +0 -0
  52. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gen_certs.py +0 -0
  53. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  54. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  55. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  56. {remoterf_server-0.2.4 → remoterf_server-1.0.1}/src/remoteRF_server.egg-info/requires.txt +0 -0
  57. {remoterf_server-0.2.4 → 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: 0.2.4
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 = "0.2.4"
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)
@@ -0,0 +1,6 @@
1
+ # Built-in import shim for the packaged ADALM Pluto MIMO schema.
2
+
3
+ from remoteRF_server.drivers.adalm_pluto.pluto_mimo_schema import ( # noqa: F401
4
+ PlutoMIMOSchema,
5
+ connect_pluto_mimo,
6
+ )
@@ -1,15 +1,14 @@
1
- # ~/.config/remoterf/drivers/pluto_schema.py
2
- #
3
- # Admin-written schema for ADALM-Pluto devices.
4
- # Drop this file into the drivers folder, restart the server. Done.
1
+ # SAMPLE schema for ADI Pluto SDR.
2
+ # Place into -> ~/.config/remoterf/drivers and restart server to utilize!
5
3
 
6
- from . import *
4
+
5
+ from remoteRF_server.common.idl import DeviceSchema, idl_register, idl_expose
7
6
 
8
7
  import subprocess
9
8
  import re
10
9
  import adi
11
10
 
12
- # ========================= PLUTO Connection Logic =========================
11
+ # == PLUTO Connection Logic ==
13
12
 
14
13
  def connect_pluto(*, serial: str):
15
14
  serial = (serial or "").strip()
@@ -46,10 +45,11 @@ def connect_pluto(*, serial: str):
46
45
 
47
46
  # ========================= PLUTO Schema =========================
48
47
 
49
- @idl_register("pluto")
48
+ @idl_register("adalm_pluto")
50
49
  class PlutoSchema(DeviceSchema):
51
50
 
52
- device_type = "pluto"
51
+ device_type = "adalm_pluto"
52
+ client_class = "Pluto"
53
53
  driver_version = "0.0.1"
54
54
 
55
55
  @staticmethod
@@ -85,11 +85,13 @@ class PlutoSchema(DeviceSchema):
85
85
 
86
86
  @idl_expose(kind="get")
87
87
  def get_rx_hardwaregain_chan0(self):
88
- return self.device.rx_hardwaregain_chan0
88
+ """RX hardware gain (dB). pyadi-iio attribute: hardwaregain_chan0"""
89
+ return self.device.hardwaregain_chan0
89
90
 
90
91
  @idl_expose(kind="set")
91
92
  def set_rx_hardwaregain_chan0(self, value):
92
- self.device.rx_hardwaregain_chan0 = value
93
+ """RX hardware gain (dB). pyadi-iio attribute: hardwaregain_chan0"""
94
+ self.device.hardwaregain_chan0 = value
93
95
 
94
96
  @idl_expose(kind="get")
95
97
  def get_tx_hardwaregain_chan0(self):
@@ -171,4 +173,57 @@ class PlutoSchema(DeviceSchema):
171
173
 
172
174
  @idl_expose(kind="call")
173
175
  def call_tx_destroy_buffer(self):
174
- self.device.tx_destroy_buffer()
176
+ self.device.tx_destroy_buffer()
177
+
178
+ @idl_expose(kind="call")
179
+ def call_disable_dds(self):
180
+ """Disable the internal DDS tone generator."""
181
+ self.device.disable_dds()
182
+
183
+ # endregion
184
+
185
+ # region ip / repr
186
+
187
+ @idl_expose(kind="call")
188
+ def call_ip(self):
189
+ """Return a connection identifier for this device (ping / verify)."""
190
+ try:
191
+ return str(self.device.uri)
192
+ except AttributeError:
193
+ return str(self.device._uri_auto)
194
+
195
+ @idl_expose(kind="get")
196
+ def get_repr(self):
197
+ """Return repr() of the underlying device object."""
198
+ return repr(self.device)
199
+
200
+ # endregion
201
+
202
+ # region _dec_int_fpga_filter
203
+
204
+ @idl_expose(kind="get")
205
+ def get_rx_dec8_filter_en(self):
206
+ """rx_dec8_filter_en: Enable decimate-by-8 filter in FPGA"""
207
+ return bool(self.device.rx_dec8_filter_en)
208
+
209
+ @idl_expose(kind="set")
210
+ def set_rx_dec8_filter_en(self, value):
211
+ """rx_dec8_filter_en: Enable decimate-by-8 filter in FPGA"""
212
+ self.device.rx_dec8_filter_en = value
213
+
214
+ @idl_expose(kind="get")
215
+ def get_tx_int8_filter_en(self):
216
+ """tx_int8_filter_en: Enable interpolate-by-8 filter in FPGA"""
217
+ return bool(self.device.tx_int8_filter_en)
218
+
219
+ @idl_expose(kind="set")
220
+ def set_tx_int8_filter_en(self, value):
221
+ """tx_int8_filter_en: Enable interpolate-by-8 filter in FPGA"""
222
+ self.device.tx_int8_filter_en = value
223
+
224
+ @idl_expose(kind="get")
225
+ def get_rates(self):
226
+ """Get decimation/interpolation rates from the FPGA filter block."""
227
+ return self.device._get_rates(self.device._rxadc, False)
228
+
229
+ # endregion
@@ -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,11 +260,13 @@ 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
- return {
269
+ body = {
267
270
  "schema_version": "1.0",
268
271
  "device_type": self.device_type,
269
272
  "driver_version": self.driver_version,
@@ -272,6 +275,104 @@ class DeviceSchema:
272
275
  "calls": calls,
273
276
  }
274
277
 
278
+ client_class = getattr(self, "client_class", "")
279
+ if client_class:
280
+ body["client_class"] = str(client_class)
281
+
282
+ return body
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
+
275
376
  # ── Dispatch (server RPC layer calls this) ────────────────────
276
377
 
277
378
  def dispatch(self, method_name: str, args: dict) -> Any:
@@ -297,32 +398,19 @@ class DeviceSchema:
297
398
  return ex.fn(self)
298
399
 
299
400
  elif ex.kind == "set":
300
- value = args.get("value")
301
- if value is None and "value" not in args:
302
- # Also check first positional-style arg key
303
- for k, v in args.items():
304
- value = v
305
- break
306
- 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)
307
410
 
308
411
  elif ex.kind == "call":
309
- # Calls can have 0 or more args
310
- # Simple convention: if the function takes 'value', pass it
311
- import inspect
312
- sig = inspect.signature(ex.fn)
313
- params = [p for p in sig.parameters if p != "self"]
314
-
315
- if len(params) == 0:
316
- return ex.fn(self)
317
- elif len(params) == 1:
318
- # Single arg — pull from args dict by param name or "value"
319
- pname = params[0]
320
- val = args.get(pname, args.get("value"))
321
- return ex.fn(self, val)
322
- else:
323
- # Multi-arg — match by name
324
- call_args = {p: args.get(p) for p in params if p in args}
325
- 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)
326
414
  else:
327
415
  raise ValueError(f"unknown kind '{ex.kind}' on {method_name}")
328
416
 
@@ -355,4 +443,4 @@ class _Exposed:
355
443
  self.name = name
356
444
  self.fn = fn
357
445
  self.kind = kind
358
- self.doc = doc
446
+ self.doc = doc
@@ -0,0 +1,153 @@
1
+ from ..grpc import grpc_pb2, grpc_pb2_grpc
2
+ import json
3
+ import numpy as np
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
+
59
+ def unmap_arg(arg):
60
+ if arg.HasField('int64_value'):
61
+ return arg.int64_value
62
+ elif arg.HasField('float_value'):
63
+ return arg.float_value
64
+ elif arg.HasField('string_value'):
65
+ return arg.string_value
66
+ elif arg.HasField('bool_value'):
67
+ return arg.bool_value
68
+ elif arg.HasField('real_array'):
69
+ shape = tuple(arg.real_array.shape.dim)
70
+ return np.array(arg.real_array.data, dtype=np.float64).reshape(shape)
71
+ elif arg.HasField('complex_array'):
72
+ shape = tuple(arg.complex_array.shape.dim)
73
+ data = [complex(c.real, c.imag) for c in arg.complex_array.data]
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))
77
+ else:
78
+ raise ValueError(f"Unknown argument type during unmapping: {arg}")
79
+
80
+ def map_arg(value):
81
+ arg = grpc_pb2.Argument()
82
+
83
+ if isinstance(value, (bool, np.bool_)):
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=(",", ":"))
87
+ elif isinstance(value, (int, np.integer)):
88
+ arg.int64_value = int(value)
89
+ elif isinstance(value, (float, np.floating)):
90
+ arg.float_value = float(value)
91
+ elif isinstance(value, str):
92
+ arg.string_value = value
93
+ elif isinstance(value, (list, tuple)):
94
+ try:
95
+ array = np.asarray(value)
96
+ except Exception:
97
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
98
+ return arg
99
+ if array.dtype == object:
100
+ try:
101
+ array = np.stack(value)
102
+ except Exception:
103
+ arg.json_value = json.dumps(_json_safe(value), separators=(",", ":"))
104
+ return arg
105
+ return map_arg(array)
106
+ elif isinstance(value, np.ndarray):
107
+ if value.dtype == object:
108
+ raise ValueError(f"Cannot map object-dtype array: {value!r}")
109
+ if np.iscomplexobj(value):
110
+ complex_array = arg.complex_array
111
+ complex_array.shape.dim.extend(value.shape)
112
+ for num in np.asarray(value, dtype=np.complex64).ravel():
113
+ complex_num = complex_array.data.add()
114
+ complex_num.real = float(num.real)
115
+ complex_num.imag = float(num.imag)
116
+ else:
117
+ float_array = arg.real_array
118
+ float_array.shape.dim.extend(value.shape)
119
+ float_array.data.extend(np.asarray(value, dtype=np.float32).ravel())
120
+ else:
121
+ raise ValueError(f"Unknown argument type during mapping: {value}")
122
+ return arg
123
+
124
+ def map_array_proto(np_array):
125
+ arg = grpc_pb2.Argument()
126
+
127
+ # Check if the array is complex
128
+ if np.iscomplexobj(np_array):
129
+ complex_array = grpc_pb2.ComplexArray()
130
+ for num in np_array.flat:
131
+ complex_number = complex_array.data.add()
132
+ complex_number.real = num.real
133
+ complex_number.imag = num.imag
134
+ arg.complex_array.CopyFrom(complex_array)
135
+ else:
136
+ # Handle as a regular float array
137
+ float_array = grpc_pb2.FloatArray()
138
+ float_array.data.extend(np_array.flat)
139
+ arg.float_array.CopyFrom(float_array)
140
+
141
+ return arg
142
+
143
+ def unmap_array_proto(arg):
144
+ # Check which type of array is available and convert appropriately
145
+ if arg.HasField('complex_array'):
146
+ # Convert ComplexArray to a numpy array of complex numbers
147
+ data = [complex(cn.real, cn.imag) for cn in arg.complex_array.data]
148
+ return np.array(data, dtype=np.complex64)
149
+ elif arg.HasField('float_array'):
150
+ # Convert FloatArray to a numpy array of floats
151
+ return np.array(arg.float_array.data, dtype=np.float32)
152
+ else:
153
+ raise ValueError("Argument does not contain a recognizable array.")