diff --git a/etc/dbus-serialbattery/bms/daly.py b/etc/dbus-serialbattery/bms/daly.py index 54226dac..8555367b 100644 --- a/etc/dbus-serialbattery/bms/daly.py +++ b/etc/dbus-serialbattery/bms/daly.py @@ -21,7 +21,7 @@ def __init__(self, port, baud, address): self.poll_interval = 1000 self.type = self.BATTERYTYPE self.has_settings = 1 - self.reset_soc = 100 + self.reset_soc = 0 self.soc_to_set = None self.runtime = 0 # TROUBLESHOOTING for no reply errors self.trigger_force_disable_discharge = None @@ -85,12 +85,13 @@ def refresh_data(self): try: with open_serial_port(self.port, self.baud_rate) as ser: result = self.read_soc_data(ser) + self.reset_soc = self.soc if self.soc else 0 if self.runtime > 0.200: # TROUBLESHOOTING for no reply errors logger.info( " |- refresh_data: read_soc_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -100,7 +101,7 @@ def refresh_data(self): " |- refresh_data: read_fed_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -110,7 +111,7 @@ def refresh_data(self): " |- refresh_data: read_cell_voltage_range_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -120,7 +121,7 @@ def refresh_data(self): " |- refresh_data: write_soc_and_datetime - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -130,7 +131,7 @@ def refresh_data(self): " |- refresh_data: read_alarm_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -140,7 +141,7 @@ def refresh_data(self): " |- refresh_data: read_temperature_range_data - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -150,7 +151,7 @@ def refresh_data(self): " |- refresh_data: read_balance_state - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -160,7 +161,7 @@ def refresh_data(self): " |- refresh_data: read_cells_volts - result: " + str(result) + " - runtime: " - + str(self.runtime) + + str(f"{self.runtime:.1f}") + "s" ) @@ -174,7 +175,7 @@ def refresh_data(self): return result def read_status_data(self, ser): - status_data = self.read_serial_data_daly(ser, self.command_status) + status_data = self.request_data(ser, self.command_status) # check if connection success if status_data is False: logger.warning("No data received in read_status_data()") @@ -208,7 +209,7 @@ def read_soc_data(self, ser): triesValid = 2 while triesValid > 0: triesValid -= 1 - soc_data = self.read_serial_data_daly(ser, self.command_soc) + soc_data = self.request_data(ser, self.command_soc) # check if connection success if soc_data is False: continue @@ -229,7 +230,7 @@ def read_soc_data(self, ser): return False def read_alarm_data(self, ser): - alarm_data = self.read_serial_data_daly(ser, self.command_alarm) + alarm_data = self.request_data(ser, self.command_alarm) # check if connection success if alarm_data is False: logger.warning("No data received in read_alarm_data()") @@ -339,76 +340,59 @@ def read_alarm_data(self, ser): return True def read_cells_volts(self, ser): - if self.cell_count is not None: - buffer = bytearray(self.command_base) - buffer[1] = self.command_address[0] # Always serial 40 or 80 - buffer[2] = self.command_cell_volts[0] - buffer[12] = sum(buffer[:12]) & 0xFF - - # logger.info(f"{bytes(buffer).hex()}") - - if (int(self.cell_count) % 3) == 0: - maxFrame = int(self.cell_count / 3) - else: - maxFrame = int(self.cell_count / 3) + 1 - lenFixed = ( - maxFrame * 13 - ) # 0xA5, 0x01, 0x95, 0x08 + 1 byte frame + 6 byte data + 1byte reserved + chksum - - cells_volts_data = self.read_serialport_data( - ser, buffer, self.LENGTH_POS, 0, lenFixed - ) - if cells_volts_data is False: - logger.warning("No data received in read_cells_volts()") - return False + if self.cell_count is None: + return True + + # calculate how many sentences we will receive + # in each sentence, the bms will send 3 cell voltages + # so for a 4s, we will receive 2 sentences + if (int(self.cell_count) % 3) == 0: + sentences_expected = int(self.cell_count / 3) + else: + sentences_expected = int(self.cell_count / 3) + 1 + + cells_volts_data = self.request_data( + ser, self.command_cell_volts, sentences_to_receive=sentences_expected + ) + + if cells_volts_data is False: + logger.debug( + "No or invalid data has been received in read_cells_volts()" + ) # just debug level, as there are DALY BMS that send broken packages occasionally + return False + + frameCell = [0, 0, 0] + lowMin = utils.MIN_CELL_VOLTAGE / 2 + frame = 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)) - frameCell = [0, 0, 0] - lowMin = utils.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)) - - # logger.warning("data " + bytes(cells_volts_data).hex()) - - while bufIdx <= len(cells_volts_data) - ( - 4 + 8 + 1 - ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum - b1, b2, b3, b4 = unpack_from(">BBBB", cells_volts_data, bufIdx) - if b1 == 0xA5 and b2 == 0x01 and b3 == 0x95 and b4 == 0x08: - ( - frame, - frameCell[0], - frameCell[1], - frameCell[2], - _, - chk, - ) = unpack_from(">BhhhBB", cells_volts_data, bufIdx + 4) - if sum(cells_volts_data[bufIdx : bufIdx + 12]) & 0xFF != chk: - logger.warning("bad cell voltages checksum") - else: - 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 += 13 # BBBBBhhhBB -> 13 byte - else: - bufIdx += 1 # step through buffer to find valid start - logger.warning("bad cell voltages header") + # logger.warning("data " + bytes(cells_volts_data).hex()) + + # from each of the received sentences, read up to 3 voltages + for i in range(sentences_expected): + ( + frame, + frameCell[0], + frameCell[1], + frameCell[2], + ) = unpack_from(">Bhhh", cells_volts_data, 8 * i) + for idx in range(3): + cellnum = ((frame - 1) * 3) + idx # daly is 1 based, driver 0 based + if cellnum >= self.cell_count: + break # ignore possible unused bytes of last sentence + cellVoltage = frameCell[idx] / 1000 + self.cells[cellnum].voltage = ( + None if cellVoltage < lowMin else cellVoltage + ) return True def read_cell_voltage_range_data(self, ser): - minmax_data = self.read_serial_data_daly(ser, self.command_minmax_cell_volts) + minmax_data = self.request_data(ser, self.command_minmax_cell_volts) # check if connection success if minmax_data is False: logger.warning("No data received in read_cell_voltage_range_data()") @@ -429,7 +413,7 @@ def read_cell_voltage_range_data(self, ser): return True def read_balance_state(self, ser): - balance_data = self.read_serial_data_daly(ser, self.command_cell_balance) + balance_data = self.request_data(ser, self.command_cell_balance) # check if connection success if balance_data is False: logger.debug("No data received in read_balance_state()") @@ -445,7 +429,7 @@ def read_balance_state(self, ser): return True def read_temperature_range_data(self, ser): - minmax_data = self.read_serial_data_daly(ser, self.command_minmax_temp) + minmax_data = self.request_data(ser, self.command_minmax_temp) # check if connection success if minmax_data is False: logger.debug("No data received in read_temperature_range_data()") @@ -457,7 +441,7 @@ def read_temperature_range_data(self, ser): return True def read_fed_data(self, ser): - fed_data = self.read_serial_data_daly(ser, self.command_fet) + fed_data = self.request_data(ser, self.command_fet) # check if connection success if fed_data is False: logger.debug("No data received in read_fed_data()") @@ -475,7 +459,7 @@ def read_fed_data(self, ser): # new def read_capacity(self, ser): - capa_data = self.read_serial_data_daly(ser, self.command_rated_params) + capa_data = self.request_data(ser, self.command_rated_params) # check if connection success if capa_data is False: logger.warning("No data received in read_capacity()") @@ -490,7 +474,7 @@ def read_capacity(self, ser): # new def read_production_date(self, ser): - production = self.read_serial_data_daly(ser, self.command_batt_details) + production = self.request_data(ser, self.command_batt_details) # check if connection success if production is False: logger.warning("No data received in read_production_date()") @@ -502,39 +486,19 @@ def read_production_date(self, ser): # new def read_battery_code(self, ser): - lenFixed = ( - 5 * 13 - ) # batt code field is 35 bytes and we transfer 7 bytes in each telegram - data = self.read_serialport_data( - ser, - self.generate_command(self.command_batt_code), - self.LENGTH_POS, - 0, - lenFixed, - ) + data = self.request_data(ser, self.command_batt_code, sentences_to_receive=5) if data is False: logger.warning("No data received in read_battery_code()") return False - bufIdx = 0 battery_code = "" # logger.warning("data " + bytes(cells_volts_data).hex()) - while ( - bufIdx <= len(data) - 13 - ): # we at least need 13 bytes to extract the identifiers + 8 bytes payload + checksum - b1, b2, b3, b4 = unpack_from(">BBBB", data, bufIdx) - if b1 == 0xA5 and b2 == 0x01 and b3 == 0x57 and b4 == 0x08: - _, part, chk = unpack_from(">B7sB", data, bufIdx + 4) - if sum(data[bufIdx : bufIdx + 12]) & 0xFF != chk: - logger.warning( - "bad battery code checksum" - ) # use string anyhow, just warn - battery_code += part.decode("utf-8") - bufIdx += 13 # BBBBB7sB -> 13 byte - else: - bufIdx += 1 # step through buffer to find valid start - logger.warning("bad battery code header") + for i in range(5): + nr, part = unpack_from(">B7s", data, i * 8) + if nr != i + 1: + logger.warning("bad battery code index") # use string anyhow, just warn + battery_code += part.decode("utf-8") if battery_code != "": self.custom_field = sub( @@ -549,145 +513,6 @@ def read_battery_code(self, ser): ) return True - def generate_command(self, command): - buffer = bytearray(self.command_base) - buffer[1] = self.command_address[0] # Always serial 40 or 80 - buffer[2] = command[0] - buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc - return buffer - - def read_serial_data_daly(self, ser, command): - data = self.read_serialport_data( - ser, self.generate_command(command), self.LENGTH_POS, self.LENGTH_CHECK - ) - if data is False: - # sleep 100 ms and retry. - sleep(0.100) - data = self.read_serialport_data( - ser, self.generate_command(command), self.LENGTH_POS, self.LENGTH_CHECK - ) - if data is False: - logger.info("No reply to cmd " + bytes(command).hex()) - return False - else: - logger.info(" |- Error cleared, received data after one retry.") - - if len(data) <= 12: - logger.debug("Too short reply to cmd " + bytes(command).hex()) - return False - - # search sentence start - try: - idx = data.index(0xA5) - except ValueError: - logger.debug( - "No Sentence Start found for reply to cmd " + bytes(command).hex() - ) - return False - - if len(data[idx:]) <= 12: - logger.debug("Too short reply to cmd " + bytes(command).hex()) - return False - - if data[12 + idx] != sum(data[idx : 12 + idx]) & 0xFF: - logger.debug("Bad checksum in reply to cmd " + bytes(command).hex()) - return False - - _, _, _, length = unpack_from(">BBBB", data, idx) - - if length == 8: - return data[4 + idx : length + 4 + idx] - else: - logger.debug( - ">>> ERROR: Incorrect Reply to CMD " - + bytes(command).hex() - + ": 0x" - + bytes(data).hex() - ) - return False - - # Read data from previously opened serial port - def read_serialport_data( - self, - ser, - command, - length_pos, - length_check, - length_fixed=None, - length_size=None, - ): - try: - # wait shortly, else the Daly is not ready and throws a lot of no reply errors - # if you see a lot of errors, try to increase in steps of 0.005 - sleep(0.020) - - time_run = 0 - time_start = time() - ser.flushOutput() - ser.flushInput() - ser.write(command) - - length_byte_size = 1 - if length_size is not None: - if length_size.upper() == "H": - length_byte_size = 2 - elif length_size.upper() == "I" or length_size.upper() == "L": - length_byte_size = 4 - - toread = ser.inWaiting() - - while toread < (length_pos + length_byte_size): - sleep(0.005) - toread = ser.inWaiting() - time_run = time() - time_start - if time_run > 0.500: - self.runtime = time_run - logger.error(">>> ERROR: No reply - returning") - return False - - # logger.info('serial data toread ' + str(toread)) - res = ser.read(toread) - if length_fixed is not None: - length = length_fixed - else: - if len(res) < (length_pos + length_byte_size): - logger.error( - ">>> ERROR: No reply - returning [len:" + str(len(res)) + "]" - ) - return False - length_size = length_size if length_size is not None else "B" - length = unpack_from(">" + length_size, res, length_pos)[0] - - data = bytearray(res) - - packetlen = ( - length_fixed - if length_fixed is not None - else length_pos + length_byte_size + length + length_check - ) - while len(data) < packetlen: - res = ser.read(packetlen - len(data)) - data.extend(res) - sleep(0.005) - time_run = time() - time_start - if time_run > 0.500: - self.runtime = time_run - logger.error( - ">>> ERROR: No reply - returning [len:" - + str(len(data)) - + "/" - + str(length + length_check) - + "]" - ) - return False - - self.runtime = time_run - return data - - except Exception as e: - logger.error(e) - return False - def reset_soc_callback(self, path, value): if value is None: return False @@ -727,9 +552,13 @@ def write_soc_and_datetime(self, ser): logger.info(f"write soc {self.soc_to_set}%") self.soc_to_set = None # Reset value, so we will set it only once - reply = self.read_serialport_data(ser, cmd, self.LENGTH_POS, self.LENGTH_CHECK) + time_start = time() + ser.flushOutput() + ser.flushInput() + ser.write(cmd) - if reply[4] != 1: + reply = self.read_sentence(ser, self.command_set_soc) + if reply[0] != 1: logger.error("write soc failed") return True @@ -778,11 +607,12 @@ def write_charge_discharge_mos(self, ser): f"write force disable charging: {'true' if self.trigger_force_disable_charge else 'false'}" ) self.trigger_force_disable_charge = None + ser.flushOutput() + ser.flushInput() + ser.write(cmd) - reply = self.read_serialport_data( - ser, cmd, self.LENGTH_POS, self.LENGTH_CHECK - ) - if reply is False or reply[4] != cmd[4]: + reply = self.read_sentence(ser, self.command_disable_charge_mos) + if reply is False or reply[0] != cmd[4]: logger.error("write force disable charge/discharge failed") return False @@ -794,11 +624,83 @@ def write_charge_discharge_mos(self, ser): f"write force disable discharging: {'true' if self.trigger_force_disable_discharge else 'false'}" ) self.trigger_force_disable_discharge = None + ser.flushOutput() + ser.flushInput() + ser.write(cmd) - reply = self.read_serialport_data( - ser, cmd, self.LENGTH_POS, self.LENGTH_CHECK - ) - if reply is False or reply[4] != cmd[4]: + reply = self.read_sentence(ser, self.command_disable_discharge_mos) + if reply is False or reply[0] != cmd[4]: logger.error("write force disable charge/discharge failed") return False return True + + def generate_command(self, command): + buffer = bytearray(self.command_base) + buffer[1] = self.command_address[0] # Always serial 40 or 80 + buffer[2] = command[0] + buffer[12] = sum(buffer[:12]) & 0xFF # checksum calc + return buffer + + def request_data(self, ser, command, sentences_to_receive=1): + # wait shortly, else the Daly is not ready and throws a lot of no reply errors + # if you see a lot of errors, try to increase in steps of 0.005 + sleep(0.020) + + self.runtime = 0 + time_start = time() + ser.flushOutput() + ser.flushInput() + ser.write(self.generate_command(command)) + + reply = bytearray() + for i in range(sentences_to_receive): + next = self.read_sentence(ser, command) + if not next: + logger.info(f"request_data: bad reply no. {i}") + return False + reply += next + self.runtime = time() - time_start + return reply + + def read_sentence(self, ser, expected_reply, timeout=0.5): + """read one 13 byte sentence from daly smart bms. + return false if less than 13 bytes received in timeout secs, or frame errors occured + return received datasection as bytearray else + """ + time_start = time() + + reply = ser.read_until(b"\xA5") + if not reply or b"\xA5" not in reply: + logger.error( + f"read_sentence {bytes(expected_reply).hex()}: no sentence start received" + ) + return False + + idx = reply.index(b"\xA5") + reply = reply[idx:] + toread = ser.inWaiting() + while toread < 12: + sleep((12 - toread) * 0.001) + toread = ser.inWaiting() + time_run = time() - time_start + if time_run > timeout: + logger.warning(f"read_sentence {bytes(expected_reply).hex()}: timeout") + return False + + reply += ser.read(12) + _, id, cmd, length = unpack_from(">BBBB", reply) + + # logger.info(f"reply: {bytes(reply).hex()}") # debug + + if id != 1 or length != 8 or cmd != expected_reply[0]: + logger.error(f"read_sentence {bytes(expected_reply).hex()}: wrong header") + return False + + chk = unpack_from(">B", reply, 12)[0] + if sum(reply[:12]) & 0xFF != chk: + logger.warning( + f"read_sentence {bytes(expected_reply).hex()}: wrong checksum" + ) + return False + + return reply[4:12] diff --git a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml index 21bfedf4..9d0ff4c4 100644 --- a/etc/dbus-serialbattery/qml/PageLynxIonIo.qml +++ b/etc/dbus-serialbattery/qml/PageLynxIonIo.qml @@ -88,5 +88,6 @@ MbPage { MbOption{description: qsTr("Active"); value: 1} ] } + } }