marilib-pkg 0.6.0__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.
- examples/frames.py +23 -0
- examples/mari_cloud.py +72 -0
- examples/mari_cloud_minimal.py +38 -0
- examples/mari_edge.py +73 -0
- examples/mari_edge_minimal.py +37 -0
- examples/mari_edge_stats.py +156 -0
- examples/uart.py +35 -0
- marilib/__init__.py +10 -0
- marilib/communication_adapter.py +212 -0
- marilib/latency.py +78 -0
- marilib/logger.py +211 -0
- marilib/mari_protocol.py +76 -0
- marilib/marilib.py +35 -0
- marilib/marilib_cloud.py +193 -0
- marilib/marilib_edge.py +248 -0
- marilib/model.py +393 -0
- marilib/protocol.py +109 -0
- marilib/serial_hdlc.py +228 -0
- marilib/serial_uart.py +84 -0
- marilib/tui.py +13 -0
- marilib/tui_cloud.py +158 -0
- marilib/tui_edge.py +185 -0
- marilib_pkg-0.6.0.dist-info/METADATA +57 -0
- marilib_pkg-0.6.0.dist-info/RECORD +30 -0
- marilib_pkg-0.6.0.dist-info/WHEEL +4 -0
- marilib_pkg-0.6.0.dist-info/licenses/AUTHORS +2 -0
- marilib_pkg-0.6.0.dist-info/licenses/LICENSE +28 -0
- tests/__init__.py +0 -0
- tests/test_hdlc.py +76 -0
- tests/test_protocol.py +35 -0
examples/frames.py
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
"""
|
2
|
+
This script is used to generate a frame to send to a node.
|
3
|
+
|
4
|
+
Usage:
|
5
|
+
python frames.py [destination]
|
6
|
+
|
7
|
+
If no destination is provided, the frame will be sent to the broadcast address.
|
8
|
+
|
9
|
+
Example usage sending to broadcast address via mosquitto_pub:
|
10
|
+
|
11
|
+
while [ 1 ]; do python examples/frames.py | xxd -r -p | base64 | mosquitto_pub -h localhost -p 1883 -t /mari/00A0/to_edge -l; done
|
12
|
+
"""
|
13
|
+
|
14
|
+
from marilib.mari_protocol import Frame, Header, MARI_BROADCAST_ADDRESS
|
15
|
+
from marilib.model import EdgeEvent
|
16
|
+
import sys
|
17
|
+
|
18
|
+
destination = sys.argv[1] if len(sys.argv) > 1 else MARI_BROADCAST_ADDRESS
|
19
|
+
|
20
|
+
header = Header(destination=destination)
|
21
|
+
frame = Frame(header=header, payload=b"NORMAL_APP_DATA")
|
22
|
+
frame_to_send = EdgeEvent.to_bytes(EdgeEvent.NODE_DATA) + frame.to_bytes()
|
23
|
+
print(frame_to_send.hex())
|
examples/mari_cloud.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import click
|
4
|
+
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, MARI_NET_ID_DEFAULT, Frame
|
5
|
+
from marilib.marilib_cloud import MarilibCloud
|
6
|
+
from marilib.model import EdgeEvent, GatewayInfo, MariNode
|
7
|
+
from marilib.communication_adapter import MQTTAdapter
|
8
|
+
from marilib.tui_cloud import MarilibTUICloud
|
9
|
+
from marilib.logger import MetricsLogger
|
10
|
+
|
11
|
+
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
12
|
+
|
13
|
+
|
14
|
+
def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
15
|
+
"""An event handler for the application."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
|
19
|
+
@click.command()
|
20
|
+
@click.option(
|
21
|
+
"--mqtt-url",
|
22
|
+
"-m",
|
23
|
+
type=str,
|
24
|
+
default="mqtt://localhost:1883",
|
25
|
+
show_default=True,
|
26
|
+
help="MQTT broker to use",
|
27
|
+
)
|
28
|
+
@click.option(
|
29
|
+
"--network-id",
|
30
|
+
"-n",
|
31
|
+
type=lambda x: int(x, 16),
|
32
|
+
default=MARI_NET_ID_DEFAULT,
|
33
|
+
help=f"Network ID to use [default: 0x{MARI_NET_ID_DEFAULT:04X}]",
|
34
|
+
)
|
35
|
+
@click.option(
|
36
|
+
"--log-dir",
|
37
|
+
default="logs",
|
38
|
+
show_default=True,
|
39
|
+
help="Directory to save metric log files.",
|
40
|
+
type=click.Path(),
|
41
|
+
)
|
42
|
+
def main(mqtt_url: str, network_id: int, log_dir: str):
|
43
|
+
"""A basic example of using the MariLibCloud library."""
|
44
|
+
|
45
|
+
mari = MarilibCloud(
|
46
|
+
on_event,
|
47
|
+
mqtt_interface=MQTTAdapter.from_url(mqtt_url, is_edge=False),
|
48
|
+
logger=MetricsLogger(
|
49
|
+
log_dir_base=log_dir, rotation_interval_minutes=1440, log_interval_seconds=1.0
|
50
|
+
),
|
51
|
+
network_id=network_id,
|
52
|
+
tui=MarilibTUICloud(),
|
53
|
+
main_file=__file__,
|
54
|
+
)
|
55
|
+
|
56
|
+
try:
|
57
|
+
while True:
|
58
|
+
mari.update()
|
59
|
+
if mari.nodes:
|
60
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, NORMAL_DATA_PAYLOAD)
|
61
|
+
mari.render_tui()
|
62
|
+
time.sleep(0.5)
|
63
|
+
|
64
|
+
except KeyboardInterrupt:
|
65
|
+
pass
|
66
|
+
finally:
|
67
|
+
mari.close_tui()
|
68
|
+
mari.logger.close()
|
69
|
+
|
70
|
+
|
71
|
+
if __name__ == "__main__":
|
72
|
+
main()
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import time
|
2
|
+
import sys
|
3
|
+
|
4
|
+
from marilib.mari_protocol import MARI_NET_ID_DEFAULT
|
5
|
+
from marilib.marilib_cloud import MarilibCloud
|
6
|
+
from marilib.communication_adapter import MQTTAdapter
|
7
|
+
from marilib.model import EdgeEvent
|
8
|
+
|
9
|
+
|
10
|
+
def on_event(event, event_data):
|
11
|
+
"""An event handler for the application."""
|
12
|
+
if event == EdgeEvent.GATEWAY_INFO:
|
13
|
+
return
|
14
|
+
print(".", end="", flush=True)
|
15
|
+
|
16
|
+
|
17
|
+
def main():
|
18
|
+
mari_cloud = MarilibCloud(
|
19
|
+
on_event,
|
20
|
+
mqtt_interface=MQTTAdapter("localhost", 1883, is_edge=False),
|
21
|
+
network_id=int(sys.argv[1], 16) if len(sys.argv) > 1 else MARI_NET_ID_DEFAULT,
|
22
|
+
)
|
23
|
+
|
24
|
+
while True:
|
25
|
+
mari_cloud.update()
|
26
|
+
for node in mari_cloud.nodes:
|
27
|
+
# print(f"Sending frame to {node.address:016X}")
|
28
|
+
mari_cloud.send_frame(dst=node.address, payload=b"NORMAL_APP_DATA")
|
29
|
+
print(",", end="", flush=True)
|
30
|
+
statistics = [
|
31
|
+
(f"{node.address:016X}", node.stats.received_rssi_dbm()) for node in mari_cloud.nodes
|
32
|
+
]
|
33
|
+
print(f"Network statistics: {statistics}")
|
34
|
+
time.sleep(1)
|
35
|
+
|
36
|
+
|
37
|
+
if __name__ == "__main__":
|
38
|
+
main()
|
examples/mari_edge.py
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import click
|
4
|
+
from marilib.logger import MetricsLogger
|
5
|
+
from marilib.mari_protocol import Frame, MARI_BROADCAST_ADDRESS
|
6
|
+
from marilib.model import EdgeEvent, MariNode
|
7
|
+
from marilib.communication_adapter import SerialAdapter, MQTTAdapter
|
8
|
+
from marilib.serial_uart import get_default_port
|
9
|
+
from marilib.tui_edge import MarilibTUIEdge
|
10
|
+
from marilib.marilib_edge import MarilibEdge
|
11
|
+
|
12
|
+
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
13
|
+
|
14
|
+
|
15
|
+
def on_event(event: EdgeEvent, event_data: MariNode | Frame):
|
16
|
+
"""An event handler for the application."""
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
@click.command()
|
21
|
+
@click.option(
|
22
|
+
"--port",
|
23
|
+
"-p",
|
24
|
+
type=str,
|
25
|
+
default=get_default_port(),
|
26
|
+
show_default=True,
|
27
|
+
help="Serial port to use (e.g., /dev/ttyACM0)",
|
28
|
+
)
|
29
|
+
@click.option(
|
30
|
+
"--mqtt-url",
|
31
|
+
"-m",
|
32
|
+
type=str,
|
33
|
+
default="mqtt://localhost:1883",
|
34
|
+
show_default=True,
|
35
|
+
help="MQTT broker to use (default: empty, no cloud)",
|
36
|
+
)
|
37
|
+
@click.option(
|
38
|
+
"--log-dir",
|
39
|
+
default="logs",
|
40
|
+
show_default=True,
|
41
|
+
help="Directory to save metric log files.",
|
42
|
+
type=click.Path(),
|
43
|
+
)
|
44
|
+
def main(port: str | None, mqtt_url: str, log_dir: str):
|
45
|
+
"""A basic example of using the MarilibEdge library."""
|
46
|
+
|
47
|
+
mari = MarilibEdge(
|
48
|
+
on_event,
|
49
|
+
serial_interface=SerialAdapter(port),
|
50
|
+
mqtt_interface=MQTTAdapter.from_url(mqtt_url, is_edge=True) if mqtt_url else None,
|
51
|
+
logger=MetricsLogger(
|
52
|
+
log_dir_base=log_dir, rotation_interval_minutes=1440, log_interval_seconds=1.0
|
53
|
+
),
|
54
|
+
tui=MarilibTUIEdge(),
|
55
|
+
main_file=__file__,
|
56
|
+
)
|
57
|
+
|
58
|
+
try:
|
59
|
+
while True:
|
60
|
+
mari.update()
|
61
|
+
if not mari.uses_mqtt and mari.nodes:
|
62
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, NORMAL_DATA_PAYLOAD)
|
63
|
+
mari.render_tui()
|
64
|
+
time.sleep(0.5)
|
65
|
+
except KeyboardInterrupt:
|
66
|
+
pass
|
67
|
+
finally:
|
68
|
+
mari.close_tui()
|
69
|
+
mari.logger.close()
|
70
|
+
|
71
|
+
|
72
|
+
if __name__ == "__main__":
|
73
|
+
main()
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from marilib.marilib_edge import MarilibEdge, EdgeEvent
|
4
|
+
from marilib.communication_adapter import MQTTAdapter, SerialAdapter
|
5
|
+
from marilib.serial_uart import get_default_port
|
6
|
+
|
7
|
+
|
8
|
+
def on_event(event, event_data):
|
9
|
+
"""An event handler for the application."""
|
10
|
+
if event == EdgeEvent.GATEWAY_INFO:
|
11
|
+
return
|
12
|
+
print(".", end="", flush=True)
|
13
|
+
|
14
|
+
|
15
|
+
def main():
|
16
|
+
mari_edge = MarilibEdge(
|
17
|
+
on_event,
|
18
|
+
serial_interface=SerialAdapter(get_default_port()),
|
19
|
+
mqtt_interface=MQTTAdapter("localhost", 1883, is_edge=True),
|
20
|
+
)
|
21
|
+
|
22
|
+
while True:
|
23
|
+
mari_edge.update()
|
24
|
+
if not mari_edge.uses_mqtt:
|
25
|
+
# only generate frames if not using MQTT
|
26
|
+
for node in mari_edge.nodes:
|
27
|
+
mari_edge.send_frame(dst=node.address, payload=b"NORMAL_APP_DATA")
|
28
|
+
print(",", end="", flush=True)
|
29
|
+
statistics = [
|
30
|
+
(f"{node.address:016X}", node.stats.received_rssi_dbm()) for node in mari_edge.nodes
|
31
|
+
]
|
32
|
+
print(f"Stats: {statistics}")
|
33
|
+
time.sleep(3)
|
34
|
+
|
35
|
+
|
36
|
+
if __name__ == "__main__":
|
37
|
+
main()
|
@@ -0,0 +1,156 @@
|
|
1
|
+
import sys
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
|
5
|
+
import click
|
6
|
+
from marilib.logger import MetricsLogger
|
7
|
+
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, Frame
|
8
|
+
from marilib.marilib_edge import MarilibEdge
|
9
|
+
from marilib.model import EdgeEvent, GatewayInfo, MariNode, TestState
|
10
|
+
from marilib.serial_uart import get_default_port
|
11
|
+
from marilib.tui_edge import MarilibTUIEdge
|
12
|
+
from marilib.communication_adapter import SerialAdapter, MQTTAdapter
|
13
|
+
|
14
|
+
LOAD_PACKET_PAYLOAD = b"L"
|
15
|
+
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
16
|
+
|
17
|
+
|
18
|
+
class LoadTester(threading.Thread):
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
mari: MarilibEdge,
|
22
|
+
test_state: TestState,
|
23
|
+
stop_event: threading.Event,
|
24
|
+
):
|
25
|
+
super().__init__(daemon=True)
|
26
|
+
self.mari = mari
|
27
|
+
self.test_state = test_state
|
28
|
+
self._stop_event = stop_event
|
29
|
+
self.has_rate = False
|
30
|
+
self.delay = None
|
31
|
+
|
32
|
+
def run(self):
|
33
|
+
while not self._stop_event.is_set():
|
34
|
+
# wait for gateway schedule to be available and try to compute rate
|
35
|
+
if not self.has_rate:
|
36
|
+
self.set_rate()
|
37
|
+
if self.delay is None:
|
38
|
+
self._stop_event.wait(0.1) # fixed, waiting for gateway schedule to be available
|
39
|
+
continue
|
40
|
+
|
41
|
+
# once we have rate, send packets at that rate
|
42
|
+
with self.mari.lock:
|
43
|
+
nodes_exist = bool(self.mari.gateway.nodes)
|
44
|
+
|
45
|
+
if nodes_exist:
|
46
|
+
self.mari.send_frame(MARI_BROADCAST_ADDRESS, LOAD_PACKET_PAYLOAD)
|
47
|
+
self._stop_event.wait(self.delay)
|
48
|
+
|
49
|
+
def set_rate(self):
|
50
|
+
if self.test_state.load == 0:
|
51
|
+
return
|
52
|
+
max_rate = self.mari.get_max_downlink_rate()
|
53
|
+
if max_rate == 0:
|
54
|
+
sys.stderr.write("Error computing max rate")
|
55
|
+
return
|
56
|
+
self.test_state.rate = int(max_rate)
|
57
|
+
packets_per_second = max_rate * (self.test_state.load / 100.0)
|
58
|
+
self.delay = 1.0 / packets_per_second if packets_per_second > 0 else float("inf")
|
59
|
+
self.has_rate = True
|
60
|
+
|
61
|
+
|
62
|
+
def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
63
|
+
"""An event handler for the application."""
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
@click.command()
|
68
|
+
@click.option(
|
69
|
+
"--port",
|
70
|
+
"-p",
|
71
|
+
type=str,
|
72
|
+
default=get_default_port(),
|
73
|
+
show_default=True,
|
74
|
+
help="Serial port to use (e.g., /dev/ttyACM0)",
|
75
|
+
)
|
76
|
+
@click.option(
|
77
|
+
"--mqtt-host",
|
78
|
+
"-m",
|
79
|
+
type=str,
|
80
|
+
default="",
|
81
|
+
show_default=True,
|
82
|
+
help="MQTT broker to use (default: empty, no cloud)",
|
83
|
+
)
|
84
|
+
@click.option(
|
85
|
+
"--load",
|
86
|
+
type=int,
|
87
|
+
default=0,
|
88
|
+
show_default=True,
|
89
|
+
help="Load percentage to apply (0–100)",
|
90
|
+
)
|
91
|
+
@click.option(
|
92
|
+
"--log-dir",
|
93
|
+
default="logs",
|
94
|
+
show_default=True,
|
95
|
+
help="Directory to save metric log files.",
|
96
|
+
type=click.Path(),
|
97
|
+
)
|
98
|
+
def main(port: str | None, mqtt_host: str, load: int, log_dir: str):
|
99
|
+
if not (0 <= load <= 100):
|
100
|
+
sys.stderr.write("Error: --load must be between 0 and 100.\n")
|
101
|
+
return
|
102
|
+
|
103
|
+
test_state = TestState(
|
104
|
+
load=load,
|
105
|
+
)
|
106
|
+
|
107
|
+
logger = MetricsLogger(log_dir_base=log_dir, rotation_interval_minutes=1440)
|
108
|
+
|
109
|
+
mari = MarilibEdge(
|
110
|
+
on_event,
|
111
|
+
serial_interface=SerialAdapter(port),
|
112
|
+
mqtt_interface=MQTTAdapter.from_host_port(mqtt_host, is_edge=True) if mqtt_host else None,
|
113
|
+
logger=logger,
|
114
|
+
main_file=__file__,
|
115
|
+
tui=MarilibTUIEdge(test_state=test_state),
|
116
|
+
)
|
117
|
+
|
118
|
+
stop_event = threading.Event()
|
119
|
+
|
120
|
+
mari.latency_test_enable()
|
121
|
+
|
122
|
+
load_tester = LoadTester(mari, test_state, stop_event)
|
123
|
+
if load > 0:
|
124
|
+
load_tester.start()
|
125
|
+
|
126
|
+
try:
|
127
|
+
normal_traffic_interval = 0.5
|
128
|
+
last_normal_send_time = 0
|
129
|
+
|
130
|
+
while not stop_event.is_set():
|
131
|
+
current_time = time.monotonic()
|
132
|
+
|
133
|
+
mari.update()
|
134
|
+
|
135
|
+
mari.render_tui()
|
136
|
+
|
137
|
+
if current_time - last_normal_send_time >= normal_traffic_interval:
|
138
|
+
if mari.nodes:
|
139
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, NORMAL_DATA_PAYLOAD)
|
140
|
+
last_normal_send_time = current_time
|
141
|
+
|
142
|
+
time.sleep(0.1)
|
143
|
+
|
144
|
+
except KeyboardInterrupt:
|
145
|
+
pass
|
146
|
+
finally:
|
147
|
+
stop_event.set()
|
148
|
+
mari.latency_test_disable()
|
149
|
+
if load_tester.is_alive():
|
150
|
+
load_tester.join()
|
151
|
+
mari.close_tui()
|
152
|
+
mari.logger.close()
|
153
|
+
|
154
|
+
|
155
|
+
if __name__ == "__main__":
|
156
|
+
main()
|
examples/uart.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from marilib.serial_hdlc import (
|
4
|
+
HDLCDecodeException,
|
5
|
+
HDLCHandler,
|
6
|
+
HDLCState,
|
7
|
+
hdlc_encode,
|
8
|
+
)
|
9
|
+
from marilib.serial_uart import SerialInterface
|
10
|
+
|
11
|
+
BAUDRATE = 1000000
|
12
|
+
# BAUDRATE = 115200
|
13
|
+
|
14
|
+
hdlc_handler = HDLCHandler()
|
15
|
+
|
16
|
+
|
17
|
+
def on_byte_received(byte):
|
18
|
+
# print(f"Received byte: {byte}")
|
19
|
+
hdlc_handler.handle_byte(byte)
|
20
|
+
if hdlc_handler.state == HDLCState.READY:
|
21
|
+
try:
|
22
|
+
payload = hdlc_handler.payload
|
23
|
+
print(f"Received payload: {payload.hex()}")
|
24
|
+
except HDLCDecodeException as e:
|
25
|
+
print(f"Error decoding payload: {e}")
|
26
|
+
|
27
|
+
|
28
|
+
serial_interface = SerialInterface("/dev/ttyACM0", BAUDRATE, on_byte_received)
|
29
|
+
|
30
|
+
|
31
|
+
while True:
|
32
|
+
time.sleep(1)
|
33
|
+
payload = b"AAA"
|
34
|
+
print(f"Sending payload: {payload.hex()}")
|
35
|
+
serial_interface.write(hdlc_encode(payload))
|
marilib/__init__.py
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
import base64
|
2
|
+
from urllib.parse import urlparse
|
3
|
+
import paho.mqtt.client as mqtt
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from rich import print
|
7
|
+
|
8
|
+
from marilib.serial_hdlc import (
|
9
|
+
HDLCDecodeException,
|
10
|
+
HDLCHandler,
|
11
|
+
HDLCState,
|
12
|
+
hdlc_encode,
|
13
|
+
)
|
14
|
+
from marilib.serial_uart import SerialInterface, SERIAL_DEFAULT_BAUDRATE
|
15
|
+
|
16
|
+
|
17
|
+
class CommunicationAdapterBase(ABC):
|
18
|
+
"""Base class for interface adapters."""
|
19
|
+
|
20
|
+
@abstractmethod
|
21
|
+
def init(self, on_data_received: callable):
|
22
|
+
"""Initialize the interface."""
|
23
|
+
|
24
|
+
@abstractmethod
|
25
|
+
def close(self):
|
26
|
+
"""Close the interface."""
|
27
|
+
|
28
|
+
|
29
|
+
class SerialAdapter(CommunicationAdapterBase):
|
30
|
+
"""Class used to interface with the serial port."""
|
31
|
+
|
32
|
+
def __init__(self, port, baudrate=SERIAL_DEFAULT_BAUDRATE):
|
33
|
+
self.port = port
|
34
|
+
self.baudrate = baudrate
|
35
|
+
self.hdlc_handler = HDLCHandler()
|
36
|
+
|
37
|
+
def on_byte_received(self, byte):
|
38
|
+
self.hdlc_handler.handle_byte(byte)
|
39
|
+
if self.hdlc_handler.state == HDLCState.READY:
|
40
|
+
try:
|
41
|
+
payload = self.hdlc_handler.payload
|
42
|
+
# print(f"Received payload: {payload.hex()}")
|
43
|
+
self.on_data_received(payload)
|
44
|
+
except HDLCDecodeException as e:
|
45
|
+
print(f"Error decoding payload: {e}")
|
46
|
+
|
47
|
+
def init(self, on_data_received: callable):
|
48
|
+
self.on_data_received = on_data_received
|
49
|
+
self.serial = SerialInterface(self.port, self.baudrate, self.on_byte_received)
|
50
|
+
print(f"[yellow]Connected to serial port {self.port} at {self.baudrate} baud[/]")
|
51
|
+
|
52
|
+
def close(self):
|
53
|
+
print("[yellow]Disconnect from gateway...[/]")
|
54
|
+
|
55
|
+
def send_data(self, data):
|
56
|
+
self.serial.serial.flush()
|
57
|
+
encoded = hdlc_encode(data)
|
58
|
+
self.serial.write(encoded)
|
59
|
+
|
60
|
+
|
61
|
+
class MQTTAdapter(CommunicationAdapterBase):
|
62
|
+
"""Class used to interface with MQTT."""
|
63
|
+
|
64
|
+
def __init__(self, host, port, is_edge: bool, use_tls: bool = False):
|
65
|
+
self.host = host
|
66
|
+
self.port = port
|
67
|
+
self.is_edge = is_edge
|
68
|
+
self.network_id = None
|
69
|
+
self.client = None
|
70
|
+
self.on_data_received = None
|
71
|
+
self.use_tls = use_tls
|
72
|
+
# optimize qos for throughput
|
73
|
+
# 0 = no delivery guarantee, 1 = at least once, 2 = exactly once
|
74
|
+
self.qos = 0
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def from_url(cls, url: str, is_edge: bool):
|
78
|
+
url = urlparse(url)
|
79
|
+
host, port = url.netloc.split(":")
|
80
|
+
if url.scheme == "mqtt":
|
81
|
+
return cls(host, int(port), is_edge, use_tls=False)
|
82
|
+
elif url.scheme == "mqtts":
|
83
|
+
return cls(host, int(port), is_edge, use_tls=True)
|
84
|
+
else:
|
85
|
+
raise ValueError(f"Invalid MQTT URL: {url} (must start with mqtt:// or mqtts://)")
|
86
|
+
|
87
|
+
# ==== public methods ====
|
88
|
+
|
89
|
+
def is_ready(self) -> bool:
|
90
|
+
return self.client is not None and self.client.is_connected()
|
91
|
+
|
92
|
+
def set_network_id(self, network_id: str):
|
93
|
+
self.network_id = network_id
|
94
|
+
|
95
|
+
def set_on_data_received(self, on_data_received: callable):
|
96
|
+
self.on_data_received = on_data_received
|
97
|
+
|
98
|
+
def update(self, network_id: str, on_data_received: callable):
|
99
|
+
if self.network_id is None:
|
100
|
+
# might have been set by set_network_id
|
101
|
+
self.network_id = network_id
|
102
|
+
else:
|
103
|
+
# TODO: handle the case when the network_id changes
|
104
|
+
pass
|
105
|
+
if self.network_id is None:
|
106
|
+
# wait a bit, network_id not set yet
|
107
|
+
return
|
108
|
+
if self.on_data_received is None:
|
109
|
+
self.set_on_data_received(on_data_received)
|
110
|
+
if not self.is_ready():
|
111
|
+
self.init()
|
112
|
+
|
113
|
+
def init(self):
|
114
|
+
if self.client:
|
115
|
+
# already initialized, do nothing
|
116
|
+
return
|
117
|
+
if self.network_id is None:
|
118
|
+
# network_id not set yet
|
119
|
+
return
|
120
|
+
|
121
|
+
self.client = mqtt.Client(
|
122
|
+
mqtt.CallbackAPIVersion.VERSION2,
|
123
|
+
protocol=mqtt.MQTTProtocolVersion.MQTTv5,
|
124
|
+
)
|
125
|
+
if self.use_tls:
|
126
|
+
self.client.tls_set_context(context=None)
|
127
|
+
self.client.on_log = self._on_log
|
128
|
+
self.client.on_connect = self._on_connect_edge if self.is_edge else self._on_connect_cloud
|
129
|
+
self.client.on_message = self._on_message_edge if self.is_edge else self._on_message_cloud
|
130
|
+
self.client.connect(self.host, self.port, 60)
|
131
|
+
print(f"[yellow]Connected to MQTT broker on {self.host}:{self.port}[/]")
|
132
|
+
self.client.loop_start()
|
133
|
+
|
134
|
+
def close(self):
|
135
|
+
self.client.disconnect()
|
136
|
+
self.client.loop_stop()
|
137
|
+
|
138
|
+
def send_data_to_edge(self, data):
|
139
|
+
if not self.is_ready():
|
140
|
+
return
|
141
|
+
self.client.publish(
|
142
|
+
f"/mari/{self.network_id}/to_edge",
|
143
|
+
base64.b64encode(data).decode(),
|
144
|
+
qos=self.qos,
|
145
|
+
)
|
146
|
+
|
147
|
+
def send_data_to_cloud(self, data):
|
148
|
+
if not self.is_ready():
|
149
|
+
return
|
150
|
+
self.client.publish(
|
151
|
+
f"/mari/{self.network_id}/to_cloud",
|
152
|
+
base64.b64encode(data).decode(),
|
153
|
+
qos=self.qos,
|
154
|
+
)
|
155
|
+
|
156
|
+
# ==== private methods ====
|
157
|
+
|
158
|
+
# TODO: de-duplicate the _on_message_* functions? decide as the integration evolves
|
159
|
+
def _on_message_edge(self, client, userdata, message):
|
160
|
+
try:
|
161
|
+
data = base64.b64decode(message.payload)
|
162
|
+
except Exception as e:
|
163
|
+
# print the error and a stacktrace
|
164
|
+
print(f"[red]Error decoding MQTT message: {e}[/]")
|
165
|
+
print(f"[red]Message: {message.payload}[/]")
|
166
|
+
return
|
167
|
+
self.on_data_received(data)
|
168
|
+
|
169
|
+
def _on_message_cloud(self, client, userdata, message):
|
170
|
+
try:
|
171
|
+
data = base64.b64decode(message.payload)
|
172
|
+
except Exception as e:
|
173
|
+
# print the error and a stacktrace
|
174
|
+
print(f"[red]Error decoding MQTT message: {e}[/]")
|
175
|
+
print(f"[red]Message: {message.payload}[/]")
|
176
|
+
return
|
177
|
+
self.on_data_received(data)
|
178
|
+
|
179
|
+
def _on_log(self, client, userdata, paho_log_level, messages):
|
180
|
+
# print(messages)
|
181
|
+
pass
|
182
|
+
|
183
|
+
def _on_connect_edge(self, client, userdata, flags, reason_code, properties):
|
184
|
+
self.client.subscribe(f"/mari/{self.network_id}/to_edge", qos=self.qos)
|
185
|
+
print(f"[yellow]Subscribed to /mari/{self.network_id}/to_edge[/]")
|
186
|
+
|
187
|
+
def _on_connect_cloud(self, client, userdata, flags, reason_code, properties):
|
188
|
+
self.client.subscribe(f"/mari/{self.network_id}/to_cloud", qos=self.qos)
|
189
|
+
print(f"[yellow]Subscribed to /mari/{self.network_id}/to_cloud[/]")
|
190
|
+
|
191
|
+
|
192
|
+
class MQTTAdapterDummy(MQTTAdapter):
|
193
|
+
"""Dummy MQTT adapter, does nothing, for when edge runs only locally, without a cloud."""
|
194
|
+
|
195
|
+
def __init__(self, host="", port=0, is_edge=True):
|
196
|
+
super().__init__(host, port, is_edge)
|
197
|
+
|
198
|
+
def is_ready(self) -> bool:
|
199
|
+
"""Dummy adapter is never ready."""
|
200
|
+
return False
|
201
|
+
|
202
|
+
def init(self):
|
203
|
+
pass
|
204
|
+
|
205
|
+
def close(self):
|
206
|
+
pass
|
207
|
+
|
208
|
+
def send_data_to_edge(self, data):
|
209
|
+
pass
|
210
|
+
|
211
|
+
def send_data_to_cloud(self, data):
|
212
|
+
pass
|