Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
8646790824
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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"
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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:])
|
|
@ -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:])
|
Loading…
Reference in New Issue