Fixed path problems in front-end (configuration tab)
Push scripts for battery logging download and battery firmware update
This commit is contained in:
parent
5d358fa269
commit
39fc4ad331
|
@ -296,7 +296,7 @@ public static class ExoCmd
|
||||||
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
|
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
|
||||||
string replyMessage = Encoding.UTF8.GetString(replyData);
|
string replyMessage = Encoding.UTF8.GetString(replyData);
|
||||||
Console.WriteLine("Received " + replyMessage + " from installation " + installation.VpnIp);
|
Console.WriteLine("Received " + replyMessage + " from installation " + installation.VpnIp);
|
||||||
break;
|
return true;
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
{
|
{
|
||||||
|
@ -310,7 +310,7 @@ public static class ExoCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
|
||||||
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
|
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
|
||||||
|
|
|
@ -0,0 +1,284 @@
|
||||||
|
#!/usr/bin/python2 -u
|
||||||
|
# coding=utf-8
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import serial
|
||||||
|
import logging
|
||||||
|
from sys import argv, exit
|
||||||
|
from datetime import datetime
|
||||||
|
from pymodbus.pdu import ModbusRequest, ModbusResponse, ExceptionResponse
|
||||||
|
from pymodbus.other_message import ReportSlaveIdRequest
|
||||||
|
from pymodbus.exceptions import ModbusException
|
||||||
|
from pymodbus.pdu import ExceptionResponse
|
||||||
|
from pymodbus.factory import ClientDecoder
|
||||||
|
from pymodbus.client import ModbusSerialClient as Modbus
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# trick the pycharm type-checker into thinking Callable is in scope, not used at runtime
|
||||||
|
# noinspection PyUnreachableCode
|
||||||
|
if False:
|
||||||
|
from typing import List, Optional, NoReturn
|
||||||
|
|
||||||
|
RESET_REGISTER = 0x2087
|
||||||
|
FIRMWARE_VERSION_REGISTER = 1054
|
||||||
|
SERIAL_STARTER_DIR = '/opt/victronenergy/serial-starter/'
|
||||||
|
INSTALLATION_NAME_FILE = '/data/innovenergy/openvpn/installation-name'
|
||||||
|
OUTPUT_DIR = '/data/innovenergy'
|
||||||
|
|
||||||
|
|
||||||
|
class ReadLogRequest(ModbusRequest):
|
||||||
|
|
||||||
|
function_code = 0x42
|
||||||
|
_rtu_frame_size = 5 # not used
|
||||||
|
|
||||||
|
def __init__(self, address = None, **kwargs):
|
||||||
|
|
||||||
|
ModbusRequest.__init__(self, **kwargs)
|
||||||
|
self.sub_function = 0 if address is None else 1
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
# FUGLY as hell, but necessary bcs PyModbus cannot deal
|
||||||
|
# with responses that have lengths depending on the sub_function.
|
||||||
|
# it goes without saying that this isn't thread-safe
|
||||||
|
ReadLogResponse._rtu_frame_size = 9 if self.sub_function == 0 else 9+128
|
||||||
|
|
||||||
|
def encode(self):
|
||||||
|
|
||||||
|
if self.sub_function == 0:
|
||||||
|
return struct.pack('>B', self.sub_function)
|
||||||
|
else:
|
||||||
|
return struct.pack('>BI', self.sub_function, self.address)
|
||||||
|
|
||||||
|
def decode(self, data):
|
||||||
|
self.sub_function = struct.unpack('>B', data)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
print("EXECUTE1")
|
||||||
|
|
||||||
|
def get_response_pdu_size(self):
|
||||||
|
return ReadLogResponse._rtu_frame_size - 3
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "ReadLogAddressRequest"
|
||||||
|
|
||||||
|
|
||||||
|
class ReadLogResponse(ModbusResponse):
|
||||||
|
|
||||||
|
function_code = 0x42
|
||||||
|
_rtu_frame_size = 9 # the WHOLE frame incl crc
|
||||||
|
|
||||||
|
def __init__(self, sub_function=0, address=b'\x00', data=None, **kwargs):
|
||||||
|
ModbusResponse.__init__(self, **kwargs)
|
||||||
|
self.sub_function = sub_function
|
||||||
|
self.address = address
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def encode(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def decode(self, data):
|
||||||
|
self.address, self.address = struct.unpack_from(">BI", data)
|
||||||
|
self.data = data[5:]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
arguments = (self.function_code, self.address)
|
||||||
|
return "ReadLogAddressResponse(%s, %s)" % arguments
|
||||||
|
|
||||||
|
# unfortunately we have to monkey-patch this global table because
|
||||||
|
# the current (victron) version of PyModbus does not have a
|
||||||
|
# way to "register" new function-codes yet
|
||||||
|
ClientDecoder.function_table.append(ReadLogResponse)
|
||||||
|
|
||||||
|
|
||||||
|
class LockTTY(object):
|
||||||
|
|
||||||
|
def __init__(self, tty):
|
||||||
|
# type: (str) -> None
|
||||||
|
self.tty = tty
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
os.system(SERIAL_STARTER_DIR + 'stop-tty.sh ' + self.tty)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
os.system(SERIAL_STARTER_DIR + 'start-tty.sh ' + self.tty)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_try_except(error_msg):
|
||||||
|
def decorate(f):
|
||||||
|
def applicator(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
except:
|
||||||
|
print(error_msg)
|
||||||
|
exit(1)
|
||||||
|
return applicator
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def init_modbus(tty):
|
||||||
|
# type: (str) -> Modbus
|
||||||
|
|
||||||
|
return Modbus(
|
||||||
|
port='/dev/' + tty,
|
||||||
|
method='rtu',
|
||||||
|
baudrate=115200,
|
||||||
|
stopbits=1,
|
||||||
|
bytesize=8,
|
||||||
|
timeout=0.5, # seconds
|
||||||
|
parity=serial.PARITY_ODD)
|
||||||
|
|
||||||
|
|
||||||
|
@wrap_try_except("Failed to download BMS log!")
|
||||||
|
def download_log(modbus, node_id, battery_id):
|
||||||
|
# type: (Modbus, int, str) -> NoReturn
|
||||||
|
|
||||||
|
# Get address of latest log entry
|
||||||
|
# request = ReadLogRequest(unit=slave_id)
|
||||||
|
|
||||||
|
print ('downloading BMS log from node ' + str(node_id) + ' ...')
|
||||||
|
|
||||||
|
progress = -1
|
||||||
|
log_file = battery_id + "-node" + str(node_id) + "-" + datetime.now().strftime('%d-%m-%Y') + ".bin"
|
||||||
|
print(log_file)
|
||||||
|
|
||||||
|
with open(log_file, 'w') as f:
|
||||||
|
|
||||||
|
eof = 0x200000
|
||||||
|
record = 0x40
|
||||||
|
for address in range(0, eof, 2*record):
|
||||||
|
|
||||||
|
percent = int(100*address/eof)
|
||||||
|
|
||||||
|
if percent != progress:
|
||||||
|
progress = percent
|
||||||
|
print('\r{}% '.format(progress),end='')
|
||||||
|
|
||||||
|
request = ReadLogRequest(address, slave=node_id)
|
||||||
|
result = modbus.execute(request) # type: ReadLogResponse
|
||||||
|
|
||||||
|
address1 = "{:06X}".format(address)
|
||||||
|
address2 = "{:06X}".format(address+record)
|
||||||
|
|
||||||
|
data1 = result.data[:record]
|
||||||
|
data2 = result.data[record:]
|
||||||
|
|
||||||
|
line1 = address1 + ":" + ''.join('{:02X}'.format(byte) for byte in data1)
|
||||||
|
line2 = address2 + ":" + ''.join('{:02X}'.format(byte) for byte in data2)
|
||||||
|
|
||||||
|
lines = line1 + "\n" + line2 + "\n"
|
||||||
|
f.write(lines)
|
||||||
|
|
||||||
|
print("\r100%")
|
||||||
|
print("done")
|
||||||
|
print("wrote log to " + log_file)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@wrap_try_except("Failed to contact battery!")
|
||||||
|
def identify_battery(modbus, node_id):
|
||||||
|
# type: (Modbus, int) -> str
|
||||||
|
|
||||||
|
target = 'battery #' + str(node_id)
|
||||||
|
print('contacting ' + target + ' ...')
|
||||||
|
|
||||||
|
request = ReportSlaveIdRequest(slave=node_id)
|
||||||
|
response = modbus.execute(request)
|
||||||
|
|
||||||
|
index_of_ff = response.identifier.find(b'\xff')
|
||||||
|
sid_response = response.identifier[index_of_ff + 1:].decode('utf-8').split(' ')
|
||||||
|
|
||||||
|
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=node_id)
|
||||||
|
|
||||||
|
fw = '{0:0>4X}'.format(response.registers[0])
|
||||||
|
print("log string is",sid_response[0]+"-"+sid_response[1]+"-"+fw)
|
||||||
|
|
||||||
|
#return re.sub(" +", "-", sid + " " + fw)
|
||||||
|
return sid_response[0]+"-"+sid_response[1]+"-"+fw
|
||||||
|
|
||||||
|
|
||||||
|
def is_int(value):
|
||||||
|
# type: (str) -> bool
|
||||||
|
try:
|
||||||
|
_ = int(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def print_usage():
|
||||||
|
print ('Usage: ' + __file__ + ' <slave id> <serial device>')
|
||||||
|
print ('Example: ' + __file__ + ' 2 ttyUSB0')
|
||||||
|
print ('')
|
||||||
|
print ('You can omit the "ttyUSB" prefix of the serial device:')
|
||||||
|
print (' ' + __file__ + ' 2 0')
|
||||||
|
print ('')
|
||||||
|
print ('You can omit the serial device entirely when the "com.victronenergy.battery.<serial device>" service is running:')
|
||||||
|
print (' ' + __file__ + ' 2')
|
||||||
|
print ('')
|
||||||
|
|
||||||
|
|
||||||
|
def get_tty_from_battery_service_name():
|
||||||
|
# type: () -> Optional[str]
|
||||||
|
|
||||||
|
import dbus
|
||||||
|
bus = dbus.SystemBus()
|
||||||
|
|
||||||
|
tty = (
|
||||||
|
name.split('.')[-1]
|
||||||
|
for name in bus.list_names()
|
||||||
|
if name.startswith('com.victronenergy.battery.')
|
||||||
|
)
|
||||||
|
|
||||||
|
return next(tty, None)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tty(tty):
|
||||||
|
# type: (Optional[str]) -> str
|
||||||
|
|
||||||
|
if tty is None:
|
||||||
|
return get_tty_from_battery_service_name()
|
||||||
|
|
||||||
|
if is_int(tty):
|
||||||
|
return 'ttyUSB' + argv[1]
|
||||||
|
else:
|
||||||
|
return tty
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cmdline_args(argv):
|
||||||
|
# type: (List[str]) -> (str, int)
|
||||||
|
|
||||||
|
slave_id = element_at_or_none(argv, 0)
|
||||||
|
tty = parse_tty(element_at_or_none(argv, 1))
|
||||||
|
|
||||||
|
if slave_id is None or tty is None:
|
||||||
|
print_usage()
|
||||||
|
exit(2)
|
||||||
|
|
||||||
|
print("tty=",tty)
|
||||||
|
print("slave id= ",slave_id)
|
||||||
|
|
||||||
|
return tty, int(slave_id)
|
||||||
|
|
||||||
|
|
||||||
|
def element_at_or_none(lst, index):
|
||||||
|
return next(iter(lst[index:]), None)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
# type: (List[str]) -> ()
|
||||||
|
|
||||||
|
tty, node_id = parse_cmdline_args(argv)
|
||||||
|
|
||||||
|
with init_modbus(tty) as modbus:
|
||||||
|
battery_id = identify_battery(modbus, node_id)
|
||||||
|
download_log(modbus, node_id, battery_id)
|
||||||
|
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
main(argv[1:])
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#Prototype 10.2.3.115 Prototype
|
||||||
|
#Salimax0001 10.2.3.104 Marti Technik (Bern)
|
||||||
|
#Salimax0002 10.2.4.29 Weidmann d (ZG)
|
||||||
|
#Salimax0003 10.2.4.33 Elektrotechnik Stefan GmbH
|
||||||
|
#Salimax0004 10.2.4.32 Biohof Gubelmann (Walde)
|
||||||
|
#Salimax0005 10.2.4.36 Schreinerei Schönthal (Thun)
|
||||||
|
#Salimax0006 10.2.4.35 Steakhouse Mettmenstetten
|
||||||
|
#Salimax0007 10.2.4.154 LerchenhofHerr Twannberg
|
||||||
|
#Salimax0008 10.2.4.113 Wittmann Kottingbrunn
|
||||||
|
|
||||||
|
dotnet_version='net6.0'
|
||||||
|
ip_address="$1"
|
||||||
|
battery_ids="$2"
|
||||||
|
username='ie-entwicklung'
|
||||||
|
root_password='Salimax4x25'
|
||||||
|
|
||||||
|
if [ "$#" -lt 2 ]; then
|
||||||
|
echo "Error: Insufficient arguments. Usage: $0 <ip_address> <battery_ids>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to expand battery ids from a range
|
||||||
|
expand_battery_ids() {
|
||||||
|
local range="$1"
|
||||||
|
local expanded_ids=()
|
||||||
|
|
||||||
|
IFS='-' read -r start end <<< "$range"
|
||||||
|
for ((i = start; i <= end; i++)); do
|
||||||
|
expanded_ids+=("$i")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "${expanded_ids[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if battery_ids_arg contains a hyphen indicating a range
|
||||||
|
if [[ "$battery_ids" == *-* ]]; then
|
||||||
|
# Expand battery ids from the range
|
||||||
|
battery_ids=$(expand_battery_ids "$battery_ids")
|
||||||
|
else
|
||||||
|
# Use the provided battery ids
|
||||||
|
battery_ids=("$battery_ids")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ip_address: $ip_address"
|
||||||
|
echo "Battery_ids: ${battery_ids[@]}"
|
||||||
|
|
||||||
|
#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
|
||||||
|
#battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
scp download-bms-log "$username"@"$ip_address":/home/"$username"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
|
||||||
|
|
||||||
|
for battery in "${battery_ids[@]}"; do
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 download-bms-log " "$battery" " ttyUSB0"
|
||||||
|
done
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm download-bms-log"
|
||||||
|
scp "$username"@"$ip_address":/home/"$username/*.bin" .
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S rm *.bin"
|
||||||
|
|
||||||
|
echo "Deployed and ran commands on $ip_address"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
dotnet_version='net6.0'
|
||||||
|
salimax_ip="$1"
|
||||||
|
username='ie-entwicklung'
|
||||||
|
root_password='Salimax4x25'
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
|
||||||
|
battery_ids=("2" "3" "4" "5" "6" "7" "8" "9" "10" "11")
|
||||||
|
|
||||||
|
|
||||||
|
for ip_address in "${ip_addresses[@]}"; do
|
||||||
|
scp upload-bms-firmware AF0A.bin "$username"@"$ip_address":/home/"$username"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl stop battery.service"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S apt install python3-pip -y"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S pip3 install pymodbus"
|
||||||
|
|
||||||
|
for battery in "${battery_ids[@]}"; do
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S python3 upload-bms-firmware ttyUSB0 " "$battery" " AF0A.bin"
|
||||||
|
done
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl start battery.service"
|
||||||
|
ssh "$username"@"$ip_address" "echo '$root_password' | sudo -S systemctl rm upload-bms-firmware AF0A.bin"
|
||||||
|
|
||||||
|
echo "Deployed and ran commands on $ip_address"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,303 @@
|
||||||
|
#!/usr/bin/python2 -u
|
||||||
|
# coding=utf-8
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from os import system
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pymodbus.client 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 =int( PAGE_SIZE / 2)
|
||||||
|
WRITE_ENABLE = [1]
|
||||||
|
FIRMWARE_VERSION_REGISTER = 1054
|
||||||
|
|
||||||
|
ERASE_FLASH_REGISTER = 0x2084
|
||||||
|
RESET_REGISTER = 0x2087
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
# 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.5, # 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], slave=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, slave=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('>' + int(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, slave=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, slave=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 = int(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
|
||||||
|
sleep(0.01)
|
||||||
|
send_half_page_1(modbus, slave_id, page_data, page)
|
||||||
|
sleep(0.01)
|
||||||
|
send_half_page_2(modbus, slave_id, page_data, page)
|
||||||
|
|
||||||
|
|
||||||
|
def is_page_empty(page):
|
||||||
|
# type: (str) -> bool
|
||||||
|
return page.count(b'\xff') == len(page)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_bms(modbus, slave_id):
|
||||||
|
# type: (Modbus, int) -> bool
|
||||||
|
|
||||||
|
print ('resetting BMS...')
|
||||||
|
|
||||||
|
result = modbus.write_registers(RESET_REGISTER, [1], slave=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]
|
||||||
|
print("slave id=",slave_id)
|
||||||
|
target = 'battery ' + str(slave_id) + ' at ' + '502'
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
print(('contacting ...'))
|
||||||
|
|
||||||
|
response = modbus.read_input_registers(address=FIRMWARE_VERSION_REGISTER, count=1, slave=slave_id)
|
||||||
|
fw = '{0:0>4X}'.format(response.registers[0])
|
||||||
|
|
||||||
|
print(('found battery with firmware ' + fw))
|
||||||
|
|
||||||
|
return fw
|
||||||
|
|
||||||
|
except:
|
||||||
|
print(('failed to communicate with '))
|
||||||
|
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 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:])
|
|
@ -78,7 +78,9 @@ function Installation(props: singleInstallationProps) {
|
||||||
|
|
||||||
const fetchDataOnlyOneTime = async () => {
|
const fetchDataOnlyOneTime = async () => {
|
||||||
let success = false;
|
let success = false;
|
||||||
while (true) {
|
const max_retransmissions = 3;
|
||||||
|
|
||||||
|
for (let i = 0; i < max_retransmissions; i++) {
|
||||||
success = await fetchDataPeriodically();
|
success = await fetchDataPeriodically();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|
|
@ -67,6 +67,25 @@ function InstallationTabs() {
|
||||||
setCurrentTab(value);
|
setCurrentTab(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const navigateToTabPath = (pathname: string, tab_value: string): string => {
|
||||||
|
let pathlist = pathname.split('/');
|
||||||
|
let ret_path = '';
|
||||||
|
for (let i = 1; i < pathlist.length; i++) {
|
||||||
|
if (Number.isNaN(Number(pathlist[i]))) {
|
||||||
|
ret_path += '/';
|
||||||
|
ret_path += pathlist[i];
|
||||||
|
} else {
|
||||||
|
ret_path += '/';
|
||||||
|
ret_path += pathlist[i];
|
||||||
|
ret_path += '/';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret_path += tab_value;
|
||||||
|
return ret_path;
|
||||||
|
};
|
||||||
|
|
||||||
const singleInstallationTabs = currentUser.hasWriteAccess
|
const singleInstallationTabs = currentUser.hasWriteAccess
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -275,10 +294,7 @@ function InstallationTabs() {
|
||||||
to={
|
to={
|
||||||
tab.value === 'list' || tab.value === 'tree'
|
tab.value === 'list' || tab.value === 'tree'
|
||||||
? routes[tab.value]
|
? routes[tab.value]
|
||||||
: location.pathname.substring(
|
: navigateToTabPath(location.pathname, routes[tab.value])
|
||||||
0,
|
|
||||||
location.pathname.lastIndexOf('/') + 1
|
|
||||||
) + routes[tab.value]
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -334,12 +350,7 @@ function InstallationTabs() {
|
||||||
value={tab.value}
|
value={tab.value}
|
||||||
component={Link}
|
component={Link}
|
||||||
label={tab.label}
|
label={tab.label}
|
||||||
to={
|
to={routes[tab.value]}
|
||||||
location.pathname.substring(
|
|
||||||
0,
|
|
||||||
location.pathname.lastIndexOf('/') + 1
|
|
||||||
) + routes[tab.value]
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
Loading…
Reference in New Issue