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/* \
|
./bin/Release/$dotnet_version/linux-x64/publish/* \
|
||||||
$username@"$salimax_ip":~/salimax
|
$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