Multifunctional Dashboard Testing
Multifunctional Dashboard Testing
Introduction:
The goal is to design a dashboard in order to read and control the battery parameters,
control the actuator and read all the sensor data. The library used to design this dashboard
is DASH, this had been used as it is the best library for creating good looking data
visualization graphs and charts, even though the current dashboard doesn’t have any
graphing this has been used in order to implement graphs in the future and make it fool
proof.
Iterations:
1) Initially there were 3 dashboards that had been created separately one for BMS
control, one for sensor data acquisition and one for actuator control. All these were
integrated and the issue faced was when one was functioning the others didn’t.
What I mean by this is that when the actuator was moving the BMS data polling and
sensor data polling to CSV didn’t happen and when the tab was switched to BMS
dashboard, the actuator control stopped. So, the problem was that only the active
dashboard functionality was working. Currently IPC was the architecture that was
being used, it was difficult to debug this also. The UI was also bad as it had not been
worked on and this was set to be worked on later.
2) Then there was this main issue, the raspberry pi is powered using the battery and
when we turn of the battery, we turn of the raspberry pi also and there is no way to
turn the raspberry pi back on. So, the plan was to use a secondary battery in order to
power the raspberry pi and this was charged using the main battery. The context for
this hardware change mentioned is that the dashboard layout was changed in such a
way that the battery control was removed and only monitoring was needed, the plan
was to keep the battery always on. Therefore, now there are only 2 dashboards in the
dropdown menu, one is for actuator control and the other was for data logging (both
sensor and BMS data). In order to make this work seamlessly the data logging was
implemented using multiprocessing and memory sharing as it had to work no matter
what happened in the dashboard and the dashboard accessed the data in such a way
that it read the shared memory. Thus, even if there is a buffer in data being displayed
on the dashboard the data acquisition worked well. Also, since it was multiprocessor
the actuator control was not affected.
3) The dashboard worked now but the UI was very bad, it just displayed dictionary of
the data and had basic grey and small buttons but it would be better when we
display the data in tables and have legible and big buttons to control and have a
gradient color scheme all this was implemented.
4) We noticed that there were a lot of free spaces and we didn’t need to have a drop
down list to switch between the 2 dashboards thus we brought all the information
and control into one dashboard and this was implemented using a neat UI.
5) Finally, there was the error handling feature that had to be added where if
abnormality was detected the actuator had to extend fully. This was integrated in the
dashboard callback where the abnormality flag was read every second and if the
abnormality flag was set tot true then the actuator extended fully. During this time,
we needed to make sure that the actuator control was disabled. So, the initial plan
was to load a new page but then we tried it and it was very hectic. So, the final
solution we came up with was to create a pop-up error screen where the close
button was disabled until the actuator extended fully. This solved the problem and
was implemented in the final code.
import time
import os
import csv
import RPi.GPIO as GPIO
import busio
import board
import serial
from datetime import datetime
import math
import threading
latest_sensor_data = {}
latest_bms_data = {}
BME680_AVAILABLE = True
except ImportError:
BME680_AVAILABLE = False
try:
import ms5837
MS5837_AVAILABLE = True
except ImportError:
MS5837_AVAILABLE = False
try:
import adafruit_ina260
INA260_AVAILABLE = True
except ImportError:
INA260_AVAILABLE = False
try:
if BME680_AVAILABLE:
sensors['bme680'] = Adafruit_BME680_I2C(i2c, address=0x77)
except Exception as e:
print(f"Failed to initialize BME680: {e}")
sensors['bme680'] = None
try:
if MS5837_AVAILABLE:
sensors['pressure'] = ms5837.MS5837_30BA()
if sensors['pressure'].init(): # Ensure sensor is initialized
correctly
print("MS5837 sensor initialized successfully.")
else:
print("Failed to initialize MS5837 sensor.")
sensors['pressure'] = None
except Exception as e:
print(f"Failed to initialize MS5837: {e}")
sensors['pressure'] = None
try:
if INA260_AVAILABLE:
sensors['ina260'] = adafruit_ina260.INA260(i2c)
except Exception as e:
print(f"Failed to initialize INA260: {e}")
sensors['ina260'] = None
return sensors
# File paths
SENSOR_LOG_FILE = "2_sensor_data.csv"
BMS_LOG_FILE = "2_bms_data.csv"
# Logging state
is_logging = False
def setup_gpio():
"""Initializes GPIO settings."""
try:
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(17, GPIO.IN) # Example: Leak Sensor Pin
print("GPIO setup complete.")
except Exception as e:
print(f"GPIO Setup Error: {e}")
def setup_serial():
"""Initializes serial communication."""
try:
ser = serial.Serial('/dev/ttyUSB1', baudrate=115200, bytesize=8,
parity='N', stopbits=1, timeout=0.05)
print("Serial communication setup complete.")
return ser
except Exception as e:
print(f"Serial Setup Error: {e}")
return None
THRESHOLDS = {
"temperature": 50, # Max temperature in °C
"humidity": 90, # Max humidity in %
"pressure_hpa": 1100, # Max pressure in hPa
"pressure_mbar": 1100, # Max pressure in mbar
"temperature_C_pressure": 50, # Max temperature from pressure sensor
"current": 10, # Max current in A
"voltage": 15 # Max voltage in V
}
global abnormality
# Abnormality is reset
def check_abnormality(sensor_data):
global abnormality
}
if 'bme680' in sensors:
try:
sensor = sensors['bme680']
data.update({
"temperature": round(sensor.temperature, 2),
"humidity": round(sensor.humidity, 2),
"pressure_hpa": round(sensor.pressure, 2)
})
except Exception:
pass
if 'pressure' in sensors:
try:
sensor = sensors['pressure']
if sensor.read():
data.update({
"pressure_mbar":
round(sensor.pressure(ms5837.UNITS_mbar), 2),
"temperature_C_pressure":
round(sensor.temperature(ms5837.UNITS_Centigrade), 2)
})
except Exception:
pass
if 'ina260' in sensors:
try:
sensor = sensors['ina260']
data.update({
"current": max(0, sensor.current),
"voltage": sensor.voltage
})
except Exception:
pass
return data
def read_ins_data():
ins_data = {}
try:
with serial.Serial("/dev/ttyUSB0", 115200, timeout=0.9) as ser:
ser.write(b"$PSPA,G\r\n")
gyro_response = ser.readline().decode('utf-8').strip()
ser.write(b"$PSPA,QUAT\r\n")
quat_response = ser.readline().decode('utf-8').strip()
ser.write(b"$PSPA,A\r\n")
accel_response = ser.readline().decode('utf-8').strip()
def parse_G(string):
split_string = string.split(",")
if len(split_string) < 4:
return None
try:
Gx = float(split_string[1].split("=")[1]) * (math.pi /
180.0) / 1000
Gy = float(split_string[2].split("=")[1]) * (math.pi /
180.0) / 1000
Gz = float(split_string[3].split("=")[1].split("*")[0])
* (math.pi / 180.0) / 1000
return {"Gx": round(Gx, 4), "Gy": round(Gy, 4), "Gz":
round(Gz, 4)}
except ValueError:
return None
def parse_A(string):
split_string = string.split(",")
if len(split_string) < 4:
return None
try:
Ax = float(split_string[1].split("=")[1]) / 1000 * 9.81
Ay = float(split_string[2].split("=")[1]) / 1000 * 9.81
Az = float(split_string[3].split("=")[1].split("*")[0])
/ 1000 * 9.81
return {"Ax": round(Ax, 4), "Ay": round(Ay, 4), "Az":
round(Az, 4)}
except ValueError:
return None
def parse_QUAT(string):
split_string = string.split(",")
if len(split_string) < 5:
return None
try:
w = float(split_string[1].split("=")[1])
x = float(split_string[2].split("=")[1])
y = float(split_string[3].split("=")[1])
z = float(split_string[4].split("=")[1].split("*")[0])
return {"orientation_w": round(w, 4), "orientation_x":
round(x, 4), "orientation_y": round(y, 4),
"orientation_z": round(z, 4)}
except ValueError:
return None
gyro_data = parse_G(gyro_response)
if gyro_data:
ins_data.update(gyro_data)
accel_data = parse_A(accel_response)
if accel_data:
ins_data.update(accel_data)
quat_data = parse_QUAT(quat_response)
if quat_data:
ins_data.update(quat_data)
except Exception as e:
print(f"INS sensor error: {e}")
return ins_data
#####################################################
def get_active_state_description(active_state_value):
state_descriptions = {
100: "System fault",
101: "Temperature trip",
102: "Short circuit trip",
103: "Overload current trip",
104: "Cell voltage fault",
105: "Over-charge trip",
106: "Over-discharge trip",
107: "Pre-charge state",
108: "Normal operation",
109: "Critical over-charge trip",
110: "Critical over-discharge trip",
90: "User disabled state",
91: "Sleep state",
}
return state_descriptions.get(active_state_value, "Unknown state")
def get_rtd(ser):
try:
if ser is None:
raise Exception("Serial connection failed.") # 🔹 Ensure serial
is valid
except Exception as e:
print(f"❌ Error reading BMS data: {e}")
return None
#####################################################
def start_logging():
global is_logging
is_logging = True
print("Logging started...")
def stop_logging():
global is_logging
is_logging = False
print("Logging stopped...")
file_exists = os.path.isfile(SENSOR_LOG_FILE)
if not file_exists:
with open(SENSOR_LOG_FILE, mode="w", newline="") as file:
writer = csv.writer(file)
writer.writerow(["temperature", "humidity", "pressure_hpa",
"pressure_mbar",
"temperature_C_pressure", "current",
"voltage", "leak_detected",
"Timestamp", "Gx", "Gy", "Gz", "Ax", "Ay",
"Az",
"orientation_w", "orientation_x",
"orientation_y", "orientation_z",
"abnormality"]) # Added abnormality column
try:
"""data = read_sensors(sensors)
ins_data = read_ins_data()
data.update(ins_data)"""
except Exception as e:
print(f"Sensor Read Error: {e}")
return
try:
if ser is None:
raise Exception("Serial connection failed.") # 🔹 If serial
setup fails, raise an error
except Exception as e:
print(f"BMS Read Error: {e}")
return # Skip logging if BMS read fails, but the file is already
created
def monitor_and_log(shared_data):
start_logging()
setup_gpio() # Setup GPIO once
ser = setup_serial() # Setup Serial once
sensors = initialize_sensors() # Setup I2C and Sensors once
try:
while True:
if is_logging:
latest_sensor = read_sensors(sensors)
ins_data = read_ins_data()
latest_sensor.update(ins_data)
log_sensor_data(sensors, latest_sensor) # Log sensor & INS
data
latest_bms = get_rtd(ser)
log_bms_data(ser, latest_bms) # Log BMS data
if __name__ == "__main__":
monitor_and_log()
Actuator control code (Custom Module):
# PWM Variables
p = None
p1 = None
def counter_callback(channel):
global pulse_count, last_pulse_time, is_extending, is_retracting
current_time = time.perf_counter_ns()
time_diff = current_time - last_pulse_time
if time_diff > 100_000:
if is_extending:
pulse_count += 1
elif is_retracting:
pulse_count -= 1
last_pulse_time = current_time
def setup_gpio():
global p, p1
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(Enc_A, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.remove_event_detect(Enc_A)
GPIO.add_event_detect(Enc_A, GPIO.BOTH, callback=counter_callback)
GPIO.setup(PWM_Extend, GPIO.OUT)
GPIO.setup(PWM_Retract, GPIO.OUT)
p = GPIO.PWM(PWM_Extend, 2000)
p1 = GPIO.PWM(PWM_Retract, 2000)
p.start(0)
p1.start(0)
'''def extend_fully():
global pulse_count, is_extending
counter_values_e = []
is_extending = True
p.start(70)
p1.start(0)
try:
while True:
counter_values_e.append(pulse_count)
time.sleep(1)
if len(counter_values_e) >= 3 and len(set(counter_values_e[-
3:])) == 1:
p.ChangeDutyCycle(0)
is_extending = False
break
finally:
p.ChangeDutyCycle(0)'''
def extend_fully():
global pulse_count, is_extending
counter_values_e = []
is_extending = True
p.start(70)
p1.start(0)
try:
while True:
counter_values_e.append(pulse_count)
print(pulse_count)
time.sleep(1)
if len(counter_values_e) >= 3 and len(set(counter_values_e[-
3:])) == 1:
p.ChangeDutyCycle(0)
is_extending = False
pulse_count = 15815 # Set count to full extension value
break
finally:
p.ChangeDutyCycle(0)
def home_actuator():
global pulse_count, is_retracting
counter_values_r = []
is_retracting = True
p1.start(70)
p.start(0)
try:
while True:
counter_values_r.append(pulse_count)
print(pulse_count)
time.sleep(1)
if len(counter_values_r) >= 3 and len(set(counter_values_r[-
3:])) == 1:
pulse_count = 0
p1.ChangeDutyCycle(0)
is_retracting = False
break
finally:
p1.ChangeDutyCycle(0)
def move_to_position(desired_length):
global pulse_count, is_extending, is_retracting
tolerance = 1.0
previous_values = []
current_length = pulse_count / count_per_mm
go_to_length = desired_length - current_length
if abs(go_to_length) < tolerance:
return
if go_to_length > 0:
is_extending = True
is_retracting = False
p.start(70)
p1.start(0)
else:
is_retracting = True
is_extending = False
p1.start(70)
p.start(0)
try:
while True:
current_length = pulse_count / count_per_mm
previous_values.append(current_length)
if len(previous_values) > 3:
previous_values.pop(0)
if abs(current_length - desired_length) < tolerance:
break
if len(previous_values) >= 3 and len(set(previous_values)) ==
1:
break
time.sleep(0.5)
finally:
p.ChangeDutyCycle(0)
p1.ChangeDutyCycle(0)
is_extending = False
is_retracting = False
def cleanup():
GPIO.cleanup()
print("GPIO cleaned up.")
Main Dashboard Code:
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc
import RPi.GPIO as GPIO
from multiprocessing import Manager, Process
import threading
import log_draft_2
from actuator_control_2 import setup_gpio, cleanup, move_to_position,
home_actuator, extend_fully
# -----------------------------
# SHARED DATA & GLOBALS
# -----------------------------
manager = Manager()
shared_data = manager.dict()
shared_data['sensor'] = {}
shared_data['bms'] = [] # BMS data stored as a list
actuator_status = ""
error_flag = False
extension_complete = threading.Event()
# -----------------------------
# HELPER: Generate Tables
# -----------------------------
def generate_table(data_dict):
if not data_dict:
return html.Div("No data available", className="text-muted")
rows = []
for key, value in data_dict.items():
rows.append(
html.Tr([
html.Td(key.title(), style={"padding": "9px", "lineHeight":
"1", "width": "50%"}),
html.Td(str(value), style={"padding": "9px", "lineHeight":
"1", "width": "50%"})
])
)
table = dbc.Table(
[
html.Thead(
html.Tr([
html.Th("Parameter",
style={"padding": "9px", "lineHeight": "1",
"width": "50%", "fontSize": "18px"}),
html.Th("Value", style={"padding": "9px", "lineHeight":
"1", "width": "50%", "fontSize": "18px"})
])
),
html.Tbody(rows)
],
bordered=True, striped=True, hover=True, responsive=True,
style={"height": "auto", "marginBottom": "0px"},
className="mt-2"
)
return table
def generate_bms_table(bms_data):
if not bms_data or not isinstance(bms_data, list):
return html.Div("No BMS data available", className="text-muted")
headers = ["Timestamp", "Voltage", "Current", "State", "BMS Temp",
"Battery Temps", "Battery Voltages"]
rows = []
for i, header in enumerate(headers):
val = bms_data[i] if i < len(bms_data) else ""
rows.append(
html.Tr([
html.Td(header, style={"padding": "9px", "lineHeight": "1",
"width": "50%"}),
html.Td(str(val), style={"padding": "9px", "lineHeight":
"1", "width": "50%"})
])
)
return dbc.Table(
[
html.Thead(
html.Tr([
html.Th("Parameter",
style={"padding": "9px", "lineHeight": "1",
"width": "50%", "fontSize": "18px"}),
html.Th("Value", style={"padding": "9px", "lineHeight":
"1", "width": "50%", "fontSize": "18px"})
])
),
html.Tbody(rows)
],
bordered=True, striped=True, hover=True, responsive=True,
style={"height": "auto", "marginBottom": "0px"},
className="mt-2"
)
# -----------------------------
# STYLES
# -----------------------------
page_style = {
"background": "linear-gradient(135deg, #74ABE2 0%, #5563DE 100%)",
"minHeight": "100vh",
"margin": "0",
"padding": "0"
}
card_style = {
"backgroundColor": "rgba(255, 255, 255, 0.85)",
"borderRadius": "8px",
"padding": "15px",
"marginBottom": "10px"
}
# -----------------------------
# DASHBOARD LAYOUT
# -----------------------------
# Header with Title (centered, bold, bigger)
header = dbc.Container(fluid=True, children=[
dbc.Row([
dbc.Col(
html.H3(
"ROuboy test",
id="page-title",
style={
"textAlign": "center",
"fontWeight": "bold",
"fontSize": "36px"
}
),
width=12
)
], className="mb-3")
])
# Top Row: Left for Latest BMS Data; Right for Actuator Control
# Top Row: Left for Latest BMS Data; Right for Actuator Control
top_row = dbc.Container(fluid=True, children=[
dbc.Row([
dbc.Col(
html.Div(style={**card_style, "height": "100%", "minHeight":
"100%"}, children=[
html.H5("LATEST BMS DATA"),
html.Div(id="bms-data-display", style={"flexGrow": "1"})
]),
width=6,
className="d-flex flex-column"
),
dbc.Col(
html.Div(style={**card_style, "height": "100%", "minHeight":
"100%", "display": "flex",
"flexDirection": "column", "justifyContent":
"center", "alignItems": "center",
"padding": "20px"}, children=[
html.H5("ACTUATOR CONTROL", style={"textAlign": "center",
"marginBottom": "15px"}),
html.Div([
dcc.Input(
id='actuator-position',
type='number',
placeholder='Enter Position (mm)',
style={'width': '100%', 'marginBottom': '15px',
'textAlign': 'center', 'padding': '10px'}
),
dbc.Row([
dbc.Col(
dbc.Button(
'Move to Position',
id='move-button',
n_clicks=0,
color="primary",
className="mx-2",
style={
"borderRadius": "20px",
"fontWeight": "bold",
"padding": "12px 24px",
"boxShadow": "0px 2px 5px
rgba(0,0,0,0.3)",
"textAlign": "center"
}
),
width="auto"
),
dbc.Col(
dbc.Button(
'Move to Mid',
id='mid-button',
n_clicks=0,
color="secondary",
className="mx-2",
style={
"borderRadius": "20px",
"fontWeight": "bold",
"padding": "12px 24px",
"boxShadow": "0px 2px 5px
rgba(0,0,0,0.3)",
"textAlign": "center"
}
),
width="auto"
),
dbc.Col(
dbc.Button(
'Home Actuator',
id='home-button',
n_clicks=0,
color="success",
className="mx-2",
style={
"borderRadius": "20px",
"fontWeight": "bold",
"padding": "12px 24px",
"boxShadow": "0px 2px 5px
rgba(0,0,0,0.3)",
"textAlign": "center"
}
),
width="auto"
),
dbc.Col(
dbc.Button(
'Extend Fully',
id='extend-button',
n_clicks=0,
color="warning",
className="mx-2",
style={
"borderRadius": "20px",
"fontWeight": "bold",
"padding": "12px 24px",
"boxShadow": "0px 2px 5px
rgba(0,0,0,0.3)",
"textAlign": "center"
}
),
width="auto"
),
], justify="center"),
html.Div(id='actuator-status', className="mt-3",
style={"textAlign": "center", "fontWeight":
"bold"}),
dcc.Interval(id='status-update', interval=1000,
n_intervals=0),
dcc.Store(id='actuator-status-store', data="")
])
]),
width=6,
className="d-flex flex-column"
)
], className="d-flex align-items-stretch mb-4") # Adds bottom margin
for spacing below the top row
])
# Bottom Row: Latest Sensor Data (left) and Latest INS Data (right)
bottom_row = dbc.Container(fluid=True, children=[
dbc.Row([
dbc.Col(
html.Div(style=card_style, children=[
html.H5("LATEST SENSOR DATA"),
html.Div(id="sensor-data-display")
]),
width=6
),
dbc.Col(
html.Div(style=card_style, children=[
html.H5("LATEST INS DATA"),
html.Div(id="ins-data-display")
]),
width=6
)
])
])
# Combine Header, Top Row, and Bottom Row into one layout
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUX],
suppress_callback_exceptions=True)
app.title = "Rouboy v2"
app.layout = html.Div(style=page_style, children=[
header,
top_row,
bottom_row,
dcc.Interval(id="interval-component", interval=1000, n_intervals=0),
dcc.Interval(id="main-interval", interval=1000, n_intervals=0),
html.Div(id="error-monitor", style={"display": "none"}),
html.Div(id="actuator-extend", style={"display": "none"}),
dbc.Modal(
[
dbc.ModalHeader("Error"),
dbc.ModalBody(id="error-modal-body"),
dbc.ModalFooter(
dbc.Button("Close", id="close-modal", n_clicks=0)
)
],
id="error-modal",
is_open=False
)
])
# -----------------------------
# CALLBACK: Update Monitoring Data
# -----------------------------
@app.callback(
[Output("sensor-data-display", "children"),
Output("ins-data-display", "children"),
Output("bms-data-display", "children")],
Input("interval-component", "n_intervals")
)
def update_data(n):
sensor_dict = shared_data.get("sensor", {})
# Separate INS data from sensor data using INS keys
ins_keys = ["Gx", "Gy", "Gz", "Ax", "Ay", "Az", "orientation_w",
"orientation_x", "orientation_y", "orientation_z"]
sensor_part = {k: v for k, v in sensor_dict.items() if k not in
ins_keys}
ins_part = {k: v for k, v in sensor_dict.items() if k in ins_keys}
sensor_table = generate_table(sensor_part)
ins_table = generate_table(ins_part)
bms_list = shared_data.get("bms", [])
bms_table = generate_bms_table(bms_list)
return sensor_table, ins_table, bms_table
# -----------------------------
# CALLBACK: Actuator Control
# -----------------------------
@app.callback(
Output("actuator-status-store", "data"),
[Input("move-button", "n_clicks"),
Input("home-button", "n_clicks"),
Input("extend-button", "n_clicks"),
Input("mid-button", "n_clicks")],
State("actuator-position", "value"),
prevent_initial_call=True
)
def control_actuator(move_clicks, home_clicks, extend_clicks, mid_clicks,
position):
global actuator_status
ctx = dash.callback_context
if not ctx.triggered:
return dash.no_update
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
setup_gpio()
if button_id == "move-button":
if position is None or not (0 <= position <= 241):
actuator_status = "❌ Invalid position! Enter a value between 0
and 241mm."
return dash.no_update
actuator_status = "⏳ Moving to position..."
move_to_position(position)
actuator_status = f"✅ Actuator moved to {position} mm"
return dash.no_update
# -----------------------------
# CALLBACK: Update Actuator Status Display
# -----------------------------
@app.callback(
Output("actuator-status", "children"),
Input("status-update", "n_intervals"),
prevent_initial_call=True
)
def update_status(n):
global actuator_status
return actuator_status if actuator_status else dash.no_update
# -----------------------------
# CALLBACK: Monitor Abnormality and Error Modal
# -----------------------------
@app.callback(
Output("error-monitor", "children"),
Input("main-interval", "n_intervals")
)
def monitor_abnormality(n):
global error_flag
if shared_data.get("sensor", {}).get("abnormality", False):
error_flag = True
print("⚠️ ERROR FLAG RAISED: Abnormality detected!")
return ""
def async_extend():
setup_gpio()
extend_fully()
extension_complete.set()
@app.callback(
[Output("actuator-extend", "children"),
Output("error-modal", "is_open"),
Output("error-modal-body", "children"),
Output("close-modal", "disabled")],
[Input("main-interval", "n_intervals"),
Input("close-modal", "n_clicks")]
)
def update_error_modal(n_intervals, close_clicks):
global error_flag
ctx = dash.callback_context
if ctx.triggered:
trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
if trigger_id == "close-modal":
print("Close button clicked: resetting error flag and extension
state.")
error_flag = False
extension_complete.clear()
return "", False, "", False
if error_flag:
if not extension_complete.is_set():
print("Error detected: starting asynchronous extension.")
threading.Thread(target=async_extend).start()
return "", True, "Abnormality raised, actuator extending...",
True
else:
print("Extension complete: waiting for modal to be closed.")
return "", True, "Actuator extended fully. Please close the
modal.", False
return "", False, dash.no_update, False
# -----------------------------
# MAIN
# -----------------------------
if __name__ == "__main__":
p = Process(target=log_draft_2.monitor_and_log, args=(shared_data,))
p.start()
try:
app.run_server(debug=False, host="0.0.0.0")
finally:
p.terminate()
p.join()
cleanup()