0% found this document useful (0 votes)
2 views21 pages

Multifunctional Dashboard Testing

The document outlines the development of a multifunctional dashboard for monitoring and controlling battery parameters, actuators, and sensors using the DASH library. It details the iterative process of integrating multiple dashboards into one, improving the user interface, implementing multiprocessing for data logging, and adding error handling features. The document also includes code snippets for data logging and sensor initialization, highlighting the challenges faced and solutions implemented during the development process.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views21 pages

Multifunctional Dashboard Testing

The document outlines the development of a multifunctional dashboard for monitoring and controlling battery parameters, actuators, and sensors using the DASH library. It details the iterative process of integrating multiple dashboards into one, improving the user interface, implementing multiprocessing for data logging, and adding error handling features. The document also includes code snippets for data logging and sensor initialization, highlighting the challenges faced and solutions implemented during the development process.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 21

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.

Data logging code (Custom Module):

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 = {}

# Attempt to import sensor libraries with error handling


try:
from adafruit_bme680 import Adafruit_BME680_I2C

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

# Function to initialize I2C


def setup_i2c():
"""Initializes I2C communication."""
try:
i2c = busio.I2C(board.SCL, board.SDA)
print("I2C setup complete.")
return i2c
except Exception as e:
print(f"I2C Setup Error: {e}")
return None

# Function to initialize sensors safely


def initialize_sensors():
"""Initialize available sensors with error handling."""
i2c = setup_i2c() # Initialize I2C once
sensors = {}

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

########### Abnormality ###########

# Define global abnormality flag


abnormality = False

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
}

# Leak detection is a boolean check (not a numerical threshold)


LEAK_DETECTED_THRESHOLD = True # If True, an abnormality is triggered

# Abnormality is not reset


"""def check_abnormality(sensor_data):

global abnormality

for key, threshold in THRESHOLDS.items():


if key in sensor_data and sensor_data[key] != "Error": # Ignore
errors
try:
if float(sensor_data[key]) > threshold: # Compare numeric
values
abnormality = True # Trigger abnormality
print(f"⚠️ Abnormality detected: {key} exceeded
threshold ({sensor_data[key]} > {threshold})")
return # Stop further checking
except ValueError:
pass # Ignore conversion errors for non-numeric values

# Special case: Leak sensor (boolean check)


if "leak_detected" in sensor_data and
isinstance(sensor_data["leak_detected"], bool):
if sensor_data["leak_detected"] == LEAK_DETECTED_THRESHOLD:
abnormality = True
print(f"⚠️ Abnormality detected: Leak detected!")"""

# Abnormality is reset
def check_abnormality(sensor_data):
global abnormality

# Assume normal state before checking


