Skip to content

Commit 68d21d0

Browse files
updated example UI
Added new MachineState monitor for parsing binary states to bool states
1 parent f40e98c commit 68d21d0

File tree

5 files changed

+408
-29
lines changed

5 files changed

+408
-29
lines changed

html_monitor_example.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
from flask_socketio import SocketIO
33
import asyncio
44
import threading
5+
import logging
56

67
from app_monitor import SocketManager
78
from app_monitor.server import OrderedDecoder, SerialUpdateServer
8-
from app_monitor.elements_base import TextElement
9+
from app_monitor.elements_base import TextElement, MachineState
910
from app_monitor.text_formatter import TextFormat
1011

12+
logging.basicConfig(level=logging.INFO)
13+
1114
app = Flask(__name__)
1215
app.config["SECRET_KEY"] = "secret!"
1316
socketio = SocketIO(app)
1417

1518
# Initialize SocketManager with Flask-SocketIO instance
16-
manager = SocketManager(socketio=socketio, frequency=20) # 20Hz update rate
19+
manager = SocketManager(socketio=socketio, frequency=10) # 20Hz update rate
1720

1821
text_format = TextFormat(width=9, precision=3, force_sign=True)
1922

@@ -33,11 +36,25 @@
3336
"motor4_speed": None,
3437
"motor4_torque": None,
3538
"motor4_status": None,
39+
"machine_status": None,
3640
}
41+
42+
machine_states = [
43+
"deadman_switch",
44+
"motors_enabled",
45+
"emergency_stop",
46+
"rapid_traverse",
47+
"fine_control",
48+
"machine_mode",
49+
]
50+
3751
# Define and add elements to the manager
3852
for name, format in data_formats.items():
39-
text_element = TextElement(element_id=name, text_format=format)
40-
manager.add_element(text_element)
53+
if name == "machine_status":
54+
element = MachineState(element_id=name, states=machine_states)
55+
else:
56+
element = TextElement(element_id=name, text_format=format)
57+
manager.add_element(element)
4158

