Tag Archives: CAN

Pylontech Battery CAN comms – V1.2 vs V1.3

In the past few years, the CAN communication for Pylontech batteries have changed. Below are CAN outputs from some US5000s. These two different Pylontech batteries are connected together on the DC output, but as you can see, there are some slightly different numbers – the ‘old’ ones are reporting a different SOC, SOH etc:

Old Pylontechs:
ID: 351 Buffer: 14 2 C0 12 C0 12 C2 1   - Battery voltage + current limits
ID: 355 Buffer: 58 0 5E 0               - State of Health / State of Charge
ID: 356 Buffer: 7A 13 F2 FE 6B 1        - Voltage / Current / Temp
ID: 359 Buffer: 0 0 0 0 6 50 4E         - Protection & Alarms
ID: 35C Buffer: C0 0                    - Battery charge requests
ID: 35E Buffer: 50 59 4C 4F 4E 20 20 20 - "PYLON  "

New Pylontechs:
ID: 351 Buffer: 10 2 C0 12 C0 12 C2 1   - Battery voltage + current limits
ID: 355 Buffer: 54 0 60 0 0 0           - State of Health / State of Charge
ID: 356 Buffer: 7B 13 E9 FE 6B 1        - Voltage / Current / Temp
ID: 35A Buffer: 0 0 0 0 0 0 0 0         - NEW
ID: 35E Buffer: 50 59 4C 4F 4E 20 20 20 - "PYLON  "
ID: 35F Buffer: 4C 49 1 3 3C 2 AA 0     - NEW

We still have six CAN messages, but the ID 0x359 (Protection & Alarms) and 0x35C (charge/discharge requests) have gone, replaced by IDs 0x35A and 0x35F. Additionally, 0x355 has changed from 4 bytes to six. A bit of an explanation is on this table I found, produced by Open Solar Energy:

CAN communication IDs and descriptions for Pylontech, SMA and Victron.

As you can see, 0x359 and 0x35C were unique to Pylon – not used by SMA and Victron. This would be a good reason to drop them – to bring things in line with other manufacturer’s protocols. 0x35A and 0x35F are both used by SMA and Victron.

So –

  • 0x35A is ‘Alarms, Warnings’, replacing 0x359.
  • 0x35F is ‘Battery type, BMS info’, replacing 0x35C.

Now, let’s go through some code to translate from the CAN message. Here I am taking a CAN message already held in a buffer, hence the buf_pos reference. First 0x351..

unsigned int charge_v;
signed int charge_a;
signed int discharge_a;

charge_v = ((int)can_buffer[buf_pos][1] << 8) + ((int)can_buffer[buf_pos][0]);
charge_a = ((int)can_buffer[buf_pos][3] << 8) + ((int)can_buffer[buf_pos][2]);
if (charge_a > 0x7FFF) batt.charge_a = charge_a - 0x10000;
discharge_a = ((int)can_buffer[buf_pos][5] << 8) + ((int)can_buffer[buf_pos][4]);
if (discharge_a > 0x7FFF) discharge_a = discharge_a - 0x10000;

Note that 0x351 has 8 bytes not 6 – the Pylontech 1.2 protocol document gives no clues as to what the last two bytes are, however, if it is unsigned, then C2 01 gives 450 – or 45.0 x10 – looks like a low voltage threshold. SMA documentation confirms this as ‘Battery discharge voltage’ being the lower voltage limit allowed during discharge. So, we can add:

unsigned int discharge_v;
discharge_v = ((int)can_buffer[buf_pos][7] << 8) + ((int)can_buffer[buf_pos][6]);

Now 0x355..

unsigned int soh;
unsigned int soc;

soc = ((int)can_buffer[buf_pos][1] << 8) + ((int)can_buffer[buf_pos][0]);
soh = ((int)can_buffer[buf_pos][3] << 8) + ((int)can_buffer[buf_pos][2]);

Now 0x356..

signed int volts;
signed int current;
signed int temp;
        
volts = ((int)can_buffer[buf_pos][1] << 8) + ((int)can_buffer[buf_pos][0]);
if (volts > 0x7FFF) volts = volts - 0x10000;

