Merge remote-tracking branch 'origin/main'

This commit is contained in:
Noe 2024-05-07 17:19:18 +02:00
commit 8646790824
9 changed files with 944 additions and 3 deletions

View File

@ -0,0 +1,110 @@
"MinSoc": Number, 0 - 100 this is the minimum State of Charge that the batteries must not go below,
"ForceCalibrationCharge": Boolean (true or false), A flag to force a calibration charge,
"DisplayIndividualBatteries": Boolean (true or false), To display the indvidual batteries
"PConstant": Number 0 - 1, P value of our controller.
"GridSetPoint": Number in Watts, The set point of our controller.
"BatterySelfDischargePower": Number, 200, this a physical measurement of the self discharging power.
"HoldSocZone": Number, 1, This is magic number for the soft landing factor.
"IslandMode": { // Dc Link Voltage in Island mode
"AcDc": {
"MaxDcLinkVoltage": Number, 810, Max Dc Link Voltage,
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
},
"DcDc": {
"LowerDcLinkVoltage": Number, 50, Lower Dc Link Window ,
"ReferenceDcLinkVoltage": 750, reference Dc Link
"UpperDcLinkVoltage": Number, 50, Upper Dc Link Window ,
}
},
"GridTie": {// Dc Link Voltage in GrieTie mode
"AcDc": {
"MaxDcLinkVoltage":Number, 780, Max Dc Link Voltage,
"MinDcLinkVoltage": Number, 690, Min Dc Link Voltage,
"ReferenceDcLinkVoltage": Number, 750, Reference Dc Link
},
"DcDc": {
"LowerDcLinkVoltage": Number, 20, Lower Dc Link Window ,
"ReferenceDcLinkVoltage": 750, reference Dc Link
"UpperDcLinkVoltage": Number, 20, Upper Dc Link Window ,
}
},
"MaxBatteryChargingCurrent":Number, 0 - 210, Max Charging current by DcDc
"MaxBatteryDischargingCurrent":Number, 0 - 210, Max Discharging current by DcDc
"MaxDcPower": Number, 0 - 10000, Max Power exported/imported by DcDc (10000 is the maximum)
"MaxChargeBatteryVoltage": Number, 57, Max Charging battery Voltage
"MinDischargeBatteryVoltage": Number, 0, Min Charging Battery Voltage
"Devices": { This is All Salimax devices (including offline ones)
"RelaysIp": {
"DeviceState": 1, // 0: is not present, 1: Present and Can be mesured, 2: Present but must be computed/calculted
"Host": "10.0.1.1", // Ip @ of the device in the local network
"Port": 502 // port
},
"GridMeterIp": {
"DeviceState": 1,
"Host": "10.0.4.1",
"Port": 502
},
"PvOnAcGrid": {
"DeviceState": 0, // If a device is not present
"Host": "false", // this is not important
"Port": 0 // this is not important
},
"LoadOnAcGrid": {
"DeviceState": 2, // this is a computed device
"Host": "true",
"Port": 0
},
"PvOnAcIsland": {
"DeviceState": 0,
"Host": "false",
"Port": 0
},
"IslandBusLoadMeterIp": {
"DeviceState": 1,
"Host": "10.0.4.2",
"Port": 502
},
"TruConvertAcIp": {
"DeviceState": 1,
"Host": "10.0.2.1",
"Port": 502
},
"PvOnDc": {
"DeviceState": 1,
"Host": "10.0.5.1",
"Port": 502
},
"LoadOnDc": {
"DeviceState": 0,
"Host": "false",
"Port": 0
},
"TruConvertDcIp": {
"DeviceState": 1,
"Host": "10.0.3.1",
"Port": 502
},
"BatteryIp": {
"DeviceState": 1,
"Host": "localhost",
"Port": 6855
},
"BatteryNodes": [ // this is a list of nodes
2,
3,
4,
5,
6
]
},
"S3": { // this is parameters of S3 Buckets and co
"Bucket": "8-3e5b3069-214a-43ee-8d85-57d72000c19d",
"Region": "sos-ch-dk-2",
"Provider": "exo.io",
"Key": "EXO502627299197f83e8b090f63",
"Secret": "jUNYJL6B23WjndJnJlgJj4rc1i7uh981u5Aba5xdA5s",
"ContentType": "text/plain; charset=utf-8",
"Host": "8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io",
"Url": "https://8-3e5b3069-214a-43ee-8d85-57d72000c19d.sos-ch-dk-2.exo.io"
}

View File

