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
marilib/mari_protocol.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import dataclasses
|
2
2
|
from dataclasses import dataclass
|
3
|
+
from enum import IntEnum
|
3
4
|
|
4
5
|
from marilib.protocol import Packet, PacketFieldMetadata, PacketType
|
5
6
|
|
@@ -8,6 +9,195 @@ MARI_BROADCAST_ADDRESS = 0xFFFFFFFFFFFFFFFF
|
|
8
9
|
MARI_NET_ID_DEFAULT = 0x0001
|
9
10
|
|
10
11
|
|
12
|
+
class DefaultPayloadType(IntEnum):
|
13
|
+
APPLICATION_DATA = 1
|
14
|
+
METRICS_REQUEST = 128
|
15
|
+
METRICS_RESPONSE = 129
|
16
|
+
METRICS_LOAD = 130
|
17
|
+
METRICS_PROBE = 140
|
18
|
+
|
19
|
+
def as_bytes(self) -> bytes:
|
20
|
+
return bytes([self.value])
|
21
|
+
|
22
|
+
|
23
|
+
@dataclass
|
24
|
+
class DefaultPayload(Packet):
|
25
|
+
metadata: list[PacketFieldMetadata] = dataclasses.field(
|
26
|
+
default_factory=lambda: [
|
27
|
+
PacketFieldMetadata(name="type", length=1),
|
28
|
+
]
|
29
|
+
)
|
30
|
+
type_: DefaultPayloadType = DefaultPayloadType.APPLICATION_DATA
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class MetricsProbePayload(Packet):
|
35
|
+
metadata: list[PacketFieldMetadata] = dataclasses.field(
|
36
|
+
default_factory=lambda: [
|
37
|
+
PacketFieldMetadata(name="type", length=1),
|
38
|
+
PacketFieldMetadata(name="cloud_tx_ts_us", length=8),
|
39
|
+
PacketFieldMetadata(name="cloud_rx_ts_us", length=8),
|
40
|
+
PacketFieldMetadata(name="cloud_tx_count", length=4),
|
41
|
+
PacketFieldMetadata(name="cloud_rx_count", length=4),
|
42
|
+
PacketFieldMetadata(name="edge_tx_ts_us", length=8),
|
43
|
+
PacketFieldMetadata(name="edge_rx_ts_us", length=8),
|
44
|
+
PacketFieldMetadata(name="edge_tx_count", length=4),
|
45
|
+
PacketFieldMetadata(name="edge_rx_count", length=4),
|
46
|
+
PacketFieldMetadata(name="gw_tx_count", length=4),
|
47
|
+
PacketFieldMetadata(name="gw_rx_count", length=4),
|
48
|
+
PacketFieldMetadata(name="gw_rx_asn", length=8),
|
49
|
+
PacketFieldMetadata(name="gw_tx_enqueued_asn", length=8),
|
50
|
+
PacketFieldMetadata(name="gw_tx_dequeued_asn", length=8),
|
51
|
+
PacketFieldMetadata(name="node_rx_count", length=4),
|
52
|
+
PacketFieldMetadata(name="node_tx_count", length=4),
|
53
|
+
PacketFieldMetadata(name="node_rx_asn", length=8),
|
54
|
+
PacketFieldMetadata(name="node_tx_enqueued_asn", length=8),
|
55
|
+
PacketFieldMetadata(name="node_tx_dequeued_asn", length=8),
|
56
|
+
PacketFieldMetadata(name="rssi_at_node", length=1),
|
57
|
+
PacketFieldMetadata(name="rssi_at_gw", length=1),
|
58
|
+
]
|
59
|
+
)
|
60
|
+
type_: DefaultPayloadType = DefaultPayloadType.METRICS_PROBE
|
61
|
+
cloud_tx_ts_us: int = 0
|
62
|
+
cloud_rx_ts_us: int = 0
|
63
|
+
cloud_tx_count: int = 0
|
64
|
+
cloud_rx_count: int = 0
|
65
|
+
edge_tx_ts_us: int = 0
|
66
|
+
edge_rx_ts_us: int = 0
|
67
|
+
edge_tx_count: int = 0
|
68
|
+
edge_rx_count: int = 0
|
69
|
+
gw_tx_count: int = 0
|
70
|
+
gw_rx_count: int = 0
|
71
|
+
gw_rx_asn: int = 0
|
72
|
+
gw_tx_enqueued_asn: int = 0
|
73
|
+
gw_tx_dequeued_asn: int = 0
|
74
|
+
node_rx_count: int = 0
|
75
|
+
node_tx_count: int = 0
|
76
|
+
node_rx_asn: int = 0
|
77
|
+
node_tx_enqueued_asn: int = 0
|
78
|
+
node_tx_dequeued_asn: int = 0
|
79
|
+
rssi_at_node: int = 0
|
80
|
+
rssi_at_gw: int = 0
|
81
|
+
|
82
|
+
@property
|
83
|
+
def packet_length(self) -> int:
|
84
|
+
return sum(field.length for field in self.metadata)
|
85
|
+
|
86
|
+
@property
|
87
|
+
def asn(self) -> int:
|
88
|
+
"""ASN at reception back from the network"""
|
89
|
+
return self.gw_rx_asn
|
90
|
+
|
91
|
+
def latency_roundtrip_node_edge_ms(self) -> float:
|
92
|
+
return (self.edge_rx_ts_us - self.edge_tx_ts_us) / 1000.0
|
93
|
+
|
94
|
+
def latency_roundtrip_node_cloud_ms(self) -> float:
|
95
|
+
return (self.cloud_rx_ts_us - self.cloud_tx_ts_us) / 1000.0
|
96
|
+
|
97
|
+
def pdr_uplink_radio(self, probe_stats_start_epoch=None) -> float:
|
98
|
+
if probe_stats_start_epoch is None:
|
99
|
+
# if no epoch is provided, use the current values
|
100
|
+
gw_rx_count = self.gw_rx_count
|
101
|
+
node_tx_count = self.node_tx_count
|
102
|
+
else:
|
103
|
+
# if epoch is provided, subtract the epoch values from the current values
|
104
|
+
if probe_stats_start_epoch.asn == 0:
|
105
|
+
return 0
|
106
|
+
gw_rx_count = self.gw_rx_count - probe_stats_start_epoch.gw_rx_count
|
107
|
+
node_tx_count = self.node_tx_count - probe_stats_start_epoch.node_tx_count
|
108
|
+
if node_tx_count <= 0:
|
109
|
+
return 0
|
110
|
+
return gw_rx_count / node_tx_count
|
111
|
+
|
112
|
+
def pdr_downlink_radio(self, probe_stats_start_epoch=None) -> float:
|
113
|
+
if probe_stats_start_epoch is None:
|
114
|
+
# if no epoch is provided, use the current values
|
115
|
+
gw_tx_count = self.gw_tx_count
|
116
|
+
node_rx_count = self.node_rx_count
|
117
|
+
else:
|
118
|
+
# if epoch is provided, subtract the epoch values from the current values
|
119
|
+
if probe_stats_start_epoch.asn == 0:
|
120
|
+
return 0
|
121
|
+
gw_tx_count = self.gw_tx_count - probe_stats_start_epoch.gw_tx_count
|
122
|
+
node_rx_count = self.node_rx_count - probe_stats_start_epoch.node_rx_count
|
123
|
+
if gw_tx_count <= 0:
|
124
|
+
return 0
|
125
|
+
return node_rx_count / gw_tx_count
|
126
|
+
|
127
|
+
def pdr_uplink_uart(self, probe_stats_start_epoch=None) -> float:
|
128
|
+
if probe_stats_start_epoch is None:
|
129
|
+
# if no epoch is provided, just wait a bit
|
130
|
+
return -1
|
131
|
+
else:
|
132
|
+
# if epoch is provided, subtract the epoch values from the current values
|
133
|
+
if probe_stats_start_epoch.asn == 0:
|
134
|
+
return 0
|
135
|
+
# if a packet was received at the gatweway, it should also be received at the edge (otherwise, it's a loss)
|
136
|
+
gw_rx_count = self.gw_rx_count - probe_stats_start_epoch.gw_rx_count
|
137
|
+
edge_rx_count = self.edge_rx_count - probe_stats_start_epoch.edge_rx_count
|
138
|
+
if gw_rx_count <= 0:
|
139
|
+
return 0
|
140
|
+
return edge_rx_count / gw_rx_count
|
141
|
+
|
142
|
+
def pdr_downlink_uart(self, probe_stats_start_epoch=None) -> float:
|
143
|
+
if probe_stats_start_epoch is None:
|
144
|
+
# if no epoch is provided, just wait a bit
|
145
|
+
return -1
|
146
|
+
else:
|
147
|
+
# if epoch is provided, subtract the epoch values from the current values
|
148
|
+
if probe_stats_start_epoch.asn == 0:
|
149
|
+
return 0
|
150
|
+
# if a packet was sent at the edge, it should also be sent at the gateway (otherwise, it's a loss)
|
151
|
+
gw_tx_count = self.gw_tx_count - probe_stats_start_epoch.gw_tx_count
|
152
|
+
edge_tx_count = self.edge_tx_count - probe_stats_start_epoch.edge_tx_count
|
153
|
+
if edge_tx_count <= 0:
|
154
|
+
return 0
|
155
|
+
return gw_tx_count / edge_tx_count
|
156
|
+
|
157
|
+
def rssi_at_node_dbm(self) -> int:
|
158
|
+
if self.rssi_at_node > 127:
|
159
|
+
return self.rssi_at_node - 255
|
160
|
+
return self.rssi_at_node
|
161
|
+
|
162
|
+
def rssi_at_gw_dbm(self) -> int:
|
163
|
+
if self.rssi_at_gw > 127:
|
164
|
+
return self.rssi_at_gw - 255
|
165
|
+
return self.rssi_at_gw
|
166
|
+
|
167
|
+
def __repr__(self):
|
168
|
+
rep = dataclasses.asdict(self)
|
169
|
+
rep.pop("metadata", None)
|
170
|
+
return f"{rep}"
|
171
|
+
|
172
|
+
|
173
|
+
@dataclass
|
174
|
+
class MetricsRequestPayload(Packet):
|
175
|
+
metadata: list[PacketFieldMetadata] = dataclasses.field(
|
176
|
+
default_factory=lambda: [
|
177
|
+
PacketFieldMetadata(name="type", length=1),
|
178
|
+
PacketFieldMetadata(name="timestamp_us", length=8),
|
179
|
+
]
|
180
|
+
)
|
181
|
+
type_: DefaultPayloadType = DefaultPayloadType.METRICS_REQUEST
|
182
|
+
timestamp_us: int = 0
|
183
|
+
|
184
|
+
|
185
|
+
@dataclass
|
186
|
+
class MetricsResponsePayload(Packet):
|
187
|
+
metadata: list[PacketFieldMetadata] = dataclasses.field(
|
188
|
+
default_factory=lambda: [
|
189
|
+
PacketFieldMetadata(name="type", length=1),
|
190
|
+
PacketFieldMetadata(name="timestamp_us", length=8),
|
191
|
+
PacketFieldMetadata(name="rx_count", length=4),
|
192
|
+
PacketFieldMetadata(name="tx_count", length=4),
|
193
|
+
]
|
194
|
+
)
|
195
|
+
type_: DefaultPayloadType = DefaultPayloadType.METRICS_RESPONSE
|
196
|
+
timestamp_us: int = 0
|
197
|
+
rx_count: int = 0
|
198
|
+
tx_count: int = 0
|
199
|
+
|
200
|
+
|
11
201
|
@dataclass
|
12
202
|
class HeaderStats(Packet):
|
13
203
|
"""Dataclass that holds MAC header stats."""
|
@@ -71,6 +261,20 @@ class Frame:
|
|
71
261
|
stats_bytes = self.stats.to_bytes(byteorder)
|
72
262
|
return header_bytes + stats_bytes + self.payload
|
73
263
|
|
264
|
+
@property
|
265
|
+
def is_test_packet(self) -> bool:
|
266
|
+
"""Returns True if either the payload is a metrics response, request, or load test packet."""
|
267
|
+
return (
|
268
|
+
self.payload.startswith(DefaultPayloadType.METRICS_RESPONSE.as_bytes())
|
269
|
+
or self.payload.startswith(DefaultPayloadType.METRICS_REQUEST.as_bytes())
|
270
|
+
or self.payload.startswith(DefaultPayloadType.METRICS_LOAD.as_bytes())
|
271
|
+
or self.payload.startswith(DefaultPayloadType.METRICS_PROBE.as_bytes())
|
272
|
+
)
|
273
|
+
|
274
|
+
@property
|
275
|
+
def is_load_test_packet(self) -> bool:
|
276
|
+
return self.payload.startswith(DefaultPayloadType.METRICS_LOAD.as_bytes())
|
277
|
+
|
74
278
|
def __repr__(self):
|
75
279
|
header_no_metadata = dataclasses.replace(self.header, metadata=[])
|
76
280
|
return f"Frame(header={header_no_metadata}, payload={self.payload})"
|
marilib/marilib_cloud.py
CHANGED
@@ -3,7 +3,7 @@ from dataclasses import dataclass, field
|
|
3
3
|
from datetime import datetime
|
4
4
|
from typing import Any, Callable
|
5
5
|
|
6
|
-
from marilib.
|
6
|
+
from marilib.metrics import MetricsTester
|
7
7
|
from marilib.mari_protocol import Frame, Header
|
8
8
|
from marilib.model import (
|
9
9
|
EdgeEvent,
|
@@ -34,7 +34,7 @@ class MarilibCloud(MarilibBase):
|
|
34
34
|
logger: Any | None = None
|
35
35
|
gateways: dict[int, MariGateway] = field(default_factory=dict)
|
36
36
|
lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
37
|
-
|
37
|
+
metrics_tester: MetricsTester | None = None
|
38
38
|
|
39
39
|
started_ts: datetime = field(default_factory=datetime.now)
|
40
40
|
last_received_mqtt_data_ts: datetime = field(default_factory=datetime.now)
|
@@ -52,6 +52,9 @@ class MarilibCloud(MarilibBase):
|
|
52
52
|
self.mqtt_interface.init()
|
53
53
|
if self.logger:
|
54
54
|
self.logger.log_setup_parameters(self.setup_params)
|
55
|
+
self.metrics_tester = MetricsTester(
|
56
|
+
self
|
57
|
+
) # just instantiate, do not start it at the cloud, for now
|
55
58
|
|
56
59
|
# ============================ MarilibBase methods =========================
|
57
60
|
|
@@ -168,12 +171,21 @@ class MarilibCloud(MarilibBase):
|
|
168
171
|
gateway_address = frame.header.destination
|
169
172
|
node_address = frame.header.source
|
170
173
|
gateway = self.gateways.get(gateway_address)
|
174
|
+
if not gateway:
|
175
|
+
return False, EdgeEvent.UNKNOWN, None
|
171
176
|
node = gateway.get_node(node_address)
|
172
177
|
if not gateway or not node:
|
173
178
|
return False, EdgeEvent.UNKNOWN, None
|
174
179
|
|
175
180
|
gateway.update_node_liveness(node_address)
|
176
|
-
gateway.register_received_frame(frame
|
181
|
+
gateway.register_received_frame(frame)
|
182
|
+
|
183
|
+
# handle metrics probe packets
|
184
|
+
if frame.is_test_packet:
|
185
|
+
payload = self.metrics_tester.handle_response_cloud(frame, gateway, node)
|
186
|
+
if payload:
|
187
|
+
frame.payload = payload.to_bytes()
|
188
|
+
|
177
189
|
return True, EdgeEvent.NODE_DATA, frame
|
178
190
|
|
179
191
|
except Exception as e:
|
marilib/marilib_edge.py
CHANGED
@@ -4,8 +4,14 @@ from datetime import datetime
|
|
4
4
|
from typing import Any, Callable
|
5
5
|
from rich import print
|
6
6
|
|
7
|
-
from marilib.
|
8
|
-
from marilib.mari_protocol import
|
7
|
+
from marilib.metrics import MetricsTester
|
8
|
+
from marilib.mari_protocol import (
|
9
|
+
MARI_BROADCAST_ADDRESS,
|
10
|
+
Frame,
|
11
|
+
Header,
|
12
|
+
DefaultPayload,
|
13
|
+
DefaultPayloadType,
|
14
|
+
)
|
9
15
|
from marilib.model import (
|
10
16
|
EdgeEvent,
|
11
17
|
GatewayInfo,
|
@@ -19,8 +25,6 @@ from marilib.communication_adapter import MQTTAdapter, MQTTAdapterDummy, SerialA
|
|
19
25
|
from marilib.marilib import MarilibBase
|
20
26
|
from marilib.tui_edge import MarilibTUIEdge
|
21
27
|
|
22
|
-
LOAD_PACKET_PAYLOAD = b"L"
|
23
|
-
|
24
28
|
|
25
29
|
@dataclass
|
26
30
|
class MarilibEdge(MarilibBase):
|
@@ -39,10 +43,11 @@ class MarilibEdge(MarilibBase):
|
|
39
43
|
logger: Any | None = None
|
40
44
|
gateway: MariGateway = field(default_factory=MariGateway)
|
41
45
|
lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
42
|
-
|
46
|
+
metrics_tester: MetricsTester | None = None
|
43
47
|
|
44
48
|
started_ts: datetime = field(default_factory=datetime.now)
|
45
49
|
last_received_serial_data_ts: datetime = field(default_factory=datetime.now)
|
50
|
+
last_received_mqtt_data_ts: datetime = field(default_factory=datetime.now)
|
46
51
|
main_file: str | None = None
|
47
52
|
|
48
53
|
def __post_init__(self):
|
@@ -53,9 +58,9 @@ class MarilibEdge(MarilibBase):
|
|
53
58
|
if self.mqtt_interface is None:
|
54
59
|
self.mqtt_interface = MQTTAdapterDummy()
|
55
60
|
self.serial_interface.init(self.on_serial_data_received)
|
56
|
-
# NOTE: MQTT interface will only be initialized when the network_id is known
|
57
61
|
if self.logger:
|
58
62
|
self.logger.log_setup_parameters(self.setup_params)
|
63
|
+
self.metrics_tester = MetricsTester(self) # it may or not be started later
|
59
64
|
|
60
65
|
# ============================ MarilibBase methods =========================
|
61
66
|
|
@@ -82,19 +87,18 @@ class MarilibEdge(MarilibBase):
|
|
82
87
|
assert self.serial_interface is not None
|
83
88
|
|
84
89
|
mari_frame = Frame(Header(destination=dst), payload=payload)
|
85
|
-
is_test = self._is_test_packet(payload)
|
86
90
|
|
87
91
|
with self.lock:
|
88
|
-
|
89
|
-
self.gateway.register_sent_frame(mari_frame, is_test)
|
92
|
+
self.gateway.register_sent_frame(mari_frame)
|
90
93
|
if dst == MARI_BROADCAST_ADDRESS:
|
91
94
|
for n in self.gateway.nodes:
|
92
|
-
n.register_sent_frame(mari_frame
|
95
|
+
n.register_sent_frame(mari_frame)
|
93
96
|
elif n := self.gateway.get_node(dst):
|
94
|
-
n.register_sent_frame(mari_frame
|
97
|
+
n.register_sent_frame(mari_frame)
|
95
98
|
|
96
|
-
|
97
|
-
|
99
|
+
self.serial_interface.send_data(
|
100
|
+
EdgeEvent.to_bytes(EdgeEvent.NODE_DATA) + mari_frame.to_bytes()
|
101
|
+
)
|
98
102
|
|
99
103
|
def render_tui(self):
|
100
104
|
if self.tui:
|
@@ -110,6 +114,10 @@ class MarilibEdge(MarilibBase):
|
|
110
114
|
def uses_mqtt(self) -> bool:
|
111
115
|
return not isinstance(self.mqtt_interface, MQTTAdapterDummy)
|
112
116
|
|
117
|
+
@property
|
118
|
+
def mqtt_connected(self) -> bool:
|
119
|
+
return self.uses_mqtt and self.mqtt_interface.is_ready()
|
120
|
+
|
113
121
|
@property
|
114
122
|
def serial_connected(self) -> bool:
|
115
123
|
return self.serial_interface is not None
|
@@ -131,6 +139,9 @@ class MarilibEdge(MarilibBase):
|
|
131
139
|
"""Just forwards the data to the serial interface."""
|
132
140
|
if len(data) < 1:
|
133
141
|
return
|
142
|
+
|
143
|
+
self.last_received_mqtt_data_ts = datetime.now()
|
144
|
+
|
134
145
|
try:
|
135
146
|
event_type = EdgeEvent(data[0])
|
136
147
|
frame = Frame().from_bytes(data[1:])
|
@@ -138,23 +149,17 @@ class MarilibEdge(MarilibBase):
|
|
138
149
|
print(f"[red]Error parsing frame: {exc}[/]")
|
139
150
|
return
|
140
151
|
if event_type != EdgeEvent.NODE_DATA:
|
141
|
-
# ignore non-data events
|
142
152
|
return
|
143
153
|
if frame.header.destination != MARI_BROADCAST_ADDRESS and not self.gateway.get_node(
|
144
154
|
frame.header.destination
|
145
155
|
):
|
146
|
-
# ignore frames for unknown nodes
|
147
156
|
return
|
148
157
|
self.send_frame(frame.header.destination, frame.payload)
|
149
158
|
|
150
159
|
def handle_serial_data(self, data: bytes) -> tuple[bool, EdgeEvent, Any]:
|
151
160
|
"""
|
152
|
-
Handles the serial data received from the radio gateway
|
153
|
-
- parses the event
|
154
|
-
- updates node or gateway information
|
155
|
-
- returns the event type and data
|
161
|
+
Handles the serial data received from the radio gateway.
|
156
162
|
"""
|
157
|
-
|
158
163
|
if len(data) < 1:
|
159
164
|
return False, EdgeEvent.UNKNOWN, None
|
160
165
|
|
@@ -196,53 +201,58 @@ class MarilibEdge(MarilibBase):
|
|
196
201
|
frame = Frame().from_bytes(data[1:])
|
197
202
|
with self.lock:
|
198
203
|
self.gateway.update_node_liveness(frame.header.source)
|
199
|
-
self.gateway.register_received_frame(frame
|
204
|
+
self.gateway.register_received_frame(frame)
|
205
|
+
|
206
|
+
# handle metrics probe packets
|
207
|
+
if frame.is_test_packet:
|
208
|
+
payload = self.metrics_tester.handle_response_edge(frame)
|
209
|
+
if payload:
|
210
|
+
frame.payload = payload.to_bytes()
|
211
|
+
|
200
212
|
return True, event_type, frame
|
201
213
|
except (ValueError, ProtocolPayloadParserException):
|
202
214
|
return False, EdgeEvent.UNKNOWN, None
|
203
|
-
|
215
|
+
|
216
|
+
return False, event_type, None
|
204
217
|
|
205
218
|
def on_serial_data_received(self, data: bytes):
|
206
219
|
res, event_type, event_data = self.handle_serial_data(data)
|
207
|
-
if res:
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
220
|
+
if not res:
|
221
|
+
return
|
222
|
+
|
223
|
+
if self.logger and event_type in [EdgeEvent.NODE_JOINED, EdgeEvent.NODE_LEFT]:
|
224
|
+
self.logger.log_event(self.gateway.info.address, event_data.address, event_type.name)
|
225
|
+
if event_type == EdgeEvent.GATEWAY_INFO:
|
226
|
+
self.mqtt_interface.update(event_data.network_id_str, self.on_mqtt_data_received)
|
227
|
+
if self.logger:
|
228
|
+
self.setup_params["schedule_name"] = self.gateway.info.schedule_name
|
229
|
+
self.logger.log_setup_parameters(self.setup_params)
|
230
|
+
|
231
|
+
if event_type == EdgeEvent.NODE_DATA and not event_data.is_test_packet:
|
232
|
+
# only notify the application if it's not a test packet
|
218
233
|
self.cb_application(event_type, event_data)
|
219
|
-
|
234
|
+
|
235
|
+
self.send_data_to_cloud(event_type, event_data)
|
220
236
|
|
221
237
|
def send_data_to_cloud(
|
222
238
|
self, event_type: EdgeEvent, event_data: NodeInfoEdge | GatewayInfo | Frame
|
223
239
|
):
|
224
240
|
if event_type in [EdgeEvent.NODE_JOINED, EdgeEvent.NODE_LEFT, EdgeEvent.NODE_KEEP_ALIVE]:
|
225
|
-
# the cloud needs to know which gateway the node belongs to
|
226
241
|
event_data = event_data.to_cloud(self.gateway.info.address)
|
227
242
|
data = EdgeEvent.to_bytes(event_type) + event_data.to_bytes()
|
228
243
|
self.mqtt_interface.send_data_to_cloud(data)
|
229
244
|
|
230
245
|
# ============================ Utility methods =============================
|
231
246
|
|
232
|
-
def
|
233
|
-
|
234
|
-
self.latency_tester = LatencyTester(self)
|
235
|
-
self.latency_tester.start()
|
247
|
+
def metrics_test_enable(self):
|
248
|
+
self.metrics_tester.start()
|
236
249
|
|
237
|
-
def
|
238
|
-
|
239
|
-
self.latency_tester.stop()
|
240
|
-
self.latency_tester = None
|
250
|
+
def metrics_test_disable(self):
|
251
|
+
self.metrics_tester.stop()
|
241
252
|
|
242
253
|
# ============================ Private methods =============================
|
243
254
|
|
244
255
|
def _is_test_packet(self, payload: bytes) -> bool:
|
245
|
-
"""Determines if a packet is for testing purposes
|
246
|
-
|
247
|
-
|
248
|
-
return is_latency or is_load
|
256
|
+
"""Determines if a packet sent FROM the edge is for testing purposes."""
|
257
|
+
payload = DefaultPayload().from_bytes(payload)
|
258
|
+
return payload.type_ in [DefaultPayloadType.LOAD_TEST, DefaultPayloadType.LATENCY_TEST]
|
marilib/metrics.py
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
import threading
|
2
|
+
import time
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
from rich import print
|
6
|
+
from marilib.mari_protocol import Frame, DefaultPayloadType
|
7
|
+
from marilib.mari_protocol import MetricsProbePayload
|
8
|
+
from marilib.model import MariGateway, MariNode
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from marilib.marilib_edge import MarilibEdge
|
12
|
+
|
13
|
+
|
14
|
+
class MetricsTester:
|
15
|
+
"""A thread-based class to periodically test metrics to all nodes."""
|
16
|
+
|
17
|
+
def __init__(self, marilib: "MarilibEdge", interval: float = 3):
|
18
|
+
self.marilib = marilib
|
19
|
+
self.interval = interval
|
20
|
+
self._stop_event = threading.Event()
|
21
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
22
|
+
|
23
|
+
def start(self):
|
24
|
+
"""Starts the metrics testing thread."""
|
25
|
+
print("[yellow]Metrics tester started.[/]")
|
26
|
+
self._thread.start()
|
27
|
+
|
28
|
+
def stop(self):
|
29
|
+
"""Stops the metrics testing thread."""
|
30
|
+
self._stop_event.set()
|
31
|
+
if self._thread.is_alive():
|
32
|
+
self._thread.join()
|
33
|
+
print("[yellow]Metrics tester stopped.[/]")
|
34
|
+
|
35
|
+
def _run(self):
|
36
|
+
"""The main loop for the testing thread."""
|
37
|
+
# Initial delay to allow nodes to join
|
38
|
+
self._stop_event.wait(self.interval)
|
39
|
+
|
40
|
+
while not self._stop_event.is_set():
|
41
|
+
nodes = list(self.marilib.nodes)
|
42
|
+
if not nodes:
|
43
|
+
self._stop_event.wait(self.interval)
|
44
|
+
continue
|
45
|
+
|
46
|
+
for node in nodes:
|
47
|
+
if self._stop_event.is_set():
|
48
|
+
break
|
49
|
+
self.send_metrics_request(node, "edge")
|
50
|
+
# Spread the requests evenly over the interval
|
51
|
+
sleep_duration = self.interval / len(nodes)
|
52
|
+
self._stop_event.wait(sleep_duration)
|
53
|
+
|
54
|
+
def timestamp_us(self) -> int:
|
55
|
+
"""Returns the current time in microseconds."""
|
56
|
+
return int(time.time() * 1000 * 1000)
|
57
|
+
|
58
|
+
def send_metrics_request(self, node: MariNode, marilib_type: str):
|
59
|
+
"""Sends a metrics request packet to a specific address."""
|
60
|
+
payload = MetricsProbePayload()
|
61
|
+
if marilib_type == "edge":
|
62
|
+
payload.edge_tx_ts_us = self.timestamp_us()
|
63
|
+
payload.edge_tx_count = node.probe_increment_tx_count()
|
64
|
+
elif marilib_type == "cloud":
|
65
|
+
payload.cloud_tx_ts_us = self.timestamp_us()
|
66
|
+
payload.cloud_tx_count = node.probe_increment_tx_count()
|
67
|
+
# print(f">>> sending metrics probe to {node.address:016x}: {payload}")
|
68
|
+
payload = payload.to_bytes()
|
69
|
+
# print(f" size is {len(payload)} bytes: {payload.hex()}\n")
|
70
|
+
self.marilib.send_frame(node.address, payload)
|
71
|
+
|
72
|
+
def handle_response_edge(self, frame: Frame):
|
73
|
+
"""
|
74
|
+
Processes a metrics response frame.
|
75
|
+
This should be called when a LATENCY_DATA event is received.
|
76
|
+
"""
|
77
|
+
node = self.marilib.gateway.get_node(frame.header.source)
|
78
|
+
if not node:
|
79
|
+
print(f"[red]Node not found: {frame.header.source:016x}[/]")
|
80
|
+
return
|
81
|
+
|
82
|
+
try:
|
83
|
+
payload = MetricsProbePayload().from_bytes(frame.payload)
|
84
|
+
if payload.type_ != DefaultPayloadType.METRICS_PROBE:
|
85
|
+
print(f"[red]Expected METRICS_PROBE, got {payload.type_}[/]")
|
86
|
+
return
|
87
|
+
|
88
|
+
except Exception as e:
|
89
|
+
print(f"[red]Error parsing metrics response: {e}[/]")
|
90
|
+
return
|
91
|
+
|
92
|
+
payload.edge_rx_ts_us = self.timestamp_us()
|
93
|
+
payload.edge_rx_count = node.probe_increment_rx_count()
|
94
|
+
|
95
|
+
node.save_probe_stats(payload)
|
96
|
+
|
97
|
+
# print(f"<<< received metrics probe from {frame.header.source:016x}: {payload}")
|
98
|
+
# print(f" size is {len(frame.payload)} bytes: {frame.payload.hex()}\n")
|
99
|
+
|
100
|
+
# print(f" latency_roundtrip_node_edge_ms: {payload.latency_roundtrip_node_edge_ms()}")
|
101
|
+
# print(f" pdr_uplink_radio: {payload.pdr_uplink_radio(node.probe_stats_start_epoch)}")
|
102
|
+
# print(f" pdr_downlink_radio: {payload.pdr_downlink_radio(node.probe_stats_start_epoch)}")
|
103
|
+
# print(f" pdr_uplink_uart: {payload.pdr_uplink_uart(node.probe_stats_start_epoch)}")
|
104
|
+
# print(f" pdr_downlink_uart: {payload.pdr_downlink_uart(node.probe_stats_start_epoch)}")
|
105
|
+
# print(f" rssi_at_node_dbm: {payload.rssi_at_node_dbm()}")
|
106
|
+
# print(f" rssi_at_gw_dbm: {payload.rssi_at_gw_dbm()}")
|
107
|
+
|
108
|
+
return payload
|
109
|
+
|
110
|
+
def handle_response_cloud(self, frame: Frame, gateway: MariGateway, node: MariNode):
|
111
|
+
"""
|
112
|
+
Processes a metrics response frame.
|
113
|
+
This should be called when a LATENCY_DATA event is received.
|
114
|
+
"""
|
115
|
+
try:
|
116
|
+
payload = MetricsProbePayload().from_bytes(frame.payload)
|
117
|
+
if payload.type_ != DefaultPayloadType.METRICS_PROBE:
|
118
|
+
print(f"[red]Expected METRICS_PROBE, got {payload.type_}[/]")
|
119
|
+
return
|
120
|
+
|
121
|
+
except Exception as e:
|
122
|
+
print(f"[red]Error parsing metrics response: {e}[/]")
|
123
|
+
return
|
124
|
+
|
125
|
+
payload.cloud_rx_ts_us = self.timestamp_us()
|
126
|
+
payload.cloud_rx_count = node.probe_increment_rx_count()
|
127
|
+
|
128
|
+
node.save_probe_stats(payload)
|
129
|
+
|
130
|
+
# print(f"<<< received metrics probe from {frame.header.source:016x}: {payload}")
|
131
|
+
# print(f" size is {len(frame.payload)} bytes: {frame.payload.hex()}\n")
|
132
|
+
|
133
|
+
# print(f" latency_roundtrip_node_edge_ms: {payload.latency_roundtrip_node_edge_ms()}")
|
134
|
+
# print(f" pdr_uplink_radio: {payload.pdr_uplink_radio(node.probe_stats_start_epoch)}")
|
135
|
+
# print(f" pdr_downlink_radio: {payload.pdr_downlink_radio(node.probe_stats_start_epoch)}")
|
136
|
+
# print(f" pdr_uplink_uart: {payload.pdr_uplink_uart(node.probe_stats_start_epoch)}")
|
137
|
+
# print(f" pdr_downlink_uart: {payload.pdr_downlink_uart(node.probe_stats_start_epoch)}")
|
138
|
+
# print(f" rssi_at_node_dbm: {payload.rssi_at_node_dbm()}")
|
139
|
+
# print(f" rssi_at_gw_dbm: {payload.rssi_at_gw_dbm()}")
|
140
|
+
|
141
|
+
return payload
|