current = ((int)can_buffer[buf_pos][3] << 8) + ((int)can_buffer[buf_pos][2]);
if (current > 0x7FFF) current = current - 0x10000;

temp = ((int)can_buffer[buf_pos][5] << 8) + ((int)can_buffer[buf_pos][4]);
if (temp > 0x7FFF) temp = temp - 0x10000;

Now 0x359 – Protection / Alarm Flags. Ok, I am being lazy here and not separating them out. But then this is dropped for V1.3..

byte pa_flags[4];

pa_flags[0] = can_buffer[buf_pos][0];
pa_flags[1] = can_buffer[buf_pos][1];
pa_flags[2] = can_buffer[buf_pos][2];
pa_flags[3] = can_buffer[buf_pos][3];

// Currently 14 flags:
// Protocol version 1.2 2018/04/08
//
// byte 0 : Protection
//  bit 7: Discharge overcurrent    PDO
//  bit 4: Cell undertemp           PUT
//  bit 3: Cell overtemp            POT
//  bit 2: Cell/module undervolt    PUV
//  bit 1: Cell/module overvolt     POV
// byte 1 : Protection
//  bit 3: System error             PSE
//  bit 0: Charge overcurrent       PCO
// byte 2 : Alarm
//  bit 7: Discharge highcurrent    ADH
//  bit 4: Cell lowtemp             ALT
//  bit 3: Cell hightemp            AHT
//  bit 2: Cell/module lowvolt      ALV
//  bit 1: Cell/module highvolt     AHV
// btye 3 : Alarm
//  bit 3: Internal comms fail      ACF
//  bit 0: Charge highcurrent       ACH
// byte 4: Module numbers - 8 bits unsigned - what is this? (ignoring)

0x35A – New for V1.3 – Alarms. Fortunately, SMA publish this.

byte pa_flags[4];

pa_flags[0] = can_buffer[buf_pos][0];
pa_flags[1] = can_buffer[buf_pos][1];
pa_flags[2] = can_buffer[buf_pos][2];
pa_flags[3] = can_buffer[buf_pos][3];

// byte 0
//  bit 7 - high temp alarm clears
//  bit 6 - high temp alarm
//  bit 5 - low voltage alarm clears
//  bit 4 - low voltage alarm
//  bit 3 - high voltage alarm clears
//  bit 2 - high voltage alarm
//  bit 1 - general alarm clears
//  bit 0 - general alarm

// byte 1
//  bit 7 - high current alarm clears
//  bit 6 - high current alarm
//  bit 5 - low temp charge alarm clears
//  bit 4 - low temp charge alarm
//  bit 3 - high temp charge alarm clears
//  bit 2 - high temp charge alarm
//  bit 1 - low temp alarm clears
//  bit 0 - low temp alarm

// byte 2
//  bit 7 - BMS internal fault clears
//  bit 6 - BMS internal fault
//  bit 5 - short circuit fault clears
//  bit 4 - short circuit fault
//  bit 3 - contactor fault clears
//  bit 2 - contactor fault
//  bit 1 - high current charge alarm clears
//  bit 0 - high current charge alarm (charge current too high)

// byte 3 - NOT FOR EXTERNAL USE
// byte 4 - SEEMS TO BE DUPLICATE OF BYTE 0
// byte 5 - SEEMS TO BE DUPLICATE OF BYTE 1
// byte 6 - SEEMS TO BE DUPLICATE OF BYTE 2
// byte 7 
//  bit 1 - cell imbalance clears
//  bit 0 - cell imbalance

0x35C – V1.2 Battery charge requests / charge, discharge enable.

byte req_flags;
bool charge_enable = false;
bool discharge_enable = false;
bool request_force_charge1 = false;
bool request_force_charge2 = false;
bool request_full_charge = false;
bool force_bypass = false;
req_flags = can_buffer[buf_pos][0];

// Request flags (0x35C)
//  byte 0: Request flags
//   bit 3: Request full charge     RFC
//   bit 4: Request force charge 2  RF2
//   bit 5: Request force charge 1  RF1
//   bit 6: Discharge enable        RDE
//   bit 7: Charge enable           RCE