abnormality_detected = False
for key, threshold in THRESHOLDS.items():
if key in sensor_data and sensor_data[key] != "Error": # Ignore
errors
try:
if float(sensor_data[key]) > threshold: # Compare numeric
values
abnormality_detected = True
print(f"⚠️ Abnormality detected: {key} exceeded
threshold ({sensor_data[key]} > {threshold})")
break # No need to check further
except ValueError:
pass # Ignore conversion errors for non-numeric values

# Special case: Leak sensor (boolean check)


if "leak_detected" in sensor_data and
isinstance(sensor_data["leak_detected"], bool):
if sensor_data["leak_detected"] == LEAK_DETECTED_THRESHOLD:
abnormality_detected = True
print(f"⚠️ Abnormality detected: Leak detected!")

# Update the global abnormality flag based on detection status


abnormality = abnormality_detected

########### Sensor #############


def read_sensors(sensors):
data = {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"temperature": None,
"humidity": None,
"pressure_hpa": None,
"pressure_mbar": None,
"temperature_C_pressure": None,
"current": None,
"voltage": None,
"leak_detected": GPIO.input(17) == 1,

}
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

#####################################################

##################### BATTERY #######################


def send_command(ser, command, address, data):
packet = [
command,
(address >> 8) & 0xFF,
address & 0xFF,
(data >> 8) & 0xFF,
data & 0xFF,
0x00, 0x00,
0x00
]
ser.write(bytes(packet))
response = ser.read(10)
return list(response)

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

temp = send_command(ser, 0x30, 0x0F00, 0x0000)


if not temp:
raise Exception("Serial read failed for voltage") # 🔹 Prevent
crashes on bad reads
v = round((temp[1] + temp[2] * 256) * 0.1, 1)

temp = send_command(ser, 0x30, 0x1300, 0x0000)


if not temp:
raise Exception("Serial read failed for current")
i = round((temp[1] + temp[2] * 256) * 0.1, 1)

temp = send_command(ser, 0x30, 0x0100, 0x0000)


status = get_active_state_description(temp[1])

temp = send_command(ser, 0x30, 0x4600, 0x0000)


if not temp:
raise Exception("Serial read failed for BMS temperature")
bms_temp = temp[1] - 128

# ✅ Check and Fix Battery Temperature Readings


bat_temp = []
hex_values = [0x4800, 0x4900, 0x4A00, 0x4B00] # 🔹 Verify these
register addresses
for register in hex_values:
temp = send_command(ser, 0x30, register, 0x0000)
if temp:
bat_temp.append(temp[1] - 128) # Convert raw data
else:
bat_temp.append("Error")

# ✅ Check and Fix Battery Voltage Readings


bat_v = []
hex_values = [0x6500, 0x6600, 0x6700, 0x6800, 0x6900, 0x6A00,
0x6B00] # 🔹 Verify these addresses
for register in hex_values:
temp = send_command(ser, 0x30, register, 0x0000)
if temp:
voltage = temp[2] * 256 + temp[1]
bat_v.append(voltage)
else:
bat_v.append("Error")

return [datetime.now().strftime("%Y-%m-%d %H:%M:%S"), v, i, status,


bms_temp, bat_temp, bat_v]

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...")

def log_sensor_data(sensors, data):


"""Reads sensor and INS data, then writes it to a CSV file with
abnormality flag."""
global abnormality

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)"""

for key in data:


if data[key] is None:
data[key] = "Error"

# Check for abnormality based on thresholds


check_abnormality(data)

# Add abnormality flag to logged data


data["abnormality"] = abnormality

# print(f"Logging sensor & INS data: {data}")

except Exception as e:
print(f"Sensor Read Error: {e}")
return

with open(SENSOR_LOG_FILE, mode="a", newline="") as file:


writer = csv.writer(file)
writer.writerow(data.values())
file.flush()

def log_bms_data(ser, data):


"""Reads BMS data and writes it to a CSV file, ensuring proper file
creation and error handling."""

# Ensure the file is created before reading BMS data


file_exists = os.path.isfile(BMS_LOG_FILE)
if not file_exists:
with open(BMS_LOG_FILE, mode="w", newline="") as file:
writer = csv.writer(file)
writer.writerow(
["Timestamp", "Voltage", "Current", "State", "BMS Temp",
"Battery Temps", "Battery Voltages"])
# 🔹 Column headers are now added if the file is new

try:
if ser is None:
raise Exception("Serial connection failed.") # 🔹 If serial
setup fails, raise an error

# data = get_rtd(ser) # Get BMS readings


# print(f"Logging BMS data: {data}") # Debugging print

except Exception as e:
print(f"BMS Read Error: {e}")
return # Skip logging if BMS read fails, but the file is already
created

# Write data to the CSV file


with open(BMS_LOG_FILE, mode="a", newline="") as file:
writer = csv.writer(file)
writer.writerow(data) # Write available BMS data
file.flush() # 🔹 Ensure immediate data storage

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

# Update shared memory for the dashboard


shared_data['sensor'] = latest_sensor
shared_data['bms'] = latest_bms

# print(latest_sensor, "\n", latest_bms)


except KeyboardInterrupt:
print("\nLogging stopped by user (Ctrl+C).")
finally:
stop_logging()
print("Data safely stored before exit.")

if __name__ == "__main__":
monitor_and_log()
Actuator control code (Custom Module):

import RPi.GPIO as GPIO


import time

# GPIO Pin Assignments


Enc_A = 16 # Encoder feedback pin
PWM_Extend = 12 # PWM for extension
PWM_Retract = 13 # PWM for retraction

# Encoder Conversion Factor


count_per_mm = 15815 / 241 # Encoder pulses per millimeter

# Position Tracking Variables


pulse_count = 0
last_pulse_time = 0
is_extending = False
is_retracting = False

# 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

# Check if GPIO is already set up to avoid reinitialization


if p is not None and p1 is not None:
return # GPIO already set up, no need to reinitialize

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"

elif button_id == "home-button":


actuator_status = "⏳ Homing actuator..."
home_actuator()
actuator_status = "✅ Actuator homed"

elif button_id == "extend-button":


actuator_status = "⏳ Extending actuator fully..."
extend_fully()
actuator_status = "✅ Actuator fully extended"

elif button_id == "mid-button":


actuator_status = "⏳ Moving to mid position..."
move_to_position(150)
actuator_status = "✅ Actuator moved to mid position"

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()

You might also like