Table of Contents

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:

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.

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:

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

BMS_Configuration_LP_2_1.ecf
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:

pc_bms_modbus_mqtt.py
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)

Receiving MQTT messages on Watson IoT platform

Received messages:

Raw status data available:

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
compose.yaml
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.

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 </code>

C++/Arduino implementation on espressiv ESP32 DevKit v4

TX_Pin = 25 (orange), RX_Pin = 26 (gelb)

esp_uart_bms.ino
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
UART_BMS_Python.py
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