Skip to content

Commit

Permalink
Support for Daly CAN Bus (#169)
Browse files Browse the repository at this point in the history
* support for Daly CAN Bus
* fix constructor args
* revert port, needs fix
* add can filters
* comment logger

Some changes are still needed to work with the latest version. They will follow in a next PR.

---------

Co-authored-by: Samuel Brucksch <[email protected]>
Co-authored-by: Manuel <[email protected]>
  • Loading branch information
3 people authored Sep 17, 2023
1 parent 1137b3e commit b1880f5
Show file tree
Hide file tree
Showing 2 changed files with 336 additions and 0 deletions.
332 changes: 332 additions & 0 deletions etc/dbus-serialbattery/daly_can.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
from battery import Protection, Battery, Cell
from utils import *
from struct import *
import can

class DalyCAN(Battery):

def __init__(self,port,baud):
super(DalyCAN, self).__init__(port,baud)
self.charger_connected = None
self.load_connected = None
self.cell_min_voltage = None
self.cell_max_voltage = None
self.cell_min_no = None
self.cell_max_no = None
self.poll_interval = 1000
self.poll_step = 0
self.type = self.BATTERYTYPE
self.bus = None
# command bytes [Priority=18][Command=94][BMS ID=01][Uplink ID=40]
command_base = 0x18940140
command_soc = 0x18900140
command_minmax_cell_volts = 0x18910140
command_minmax_temp = 0x18920140
command_fet = 0x18930140
command_status = 0x18940140
command_cell_volts = 0x18950140
command_temp = 0x18960140
command_cell_balance = 0x18970140
command_alarm = 0x18980140

response_base = 0x18944001
response_soc = 0x18904001
response_minmax_cell_volts = 0x18914001
response_minmax_temp = 0x18924001
response_fet = 0x18934001
response_status = 0x18944001
response_cell_volts = 0x18954001
response_temp = 0x18964001
response_cell_balance = 0x18974001
response_alarm = 0x18984001

BATTERYTYPE = "Daly_CAN"
LENGTH_CHECK = 4
LENGTH_POS = 3
CURRENT_ZERO_CONSTANT = 30000
TEMP_ZERO_CONSTANT = 40

def test_connection(self):
result = False

# TODO handle errors?
can_filters = [{"can_id":self.response_base, "can_mask": 0xFFFFFFF},
{"can_id":self.response_soc, "can_mask": 0xFFFFFFF},
{"can_id":self.response_minmax_cell_volts, "can_mask": 0xFFFFFFF},
{"can_id":self.response_minmax_temp, "can_mask": 0xFFFFFFF},
{"can_id":self.response_fet, "can_mask": 0xFFFFFFF},
{"can_id":self.response_status, "can_mask": 0xFFFFFFF},
{"can_id":self.response_cell_volts, "can_mask": 0xFFFFFFF},
{"can_id":self.response_temp, "can_mask": 0xFFFFFFF},
{"can_id":self.response_cell_balance, "can_mask": 0xFFFFFFF},
{"can_id":self.response_alarm, "can_mask": 0xFFFFFFF}]
self.bus = can.Bus(interface='socketcan',
channel='can0',
receive_own_messages=False,
can_filters=can_filters)

result = self.read_status_data(self.bus)

return result

def get_settings(self):
self.capacity = BATTERY_CAPACITY
self.max_battery_current = MAX_BATTERY_CURRENT
self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT
return True

def refresh_data(self):
result = False

result = self.read_soc_data(self.bus)
result = result and self.read_fed_data(self.bus)
if self.poll_step == 0:
# This must be listed in step 0 as get_min_cell_voltage and get_max_cell_voltage in battery.py needs it at first cycle for publish_dbus in dbushelper.py
result = result and self.read_cell_voltage_range_data(self.bus)
elif self.poll_step == 1:
result = result and self.read_alarm_data(self.bus)
elif self.poll_step == 2:
result = result and self.read_cells_volts(self.bus)
elif self.poll_step == 3:
result = result and self.read_temperature_range_data(self.bus)
#else: # A placeholder to remind this is the last step. Add any additional steps before here
# This is last step so reset poll_step
self.poll_step = -1

self.poll_step += 1

return result

def read_status_data(self, bus):
status_data = self.read_bus_data_daly(bus, self.command_status)
# check if connection success
if status_data is False:
logger.debug("read_status_data")
return False

self.cell_count, self.temp_sensors, self.charger_connected, self.load_connected, \
state, self.cycles = unpack_from('>bb??bhx', status_data)

self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count
self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count

self.hardware_version = "DalyBMS " + str(self.cell_count) + " cells"
logger.info(self.hardware_version)
return True

def read_soc_data(self, ser):
# Ensure data received is valid
crntMinValid = -(MAX_BATTERY_DISCHARGE_CURRENT * 2.1)
crntMaxValid = (MAX_BATTERY_CURRENT * 1.3)
triesValid = 2
while triesValid > 0:
soc_data = self.read_bus_data_daly(ser, self.command_soc)
# check if connection success
if soc_data is False:
return False

voltage, tmp, current, soc = unpack_from('>hhhh', soc_data)
current = ((current - self.CURRENT_ZERO_CONSTANT) / -10 * INVERT_CURRENT_MEASUREMENT)
#logger.info("voltage: " + str(voltage) + ", current: " + str(current) + ", soc: " + str(soc))
if crntMinValid < current < crntMaxValid:
self.voltage = (voltage / 10)
self.current = current
self.soc = (soc / 10)
return True

logger.warning("read_soc_data - triesValid " + str(triesValid))
triesValid -= 1

return False

def read_alarm_data(self, ser):
alarm_data = self.read_bus_data_daly(ser, self.command_alarm)
# check if connection success
if alarm_data is False:
logger.warning("read_alarm_data")
return False

al_volt, al_temp, al_crnt_soc, al_diff, \
al_mos, al_misc1, al_misc2, al_fault = unpack_from('>bbbbbbbb', alarm_data)

if al_volt & 48:
# High voltage levels - Alarm
self.voltage_high = 2
elif al_volt & 15:
# High voltage Warning levels - Pre-alarm
self.voltage_high = 1
else:
self.voltage_high = 0

if al_volt & 128:
# Low voltage level - Alarm
self.voltage_low = 2
elif al_volt & 64:
# Low voltage Warning level - Pre-alarm
self.voltage_low = 1
else:
self.voltage_low = 0

if al_temp & 2:
# High charge temp - Alarm
self.temp_high_charge = 2
elif al_temp & 1:
# High charge temp - Pre-alarm
self.temp_high_charge = 1
else:
self.temp_high_charge = 0

if al_temp & 8:
# Low charge temp - Alarm
self.temp_low_charge = 2
elif al_temp & 4:
# Low charge temp - Pre-alarm
self.temp_low_charge = 1
else:
self.temp_low_charge = 0


if al_temp & 32:
# High discharge temp - Alarm
self.temp_high_discharge = 2
elif al_temp & 16:
# High discharge temp - Pre-alarm
self.temp_high_discharge = 1
else:
self.temp_high_discharge = 0

if al_temp & 128:
# Low discharge temp - Alarm
self.temp_low_discharge = 2
elif al_temp & 64:
# Low discharge temp - Pre-alarm
self.temp_low_discharge = 1
else:
self.temp_low_discharge = 0

#if al_crnt_soc & 2:
# # High charge current - Alarm
# self.current_over = 2
#elif al_crnt_soc & 1:
# # High charge current - Pre-alarm
# self.current_over = 1
#else:
# self.current_over = 0

#if al_crnt_soc & 8:
# # High discharge current - Alarm
# self.current_over = 2
#elif al_crnt_soc & 4:
# # High discharge current - Pre-alarm
# self.current_over = 1
#else:
# self.current_over = 0

if al_crnt_soc & 2 or al_crnt_soc & 8:
# High charge/discharge current - Alarm
self.current_over = 2
elif al_crnt_soc & 1 or al_crnt_soc & 4:
# High charge/discharge current - Pre-alarm
self.current_over = 1
else:
self.current_over = 0

if al_crnt_soc & 128:
# Low SoC - Alarm
self.soc_low = 2
elif al_crnt_soc & 64:
# Low SoC Warning level - Pre-alarm
self.soc_low = 1
else:
self.soc_low = 0

return True

def read_cells_volts(self, bus):
if self.cell_count is not None:
cells_volts_data = self.read_bus_data_daly(bus, self.command_cell_volts, 6)
if cells_volts_data is False:
logger.warning("read_cells_volts")
return False

frameCell = [0, 0, 0]
lowMin = (MIN_CELL_VOLTAGE / 2)
frame = 0
bufIdx = 0

if len(self.cells) != self.cell_count:
# init the numbers of cells
self.cells = []
for idx in range(self.cell_count):
self.cells.append(Cell(True))

while bufIdx < len(cells_volts_data):
frame, frameCell[0], frameCell[1], frameCell[2] = unpack_from('>Bhhh', cells_volts_data, bufIdx)
for idx in range(3):
cellnum = ((frame - 1) * 3) + idx # daly is 1 based, driver 0 based
if cellnum >= self.cell_count:
break
cellVoltage = frameCell[idx] / 1000
self.cells[cellnum].voltage = None if cellVoltage < lowMin else cellVoltage
bufIdx += 8

return True

def read_cell_voltage_range_data(self, ser):
minmax_data = self.read_bus_data_daly(ser, self.command_minmax_cell_volts)
# check if connection success
if minmax_data is False:
logger.warning("read_cell_voltage_range_data")
return False

cell_max_voltage,self.cell_max_no,cell_min_voltage, self.cell_min_no = unpack_from('>hbhb', minmax_data)
# Daly cells numbers are 1 based and not 0 based
self.cell_min_no -= 1
self.cell_max_no -= 1
# Voltage is returned in mV
self.cell_max_voltage = cell_max_voltage / 1000
self.cell_min_voltage = cell_min_voltage / 1000
return True

def read_temperature_range_data(self, ser):
minmax_data = self.read_bus_data_daly(ser, self.command_minmax_temp)
# check if connection success
if minmax_data is False:
logger.debug("read_temperature_range_data")
return False

max_temp,max_no,min_temp, min_no = unpack_from('>bbbb', minmax_data)
self.temp1 = min_temp - self.TEMP_ZERO_CONSTANT
self.temp2 = max_temp - self.TEMP_ZERO_CONSTANT
return True

def read_fed_data(self, ser):
fed_data = self.read_bus_data_daly(ser, self.command_fet)
# check if connection success
if fed_data is False:
logger.debug("read_fed_data")
return False

status, self.charge_fet, self.discharge_fet, bms_cycles, capacity_remain = unpack_from('>b??BL', fed_data)
self.capacity_remain = capacity_remain / 1000
return True

def read_bus_data_daly(self, bus, command, expectedMessageCount = 1):
# TODO handling of error cases
message = can.Message(arbitration_id=command)
bus.send(message, timeout=0.2)
response = bytearray()

# TODO use async notifier instead of this where we expect a specific frame to be received
# this could end up in a deadlock if a package is not received
count = 0
for msg in bus:
# print(f"{msg.arbitration_id:X}: {msg.data}")
# logger.info('Frame: ' + ", ".join(hex(b) for b in msg.data))
response.extend(msg.data)
count += 1
if count == expectedMessageCount:
break
return response
4 changes: 4 additions & 0 deletions etc/dbus-serialbattery/dbus-serialbattery.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
# enabled only if explicitly set in config under "BMS_TYPE"
if "ANT" in utils.BMS_TYPE:
from bms.ant import ANT
if "Daly_CAN" in utils.BMS_TYPE:
from bms.daly_can import Daly_CAN
if "MNB" in utils.BMS_TYPE:
from bms.mnb import MNB
if "Sinowealth" in utils.BMS_TYPE:
Expand All @@ -56,6 +58,8 @@
# enabled only if explicitly set in config under "BMS_TYPE"
if "ANT" in utils.BMS_TYPE:
supported_bms_types.append({"bms": ANT, "baud": 19200})
if "Daly_CAN" in utils.BMS_TYPE:
supported_bms_types.append({"bms": Daly_CAN, "baud": 9600})
if "MNB" in utils.BMS_TYPE:
supported_bms_types.append({"bms": MNB, "baud": 9600})
if "Sinowealth" in utils.BMS_TYPE:
Expand Down

0 comments on commit b1880f5

Please sign in to comment.