0% found this document useful (0 votes)
11 views

Battery Management System Testing

The document details the testing and debugging process of a Battery Management System (BMS) integrated with a battery purchased from D-espat. It outlines issues encountered with communication protocols, register access, and data interpretation, leading to successful implementation of a dashboard for monitoring battery parameters. Final code and UI for the dashboard are provided, showcasing real-time data logging and control functionalities.
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)
11 views

Battery Management System Testing

The document details the testing and debugging process of a Battery Management System (BMS) integrated with a battery purchased from D-espat. It outlines issues encountered with communication protocols, register access, and data interpretation, leading to successful implementation of a dashboard for monitoring battery parameters. Final code and UI for the dashboard are provided, showcasing real-time data logging and control functionalities.
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/ 11

Battery Management System Testing

Introduction:

We had purchased a battery from D-espat in order to power the whole system. This battery
was also integrated with a BMS (Battery management system) inside it. Basically, a BMS is s
device used to control and monitor the battery parameters.

Initially when we received this battery there were 3 ports, one for power another for
charging and the last one for communication. We also got a charger and a connector to USM
wire to connect the BMS to the system (laptop/ raspberry pi) to monitor and control BMS
parameters.

There was no instruction manual received with this, initially when we asked for BMS
documentation they gave the technical specification document, this wasn’t useful as it had
no information on how to communicate with the system. Then a request for the
communication protocol document was requested and this was given. Also, with this their
proprietary software was also given (only works on Windows).

We first connected the BMS to the laptop to see if it was functioning properly and all the
data seemed to be displaying well. Then we referred the document to understand what
were the available registers and how to access them. After going through it, a test python
script was written and the data was read but the response was not as expected.

Debugging:

1) We thought that the way in which checksum was being calculated was the mistake
but no matter what the checksum was we got the same response.

Inference: Checksum was disabled/ ignored by the BMS. When this issue was raised
in the mail it was confirmed by the vendor.

2) The baud rate in the document was mentioned as 9600 and 15200 but after trying
both 115200 only gave response.

Inference: The baud rate for UART communication was 115200.

3) We sent test values to access the RTD bank, CTRL bank and CFG bank based on the
“Master command packet format” given in the document and tried to observe the
output. It was not as expected. Instead of sending values randomly one by one we
wrote a script to read all possible values from 0000 to FFFF (Hex) and see the
response as the command was of 4 byte length. We only got error free response
when the first byte was set and the next byte was 0. Later we got a conformation
that the document was wrong and we only had set the first byte to send the
command. Eg. If we had to access the first register we had to send 0x 0100 and not
0x 0001.
Inference: The command is only single byte and not 2 bytes.
4) We tried to access different registers and found that the CFG bank gave valid
response when we converted it into ASCII character but we didn’t write command to
access CFG bank we tried to access a different bank. Then later after multiple testing
we found that the binary address to access the bank was not given correctly and this
was confirmed by the company. We came to this inference based on the data length
given by the BMS.

Inference: The bank address for command byte is wrong in the document the correct
assignment are : 001-Config Bank, 010-Control Bank, 011-Real Time Data Bank.

5) The ASCII characters made sense, but when we tried to access other data it made no
sense and we wrote a mail to raise this issue and we got a response saying “When
sending an address or data in command, LSB is sent first. The BMS responds with LSB
first data in case of RTD bank and MSB first data in case of CONFIG bank.”

Inference: We should infer numerical data according to how the register works.

6) In order to read voltage from RTD Bank we sent the necessary command and got
[1,27] as response. This corresponded to 27 * 256 + 1 = 283mV. But after raising this
issue, we understood that the voltage is in 0.1V and not in mV. This it corresponds to
28.3V which made sense as this was the same as shown in proprietary software.
However, the data for cell voltage should be treated as mV only. For nominal voltage
from CFG bank we need to ignore the MSB and only process the LSB.

Inference: Unit for a battery related voltage is 0.1V and for cell related voltage is mV.
Registers with MSB and LSB, mostly likely you have to ignore MSB to make sense of
the data.

