Skip to content

Commit 467790e

Browse files
Added HTML montior class for piping data to a webserver over a socket
1 parent e0798bb commit 467790e

File tree

8 files changed

+177
-89
lines changed

8 files changed

+177
-89
lines changed

html_monitor_example.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from flask import Flask, render_template
2+
from flask_socketio import SocketIO
3+
import asyncio
4+
import threading
5+
6+
from app_monitor import SocketManager
7+
from app_monitor.server import OrderedDecoder, SerialUpdateServer
8+
from app_monitor.elements_base import TextElement
9+
10+
app = Flask(__name__)
11+
app.config["SECRET_KEY"] = "secret!"
12+
socketio = SocketIO(app)
13+
14+
# Initialize SocketManager with Flask-SocketIO instance
15+
manager = SocketManager(socketio=socketio, frequency=20) # 20Hz update rate
16+
17+
# Define and add elements to the manager
18+
text_element1 = TextElement(static_text="Element1: ", element_id="element1")
19+
text_element2 = TextElement(static_text="Element2: ", element_id="element2")
20+
text_element3 = TextElement(static_text="Element3: ", element_id="element3")
21+
manager.add_element(text_element1)
22+
manager.add_element(text_element2)
23+
manager.add_element(text_element3)
24+
25+
# Set up the Serial Update Server
26+
server = SerialUpdateServer(
27+
manager,
28+
port="/dev/tty.usbserial-1460",
29+
baudrate=115200,
30+
decoder=OrderedDecoder(keys=["element1", "element2", "element3"]),
31+
)
32+
33+
34+
@app.route("/")
35+
def index():
36+
return render_template("example.html")
37+
38+
39+
def run_flask():
40+
"""Run Flask in a separate thread."""
41+
socketio.run(app, port=5000)
42+
43+
44+
async def main():
45+
# Start the Flask app in a separate thread
46+
flask_thread = threading.Thread(target=run_flask)
47+
flask_thread.start()
48+
49+
# Start the asyncio tasks
50+
await asyncio.gather(manager.push_data(), server.start(frequency=20))
51+
52+
53+
if __name__ == "__main__":
54+
asyncio.run(main())

html_test.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from config_loader import load_configs
1818

