marilib-pkg 0.6.0__py3-none-any.whl → 0.7.0rc1__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 +7 -1
- examples/mari_cloud.py +25 -7
- examples/mari_edge.py +4 -7
- examples/mari_edge_stats.py +26 -14
- examples/raspberry-pi/sense_hat_ui.py +244 -0
- marilib/__init__.py +1 -1
- marilib/communication_adapter.py +4 -3
- marilib/logger.py +41 -23
- marilib/mari_protocol.py +204 -0
- marilib/marilib_cloud.py +15 -3
- marilib/marilib_edge.py +57 -47
- marilib/metrics.py +141 -0
- marilib/model.py +221 -38
- marilib/pdr.py +99 -0
- marilib/serial_uart.py +7 -6
- marilib/tui_cloud.py +26 -1
- marilib/tui_edge.py +152 -44
- {marilib_pkg-0.6.0.dist-info → marilib_pkg-0.7.0rc1.dist-info}/METADATA +25 -3
- marilib_pkg-0.7.0rc1.dist-info/RECORD +33 -0
- marilib_pkg-0.7.0rc1.dist-info/entry_points.txt +2 -0
- marilib/latency.py +0 -78
- marilib_pkg-0.6.0.dist-info/RECORD +0 -30
- {marilib_pkg-0.6.0.dist-info → marilib_pkg-0.7.0rc1.dist-info}/WHEEL +0 -0
- {marilib_pkg-0.6.0.dist-info → marilib_pkg-0.7.0rc1.dist-info}/licenses/AUTHORS +0 -0
- {marilib_pkg-0.6.0.dist-info → marilib_pkg-0.7.0rc1.dist-info}/licenses/LICENSE +0 -0
examples/frames.py
CHANGED
@@ -11,13 +11,19 @@ Example usage sending to broadcast address via mosquitto_pub:
|
|
11
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
12
|
"""
|
13
13
|
|
14
|
-
from marilib.mari_protocol import Frame, Header, MARI_BROADCAST_ADDRESS
|
14
|
+
from marilib.mari_protocol import Frame, Header, MARI_BROADCAST_ADDRESS, MetricsProbePayload
|
15
15
|
from marilib.model import EdgeEvent
|
16
|
+
from rich import print
|
16
17
|
import sys
|
17
18
|
|
18
19
|
destination = sys.argv[1] if len(sys.argv) > 1 else MARI_BROADCAST_ADDRESS
|
19
20
|
|
20
21
|
header = Header(destination=destination)
|
21
22
|
frame = Frame(header=header, payload=b"NORMAL_APP_DATA")
|
23
|
+
print(frame)
|
22
24
|
frame_to_send = EdgeEvent.to_bytes(EdgeEvent.NODE_DATA) + frame.to_bytes()
|
23
25
|
print(frame_to_send.hex())
|
26
|
+
|
27
|
+
probe_payload = MetricsProbePayload()
|
28
|
+
print(probe_payload.packet_length, probe_payload)
|
29
|
+
print(probe_payload.to_bytes().hex())
|
examples/mari_cloud.py
CHANGED
@@ -1,15 +1,13 @@
|
|
1
1
|
import time
|
2
2
|
|
3
3
|
import click
|
4
|
-
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, MARI_NET_ID_DEFAULT, Frame
|
4
|
+
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, MARI_NET_ID_DEFAULT, DefaultPayload, Frame
|
5
5
|
from marilib.marilib_cloud import MarilibCloud
|
6
6
|
from marilib.model import EdgeEvent, GatewayInfo, MariNode
|
7
7
|
from marilib.communication_adapter import MQTTAdapter
|
8
8
|
from marilib.tui_cloud import MarilibTUICloud
|
9
9
|
from marilib.logger import MetricsLogger
|
10
10
|
|
11
|
-
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
12
|
-
|
13
11
|
|
14
12
|
def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
15
13
|
"""An event handler for the application."""
|
@@ -32,6 +30,14 @@ def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
|
32
30
|
default=MARI_NET_ID_DEFAULT,
|
33
31
|
help=f"Network ID to use [default: 0x{MARI_NET_ID_DEFAULT:04X}]",
|
34
32
|
)
|
33
|
+
@click.option(
|
34
|
+
"--send-periodic",
|
35
|
+
"-s",
|
36
|
+
type=float,
|
37
|
+
default=0,
|
38
|
+
show_default=True,
|
39
|
+
help="Send periodic packet every N seconds (0 = disabled)",
|
40
|
+
)
|
35
41
|
@click.option(
|
36
42
|
"--log-dir",
|
37
43
|
default="logs",
|
@@ -39,7 +45,7 @@ def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
|
39
45
|
help="Directory to save metric log files.",
|
40
46
|
type=click.Path(),
|
41
47
|
)
|
42
|
-
def main(mqtt_url: str, network_id: int, log_dir: str):
|
48
|
+
def main(mqtt_url: str, network_id: int, send_periodic: float, log_dir: str):
|
43
49
|
"""A basic example of using the MariLibCloud library."""
|
44
50
|
|
45
51
|
mari = MarilibCloud(
|
@@ -54,12 +60,24 @@ def main(mqtt_url: str, network_id: int, log_dir: str):
|
|
54
60
|
)
|
55
61
|
|
56
62
|
try:
|
63
|
+
if send_periodic > 0:
|
64
|
+
normal_traffic_interval = send_periodic
|
65
|
+
last_normal_send_time = 0
|
66
|
+
|
57
67
|
while True:
|
68
|
+
current_time = time.monotonic()
|
69
|
+
|
58
70
|
mari.update()
|
59
|
-
if
|
60
|
-
|
71
|
+
if (
|
72
|
+
send_periodic > 0
|
73
|
+
and current_time - last_normal_send_time >= normal_traffic_interval
|
74
|
+
):
|
75
|
+
if mari.nodes:
|
76
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, DefaultPayload().to_bytes())
|
77
|
+
last_normal_send_time = current_time
|
78
|
+
|
61
79
|
mari.render_tui()
|
62
|
-
time.sleep(
|
80
|
+
time.sleep(1)
|
63
81
|
|
64
82
|
except KeyboardInterrupt:
|
65
83
|
pass
|
examples/mari_edge.py
CHANGED
@@ -2,15 +2,13 @@ import time
|
|
2
2
|
|
3
3
|
import click
|
4
4
|
from marilib.logger import MetricsLogger
|
5
|
-
from marilib.mari_protocol import Frame, MARI_BROADCAST_ADDRESS
|
5
|
+
from marilib.mari_protocol import Frame, MARI_BROADCAST_ADDRESS, DefaultPayload
|
6
6
|
from marilib.model import EdgeEvent, MariNode
|
7
7
|
from marilib.communication_adapter import SerialAdapter, MQTTAdapter
|
8
8
|
from marilib.serial_uart import get_default_port
|
9
9
|
from marilib.tui_edge import MarilibTUIEdge
|
10
10
|
from marilib.marilib_edge import MarilibEdge
|
11
11
|
|
12
|
-
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
13
|
-
|
14
12
|
|
15
13
|
def on_event(event: EdgeEvent, event_data: MariNode | Frame):
|
16
14
|
"""An event handler for the application."""
|
@@ -30,9 +28,8 @@ def on_event(event: EdgeEvent, event_data: MariNode | Frame):
|
|
30
28
|
"--mqtt-url",
|
31
29
|
"-m",
|
32
30
|
type=str,
|
33
|
-
default=
|
34
|
-
|
35
|
-
help="MQTT broker to use (default: empty, no cloud)",
|
31
|
+
default=None,
|
32
|
+
help="MQTT broker to use (default: None, no cloud)",
|
36
33
|
)
|
37
34
|
@click.option(
|
38
35
|
"--log-dir",
|
@@ -59,7 +56,7 @@ def main(port: str | None, mqtt_url: str, log_dir: str):
|
|
59
56
|
while True:
|
60
57
|
mari.update()
|
61
58
|
if not mari.uses_mqtt and mari.nodes:
|
62
|
-
mari.send_frame(MARI_BROADCAST_ADDRESS,
|
59
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, DefaultPayload().to_bytes())
|
63
60
|
mari.render_tui()
|
64
61
|
time.sleep(0.5)
|
65
62
|
except KeyboardInterrupt:
|
examples/mari_edge_stats.py
CHANGED
@@ -4,16 +4,13 @@ import time
|
|
4
4
|
|
5
5
|
import click
|
6
6
|
from marilib.logger import MetricsLogger
|
7
|
-
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, Frame
|
7
|
+
from marilib.mari_protocol import MARI_BROADCAST_ADDRESS, Frame, DefaultPayload, DefaultPayloadType
|
8
8
|
from marilib.marilib_edge import MarilibEdge
|
9
9
|
from marilib.model import EdgeEvent, GatewayInfo, MariNode, TestState
|
10
10
|
from marilib.serial_uart import get_default_port
|
11
11
|
from marilib.tui_edge import MarilibTUIEdge
|
12
12
|
from marilib.communication_adapter import SerialAdapter, MQTTAdapter
|
13
13
|
|
14
|
-
LOAD_PACKET_PAYLOAD = b"L"
|
15
|
-
NORMAL_DATA_PAYLOAD = b"NORMAL_APP_DATA"
|
16
|
-
|
17
14
|
|
18
15
|
class LoadTester(threading.Thread):
|
19
16
|
def __init__(
|
@@ -43,7 +40,10 @@ class LoadTester(threading.Thread):
|
|
43
40
|
nodes_exist = bool(self.mari.gateway.nodes)
|
44
41
|
|
45
42
|
if nodes_exist:
|
46
|
-
self.mari.send_frame(
|
43
|
+
self.mari.send_frame(
|
44
|
+
MARI_BROADCAST_ADDRESS,
|
45
|
+
DefaultPayload(type_=DefaultPayloadType.METRICS_LOAD).to_bytes(),
|
46
|
+
)
|
47
47
|
self._stop_event.wait(self.delay)
|
48
48
|
|
49
49
|
def set_rate(self):
|
@@ -88,6 +88,14 @@ def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
|
88
88
|
show_default=True,
|
89
89
|
help="Load percentage to apply (0–100)",
|
90
90
|
)
|
91
|
+
@click.option(
|
92
|
+
"--send-periodic",
|
93
|
+
"-s",
|
94
|
+
type=float,
|
95
|
+
default=0,
|
96
|
+
show_default=True,
|
97
|
+
help="Send periodic packet every N seconds (0 = disabled)",
|
98
|
+
)
|
91
99
|
@click.option(
|
92
100
|
"--log-dir",
|
93
101
|
default="logs",
|
@@ -95,7 +103,7 @@ def on_event(event: EdgeEvent, event_data: MariNode | Frame | GatewayInfo):
|
|
95
103
|
help="Directory to save metric log files.",
|
96
104
|
type=click.Path(),
|
97
105
|
)
|
98
|
-
def main(port: str | None, mqtt_host: str, load: int, log_dir: str):
|
106
|
+
def main(port: str | None, mqtt_host: str, load: int, send_periodic: float, log_dir: str):
|
99
107
|
if not (0 <= load <= 100):
|
100
108
|
sys.stderr.write("Error: --load must be between 0 and 100.\n")
|
101
109
|
return
|
@@ -109,7 +117,7 @@ def main(port: str | None, mqtt_host: str, load: int, log_dir: str):
|
|
109
117
|
mari = MarilibEdge(
|
110
118
|
on_event,
|
111
119
|
serial_interface=SerialAdapter(port),
|
112
|
-
mqtt_interface=MQTTAdapter.
|
120
|
+
mqtt_interface=MQTTAdapter.from_url(mqtt_host, is_edge=True) if mqtt_host else None,
|
113
121
|
logger=logger,
|
114
122
|
main_file=__file__,
|
115
123
|
tui=MarilibTUIEdge(test_state=test_state),
|
@@ -117,15 +125,16 @@ def main(port: str | None, mqtt_host: str, load: int, log_dir: str):
|
|
117
125
|
|
118
126
|
stop_event = threading.Event()
|
119
127
|
|
120
|
-
mari.
|
128
|
+
mari.metrics_test_enable()
|
121
129
|
|
122
130
|
load_tester = LoadTester(mari, test_state, stop_event)
|
123
131
|
if load > 0:
|
124
132
|
load_tester.start()
|
125
133
|
|
126
134
|
try:
|
127
|
-
|
128
|
-
|
135
|
+
if send_periodic > 0:
|
136
|
+
normal_traffic_interval = send_periodic
|
137
|
+
last_normal_send_time = 0
|
129
138
|
|
130
139
|
while not stop_event.is_set():
|
131
140
|
current_time = time.monotonic()
|
@@ -134,18 +143,21 @@ def main(port: str | None, mqtt_host: str, load: int, log_dir: str):
|
|
134
143
|
|
135
144
|
mari.render_tui()
|
136
145
|
|
137
|
-
if
|
146
|
+
if (
|
147
|
+
send_periodic > 0
|
148
|
+
and current_time - last_normal_send_time >= normal_traffic_interval
|
149
|
+
):
|
138
150
|
if mari.nodes:
|
139
|
-
mari.send_frame(MARI_BROADCAST_ADDRESS,
|
151
|
+
mari.send_frame(MARI_BROADCAST_ADDRESS, DefaultPayload().to_bytes())
|
140
152
|
last_normal_send_time = current_time
|
141
153
|
|
142
|
-
time.sleep(
|
154
|
+
time.sleep(1)
|
143
155
|
|
144
156
|
except KeyboardInterrupt:
|
145
157
|
pass
|
146
158
|
finally:
|
147
159
|
stop_event.set()
|
148
|
-
mari.
|
160
|
+
mari.metrics_test_disable()
|
149
161
|
if load_tester.is_alive():
|
150
162
|
load_tester.join()
|
151
163
|
mari.close_tui()
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# this only works on raspberry pi
|
2
|
+
# to install the emulator sense_emu library:
|
3
|
+
# - sudo apt-get install python3-gi python3-gi-cairo
|
4
|
+
# - pip install sense-emu
|
5
|
+
# to install the real sense_hat library:
|
6
|
+
# - sudo apt-get install sense-hat
|
7
|
+
|
8
|
+
from sense_emu import SenseHat
|
9
|
+
import threading
|
10
|
+
import time
|
11
|
+
|
12
|
+
# ======================= Code for driving the sense hat =======================
|
13
|
+
|
14
|
+
s = SenseHat()
|
15
|
+
# s.set_rotation(90)
|
16
|
+
# ---------- colors ----------
|
17
|
+
r = [255, 0, 0]
|
18
|
+
w = [255, 255, 255]
|
19
|
+
g = [0, 128, 0]
|
20
|
+
y = [255, 255, 0]
|
21
|
+
b = [0, 0, 255]
|
22
|
+
p = [191, 0, 255]
|
23
|
+
|
24
|
+
# clear the screen to have a white background
|
25
|
+
s.clear(w)
|
26
|
+
|
27
|
+
# ---------- config ----------
|
28
|
+
nb_max = 102 # maximum number of nodes for this schedule -- will come from marilib
|
29
|
+
nb_row = 8 # number of pixels in a row
|
30
|
+
bg = w # background of the bottom 6*6 area
|
31
|
+
nb_nodes_per_pixel = nb_max // nb_row
|
32
|
+
|
33
|
+
# ---------- shared state ----------
|
34
|
+
state_lock = threading.Lock()
|
35
|
+
node_changed = threading.Event()
|
36
|
+
|
37
|
+
nb_nodes = 0 # current nodes
|
38
|
+
prev_pixels = 0 # how many pixels were lit last time
|
39
|
+
|
40
|
+
|
41
|
+
# 6x6 font: each row is a 6-bit number (bit 5 = leftmost, bit 0 = rightmost)
|
42
|
+
font_6x6 = {
|
43
|
+
" ": [0b000000, 0b000000, 0b000000, 0b000000, 0b000000, 0b000000],
|
44
|
+
"0": [0b011110, 0b100001, 0b100001, 0b100001, 0b100001, 0b011110],
|
45
|
+
"1": [0b001000, 0b011000, 0b001000, 0b001000, 0b001000, 0b111110],
|
46
|
+
"2": [0b011110, 0b100001, 0b000010, 0b000100, 0b001000, 0b111111],
|
47
|
+
"3": [0b011110, 0b100001, 0b000110, 0b000001, 0b100001, 0b011110],
|
48
|
+
"4": [0b000100, 0b001100, 0b010100, 0b100100, 0b111111, 0b000100],
|
49
|
+
"5": [0b111111, 0b100000, 0b111110, 0b000001, 0b100001, 0b011110],
|
50
|
+
"6": [0b011110, 0b100000, 0b111110, 0b100001, 0b100001, 0b011110],
|
51
|
+
"7": [0b111111, 0b000001, 0b000010, 0b000100, 0b001000, 0b010000],
|
52
|
+
"8": [0b011110, 0b100001, 0b011110, 0b100001, 0b100001, 0b011110],
|
53
|
+
"9": [0b011110, 0b100001, 0b100001, 0b111111, 0b000001, 0b011110],
|
54
|
+
}
|
55
|
+
|
56
|
+
|
57
|
+
def choose_color(multiplier: int):
|
58
|
+
"""
|
59
|
+
Choose color of pixels depending on number of nodes that joined
|
60
|
+
"""
|
61
|
+
if multiplier > 8:
|
62
|
+
return w
|
63
|
+
if multiplier <= 3:
|
64
|
+
return g
|
65
|
+
if multiplier < 6:
|
66
|
+
return y
|
67
|
+
if multiplier <= 8:
|
68
|
+
return r
|
69
|
+
|
70
|
+
|
71
|
+
def _set_top(x, color):
|
72
|
+
s.set_pixel(x, 0, color)
|
73
|
+
|
74
|
+
|
75
|
+
def display_num_nodes():
|
76
|
+
"""
|
77
|
+
update the top bar so that every (nb_nodes//nb_row) nodes that joined or left
|
78
|
+
changes the bar by 1 pixel.
|
79
|
+
example: if nb_max of nodes is 66 then 66//8 = 8 so every 8 nodes joining lights up one more pixel
|
80
|
+
and every 8 nodes leaving resets 1 pixel
|
81
|
+
"""
|
82
|
+
global prev_pixels
|
83
|
+
|
84
|
+
with state_lock:
|
85
|
+
current_nodes = nb_nodes
|
86
|
+
old_pixels = prev_pixels
|
87
|
+
|
88
|
+
new_pixels = current_nodes // nb_nodes_per_pixel
|
89
|
+
# nodes have joined
|
90
|
+
if new_pixels > old_pixels:
|
91
|
+
# light pixels from old_pixels .. new_pixels-1
|
92
|
+
for i in range(old_pixels, new_pixels):
|
93
|
+
_set_top(i, choose_color(i + 1))
|
94
|
+
|
95
|
+
# nodes left
|
96
|
+
elif new_pixels < old_pixels:
|
97
|
+
# clear pixels from new_pixels .. old_pixels-1
|
98
|
+
for i in range(new_pixels, old_pixels):
|
99
|
+
_set_top(i, bg)
|
100
|
+
|
101
|
+
# update number of pixels
|
102
|
+
with state_lock:
|
103
|
+
prev_pixels = new_pixels
|
104
|
+
|
105
|
+
|
106
|
+
def display_num_nodes_thread():
|
107
|
+
"""
|
108
|
+
Background thread: wait for node changes and update the top bar.
|
109
|
+
"""
|
110
|
+
display_num_nodes() # initial paint
|
111
|
+
while True:
|
112
|
+
node_changed.wait()
|
113
|
+
node_changed.clear()
|
114
|
+
display_num_nodes()
|
115
|
+
|
116
|
+
|
117
|
+
def number_to_columns(rows):
|
118
|
+
"""Convert 6 rows of 6-bit ints (a number) to a list of 6 columns of pixels
|
119
|
+
(each column is a list of 6 booleans
|
120
|
+
True=ON, False=OFF)."""
|
121
|
+
cols = []
|
122
|
+
for c in range(6): # 0..5 (left->right)
|
123
|
+
mask = 1 << (5 - c)
|
124
|
+
col = [(rows[r] & mask) != 0 for r in range(6)] # top->bottom
|
125
|
+
cols.append(col)
|
126
|
+
return cols
|
127
|
+
|
128
|
+
|
129
|
+
def text_to_columns(text):
|
130
|
+
"""Convert text to a list of columns with 1 blank column between the numbers."""
|
131
|
+
stream = []
|
132
|
+
for ch in text:
|
133
|
+
rows = font_6x6.get(ch, font_6x6[" "])
|
134
|
+
stream.extend(number_to_columns(rows))
|
135
|
+
stream.append([False] * 6) # spacing column
|
136
|
+
return stream
|
137
|
+
|
138
|
+
|
139
|
+
def display_scrolling_message(text, fg, bg, speed, scrolling_times):
|
140
|
+
"""
|
141
|
+
Scroll a 6x6 message across the Sense HAT.
|
142
|
+
Draws ONLY on rows y=2..7 (leaves y=0 and y=1 unchanged).
|
143
|
+
"""
|
144
|
+
text = " " + text
|
145
|
+
cols = text_to_columns(text)
|
146
|
+
cols.extend([[False] * 6 for _ in range(8)]) # right padding so it scrolls off
|
147
|
+
|
148
|
+
width = 8
|
149
|
+
for i in range(scrolling_times):
|
150
|
+
for offset in range(len(cols)):
|
151
|
+
for x in range(width):
|
152
|
+
col_index = offset + x
|
153
|
+
col = cols[col_index] if 0 <= col_index < len(cols) else [False] * 6
|
154
|
+
|
155
|
+
# Map 6-tall column onto y = 2..7
|
156
|
+
for y6 in range(6):
|
157
|
+
y = 2 + y6
|
158
|
+
s.set_pixel(x, y, fg if col[y6] else bg)
|
159
|
+
time.sleep(speed)
|
160
|
+
|
161
|
+
|
162
|
+
def display_static_message(text, fg, bg):
|
163
|
+
"""
|
164
|
+
Show a 6x6 message without scrolling.
|
165
|
+
Draws only on rows y=2..7, rows 0 and 1 are not used
|
166
|
+
"""
|
167
|
+
cols = text_to_columns(text)
|
168
|
+
width = 8
|
169
|
+
# draw 8 columns
|
170
|
+
for x in range(width):
|
171
|
+
src_index = x - 1 # shift right by 1 column
|
172
|
+
if 0 <= src_index < len(cols):
|
173
|
+
col = cols[src_index]
|
174
|
+
else:
|
175
|
+
col = [False] * 6 # blank when out of bounds
|
176
|
+
|
177
|
+
for y6 in range(6):
|
178
|
+
y = 2 + y6 # vertical position unchanged
|
179
|
+
s.set_pixel(x, y, fg if col[y6] else bg)
|
180
|
+
|
181
|
+
|
182
|
+
def message_thread(
|
183
|
+
static_text, scroll_text, fg_static, fg_scroll, bg, scroll_speed, scroll_repeats
|
184
|
+
):
|
185
|
+
"""
|
186
|
+
alternates:
|
187
|
+
1) draw static message; schedule
|
188
|
+
2) scroll another message: network id (net_id) scroll_repeats times at scroll_speed
|
189
|
+
Loops forever.
|
190
|
+
"""
|
191
|
+
while True:
|
192
|
+
display_static_message(static_text, fg_static, bg)
|
193
|
+
time.sleep(3)
|
194
|
+
display_scrolling_message(scroll_text, fg_scroll, bg, scroll_speed, scroll_repeats)
|
195
|
+
time.sleep(0.2)
|
196
|
+
|
197
|
+
|
198
|
+
# ========================= functions just for testing =========================
|
199
|
+
|
200
|
+
|
201
|
+
def node_join(n: int):
|
202
|
+
"""
|
203
|
+
Safely add n nodes and notify the drawer.
|
204
|
+
"""
|
205
|
+
global nb_nodes
|
206
|
+
with state_lock:
|
207
|
+
nb_nodes = min(nb_nodes + n, nb_max)
|
208
|
+
node_changed.set()
|
209
|
+
|
210
|
+
|
211
|
+
# just for testing
|
212
|
+
def node_leave(n: int):
|
213
|
+
"""
|
214
|
+
Safely remove n nodes and notify the drawer.
|
215
|
+
"""
|
216
|
+
global nb_nodes
|
217
|
+
with state_lock:
|
218
|
+
nb_nodes = max(nb_nodes - n, 0)
|
219
|
+
node_changed.set()
|
220
|
+
|
221
|
+
|
222
|
+
# ================================ main function ===============================
|
223
|
+
|
224
|
+
if __name__ == "__main__":
|
225
|
+
scroll = True
|
226
|
+
speed = 0.1
|
227
|
+
schedule = 5 # will come from marilib
|
228
|
+
net_id = 1200 # will come from marilib
|
229
|
+
scrolling_times = 3
|
230
|
+
|
231
|
+
threading.Thread(target=display_num_nodes_thread, daemon=True).start()
|
232
|
+
|
233
|
+
threading.Thread(
|
234
|
+
target=message_thread,
|
235
|
+
args=(str(schedule), str(net_id), b, p, w, speed, scrolling_times),
|
236
|
+
daemon=True,
|
237
|
+
).start()
|
238
|
+
|
239
|
+
# demo
|
240
|
+
while True:
|
241
|
+
time.sleep(0.8)
|
242
|
+
node_join(24) # +1 bar pixel
|
243
|
+
time.sleep(0.8)
|
244
|
+
node_leave(8) # -1 bar pixel
|
marilib/__init__.py
CHANGED
marilib/communication_adapter.py
CHANGED
@@ -53,9 +53,10 @@ class SerialAdapter(CommunicationAdapterBase):
|
|
53
53
|
print("[yellow]Disconnect from gateway...[/]")
|
54
54
|
|
55
55
|
def send_data(self, data):
|
56
|
-
self.serial.
|
57
|
-
|
58
|
-
|
56
|
+
with self.serial.lock: # Use the existing lock for thread safety
|
57
|
+
self.serial.serial.flush()
|
58
|
+
encoded = hdlc_encode(data)
|
59
|
+
self.serial.write(encoded)
|
59
60
|
|
60
61
|
|
61
62
|
class MQTTAdapter(CommunicationAdapterBase):
|
marilib/logger.py
CHANGED
@@ -85,11 +85,17 @@ class MetricsLogger:
|
|
85
85
|
"gateway_address",
|
86
86
|
"schedule_id",
|
87
87
|
"connected_nodes",
|
88
|
-
"tx_total",
|
89
|
-
"rx_total",
|
90
|
-
"tx_rate_1s",
|
91
|
-
"rx_rate_1s",
|
88
|
+
# "tx_total",
|
89
|
+
# "rx_total",
|
90
|
+
# "tx_rate_1s",
|
91
|
+
# "rx_rate_1s",
|
92
92
|
"avg_latency_ms",
|
93
|
+
"avg_pdr_downlink_radio",
|
94
|
+
"avg_pdr_uplink_radio",
|
95
|
+
"latest_node_tx_count",
|
96
|
+
"latest_node_rx_count",
|
97
|
+
"latest_gw_tx_count",
|
98
|
+
"latest_gw_rx_count",
|
93
99
|
]
|
94
100
|
self._gateway_writer.writerow(gateway_header)
|
95
101
|
|
@@ -100,17 +106,20 @@ class MetricsLogger:
|
|
100
106
|
"gateway_address",
|
101
107
|
"node_address",
|
102
108
|
"is_alive",
|
103
|
-
"tx_total",
|
104
|
-
"rx_total",
|
105
|
-
"tx_rate_1s",
|
106
|
-
"rx_rate_1s",
|
109
|
+
# "tx_total",
|
110
|
+
# "rx_total",
|
111
|
+
# "tx_rate_1s",
|
112
|
+
# "rx_rate_1s",
|
107
113
|
"success_rate_30s",
|
108
114
|
"success_rate_total",
|
109
115
|
"pdr_downlink",
|
110
116
|
"pdr_uplink",
|
111
|
-
"
|
112
|
-
"
|
113
|
-
"
|
117
|
+
"rssi_node_dbm",
|
118
|
+
"rssi_gw_dbm",
|
119
|
+
"avg_latency_edge_ms",
|
120
|
+
"avg_latency_cloud_ms",
|
121
|
+
"last_latency_edge_ms",
|
122
|
+
"last_latency_cloud_ms",
|
114
123
|
]
|
115
124
|
self._nodes_writer.writerow(nodes_header)
|
116
125
|
|
@@ -141,11 +150,17 @@ class MetricsLogger:
|
|
141
150
|
f"0x{gateway.info.address:016X}",
|
142
151
|
gateway.info.schedule_id,
|
143
152
|
len(gateway.nodes),
|
144
|
-
gateway.stats.sent_count(include_test_packets=False),
|
145
|
-
gateway.stats.received_count(include_test_packets=False),
|
146
|
-
gateway.stats.sent_count(1, include_test_packets=False),
|
147
|
-
gateway.stats.received_count(1, include_test_packets=False),
|
148
|
-
f"{gateway.
|
153
|
+
# gateway.stats.sent_count(include_test_packets=False),
|
154
|
+
# gateway.stats.received_count(include_test_packets=False),
|
155
|
+
# gateway.stats.sent_count(1, include_test_packets=False),
|
156
|
+
# gateway.stats.received_count(1, include_test_packets=False),
|
157
|
+
f"{gateway.stats_avg_latency_roundtrip_node_edge_ms():.2f}",
|
158
|
+
f"{gateway.stats_avg_pdr_downlink_radio():.2f}",
|
159
|
+
f"{gateway.stats_avg_pdr_uplink_radio():.2f}",
|
160
|
+
gateway.stats_latest_node_tx_count(),
|
161
|
+
gateway.stats_latest_node_rx_count(),
|
162
|
+
gateway.stats_latest_gw_tx_count(),
|
163
|
+
gateway.stats_latest_gw_rx_count(),
|
149
164
|
]
|
150
165
|
self._gateway_writer.writerow(row)
|
151
166
|
|
@@ -161,17 +176,20 @@ class MetricsLogger:
|
|
161
176
|
f"0x{node.gateway_address:016X}",
|
162
177
|
f"0x{node.address:016X}",
|
163
178
|
node.is_alive,
|
164
|
-
node.stats.sent_count(include_test_packets=False),
|
165
|
-
node.stats.received_count(include_test_packets=False),
|
166
|
-
node.stats.sent_count(1, include_test_packets=False),
|
167
|
-
node.stats.received_count(1, include_test_packets=False),
|
179
|
+
# node.stats.sent_count(include_test_packets=False),
|
180
|
+
# node.stats.received_count(include_test_packets=False),
|
181
|
+
# node.stats.sent_count(1, include_test_packets=False),
|
182
|
+
# node.stats.received_count(1, include_test_packets=False),
|
168
183
|
f"{node.stats.success_rate(30):.2%}",
|
169
184
|
f"{node.stats.success_rate():.2%}",
|
170
185
|
f"{node.pdr_downlink:.2%}",
|
171
186
|
f"{node.pdr_uplink:.2%}",
|
172
|
-
node.
|
173
|
-
|
174
|
-
f"{node.
|
187
|
+
node.stats_rssi_node_dbm(),
|
188
|
+
node.stats_rssi_gw_dbm(),
|
189
|
+
f"{node.stats_avg_latency_roundtrip_node_edge_ms():.2f}",
|
190
|
+
f"{node.stats_avg_latency_roundtrip_node_edge_ms():.2f}", # FIXME!: should use cloud option
|
191
|
+
f"{node.stats_latest_latency_roundtrip_node_edge_ms():.2f}",
|
192
|
+
f"{node.stats_latest_latency_roundtrip_node_edge_ms():.2f}", # FIXME!: should use cloud option
|
175
193
|
]
|
176
194
|
self._nodes_writer.writerow(row)
|
177
195
|
|