Skip to content

Commit 7b2af1b

Browse files
committed
Merge branch 'main' of https://github.com/davidson-engineering/remote-app-monitor into davidson-engineering-main
2 parents 97e9765 + 5e1e4f8 commit 7b2af1b

File tree

9 files changed

+639
-78
lines changed

9 files changed

+639
-78
lines changed

README.md

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,120 @@
1-
# project_name
2-
### project_summary
1+
# Remote App Monitor
32

4-
project description
3+
`remote-app-monitor` is a Python-based tool designed to facilitate remote monitoring of applications by creating and managing monitor elements like progress bars, tables, and range bars. It allows for efficient, customizable data visualization and monitoring from both serial and ZeroMQ data sources.
54

5+
## Features
66

7+
- **Modular Monitors**: Configure multiple monitor elements such as RangeBars, Log Monitors, and Tables.
8+
- **Group Functionality**: Group elements under specific IDs for easy reference and enhanced organization (e.g., `X.velocity`, `X.torque`).
9+
- **Asynchronous Updating**: Updates monitored data asynchronously at a set rate, independent of data input frequency.
10+
- **Serial and ZeroMQ Communication**: Supports both serial and ZeroMQ data sources, enabling flexible data streaming.
11+
- **ANSI-Formatted Terminal Output**: Customize terminal output with ANSI colors and formatting for clear visual differentiation.
12+
13+
## Installation
14+
15+
1. **Clone the Repository**:
16+
```bash
17+
git clone https://github.com/davidson-engineering/remote-app-monitor.git
18+
cd remote-app-monitor
19+
```
20+
21+
2. **Install Dependencies**:
22+
Make sure you have Python 3.7+ installed, then install dependencies:
23+
```bash
24+
pip install .
25+
```
26+
27+
## Usage
28+
29+
### Basic Setup
30+
31+
1. **Initialize the Monitor**:
32+
```python
33+
from remote_app_monitor import Monitor
34+
35+
monitor = Monitor()
36+
```
37+
38+
2. **Add Monitor Elements**:
39+
Add various monitor elements like `RangeBar`, `LogMonitor`, or `Table` with unique IDs:
40+
```python
41+
monitor.add_range_bar("X.velocity", min_val=0, max_val=100)
42+
monitor.add_log("X.torque", timestamp=True)
43+
monitor.add_table("system_status")
44+
```
45+
46+
3. **Start Monitoring**:
47+
Start the monitor asynchronously. Data can then be sent to update each element.
48+
```python
49+
monitor.start()
50+
```
51+
52+
### Serial Server
53+
54+
The `remote-app-monitor` includes a **Serial Server** to handle communication with serial devices, enabling real-time monitoring from an Arduino or similar microcontroller. The Serial Server is optimized for speed, using a custom protocol to minimize overhead.
55+
56+
1. **Initialize the Serial Server**:
57+
```python
58+
from remote_app_monitor import SerialServer
59+
60+
serial_server = SerialServer(port='COM3', baudrate=9600)
61+
monitor.add_server(serial_server)
62+
```
63+
64+
2. **Start Serial Communication**:
65+
Once the Serial Server is added, the monitor will handle incoming data:
66+
```python
67+
serial_server.start()
68+
```
69+
70+
3. **Updating Elements via Serial**:
71+
Data packets received over serial should match the monitor element IDs (e.g., `X.velocity`, `X.torque`). The Serial Server will parse and update the respective elements.
72+
73+
### ZeroMQ Server
74+
75+
`remote-app-monitor` also includes a **ZeroMQ Server** to support scalable data communication through a PUB-SUB pattern, ideal for real-time, remote monitoring from multiple data sources.
76+
77+
1. **Initialize the ZeroMQ Server**:
78+
```python
79+
from remote_app_monitor import ZeroMQServer
80+
81+
zmq_server = ZeroMQServer("tcp://localhost:5555")
82+
monitor.add_server(zmq_server)
83+
```
84+
85+
2. **Start ZeroMQ Communication**:
86+
Similar to the Serial Server, the ZeroMQ server will handle incoming data from remote publishers:
87+
```python
88+
zmq_server.start()
89+
```
90+
91+
3. **Publishing Data to ZeroMQ**:
92+
From a remote data source, publish updates that match monitor element IDs (e.g., `X.velocity`, `X.torque`). ZeroMQ will parse and distribute the data to the correct elements.
93+
94+
### Example: Grouping Elements
95+
96+
Elements can be grouped under an ID to allow for structured, hierarchical monitoring. For example, `X` could represent an axis, with `X.velocity` and `X.torque` representing specific monitored metrics:
97+
98+
```python
99+
monitor.add_range_bar("X.velocity", min_val=0, max_val=100)
100+
monitor.add_range_bar("X.torque", min_val=0, max_val=500)
101+
monitor.add_log("X.status", timestamp=True)
7102
```
8-
some demonstrative code
9-
```
103+
104+
### Example: Socket Based Remote Monitor
105+
106+
The SocketManager class can be used to push data to a socket that can be fed to a service such as an HTML page.
107+
See the file [html_monitor_example.py](https://github.com/davidson-engineering/remote-app-monitor/blob/f40e98c6b4bc729f2f2e2dbddc4e4c305bfd0a42/html_monitor_example.py)
108+
<img width="863" alt="image" src="https://github.com/user-attachments/assets/25d4d863-92a9-4207-bdc9-0849cbb3f10c">
109+
110+
## Customization
111+
112+
Each monitor element can be customized with parameters during initialization:
113+
114+
- **RangeBar**: Configure min/max values, colors, and update frequency.
115+
- **LogMonitor**: Set timestamps, log format, and custom colors.
116+
- **Table**: Define columns, row structure, and update formats.
117+
118+
## License
119+
120+
This project is licensed under the MIT License.

html_monitor_example.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
from app_monitor.text_formatter import TextFormat
10+
11+
app = Flask(__name__)
12+
app.config["SECRET_KEY"] = "secret!"
13+
socketio = SocketIO(app)
14+
15+
# Initialize SocketManager with Flask-SocketIO instance
16+
manager = SocketManager(socketio=socketio, frequency=20) # 20Hz update rate
17+
18+
text_format = TextFormat(width=9, precision=3, force_sign=True)
19+
20+
data_formats = {
21+
"position_x": text_format,
22+
"position_y": text_format,
23+
"position_z": text_format,
24+
"motor1_speed": None,
25+
"motor1_torque": None,
26+
"motor1_status": None,
27+
"motor2_speed": None,
28+
"motor2_torque": None,
29+
"motor2_status": None,
30+
"motor3_speed": None,
31+
"motor3_torque": None,
32+
"motor3_status": None,
33+
"motor4_speed": None,
34+
"motor4_torque": None,
35+
"motor4_status": None,
36+
}
37+
# Define and add elements to the manager
38+
for name, format in data_formats.items():
39+
text_element = TextElement(element_id=name, text_format=format)
40+
manager.add_element(text_element)
41+
42+
# Set up the Serial Update Server
43+
server = SerialUpdateServer(
44+
manager,
45+
detect_devices=True,
46+
baudrate=115200,
47+
decoder=OrderedDecoder(keys=data_formats.keys()),
48+
)
49+
50+
51+
@app.route("/")
52+
def index():
53+
return render_template("example.html")
54+
55+
56+
def run_flask():
57+
"""Run Flask in a separate thread."""
58+
socketio.run(app, port=5000)
59+
60+
61+
async def main():
62+
# Start the Flask app in a separate thread
63+
flask_thread = threading.Thread(target=run_flask)
64+
flask_thread.start()
65+
66+
# Start the asyncio tasks
67+
await asyncio.gather(manager.push_data(), server.start(frequency=20))
68+
69+
70+
if __name__ == "__main__":
71+
asyncio.run(main())

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
__version__ = "0.1.1"
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")

0 commit comments

Comments
 (0)