marilib-pkg 0.6.0__tar.gz → 0.7.0rc1__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.
Files changed (36) hide show
  1. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/PKG-INFO +25 -3
  2. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/README.md +24 -2
  3. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/frames.py +7 -1
  4. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/mari_cloud.py +25 -7
  5. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/mari_edge.py +4 -7
  6. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/mari_edge_stats.py +26 -14
  7. marilib_pkg-0.7.0rc1/examples/raspberry-pi/sense_hat_ui.py +244 -0
  8. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/__init__.py +1 -1
  9. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/communication_adapter.py +4 -3
  10. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/logger.py +41 -23
  11. marilib_pkg-0.7.0rc1/marilib/mari_protocol.py +280 -0
  12. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/marilib_cloud.py +15 -3
  13. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/marilib_edge.py +57 -47
  14. marilib_pkg-0.7.0rc1/marilib/metrics.py +141 -0
  15. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/model.py +221 -38
  16. marilib_pkg-0.7.0rc1/marilib/pdr.py +99 -0
  17. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/serial_uart.py +7 -6
  18. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/tui_cloud.py +26 -1
  19. marilib_pkg-0.7.0rc1/marilib/tui_edge.py +293 -0
  20. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/pyproject.toml +4 -2
  21. marilib_pkg-0.6.0/marilib/latency.py +0 -78
  22. marilib_pkg-0.6.0/marilib/mari_protocol.py +0 -76
  23. marilib_pkg-0.6.0/marilib/tui_edge.py +0 -185
  24. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/.gitignore +0 -0
  25. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/AUTHORS +0 -0
  26. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/LICENSE +0 -0
  27. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/mari_cloud_minimal.py +0 -0
  28. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/mari_edge_minimal.py +0 -0
  29. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/examples/uart.py +0 -0
  30. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/marilib.py +0 -0
  31. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/protocol.py +0 -0
  32. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/serial_hdlc.py +0 -0
  33. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/marilib/tui.py +0 -0
  34. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/tests/__init__.py +0 -0
  35. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/tests/test_hdlc.py +0 -0
  36. {marilib_pkg-0.6.0 → marilib_pkg-0.7.0rc1}/tests/test_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: marilib-pkg
3
- Version: 0.6.0
3
+ Version: 0.7.0rc1
4
4
  Summary: MariLib is a Python library for interacting with the Mari network.
5
5
  Project-URL: Homepage, https://github.com/DotBots/marilib
6
6
  Project-URL: Bug Tracker, https://github.com/DotBots/marilib/issues
@@ -25,14 +25,36 @@ Description-Content-Type: text/markdown
25
25
  # MariLib 💫 👀 🐍
26
26
 
27
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.
28
+ It connects to a Mari gateway via:
29
+ - UART, using MarilibEdge
30
+ - MQTT, using MarilibCloud
29
31
 
30
32
  ## Example with TUI
31
33
  MariLib provides a stateful class with gateway and node information, network statistics, and a rich real-time TUI:
32
34
 
33
35
  [mari-edge-2.webm](https://github.com/user-attachments/assets/fe50f2ba-8e67-4522-8700-69730f8e3aee)
34
36
 
35
- See the how it works in `examples/basic.py`.
37
+ To run with a gateway connected via UART:
38
+ ```bash
39
+ # for example, using the Inria Argus MQTT broker
40
+ (.venv) $ python examples/mari_edge.py -m mqtts://argus.paris.inria.fr:8883
41
+ ```
42
+ You can see how it works using `examples/mari_edge.py --help`.
43
+
44
+ To run with a gateway connected via MQTT:
45
+ ```bash
46
+ # for example, using the Inria Argus MQTT broker
47
+ (.venv) $ python examples/mari_cloud.py -n 0x0100 -m mqtts://argus.paris.inria.fr:8883
48
+ ```
49
+
50
+ ## Setup and dependencies
51
+ To setup the environment, do:
52
+
53
+ ```bash
54
+ $ python -m venv .venv
55
+ $ source .venv/bin/activate
56
+ (.venv) $ pip install -e .
57
+ ```
36
58
 
37
59
  ## Minimal example
38
60
  Here is a minimal example showcasing how to use MariLib:
@@ -1,14 +1,36 @@
1
1
  # MariLib 💫 👀 🐍
2
2
 
3
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.
4
+ It connects to a Mari gateway via:
5
+ - UART, using MarilibEdge
6
+ - MQTT, using MarilibCloud
5
7
 
6
8
  ## Example with TUI
7
9
  MariLib provides a stateful class with gateway and node information, network statistics, and a rich real-time TUI:
8
10
 
9
11
  [mari-edge-2.webm](https://github.com/user-attachments/assets/fe50f2ba-8e67-4522-8700-69730f8e3aee)
10
12
 
11
- See the how it works in `examples/basic.py`.
13
+ To run with a gateway connected via UART:
14
+ ```bash
15
+ # for example, using the Inria Argus MQTT broker
16
+ (.venv) $ python examples/mari_edge.py -m mqtts://argus.paris.inria.fr:8883
17
+ ```
18
+ You can see how it works using `examples/mari_edge.py --help`.
19
+
20
+ To run with a gateway connected via MQTT:
21
+ ```bash
22
+ # for example, using the Inria Argus MQTT broker
23
+ (.venv) $ python examples/mari_cloud.py -n 0x0100 -m mqtts://argus.paris.inria.fr:8883
24
+ ```
25
+
26
+ ## Setup and dependencies
27
+ To setup the environment, do:
28
+
29
+ ```bash
30
+ $ python -m venv .venv
31
+ $ source .venv/bin/activate
32
+ (.venv) $ pip install -e .
33
+ ```
12
34
 
13
35
  ## Minimal example
14
36
  Here is a minimal example showcasing how to use MariLib:
@@ -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())
@@ -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 mari.nodes:
60
- mari.send_frame(MARI_BROADCAST_ADDRESS, NORMAL_DATA_PAYLOAD)
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(0.5)
80
+ time.sleep(1)
63
81
 
64
82
  except KeyboardInterrupt:
65
83
  pass
@@ -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="mqtt://localhost:1883",
34
- show_default=True,
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, NORMAL_DATA_PAYLOAD)
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:
@@ -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(MARI_BROADCAST_ADDRESS, LOAD_PACKET_PAYLOAD)
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.from_host_port(mqtt_host, is_edge=True) if mqtt_host else None,
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.latency_test_enable()
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
- normal_traffic_interval = 0.5
128
- last_normal_send_time = 0
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 current_time - last_normal_send_time >= normal_traffic_interval:
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, NORMAL_DATA_PAYLOAD)
151
+ mari.send_frame(MARI_BROADCAST_ADDRESS, DefaultPayload().to_bytes())
140
152
  last_normal_send_time = current_time
141
153
 
142
- time.sleep(0.1)
154
+ time.sleep(1)
143
155
 
144
156
  except KeyboardInterrupt:
145
157
  pass
146
158
  finally:
147
159
  stop_event.set()
148
- mari.latency_test_disable()
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
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.0"
1
+ __version__ = "0.7.0-rc1"
2
2
 
3
3
 
4
4
  # declare these just to avoid circular imports
@@ -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.serial.flush()
57
- encoded = hdlc_encode(data)
58
- self.serial.write(encoded)
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):
@@ -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
- "rssi_dbm_5s",
112
- "last_latency_ms",
113
- "avg_latency_ms",
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.latency_stats.avg_ms:.2f}",
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.stats.received_rssi_dbm(5),
173
- f"{node.latency_stats.last_ms:.2f}",
174
- f"{node.latency_stats.avg_ms:.2f}",
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