marilib-pkg 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ # Segger Studio specific files
2
+
3
+ *.emSession
4
+ *.jlink
5
+
6
+ # Python compiled files
7
+ *.pyc
8
+
9
+ # Git conflict files
10
+ *.orig
11
+
12
+ # Visual Studio specific files
13
+ .vscode/
14
+
15
+ # Segger Studio output directory
16
+ Output/
17
+
18
+ # Python package dist
19
+ dist/
20
+
21
+ # Virtual env folder
22
+ .venv/
23
+
24
+ # Tox
25
+ .tox/
26
+
27
+ # Venv files
28
+ .venv*
29
+ venv
30
+
31
+ # Python cache files
32
+ __pycache__/
33
+
34
+ # Build files
35
+ build/
36
+
37
+ # Distribution files
38
+ dist/
39
+
40
+ # Coverage reports
41
+ .coverage
42
+ coverage.*
43
+
44
+ # do not commit logs
45
+ logs_metrics
46
+ logs
@@ -0,0 +1,2 @@
1
+ Geovane Fedrecheski <geovane.fedrecheski@inria.fr>
2
+ Alexandre Abadie <alexandre.abadie@inria.fr>
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Inria
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: marilib-pkg
3
+ Version: 0.6.0
4
+ Summary: MariLib is a Python library for interacting with the Mari network.
5
+ Project-URL: Homepage, https://github.com/DotBots/marilib
6
+ Project-URL: Bug Tracker, https://github.com/DotBots/marilib/issues
7
+ Author-email: Geovane Fedrecheski <geovane.fedrecheski@inria.fr>
8
+ License: BSD
9
+ License-File: AUTHORS
10
+ License-File: LICENSE
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.8
17
+ Requires-Dist: click==8.1.7
18
+ Requires-Dist: paho-mqtt==2.1.0
19
+ Requires-Dist: pyserial==3.5
20
+ Requires-Dist: rich==14.0.0
21
+ Requires-Dist: structlog==24.4.0
22
+ Requires-Dist: tqdm==4.66.5
23
+ Description-Content-Type: text/markdown
24
+
25
+ # MariLib 💫 👀 🐍
26
+
27
+ MariLib is a Python library to interact with a local [Mari](https://github.com/DotBots/mari) network.
28
+ It connects to a Mari gateway via UART.
29
+
30
+ ## Example with TUI
31
+ MariLib provides a stateful class with gateway and node information, network statistics, and a rich real-time TUI:
32
+
33
+ [mari-edge-2.webm](https://github.com/user-attachments/assets/fe50f2ba-8e67-4522-8700-69730f8e3aee)
34
+
35
+ See the how it works in `examples/basic.py`.
36
+
37
+ ## Minimal example
38
+ Here is a minimal example showcasing how to use MariLib:
39
+
40
+ ```python
41
+ import time
42
+ from marilib.marilib import MarilibEdge
43
+ from marilib.serial_uart import get_default_port
44
+
45
+ def main():
46
+ mari = MarilibEdge(lambda event, data: print(event.name, data), get_default_port())
47
+ while True:
48
+ for node in mari.gateway.nodes:
49
+ mari.send_frame(dst=node.address, payload=b"A" * 3)
50
+ statistics = [(f"{node.address:016X}", node.stats.received_rssi_dbm()) for node in mari.gateway.nodes]
51
+ print(f"Network statistics: {statistics}")
52
+ time.sleep(0.25)
53
+
54
+ if __name__ == "__main__":
55
+ main()
56
+ ```
57
+ See it in action in `examples/minimal.py`.
@@ -0,0 +1,33 @@
1
+ # MariLib 💫 👀 🐍
2
+
3
+ MariLib is a Python library to interact with a local [Mari](https://github.com/DotBots/mari) network.
4
+ It connects to a Mari gateway via UART.
5
+
6
+ ## Example with TUI
7
+ MariLib provides a stateful class with gateway and node information, network statistics, and a rich real-time TUI:
8
+
9
+ [mari-edge-2.webm](https://github.com/user-attachments/assets/fe50f2ba-8e67-4522-8700-69730f8e3aee)
10
+
11
+ See the how it works in `examples/basic.py`.
12
+
13
+ ## Minimal example
14
+ Here is a minimal example showcasing how to use MariLib:
15
+
16
+ ```python
17
+ import time
18
+ from marilib.marilib import MarilibEdge
19
+ from marilib.serial_uart import get_default_port
20
+
21
+ def main():
22
+ mari = MarilibEdge(lambda event, data: print(event.name, data), get_default_port())
23
+ while True:
24
+ for node in mari.gateway.nodes:
25
+ mari.send_frame(dst=node.address, payload=b"A" * 3)
26
+ statistics = [(f"{node.address:016X}", node.stats.received_rssi_dbm()) for node in mari.gateway.nodes]
27
+ print(f"Network statistics: {statistics}")
28
+ time.sleep(0.25)
29
+
30
+ if __name__ == "__main__":
31
+ main()
32
+ ```
33
+ See it in action in `examples/minimal.py`.
@@ -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())
@@ -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()
@@ -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()
@@ -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))
@@ -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