4259
# Set up the Serial Update Server
4360
server = SerialUpdateServer(

src/app_monitor/app_monitor.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,16 @@ def __init__(self, socketio: SocketIO, frequency=1):
130130

131131
def to_json(self):
132132
"""Convert all monitor elements to JSON format."""
133-
data = {}
133+
data = []
134134
for element in self.elements:
135135
if isinstance(element, MonitorGroup):
136-
data[element.group_id] = {
137-
e_id: el.display() for e_id, el in element.elements.items()
138-
}
136+
raise NotImplementedError(
137+
"MonitorGroup is not supported in JSON format."
138+
)
139139
else:
140-
data[element.element_id] = element.display()
140+
data.append(element.as_dict())
141+
# Merge and flatten all element dict objects into a single JSON object
142+
data = {key: value for element in data for key, value in element.items()}
141143
return json.dumps(data)
142144

143145
async def push_data(self):

src/app_monitor/elements_base.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from datetime import datetime
23
from tabulate import tabulate
34

@@ -33,6 +34,10 @@ def update(self, data):
3334
"""Update element with data."""
3435
raise NotImplementedError("Subclasses must implement the update method")
3536

37+
def as_dict(self):
38+
"""Return the element as a dictionary."""
39+
return {self.element_id: self.display()}
40+
3641
def add_border(self, content):
3742
"""Wrap content in a simple border if self.border is True."""
3843
if not self.border:
@@ -79,7 +84,9 @@ def display(self):
7984
element_displays = "\n".join(
8085
element.display() for element in self.elements.values()
8186
)
82-
return self.add_border(element_displays)
87+
if self.border:
88+
return self.add_border(element_displays)
89+
return element_displays
8390

8491
def add_border(self, content):
8592
"""Wrap the content of the group in a border with a group ID as a header."""
@@ -135,11 +142,6 @@ def display(self):
135142

136143
return full_text
137144

138-
# # Format the text with padding
139-
# padded_text = full_text.ljust(self.width)
140-
141-
# return self.add_border(padded_text)
142-
143145
def get_height(self):
144146
"""Calculate the number of lines the text element occupies."""
145147
return self.text.count("\n") + 1
@@ -477,3 +479,61 @@ def add_border(self, content):
477479
bordered_content = [f"| {line} |" for line in lines]
478480
border_bottom = "+" + "-" * (self.width - 2) + "+"
479481
return "\n".join([header_line] + bordered_content + [border_bottom])
482+
483+
484+
class IndicatorLamp(MonitorElement):
485+
"""Class for rendering an indicator lamp element."""
486+
487+
id_generator = id_generator("lamp")
488+
489+
def __init__(
490+
self,
491+
element_id=None,
492+
label="Lamp",
493+
on_color="green",
494+
off_color="red",
495+
width=MAX_MONITOR_WIDTH,
496+
):
497+
super().__init__(element_id)
498+
self.label = label
499+
self.on_color = on_color
500+
self.off_color = off_color
501+
self.width = width
502+
self.state = False
503+
504+
def update(self, state):
505+
"""Update the indicator lamp state."""
506+
self.state = bool(state)
507+
508+
def display(self):
509+
"""Generate the indicator lamp for display."""
510+
color = self.on_color if self.state else self.off_color
511+
return f"{self.label}: \033[1;{color}m●\033[0m".ljust(self.width)
512+
513+
def as_dict(self):
514+
return {self.element_id: self.state}
515+
516+
def get_height(self):
517+
"""Indicator lamp only occupies one line."""
518+
return 1
519+
520+
521+
class MachineState(MonitorElement):
522+
523+
def __init__(self, element_id=None, states: list[str] = None):
524+
super().__init__(element_id)
525+
self.states = states or []
526+
self.state_binary = "0" * len(self.states)
527+
528+
def update(self, state):
529+
# Convert state from int32_t to binary string
530+
self.state_binary = format(int(state), "032b")[::-1]
531+
# Unpack state binary string into individual element states
532+
533+
def display(self):
534+
return self.state_binary
535+
536+
def as_dict(self):
537+
return {
538+
state: bool(int(bit)) for state, bit in zip(self.states, self.state_binary)
539+
}

templates/example.html

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424

2525
.section {
2626
display: flex;
27-
flex-direction: column;
28-
gap: 15px;
27+
flex-direction: row;
28+
justify-content: space-between;
29+
align-items: flex-start;
2930
padding: 20px;
3031
border: 1px solid #ccc;
3132
border-radius: 8px;
@@ -46,8 +47,6 @@
4647
display: flex;
4748
align-items: center;
4849
font-size: 4rem;
49-
50-
/* Set uniform font size */
5150
}
5251

5352
.axis-label {
@@ -68,6 +67,40 @@
6867
margin-left: 10px;
6968
}
7069

70+
.led-container {
71+
display: flex;
72+
flex-direction: column;
73+
align-items: flex-start;
74+
gap: 15px;
75+
margin-left: 20px;
76+
}
77+
78+
.led-wrapper {
79+
display: flex;
80+
align-items: center;
81+
gap: 10px;
82+
}
83+
84+
.led {
85+
width: 25px;
86+
height: 25px;
87+
background-color: #555;
88+
border-radius: 50%;
89+
box-shadow: inset -2px -2px 4px rgba(0, 0, 0, 0.4), inset 2px 2px 4px rgba(255, 255, 255, 0.7);
90+
}
91+
92+
.led.on {
93+
background-color: #00d207;
94+
/* Green color when on */
95+
box-shadow: inset -2px -2px 4px rgba(0, 0, 0, 0.4), inset 2px 2px 4px rgba(255, 255, 255, 0.7), 0 0 8px #69fa6e;
96+
/* Glow */
97+
}
98+
99+
.led-label {
100+
font-weight: normal;
101+
text-transform: uppercase;
102+
}
103+
71104
.motor-container {
72105
display: grid;
73106
grid-template-columns: repeat(4, 1fr);
@@ -84,9 +117,7 @@
84117
flex-direction: column;
85118
gap: 5px;
86119
font-family: 'Roboto', sans-serif;
87-
/* Uniform font */
88120
font-size: 1rem;
89-
/* Uniform font size */
90121
position: relative;
91122
}
92123

@@ -121,18 +152,14 @@
121152
.torque-bar {
122153
height: 100%;
123154
background-color: #4caf50;
124-
/* Green bar */
125155
width: 0%;
126-
/* Dynamic width based on torque */
127156
transition: width 0.3s ease;
128-
/* Smooth transition */
129157
}
130158
</style>
131159
</head>
132160

133161
<body>
134-
135-
<!-- Position Display Section -->
162+
<!-- Position Display and LED Indicators Section -->
136163
<div class="section">
137164
<div class="position-display">
138165
<div class="position-row">
@@ -151,6 +178,34 @@
151178
<div class="unit">mm</div>
152179
</div>
153180
</div>
181+
182+
<!-- LED Indicators Column -->
183+
<div class="led-container">
184+
<div class="led-wrapper">
185+
<div class="led" id="led-deadman-switch"></div>
186+
<div class="led-label">Deadman Switch</div>
187+
</div>
188+
<div class="led-wrapper">
189+
<div class="led" id="led-motors-enabled"></div>
190+
<div class="led-label">Motors Enabled</div>
191+
</div>
192+
<div class="led-wrapper">
193+
<div class="led" id="led-emergency-stop"></div>
194+
<div class="led-label">Emergency Stop</div>
195+
</div>
196+
<div class="led-wrapper">
197+
<div class="led" id="led-rapid-traverse"></div>
198+
<div class="led-label">Rapid Traverse</div>
199+
</div>
200+
<div class="led-wrapper">
201+
<div class="led" id="led-fine-control"></div>
202+
<div class="led-label">Fine Control</div>
203+
</div>
204+
<div class="led-wrapper">
205+
<div class="led" id="led-machine-mode"></div>
206+
<div class="led-label">Machine Mode</div>
207+
</div>
208+
</div>
154209
</div>
155210

156211
<!-- Motor Control Section -->
@@ -195,11 +250,23 @@
195250
const socket = io();
196251
const maxTorque = 100; // Define maximum torque value for scaling
197252

198-
// Update the positions and motor metrics when data is received from the server
253+
// Update the positions, motor metrics, and LED indicators when data is received from the server
199254
socket.on('update', function (data) {
200255
const parsedData = JSON.parse(data);
201256

202-
// Display raw values for position
257+
// Update LED indicators based on data (accept True/False, true/false, or 1/0 as inputs)
258+
function parseBoolean(value) {
259+
return value === true || value === 1 || value === "True" || value === "true";
260+
}
261+
262+
document.getElementById('led-deadman-switch').className = parseBoolean(parsedData['deadman_switch']) ? 'led on' : 'led off';
263+
document.getElementById('led-motors-enabled').className = parseBoolean(parsedData['motors_enabled']) ? 'led on' : 'led off';
264+
document.getElementById('led-emergency-stop').className = parseBoolean(parsedData['emergency_stop']) ? 'led on' : 'led off';
265+
document.getElementById('led-rapid-traverse').className = parseBoolean(parsedData['rapid_traverse']) ? 'led on' : 'led off';
266+
document.getElementById('led-fine-control').className = parseBoolean(parsedData['fine_control']) ? 'led on' : 'led off';
267+
document.getElementById('led-machine-mode').className = parseBoolean(parsedData['machine_mode']) ? 'led on' : 'led off';
268+
269+
// Update position values
203270
document.getElementById('posX').textContent = parsedData['position_x'];
204271
document.getElementById('posY').textContent = parsedData['position_y'];
205272
document.getElementById('posZ').textContent = parsedData['position_z'];
@@ -228,7 +295,6 @@
228295
}
229296
});
230297
</script>
231-
232298
</body>
233299

234300
</html>

0 commit comments

Comments
 (0)