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.
marilib/tui_edge.py ADDED
@@ -0,0 +1,185 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from rich.columns import Columns
4
+ from rich.console import Console, Group
5
+ from rich.layout import Layout
6
+ from rich.live import Live
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+ from rich.text import Text
10
+
11
+ from marilib import MarilibEdge
12
+ from marilib.model import MariNode, TestState
13
+ from marilib.tui import MarilibTUI
14
+
15
+
16
+ class MarilibTUIEdge(MarilibTUI):
17
+ """A Text-based User Interface for MarilibEdge."""
18
+
19
+ def __init__(
20
+ self,
21
+ max_tables=3,
22
+ re_render_max_freq=0.2,
23
+ test_state: TestState | None = None,
24
+ ):
25
+ self.console = Console()
26
+ self.live = Live(console=self.console, auto_refresh=False, transient=True)
27
+ self.live.start()
28
+ self.max_tables = max_tables
29
+ self.re_render_max_freq = re_render_max_freq
30
+ self.last_render_time = datetime.now()
31
+ self.test_state = test_state
32
+
33
+ def get_max_rows(self) -> int:
34
+ """Calculate maximum rows based on terminal height."""
35
+ terminal_height = self.console.height
36
+ available_height = terminal_height - 10 - 2 - 2 - 1 - 2
37
+ return max(2, available_height)
38
+
39
+ def render(self, mari: MarilibEdge):
40
+ """Render the TUI layout."""
41
+ with mari.lock:
42
+ if datetime.now() - self.last_render_time < timedelta(seconds=self.re_render_max_freq):
43
+ return
44
+ self.last_render_time = datetime.now()
45
+ layout = Layout()
46
+ layout.split(
47
+ Layout(self.create_header_panel(mari), size=12),
48
+ Layout(self.create_nodes_panel(mari)),
49
+ )
50
+ self.live.update(layout, refresh=True)
51
+
52
+ def create_header_panel(self, mari: MarilibEdge) -> Panel:
53
+ """Create the header panel with gateway and network stats."""
54
+ status = Text()
55
+ status.append("MarilibEdge is ", style="bold")
56
+ status.append(
57
+ "connected" if mari.serial_connected else "disconnected",
58
+ style="bold green" if mari.serial_connected else "bold red",
59
+ )
60
+ status.append(
61
+ f" via {mari.serial_interface.port} at {mari.serial_interface.baudrate} baud "
62
+ f"since {mari.started_ts.strftime('%Y-%m-%d %H:%M:%S')}"
63
+ )
64
+ status.append(" | ")
65
+ secs = int((datetime.now() - mari.last_received_serial_data_ts).total_seconds())
66
+ status.append(
67
+ f"last received: {secs}s ago",
68
+ style="bold green" if secs <= 1 else "bold red",
69
+ )
70
+
71
+ status.append("\n\nGateway: ", style="bold cyan")
72
+ status.append(f"0x{mari.gateway.info.address:016X} | ")
73
+ status.append("Network ID: ", style="bold cyan")
74
+ status.append(f"0x{mari.gateway.info.network_id:04X} | ")
75
+
76
+ status.append("\n\n")
77
+ status.append("Schedule: ", style="bold cyan")
78
+ status.append(f"#{mari.gateway.info.schedule_id} ({mari.gateway.info.schedule_name}) | ")
79
+ status.append(mari.gateway.info.repr_schedule_cells_with_colors())
80
+ status.append("\n\n")
81
+
82
+ if mari.gateway.latency_stats.last_ms > 0:
83
+ status.append("Latency: ", style="bold cyan")
84
+ lat = mari.gateway.latency_stats
85
+ status.append(
86
+ f"Last: {lat.last_ms:.1f}ms | Avg: {lat.avg_ms:.1f}ms | "
87
+ f"Min: {lat.min_ms:.1f}ms | Max: {lat.max_ms:.1f}ms"
88
+ )
89
+
90
+ status.append("\n\nStats: ", style="bold yellow")
91
+ if self.test_state and self.test_state.load > 0 and self.test_state.rate > 0:
92
+ status.append(
93
+ "Test load: ",
94
+ # style="bold yellow",
95
+ )
96
+ status.append(f"{self.test_state.load}% of {self.test_state.rate} pps")
97
+ status.append(" | ")
98
+
99
+ stats = mari.gateway.stats
100
+ status.append(f"Nodes: {len(mari.gateway.nodes)} | ")
101
+ status.append(f"Frames TX: {stats.sent_count(include_test_packets=False)} | ")
102
+ status.append(f"Frames RX: {stats.received_count(include_test_packets=False)} | ")
103
+ status.append(f"TX/s: {stats.sent_count(1, include_test_packets=False)} | ")
104
+ status.append(f"RX/s: {stats.received_count(1, include_test_packets=False)}")
105
+
106
+ return Panel(status, title="[bold]MarilibEdge Status", border_style="blue")
107
+
108
+ def create_nodes_table(self, nodes: list[MariNode], title="") -> Table:
109
+ """Create a table displaying information about connected nodes."""
110
+ table = Table(
111
+ show_header=True,
112
+ header_style="bold cyan",
113
+ border_style="blue",
114
+ padding=(0, 1),
115
+ title=title,
116
+ )
117
+ table.add_column("Node Address", style="cyan")
118
+ table.add_column("TX", justify="right")
119
+ table.add_column("TX/s", justify="right")
120
+ table.add_column("RX", justify="right")
121
+ table.add_column("RX/s", justify="right")
122
+ table.add_column("SR(total)", justify="right")
123
+ table.add_column("PDR Down", justify="right")
124
+ table.add_column("PDR Up", justify="right")
125
+ table.add_column("RSSI", justify="right")
126
+ table.add_column("Latency (ms)", justify="right")
127
+ for node in nodes:
128
+ lat_str = (
129
+ f"{node.latency_stats.avg_ms:.1f}" if node.latency_stats.last_ms > 0 else "..."
130
+ )
131
+ table.add_row(
132
+ f"0x{node.address:016X}",
133
+ str(node.stats.sent_count(include_test_packets=False)),
134
+ str(node.stats.sent_count(1, include_test_packets=False)),
135
+ str(node.stats.received_count(include_test_packets=False)),
136
+ str(node.stats.received_count(1, include_test_packets=False)),
137
+ f"{node.stats.success_rate():>4.0%}",
138
+ f"{node.pdr_downlink:>4.0%}",
139
+ f"{node.pdr_uplink:>4.0%}",
140
+ f"{node.stats.received_rssi_dbm(5)}",
141
+ lat_str,
142
+ )
143
+ return table
144
+
145
+ def create_nodes_panel(self, mari: MarilibEdge) -> Panel:
146
+ """Create the panel that contains the nodes table."""
147
+ nodes = mari.gateway.nodes
148
+ max_rows = self.get_max_rows()
149
+ max_displayable_nodes = self.max_tables * max_rows
150
+ nodes_to_display = nodes[:max_displayable_nodes]
151
+ remaining_nodes = max(0, len(nodes) - max_displayable_nodes)
152
+ tables = []
153
+ current_table_nodes = []
154
+ for i, node in enumerate(nodes_to_display):
155
+ current_table_nodes.append(node)
156
+ if len(current_table_nodes) == max_rows or i == len(nodes_to_display) - 1:
157
+ title = f"Nodes {i - len(current_table_nodes) + 2}-{i + 1}"
158
+ tables.append(self.create_nodes_table(current_table_nodes, title))
159
+ current_table_nodes = []
160
+ if len(tables) >= self.max_tables:
161
+ break
162
+ if len(tables) > 1:
163
+ content = Columns(tables, equal=True, expand=True)
164
+ else:
165
+ content = tables[0] if tables else Table()
166
+ if remaining_nodes > 0:
167
+ panel_content = Group(
168
+ content,
169
+ Text(
170
+ f"\n(...and {remaining_nodes} more nodes)",
171
+ style="bold yellow",
172
+ ),
173
+ )
174
+ else:
175
+ panel_content = content
176
+ return Panel(
177
+ panel_content,
178
+ title="[bold]Connected Nodes",
179
+ border_style="blue",
180
+ )
181
+
182
+ def close(self):
183
+ """Clean up the live display."""
184
+ self.live.stop()
185
+ print("")
@@ -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,30 @@
1
+ examples/frames.py,sha256=SSC36A66kFwL-U5HtFgAuK6_BI1ik_84EFJ7DoG0oBo,780
2
+ examples/mari_cloud.py,sha256=EQmQb6AiZt49j6bI4h9kAe8nfy6Zg5PfY0WTYKc8Kes,1860
3
+ examples/mari_cloud_minimal.py,sha256=zaLnd61gkqht5AclJC6Hfc1CPS_8BzTmGE-tItbWlao,1125
4
+ examples/mari_edge.py,sha256=VI5iv46f5ejbNn7H6A7pQjSDrdWsj3pDM6DrLos5SuI,1936
5
+ examples/mari_edge_minimal.py,sha256=9vOsIdW9WnS_GQg9fBw_G1wkvGFokwsiM83Q8etoUpA,1090
6
+ examples/mari_edge_stats.py,sha256=LQx4gtfFCp1rP0uBo1UCLg5e5eusDKsH_beoKj5_CAI,4340
7
+ examples/uart.py,sha256=pUvkpVf64T94XFLzgyHSzMOZISMrm0wqyurjNyS3JhY,823
8
+ marilib/__init__.py,sha256=PCxfi_8iZpMbc5DscErorPXqdO7qghp6t1MFU0NnYfk,130
9
+ marilib/communication_adapter.py,sha256=IEcSLm5lbtFMtFCZ784QdvZZ5zcqL1IZQ90CSSp-XvM,6998
10
+ marilib/latency.py,sha256=jz-7Yvgmk-BO40FHW_pltHbRV2OL1QD-dCkV9iSJiiY,2729
11
+ marilib/logger.py,sha256=IgkC1JM9sf3JxiSBYUcHeuYpiRrwcODVR2Y54GA2LPA,7867
12
+ marilib/mari_protocol.py,sha256=b1hQdSJX8CjPtKgosO7F-vt8ZHsnZ9QqMI2oKNL4r0E,2516
13
+ marilib/marilib.py,sha256=m7drdxhAMnjTrqYjUvwqunreYpffz7-AKgaOkuXHukg,939
14
+ marilib/marilib_cloud.py,sha256=0yPTea0J2h4EkWwTcU4QAr001srd9_P6LOPtmJmhH24,7346
15
+ marilib/marilib_edge.py,sha256=tzjeNtI_SpvghYxgV_y8j27d6xXX-1HIQc2TGKuNOg0,9743
16
+ marilib/model.py,sha256=0WAEtMxqnqziSG0epZfLXlXVAzVRNfou4n6suhtdB1k,12831
17
+ marilib/protocol.py,sha256=wOsG_oIk2Ls6gnbDYa5_g1sbHGuYgsj6liiax_yTn34,3786
18
+ marilib/serial_hdlc.py,sha256=6EDbjkJ5RU_1Gv3OhHaIhLAXscce_2-yCv-602sw2-8,8119
19
+ marilib/serial_uart.py,sha256=7sv8Ouj-y7k2Yry6cINil9WgVYvy1cRPPpIbW4JLVQo,2764
20
+ marilib/tui.py,sha256=doTpHnZqyHEglSck_a1cl9DU1uTCZavVOlcZjA8aTV4,231
21
+ marilib/tui_cloud.py,sha256=LZQVYLSwdLBubUcbUy7Eoahma_98b690ndCVgXSm_EA,5672
22
+ marilib/tui_edge.py,sha256=N2i0aUZ2zeaC0oPcCamNd5FY0wTDtmp2KWFF1yL6ABA,7573
23
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ tests/test_hdlc.py,sha256=SSgDpdTa2ga-P5tlXZtGvjliame4Ug9FwyreSw_nT9k,2450
25
+ tests/test_protocol.py,sha256=JJRtzCV2BygogSFFmss4FwYLb7x3Zy8sIEWxzopfxNE,1167
26
+ marilib_pkg-0.6.0.dist-info/METADATA,sha256=FuhL1nVncSyrIJ1mpqqReAajGZSCQcCJW-gEsQbE0h4,1992
27
+ marilib_pkg-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
+ marilib_pkg-0.6.0.dist-info/licenses/AUTHORS,sha256=gGgRHmEH6klwG7KQd4rYVy0AT8MkA9mMimfiN1NvOHU,96
29
+ marilib_pkg-0.6.0.dist-info/licenses/LICENSE,sha256=j97C1uBc5chpQWi4bv_2SrqExuvKaJK2Ch6L2LFkoc4,1492
30
+ marilib_pkg-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.
tests/__init__.py ADDED
File without changes
tests/test_hdlc.py ADDED
@@ -0,0 +1,76 @@
1
+ """Test module for HDLC handler class."""
2
+
3
+ import pytest
4
+
5
+ from marilib.serial_hdlc import HDLCDecodeException, HDLCHandler, HDLCState
6
+
7
+
8
+ def test_hdlc_handler_states():
9
+ handler = HDLCHandler()
10
+ assert handler.state == HDLCState.IDLE
11
+ handler.handle_byte(b"A")
12
+ assert handler.state == HDLCState.IDLE
13
+ handler.handle_byte(b"~")
14
+ assert handler.state == HDLCState.RECEIVING
15
+ handler.handle_byte(b"A")
16
+ assert handler.state == HDLCState.RECEIVING
17
+ handler.handle_byte(b"~")
18
+ assert handler.state == HDLCState.READY
19
+ handler.handle_byte(b"~")
20
+ assert handler.state == HDLCState.RECEIVING
21
+ handler.handle_byte(b"A")
22
+ handler.handle_byte(b"\xf5")
23
+ handler.handle_byte(b"\xa3")
24
+ assert handler.state == HDLCState.RECEIVING
25
+ handler.handle_byte(b"~")
26
+ assert handler.state == HDLCState.READY
27
+ handler.handle_byte(b"~")
28
+ assert handler.state == HDLCState.RECEIVING
29
+ handler.handle_byte(b"~")
30
+ assert handler.output == bytearray()
31
+ assert handler.state == HDLCState.RECEIVING
32
+ handler.handle_byte(b"~")
33
+ assert handler.output == bytearray()
34
+ assert handler.state == HDLCState.RECEIVING
35
+
36
+
37
+ def test_hdlc_handler_decode():
38
+ handler = HDLCHandler()
39
+ for byte in b"~test\x88\x07~":
40
+ handler.handle_byte(int(byte).to_bytes(1, "little"))
41
+ assert handler.payload == b"test"
42
+ assert handler.state == HDLCState.IDLE
43
+
44
+
45
+ def test_hdlc_handler_decode_with_flags():
46
+ handler = HDLCHandler()
47
+ for byte in b"~}^test}]\x06\x94~":
48
+ handler.handle_byte(int(byte).to_bytes(1, "little"))
49
+ assert handler.state == HDLCState.READY
50
+ assert handler.payload == bytearray(b"~test}")
51
+ assert handler.state == HDLCState.IDLE
52
+
53
+
54
+ def test_hdlc_handler_invalid_state():
55
+ handler = HDLCHandler()
56
+ for byte in b"~test\x42\x42":
57
+ handler.handle_byte(int(byte).to_bytes(1, "little"))
58
+ with pytest.raises(HDLCDecodeException) as exc:
59
+ _ = handler.payload
60
+ assert str(exc.value) == "Incomplete HDLC frame"
61
+
62
+
63
+ def test_hdlc_handler_invalid_fcs():
64
+ handler = HDLCHandler()
65
+ for byte in b"~test\x42\x42~":
66
+ handler.handle_byte(int(byte).to_bytes(1, "little"))
67
+ payload = handler.payload
68
+ assert payload == bytearray()
69
+
70
+
71
+ def test_hdlc_handler_payload_too_short():
72
+ handler = HDLCHandler()
73
+ for byte in b"~a~":
74
+ handler.handle_byte(int(byte).to_bytes(1, "little"))
75
+ payload = handler.payload
76
+ assert payload == bytearray()
tests/test_protocol.py ADDED
@@ -0,0 +1,35 @@
1
+ from marilib.mari_protocol import Frame, Header
2
+
3
+
4
+ def test_header_size():
5
+ assert Header().size == 20
6
+
7
+
8
+ def test_header_from_bytes():
9
+ header = Header().from_bytes(
10
+ bytes.fromhex("0210170059291ba8fdcecef531eb7f2526ef0399f0f0f0f0f0")[0:20]
11
+ )
12
+ assert header.version == 2
13
+ assert header.type_ == 16
14
+ assert header.network_id == 23
15
+ assert header.destination == int.from_bytes(
16
+ bytes.fromhex("59291ba8fdcecef5"), byteorder="little"
17
+ )
18
+ assert header.source == int.from_bytes(bytes.fromhex("31eb7f2526ef0399"), byteorder="little")
19
+
20
+
21
+ def test_frame_from_bytes():
22
+ frame = Frame().from_bytes(
23
+ bytes.fromhex("0210170059291ba8fdcecef531eb7f2526ef0399dcf0f0f0f0f0")
24
+ )
25
+ assert frame.header.version == 2
26
+ assert frame.header.type_ == 16
27
+ assert frame.header.network_id == 23
28
+ assert frame.header.destination == int.from_bytes(
29
+ bytes.fromhex("59291ba8fdcecef5"), byteorder="little"
30
+ )
31
+ assert frame.header.source == int.from_bytes(
32
+ bytes.fromhex("31eb7f2526ef0399"), byteorder="little"
33
+ )
34
+ assert frame.stats.rssi_dbm == -35
35
+ assert frame.payload == bytes.fromhex("f0f0f0f0f0")