7) When we tried to access the state of charge (SOC), there was no response and when
this issue was raised the BMS vendor had told that this model doesn’t have the SOC
and the software that they gave calculates it based on the voltage level.

Inference: There is no SOC thus we can’t find the battery percentage.

8) When we tried to access temperature data in the way they told to access the voltage
data it made no sense. Then we got feedback to just take the first byte and subtract it
with 128 in order to get the temperature in degree Celsius.

Inference: You have to subtract 128 from the BMS data to get temperature.
9) Currently after successfully creating a communication with BMS, even though the
task given was to only interface with UART I tried to enable the broadcast feature by
enabling the but in CTRL bank. I tried enabling one while disabling the other, kept
both enabled but was not able to read data. For reading the data through CAN I
wrote a new script but I was not able to read any data. For both CAN and RS485 I
tried all possible standard baud rates but I didn’t get any response. I sent this
complaint and got a response that the RS485 broadcast is disabled as it is very
inefficient because of the overall current consumption that it uses. But I got the same
response for CAN protocol and I didn’t proceed further to clarify this as everything
worked fine as it was.

Inference: RS485 broadcast is permanently disabled and I couldn’t find a solution on


how to read the CAN broadcast (not even sure of it is working or not).

Final Working:

Based on the register value conversion that has been obtained through testing it was found
that sending a command each second to the RTD bank and reading the response and
decoding it was the easiest solution to implement. Thus this has been done and a dashboard
has been made to control (on and off) and display the CFG bank values and the RTD values
each second and this is stored in a CSV file.

Final Code with neat UI:

import dash
from dash import html, dcc, Input, Output
import dash_daq as daq
import csv
import time
import os
from datetime import datetime
import serial

# Configure the serial connection


ser = serial.Serial('/dev/ttyUSB1', baudrate=115200, bytesize=8,
parity='N', stopbits=1, timeout=0.05)

if ser != None:
print("COnnection success")

# File to save the real-time data


output_file = "real_time_data_dashboard.csv"
is_logging = False # Logging control
battery_status = "Unknown"

# Functions to interact with BMS


def send_command(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(20)
return list(response)

def send_command_read_string(command, address, data):


response = send_command(command, address, data)
return ''.join(chr(i) for i in response[1:] if i != 0)

def get_config_fixed():
d = {
"Manufacturer name": None,
"Model number": None,
"Hardware version": None,
"Firmware version": None,
"Cell chemistry": None,
"Date of manufacture": None,
"Client Name": None,
"BMS Serial Number": None,
"Nominal Voltage": None,
"Rated Current": None,
"Cell Series": None
}
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(1,
9)]
x = []
for i in hex_values:
COMMAND = 0x10
REGISTER_ADDRESS = int(i, 16)
DATA_VALUE = 0x0000
x.append(send_command_read_string(COMMAND, REGISTER_ADDRESS,
DATA_VALUE))
for i, key in enumerate(d.keys()):
if i < len(x):
d[key] = x[i]
d["Cell Series"] = send_command(0x10, 0x2100, 0x0000)[2]
temp = send_command(0x10, 0x2200, 0x0000)
d["Nominal Voltage"] = str(round((temp[1] * 256 + temp[2]) * 0.1, 1)) +
"V"
d["Rated Current"] = d["Model number"][-4:]
return d

def get_config_var():
d = {
"bp": {
"Charge Cutoff (V)": None,
"Discharge Cutoff (V)": None,
"Sleep Cutoff (V)": None,
"Max Charging Current (A)": None,
"Max Discharging Current (A)": None,
"High Temperature Cutoff (°C)": None,
"Low Temperature Cutoff (°C)": None
},
"cp": {
"Charge Cutoff (mV)": None,
"Discharge Cutoff (mV)": None,
"Sleep Cutoff (mV)": None,
},
"op": {
"Balance Type": None
}
}

temp = send_command(0x10, 0x2300, 0x0000)


