remoteRF-server 0.1.136__tar.gz → 0.2.0__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 (49) hide show
  1. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/PKG-INFO +2 -1
  2. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/pyproject.toml +3 -2
  3. remoterf_server-0.2.0/src/remoteRF_server/common/idl/__init__.py +1 -0
  4. remoterf_server-0.2.0/src/remoteRF_server/common/idl/device_schema.py +39 -0
  5. remoterf_server-0.2.0/src/remoteRF_server/common/idl/pluto_schema.py +174 -0
  6. remoterf_server-0.2.0/src/remoteRF_server/common/idl/schema.py +356 -0
  7. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/__init__.py +1 -1
  8. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/db_location.py +3 -0
  9. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/device_manager.py +59 -76
  10. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/reservation.py +0 -13
  11. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gist_status.py +1 -1
  12. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/PKG-INFO +2 -1
  13. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/SOURCES.txt +4 -0
  14. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/requires.txt +1 -0
  15. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/README.md +0 -0
  16. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/setup.cfg +0 -0
  17. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/__init__.py +0 -0
  18. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/__init__.py +0 -0
  19. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  20. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  21. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  22. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  23. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  24. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  25. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/api_token.py +0 -0
  26. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  27. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/list_string.py +0 -0
  28. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  29. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/__init__.py +0 -0
  30. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  31. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  32. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/host/__init__.py +0 -0
  33. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_auth_token.py +0 -0
  34. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_directory_store.py +0 -0
  35. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  36. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/__init__.py +0 -0
  37. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/acc_perms.py +0 -0
  38. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/cert_provider.py +0 -0
  39. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/grpc_server.py +0 -0
  40. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/rpc_manager.py +0 -0
  41. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_cli.py +0 -0
  42. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_handler.py +0 -0
  43. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/serverrf_cli.py +0 -0
  44. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/tools/__init__.py +0 -0
  45. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gen_certs.py +0 -0
  46. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  47. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  48. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  49. {remoterf_server-0.1.136 → remoterf_server-0.2.0}/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.1.136
3
+ Version: 0.2.0
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -9,6 +9,7 @@ Requires-Dist: prompt_toolkit
9
9
  Requires-Dist: python-dotenv
10
10
  Requires-Dist: grpcio-tools
11
11
  Requires-Dist: requests
12
+ Requires-Dist: pyyaml
12
13
 
13
14
  # Remote RF Server Guide (Linux)
