====== BMS: ====== The battery management system is a important part of the robot. It is responsible to monitor all values regarding electrical power and available energy for the robot. It also has protection features that ensure, that the battery used lasts as long as possible to make the system safe, reliable and sustainable. Based on the measured values, the system can decide when to return to the home base for charging. Also the important metrics can be sent to a control station to let the owner know about the status of the system. This part of the project tries to implement a monitoring system for the important metrics of the power delivery system. ===== TinyBMS s516 ===== Product: [[https://www.energusps.com/shop/product/tiny-bms-s516-150a-750a-36]] The TinyBMS s516 is a very capable and small battery management board with battery protection functions. It has many additional features like temperature and speed measurement and can be activated with an ignition/switch. It also has a precharge mode for an precharge circuit to avoid sparking or big power spikes when starting. The main feature that makes this BMS a good choice is the ability to communicate with the chips via UART to access measured values. User Manual: [[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21072]] ===== Power, balance and UART cables ===== Standard connection scheme: [[https://www.energusps.com/website/image/ir.attachment/10097_bde823f/datas]] Balance wires starting highest cell at pin 16 Custom UART connection plug/cable, FTDI similar:\\ {{:projects:farmrobot:20201203_234347_2.jpg?nolink&200|}} To connect to the UART port on the BMS a FST connector is needed with female pin headers on the other side to connect to the respective pins on the microcontroller / GPIO or FTDI adapter for a connection via USB to a PC. The pins of the BMS are labled in the user manual.\\ The rx, tx and ground pins are connected between the BMS and the controller, where the tx cable from the BMS is attached to the specified rx pin of the microcontroller. {{:projects:farmrobot:img_20201214_222504.jpg?nolink&200|}} ===== UART communication ===== UART Communication protocol: [[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21208]] ===== BatteryInsider PC Application ===== This application is provided alongside the BMS and can be downloaded at the product page: [[https://www.energusps.com/web/binary/saveas?filename_field=datas_fname&field=datas&model=ir.attachment&id=21067]] Functions: * Live Data * Setting * Statistics * Cell Settings * Logging data to files * Maintenance {{:projects:farmrobot:ir_attachment_1257.png?400|}} https://www.energusps.com/website/image/ir.attachment/1257_da9a968/datas The BMS firmware has been updated to Version 245, which has been send upon request by EnergusPS. The firmware was flashed to the BMS board using the BatteryInsider Application connected via UART. ===== BMS configuration ===== Fully Charged Voltage: 4,00 Fully Discharged Voltage: 3,00 Early Balancing Threshold: 3,95 Charge Finished Current: 0,20 Battery Capacity: 30,0 Number of Series Cells: 6 Allowed Disbalance: 15 Set SOC manually, %: 95 Over-Voltage Cutoff: 4,20 Under-Voltage Cutoff: 2,80 Discharge Overcurrent Cutoff: 30 Charge Overcurrent Cutoff: 20 Over-Heat Cutoff: 40 Low Temp. Charger Disable: 0 Automatic Recovery: 1 BMS Mode: Dual Port Single Port Switch Type: Internal FET Load Switch Type: Discharge FET Load Ignition: Disabled Load Precharge: Disabled Load Precharge Duration: 0.1 s Charger Type: Generic CC/CV Charger Switch Type: Charge FET Charger Detection: Internal Pulses Per Unit: 1 Distance Unit: Kilometers Speed Sensor Input: Disabled Broadcast: Disabled Protocol: ASCII Temperature Sensor Type: Dual 10K NTC Sensor Invert External Current Sensor Direction: 0 Disable Load/Charger Switch Diagnostics: 0 ===== Monitoring BMS ===== The UART interface is the only port that can make the internally gathered data of the BMS available to other devices.\\ But the monitoring device that is communicating with the BMS board is variable.\\ For easy and standard python implementation and further appliances, a Raspberry Pi 4B can be used, as it has GPIO and USB interfaces and can run the standard python scripts.\\ It could run as the standard network interface, making internet connections available to other systems or can host multiple appliances like camera live streaming, mqtt client and commander for the robot system which can send commands from remote to a device of the robot.\\ For more embedded and efficient devices, an Arduino or ESP32 can be used. The ESP32 can run micropython code, which is a reduced feature set of python, which can also be extended with libaries, speciffic to micropython.\\ This is an additional hurdle, as the libaries are not identical to the standard python featureset.\\ For simplicity the appliances are developed on one plattform, the Raspberry Pi 4B. ===== Modbus Python and Watson-IoT MQTT publish on Raspberry Pi 4B ===== Using FTDI USB adapter which can be interfaced on /dev/ttyUSB0\\ Adding the current user to the "dialout" user group to access serial interfaces without root permissions: sudo adduser pi dialout Installing prerequisites:\\ sudo apt install python3 wget https://bootstrap.pypa.io/get-pip.py sudo python3 get-pip.py sudo pip install pymodbus\\ ModbusSerialClient is the Modbus client that is used to interface the registers on the BMS: from pymodbus.client.sync import ModbusSerialClient === IBM Watson IoT platform === Creating a new device and gathering credentials: https://q74k3e.internetofthings.ibmcloud.com/dashboard/devices/browse/ Organization ID: q74k3e\\ Device Type: RaspberryPi4B\\ Device ID: farmrobot-raspi-4b-xi-lab-ip-15\\ Authentication Method: use-token-auth\\ Authentication Token: 5GNOxfx9&mUo7*?8fP\\ Watson IoT Python SDK documentation: https://ibm-watson-iot.github.io/iot-python/device/ pip install wiotp-sdk Modbus communication implementation based on: https://github.com/clarkni5/tinybms/blob/master/python/tinybms.py\\ === Code: === import numpy as np import wiotp.sdk.device from time import sleep from datetime import datetime import pymodbus.client.sync dev_port = '/dev/ttyUSB0' modbus_client = pymodbus.client.sync.ModbusSerialClient(method='rtu', port=dev_port, baudrate=115200, parity='N', bytesize=8, stopbits=1, timeout=2, strict=False) my_config = wiotp.sdk.device.parseConfigFile("device.yaml") mqtt_client = wiotp.sdk.device.DeviceClient(config=my_config, logHandlers=None) def connect_modbus(): if not modbus_client.is_socket_open(): modbus_client.connect() print("connect_modbus: ok") def connect_mqtt(): mqtt_client.connect() print("connect_mqtt: ok") def convert(array, da_type): return np.array(array, dtype=np.uint16).view(dtype=da_type)[0] def read_registers(address, count): while True: result = modbus_client.read_holding_registers(address, count, unit=0xAA) if not result.isError(): register = result.registers return register def ask_registers(): read_registers(0, 1) lifetime_counter = (convert(read_registers(32, 2), np.uint32))/60 # min time_left = (convert(read_registers(34, 2), np.uint32))/60 # min pack_voltage = convert(read_registers(36, 2), np.float32) # V pack_current = convert(read_registers(38, 2), np.float32) # C min_cell = (read_registers(40, 1)[0])/1000 # V max_cell = (read_registers(41, 1)[0])/1000 # V cell_diff = (read_registers(104, 1)[0])/10000 # V soc = (convert(read_registers(46, 2), np.uint32))/1000000 # % bms_temperature = (read_registers(48, 1)[0])/10 # °C bms_online = hex(read_registers(50, 1)[0]) max_discharge_current = (read_registers(102, 1)[0])/1000 # A max_charge_current = (read_registers(103, 1)[0])/1000 # A charge_count = read_registers(111, 1)[0] f_data = { 'lifetime_counter': "%.2f" % lifetime_counter, 'time_left': "%.2f" % time_left, 'pack_voltage': "%.2f" % pack_voltage, 'pack_current': "%.2f" % pack_current, 'min_cell': "%.2f" % min_cell, 'max_cell': "%.2f" % max_cell, 'cell_diff': "%.2f" % cell_diff, 'soc': "%.2f" % soc, 'bms_temperature': "%.1f" % bms_temperature, 'bms_online': str(bms_online), 'max_discharge_current': "%.2f" % max_discharge_current, 'max_charge_current': "%.2f" % max_charge_current, 'charge_count': str(charge_count) } print(f_data) print("stored register values: ok") return f_data def publish(p_data): mqtt_client.publishEvent(eventId="status", msgFormat="json", data=p_data, qos=0, onPublish=print("publish: ok")) def output_time(): now = datetime.now() current_time = now.strftime("%H:%M:%S") print("-------------------------------------------------------------------") print(current_time) print("-------------------------------------------------------------------") while True: output_time() connect_modbus() connect_mqtt() my_data = ask_registers() publish(my_data) sleep(10) {{:projects:farmrobot:run_terminal.png|}} ==== Receiving MQTT messages on Watson IoT platform ==== Received messages:\\ {{:projects:farmrobot:watson_iot_received_messages.png?600|}}\\ Raw status data available:\\ {{:projects:farmrobot:watson_iot_raw_data.png?600|}} ===== Web Interface to view and graph the data ===== An easy way to set up a web interface is to host a node-red instance on a server, for example on a stationary Raspberry Pi 4, which can be accesses via network or can be made accessible with port forwarding from the internet.\\ Setting up a Raspberry Pi 4B with docker and docker run portainer. Create a new Node-Red Stack with a compose file, which creates a node-red web instance on the device on port 1880. The ip adress is needed which can be requested with: ifconfig version: "2" services: node-red: image: nodered/node-red:latest environment: - TZ=Europe/Berlin ports: - "1880:1880" networks: - node-red-net volumes: - ~/data/node-red:/data networks: node-red-net: The node-red webapp can then be accessed via [[http://ip-adress:1880]]\\ The Node-RED Dashboard module is needed to display the data with node-red\\ To install, click the Menu Button and choose "Manage palette". Click the "Install" tab and search for "node-red-dashboard".\\ Then click on install and install in the pop-up window. Then return to the main view.\\ {{:projects:farmrobot:node-red-palette.png?600|}} ===== Micropython implementation on espressiv ESP32 DevKitc v4 ===== Required software: Linux OS and python3 (sudo apt-get install python3) Powering Esp32 via USB of computer Downloading micropython firmware for microcontroller: https://micropython.org/download/esp32/ The modules that have an USB/UART chip like the espressiv ESP32 DevKitc v4 can be flashed and communicated with via the USB port. Installing esptool for flashing to board pip install esptool Erasing existing firmware of microcontroller esptool.py --port /dev/ttyUSB0 erase_flash Flashing new micropython firmware esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 esp32-idf4-20210202-v1.14.bin All available commands of esptool can be seen when calling esptool.py -h The REPL (Python prompt) is available on UART0 (which is connected to a USB-serial convertor, depending on the board) The baudrate is 115200. picocom is a client to connect to a serial device. sudo apt-get install picocom picocom /dev/ttyUSB0 -b 115200 -f n -y n -d 8 -p 1 Connect to a WLAN network with internet access (smartphone hotspot) via the REPL and import and install upip (micro-pip / python package installer) and install umqtt packages.\\ >>> import network >>> wlan = network.WLAN(network.STA_IF) >>> wlan.active(True) True >>> wlan.connect('Jonas','test1234') >>> import upip >>> upip.install('micropython-umqtt.robust') Installing to: /lib/ Warning: micropython.org SSL certificate is not validated Installing micropython-umqtt.robust 1.0.1 from https://micropython.org/pi/umqtt.robust/umqtt.robust-1.0.1.tar.gz >>> upip.install('micropython-umqtt.simple') Installing to: /lib/ Installing micropython-umqtt.simple 1.3.4 from https://micropython.org/pi/umqtt.simple/umqtt.simple-1.3.4.tar.gz from umqtt.robust import MQTTClient IBM IoT cloud is an alternative also using mqtt, but there is a seperate client available.\\ 1. Register with IBM Cloud\\ 2. Create a Internet of Things Platform instance in Frankfurt using the free "lite" plan\\ Lite plan of IBM Watson IoT platform: Includes up to 500 registered devices, and a maximum of 200 MB of each data metric Maximum of 500 registered devices Maximum of 500 application bindings Maximum of 200 MB of each of data exchanged, data analyzed and edge data analyzed 3. Click “Create”\\ 4. Click "Launch"\\ IBM IoT mqtt client: https://github.com/boneskull/micropython-watson-iot \\ import upip upip.install('micropython-watson-iot') https://q74k3e.internetofthings.ibmcloud.com/dashboard/devices/browse \\ "Add Device":\\ Organization ID: q74k3e\\ Device Type: ESP32\\ Device ID: farmrobot-bms-2021-47475\\ Authentication Method: use-token-auth\\ Authentication Token: ZrpD@cxveaiF?-WxE\\ Example: from watson_iot import Device d = Device(device_id='boneskull-esp32-test') d.connect() d.publishEvent('temperature', {'degrees': 68.5, 'unit': 'F'}) === Connecting iot module === from watson_iot import Device my_device = Device( device_id='my-device-id', # required device_type='my-device-type', # required token='my-device-token', # required # optional parameters shown with defaults below org='quickstart', username='use-token-auth', port=8883, # this is 1883 if default `org` used clean_session=True, domain='internetofthings.ibmcloud.com', ssl_params=None, log_level='info' ) my_device.connect() === Publishing an Event === Assuming the Device is connected, this example will publish a single event with ID my_event_id. my_device.publishEvent( 'my_event_id', # event name {'ok': True}, # message payload message_format='json', # 'text' is also built-in qos=0 # QoS 0 or QoS 1 ) === Exploring files of esp32 === sudo pip3 install adafruit-ampy ampy -p /dev/ttyUSB0 -b 115200 Pushing a file: ampy -p /dev/ttyUSB0 -b 115200 put file(path) Listing all files and directories in flash: ampy -p /dev/ttyUSB0 -b 115200 ls /boot.py /main.py /lib Removing a file to replace it: ampy -p /dev/ttyUSB0 -b 115200 rm main.py ampy -p /dev/ttyUSB0 -b 115200 put /home/jonas/Documents/GitHub/farmrobot_jonas/bms/main.py ===== C++/Arduino implementation on espressiv ESP32 DevKit v4 ===== TX_Pin = 25 (orange), RX_Pin = 26 (gelb)\\ HardwareSerial BMS(1); //defining "BMS" as HardwareSerial on UART 1 byte rx_data[57]; byte bms_registers_msg[] = {0xAA, 0x09, 0x1A, 0x24, 0x00, 0x25, 0x00, 0x26, 0x00, 0x27, 0x00, 0x28, 0x00, 0x29, 0x00, 0x2E, 0x00, 0x2F, 0x00, 0x30, 0x00, 0x32, 0x00, 0x33, 0x00, 0x34, 0x00, 0x04, 0x01, 0xA8, 0x3F}; byte init_seq[] = {0xAA, 0x09, 0x34}; float PackVoltage = 0; float PackCurrent = 0; float SystemPower = 0; uint16_t MinCellV = 0; // (1 mV) uint32_t SOC = 0; // (0.000001 %) void setup() { Serial.begin(115200); BMS.begin(115200, SERIAL_8N1, 26, 25); //rx, tx } void loop() { askRegisters(); delay(100); } float bytesToFloat(byte byte_array[4]) { float float_var; memcpy(&float_var, byte_array, 4); return float_var; } uint16_t bytesToUint16(byte byte_array[2]) { uint16_t int_var; memcpy(&int_var, byte_array, 2); return int_var; } uint32_t bytesToUint32(byte byte_array[4]) { uint32_t int_var; memcpy(&int_var, byte_array, 4); return int_var; } void askRegisters() { byte rx[500]; int j = 0; BMS.write(bms_registers_msg, 31); delay(10); BMS.write(bms_registers_msg, 31); while (BMS.available() > 0) { //read bytes when UART buffer is not empty BMS.readBytes(rx, 500); } //finding start sequence for (int i = 0; i < 500; i++) { byte rx_current[] = {rx[i], rx[i + 1], rx[i + 2]}; if (memcmp(rx_current, init_seq, 3) == 0) { //comparing the memory content of the arrays to find the starting sequence of 0xAA, 0x09; 2 bytes long; memcmp returns 0 if it matches /* check for buffer overflow Serial.print("found: break at i= ");Serial.println(i);*/ j = i; break; } } for (int m = 0; m < 500; m++) { Serial.println(rx[m],HEX); } int PL = (int)rx[j + 2]; Serial.print("PL = "); Serial.println(PL); for (int n = 0; n < 57; n++) { rx_data[n] = (rx[j]); Serial.print(rx_data[n], HEX); Serial.print(","); j++; } Serial.print("\n"); byte PackVoltage_array[4] = {rx_data[5], rx_data[6], rx_data[9], rx_data[10]}; PackVoltage = bytesToFloat(PackVoltage_array); for (int k = 0; k < 4; k++) { Serial.println(PackVoltage_array[k],HEX); } Serial.print("Batterypack Voltage: "); Serial.print(PackVoltage); Serial.println("V"); byte PackCurrent_array[4] = {rx_data[13], rx_data[14], rx_data[17], rx_data[18]}; PackCurrent = bytesToFloat(PackCurrent_array); /* for (int k = 0; k < 4; k++) { Serial.println(PackCurrent_array[k],HEX); }*/ Serial.print("Batterypack Current: "); Serial.print(PackCurrent); Serial.println("A"); SystemPower = PackVoltage * PackCurrent; Serial.print("System Power: "); Serial.print(SystemPower); Serial.println("W"); byte MinCellV_array[2] = {rx_data[21], rx_data[22]}; MinCellV = bytesToUint16(MinCellV_array); // (1 mV) i+21, i+22 2 bytes /* for (int k = 0; k < 2; k++) { Serial.println(MinCellV_array[k],HEX); }*/ Serial.print("Minimal Cell Voltage: "); Serial.print(MinCellV / 1000); Serial.println("V"); byte SOC_array[4] = {rx_data[25], rx_data[26], rx_data[29], rx_data[30]}; SOC = bytesToUint32(SOC_array); // Resolution 0.000001 % /* for (int k = 0; k < 4; k++) { Serial.println(SOC_array[k],HEX); }*/ Serial.print("State of Charge: "); Serial.print((SOC / 1000000)); Serial.println("%"); } ===== UART Python implementation on Raspberry Pi 4B ===== www.raspberrypi.org/documentation/configuration/uart.md Pi 4 - 6 UARTS (UART1 is mini UART) UART0 is secondary, which is used to connect bluetooth interface by default first PL011 (UART0) is found as linux device under /dev/ttyAMA0 enable_uart=1 in /boot/firmware/config.txt enables PL011 (UART0) as primary interface dtoverlay=disable-bt #restoring UART0/ttyAMA0 over GPIOs 14(tx) & 15(rx), making the full UART PL011 the primary interface /dev/serial0 sudo adduser pi dialout enabling further uarts: www.raspberrypi.org/documentation/configuration/device-tree.md sudo apt install python3 wget https://bootstrap.pypa.io/get-pip.py sudo python3 get-pip.py sudo pip install pyserial backup: sudo cp /boot/firmware/cmdline.txt /boot/firmware/cmdline-bp.txt sudo nano /boot/firmware/cmdline.txt remove 'console=ttyAMA0,115200' and 'kgdboc=ttyAMA0,115200' if present MQTT ThingSpeak https://de.mathworks.com/help/thingspeak/use-raspberry-pi-board-that-runs-python-websockets-to-publish-to-a-channel.html sudo pip3 install paho-mqtt sudo pip3 install psutil import sys import os import serial import time import paho.mqtt.publish as publish import psutil import string writeAPIKey = "V13QLD2JANKYVBV1" mqttAPIKey = "R95SKCL1DYRQNF67" channelID = "1314526" mqttHost = "mqtt.thingspeak.com" mqttUsername = "JonasGessmann" tTransport = "websockets" tPort = 80 topic = "channels/" + channelID + "/publish/" + writeAPIKey ask_etl = b'\xAA\x09\x04\x22\x00\x23\x00\xF3\x1B'#ask_etl = bytes([0xAA, 0x09, 0x04, 0x22, 0x00, 0x23, 0x00, 0xF3, 0x1B]) #reg:34,35; [UINT_32] / Resolution 1 s R ask_packv = bytes([0xAA, 0x09, 0x04, 0x24, 0x00, 0x25, 0x00, 0xF0, 0x33]) #reg:36,37; [FLOAT] / Resolution 1 V R ask_packc = bytes([0xAA, 0x09, 0x04, 0x26, 0x00, 0x27, 0x00, 0xF0, 0xEB]) #reg:38,39; [FLOAT] / Resolution 1 A R #syspow [FLOAT] / Resolution 1 W R ask_mincv = bytes([0xAA, 0x09, 0x02, 0x28, 0x00, 0x60, 0x45]) #reg:40; [UINT_16] / Resolution 1 mV R ask_maxcv = bytes([0xAA, 0x09, 0x02, 0x29, 0x00, 0x81, 0xD4]) #reg:41; [UINT_16] / Resolution 1 mV R #inbalance of cells (maxcv - mincv ask_soc = bytes([0xAA, 0x09, 0x04, 0x2E, 0x00, 0x2F, 0x00, 0xF5, 0x4B]) #reg:46,47; [UINT_32] / Resolution 0.000001 % R ask_bmstemp = bytes([0xAA, 0x09, 0x02, 0x30, 0x00, 0x8A, 0x44])#reg:48; [INT_16] / Resolution 0.1 °C R ask_bmsstatus = bytes([0xAA, 0x09, 0x02, 0x32, 0x00, 0x8B, 0x24]) #reg:50 BMS Online Status [UINT_16] / 0x91-Charging, 0x92-Fully Charged, 0x93-Discharging, 0x96-Regenertion, 0x97-Idle, 0x9B-Fault R ask_nevents = bytes([0xAA, 0x11, 0xBF, 0x1C]) #Read Tiny BMS newest Events init_seq_2 = b'\xAA\x09\x04' init_seq_4 = b'\xAA\x09\x08' int etl #convert seconds to minutes? float packv float packc float syspow #SystemPower = PackVoltage * PackCurrent float mincv #MinCellV/1000 #float maxcv #float cvinbal int soc #SOC/1000000 float bmstemp #str bmsstatus #str events BMS = serial.Serial(port='/dev/ttyAMA0',baudrate=115200,bytesize=serial.EIGHTBITS,parity=serial.PARITY_NONE,stopbits=serial.STOPBITS_ONE, timeout=1,xonxoff=0,rtscts=0,dsrdtr=0,write_timeout=1,) while 1: etl = int(read_status_4(ask_etl)) payload = "etl=" + str(etl) + "&packv=" + str(packv) + "&packc=" + str(packc) + "&syspow=" + str(syspow) + "&mincv=" + str(mincv) + "&soc=" + str(soc) + "&bmstemp=" + str(bmstemp) """+ "&bmsstatus=" + str(bmsstatus""") try: publish.single(topic, payload, hostname=mqttHost, transport=tTransport, port=tPort,auth={'username':mqttUsername,'password':mqttAPIKey}) def read_status_2(ask_seq) { bms_dump = bytes([]) BMS.reset_input_buffer() BMS.reset_output_buffer() BMS.write(ask_seq) BMS.flush() BMS.write(ask_seq) BMS.flush() bms_dump = BMS.read_until(init_seq_2) msg = BMS.read(size=6) return msg } def read_status_4(ask_seq) { bms_dump = bytes([]) msg = bytes ([]) pl_len = 8 BMS.reset_input_buffer() BMS.reset_output_buffer() BMS.write(ask_seq) BMS.flush() BMS.write(ask_seq) BMS.flush() bms_dump = BMS.read_until(init_seq_4) msg = BMS.read(size=(pl_len+5)) return msg } """send ask_sequence twice receive answer sequence and store it check init Sequence (eg AA 09 04) calculate and check CRC16 MODBUS checkbit read and encode value to variable""" ===== Authors ===== * Jonas Geßmann, Environment & Energy, jonas.gessmann@protonmail.com\\