d["bp"]["Charge Cutoff (V)"] = (temp[1] * 256 + temp[2]) * 0.1

temp = send_command(0x10, 0x2700, 0x0000)


d["bp"]["Discharge Cutoff (V)"] = (temp[1] * 256 + temp[2]) * 0.1

temp = send_command(0x10, 0x2800, 0x0000)


d["bp"]["Sleep Cutoff (V)"] = (temp[1] * 256 + temp[2]) * 0.1

temp = send_command(0x10, 0x3300, 0x0000)


d["bp"]["Max Charging Current (A)"] = (temp[1] * 256 + temp[2]) * 0.1

temp = send_command(0x10, 0x3400, 0x0000)


d["bp"]["Max Discharging Current (A)"] = (temp[1] * 256 + temp[2]) *
0.1

temp = send_command(0x10, 0x3700, 0x0000)


d["bp"]["High Temperature Cutoff (°C)"] = temp[2] - 128

temp = send_command(0x10, 0x3800, 0x0000)


d["bp"]["Low Temperature Cutoff (°C)"] = temp[2] - 128

temp = send_command(0x10, 0x2B00, 0x0000)


d["cp"]["Charge Cutoff (mV)"] = temp[1] * 256 + temp[2]

temp = send_command(0x10, 0x2F00, 0x0000)


d["cp"]["Discharge Cutoff (mV)"] = temp[1] * 256 + temp[2]

temp = send_command(0x10, 0x3000, 0x0000)


d["cp"]["Sleep Cutoff (mV)"] = temp[1] * 256 + temp[2]

temp = send_command(0x10, 0x3D00, 0x0000)


d["op"]["Balance Type"] = temp[2]

return d

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():
temp = send_command(0x30, 0x0F00, 0x0000)
v = round((temp[1] + temp[2] * 256) * 0.1, 1)
temp = send_command(0x30, 0x1300, 0x0000)
i = round((temp[1] + temp[2] * 256) * 0.1, 1)
temp = send_command(0x30, 0x0100, 0x0000)
status = get_active_state_description(temp[1])
temp = send_command(0x30, 0x4600, 0x0000)
bms_temp = temp[1] - 128
bat_temp = []
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(72,
76)]
for j in hex_values:
COMMAND = 0x30
REGISTER_ADDRESS = int(j, 16)
DATA_VALUE = 0x0000
bat_temp.append((send_command(COMMAND, REGISTER_ADDRESS,
DATA_VALUE)[1]) - 128)
bat_v = []
hex_values = [f"0x{hex(i)[2:].upper().zfill(2)}00" for i in range(101,
108)]
for j in hex_values:
COMMAND = 0x30
REGISTER_ADDRESS = int(j, 16)
DATA_VALUE = 0x0000
temp = send_command(COMMAND, REGISTER_ADDRESS, DATA_VALUE)
temp = temp[2] * 256 + temp[1]
bat_v.append(temp)
return [datetime.now().strftime("%H:%M:%S"), v, i, status, bms_temp,
bat_temp, bat_v]

# Dash app setup


app = dash.Dash(__name__)
app.title = "BMS Dashboard"