request_full_charge = (can_buffer[buf_pos][0] & 0x08);
request_force_charge2 = (can_buffer[buf_pos][0] & 0x10);
request_force_charge1 = (can_buffer[buf_pos][0] & 0x20);
discharge_enable = (can_buffer[buf_pos][0] & 0x40);
charge_enable = (can_buffer[buf_pos][0] & 0x80);

0x35F – V1.3 Battery information

// 0x35F - 4C 49 01 03 3C 02 AA 00
// Byte 0,1 - Battery type
// Byte 2,3 - BMS Version
// Byte 4,5 - Battery Capacity Ah (x10)
// Byte 6,7 - (reserved) Manufacturer ID

unsigned int type;
unsigned int bms_version;
unsigned int capacity;
unsigned int manufacturer_id;

type = ((int)can_buffer[buf_pos][1] << 8) + ((int)can_buffer[buf_pos][0]);
bms_version = ((int)can_buffer[buf_pos][3] << 8) + ((int)can_buffer[buf_pos][2]);
capacity = ((int)can_buffer[buf_pos][5] << 8) + ((int)can_buffer[buf_pos][4]);
manufacturer_id = ((int)can_buffer[buf_pos][7] << 8) + ((int)can_buffer[buf_pos][6]);

Loss of 0x35C

It’s a shame that the 0x35C CAN frame is no longer used. This could provide a great way to control the charging where it is difficult to do it directly on the inverter. For example, the SMA Sunny Islands are controlled via the Sunny Home Manager, which itself is controlled via the Sunny Portal. This is not ideal where you want your home automation system to do the work. Being able to leave the Sunny Island in zero export mode for example, but then force an overnight charge via 0x35C frames would be handy.

Next job – text whether the 0x35C frame is still obeyed..

Pylontech US2000/US3000 NodeMCU CAN reader

The Pylontech batteries use either CAN or RS485 to communicate with the inverter. This post is a look at the CAN interface, and how to read that information to allow output to something like emoncms or MQTT.

Pylontech US2000 Battery
Pylontech US2000

Battery communication via the CAN interface is used by the Sofar Solar ME3000 inverter/charger in the setup we have and so the information here is based on this.

There is a Pylontech document specifying the CAN communications around on the internet although it is not that easy to find, and people connected with or in communication with Pylontech seem reluctant to share is, I think because of a NDA. It is, however, out there and if you know what to search for you can find it (ahem, CAN Bus Protocol Pylon low voltage).

The data is output by the battery (or master of a connected battery stack) every 1 second. The data rate is 500kbps and the output information is:

  • Protection Flags (eg. over/under temp)
  • Alarm Flags (eg. over current / voltage, communication fail)
  • Request Flags (eg. force charge)
  • State of Charge and State of Health
  • Voltage, current, Temperature
  • Maximum Voltage, Maximum charge current, Maximum discharge current

The information is for a complete pile (stack of batteries) basis and so there is no information on individual cells, for example.

Reading CAN

Since the CAN bus is a bus (!), multiple communicators can be connected to it. The Pylontech and the ME3000 inverter are the two end-points as they have the resistor terminators. This means another CAN device on the same must be unterminated. (So you can’t for example connect another ME3000 to it and expect it to work.) You can connect into the bus with an unterminated device and read the data without interfering with it. THIS IS GOOD because there is important battery management stuff going on between the inverter/charger and the batteries which you SHOULD NOT MEDDLE WITH. For example, The battery will force a charge from the inverter if it is at a very low state of charge, or if it thinks the state of charge is inaccurate (due to not having reached full charge for a while). Ultimately, you don’t want dead batteries, or a fire on your hands.

MCP2515_CAN Module
MCP2515_CAN Module

The MCP2515_CAN module is a CAN interface which communicates with the NodeMCU via SPI. It can also be powered from 5V or (in our case) 3.3V. J1 is for enabling the terminator resistor so we leave it unconnected. J2 is for the connection to the CAN bus.

SPI (Serial Peripheral Interface) uses four wires for communication. It is a full-duplex, master-slave interface where only one peripheral can communicate with the master at one time. However, multiple peripherals can be connected – and then a fifth wire is used to select which device to talk to – CS or ‘Chip Select’. 