1919
from app_monitor import (
20-
MonitorManager,
20+
TerminalManager,
2121
ProgressBar,
2222
Table,
2323
RangeBar,
@@ -43,7 +43,7 @@ async def main():
4343
logging.info(configs["application"])
4444

4545
# Create a MonitorManager instances
46-
manager = MonitorManager()
46+
manager = TerminalManager()
4747

4848
# Add a progress bar and table elements with formatting
4949
BAR_WIDTH = 30

src/app_monitor/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
__version__ = "0.0.2"
1+
__version__ = "0.1.0"
22

33

4-
from .app_monitor import MonitorManager
4+
from .app_monitor import TerminalManager, SocketManager
55
from .elements_base import (
66
ProgressBar,
77
Table,

src/app_monitor/app_monitor.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,29 @@ def update(self, element_id, *args):
4747
element.update(*args)
4848
break
4949

50-
def update_all_elements(self):
50+
def generate_element_id_map(self):
51+
"""Generate a list of all element IDs in the monitor manager."""
52+
element_count = 0
53+
54+
def _element_id_generator(elements):
55+
"""Recursively generate element IDs."""
56+
nonlocal element_count
57+
for element in elements:
58+
if isinstance(element, MonitorGroup):
59+
yield from _element_id_generator(element.elements.values())
60+
else:
61+
yield element_count, element.element_id
62+
element_count += 1
63+
64+
return {
65+
str(number): element_id
66+
for number, element_id in _element_id_generator(self.elements)
67+
}
68+
69+
70+
class TerminalManager(MonitorManager):
71+
72+
def update_screen_buffer(self):
5173
"""Construct the full screen content in a buffer"""
5274
self.buffer = [] # Clear the buffer for the new frame
5375

@@ -73,42 +95,67 @@ async def update_screen_fixed_rate(self, frequency=1):
7395
self.update_screen() # Display the current metrics
7496
await asyncio.sleep(1 / frequency) # Wait for the next update cycle
7597

76-
async def update_all_elements_fixed_rate(self, frequency=1):
98+
async def update_screen_buffer_fixed_rate(self, frequency=1):
7799
"""Asynchronously update all elements at a fixed rate."""
78100
assert frequency > 0, "Frequency must be greater than 0."
79101
while True:
80-
self.update_all_elements()
102+
self.update_screen_buffer()
81103
await asyncio.sleep(1 / frequency)
82104

83105
async def update_fixed_rate(self, frequency=30):
84106
await asyncio.gather(
85107
self.update_screen_fixed_rate(frequency=frequency),
86-
self.update_all_elements_fixed_rate(frequency=frequency),
108+
self.update_screen_buffer_fixed_rate(frequency=frequency),
87109
)
88110

89-
def generate_element_id_map(self):
90-
"""Generate a list of all element IDs in the monitor manager."""
91-
element_count = 0
92111

93-
def _element_id_generator(elements):
94-
"""Recursively generate element IDs."""
95-
nonlocal element_count
96-
for element in elements:
97-
if isinstance(element, MonitorGroup):
98-
yield from _element_id_generator(element.elements.values())
99-
else:
100-
yield element_count, element.element_id
101-
element_count += 1
112+
import asyncio
113+
import json
114+
from flask_socketio import SocketIO
102115

103-
return {
104-
str(number): element_id
105-
for number, element_id in _element_id_generator(self.elements)
106-
}
116+
117+
class SocketManager(MonitorManager):
118+
"""Subclass of MonitorManager that adds functionality to push data to a WebSocket."""
119+
120+
def __init__(self, socketio: SocketIO, frequency=1):
121+
"""
122+
Initialize with a SocketIO instance and a push frequency.
123+
124+
:param socketio: SocketIO instance to handle WebSocket communication.
125+
:param frequency: Frequency in Hz for pushing updates to clients.
126+
"""
127+
super().__init__() # Initialize the parent MonitorManager
128+
self.socketio = socketio
129+
self.frequency = frequency
130+
131+
def to_json(self):
132+
"""Convert all monitor elements to JSON format."""
133+
data = {}
134+
for element in self.elements:
135+
if isinstance(element, MonitorGroup):
136+
data[element.group_id] = {
137+
e_id: el.display() for e_id, el in element.elements.items()
138+
}
139+
else:
140+
data[element.element_id] = element.display()
141+
return json.dumps(data)
142+
143+
async def push_data(self):
144+
"""Asynchronously push data to all connected WebSocket clients at the specified frequency."""
145+
while True:
146+
data = self.to_json() # Get data in JSON format
147+
self.socketio.emit("update", data) # Push data to WebSocket clients
148+
await asyncio.sleep(1 / self.frequency) # Control push frequency
149+
150+
def set_frequency(self, frequency):
151+
"""Set the frequency at which data is pushed to clients."""
152+
assert frequency > 0, "Frequency must be greater than 0."
153+
self.frequency = frequency
107154

108155

109156
async def main():
110157
# Create a MonitorManager instance
111-
manager = MonitorManager()
158+
manager = TerminalManager()
112159

113160
# Add a text element and a progress bar
114161
text = TextElement("Buffers")

src/app_monitor/elements_base.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,16 @@ class TextElement(MonitorElement):
108108

109109
id_generator = id_generator("text")
110110

111-
def __init__(self, text, static_text=None, element_id=None, text_format=None, width=MAX_MONITOR_WIDTH):
111+
def __init__(
112+
self,
113+
text=None,
114+
static_text=None,
115+
element_id=None,
116+
text_format=None,
117+
width=MAX_MONITOR_WIDTH,
118+
):
112119
super().__init__(element_id, width=width)
113-
self.text = text
120+
self.text = "" if text is None else str(text)
114121
self.text_format = text_format
115122
self.static_text = static_text
116123

@@ -122,7 +129,7 @@ def display(self):
122129
"""Generate the text element for display."""
123130
# Combine static and dynamic text
124131
full_text = self.static_text + self.text if self.static_text else self.text
125-
132+
126133
# Format the text with padding
127134
padded_text = full_text.ljust(self.width)
128135

@@ -139,7 +146,6 @@ def get_height(self):
139146
return self.text.count("\n") + 1
140147

141148

142-
143149
class ProgressBar(MonitorElement):
144150
"""Class for rendering a progress bar."""
145151

templates/example.html

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Monitor Display</title>
7+
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
8+
<style>
9+
body {
10+
font-family: Arial, sans-serif;
11+
padding: 20px;
12+
}
13+
.element {
14+
padding: 5px 0;
15+
}
16+
</style>
17+
</head>
18+
<body>
19+
<h1>Monitor Display</h1>
20+
<div id="monitor"></div>
21+
22+
<script>
23+
const monitorDiv = document.getElementById('monitor');
24+
const socket = io();
25+
26+
// Listen for updates from the server
27+
socket.on('update', function(data) {
28+
const parsedData = JSON.parse(data);
29+
monitorDiv.innerHTML = ''; // Clear previous content
30+
31+
// Display each element_id: text pair
32+
for (const [element_id, text] of Object.entries(parsedData)) {
33+
const elementDiv = document.createElement('div');
34+
elementDiv.classList.add('element');
35+
elementDiv.textContent = `${text}`;
36+
monitorDiv.appendChild(elementDiv);
37+
}
38+
});
39+
</script>
40+
</body>
41+
</html>

templates/index.html

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)