Nyx Node
Loading...
Searching...
No Matches
node.py
1# -*- coding: utf-8 -*-
2########################################################################################################################
3# NyxNode
4# Author: Jérôme ODIER <jerome.odier@lpsc.in2p3.fr>
5# SPDX-License-Identifier: GPL-3.0+
6########################################################################################################################
7
8import enum
9import ctypes
10import typing
11import argparse
12
13########################################################################################################################
14
15from . import bind
16from . import json
17
18########################################################################################################################
19
20## @defgroup NODE_PY Nyx node
21# @brief Nyx node.
22
23########################################################################################################################
24
25class NyxMQTTEvent(enum.IntEnum):
26 """!
27 @ingroup NODE_PY
28 @brief MQTT event type.
29
30 | Symbol | Value | Description |
31 | :------------------ | :---- | :---------------------- |
32 | `NyxMQTTEvent.OPEN` | 1100 | A connection is opened. |
33 | `NyxMQTTEvent.MSG` | 1101 | A message is received. |
34 """
35
36 OPEN = 1100
37 MSG = 1101
38
39########################################################################################################################
40
41class NyxNode:
42 """!
43 @ingroup NODE_PY
44 @brief Nyx node exposing INDI, MQTT and Nyx Stream endpoints.
45 """
46
47 ####################################################################################################################
48
50 self,
51 node_id: str,
52 vectors: typing.List[json.NyxDict],
53 indi_url: str | None,
54 mqtt_url: str | None,
55 nss_url: str | None,
56 mqtt_username: str | None,
57 mqtt_password: str | None,
58 retry_ms: int,
59 enable_xml: bool,
60 args: argparse.Namespace | None = None
61 ):
62 """!
63 @brief Allocates and initializes a Nyx node.
64
65 @param node_id Unique node identifier.
66 @param vectors Array of vectors.
67 @param indi_url Optional INDI URL.
68 @param mqtt_url Optional MQTT URL.
69 @param nss_url Optional Nyx Stream URL.
70 @param mqtt_username Optional MQTT username.
71 @param mqtt_password Optional MQTT password.
72 @param retry_ms Connect retry time [milliseconds].
73 @param enable_xml Enables the XML messages for INDI compatibility.
74 @param args Optional command-line arguments.
75 """
76
77 ################################################################################################################
78
79 self._vectors = tuple(vectors)
80
81 ################################################################################################################
82
83 self._mqtt_open_handlers = []
84 self._mqtt_msg_handlers = []
85 self._timer_contexts = []
86
87 ################################################################################################################
88
89 self._mqtt_callback = bind.nyx_mqtt_handler_t(self._on_mqtt)
90
91 ################################################################################################################
92
93 # noinspection PyCallingNonCallable
94 self._vectors_ptr = (bind.nyx_dict_p * (len(self._vectors) + 1))()
95
96 ################################################################################################################
97
98 for i, vector in enumerate(self._vectors):
99
100 if not isinstance(vector, json.NyxDict):
101
102 raise TypeError('Expected Nyx Dict object')
103
104 self._vectors_ptr[i] = ctypes.cast(vector.ptr, bind.nyx_dict_p)
105
106 self._vectors_ptr[-1] = bind.nyx_dict_p()
107
108 ################################################################################################################
109
110 self._ptr = bind.lib.nyx_node_initialize(
111 bind.as_bytes(node_id, allow_none = False),
112 self._vectors_ptr,
113 bind.as_bytes(indi_url, allow_none = True),
114 bind.as_bytes(mqtt_url, allow_none = True),
115 bind.as_bytes(nss_url, allow_none = True),
116 bind.as_bytes(mqtt_username, allow_none = True),
117 bind.as_bytes(mqtt_password, allow_none = True),
118 self._mqtt_callback,
119 retry_ms,
120 enable_xml,
121 )
122
123 ################################################################################################################
124
125 self._args = args
126
127 ####################################################################################################################
128
129 @property
130 def ptr(self):
131 """!
132 @private
133 @brief C pointer to the Nyx Node object.
134 """
135
136 return self._ptr
137
138 ####################################################################################################################
139
140 @property
141 def args(self) -> argparse.Namespace | None:
142 """!
143 @brief Optional command-line arguments provided in the constructor.
144 """
145
146 return self._args
147
148 ####################################################################################################################
149
150 def _on_mqtt(
151 self,
152 _,
153 event_type: int,
154 topic_size: int,
155 topic_buff: bind.c_void_p,
156 message_size: int,
157 message_buff: bind.c_void_p,
158 ) -> None:
159
160 ################################################################################################################
161
162 event_type = NyxMQTTEvent(event_type)
163
164 ################################################################################################################
165 # OPEN EVENT #
166 ################################################################################################################
167
168 if event_type == NyxMQTTEvent.OPEN:
169
170 callbacks = tuple(self._mqtt_open_handlers)
171
172 for callback in callbacks:
173
174 callback()
175
176 ################################################################################################################
177 # MSG EVENT #
178 ################################################################################################################
179
180 elif event_type == NyxMQTTEvent.MSG:
181
182 callbacks = tuple(self._mqtt_msg_handlers)
183
184 if callbacks:
185
186 topic = ctypes.string_at(topic_buff, topic_size) if topic_size else b''
187 message = ctypes.string_at(message_buff, message_size) if message_size else b''
188
189 topic = topic.decode('utf-8')
190
191 for callback in callbacks:
192
193 callback(topic, message)
194
195 ####################################################################################################################
196
197 @staticmethod
198 @bind.nyx_timer_callback_t
199 def _on_timer(arg: bind.c_void_p) -> None:
200
201 ctypes.cast(arg, ctypes.POINTER(ctypes.py_object)).contents.value()
202
203 ####################################################################################################################
204
205 def on_mqtt(self, event_type: NyxMQTTEvent) -> typing.Callable:
206 """!
207 @brief Registers an MQTT event handler.
208
209 @param event_type MQTT event type.
210 @return A decorator registering the MQTT event handler.
211
212 @code{.py}
213 with nyx.NyxNode(...) as node:
214
215 @node.on_mqtt(nyx.NyxMQTTEvent.OPEN)
216 def on_mqtt_open():
217 ...
218
219 @node.on_mqtt(nyx.NyxMQTTEvent.MSG)
220 def on_mqtt_msg(topic, message):
221 ...
222 @endcode
223 """
224
225 ################################################################################################################
226
227 if not isinstance(event_type, NyxMQTTEvent):
228
229 raise TypeError('Expected NyxMQTTEvent enum')
230
231 ################################################################################################################
232
233 def decorate(callback: typing.Callable) -> typing.Callable:
234
235 if not callable(callback):
236
237 raise TypeError('Expected a callable MQTT handler')
238
239 ############################################################################################################
240
241 if event_type == NyxMQTTEvent.OPEN:
242
243 self._mqtt_open_handlers.append(callback)
244
245 elif event_type == NyxMQTTEvent.MSG:
246
247 self._mqtt_msg_handlers.append(callback)
248
249 else:
250
251 raise ValueError(f'Unsupported MQTT event: {event_type!r}')
252
253 ############################################################################################################
254
255 return callback
256
257 ################################################################################################################
258
259 return decorate
260
261 ####################################################################################################################
262
263 def on_timer(self, interval_ms: int):
264 """!
265 @brief Registers a timer handler.
266
267 @param interval_ms Interval [milliseconds].
268 @return A decorator registering the timer handler.
269
270 @note Timers are triggered by the @ref nyx.node.NyxNode.poll method.
271
272 @code{.py}
273 with nyx.NyxNode(...) as node:
274
275 @node.on_timer(50)
276 def on_timer():
277 ...
278 @endcode
279 """
280
281 ################################################################################################################
282
283 if not isinstance(interval_ms, int):
284
285 raise TypeError('Expected timer interval in milliseconds')
286
287 if interval_ms < 1:
288
289 raise ValueError('Timer interval must be positive')
290
291 ################################################################################################################
292
293 def decorate(callback: typing.Callable) -> typing.Callable:
294
295 if not callable(callback):
296
297 raise TypeError('Expected a callable timer handler')
298
299 ############################################################################################################
300
301 timer_context = ctypes.py_object(callback)
302
303 self._timer_contexts.append(timer_context)
304
305 ############################################################################################################
306
307 bind.lib.nyx_node_add_timer(
308 self.ptr,
309 interval_ms,
310 type(self)._on_timer,
311 bind.c_void_p(ctypes.addressof(timer_context)),
312 )
313
314 ############################################################################################################
315
316 return callback
317
318 ################################################################################################################
319
320 return decorate
321
322 ####################################################################################################################
323
324 def close(self) -> None:
325 """!
326 @brief Finalizes the Nyx node.
327
328 @return None
329 """
330
331 bind.lib.nyx_node_finalize(self.ptr, False)
332
333 ####################################################################################################################
334
335 def poll(self, timeout_ms: int) -> None:
336 """! @brief Performs a single poll iteration.
337
338 @param timeout_ms Timeout [milliseconds].
339 @return None
340
341 @note @c timeout_ms determines the minimum timer resolution.
342 """
343
344 bind.lib.nyx_node_poll(self.ptr, timeout_ms)
345
346 ####################################################################################################################
347
348 def enable(self, device: str, name: str | None = None, message: str | None = None) -> None:
349 """!
350 @brief Enables a device or a vector and notifies clients.
351
352 @param device Device name.
353 @param name Optional vector name (`None` means the whole device).
354 @param message Optional human-oriented message.
355 @return None
356 """
357
358 bind.lib.nyx_node_enable(
359 self.ptr,
360 bind.as_bytes(device, allow_none = False),
361 bind.as_bytes(name, allow_none = True),
362 bind.as_bytes(message, allow_none = True)
363 )
364
365 ####################################################################################################################
366
367 def disable(self, device: str, name: str | None = None, message: str | None = None) -> None:
368 """!
369 @brief Disables a device or a vector and notifies clients.
370
371 @param device Device name.
372 @param name Optional vector name (`None` means the whole device).
373 @param message Optional human-oriented message.
374 @return None
375 """
376
377 bind.lib.nyx_node_disable(
378 self.ptr,
379 bind.as_bytes(device, allow_none = False),
380 bind.as_bytes(name, allow_none = True),
381 bind.as_bytes(message, allow_none = True)
382 )
383
384 ####################################################################################################################
385
386 def send_message(self, device: str, message: str | None = None) -> None:
387 """!
388 @brief Sends a human-oriented message to the clients.
389
390 @param device Device name.
391 @param message Human-oriented message.
392 @return None
393 """
394
395 bind.lib.nyx_node_send_message(
396 self.ptr,
397 bind.as_bytes(device, allow_none = False),
398 bind.as_bytes(message, allow_none = True)
399 )
400
401 ####################################################################################################################
402
403 def send_del_property(self, device: str, name: str | None = None, message: str | None = None) -> None:
404 """!
405 @brief Sends a `del-property` message to the clients.
406
407 @param device Device name.
408 @param name Optional vector name (`None` means the whole device).
409 @param message Optional human-oriented message.
410 @return None
411 """
412
413 bind.lib.nyx_node_send_del_property(
414 self.ptr,
415 bind.as_bytes(device, allow_none = False),
416 bind.as_bytes(name, allow_none = True),
417 bind.as_bytes(message, allow_none = True)
418 )
419
420 ####################################################################################################################
421
422 def mqtt_sub(self, topic: str, qos: int = 0) -> None:
423 """!
424 @brief If MQTT is enabled, subscribes to an MQTT topic.
425
426 @param topic MQTT topic.
427 @param qos MQTT Quality Of Service.
428 @return None
429
430 @note MQTT handlers have to be added with the @ref nyx.node.NyxNode.on_mqtt decorator.
431 """
432
433 bind.lib.nyx_mqtt_sub(
434 self.ptr,
435 bind.as_bytes(topic, allow_none = False),
436 qos
437 )
438
439 ####################################################################################################################
440
441 def mqtt_pub(self, topic: str, message: bytes, qos: int = 0) -> None:
442 """!
443 @brief If MQTT is enabled, publishes an MQTT message.
444
445 @param topic MQTT topic.
446 @param message Message payload.
447 @param qos MQTT Quality Of Service.
448 @return None
449
450 @note The message payload may contain arbitrary binary data.
451 """
452
453 message = bind.as_bytes(message, allow_none = False)
454
455 bind.lib.nyx_mqtt_pub(
456 self.ptr,
457 bind.as_bytes(topic, allow_none = False),
458 len(message),
459 message,
460 qos
461 )
462
463 ####################################################################################################################
464
465 def __enter__(self):
466
467 return self
468
469 ####################################################################################################################
470
471 def __exit__(self, exc_type, exc_value, traceback):
472
473 self.close()
474
475########################################################################################################################
476
477__all__ = ['NyxMQTTEvent', 'NyxNode']
478
479########################################################################################################################
JSON dict object.
Definition json_dict.py:22
None send_message(self, str device, str|None message=None)
Sends a human-oriented message to the clients.
Definition node.py:386
list _timer_contexts
Definition node.py:85
__init__(self, str node_id, typing.List[json.NyxDict] vectors, str|None indi_url, str|None mqtt_url, str|None nss_url, str|None mqtt_username, str|None mqtt_password, int retry_ms, bool enable_xml, argparse.Namespace|None args=None)
Allocates and initializes a Nyx node.
Definition node.py:61
tuple _vectors_ptr
Definition node.py:94
list _mqtt_open_handlers
Definition node.py:83
None send_del_property(self, str device, str|None name=None, str|None message=None)
Sends a del-property message to the clients.
Definition node.py:403
list _mqtt_msg_handlers
Definition node.py:84
None close(self)
Finalizes the Nyx node.
Definition node.py:324
None poll(self, int timeout_ms)
Performs a single poll iteration.
Definition node.py:335
None enable(self, str device, str|None name=None, str|None message=None)
Enables a device or a vector and notifies clients.
Definition node.py:348
typing.Callable on_mqtt(self, NyxMQTTEvent event_type)
Registers an MQTT event handler.
Definition node.py:205
argparse.Namespace|None _args
Definition node.py:125
None _on_mqtt(self, _, int event_type, int topic_size, bind.c_void_p topic_buff, int message_size, bind.c_void_p message_buff)
Definition node.py:158
None mqtt_sub(self, str topic, int qos=0)
If MQTT is enabled, subscribes to an MQTT topic.
Definition node.py:422
argparse.Namespace|None args(self)
Optional command-line arguments provided in the constructor.
Definition node.py:141
None mqtt_pub(self, str topic, bytes message, int qos=0)
If MQTT is enabled, publishes an MQTT message.
Definition node.py:441
on_timer(self, int interval_ms)
Registers a timer handler.
Definition node.py:263
None disable(self, str device, str|None name=None, str|None message=None)
Disables a device or a vector and notifies clients.
Definition node.py:367
MQTT event type.
Definition node.py:25
Nyx node exposing INDI, MQTT and Nyx Stream endpoints.
Definition node.py:41