Tag Archives: Pylontech

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