Skip to content

Commit

Permalink
added CAN bms to installation script
Browse files Browse the repository at this point in the history
optimized CAN drivers
  • Loading branch information
mr-manuel committed Sep 17, 2023
1 parent d4aef1c commit a84a29b
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 70 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
* Added: Create unique identifier, if not provided from BMS by @mr-manuel
* Added: Current average of the last 5 minutes by @mr-manuel
* Added: Daly BMS - Auto reset SoC when changing to float (can be turned off in the config file) by @transistorgit
* Added: Daly BMS connect via CAN (experimental, some limits apply) with https://github.com/Louisvdw/dbus-serialbattery/pull/169 by @SamuelBrucksch and @mr-manuel
* Added: Exclude a device from beeing used by the dbus-serialbattery driver by @mr-manuel
* Added: Implement callback function for update by @seidler2547
* Added: JKBMS BLE - Automatic SOC reset with https://github.com/Louisvdw/dbus-serialbattery/pull/736 by @ArendsM
* Added: JKBMS BLE - Show last five characters from the MAC address in the custom name (which is displayed in the device list) by @mr-manuel
* Added: JKBMS BMS connect via CAN (experimental, some limits apply) by @IrisCrimson and @mr-manuel
* Added: LLT/JBD BMS - Discharge / Charge Mosfet and disable / enable balancer switching over remote console/GUI with https://github.com/Louisvdw/dbus-serialbattery/pull/761 by @idstein
* Added: LLT/JBD BMS - Show balancer state in GUI under the IO page with https://github.com/Louisvdw/dbus-serialbattery/pull/763 by @idstein
* Added: Load to bulk voltage every x days to reset the SoC to 100% for some BMS by @mr-manuel
Expand Down
65 changes: 40 additions & 25 deletions etc/dbus-serialbattery/bms/daly_can.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
# -*- 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 *
from battery import Battery, Cell
from utils import (
BATTERY_CAPACITY,
INVERT_CURRENT_MEASUREMENT,
logger,
MAX_BATTERY_CHARGE_CURRENT,
MAX_BATTERY_DISCHARGE_CURRENT,
MAX_CELL_VOLTAGE,
MIN_CELL_VOLTAGE,
)
from struct import unpack_from
import can

"""
https://github.com/Louisvdw/dbus-serialbattery/pull/169
"""


class Daly_Can(Battery):
def __init__(self, port, baud):
super(Daly_Can, self).__init__(port, baud)
def __init__(self, port, baud, address):
super(Daly_Can, self).__init__(port, baud, address)
self.charger_connected = None
self.load_connected = None
self.cell_min_voltage = None
Expand All @@ -18,7 +30,7 @@ def __init__(self, port, baud):
self.poll_interval = 1000
self.poll_step = 0
self.type = self.BATTERYTYPE
self.bus = None
self.can_bus = None

