remoteRF-server 0.1.135__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.
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/PKG-INFO +2 -1
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/pyproject.toml +3 -2
- remoterf_server-0.2.0/src/remoteRF_server/common/idl/__init__.py +1 -0
- remoterf_server-0.2.0/src/remoteRF_server/common/idl/device_schema.py +39 -0
- remoterf_server-0.2.0/src/remoteRF_server/common/idl/pluto_schema.py +174 -0
- remoterf_server-0.2.0/src/remoteRF_server/common/idl/schema.py +356 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/__init__.py +1 -1
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/db_location.py +3 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/device_manager.py +59 -76
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/reservation.py +32 -140
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gist_status.py +1 -1
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/PKG-INFO +2 -1
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/SOURCES.txt +4 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/requires.txt +1 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/README.md +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/setup.cfg +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/api_token.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/db_connection.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/list_string.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/process_arg.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_auth_token.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_directory_store.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/acc_perms.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/cert_provider.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/grpc_server.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/rpc_manager.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_cli.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_handler.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/serverrf_cli.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/tools/__init__.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gen_certs.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
- {remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
- {remoterf_server-0.1.135 → 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.
|
|
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.
|
|
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
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/__init__.py
RENAMED
|
@@ -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
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/device_manager.py
RENAMED
|
@@ -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
|
|
230
|
-
return Path(os.getenv("
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
|
@@ -478,12 +478,7 @@ class ReservationHandler:
|
|
|
478
478
|
print("Username:", account[1], f'<{perms.get_perms(account[1])[0][0]}>', " Email:", account[2], " Created At:", account[5])
|
|
479
479
|
else:
|
|
480
480
|
print("No accounts found.")
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
# ──────────────────────────────────────────────────────────────
|
|
484
|
-
# UPDATED: get_obfuscated_device_reservations_grouped_past_days
|
|
485
|
-
# (logs-only, no DB access)
|
|
486
|
-
# ──────────────────────────────────────────────────────────────
|
|
481
|
+
|
|
487
482
|
@db_connection
|
|
488
483
|
def get_obfuscated_device_reservations_grouped_past_days(self, days: int) -> dict[str, dict[str, int]]:
|
|
489
484
|
try:
|
|
@@ -505,105 +500,62 @@ class ReservationHandler:
|
|
|
505
500
|
|
|
506
501
|
s = str(v).strip()
|
|
507
502
|
|
|
503
|
+
# Handles strings like:
|
|
504
|
+
# "int64_value: 2"
|
|
505
|
+
# "int64_value: 1772587800"
|
|
508
506
|
if ":" in s:
|
|
509
507
|
s = s.split(":", 1)[1].strip()
|
|
510
508
|
|
|
509
|
+
# remove quotes/newlines if present
|
|
511
510
|
s = s.strip().strip('"').strip("'")
|
|
512
511
|
return int(float(s))
|
|
513
512
|
|
|
514
|
-
|
|
515
|
-
if not isinstance(v, str):
|
|
516
|
-
return str(v).strip()
|
|
517
|
-
s = v.strip()
|
|
518
|
-
if ":" in s:
|
|
519
|
-
s = s.split(":", 1)[1].strip()
|
|
520
|
-
s = s.strip().strip('"').strip("'").strip()
|
|
521
|
-
return s
|
|
522
|
-
|
|
513
|
+
# Take a consistent snapshot of the log file under the same lock used by writers.
|
|
523
514
|
try:
|
|
524
515
|
with log_lock:
|
|
525
516
|
activity_log = _load_logs()
|
|
526
517
|
except Exception:
|
|
527
518
|
activity_log = []
|
|
528
519
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
for
|
|
520
|
+
out: dict[str, dict[str, int]] = {}
|
|
521
|
+
|
|
522
|
+
for entry in activity_log:
|
|
532
523
|
try:
|
|
533
524
|
if not isinstance(entry, dict):
|
|
534
525
|
continue
|
|
526
|
+
|
|
535
527
|
if str(entry.get("action", "")).strip() != "reserve_device":
|
|
536
528
|
continue
|
|
529
|
+
|
|
537
530
|
details = entry.get("details", {})
|
|
538
531
|
if not isinstance(details, dict):
|
|
539
532
|
continue
|
|
533
|
+
|
|
540
534
|
args = details.get("args", {})
|
|
541
535
|
result = details.get("result", {})
|
|
542
|
-
if not isinstance(args, dict) or not isinstance(result, dict):
|
|
543
|
-
continue
|
|
544
|
-
if "Token" not in result:
|
|
545
|
-
continue
|
|
546
536
|
|
|
547
|
-
|
|
548
|
-
raw_dd = args.get("dd")
|
|
549
|
-
raw_st = args.get("st")
|
|
550
|
-
raw_et = args.get("et")
|
|
551
|
-
if raw_dd is None or raw_st is None or raw_et is None:
|
|
537
|
+
if not isinstance(args, dict) or not isinstance(result, dict):
|
|
552
538
|
continue
|
|
553
539
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
start_ts = _parse_logged_int(raw_st)
|
|
557
|
-
end_ts = _parse_logged_int(raw_et)
|
|
558
|
-
|
|
559
|
-
reserves.append((i, username, device_id, start_ts, end_ts))
|
|
560
|
-
except Exception:
|
|
561
|
-
continue
|
|
562
|
-
|
|
563
|
-
# ── Pass 2: Collect successful cancel_res entries ──
|
|
564
|
-
cancels = [] # list of cancel_username (in log order)
|
|
565
|
-
for entry in activity_log:
|
|
566
|
-
try:
|
|
567
|
-
if not isinstance(entry, dict):
|
|
568
|
-
continue
|
|
569
|
-
if str(entry.get("action", "")).strip() != "cancel_res":
|
|
570
|
-
continue
|
|
571
|
-
details = entry.get("details", {})
|
|
572
|
-
if not isinstance(details, dict):
|
|
573
|
-
continue
|
|
574
|
-
result = details.get("result", {})
|
|
575
|
-
args = details.get("args", {})
|
|
576
|
-
if not isinstance(result, dict) or not isinstance(args, dict):
|
|
577
|
-
continue
|
|
578
|
-
if "UC" not in result:
|
|
540
|
+
# Only count SUCCESSFUL reservations
|
|
541
|
+
if "Token" not in result:
|
|
579
542
|
continue
|
|
580
543
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
continue
|
|
544
|
+
raw_device_id = args.get("dd")
|
|
545
|
+
raw_start = args.get("st")
|
|
546
|
+
raw_end = args.get("et")
|
|
585
547
|
|
|
586
|
-
|
|
587
|
-
canceled_indices: set[int] = set()
|
|
588
|
-
for cancel_username in cancels:
|
|
589
|
-
for (log_i, res_un, dev_id, st, et) in reserves:
|
|
590
|
-
if log_i in canceled_indices:
|
|
548
|
+
if raw_device_id is None or raw_start is None or raw_end is None:
|
|
591
549
|
continue
|
|
592
|
-
if res_un == cancel_username:
|
|
593
|
-
canceled_indices.add(log_i)
|
|
594
|
-
break # one cancel removes one reservation
|
|
595
|
-
|
|
596
|
-
# ── Build output, skipping canceled entries ──
|
|
597
|
-
out: dict[str, dict[str, int]] = {}
|
|
598
550
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
continue
|
|
551
|
+
device_id = _parse_logged_int(raw_device_id)
|
|
552
|
+
start_ts = _parse_logged_int(raw_start)
|
|
553
|
+
end_ts = _parse_logged_int(raw_end)
|
|
603
554
|
|
|
604
555
|
start_dt = datetime.fromtimestamp(start_ts)
|
|
605
556
|
end_dt = datetime.fromtimestamp(end_ts)
|
|
606
557
|
|
|
558
|
+
# Same overlap semantics as before
|
|
607
559
|
if end_dt < cutoff or start_dt > now:
|
|
608
560
|
continue
|
|
609
561
|
|
|
@@ -619,11 +571,7 @@ class ReservationHandler:
|
|
|
619
571
|
continue
|
|
620
572
|
|
|
621
573
|
return dict(sorted(out.items(), key=lambda kv: int(kv[0])))
|
|
622
|
-
|
|
623
|
-
# ──────────────────────────────────────────────
|
|
624
|
-
# UPDATED: get_obfuscated_usage_summary
|
|
625
|
-
# (logs-only, no DB access)
|
|
626
|
-
# ──────────────────────────────────────────────
|
|
574
|
+
|
|
627
575
|
@db_connection
|
|
628
576
|
def get_obfuscated_usage_summary(self) -> dict[str, int | float]:
|
|
629
577
|
|
|
@@ -646,26 +594,17 @@ class ReservationHandler:
|
|
|
646
594
|
|
|
647
595
|
return v
|
|
648
596
|
|
|
649
|
-
def _parse_logged_str(v) -> str:
|
|
650
|
-
if not isinstance(v, str):
|
|
651
|
-
return str(v).strip()
|
|
652
|
-
s = v.strip()
|
|
653
|
-
if ":" in s:
|
|
654
|
-
s = s.split(":", 1)[1].strip()
|
|
655
|
-
s = s.strip().strip('"').strip("'").strip()
|
|
656
|
-
return s
|
|
657
|
-
|
|
658
597
|
try:
|
|
659
598
|
with log_lock:
|
|
660
599
|
activity_log = _load_logs()
|
|
661
600
|
except Exception:
|
|
662
601
|
activity_log = []
|
|
663
602
|
|
|
664
|
-
# ── Collect successful reserves and account logins ──
|
|
665
|
-
reserves = [] # (log_index, username, start_ts, end_ts)
|
|
666
603
|
accounts: set[str] = set()
|
|
604
|
+
total_reservations = 0
|
|
605
|
+
total_reserved_seconds = 0
|
|
667
606
|
|
|
668
|
-
for
|
|
607
|
+
for entry in activity_log:
|
|
669
608
|
try:
|
|
670
609
|
if not isinstance(entry, dict):
|
|
671
610
|
continue
|
|
@@ -687,71 +626,24 @@ class ReservationHandler:
|
|
|
687
626
|
elif username:
|
|
688
627
|
accounts.add(str(username))
|
|
689
628
|
|
|
690
|
-
#
|
|
691
|
-
if action == "reserve_device"
|
|
692
|
-
|
|
629
|
+
# Count reservations + reserved time
|
|
630
|
+
if action == "reserve_device":
|
|
631
|
+
total_reservations += 1
|
|
693
632
|
|
|
694
633
|
st = _parse_proto_value(args.get("st"))
|
|
695
634
|
et = _parse_proto_value(args.get("et"))
|
|
696
635
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
reserves.append((i, res_username, start_ts, end_ts))
|
|
636
|
+
if isinstance(st, int) and isinstance(et, int) and et > st:
|
|
637
|
+
total_reserved_seconds += (et - st)
|
|
701
638
|
|
|
702
639
|
except Exception:
|
|
703
640
|
continue
|
|
704
641
|
|
|
705
|
-
# ── Collect successful cancels ──
|
|
706
|
-
cancels = []
|
|
707
|
-
for entry in activity_log:
|
|
708
|
-
try:
|
|
709
|
-
if not isinstance(entry, dict):
|
|
710
|
-
continue
|
|
711
|
-
if str(entry.get("action", "")).strip() != "cancel_res":
|
|
712
|
-
continue
|
|
713
|
-
details = entry.get("details", {})
|
|
714
|
-
if not isinstance(details, dict):
|
|
715
|
-
continue
|
|
716
|
-
result = details.get("result", {})
|
|
717
|
-
args = details.get("args", {})
|
|
718
|
-
if not isinstance(result, dict) or not isinstance(args, dict):
|
|
719
|
-
continue
|
|
720
|
-
if "UC" not in result:
|
|
721
|
-
continue
|
|
722
|
-
|
|
723
|
-
cancel_username = _parse_logged_str(args.get("un", ""))
|
|
724
|
-
cancels.append(cancel_username)
|
|
725
|
-
except Exception:
|
|
726
|
-
continue
|
|
727
|
-
|
|
728
|
-
# ── Match each cancel to the earliest unmatched reserve for that user ──
|
|
729
|
-
canceled_indices: set[int] = set()
|
|
730
|
-
for cancel_username in cancels:
|
|
731
|
-
for (log_i, res_un, st, et) in reserves:
|
|
732
|
-
if log_i in canceled_indices:
|
|
733
|
-
continue
|
|
734
|
-
if res_un == cancel_username:
|
|
735
|
-
canceled_indices.add(log_i)
|
|
736
|
-
break
|
|
737
|
-
|
|
738
|
-
# ── Tally, excluding canceled ──
|
|
739
|
-
total_reservations = 0
|
|
740
|
-
total_reserved_seconds = 0
|
|
741
|
-
|
|
742
|
-
for (log_i, res_un, start_ts, end_ts) in reserves:
|
|
743
|
-
if log_i in canceled_indices:
|
|
744
|
-
continue
|
|
745
|
-
total_reservations += 1
|
|
746
|
-
if end_ts > start_ts:
|
|
747
|
-
total_reserved_seconds += (end_ts - start_ts)
|
|
748
|
-
|
|
749
642
|
return {
|
|
750
643
|
"account_count": len(accounts),
|
|
751
644
|
"total_reservations": int(total_reservations),
|
|
752
645
|
"total_reserved_hours": round(total_reserved_seconds / 3600.0, 1),
|
|
753
646
|
}
|
|
754
|
-
|
|
755
647
|
|
|
756
648
|
def get_reservation_count(self, username):
|
|
757
649
|
self.cursor.execute("SELECT COUNT(*) FROM Reservations WHERE username = ?", (username,))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoteRF-server
|
|
3
|
-
Version: 0.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/__init__.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_host_pb2.py
RENAMED
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/ansi_codes.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/api_token.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/db_connection.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/list_string.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/common/utils/process_arg.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_auth_token.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_directory_store.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/host/host_tunnel_server.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/cert_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_cli.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/server/user_group_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server/tools/gist_status_testing.py
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{remoterf_server-0.1.135 → remoterf_server-0.2.0}/src/remoteRF_server.egg-info/top_level.txt
RENAMED
|
File without changes
|