14
15
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "0.1.136"
7
+ version = "0.2.0"
8
8
  dependencies = [
9
9
  "grpcio",
10
10
  "protobuf",
@@ -12,7 +12,8 @@ dependencies = [
12
12
  "prompt_toolkit",
13
13
  "python-dotenv",
14
14
  "grpcio-tools",
15
- "requests"
15
+ "requests",
16
+ "pyyaml"
16
17
  ]
17
18
  readme = "README.md"
18
19
 
@@ -0,0 +1 @@
1
+ from .schema import DeviceSchema, idl_register, idl_expose
@@ -0,0 +1,39 @@
1
+ # remoteRF_server/common/idl/schema_template.py
2
+
3
+ # Include any necessary device imports, etc!
4
+
5
+ # todo: 'throws error' if stuff isn't implemented, etc.
6
+ # todo: have device_manifest live in .config, etc. (not in codebase)
7
+
8
+ from . import *
9
+
10
+ # todo: would import the decorators from remoteRF_server ... -> which allows any admin script to run with the platform!
11
+ # todo: you would run a 'baking the driver' (so it populates onto the .config or something with the proper version, etc. so the server knows!)
12
+
13
+ class DeviceSchema:
14
+
15
+ device_type = "template" # MUST match device_type in the device manifest!
16
+ driver_version = "0.0.1"
17
+
18
+ # Returns instance of the device (ie: a class, etc!)
19
+ # Used by the server to actually 'connect' to the hardware devices!
20
+ # Define the proper args in the device Manifest -> pipes to this factory method to populate device list!
21
+ # todo: kwargs are EXACTLY the 'init' section of the device manifest!
22
+ @staticmethod
23
+ def make_device(**kwargs):
24
+ # Implement whatever logic you need to connect to the device (or init parameters, like BW, etc.)
25
+ raise NotImplementedError
26
+
27
+ # Called by the server on init!
28
+ def __init__(self, device_instance):
29
+ self.device = device_instance
30
+
31
+ @idl_expose # Decorators for each 'exposed' method/property!
32
+ def schema_info(self) -> float:
33
+ return f"{self.device_type} v{self.driver_version}"
34
+
35
+ # Example exposed method:
36
+ @idl_expose # Decorators for each 'exposed' method/property!
37
+ def some_method(self, arg1: float, arg2: float) -> float:
38
+ return self.device.some_method(arg1, arg2) # EXAMPLE
39
+ # raise NotImplementedError
@@ -0,0 +1,174 @@
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.
5
+
6
+ from . import *
7
+
8
+ import subprocess
9
+ import re
10
+ import adi
11
+
12
+ # ========================= PLUTO Connection Logic =========================
13
+
14
+ def connect_pluto(*, serial: str):
15
+ serial = (serial or "").strip()
16
+ if not serial:
17
+ print("A Pluto serial must be provided")
18
+ return None
19
+
20
+ try:
21
+ out = subprocess.check_output(
22
+ ["iio_info", "-s"],
23
+ text=True,
24
+ stderr=subprocess.STDOUT
25
+ )
26
+
27
+ usb = None
28
+ for line in out.splitlines():
29
+ if (f"serial={serial}" in line) or (f"hw_serial={serial}" in line):
30
+ m = re.search(r"\[usb:([^\]]+)\]", line)
31
+ if m:
32
+ usb = m.group(1).strip()
33
+ break
34
+
35
+ if not usb:
36
+ print(f"No device found with serial {serial}")
37
+ return None
38
+
39
+ dev = adi.Pluto(f"usb:{usb}")
40
+ print(f"Connected to Pluto serial={serial} via usb:{usb}")
41
+ return dev
42
+
43
+ except Exception as e:
44
+ print(f"Failed to connect to Pluto serial={serial}: {e}")
45
+ return None
46
+
47
+ # ========================= PLUTO Schema =========================
48
+
49
+ @idl_register("pluto")
50
+ class PlutoSchema(DeviceSchema):
51
+
52
+ device_type = "pluto"
53
+ driver_version = "0.0.1"
54
+
55
+ @staticmethod
56
+ def make_device(**kwargs):
57
+ serial = kwargs.get("serial")
58
+ return connect_pluto(serial=serial)
59
+
60
+ # region ad9364
61
+
62
+ @idl_expose(kind="get")
63
+ def get_filter(self):
64
+ return self.device.filter
65
+
66
+ @idl_expose(kind="set")
67
+ def set_filter(self, value):
68
+ self.device.filter = value
69
+
70
+ @idl_expose(kind="get")
71
+ def get_loopback(self):
72
+ return self.device.loopback
73
+
74
+ @idl_expose(kind="set")
75
+ def set_loopback(self, value):
76
+ self.device.loopback = value
77
+
78
+ @idl_expose(kind="get")
79
+ def get_gain_control_mode_chan0(self):
80
+ return self.device.gain_control_mode_chan0
81
+
82
+ @idl_expose(kind="set")
83
+ def set_gain_control_mode_chan0(self, value):
84
+ self.device.gain_control_mode_chan0 = value
85
+
86
+ @idl_expose(kind="get")
87
+ def get_rx_hardwaregain_chan0(self):
88
+ return self.device.rx_hardwaregain_chan0
89
+
90
+ @idl_expose(kind="set")
91
+ def set_rx_hardwaregain_chan0(self, value):
92
+ self.device.rx_hardwaregain_chan0 = value
93
+
94
+ @idl_expose(kind="get")
95
+ def get_tx_hardwaregain_chan0(self):
96
+ return self.device.tx_hardwaregain_chan0
97
+
98
+ @idl_expose(kind="set")
99
+ def set_tx_hardwaregain_chan0(self, value):
100
+ self.device.tx_hardwaregain_chan0 = value
101
+
102
+ @idl_expose(kind="get")
103
+ def get_rx_rf_bandwidth(self):
104
+ return self.device.rx_rf_bandwidth
105
+
106
+ @idl_expose(kind="set")
107
+ def set_rx_rf_bandwidth(self, value):
108
+ self.device.rx_rf_bandwidth = value
109
+
110
+ @idl_expose(kind="get")
111
+ def get_tx_rf_bandwidth(self):
112
+ return self.device.tx_rf_bandwidth
113
+
114
+ @idl_expose(kind="set")
115
+ def set_tx_rf_bandwidth(self, value):
116
+ self.device.tx_rf_bandwidth = value
117
+
118
+ @idl_expose(kind="get")
119
+ def get_sample_rate(self):
120
+ return self.device.sample_rate
121
+
122
+ @idl_expose(kind="set")
123
+ def set_sample_rate(self, value):
124
+ self.device.sample_rate = value
125
+
126
+ @idl_expose(kind="get")
127
+ def get_rx_lo(self):
128
+ return self.device.rx_lo
129
+
130
+ @idl_expose(kind="set")
131
+ def set_rx_lo(self, value):
132
+ self.device.rx_lo = value
133
+
134
+ @idl_expose(kind="get")
135
+ def get_tx_lo(self):
136
+ return self.device.tx_lo
137
+
138
+ @idl_expose(kind="set")
139
+ def set_tx_lo(self, value):
140
+ self.device.tx_lo = value
141
+
142
+ @idl_expose(kind="get")
143
+ def get_tx_cyclic_buffer(self):
144
+ return self.device.tx_cyclic_buffer
145
+
146
+ @idl_expose(kind="set")
147
+ def set_tx_cyclic_buffer(self, value):
148
+ self.device.tx_cyclic_buffer = value
149
+
150
+ # region RX/TX
151
+
152
+ @idl_expose(kind="call")
153
+ def call_rx(self):
154
+ return self.device.rx()
155
+
156
+ @idl_expose(kind="get")
157
+ def get_rx_buffer_size(self):
158
+ return self.device.rx_buffer_size
159
+
160
+ @idl_expose(kind="set")
161
+ def set_rx_buffer_size(self, value):
162
+ self.device.rx_buffer_size = value
163
+
164
+ @idl_expose(kind="call")
165
+ def call_rx_destroy_buffer(self):
166
+ self.device.rx_destroy_buffer()
167
+
168
+ @idl_expose(kind="call")
169
+ def call_tx(self, value):
170
+ self.device.tx(value)
171
+
172
+ @idl_expose(kind="call")
173
+ def call_tx_destroy_buffer(self):
174
+ self.device.tx_destroy_buffer()
@@ -0,0 +1,356 @@
1
+ # src/remoteRF_server/common/idl/schema.py
2
+ """
3
+ IDL Schema framework.
4
+
5
+ Admin writes one .py per device TYPE, drops it in ~/.config/remoterf/drivers/,
6
+ and the server auto-discovers it on startup via load_drivers().
7
+
8
+ ~/.config/remoterf/
9
+ devices.env ← "I have 3 plutos at these serials"
10
+ drivers/
11
+ pluto_schema.py ← "here's what a pluto IS"
12
+ hackrf_schema.py
13
+ webcam_schema.py
14
+
15
+ The admin's schema file:
16
+ 1. Imports from this module
17
+ 2. Subclasses DeviceSchema
18
+ 3. Decorates with @register_driver("pluto")
19
+ 4. Implements make_device(**kwargs) — factory from manifest config
20
+ 5. Decorates exposed methods with @idl_expose(kind="get"|"set"|"call")
21
+
22
+ That's it. The server never needs to be modified.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import hashlib
27
+ import importlib.util
28
+ import json
29
+ import sys
30
+ from pathlib import Path
31
+ from typing import Any, Dict, Optional
32
+
33
+ from ..utils import *
34
+
35
+ # ───────────────────────────────────────────────────────────────────
36
+ # @idl_expose decorator
37
+ # ───────────────────────────────────────────────────────────────────
38
+
39
+ _EXPOSE_KEY = "__idl__"
40
+
41
+ def idl_expose(_fn=None, *, kind: str = "call", doc: str = ""):
42
+ """
43
+ Mark a method as exposed over IDL.
44
+
45
+ @idl_expose(kind="get")
46
+ def get_tx_lo(self):
47
+ return self.device.tx_lo
48
+
49
+ @idl_expose(kind="set")
50
+ def set_tx_lo(self, value):
51
+ self.device.tx_lo = value
52
+
53
+ @idl_expose(kind="call")
54
+ def call_rx(self):
55
+ return self.device.rx()
56
+
57
+ @idl_expose # bare — defaults to kind="call"
58
+ def some_method(self, x):
59
+ ...
60
+
61
+ kind:
62
+ "get" — property getter (no args beyond self)
63
+ "set" — property setter (one arg: value)
64
+ "call" — general method (any args)
65
+ """
66
+ def _apply(fn):
67
+ meta = {
68
+ "kind": kind,
69
+ "doc": doc or (fn.__doc__ or "").strip(),
70
+ }
71
+ setattr(fn, _EXPOSE_KEY, meta)
72
+ return fn
73
+
74
+ if _fn is not None:
75
+ return _apply(_fn)
76
+ return _apply
77
+
78
+
79
+ # ───────────────────────────────────────────────────────────────────
80
+ # Driver registry
81
+ # ───────────────────────────────────────────────────────────────────
82
+
83
+ _DRIVER_REGISTRY: Dict[str, type] = {}
84
+
85
+ def register_driver(device_type: str):
86
+ """
87
+ Class decorator — registers a schema so the server can find it by type string.
88
+
89
+ @register_driver("pluto")
90
+ class PlutoSchema(DeviceSchema):
91
+ ...
92
+ """
93
+ def decorator(cls):
94
+ key = device_type.strip().lower()
95
+ _DRIVER_REGISTRY[key] = cls
96
+ return cls
97
+ return decorator
98
+
99
+ def get_driver_class(device_type: str) -> type:
100
+ cls = _DRIVER_REGISTRY.get(device_type.strip().lower())
101
+ if cls is None:
102
+ raise KeyError(
103
+ f"no driver for type '{device_type}'. "
104
+ f"registered: {list(_DRIVER_REGISTRY.keys()) or ['(none)']}"
105
+ )
106
+ return cls
107
+
108
+ def list_registered_drivers() -> Dict[str, str]:
109
+ return {
110
+ k: f"{cls.device_type} v{cls.driver_version}"
111
+ for k, cls in _DRIVER_REGISTRY.items()
112
+ }
113
+
114
+ # ───────────────────────────────────────────────────────────────────
115
+ # Plugin loader — auto-discover driver schemas from a folder
116
+ # ───────────────────────────────────────────────────────────────────
117
+
118
+ def load_drivers() -> Dict[str, str]:
119
+ """
120
+ Import every .py in the drivers config dir. Each file's @idl_register
121
+ decorator self-registers into the global registry.
122
+
123
+ Returns {filename: status} for logging.
124
+
125
+ Called once at server startup:
126
+ load_drivers()
127
+ """
128
+ drivers_dir = get_drivers_dir()
129
+ results: Dict[str, str] = {}
130
+
131
+ if not drivers_dir.is_dir():
132
+ return {"__error__": f"{drivers_dir} is not a directory"}
133
+
134
+ for f in sorted(drivers_dir.glob("*.py")):
135
+ if f.name.startswith("_"):
136
+ continue
137
+ try:
138
+ spec = importlib.util.spec_from_file_location(f.stem, str(f))
139
+ if spec is None or spec.loader is None:
140
+ results[f.name] = "skip: not a valid module"
141
+ continue
142
+ mod = importlib.util.module_from_spec(spec)
143
+ # Make our module importable from inside the driver file
144
+ # so `from schema import ...` works even when loaded as a plugin
145
+ sys.modules[f.stem] = mod
146
+ spec.loader.exec_module(mod)
147
+ results[f.name] = "ok"
148
+ except Exception as e:
149
+ results[f.name] = f"error: {e}"
150
+
151
+ return results
152
+
153
+
154
+ # ───────────────────────────────────────────────────────────────────
155
+ # DeviceSchema base class
156
+ # ───────────────────────────────────────────────────────────────────
157
+
158
+ class DeviceSchema:
159
+ """
160
+ Base for all device schemas. Admin subclasses this once per device type.
161
+
162
+ Class attributes (admin sets these):
163
+ device_type str — must match the TYPE in devices.env
164
+ driver_version str — semver
165
+
166
+ Admin implements:
167
+ make_device(**kwargs) — factory: manifest config → live device handle
168
+ returns the device object, or None on failure
169
+
170
+ Admin decorates exposed methods with @idl_expose(kind=...).
171
+
172
+ The server calls:
173
+ schema = PlutoSchema()
174
+ dev = schema.make_device(serial="00000001")
175
+ schema.device = dev # or schema.bind(dev)
176
+ schema.dispatch("get_tx_lo", {}) # → calls get_tx_lo()
177
+ """
178
+
179
+ device_type: str = ""
180
+ driver_version: str = ""
181
+
182
+ def __init__(self):
183
+ self.device = None # set after make_device()
184
+ self._exposed: Dict[str, _Exposed] = {}
185
+ self._schema_cache: dict | None = None
186
+ self._hash_cache: str | None = None
187
+ self._introspect()
188
+
189
+ # ── Factory (admin overrides) ─────────────────────────────────
190
+
191
+ @staticmethod
192
+ def make_device(**kwargs):
193
+ """
194
+ Create and return a live device handle using kwargs from the manifest.
195
+ Admin MUST override this.
196
+ """
197
+ raise NotImplementedError("make_device() must be implemented in the schema")
198
+
199
+ # ── Bind device to schema instance ────────────────────────────
200
+
201
+ def bind(self, device) -> None:
202
+ """Attach a live device handle so exposed methods can use self.device."""
203
+ self.device = device
204
+
205
+ def is_connected(self) -> bool:
206
+ return self.device is not None
207
+
208
+ # ── Introspection ─────────────────────────────────────────────
209
+
210
+ def _introspect(self):
211
+ seen: set = set()
212
+ for klass in type(self).__mro__:
213
+ for name, obj in vars(klass).items():
214
+ if name in seen or name.startswith("_"):
215
+ continue
216
+ meta = getattr(obj, _EXPOSE_KEY, None)
217
+ if meta is None:
218
+ continue
219
+ seen.add(name)
220
+ self._exposed[name] = _Exposed(
221
+ name=name, fn=obj, kind=meta["kind"], doc=meta["doc"],
222
+ )
223
+
224
+ # ── IDL generation ────────────────────────────────────────────
225
+
226
+ def get_idl(self) -> dict:
227
+ """Full wire-ready schema dict with content-addressed hash."""
228
+ if self._schema_cache is not None:
229
+ return self._schema_cache
230
+
231
+ body = self._build_body()
232
+ canon = json.dumps(body, sort_keys=True, separators=(",", ":"))
233
+ h = f"sha256:{hashlib.sha256(canon.encode()).hexdigest()}"
234
+
235
+ self._hash_cache = h
236
+ self._schema_cache = {**body, "schema_hash": h}
237
+ return self._schema_cache
238
+
239
+ def get_idl_hash(self) -> str:
240
+ if self._hash_cache is None:
241
+ self.get_idl()
242
+ return self._hash_cache # type: ignore
243
+
244
+ def get_idl_json(self, *, pretty: bool = False) -> str:
245
+ d = self.get_idl()
246
+ if pretty:
247
+ return json.dumps(d, indent=2, sort_keys=True)
248
+ return json.dumps(d, sort_keys=True, separators=(",", ":"))
249
+
250
+ def _build_body(self) -> dict:
251
+ getters = {}
252
+ setters = {}
253
+ calls = {}
254
+
255
+ for name, ex in sorted(self._exposed.items()):
256
+ entry = {"doc": ex.doc} if ex.doc else {}
257
+ if ex.kind == "get":
258
+ getters[name] = entry
259
+ elif ex.kind == "set":
260
+ setters[name] = entry
261
+ elif ex.kind == "call":
262
+ calls[name] = entry
263
+
264
+ return {
265
+ "schema_version": "1.0",
266
+ "device_type": self.device_type,
267
+ "driver_version": self.driver_version,
268
+ "getters": getters,
269
+ "setters": setters,
270
+ "calls": calls,
271
+ }
272
+
273
+ # ── Dispatch (server RPC layer calls this) ────────────────────
274
+
275
+ def dispatch(self, method_name: str, args: dict) -> Any:
276
+ """
277
+ Called by the RPC layer. Finds the exposed method and calls it.
278
+
279
+ dispatch("get_tx_lo", {}) → self.get_tx_lo()
280
+ dispatch("set_tx_lo", {"value": 2.4e9}) → self.set_tx_lo(2.4e9)
281
+ dispatch("call_rx", {}) → self.call_rx()
282
+ dispatch("call_tx", {"value": samples}) → self.call_tx(samples)
283
+ """
284
+ ex = self._exposed.get(method_name)
285
+ if ex is None:
286
+ raise KeyError(
287
+ f"'{method_name}' is not exposed on {self.device_type}. "
288
+ f"Available: {list(self._exposed.keys())}"
289
+ )
290
+
291
+ if self.device is None:
292
+ raise RuntimeError(f"no device bound to {self.device_type} schema")
293
+
294
+ if ex.kind == "get":
295
+ return ex.fn(self)
296
+
297
+ elif ex.kind == "set":
298
+ value = args.get("value")
299
+ if value is None and "value" not in args:
300
+ # Also check first positional-style arg key
301
+ for k, v in args.items():
302
+ value = v
303
+ break
304
+ return ex.fn(self, value)
305
+
306
+ elif ex.kind == "call":
307
+ # Calls can have 0 or more args
308
+ # Simple convention: if the function takes 'value', pass it
309
+ import inspect
310
+ sig = inspect.signature(ex.fn)
311
+ params = [p for p in sig.parameters if p != "self"]
312
+
313
+ if len(params) == 0:
314
+ return ex.fn(self)
315
+ elif len(params) == 1:
316
+ # Single arg — pull from args dict by param name or "value"
317
+ pname = params[0]
318
+ val = args.get(pname, args.get("value"))
319
+ return ex.fn(self, val)
320
+ else:
321
+ # Multi-arg — match by name
322
+ call_args = {p: args.get(p) for p in params if p in args}
323
+ return ex.fn(self, **call_args)
324
+ else:
325
+ raise ValueError(f"unknown kind '{ex.kind}' on {method_name}")
326
+
327
+ # ── Debug ─────────────────────────────────────────────────────
328
+
329
+ def list_exposed(self) -> dict:
330
+ by_kind: Dict[str, list] = {"get": [], "set": [], "call": []}
331
+ for name, ex in sorted(self._exposed.items()):
332
+ by_kind.get(ex.kind, []).append(name)
333
+ return {
334
+ "device_type": self.device_type,
335
+ "driver_version": self.driver_version,
336
+ "connected": self.is_connected(),
337
+ "getters": by_kind["get"],
338
+ "setters": by_kind["set"],
339
+ "calls": by_kind["call"],
340
+ }
341
+
342
+ def __repr__(self) -> str:
343
+ return f"<{type(self).__name__} type={self.device_type!r} connected={self.is_connected()}>"
344
+
345
+
346
+ # ───────────────────────────────────────────────────────────────────
347
+ # Internal
348
+ # ───────────────────────────────────────────────────────────────────
349
+
350
+ class _Exposed:
351
+ __slots__ = ("name", "fn", "kind", "doc")
352
+ def __init__(self, *, name, fn, kind, doc):
353
+ self.name = name
354
+ self.fn = fn
355
+ self.kind = kind
356
+ self.doc = doc
@@ -3,4 +3,4 @@ from .process_arg import unmap_arg, map_arg
3
3
  from .ansi_codes import printf, stylize, Sty
4
4
  from .list_string import list_to_str, str_to_list
5
5
  from .db_connection import db_connection
6
- from .db_location import get_db_dir
6
+ from .db_location import get_db_dir, get_certs_dir, get_drivers_dir, get_remoterf_root
@@ -19,3 +19,6 @@ def get_certs_dir() -> Path:
19
19
 
20
20
  def get_db_dir() -> Path:
21
21
  return get_remoterf_root() / "db"
22
+
23
+ def get_drivers_dir() -> Path:
24
+ return get_remoterf_root() / "drivers"
@@ -6,6 +6,7 @@ import os
6
6
  import re
7
7
  import time
8
8
  import adi
9
+ import yaml
9
10
  import subprocess
10
11
  import threading
11
12
  from dataclasses import dataclass, field
@@ -13,7 +14,7 @@ from pathlib import Path
13
14
  from typing import Dict, Tuple, Optional, Any, List, Iterator
14
15
  from contextlib import contextmanager
15
16
 
16
- from ..common.utils import validate_token
17
+ from ..common.utils import validate_token, get_remoterf_root
17
18
  from ..host import host_tunnel_server as hts
18
19
 
19
20
  _state_lock = threading.RLock()
@@ -226,17 +227,67 @@ def _host_devices_str_snapshot() -> Dict[int, str]:
226
227
 
227
228
  # Config paths (server-side)
228
229
 
229
- def _xdg_config_home() -> Path:
230
- return Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
230
+ def _cfg_dir() -> Path:
231
+ return Path(os.getenv("REMOTERF_CONFIG_DIR", get_remoterf_root()))
231
232
 
233
+ def _devices_yaml_path() -> Path:
234
+ p1 = _cfg_dir() / "devices.yml"
235
+ if p1.exists():
236
+ return p1
237
+ return _cfg_dir() / "devices.yaml"
232
238
 
233
- def _cfg_dir() -> Path:
234
- # default: ~/.config/remoterf
235
- return Path(os.getenv("REMOTERF_CONFIG_DIR", _xdg_config_home() / "remoterf"))
239
+ def _load_device_records() -> Dict[int, Dict[str, str]]:
240
+ path = _devices_yaml_path()
241
+ if not path.exists():
242
+ return {}
243
+
244
+ try:
245
+ with path.open("r", encoding="utf-8") as f:
246
+ data = yaml.safe_load(f) or {}
247
+ except Exception as e:
248
+ print(f"Error reading YAML config {path}: {e}")
249
+ return {}
250
+
251
+ devices = data.get("devices") or []
252
+ if not isinstance(devices, list):
253
+ print(f"Invalid YAML config {path}: 'devices' must be a list")
254
+ return {}
255
+
256
+ recs: Dict[int, Dict[str, str]] = {}
257
+
258
+ for item in devices:
259
+ if not isinstance(item, dict):
260
+ continue
261
+
262
+ try:
263
+ gid = int(item["device_id"])
264
+ except Exception:
265
+ print(f"Skipping device entry with invalid/missing device_id: {item}")
266
+ continue
267
+
268
+ dtype = str(item.get("device_type") or "pluto").strip().lower()
269
+ if dtype != "pluto":
270
+ print(f"Skipping unsupported device_type={dtype!r} for gid={gid}")
271
+ continue
236
272
 
273
+ name = str(item.get("name") or f"device-{gid}").strip()
274
+ init = item.get("init") or {}
275
+ if not isinstance(init, dict):
276
+ init = {}
237
277
 
238
- def _devices_env_path() -> Path:
239
- return _cfg_dir() / "devices.env"
278
+ serial = str(init.get("serial") or "").strip()
279
+ if not serial:
280
+ print(f"Skipping Pluto gid={gid}: missing init.serial")
281
+ continue
282
+
283
+ recs[gid] = {
284
+ "TYPE": "pluto",
285
+ "NAME": name,
286
+ "IDENT_KIND": "iio_serial",
287
+ "IDENT": serial,
288
+ }
289
+
290
+ return recs
240
291
 
241
292
  # Pluto helpers
242
293
 
@@ -273,74 +324,6 @@ def get_usb_port_from_serial(serial: str) -> str | None:
273
324
  print(f"No device found with serial {serial}")
274
325
  return None
275
326
 
276
- # Parse devices.env
277
-
278
- _ENV_LINE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
279
- _DEVICE_KEY_RE = re.compile(r"^DEVICE_(\d+)_(.+)$", re.IGNORECASE)
280
-
281
- def _strip_quotes(v: str) -> str:
282
- v = (v or "").strip()
283
- if len(v) >= 2 and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
284
- return v[1:-1]
285
- return v
286
-
287
- def _read_env_file(path: Path) -> Dict[str, str]:
288
- out: Dict[str, str] = {}
289
- if not path.exists():
290
- return out
291
- try:
292
- txt = path.read_text(encoding="utf-8")
293
- except Exception:
294
- return out
295
-
296
- for raw in txt.splitlines():
297
- line = raw.strip()
298
- if not line or line.startswith("#"):
299
- continue
300
- m = _ENV_LINE.match(line)
301
- if not m:
302
- continue
303
- k = m.group(1).strip()
304
- v = _strip_quotes(m.group(2))
305
- out[k] = v
306
- return out
307
-
308
- def _load_device_records() -> Dict[int, Dict[str, str]]:
309
- # Returns: { gid: { "TYPE":..., "NAME":..., "IDENT_KIND":..., "IDENT":..., ... } }
310
-
311
- kv = _read_env_file(_devices_env_path())
312
- recs: Dict[int, Dict[str, str]] = {}
313
-
314
- # New format: DEVICE_<gid>_<FIELD>
315
- for k, v in kv.items():
316
- m = _DEVICE_KEY_RE.match(k.strip())
317
- if not m:
318
- continue
319
- try:
320
- gid = int(m.group(1))
321
- except Exception:
322
- continue
323
- field = m.group(2).strip().upper()
324
- recs.setdefault(gid, {})[field] = str(v).strip()
325
-
326
- # Back-compat: DEVICE_<gid>_ID treated as IDENT
327
- for k, v in kv.items():
328
- kk = k.strip().upper()
329
- if not kk.startswith("DEVICE_") or not kk.endswith("_ID"):
330
- continue
331
- mid = kk[7:-3]
332
- try:
333
- gid = int(mid)
334
- except Exception:
335
- continue
336
- recs.setdefault(gid, {})
337
- recs[gid].setdefault("TYPE", "pluto")
338
- recs[gid].setdefault("NAME", f"device-{gid}")
339
- recs[gid].setdefault("IDENT_KIND", "iio_serial")
340
- recs[gid].setdefault("IDENT", str(v).strip())
341
-
342
- return recs
343
-
344
327
  def _connect_from_record(rec: Dict[str, str]):
345
328
  # Returns (device_obj, ident, dtype).
346
329
 
@@ -99,11 +99,6 @@ class ReservationHandler:
99
99
  password = unmap_arg(args['pw'])
100
100
  if self.login_user(username, password) == {"UC": map_arg(f'{username}')}:
101
101
  if self.validate_cancel_reservation(unmap_arg(args['res_id']), username):
102
- res_info = self.get_reservation_details(unmap_arg(args['res_id']))
103
- if res_info:
104
- args['cancelled_device_id'] = map_arg(res_info['device_id'])
105
- args['cancelled_start_time'] = map_arg(res_info['start_time'])
106
- args['cancelled_end_time'] = map_arg(res_info['end_time'])
107
102
  return self.remove_reservation(res_id=unmap_arg(args['res_id']))
108
103
  else:
109
104
  return {"ace": map_arg("Invalid reservation ID or user does not own reservation.")}
@@ -649,14 +644,6 @@ class ReservationHandler:
649
644
  "total_reservations": int(total_reservations),
650
645
  "total_reserved_hours": round(total_reserved_seconds / 3600.0, 1),
651
646
  }
652
-
653
- @db_connection
654
- def get_reservation_details(self, res_id: int) -> dict | None:
655
- self.cursor.execute('SELECT device_id, start_time, end_time FROM Reservations WHERE res_id = ?', (res_id,))
656
- row = self.cursor.fetchone()
657
- if row:
658
- return {"device_id": row[0], "start_time": row[1], "end_time": row[2]}
659
- return None
660
647
 
661
648
  def get_reservation_count(self, username):
662
649
  self.cursor.execute("SELECT COUNT(*) FROM Reservations WHERE username = ?", (username,))
@@ -134,6 +134,6 @@ def start_status_publisher(*, period_sec: int = 60) -> None:
134
134
 
135
135
 
136
136
  if __name__ == "__main__":
137
- start_status_publisher(period_sec=60)
137
+ start_status_publisher(period_sec=120)
138
138
  while True:
139
139
  time.sleep(3600)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 0.1.136
3
+ Version: 0.2.0
4
4
  Description-Content-Type: text/markdown
5
5
  Requires-Dist: grpcio
6
6
  Requires-Dist: protobuf
@@ -9,6 +9,7 @@ Requires-Dist: prompt_toolkit
9
9
  Requires-Dist: python-dotenv
10
10
  Requires-Dist: grpcio-tools
11
11
  Requires-Dist: requests
12
+ Requires-Dist: pyyaml
12
13
 
13
14
  # Remote RF Server Guide (Linux)
14
15
 
@@ -14,6 +14,10 @@ src/remoteRF_server/common/grpc/grpc_host_pb2.py
14
14
  src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py
15
15
  src/remoteRF_server/common/grpc/grpc_pb2.py
16
16
  src/remoteRF_server/common/grpc/grpc_pb2_grpc.py
17
+ src/remoteRF_server/common/idl/__init__.py
18
+ src/remoteRF_server/common/idl/device_schema.py
19
+ src/remoteRF_server/common/idl/pluto_schema.py
20
+ src/remoteRF_server/common/idl/schema.py
17
21
  src/remoteRF_server/common/utils/__init__.py
18
22
  src/remoteRF_server/common/utils/ansi_codes.py
19
23
  src/remoteRF_server/common/utils/api_token.py
@@ -5,3 +5,4 @@ prompt_toolkit
5
5
  python-dotenv
6
6
  grpcio-tools
7
7
  requests
8
+ pyyaml