app.layout = html.Div([
html.Div([
html.H1("Battery Management System Dashboard", style={
"textAlign": "center",
"color": "white",
"fontWeight": "bold",
"margin": "0",
"padding": "20px" # Padding for spacing inside the background
})
], style={
"backgroundColor": "#003366", # Background color
"borderRadius": "10px", # Rounded corners
"boxShadow": "0px 4px 10px rgba(0, 0, 0, 0.2)", # Subtle shadow
"marginBottom": "20px" # Spacing below the title
}),

# Fixed and Variable Configuration side by side


html.Div([
html.Div([
html.H2("Fixed Configuration", style={"color": "#003366",
"marginBottom": "10px"}),
html.Table([
html.Thead(html.Tr([html.Th(col, style={
"border": "1px solid black",
"padding": "5px",
"textAlign": "left",
"backgroundColor": "#e6f2ff"
}) for col in ["Parameter", "Value"]])),
html.Tbody(id="fixed-config", style={"border": "1px solid
black"})
], style={"width": "100%", "borderCollapse": "collapse"}),
], style={"width": "48%", "marginRight": "2%"}),

html.Div([
html.H2("Variable Configuration", style={"color": "#003366",
"marginBottom": "10px"}),
html.Table([
html.Thead(html.Tr([html.Th(col, style={
"border": "1px solid black",
"padding": "5px",
"textAlign": "left",
"backgroundColor": "#e6f2ff"
}) for col in ["Category", "Parameter", "Value"]])),
html.Tbody(id="variable-config", style={"border": "1px
solid black"})
], style={"width": "100%", "borderCollapse": "collapse"}),
], style={"width": "48%"}),
], style={"display": "flex", "justifyContent": "space-between",
"marginBottom": "20px"}),

# Center-aligned Fetch Configuration button


html.Div([
html.Button("Fetch Configuration", id="fetch-config-btn", style={
"backgroundColor": "#0066cc",
"color": "white",
"padding": "10px 20px",
"border": "none",
"borderRadius": "5px",
"cursor": "pointer"
}),
], style={"textAlign": "center", "marginBottom": "20px"}),

# Real-Time Data and Battery State Control side by side


html.Div([
# Battery State Control on the left
html.Div([
html.H2("Battery State Control", style={
"color": "#003366",
"marginBottom": "10px",
"textAlign": "center"
}),
daq.ToggleSwitch(id="battery-toggle", label="Battery State",
style={
"marginBottom": "20px",
"marginTop": "10px"
}),
html.Div(id="battery-status", style={
"color": "#003366",
"fontWeight": "bold",
"textAlign": "center"
}),
], style={
"width": "45%",
"textAlign": "center",
"padding": "10px",
"boxSizing": "border-box"
}),

# Real-Time Data on the right


html.Div([
html.H2("Real-Time Data", style={
"color": "#003366",
"marginBottom": "10px",
"textAlign": "center"
}),

# Text box and buttons in a row


html.Div([
# Text box for real-time data
dcc.Textarea(id="real-time-data", style={
"width": "65%",
"height": "200px",
"border": "1px solid black",
"borderRadius": "5px",
"padding": "10px",
"fontSize": "16px",
"marginRight": "10px" # Space between text box and buttons
}),

# Buttons in a 2x1 matrix beside the text box


html.Div([
html.Button("Start Logging", id="log-data-btn", style={
"backgroundColor": "#0066cc",
"color": "white",
"padding": "10px",
"border": "none",
"borderRadius": "5px",
"cursor": "pointer",
"fontSize": "14px",
"fontWeight": "bold",
"marginBottom": "10px", # Spacing between buttons
"height": "80px", # Match proportional height
"width": "120px" # Consistent button width
}),
html.Button("Stop Logging", id="stop-logging-btn", style={
"backgroundColor": "#cc0000",
"color": "white",
"padding": "10px",
"border": "none",
"borderRadius": "5px",
"cursor": "pointer",
"fontSize": "14px",
"fontWeight": "bold",
"height": "80px", # Match proportional height
"width": "120px" # Consistent button width
}),
], style={
"display": "flex",
"flexDirection": "column", # Stack buttons vertically
"justifyContent": "center", # Center-align buttons
"height": "200px" # Match text box height
}),
], style={
"display": "flex",
"alignItems": "center", # Align text box and buttons
vertically
"justifyContent": "center" # Center-align the entire row
}),
], style={
"width": "50%",
"textAlign": "center",
"padding": "10px",
"boxSizing": "border-box"
}),
], style={
"display": "flex",
"justifyContent": "space-between",
"alignItems": "center",
"marginBottom": "20px"
}),

# Interval for real-time updates


dcc.Interval(
id="interval-component",
interval=1 * 1000, # in milliseconds
n_intervals=0
),
], style={
"fontFamily": "Arial, sans-serif",
"padding": "10px",
"maxWidth": "100vw", # Adjusted to reduce white space
"margin": "auto",
"boxShadow": "0px 4px 10px rgba(0, 0, 0, 0.1)",
"borderRadius": "10px",
"backgroundColor": "#f2f2f2"
})

@app.callback(
[Output("fixed-config", "children"),
Output("variable-config", "children")],
Input("fetch-config-btn", "n_clicks"),
prevent_initial_call=True
)
def fetch_config(n_clicks):
fixed_config = get_config_fixed()
time.sleep(1)
fixed_config = get_config_fixed()
variable_config = get_config_var()

# Create rows for fixed configuration table


fixed_config_rows = [
html.Tr([
html.Td(key, style={"border": "1px solid black", "padding":
"5px", "textAlign": "left"}),
html.Td(value, style={"border": "1px solid black", "padding":
"5px", "textAlign": "left"})
]) for key, value in fixed_config.items()
]

# Create rows for variable configuration table with merged category


rows
variable_config_rows = []
category_names = {
"bp": "Battery Parameters",
"cp": "Cell Parameters",
"op": "Other Parameters"
}

for category, parameters in variable_config.items():


# Add the first row with category name and rowspan
first_row = True
for param, value in parameters.items():
if first_row:
variable_config_rows.append(
html.Tr([
html.Td(category_names[category], style={
"border": "1px solid black",
"padding": "5px",
"textAlign": "center",
"verticalAlign": "middle"
}, rowSpan=len(parameters)), # Merge rows
html.Td(param, style={"border": "1px solid black",
"padding": "5px", "textAlign": "left"}),
html.Td(value, style={"border": "1px solid black",
"padding": "5px", "textAlign": "left"})
])
)
first_row = False
else:
# Add subsequent rows without category column
variable_config_rows.append(
html.Tr([
html.Td(param, style={"border": "1px solid black",
"padding": "5px", "textAlign": "left"}),
html.Td(value, style={"border": "1px solid black",
"padding": "5px", "textAlign": "left"})
])
)

return fixed_config_rows, variable_config_rows

@app.callback(
Output("battery-status", "children"),
Input("battery-toggle", "value")
)
def toggle_battery(state):
COMMAND = 0xA0
REGISTER_ADDRESS = 0x0100
DATA_VALUE = 0x0100 if state else 0x0000
send_command(COMMAND, REGISTER_ADDRESS, DATA_VALUE)
return f"Battery is now {'On' if state else 'Off'}"

@app.callback(
Output("real-time-data", "value"),
[Input("log-data-btn", "n_clicks"), Input("stop-logging-btn",
"n_clicks")],
prevent_initial_call=True
)
def control_data_logging(log_clicks, stop_clicks):
global is_logging
ctx = dash.callback_context

if not ctx.triggered:
return dash.no_update

triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]

if triggered_id == "log-data-btn":
is_logging = True
return "Logging started..."
elif triggered_id == "stop-logging-btn":
is_logging = False
return "Logging stopped."

@app.callback(
Output("real-time-data", "value", allow_duplicate=True),
Input("interval-component", "n_intervals"),
prevent_initial_call=True
)
def update_real_time_data(_):
global is_logging
if not is_logging:
return dash.no_update

file_exists = os.path.isfile(output_file)
with open(output_file, mode="a", newline="") as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(["Time", "Voltage", "Current", "State", "BMS
Temp", "Battery Temps", "Battery Voltages"])

rtd_data = get_rtd()
writer.writerow(rtd_data)

display_data = (
f"Time: {rtd_data[0]}, Voltage: {rtd_data[1]}V, Current:
{rtd_data[2]}A, "
f"State: {rtd_data[3]}, BMS Temp: {rtd_data[4]}°C\n"
f"Battery Temps: {rtd_data[5]}\nBattery Voltages: {rtd_data[6]}"
)
return display_data

if __name__ == "__main__":
app.run_server(debug=True, use_reloader=False)

You might also like