py-multitrack 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- multitrack/Elements.py +835 -0
- multitrack/MultitrackAPI.py +169 -0
- multitrack/SerialBridge.py +61 -0
- multitrack/SerialDriver.py +65 -0
- multitrack/StateStore.py +35 -0
- multitrack/UDPListener.py +64 -0
- multitrack/__init__.py +10 -0
- multitrack/models.py +184 -0
- py_multitrack-0.1.0a1.dist-info/METADATA +13 -0
- py_multitrack-0.1.0a1.dist-info/RECORD +13 -0
- py_multitrack-0.1.0a1.dist-info/WHEEL +5 -0
- py_multitrack-0.1.0a1.dist-info/licenses/LICENSE +674 -0
- py_multitrack-0.1.0a1.dist-info/top_level.txt +1 -0
multitrack/Elements.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Callable, Literal, Any
|
|
3
|
+
from .models import Element, EntityInfo, GeneralMetadata, ToggleMetadata, ChatboxMetadata, NumberMetadata, DropdownMetadata
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger("MultitrackAPI")
|
|
6
|
+
|
|
7
|
+
class RemoteElements():
|
|
8
|
+
def __init__(self, verbose: bool = False):
|
|
9
|
+
self.verbose = verbose
|
|
10
|
+
self.elements: dict[str, Element] = {}
|
|
11
|
+
|
|
12
|
+
def list_elements(self):
|
|
13
|
+
elements: list[GeneralMetadata | type[GeneralMetadata]]= []
|
|
14
|
+
for e in self.elements.values():
|
|
15
|
+
elements.append(e.metadata)
|
|
16
|
+
return elements
|
|
17
|
+
|
|
18
|
+
# Decorators definitions
|
|
19
|
+
def button(
|
|
20
|
+
self,
|
|
21
|
+
path: str,
|
|
22
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
23
|
+
role: Literal["operator", "admin"] = "operator",
|
|
24
|
+
method: Literal["GET", "POST"] = "POST",
|
|
25
|
+
group: str | None = None,
|
|
26
|
+
sensible: bool = False,
|
|
27
|
+
display_text: str | None = None,
|
|
28
|
+
tooltip: str | None = None
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Creates a button to execute serial commands.\n
|
|
32
|
+
### Params
|
|
33
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
34
|
+
role: Determine which role has access to this route.\n
|
|
35
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
36
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
37
|
+
group: Specify the UI group to which the action belongs.\n
|
|
38
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
39
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
40
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
41
|
+
### Decorated params
|
|
42
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
43
|
+
### Decorated return
|
|
44
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
45
|
+
### Example
|
|
46
|
+
```
|
|
47
|
+
@app.remote.button(
|
|
48
|
+
path = "get_position",
|
|
49
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
50
|
+
group = "Basic actions",
|
|
51
|
+
tooltip = "Ask for the position of the remote entity"
|
|
52
|
+
)
|
|
53
|
+
def get_position(e: EntityInfo):
|
|
54
|
+
return f'*SET,IDAS,TXSTAT,IND,{e.serial},{driver.local_id},24'
|
|
55
|
+
```
|
|
56
|
+
"""
|
|
57
|
+
def button(func: Callable[[EntityInfo], str]):
|
|
58
|
+
def nfunc(e: EntityInfo):
|
|
59
|
+
if self.verbose: log.info(f"Executed {func(e)}")
|
|
60
|
+
return func(e)
|
|
61
|
+
self.elements[path] = Element(
|
|
62
|
+
handler = nfunc,
|
|
63
|
+
check_ack = check_ack,
|
|
64
|
+
metadata = GeneralMetadata(
|
|
65
|
+
path=path,
|
|
66
|
+
element="button",
|
|
67
|
+
blocking=check_ack != None,
|
|
68
|
+
scope="remote",
|
|
69
|
+
role=role,
|
|
70
|
+
method=method,
|
|
71
|
+
group=group,
|
|
72
|
+
sensible=sensible,
|
|
73
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
74
|
+
tooltip=tooltip
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
78
|
+
return nfunc
|
|
79
|
+
return button
|
|
80
|
+
|
|
81
|
+
def toggle(
|
|
82
|
+
self,
|
|
83
|
+
path: str,
|
|
84
|
+
ref: str,
|
|
85
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
86
|
+
role: Literal["operator", "admin"] = "operator",
|
|
87
|
+
method: Literal["GET", "POST"] = "POST",
|
|
88
|
+
group: str | None = None,
|
|
89
|
+
sensible: bool = False,
|
|
90
|
+
display_text: str | None = None,
|
|
91
|
+
tooltip: str | None = None,
|
|
92
|
+
colors: tuple[str, str] | None = None
|
|
93
|
+
):
|
|
94
|
+
"""
|
|
95
|
+
Creates a toggle button to execute serial commands.\n
|
|
96
|
+
### Params
|
|
97
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
98
|
+
ref: Value bound to the toggle's state. Check full documentation for more info. [REQUIRED]\n
|
|
99
|
+
role: Determine which role has access to this route.\n
|
|
100
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
101
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
102
|
+
group: Specify the UI group to which the action belongs.\n
|
|
103
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
104
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
105
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
106
|
+
colors: HEX values for the button's off/on colors respectively. If None, the Core will default to red/green.\n
|
|
107
|
+
### Decorated params
|
|
108
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
109
|
+
state: State of the *ref* from the Core at request time.\n
|
|
110
|
+
### Decorated return
|
|
111
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
112
|
+
### Example
|
|
113
|
+
```
|
|
114
|
+
@app.remote.toggle(
|
|
115
|
+
path = "stun",
|
|
116
|
+
ref = "is_stun",
|
|
117
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
118
|
+
group = "Basic actions",
|
|
119
|
+
tooltip = "Toggle the STUN status of a radio."
|
|
120
|
+
)
|
|
121
|
+
def toggle_stun(e: EntityInfo, state: bool):
|
|
122
|
+
if state:
|
|
123
|
+
return f'*SET,IDAS,TXREVIVE,IND,{e.serial},{driver.local_id}'
|
|
124
|
+
else:
|
|
125
|
+
return f'*SET,IDAS,TXSTUN,IND,{e.serial},{driver.local_id}'
|
|
126
|
+
```
|
|
127
|
+
"""
|
|
128
|
+
def toggle(func: Callable[[EntityInfo, bool], str]):
|
|
129
|
+
def nfunc(e: EntityInfo, state: bool):
|
|
130
|
+
if self.verbose: log.info(f"Executed {func(e, state)}")
|
|
131
|
+
return func(e, state)
|
|
132
|
+
self.elements[path] = Element(
|
|
133
|
+
handler = nfunc,
|
|
134
|
+
check_ack = check_ack,
|
|
135
|
+
metadata = ToggleMetadata(
|
|
136
|
+
path=path,
|
|
137
|
+
element="toggle",
|
|
138
|
+
blocking=check_ack != None,
|
|
139
|
+
scope="remote",
|
|
140
|
+
role=role,
|
|
141
|
+
method=method,
|
|
142
|
+
group=group,
|
|
143
|
+
sensible=sensible,
|
|
144
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
145
|
+
tooltip=tooltip,
|
|
146
|
+
ref=ref,
|
|
147
|
+
colors=colors
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
151
|
+
return nfunc
|
|
152
|
+
return toggle
|
|
153
|
+
|
|
154
|
+
def chatbox(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
ref: str = "general",
|
|
158
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
159
|
+
role: Literal["operator", "admin"] = "operator",
|
|
160
|
+
method: Literal["GET", "POST"] = "POST",
|
|
161
|
+
group: str | None = None,
|
|
162
|
+
sensible: bool = False,
|
|
163
|
+
display_text: str | None = None,
|
|
164
|
+
tooltip: str | None = None
|
|
165
|
+
):
|
|
166
|
+
"""
|
|
167
|
+
Creates a chatbox (input and history) to handle text messages. You can specify a *ref* to filter where the messages come from/go to if your device supports multiple "channel-like" messaging.\n
|
|
168
|
+
### Params
|
|
169
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
170
|
+
ref: Value bound to the origin/destination of the messages. Check full documentation for more info.\n
|
|
171
|
+
role: Determine which role has access to this route.\n
|
|
172
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
173
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
174
|
+
group: Specify the UI group to which the action belongs.\n
|
|
175
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
176
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
177
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
178
|
+
### Decorated params
|
|
179
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
180
|
+
msg: Message to send.\n
|
|
181
|
+
### Decorated return
|
|
182
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
183
|
+
### Example
|
|
184
|
+
```
|
|
185
|
+
@app.remote.chatbox(
|
|
186
|
+
path = "send_message",
|
|
187
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
188
|
+
tooltip = "Send a message to a remote entity"
|
|
189
|
+
)
|
|
190
|
+
def send_message(e: EntityInfo, msg: str):
|
|
191
|
+
return f'*SET,IDAS,TXMSG,IND,{e.serial},{driver.local_id},MSG,"{msg.replace("\"", "\'")}"'
|
|
192
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
def chatbox(func: Callable[[EntityInfo, str], str]):
|
|
195
|
+
def nfunc(e: EntityInfo, msg: str):
|
|
196
|
+
if self.verbose: log.info(f"Executed {func(e, msg)}")
|
|
197
|
+
return func(e, msg)
|
|
198
|
+
self.elements[path] = Element(
|
|
199
|
+
handler = nfunc,
|
|
200
|
+
check_ack = check_ack,
|
|
201
|
+
metadata = ChatboxMetadata(
|
|
202
|
+
path=path,
|
|
203
|
+
element="chatbox",
|
|
204
|
+
blocking=check_ack != None,
|
|
205
|
+
scope="remote",
|
|
206
|
+
role=role,
|
|
207
|
+
method=method,
|
|
208
|
+
group=group,
|
|
209
|
+
sensible=sensible,
|
|
210
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
211
|
+
tooltip=tooltip,
|
|
212
|
+
ref=ref
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
216
|
+
return nfunc
|
|
217
|
+
return chatbox
|
|
218
|
+
|
|
219
|
+
def text(
|
|
220
|
+
self,
|
|
221
|
+
path: str,
|
|
222
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
223
|
+
role: Literal["operator", "admin"] = "operator",
|
|
224
|
+
method: Literal["GET", "POST"] = "POST",
|
|
225
|
+
group: str | None = None,
|
|
226
|
+
sensible: bool = False,
|
|
227
|
+
display_text: str | None = None,
|
|
228
|
+
tooltip: str | None = None
|
|
229
|
+
):
|
|
230
|
+
"""
|
|
231
|
+
Creates a text input to send texts to a remote entity.\n
|
|
232
|
+
For managing messages with history, you'd want to use a Chatbox instead.\n
|
|
233
|
+
### Params
|
|
234
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
235
|
+
role: Determine which role has access to this route.\n
|
|
236
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
237
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
238
|
+
group: Specify the UI group to which the action belongs.\n
|
|
239
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
240
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
241
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
242
|
+
### Decorated params
|
|
243
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
244
|
+
msg: Message to send.\n
|
|
245
|
+
### Decorated return
|
|
246
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
247
|
+
### Example
|
|
248
|
+
```
|
|
249
|
+
@app.remote.text(
|
|
250
|
+
path = "set_friendly_name",
|
|
251
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
252
|
+
tooltip = "Set a remote friendly name."
|
|
253
|
+
)
|
|
254
|
+
def set_friendly_name(e: EntityInfo, msg: str):
|
|
255
|
+
return f'*SET,IDAS,TXFNAME,IND,{e.serial},{driver.local_id},MSG,"{msg.replace("\"", "\'")}"'
|
|
256
|
+
```
|
|
257
|
+
"""
|
|
258
|
+
def text(func: Callable[[EntityInfo, str], str]):
|
|
259
|
+
def nfunc(e: EntityInfo, msg: str):
|
|
260
|
+
if self.verbose: log.info(f"Executed {func(e, msg)}")
|
|
261
|
+
return func(e, msg)
|
|
262
|
+
self.elements[path] = Element(
|
|
263
|
+
handler = nfunc,
|
|
264
|
+
check_ack = check_ack,
|
|
265
|
+
metadata = GeneralMetadata(
|
|
266
|
+
path=path,
|
|
267
|
+
element="text",
|
|
268
|
+
blocking=check_ack != None,
|
|
269
|
+
scope="remote",
|
|
270
|
+
role=role,
|
|
271
|
+
method=method,
|
|
272
|
+
group=group,
|
|
273
|
+
sensible=sensible,
|
|
274
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
275
|
+
tooltip=tooltip,
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
279
|
+
return nfunc
|
|
280
|
+
return text
|
|
281
|
+
|
|
282
|
+
def number(
|
|
283
|
+
self,
|
|
284
|
+
path: str,
|
|
285
|
+
ref: str,
|
|
286
|
+
vartype: Literal["int", "float"],
|
|
287
|
+
min_max: tuple[int | float, int | float] | None = None,
|
|
288
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
289
|
+
role: Literal["operator", "admin"] = "operator",
|
|
290
|
+
method: Literal["GET", "POST"] = "POST",
|
|
291
|
+
group: str | None = None,
|
|
292
|
+
sensible: bool = False,
|
|
293
|
+
display_text: str | None = None,
|
|
294
|
+
tooltip: str | None = None
|
|
295
|
+
):
|
|
296
|
+
"""
|
|
297
|
+
Creates a number input to send numeric values.\n
|
|
298
|
+
Can be either int or float.\n
|
|
299
|
+
### Params
|
|
300
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
301
|
+
ref: Value bound to the number input. Check full documentation for more info. [REQUIRED].\n
|
|
302
|
+
vartype: Specify whether it's an int or a float. [REQUIRED].\n
|
|
303
|
+
min_max: Minimum and maximum values (respectively) for the number input.\n
|
|
304
|
+
role: Determine which role has access to this route.\n
|
|
305
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
306
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
307
|
+
group: Specify the UI group to which the action belongs.\n
|
|
308
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
309
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
310
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
311
|
+
### Decorated params
|
|
312
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
313
|
+
value: Value sent.\n
|
|
314
|
+
### Decorated return
|
|
315
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
316
|
+
### Example
|
|
317
|
+
```
|
|
318
|
+
@app.remote.number(
|
|
319
|
+
path = "set_channel",
|
|
320
|
+
ref = "channel",
|
|
321
|
+
vartype = "int",
|
|
322
|
+
min_max = (1, 16),
|
|
323
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
324
|
+
tooltip = "Set a remote channel."
|
|
325
|
+
)
|
|
326
|
+
def set_channel(e: EntityInfo, value: int | float):
|
|
327
|
+
return f'*SET,IDAS,TXCH,IND,{e.serial},{driver.local_id},CH,"{value}"'
|
|
328
|
+
```
|
|
329
|
+
"""
|
|
330
|
+
def number(func: Callable[[EntityInfo, int | float], str]):
|
|
331
|
+
def nfunc(e: EntityInfo, value: int | float):
|
|
332
|
+
if self.verbose: log.info(f"Executed {func(e, value)}")
|
|
333
|
+
return func(e, value)
|
|
334
|
+
self.elements[path] = Element(
|
|
335
|
+
handler = nfunc,
|
|
336
|
+
check_ack = check_ack,
|
|
337
|
+
metadata = NumberMetadata(
|
|
338
|
+
path=path,
|
|
339
|
+
element="number",
|
|
340
|
+
blocking=check_ack != None,
|
|
341
|
+
scope="remote",
|
|
342
|
+
role=role,
|
|
343
|
+
method=method,
|
|
344
|
+
group=group,
|
|
345
|
+
sensible=sensible,
|
|
346
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
347
|
+
tooltip=tooltip,
|
|
348
|
+
ref=ref,
|
|
349
|
+
vartype=vartype,
|
|
350
|
+
min_max=min_max
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
354
|
+
return nfunc
|
|
355
|
+
return number
|
|
356
|
+
|
|
357
|
+
def dropdown(
|
|
358
|
+
self,
|
|
359
|
+
path: str,
|
|
360
|
+
ref: str,
|
|
361
|
+
values: list[str],
|
|
362
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
363
|
+
role: Literal["operator", "admin"] = "operator",
|
|
364
|
+
method: Literal["GET", "POST"] = "POST",
|
|
365
|
+
group: str | None = None,
|
|
366
|
+
sensible: bool = False,
|
|
367
|
+
display_text: str | None = None,
|
|
368
|
+
tooltip: str | None = None
|
|
369
|
+
):
|
|
370
|
+
"""
|
|
371
|
+
Creates a dropdown that restrict the user to the specified options.\n
|
|
372
|
+
### Params
|
|
373
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
374
|
+
ref: Value bound to the dropdown. Check full documentation for more info. [REQUIRED].\n
|
|
375
|
+
values: List of values (str) the dropdown has. [REQUIRED].\n
|
|
376
|
+
role: Determine which role has access to this route.\n
|
|
377
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
378
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
379
|
+
group: Specify the UI group to which the action belongs.\n
|
|
380
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
381
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
382
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
383
|
+
### Decorated params
|
|
384
|
+
e: Snapshot of the targetted EntityInfo from the Core at request time.\n
|
|
385
|
+
value: Value sent.\n
|
|
386
|
+
### Decorated return
|
|
387
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
388
|
+
### Example
|
|
389
|
+
```
|
|
390
|
+
@app.remote.dropdown(
|
|
391
|
+
path = "set_channel",
|
|
392
|
+
ref = "channel",
|
|
393
|
+
values = ["2", "8", "24"]
|
|
394
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
395
|
+
tooltip = "Set a remote channel."
|
|
396
|
+
)
|
|
397
|
+
def set_channel(e: EntityInfo, value: str):
|
|
398
|
+
return f'*SET,IDAS,TXCH,IND,{e.serial},{driver.local_id},CH,"{value}"'
|
|
399
|
+
```
|
|
400
|
+
"""
|
|
401
|
+
def dropdown(func: Callable[[EntityInfo, str], str]):
|
|
402
|
+
def nfunc(e: EntityInfo, value: str):
|
|
403
|
+
if self.verbose: log.info(f"Executed {func(e, value)}")
|
|
404
|
+
return func(e, value)
|
|
405
|
+
self.elements[path] = Element(
|
|
406
|
+
handler = nfunc,
|
|
407
|
+
check_ack = check_ack,
|
|
408
|
+
metadata = DropdownMetadata(
|
|
409
|
+
path=path,
|
|
410
|
+
element="dropdown",
|
|
411
|
+
blocking=check_ack != None,
|
|
412
|
+
scope="remote",
|
|
413
|
+
role=role,
|
|
414
|
+
method=method,
|
|
415
|
+
group=group,
|
|
416
|
+
sensible=sensible,
|
|
417
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
418
|
+
tooltip=tooltip,
|
|
419
|
+
ref=ref,
|
|
420
|
+
values=values
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
424
|
+
return nfunc
|
|
425
|
+
return dropdown
|
|
426
|
+
|
|
427
|
+
class LocalElements():
|
|
428
|
+
def __init__(self, verbose: bool = False):
|
|
429
|
+
self.verbose = verbose
|
|
430
|
+
self.elements: dict[str, Element] = {}
|
|
431
|
+
|
|
432
|
+
def list_elements(self):
|
|
433
|
+
elements: list[GeneralMetadata | type[GeneralMetadata]]= []
|
|
434
|
+
for e in self.elements.values():
|
|
435
|
+
elements.append(e.metadata)
|
|
436
|
+
return elements
|
|
437
|
+
|
|
438
|
+
# Decorators definitions
|
|
439
|
+
def button(
|
|
440
|
+
self,
|
|
441
|
+
path: str,
|
|
442
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
443
|
+
role: Literal["operator", "admin"] = "operator",
|
|
444
|
+
method: Literal["GET", "POST"] = "POST",
|
|
445
|
+
group: str | None = None,
|
|
446
|
+
sensible: bool = False,
|
|
447
|
+
display_text: str | None = None,
|
|
448
|
+
tooltip: str | None = None
|
|
449
|
+
):
|
|
450
|
+
"""
|
|
451
|
+
Creates a button to execute serial commands.\n
|
|
452
|
+
### Params
|
|
453
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
454
|
+
role: Determine which role has access to this route.\n
|
|
455
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
456
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
457
|
+
group: Specify the UI group to which the action belongs.\n
|
|
458
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
459
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
460
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
461
|
+
### Decorated params
|
|
462
|
+
None
|
|
463
|
+
### Decorated return
|
|
464
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
465
|
+
### Example
|
|
466
|
+
```
|
|
467
|
+
@app.local.button(
|
|
468
|
+
path = "get_channel",
|
|
469
|
+
group = "Basic actions",
|
|
470
|
+
tooltip = "Ask for the channel."
|
|
471
|
+
)
|
|
472
|
+
def get_id():
|
|
473
|
+
return '*GET,MCH,SEL'
|
|
474
|
+
```
|
|
475
|
+
"""
|
|
476
|
+
def button(func: Callable[[], str]):
|
|
477
|
+
def nfunc():
|
|
478
|
+
if self.verbose: log.info(f"Executed {func()}")
|
|
479
|
+
return func()
|
|
480
|
+
self.elements[path] = Element(
|
|
481
|
+
handler = nfunc,
|
|
482
|
+
check_ack = check_ack,
|
|
483
|
+
metadata = GeneralMetadata(
|
|
484
|
+
path=path,
|
|
485
|
+
element="button",
|
|
486
|
+
blocking=check_ack != None,
|
|
487
|
+
scope="local",
|
|
488
|
+
role=role,
|
|
489
|
+
method=method,
|
|
490
|
+
group=group,
|
|
491
|
+
sensible=sensible,
|
|
492
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
493
|
+
tooltip=tooltip
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
497
|
+
return nfunc
|
|
498
|
+
return button
|
|
499
|
+
|
|
500
|
+
def toggle(
|
|
501
|
+
self,
|
|
502
|
+
path: str,
|
|
503
|
+
ref: str,
|
|
504
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
505
|
+
role: Literal["operator", "admin"] = "operator",
|
|
506
|
+
method: Literal["GET", "POST"] = "POST",
|
|
507
|
+
group: str | None = None,
|
|
508
|
+
sensible: bool = False,
|
|
509
|
+
display_text: str | None = None,
|
|
510
|
+
tooltip: str | None = None,
|
|
511
|
+
colors: tuple[str, str] | None = None
|
|
512
|
+
):
|
|
513
|
+
"""
|
|
514
|
+
Creates a toggle button to execute serial commands.\n
|
|
515
|
+
### Params
|
|
516
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
517
|
+
ref: Value bound to the toggle's state. Check full documentation for more info. [REQUIRED]\n
|
|
518
|
+
role: Determine which role has access to this route.\n
|
|
519
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
520
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
521
|
+
group: Specify the UI group to which the action belongs.\n
|
|
522
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
523
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
524
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
525
|
+
colors: HEX values for the button's off/on colors respectively. If None, the Core will default to red/green.\n
|
|
526
|
+
### Decorated params
|
|
527
|
+
state: State of the *ref* from the Core at request time.\n
|
|
528
|
+
### Decorated return
|
|
529
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
530
|
+
### Example
|
|
531
|
+
```
|
|
532
|
+
@app.local.toggle(
|
|
533
|
+
path = "ptt",
|
|
534
|
+
ref = "is_ptt_on",
|
|
535
|
+
group = "Basic actions",
|
|
536
|
+
tooltip = "Toggle the PTT."
|
|
537
|
+
)
|
|
538
|
+
def toggle_ptt(state: bool):
|
|
539
|
+
if state:
|
|
540
|
+
return f'*SET,PTT,OFF'
|
|
541
|
+
else:
|
|
542
|
+
return f'*SET,PTT,ON'
|
|
543
|
+
```
|
|
544
|
+
"""
|
|
545
|
+
def toggle(func: Callable[[bool], str]):
|
|
546
|
+
def nfunc(state: bool):
|
|
547
|
+
if self.verbose: log.info(f"Executed {func(state)}")
|
|
548
|
+
return func(state)
|
|
549
|
+
self.elements[path] = Element(
|
|
550
|
+
handler = nfunc,
|
|
551
|
+
check_ack = check_ack,
|
|
552
|
+
metadata = ToggleMetadata(
|
|
553
|
+
path=path,
|
|
554
|
+
element="toggle",
|
|
555
|
+
blocking=check_ack != None,
|
|
556
|
+
scope="local",
|
|
557
|
+
role=role,
|
|
558
|
+
method=method,
|
|
559
|
+
group=group,
|
|
560
|
+
sensible=sensible,
|
|
561
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
562
|
+
tooltip=tooltip,
|
|
563
|
+
ref=ref,
|
|
564
|
+
colors=colors
|
|
565
|
+
)
|
|
566
|
+
)
|
|
567
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
568
|
+
return nfunc
|
|
569
|
+
return toggle
|
|
570
|
+
|
|
571
|
+
def chatbox(
|
|
572
|
+
self,
|
|
573
|
+
path: str,
|
|
574
|
+
ref: str = "general",
|
|
575
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
576
|
+
role: Literal["operator", "admin"] = "operator",
|
|
577
|
+
method: Literal["GET", "POST"] = "POST",
|
|
578
|
+
group: str | None = None,
|
|
579
|
+
sensible: bool = False,
|
|
580
|
+
display_text: str | None = None,
|
|
581
|
+
tooltip: str | None = None
|
|
582
|
+
):
|
|
583
|
+
"""
|
|
584
|
+
Creates a chatbox (input and history) to handle text messages. You can specify a *ref* to filter where the messages come from/go to if your device supports multiple "channel-like" messaging.\n
|
|
585
|
+
### Params
|
|
586
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
587
|
+
ref: Value bound to the origin/destination of the messages. Check full documentation for more info.\n
|
|
588
|
+
role: Determine which role has access to this route.\n
|
|
589
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
590
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
591
|
+
group: Specify the UI group to which the action belongs.\n
|
|
592
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
593
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
594
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
595
|
+
### Decorated params
|
|
596
|
+
msg: Message to send.\n
|
|
597
|
+
### Decorated return
|
|
598
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
599
|
+
### Example
|
|
600
|
+
```
|
|
601
|
+
@app.local.chatbox(
|
|
602
|
+
path = "send_command",
|
|
603
|
+
check_ack = lambda x: "ACK,OK" in x or "ACK,NG" in x,
|
|
604
|
+
tooltip = "Send a command to the local radio."
|
|
605
|
+
)
|
|
606
|
+
def send_command(msg: str):
|
|
607
|
+
return msg
|
|
608
|
+
```
|
|
609
|
+
"""
|
|
610
|
+
def chatbox(func: Callable[[str], str]):
|
|
611
|
+
def nfunc(msg: str):
|
|
612
|
+
if self.verbose: log.info(f"Executed {func(msg)}")
|
|
613
|
+
return func(msg)
|
|
614
|
+
self.elements[path] = Element(
|
|
615
|
+
handler = nfunc,
|
|
616
|
+
check_ack = check_ack,
|
|
617
|
+
metadata = ChatboxMetadata(
|
|
618
|
+
path=path,
|
|
619
|
+
element="chatbox",
|
|
620
|
+
blocking=check_ack != None,
|
|
621
|
+
scope="local",
|
|
622
|
+
role=role,
|
|
623
|
+
method=method,
|
|
624
|
+
group=group,
|
|
625
|
+
sensible=sensible,
|
|
626
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
627
|
+
tooltip=tooltip,
|
|
628
|
+
ref=ref
|
|
629
|
+
)
|
|
630
|
+
)
|
|
631
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
632
|
+
return nfunc
|
|
633
|
+
return chatbox
|
|
634
|
+
|
|
635
|
+
def text(
|
|
636
|
+
self,
|
|
637
|
+
path: str,
|
|
638
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
639
|
+
role: Literal["operator", "admin"] = "operator",
|
|
640
|
+
method: Literal["GET", "POST"] = "POST",
|
|
641
|
+
group: str | None = None,
|
|
642
|
+
sensible: bool = False,
|
|
643
|
+
display_text: str | None = None,
|
|
644
|
+
tooltip: str | None = None
|
|
645
|
+
):
|
|
646
|
+
"""
|
|
647
|
+
Creates a text input to send texts to a remote entity.\n
|
|
648
|
+
For managing messages with history, you'd want to use a Chatbox instead.\n
|
|
649
|
+
### Params
|
|
650
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
651
|
+
role: Determine which role has access to this route.\n
|
|
652
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
653
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
654
|
+
group: Specify the UI group to which the action belongs.\n
|
|
655
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
656
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
657
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
658
|
+
### Decorated params
|
|
659
|
+
msg: Message to send.\n
|
|
660
|
+
### Decorated return
|
|
661
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
662
|
+
### Example
|
|
663
|
+
```
|
|
664
|
+
@app.local.text(
|
|
665
|
+
path = "set_ui_text",
|
|
666
|
+
tooltip = "Set the radio UI text."
|
|
667
|
+
)
|
|
668
|
+
def set_ui_text(msg: str):
|
|
669
|
+
return f'*SET,UI,TEXT,{msg}'
|
|
670
|
+
```
|
|
671
|
+
"""
|
|
672
|
+
def text(func: Callable[[str], str]):
|
|
673
|
+
def nfunc(msg: str):
|
|
674
|
+
if self.verbose: log.info(f"Executed {func(msg)}")
|
|
675
|
+
return func(msg)
|
|
676
|
+
self.elements[path] = Element(
|
|
677
|
+
handler = nfunc,
|
|
678
|
+
check_ack = check_ack,
|
|
679
|
+
metadata = GeneralMetadata(
|
|
680
|
+
path=path,
|
|
681
|
+
element="text",
|
|
682
|
+
blocking=check_ack != None,
|
|
683
|
+
scope="local",
|
|
684
|
+
role=role,
|
|
685
|
+
method=method,
|
|
686
|
+
group=group,
|
|
687
|
+
sensible=sensible,
|
|
688
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
689
|
+
tooltip=tooltip,
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
693
|
+
return nfunc
|
|
694
|
+
return text
|
|
695
|
+
|
|
696
|
+
def number(
|
|
697
|
+
self,
|
|
698
|
+
path: str,
|
|
699
|
+
ref: str,
|
|
700
|
+
vartype: Literal["int", "float"],
|
|
701
|
+
min_max: tuple[int | float, int | float] | None = None,
|
|
702
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
703
|
+
role: Literal["operator", "admin"] = "operator",
|
|
704
|
+
method: Literal["GET", "POST"] = "POST",
|
|
705
|
+
group: str | None = None,
|
|
706
|
+
sensible: bool = False,
|
|
707
|
+
display_text: str | None = None,
|
|
708
|
+
tooltip: str | None = None
|
|
709
|
+
):
|
|
710
|
+
"""
|
|
711
|
+
Creates a number input to send numeric values.\n
|
|
712
|
+
Can be either int or float.\n
|
|
713
|
+
### Params
|
|
714
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
715
|
+
ref: Value bound to the number input. Check full documentation for more info. [REQUIRED].\n
|
|
716
|
+
vartype: Specify whether it's an int or a float. [REQUIRED].\n
|
|
717
|
+
min_max: Minimum and maximum values (respectively) for the number input.\n
|
|
718
|
+
role: Determine which role has access to this route.\n
|
|
719
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
720
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
721
|
+
group: Specify the UI group to which the action belongs.\n
|
|
722
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
723
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
724
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
725
|
+
### Decorated params
|
|
726
|
+
value: Value sent.\n
|
|
727
|
+
### Decorated return
|
|
728
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
729
|
+
### Example
|
|
730
|
+
```
|
|
731
|
+
@app.local.number(
|
|
732
|
+
path = "set_channel",
|
|
733
|
+
ref = "channel",
|
|
734
|
+
vartype = "int",
|
|
735
|
+
min_max = (1, 16),
|
|
736
|
+
tooltip = "Set the local channel."
|
|
737
|
+
)
|
|
738
|
+
def set_channel(value: int | float):
|
|
739
|
+
return f'*SET,MCH,SEL,{value}'
|
|
740
|
+
```
|
|
741
|
+
"""
|
|
742
|
+
def number(func: Callable[[int | float], str]):
|
|
743
|
+
def nfunc(value: int | float):
|
|
744
|
+
if self.verbose: log.info(f"Executed {func(value)}")
|
|
745
|
+
return func(value)
|
|
746
|
+
self.elements[path] = Element(
|
|
747
|
+
handler = nfunc,
|
|
748
|
+
check_ack = check_ack,
|
|
749
|
+
metadata = NumberMetadata(
|
|
750
|
+
path=path,
|
|
751
|
+
element="number",
|
|
752
|
+
blocking=check_ack != None,
|
|
753
|
+
scope="local",
|
|
754
|
+
role=role,
|
|
755
|
+
method=method,
|
|
756
|
+
group=group,
|
|
757
|
+
sensible=sensible,
|
|
758
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
759
|
+
tooltip=tooltip,
|
|
760
|
+
ref=ref,
|
|
761
|
+
vartype=vartype,
|
|
762
|
+
min_max=min_max
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
766
|
+
return nfunc
|
|
767
|
+
return number
|
|
768
|
+
|
|
769
|
+
def dropdown(
|
|
770
|
+
self,
|
|
771
|
+
path: str,
|
|
772
|
+
ref: str,
|
|
773
|
+
values: list[str],
|
|
774
|
+
check_ack: Callable[[str], bool] | None = None,
|
|
775
|
+
role: Literal["operator", "admin"] = "operator",
|
|
776
|
+
method: Literal["GET", "POST"] = "POST",
|
|
777
|
+
group: str | None = None,
|
|
778
|
+
sensible: bool = False,
|
|
779
|
+
display_text: str | None = None,
|
|
780
|
+
tooltip: str | None = None
|
|
781
|
+
):
|
|
782
|
+
"""
|
|
783
|
+
Creates a dropdown that restrict the user to specific options.\n
|
|
784
|
+
### Params
|
|
785
|
+
path: Name of the endpoint. Will then be accessible through `/action/{path}`. [REQUIRED].\n
|
|
786
|
+
ref: Value bound to the dropdown. Check full documentation for more info. [REQUIRED].\n
|
|
787
|
+
values: List of values (str) the dropdown has. [REQUIRED].\n
|
|
788
|
+
role: Determine which role has access to this route.\n
|
|
789
|
+
method: Define the HTTP method to which the API binds the action.\n
|
|
790
|
+
check_ack: If specified, the SerialBridge will queue every other outgoing commands until the condition is fullfilled. Takes the read line as param.\n
|
|
791
|
+
group: Specify the UI group to which the action belongs.\n
|
|
792
|
+
sensible: Specify whether the action needs confirmation before executing.\n
|
|
793
|
+
display_text: Name to display on the UI. If None, capitalize the *label* and replace every "_" with whitespaces.\n
|
|
794
|
+
tooltip: Specify the on_hover tooltip.\n
|
|
795
|
+
### Decorated params
|
|
796
|
+
value: Value sent.\n
|
|
797
|
+
### Decorated return
|
|
798
|
+
Your Serial command (as a *string*) for the specific *action*.
|
|
799
|
+
### Example
|
|
800
|
+
```
|
|
801
|
+
@app.local.dropdown(
|
|
802
|
+
path = "set_channel",
|
|
803
|
+
ref = "channel",
|
|
804
|
+
values = ["2", "8", "24"],
|
|
805
|
+
tooltip = "Set the local channel."
|
|
806
|
+
)
|
|
807
|
+
def set_channel(value: str):
|
|
808
|
+
return f'*SET,MCH,SEL,{value}'
|
|
809
|
+
```
|
|
810
|
+
"""
|
|
811
|
+
def dropdown(func: Callable[[str], str]):
|
|
812
|
+
def nfunc(value: str):
|
|
813
|
+
if self.verbose: log.info(f"Executed {func(value)}")
|
|
814
|
+
return func(value)
|
|
815
|
+
self.elements[path] = Element(
|
|
816
|
+
handler = nfunc,
|
|
817
|
+
check_ack = check_ack,
|
|
818
|
+
metadata = DropdownMetadata(
|
|
819
|
+
path=path,
|
|
820
|
+
element="dropdown",
|
|
821
|
+
blocking=check_ack != None,
|
|
822
|
+
scope="local",
|
|
823
|
+
role=role,
|
|
824
|
+
method=method,
|
|
825
|
+
group=group,
|
|
826
|
+
sensible=sensible,
|
|
827
|
+
display_text=display_text if display_text else path.replace("_", " ").capitalize(),
|
|
828
|
+
tooltip=tooltip,
|
|
829
|
+
ref=ref,
|
|
830
|
+
values=values
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
if self.verbose: log.info(f"{path} bound.")
|
|
834
|
+
return nfunc
|
|
835
|
+
return dropdown
|