@ -21,6 +21,6 @@ rsync -v \
./bin/Release/$dotnet_version/linux-x64/publish/* \
$username@"$salimax_ip":~/salimax
echo -e "\n============================ Execute ============================\n"
#echo -e "\n============================ Execute ============================\n"
sshpass -p "$root_password" ssh -o StrictHostKeyChecking=no -t "$username"@"$salimax_ip" "echo '$root_password' | sudo -S sh -c 'cd salimax && ./restart'" 2>/dev/null
#sshpass -p "$root_password" ssh -o StrictHostKeyChecking=no -t "$username"@"$salimax_ip" "echo '$root_password' | sudo -S sh -c 'cd salimax && ./restart'" 2>/dev/null

View File

@ -34,7 +34,7 @@ tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
tunnel "Int Emu Meter " 10.0.4.2 502 5004
tunnel "AMPT (modbus) " 10.0.5.1 502 5005
tunnel "Adam " 10.0.1.1 502 5006 #for AMAX is 10.0.1.3
tunnel "Batteries " 127.0.0.1 6855 5007
tunnel "Batteries " 127.0.0.1 6855 5007
echo
echo "press any key to close the tunnels ..."

View File

@ -0,0 +1,127 @@
This README file provides a comprehensive guide to utilizing a Python script for interacting with S3 storage,
specifically designed for downloading and processing data files based on a specified time range and key parameters.
The script requires Python3 installed on your system and makes use of the s3cmd tool for accessing data in cloud storage.
It also illustrates the process of configuring s3cmd by creating a .s3cfg file with your access credentials.
############ Create the .s3cfg file in home directory ################
nano .s3cfg
Copy this lines inside the file.
[default]
host_base = sos-ch-dk-2.exo.io
host_bucket = %(bucket)s.sos-ch-dk-2.exo.io
access_key = EXO4d838d1360ba9fb7d51648b0
secret_key = _bmrp6ewWAvNwdAQoeJuC-9y02Lsx7NV6zD-WjljzCU
use_https = True
############ S3cmd instalation ################
Please install s3cmd for retrieving data from our Cloud storage.
sudo apt install s3cmd
############ Python3 instalation ################
To check if you have already have python3, run this command
python3 --version
To install you can use this command:
1) sudo apt update
2) sudo apt install python3
3) python3 --version (to check if pyhton3 installed correctly)
############ Run extractRange.py ################
usage: extractRange.py [-h] --key KEY --bucket-number BUCKET_NUMBER start_timestamp end_timestamp
KEY: the key can be a one word or a path
for example: /DcDc/Devices/2/Status/Dc/Battery/voltage ==> this will provide us a Dc battery Voltage of the DcDc device 2.
example : Dc/Battery/voltage ==> This will provide all DcDc Device voltage (including the avg voltage of all DcDc device)
example : voltage ==> This will provide all voltage of all devices in the Salimax
BUCKET_NUMBER: This a number of bucket name for the instalation
List of bucket number/ instalation:
1: Prototype
2: Marti Technik (Bern)
3: Schreinerei Schönthal (Thun)
4: Wittmann Kottingbrunn
5: Biohof Gubelmann (Walde)
6: Steakhouse Mettmenstetten
7: Andreas Ballif / Lerchenhof
8: Weidmann Oberwil (ZG)
9: Christian Huber (EBS Elektrotechnik)
start_timestamp end_timestamp: this must be a correct timestamp of 10 digits.
The start_timestamp must be smaller than the end_timestamp.
PS: The data will be downloaded to a folder named S3cmdData_{Bucket_Number}. If this folder does not exist, it will be created.
If the folder exist, it will try to download data if there is no files in the folder.
If the folder exist and contains at least one file, it will only data extraction.
Example command:
python3 extractRange.py 1707087500 1707091260 --key ActivePowerImportT2 --bucket-number 1
################################ EXTENDED FEATURES FOR MORE ADVANCED USAGE ################################
1) Multiple Keys Support:
The script supports the extraction of data using multiple keys. Users can specify one or multiple keys separated by commas with the --keys parameter.
This feature allows for more granular data extraction, catering to diverse data analysis requirements. For example, users can extract data for different
metrics or parameters from the same or different CSV files within the specified range.
2) Exact Match for Keys:
With the --exact_match flag, the script offers an option to enforce exact matching of keys. This means that only the rows containing a key that exactly
matches the specified key(s) will be considered during the data extraction process. This option enhances the precision of the data extraction, making it
particularly useful when dealing with CSV files that contain similar but distinct keys.
3) Dynamic Header Generation:
The script dynamically generates headers for the output CSV file based on the keys provided. This ensures that the output file accurately reflects the
extracted data, providing a clear and understandable format for subsequent analysis. The headers correspond to the keys used for data extraction, making
it easy to identify and analyze the extracted data.
4)Advanced Data Processing Capabilities:
i) Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature
is particularly useful for analytical tasks that require numerical data processing.
ii) Sampling Stepsize: The --sampling_stepsize parameter enables users to define the granularity of the time range for data extraction. By specifying the number
of 2-second intervals, users can adjust the sampling interval, allowing for flexible data retrieval based on time.
Example Command:
python3 extractRange.py 1707087500 1707091260 --keys ActivePowerImportT2,Soc --bucket-number 1 --exact_match --booleans_as_numbers
This command extracts data for ActivePowerImportT2 and TotalEnergy keys from bucket number 1, between the specified timestamps, with exact
matching of keys and boolean values converted to numbers.
Visualization and Data Analysis:
After data extraction, the script facilitates data analysis by:
i) Providing a visualization function to plot the extracted data. Users can modify this function to suit their specific analysis needs, adjusting
plot labels, titles, and other matplotlib parameters.
ii) Saving the extracted data in a CSV file, with dynamically generated headers based on the specified keys. This file can be used for further
analysis or imported into data analysis tools.
This Python script streamlines the process of data retrieval from S3 storage, offering flexible and powerful options for data extraction, visualization,
and analysis. Its support for multiple keys, exact match filtering, and advanced processing capabilities make it a valuable tool for data analysts and
researchers working with time-series data or any dataset stored in S3 buckets.

View File

@ -0,0 +1,44 @@
#!/bin/bash
host="ie-entwicklung@$1"
tunnel() {
name=$1
ip=$2
rPort=$3
lPort=$4
echo -n "$name @ $ip mapped to localhost:$lPort "
ssh -nNTL "$lPort:$ip:$rPort" "$host" 2> /dev/null &
until nc -vz 127.0.0.1 $lPort 2> /dev/null
do
echo -n .
sleep 0.3
done
echo "ok"
}
echo ""
tunnel "Trumpf Inverter (http) " 10.0.2.1 80 8001
tunnel "Trumpf DCDC (http) " 10.0.3.1 80 8002
tunnel "Ext Emu Meter (http) " 10.0.4.1 80 8003
tunnel "Int Emu Meter (http) " 10.0.4.2 80 8004
tunnel "AMPT (http) " 10.0.5.1 8080 8005
tunnel "Trumpf Inverter (modbus)" 10.0.2.1 502 5001
tunnel "Trumpf DCDC (modbus) " 10.0.3.1 502 5002
tunnel "Ext Emu Meter (modbus) " 10.0.4.1 502 5003
tunnel "Int Emu Meter " 10.0.4.2 502 5004
tunnel "AMPT (modbus) " 10.0.5.1 502 5005
tunnel "Adam " 10.0.1.1 502 5006
tunnel "Batteries " 127.0.0.1 6855 5007
echo
echo "press any key to close the tunnels ..."
read -r -n 1 -s
kill $(jobs -p)
echo "done"

View File

@ -0,0 +1,31 @@
#!/bin/bash
dotnet_version='net6.0'
salimax_ip="$1"
username='ie-entwicklung'
root_password='Salimax4x25'
set -e
echo -e "\n============================ Build ============================\n"
dotnet publish \
./SaliMax.csproj \
-p:PublishTrimmed=false \
-c Release \
-r linux-x64
echo -e "\n============================ Deploy ============================\n"
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")
for ip_address in "${ip_addresses[@]}"; do
rsync -v \
--exclude '*.pdb' \
./bin/Release/$dotnet_version/linux-x64/publish/* \
$username@"$ip_address":~/salimax
ssh "$username"@"$ip_address" "cd salimax && echo '$root_password' | sudo -S ./restart"
echo "Deployed and ran commands on $ip_address"
done

View File

@ -0,0 +1,23 @@
#!/bin/bash
dotnet_version='net6.0'
salimax_ip="$1"
username='ie-entwicklung'
set -e
echo -e "\n============================ Build ============================\n"
dotnet publish \
./SaliMax.csproj \
-p:PublishTrimmed=false \
-c Release \
-r linux-x64
echo -e "\n============================ Deploy ============================\n"
rsync -v \
--exclude '*.pdb' \
./bin/Release/$dotnet_version/linux-x64/publish/* \
$username@"$salimax_ip":~/salimax

View File

@ -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:])

View File

@ -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:])