add warning and error map to make it more readable in the monitor
This commit is contained in:
parent
6d90b65bf6
commit
7428d49a6e
|
@ -1,303 +0,0 @@
|
||||||
#!/usr/bin/python2 -u
|
|
||||||
# coding=utf-8
|
|
||||||
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
import serial
|
|
||||||
from os import system
|
|
||||||
|
|
||||||
from pymodbus.client.sync import ModbusSerialClient as Modbus
|
|
||||||
from pymodbus.exceptions import ModbusIOException
|
|
||||||
from pymodbus.pdu import ModbusResponse
|
|
||||||
from os.path import dirname, abspath
|
|
||||||
from sys import path, argv, exit
|
|
||||||
|
|
||||||
path.append(dirname(dirname(abspath(__file__))))
|
|
||||||
|
|
||||||
PAGE_SIZE = 0x100
|
|
||||||
HALF_PAGE = PAGE_SIZE / 2
|
|
||||||
WRITE_ENABLE = [1]
|
|
||||||
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
|
|
||||||
FIRMWARE_VERSION_REGISTER = 1054
|
|
||||||
|
|
||||||
ERASE_FLASH_REGISTER = 0x2084
|
|
||||||
RESET_REGISTER = 0x2087
|
|
||||||
|
|
||||||
|
|
||||||
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
|
||||||
# noinspection PyUnreachableCode
|
|
||||||
if False:
|
|
||||||
from typing import List, NoReturn, Iterable, Optional
|
|
||||||
|
|
||||||
|
|
||||||
class LockTTY(object):
|
|
||||||
|
|
||||||
def __init__(self, tty):
|
|
||||||
# type: (str) -> None
|
|
||||||
self.tty = tty
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
|
|
||||||
|
|
||||||
|
|
||||||
def calc_stm32_crc_round(crc, data):
|
|
||||||
# type: (int, int) -> int
|
|
||||||
crc = crc ^ data
|
|
||||||
for _ in range(32):
|
|
||||||
xor = (crc & 0x80000000) != 0
|
|
||||||
crc = (crc & 0x7FFFFFFF) << 1 # clear bit 31 because python ints have "infinite" bits
|
|
||||||
if xor:
|
|
||||||
crc = crc ^ 0x04C11DB7
|
|
||||||
|
|
||||||
return crc
|
|
||||||
|
|
||||||
|
|
||||||
def calc_stm32_crc(data):
|
|
||||||
# type: (Iterable[int]) -> int
|
|
||||||
crc = 0xFFFFFFFF
|
|
||||||
|
|
||||||
for dw in data:
|
|
||||||
crc = calc_stm32_crc_round(crc, dw)
|
|
||||||
|
|
||||||
return crc
|
|
||||||
|
|
||||||
|
|
||||||
def init_modbus(tty):
|
|
||||||
# type: (str) -> Modbus
|
|
||||||
|
|
||||||
return Modbus(
|
|
||||||
port='/dev/' + tty,
|
|
||||||
method='rtu',
|
|
||||||
baudrate=115200,
|
|
||||||
stopbits=1,
|
|
||||||
bytesize=8,
|
|
||||||
timeout=0.15, # seconds
|
|
||||||
parity=serial.PARITY_ODD)
|
|
||||||
|
|
||||||
|
|
||||||
def failed(response):
|
|
||||||
# type: (ModbusResponse) -> bool
|
|
||||||
|
|
||||||
# Todo 'ModbusIOException' object has no attribute 'function_code'
|
|
||||||
return response.function_code > 0x80
|
|
||||||
|
|
||||||
|
|
||||||
def clear_flash(modbus, slave_address):
|
|
||||||
# type: (Modbus, int) -> bool
|
|
||||||
|
|
||||||
print ('erasing flash...')
|
|
||||||
|
|
||||||
write_response = modbus.write_registers(address=0x2084, values=[1], unit=slave_address)
|
|
||||||
|
|
||||||
if failed(write_response):
|
|
||||||
print('erasing flash FAILED')
|
|
||||||
return False
|
|
||||||
|
|
||||||
flash_countdown = 17
|
|
||||||
while flash_countdown > 0:
|
|
||||||
read_response = modbus.read_holding_registers(address=0x2085, count=1, unit=slave_address)
|
|
||||||
|
|
||||||
if failed(read_response):
|
|
||||||
print('erasing flash FAILED')
|
|
||||||
return False
|
|
||||||
|
|
||||||
if read_response.registers[0] != flash_countdown:
|
|
||||||
flash_countdown = read_response.registers[0]
|
|
||||||
|
|
||||||
msg = str(100 * (16 - flash_countdown) / 16) + '%'
|
|
||||||
print('\r{0} '.format(msg), end=' ')
|
|
||||||
|
|
||||||
print('done!')
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
|
||||||
def bytes_to_words(bytes):
|
|
||||||
# type: (str) -> List[int]
|
|
||||||
return list(struct.unpack('>' + len(bytes)/2 * 'H', bytes))
|
|
||||||
|
|
||||||
|
|
||||||
def send_half_page_1(modbus, slave_address, data, page):
|
|
||||||
# type: (Modbus, int, str, int) -> NoReturn
|
|
||||||
|
|
||||||
first_half = [page] + bytes_to_words(data[:HALF_PAGE])
|
|
||||||
write_first_half = modbus.write_registers(0x2000, first_half, unit=slave_address)
|
|
||||||
|
|
||||||
if failed(write_first_half):
|
|
||||||
raise Exception("Failed to write page " + str(page))
|
|
||||||
|
|
||||||
|
|
||||||
def send_half_page_2(modbus, slave_address, data, page):
|
|
||||||
# type: (Modbus, int, str, int) -> NoReturn
|
|
||||||
|
|
||||||
registers = bytes_to_words(data[HALF_PAGE:]) + calc_crc(page, data) + WRITE_ENABLE
|
|
||||||
result = modbus.write_registers(0x2041, registers, unit=slave_address)
|
|
||||||
|
|
||||||
if failed(result):
|
|
||||||
raise Exception("Failed to write page " + str(page))
|
|
||||||
|
|
||||||
|
|
||||||
def get_fw_name(fw_path):
|
|
||||||
# type: (str) -> str
|
|
||||||
return fw_path.split('/')[-1].split('.')[0]
|
|
||||||
|
|
||||||
|
|
||||||
def upload_fw(modbus, slave_id, fw_path, fw_name):
|
|
||||||
# type: (Modbus, int, str, str) -> NoReturn
|
|
||||||
|
|
||||||
with open(fw_path, "rb") as f:
|
|
||||||
|
|
||||||
size = os.fstat(f.fileno()).st_size
|
|
||||||
n_pages = size / PAGE_SIZE
|
|
||||||
|
|
||||||
print('uploading firmware ' + fw_name + ' to BMS ...')
|
|
||||||
|
|
||||||
for page in range(0, n_pages):
|
|
||||||
|
|
||||||
page_data = f.read(PAGE_SIZE)
|
|
||||||
|
|
||||||
msg = "page " + str(page + 1) + '/' + str(n_pages) + ' ' + str(100 * page / n_pages + 1) + '%'
|
|
||||||
print('\r{0} '.format(msg), end=' ')
|
|
||||||
|
|
||||||
if is_page_empty(page_data):
|
|
||||||
continue
|
|
||||||
|
|
||||||
send_half_page_1(modbus, slave_id, page_data, page)
|
|
||||||
send_half_page_2(modbus, slave_id, page_data, page)
|
|
||||||
|
|
||||||
|
|
||||||
def is_page_empty(page):
|
|
||||||
# type: (str) -> bool
|
|
||||||
return page.count('\xff') == len(page)
|
|
||||||
|
|
||||||
|
|
||||||
def reset_bms(modbus, slave_id):
|
|
||||||
# type: (Modbus, int) -> bool
|
|
||||||
|
|
||||||
print ('resetting BMS...')
|
|
||||||
|
|
||||||
result = modbus.write_registers(RESET_REGISTER, [1], unit=slave_id)
|
|
||||||
|
|
||||||
# expecting a ModbusIOException (timeout)
|
|
||||||
# BMS can no longer reply because it is already reset
|
|
||||||
success = isinstance(result, ModbusIOException)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
print('done')
|
|
||||||
else:
|
|
||||||
print('FAILED to reset battery!')
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
|
|
||||||
def calc_crc(page, data):
|
|
||||||
# type: (int, str) -> List[int]
|
|
||||||
|
|
||||||
crc = calc_stm32_crc([page] + bytes_to_words(data))
|
|
||||||
crc_bytes = struct.pack('>L', crc)
|
|
||||||
|
|
||||||
return bytes_to_words(crc_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def identify_battery(modbus, slave_id):
|
|
||||||
# type: (Modbus, int) -> Optional[str]
|
|
||||||
|
|
||||||
target = 'battery #' + str(slave_id) + ' at ' + modbus.port
|
|
||||||
|
|
||||||
try:
|
|
||||||
|
|
||||||
print(('contacting ' + target + ' ...'))
|
|
||||||
|
|
||||||
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, unit=slave_id)
|
|
||||||
fw = '{0:0>4X}'.format(response.registers[0])
|
|
||||||
|
|
||||||
print(('found battery with firmware ' + fw))
|
|
||||||
|
|
||||||
return fw
|
|
||||||
|
|
||||||
except:
|
|
||||||
print(('failed to communicate with ' + target + ' !'))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def print_usage():
|
|
||||||
print(('Usage: ' + __file__ + ' <serial device> <battery id> <firmware>'))
|
|
||||||
print(('Example: ' + __file__ + ' ttyUSB0 2 A08C.bin'))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_cmdline_args(argv):
|
|
||||||
# type: (List[str]) -> (str, str, str, str)
|
|
||||||
|
|
||||||
def fail_with(msg):
|
|
||||||
print(msg)
|
|
||||||
print_usage()
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if len(argv) < 1:
|
|
||||||
fail_with('missing argument for tty device')
|
|
||||||
|
|
||||||
if len(argv) < 2:
|
|
||||||
fail_with('missing argument for battery ID')
|
|
||||||
|
|
||||||
if len(argv) < 3:
|
|
||||||
fail_with('missing argument for firmware')
|
|
||||||
|
|
||||||
return argv[0], int(argv[1]), argv[2], get_fw_name(argv[2])
|
|
||||||
|
|
||||||
|
|
||||||
def verify_firmware(modbus, battery_id, fw_name):
|
|
||||||
# type: (Modbus, int, str) -> NoReturn
|
|
||||||
|
|
||||||
fw_verify = identify_battery(modbus, battery_id)
|
|
||||||
|
|
||||||
if fw_verify == fw_name:
|
|
||||||
print('SUCCESS')
|
|
||||||
else:
|
|
||||||
print('FAILED to verify uploaded firmware!')
|
|
||||||
if fw_verify is not None:
|
|
||||||
print('expected firmware version ' + fw_name + ' but got ' + fw_verify)
|
|
||||||
|
|
||||||
|
|
||||||
def wait_for_bms_reboot():
|
|
||||||
# type: () -> NoReturn
|
|
||||||
|
|
||||||
# wait 20s for the battery to reboot
|
|
||||||
|
|
||||||
print('waiting for BMS to reboot...')
|
|
||||||
|
|
||||||
for t in range(20, 0, -1):
|
|
||||||
print('\r{0} '.format(t), end=' ')
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
print('0')
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv):
|
|
||||||
# type: (List[str]) -> NoReturn
|
|
||||||
|
|
||||||
tty, battery_id, fw_path, fw_name = parse_cmdline_args(argv)
|
|
||||||
|
|
||||||
with LockTTY(tty), init_modbus(tty) as modbus:
|
|
||||||
|
|
||||||
if identify_battery(modbus, battery_id) is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
clear_flash(modbus, battery_id)
|
|
||||||
upload_fw(modbus, battery_id, fw_path, fw_name)
|
|
||||||
|
|
||||||
if not reset_bms(modbus, battery_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
wait_for_bms_reboot()
|
|
||||||
|
|
||||||
verify_firmware(modbus, battery_id, fw_name)
|
|
||||||
|
|
||||||
|
|
||||||
main(argv[1:])
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,6 +20,7 @@
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"linq-to-typescript": "^11.0.0",
|
"linq-to-typescript": "^11.0.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"numeral": "2.0.6",
|
"numeral": "2.0.6",
|
||||||
|
|
|
@ -107,6 +107,55 @@ function Log(props: LogProps) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const warningDescriptionMap: { [key: string]: string } = {
|
||||||
|
"TaM1": "TaM1: BMS temperature high",
|
||||||
|
"TbM1": "TbM1: Battery temperature high",
|
||||||
|
"VBm1": "VBm1: Bus voltage low",
|
||||||
|
"VBM1": "VBM1: Bus voltage high",
|
||||||
|
"IDM1": "IDM1: Discharge current high",
|
||||||
|
"vsm1": "vsm1: String voltage low",
|
||||||
|
"vsM1": "vsM1: String voltage high",
|
||||||
|
"iCM1": "iCM1: Charge current high",
|
||||||
|
"iDM1": "iDM1: Discharge current high",
|
||||||
|
"MID1": "MID1: String voltages unbalanced",
|
||||||
|
"BLPW": "BLPW: Not enough charging power on bus",
|
||||||
|
"CCBF": "CCBF: Internal charger hardware failure",
|
||||||
|
"Ah_W": "Ah_W: String SOC low",
|
||||||
|
"MPMM": "MPMM: Midpoint wiring problem",
|
||||||
|
"TCdi": "TCdi: Temperature difference between strings high",
|
||||||
|
"LMPW": "LMPW: String voltages unbalance warning",
|
||||||
|
"TOCW": "TOCW: Top of Charge requested"
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorDescriptionMap: { [key: string]: string } = {
|
||||||
|
"Tam": "Tam: Recoverable, BMS temperature too low",
|
||||||
|
"TaM2": "TaM2: Recoverable, BMS temperature too high",
|
||||||
|
"Tbm": "Tbm: Recoverable, Battery temperature too low",
|
||||||
|
"TbM2": "TbM2: Recoverable, Battery temperature too high",
|
||||||
|
"VBm2": "VBm2: Recoverable, Recoverable: Bus voltage too low",
|
||||||
|
"VBM2": "VBM2: Recoverable,Recoverable: Bus voltage too high",
|
||||||
|
"IDM2": "IDM2: Recoverable, Discharge current too high",
|
||||||
|
"ISOB": "ISOB: Unrecoverable, Electrical insulation failure",
|
||||||
|
"MSWE": "MSWE: Unrecoverable, Main switch failure",
|
||||||
|
"FUSE": "FUSE: Unrecoverable, Main fuse blown",
|
||||||
|
"HTRE": "HTRE: Recoverable, Battery failed to warm up",
|
||||||
|
"TCPE": "TCPE: Unrecoverable, Temperature sensor failure",
|
||||||
|
"STRE": "STRE: Recoverable, Voltage measurement circuit fails",
|
||||||
|
"CME": "CME: Recoverable, Current sensor failure",
|
||||||
|
"HWFL": "HWFL: Recoverable, BMS hardware failure",
|
||||||
|
"HWEM": "HWEM: Recoverable, Hardware protection tripped",
|
||||||
|
"ThM": "ThM: Recoverable, Heatsink temperature too high",
|
||||||
|
"vsm2": "vsm2: Unrecoverable, Low string voltage failure",
|
||||||
|
"vsM2": "vsM2: Recoverable, String voltage too high",
|
||||||
|
"iCM2": "iCM2: Unrecoverable, Charge current too high",
|
||||||
|
"iDM2": "iDM2: Recoverable, Discharge current too high",
|
||||||
|
"MID2": "MID2: Recoverable, String voltage unbalance too high",
|
||||||
|
"HTFS": "HTFS: Recoverable, Unrecoverable: Heater Fuse Blown",
|
||||||
|
"DATA": "DATA: Recoverable, Unrecoverable: Parameters out of range",
|
||||||
|
"LMPA": "LMPA: Unrecoverable, String voltages unbalance alarm",
|
||||||
|
"HEBT": "HEBT: Recoverable, oss of heartbeat"
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl">
|
<Container maxWidth="xl">
|
||||||
<Grid container>
|
<Grid container>
|
||||||
|
@ -304,7 +353,7 @@ function Log(props: LogProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
{error.description}
|
{errorDescriptionMap[error.description] || error.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -619,7 +668,7 @@ function Log(props: LogProps) {
|
||||||
gutterBottom
|
gutterBottom
|
||||||
noWrap
|
noWrap
|
||||||
>
|
>
|
||||||
{warning.description}
|
{warningDescriptionMap[warning.description] || warning.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
Loading…
Reference in New Issue