In addition, an interrupt line can be used for the peripheral to signal that there is something to read from it.

The NodeMCU has two SPI interfaces (SPI and HSPI) built-in to the hardware, but one is used internally (SPI – for flash) and so we use the other – HSPI. This is specified here: https://nodemcu.readthedocs.io/en/release/modules/spi/

On the NodeMCU V0.9 board, the useable SPI interface was labelled:

NodeMCU V0.9
NodeMCU V0.9 Pinout

But on the V1.0, only the D lines are labelled:

NodeMCU V1.0 SPI Interface
NodeMCU V1.0 SPI Interface

They are both in the same place though. Here are the assignments:

SignalIO indexESP8266 pin
HSPI CLKD5GPIO14
HSPI /CSD8GPIO15
HSPI MOSID7GPIO13
HSPI MISOD6GPIO12

We are using the NodeMCU as the SPI Master device, so MOSI becomes Master Out and MISO becomes Master In.

MCP2515_CAN J4:

  • INT – interrupt
  • SCK – Serial Clock
  • SI – Slave In
  • SO – Slave Out
  • CS – Chip Select
  • GND – Ground power connection
  • VCC – + Volts connection

We connect the Master Out on the NodeMCU to Slave In and Master In to Slave out:

NodeMCU to MCP2515_CAN module
NodeMCU to MCP2515_CAN module

Powering the interface module

Now, the MCP2515_CAN requires that its power supply matches the voltage of the communicating device. With NodeMCU we are running at 3.3V and so the MCP2515_CAN can also be powered from 3.3V. 

HOWEVER, with the NodeMCU board  powered via the USB connector it is creating its own 3.3V via an on-board regulator from the 5V USB input. This is not sufficient to power the MCP2515_CAN for transmissions on the CAN bus. It is enough for just reading the bus though and so you can see we have powered the board from a 3.3V pin on the NodeMCU. (If you needed more power then a step-down module such as the AMS1117-3.3 LDO module could be connected to the 5V pin to provide the 3.3V for the MCP2515_CAN.

Connecting to the CAN bus

The CAN bus connector on the Pylontech battery is an RJ45 connector – the same as used for networking. CAN uses two wires and these are the blue pair – the centre two pins of the connector. To connect into the bus, we can use a pair of RJ45 sockets connected together with the pair connected to our CAN input:

CAN RJ45 connections
CAN RJ45 connections

All hardware is now ready! It can be tested using the coryjfowler library https://github.com/coryjfowler/MCP_CAN_lib and the example sketch CAN_receive.ino with the CS and INT data pins set to match ours:

#define CAN0_INT D1
MCP_CAN CAN0(D2);

Now look for the line: if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_16MHZ) == CAN_OK) and if you have an 8MHz crystal on your MCP2515_CAN module, change it to:

if(CAN0.begin(MCP_ANY, CAN_500KBPS, MCP_8HZ) == CAN_OK)

Connecting into to the CAN bus with an RJ45 cable from the top battery CAN port to our RJ45 socket, and then connecting the inverter comms cable to the other RJ45 puts us on the CAN bus, and so the sketch can be uploaded and run. If you have been successful, you should get some CAN data in your serial monitor like:

Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x351       DLC: 8  Data: 0x14 0x02 0xC8 0x05 0xC8 0x05 0xCC 0x01
Standard ID: 0x305       DLC: 8  Data: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Standard ID: 0x35E       DLC: 8  Data: 0x50 0x59 0x4C 0x4F 0x4E 0x20 0x20 0x20
Standard ID: 0x359       DLC: 7  Data: 0x00 0x00 0x00 0x00 0x04 0x50 0x4E

A couple of things to point out here: outputting all this junk to the serial port is causing some packets to be missed – the loop is too slow and the CAN buffer is not cleared in time to receive them all (eg. there is no 0x35C ID here) and also the CAN_receive.ino sketch puts our CAN interface into MCP_NORMAL mode – which sends ACKs (we should really be in CAN_LISTEN mode).

Next post – a sketch to decode this data and send it somewhere useful..

Update – post is here: Complete sketch for reading and posting the Pylontech CAN data to emoncms, plus replication to additional CAN busses.