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 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,10 @@
1
+ __version__ = "0.6.0"
2
+
3
+
4
+ # declare these just to avoid circular imports
5
+ class MarilibEdge:
6
+ pass
7
+
8
+
9
+ class MarilibCloud:
10
+ pass
@@ -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