# command bytes [Priority=18][Command=94][BMS ID=01][Uplink ID=40]
command_base = 0x18940140
Expand Down Expand Up @@ -65,37 +77,38 @@ def test_connection(self):
{"can_id": self.response_cell_balance, "can_mask": 0xFFFFFFF},
{"can_id": self.response_alarm, "can_mask": 0xFFFFFFF},
]
self.bus = can.Bus(
self.can_bus = can.Bus(
interface="socketcan",
channel="can0",
channel=self.port,
receive_own_messages=False,
can_filters=can_filters,
)

result = self.read_status_data(self.bus)
result = self.read_status_data(self.can_bus)

return result

def get_settings(self):
self.capacity = BATTERY_CAPACITY
self.max_battery_current = MAX_BATTERY_CURRENT
self.max_battery_current = MAX_BATTERY_CHARGE_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)
result = self.read_soc_data(self.can_bus)
result = result and self.read_fed_data(self.can_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)
# 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.can_bus)
elif self.poll_step == 1:
result = result and self.read_alarm_data(self.bus)
result = result and self.read_alarm_data(self.can_bus)
elif self.poll_step == 2:
result = result and self.read_cells_volts(self.bus)
result = result and self.read_cells_volts(self.can_bus)
elif self.poll_step == 3:
result = result and self.read_temperature_range_data(self.bus)
result = result and self.read_temperature_range_data(self.can_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
Expand All @@ -104,8 +117,8 @@ def refresh_data(self):

return result

def read_status_data(self, bus):
status_data = self.read_bus_data_daly(bus, self.command_status)
def read_status_data(self, can_bus):
status_data = self.read_bus_data_daly(can_bus, self.command_status)
# check if connection success
if status_data is False:
logger.debug("read_status_data")
Expand All @@ -130,7 +143,7 @@ def read_status_data(self, bus):
def read_soc_data(self, ser):
# Ensure data received is valid
crntMinValid = -(MAX_BATTERY_DISCHARGE_CURRENT * 2.1)
crntMaxValid = MAX_BATTERY_CURRENT * 1.3
crntMaxValid = MAX_BATTERY_CHARGE_CURRENT * 1.3
triesValid = 2
while triesValid > 0:
soc_data = self.read_bus_data_daly(ser, self.command_soc)
Expand Down Expand Up @@ -266,9 +279,11 @@ def read_alarm_data(self, ser):

return True

def read_cells_volts(self, bus):
def read_cells_volts(self, can_bus):
if self.cell_count is not None:
cells_volts_data = self.read_bus_data_daly(bus, self.command_cell_volts, 6)
cells_volts_data = self.read_bus_data_daly(
can_bus, self.command_cell_volts, 6
)
if cells_volts_data is False:
logger.warning("read_cells_volts")
return False
Expand Down Expand Up @@ -350,16 +365,16 @@ def read_fed_data(self, ser):
self.capacity_remain = capacity_remain / 1000
return True

def read_bus_data_daly(self, bus, command, expectedMessageCount=1):
def read_bus_data_daly(self, can_bus, command, expectedMessageCount=1):
# TODO handling of error cases
message = can.Message(arbitration_id=command)
bus.send(message, timeout=0.2)
can_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:
for msg in can_bus:
# print(f"{msg.arbitration_id:X}: {msg.data}")
# logger.info('Frame: ' + ", ".join(hex(b) for b in msg.data))
response.extend(msg.data)
Expand Down
33 changes: 27 additions & 6 deletions etc/dbus-serialbattery/bms/jkbms_brn.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,12 @@ def crc(self, arr: bytearray, length: int) -> int:
return crc.to_bytes(2, "little")[0]

async def write_register(
self, address, vals: bytearray, length: int, bleakC: BleakClient, awaitresponse: bool
self,
address,
vals: bytearray,
length: int,
bleakC: BleakClient,
awaitresponse: bool,
):
frame = bytearray(20)
frame[0] = 0xAA # start sequence
Expand Down Expand Up @@ -428,23 +433,39 @@ async def enable_charging(self, c):

def jk_float_to_hex_little(self, val: float):
intval = int(val * 1000)
hexval = f'{intval:0>8X}'
hexval = f"{intval:0>8X}"
return bytearray.fromhex(hexval)[::-1]

async def reset_soc_jk(self, c):
# Lowering OVPR / OVP based on the maximum cell voltage at the time
# That will trigger a High Voltage Alert and resets SOC to 100%
ovp_trigger = round(self.max_cell_voltage - 0.05, 3)
ovpr_trigger = round(self.max_cell_voltage - 0.10, 3)
await self.write_register(JK_REGISTER_OVPR, self.jk_float_to_hex_little(ovpr_trigger), 0x04, c, True)
await self.write_register(JK_REGISTER_OVP, self.jk_float_to_hex_little(ovp_trigger), 0x04, c, True)
await self.write_register(
JK_REGISTER_OVPR, self.jk_float_to_hex_little(ovpr_trigger), 0x04, c, True
)
await self.write_register(
JK_REGISTER_OVP, self.jk_float_to_hex_little(ovp_trigger), 0x04, c, True
)

# Give BMS some time to recognize
await asyncio.sleep(5)

# Set values back to initial values
await self.write_register(JK_REGISTER_OVP, self.jk_float_to_hex_little(self.ovp_initial_voltage), 0X04, c, True)
await self.write_register(JK_REGISTER_OVPR, self.jk_float_to_hex_little(self.ovpr_initial_voltage), 0x04, c, True)
await self.write_register(
JK_REGISTER_OVP,
self.jk_float_to_hex_little(self.ovp_initial_voltage),
0x04,
c,
True,
)
await self.write_register(
JK_REGISTER_OVPR,
self.jk_float_to_hex_little(self.ovpr_initial_voltage),
0x04,
c,
True,
)

logging.info("JK BMS SOC reset finished.")

Expand Down
56 changes: 35 additions & 21 deletions etc/dbus-serialbattery/bms/jkbms_can.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
# -*- 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 *
from battery import Battery, Cell
from utils import (
is_bit_set,
logger,
MAX_BATTERY_CHARGE_CURRENT,
MAX_BATTERY_DISCHARGE_CURRENT,
MAX_CELL_VOLTAGE,
MIN_CELL_VOLTAGE,
zero_char,
)
from struct import unpack_from
import can
import time

CAN_BUS_TYPE = "socketcan"
"""
https://github.com/Louisvdw/dbus-serialbattery/compare/dev...IrisCrimson:dbus-serialbattery:jkbms_can
# Restrictions seen from code:
-
"""

class Jkbms_CAN(Battery):
def __init__(self, port, baud):
super(Jkbms_CAN, self).__init__(port, baud)
self._can_bus = False

class Jkbms_Can(Battery):
def __init__(self, port, baud, address):
super(Jkbms_Can, self).__init__(port, baud, address)
self.can_bus = False
self.cell_count = 1
self.poll_interval = 1500
self.type = self.BATTERYTYPE
self.last_error_time = time.time()
self.error_active = False

def __del__(self):
if self._can_bus:
self._can_bus.shutdown()
self._can_bus = False
if self.can_bus:
self.can_bus.shutdown()
self.can_bus = False
logger.debug("bus shutdown")

BATTERYTYPE = "Jkbms"
BATTERYTYPE = "Jkbms_Can"
CAN_BUS_TYPE = "socketcan"

CURRENT_ZERO_CONSTANT = 400
BATT_STAT = "BATT_STAT"
Expand All @@ -52,10 +66,10 @@ def get_settings(self):
# After successful connection get_settings will be call to set up the battery.
# Set the current limits, populate cell count, etc
# Return True if success, False for failure
self.max_battery_current = MAX_BATTERY_CURRENT
self.max_battery_current = MAX_BATTERY_CHARGE_CURRENT
self.max_battery_discharge_current = MAX_BATTERY_DISCHARGE_CURRENT
self.max_battery_voltage = MAX_CELL_VOLTAGE * 16 # self.cell_count
self.min_battery_voltage = MIN_CELL_VOLTAGE * 16 # self.cell_count
self.max_battery_voltage = MAX_CELL_VOLTAGE * self.cell_count
self.min_battery_voltage = MIN_CELL_VOLTAGE * self.cell_count

# init the cell array add only missing Cell instances
missing_instances = self.cell_count - len(self.cells)
Expand Down Expand Up @@ -157,30 +171,30 @@ def reset_protection_bits(self):
self.protection.internal_failure = 0

def read_serial_data_jkbms_CAN(self):
if self._can_bus is False:
if self.can_bus is False:
logger.debug("Can bus init")
# intit the can interface
try:
self._can_bus = can.interface.Bus(
bustype=CAN_BUS_TYPE, channel=self.port, bitrate=self.baud_rate
self.can_bus = can.interface.Bus(
bustype=self.CAN_BUS_TYPE, channel=self.port, bitrate=self.baud_rate
)
except can.CanError as e:
logger.error(e)

if self._can_bus is None:
if self.can_bus is None:
return False

logger.debug("Can bus init done")

# reset errors after timeout
if ((time.time() - self.last_error_time) > 120.0) and self.error_active == True:
if ((time.time() - self.last_error_time) > 120.0) and self.error_active is True:
self.error_active = False
self.reset_protection_bits()

# read msgs until we get one we want
messages_to_read = self.MESSAGES_TO_READ
while messages_to_read > 0:
msg = self._can_bus.recv(1)
msg = self.can_bus.recv(1)
if msg is None:
logger.info("No CAN Message received")
return False
Expand Down
9 changes: 9 additions & 0 deletions etc/dbus-serialbattery/config.default.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ BULK_AFTER_DAYS =
; 3 BMS: Jkbms_Ble C8:47:8C:00:00:00, Jkbms_Ble C8:47:8C:00:00:11, Jkbms_Ble C8:47:8C:00:00:22
BLUETOOTH_BMS =

; --------- CAN BMS ---------
; Description: Specify the CAN port(s) where the BMS is connected to. Leave empty to disable
; -- Available CAN BMS:
; Daly_Can, Jkbms_Can
; Example:
; can0
; can0, can8, can9
CAN_PORT =

; --------- BMS disconnect behaviour ---------
; Description: Block charge and discharge when the communication to the BMS is lost. If you are removing the
; BMS on purpose, then you have to restart the driver/system to reset the block.
Expand Down
30 changes: 22 additions & 8 deletions etc/dbus-serialbattery/dbus-serialbattery.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@
# 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 "Jkbms_Can" in utils.BMS_TYPE:
from bms.jkbms_can import Jkbms_Can
if "MNB" in utils.BMS_TYPE:
from bms.mnb import MNB
if "Sinowealth" in utils.BMS_TYPE:
Expand All @@ -60,10 +56,6 @@
# 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 "Jkbms_Can" in utils.BMS_TYPE:
supported_bms_types.append({"bms": Jkbms_Can, "baud": 250000})
if "MNB" in utils.BMS_TYPE:
supported_bms_types.append({"bms": MNB, "baud": 9600})
if "Sinowealth" in utils.BMS_TYPE:
Expand Down Expand Up @@ -177,6 +169,28 @@ def get_port() -> str:
if testbms.test_connection():
logger.info("Connection established to " + testbms.__class__.__name__)
battery = testbms
elif port.startswith("can"):
"""
Import CAN classes only, if it's a can port, else the driver won't start due to missing python modules
This prevent problems when using the driver only with a serial connection
"""
from bms.daly_can import Daly_Can
from bms.jkbms_can import Jkbms_Can

# only try CAN BMS on CAN port
supported_bms_types = [
{"bms": Daly_Can, "baud": 250000},
{"bms": Jkbms_Can, "baud": 250000},
]

expected_bms_types = [
battery_type
for battery_type in supported_bms_types
if battery_type["bms"].__name__ in utils.BMS_TYPE
or len(utils.BMS_TYPE) == 0
]

battery = get_battery(port)
else:
battery = get_battery(port)

Expand Down
Loading

0 comments on commit a84a29b

Please sign in to comment.