Turning a managed EV charger into stand-alone
Project – | Article by Maarten Tromp | Published | 2453 words.
This article covers the reverse engineering of the EVBox internal protocol, and re-implementing it to make the charger function stand-alone.
Results are collected on the protocol documentation page.
This article featured on Hackaday.
In this article:
To charge my new electric motorcycle at home, I bought an used EVBox wallbox charger. To my surprise it wasn't plug and play, but required a subscription to function. The charger contains a cellular modem and needs to connect to a backend. On the backend you can control and configure all kinds of things. Wonderful though that might be, I prefer my chargers stand-alone.
So what would it take to make that possible? My goal became to figure this out and open-source the solution, so everyone can use it or build upon it.
The first thing I did, was to open op the wallbox to see what's inside. It looked well built, albeit a lot bigger than really necessary. There are clearly separated sections for power, control and communications.
The enclosure has a DIN rails section for all mains components. There are circuit breakers, a power meter, low voltage supply, and a big magnetic switch. To my surprise there's no residual-current device. However, it came with an additional circuit breaker and residual-current device for on the breaker panel, so it's all up to spec.
Mounted to the front of the enclosure is the charger module. It's entirely potted, and only connectors and the tops of some bigger parts are visible. It looks like all through-hole components of premium brands. The module looks like it's built to last.
One side of the module has an integrated RFID reader for scanning charge cards. In another corner a header is visible, which I assume is for factory programming. The piezo beeper is really loud and annoying, so I filled it with hot glue.
The potting material looks blueish, is not a thermoplastic and not soluble in anything I have at home.
Not all connectors were used, so I assume the charger module can be used in multiple configurations.
Without the communications module connected, the charger did not function. It kept blinking its led ring red.
In EVBox documentation the charger module is called ChargeBox.
Plugged into the charger module is a communications module. This board is not potted. It looks like a multi-layer PCB, well laid out, nearly all surface mount components, again using premium brands. It mainly contains a SIM5320E cellular modem and a PIC32MX675F512H-80I/PT microcontroller. This would account for the networked part of the charger.
Close to the processor is, again, a 6-pin header, which I assume is for factory programming. Close to the cellular modem is a 3-pin header, with might be serial.
According to the EVBox website, several different versions of their chargers exist. Some are stand-alone, some with WiFi and Bluetooth, others with GSM/GPRS/UMTS modems. Those might share the same charger module, but use different communications modules.
There are no thermal reliefs on the board, which makes it difficult to solder to power pins.
In EVBox documentation the communications module is called ChargePoint (CP).
Searching for documentation
Next I spent a good while searching for documentation on this charger, or any other EVBox charger. Unfortunately I was unable to find anything other than marketing material.
The presumed programming headers looked inviting. On both modules those contained VCC, GND, a pin pulled-up with 10kΩ to VCC which could be VPP, two pins with 3MΩ to VCC and GND which could be clock and data, and one unconnected pin. This matches the connections for 2-wire in-circuit programming a PIC microcontroller.
So I connected the headers to Raspberry Pi GPIO pins and used pickle to try to read out the PIC. The PIC on the communications module turned out to be protected against readout, and the PIC on the charger module wouldn't respond at all. This is my first time working with PIC, so I might be making some beginner mistakes here, but I was stuck. My goal was to get the charger up and running, not diving into the rabbit hole of PIC programming and glitching.
So on to plan B; observing the communications between charger and communications modules. Having a closer look at the cellular modem might be interesting as well, but if I could take over communications between modules, I could take out the communications module entirely.
I have tried to match my findings with the OSI model. The following section summarizes the process, and the full results are collected on the protocol documentation page.
The modules connect with 4 pin headers. A poke with the scope revealed that those contain +12V, GND, and a differential serial bus with 38400bps baud rate. On the communications module, traces from the connector lead to an ADM3485 RS-485 transceiver, which lead to serial port on the microcontroller. I assume a similar configuration on the charger module.
To have a look at the data I took a USB to serial converter, added a RS-485 transceiver, connected it to a Raspberry Pi running Python with pySerial, and started writing some software.
Data came in bursts, always starting with 0x02
and ending with 0x03FF
. Those were obviously frames. The frames contain payload, one byte checksum, and one byte parity. Frames with invalid checksum or parity are ignored, which prevents from corrupt data. There are no sequence numbers or payload length present in the frame.
All data is sent as ASCII encoded strings of capital letters and numbers. To send integer values, which make up most of the data, those are hex encoded. This takes up double the space, but keeps the values within a predictable range, so they don't interfere with frame start and end markers. There is no validation against invalid data. Acknowledgement happens on application layer. The ASCII-decoded frame payload is referred to as packet.
The first two bytes of a packet are destination and source address. The communications module is always at fixed address 0x80
, while the charger is at a dynamic address. Broadcasts use address 0xBC
, which took me a moment to realize it's an acronym.
The next packet byte is command, and the rest is a variable number of parameter bytes. Each command has a fixed number of parameter bytes, with the actual number differing between commands. Parameter length is not present in the packet.
Parameters are mostly hex-encoded numbers, but sometimes string. This is also fixed per command. Parameters are padded with zero. There is no validation on the number of parameter bytes or their contents.
Most requests require a response. Request and response have the same command number, with (usually) a different number of parameters. So to identify individual messages, you have to know the protocol.
It turned out all the decision making is done in the communications module. The charger module simply does what it's told, and occasionally sends status updates.
In EVBox documentation parameters are called data, and commands are sometimes called actions.
With the reversing of the protocol mostly done, I started looking into Home Assistant integration. To my surprise I found several projects that interfaced with EVBox chargers, which I had missed on my earlier search.
One of the interesting things I found was EVBox documentation for a 3rd party interface. It described exactly the protocol I had been working on. It confirmed my findings and cleared up some remaining questions about parameter length and padding. Who would have known there was already documentation available of this?
Another useful resource was this blog post from Harm Otten. He had also reversed and documented the protocol, over a decade ago.
And lastly I found out that several EVBox chargers have a stand-alone option. For some it's a different communications module, for others it's configurable form the portal. So, again, no award for originality.
Now that I've observed communications, and partially understand the workings, it's time to start creating my own implementation. Short term goal is to replace the communications module, with a Raspberry Pi running my code. Once this works, I planned to port it to an ESP32, design a PCB, and add Home Assistant integration. The code is written in Python, so it can easily be ported to MicroPython for ESP32. Let's see if we can fool the charger into talking to us.
At first, every message I sent was ignored. Looking at the bus with the scope, it turned out that no data was sent at all. The RS-485 transmitter had to be enabled during sending. So I wired transmitter enable to RTS on the USB to serial converter, added configuration to pySerial, and tried again. Now some transmitted data was visible on the bus, but only the first few milliseconds of it. After tweaking some pySerial delays I was able to send a simple message. Later the problem returned for longer messages. I tweaked the delays again, but that broke the sending of short messages. Luckily then the USB to serial converter with built-in RS-485 transceiver I had ordered at the beginning of this project arrived, solving the problem once and for all.
My implementation acts as a simple replay attack. For every incoming message, it sends a predetermined response. Both code and documentation are a lot more thorough though, replay is only the first step.
This worked perfectly, so the Raspberry Pi is installed inside the wallbox, and connects to the charger where the communications module used to be. This will keep the charger working until the PCB and ESP32 are ready.
If you want to build your own stand-alone charger, this is probably where you should stop doing what I did. Some of the experiments below caused me bricking my charger module.
I had noticed not all messages need to be replayed for the charger to function. For instance, during charger initialization, a number of messages are sent for reading and writing settings, and exchanging status updates. Leaving all of those out didn't seem to negatively impact the charger.
After accidentally sending a shortened message, the charger responded normally. The same happened when I accidentally had set some parameter to zero. It seems there is only limited validation going on in the charger firmware. So I started testing what more can be left out. For instance, a typical response to a charge card authentication request would be 010E [card number] FFFF
. It turned out only the first 4 bytes need to be sent, of which only the first 2 are used, so sending 0100
has the same effect.
Sending a response, without the charger sending a request first, resulted in the charger sending the message that would typically follow on a successful request-response exchange. In other words, the charger didn't care that the first request was missing. Also, all requests are accepted by the charger regardless of charging state. So a stop charging request was accepted, even though the system was not currently charging. It seems the charger firmware doesn't care about which state it's in. However, the address negotiation after boot is properly stateful and validates incoming messages.
Inspired by the previous results, I decided to poke around some more. There is a single message byte reserved for command, giving 256 possible commands, of which only 14 were observed so far. So I decided to scan the entire command space, by sending a simple message with no parameters, observing return messages. This was made easy by the total lack of validation in the charger firmware. The scan yielded some interesting results. Some messages got responses with many parameter, some other messages triggered the sending of a new request by the charger, some messages got multiple responses, and some even triggered messages sent to different addresses.
But by now some odd things had started to happen. Sometimes, between valid packets, some stray bytes were sent by the charger. And sometimes also half packets. The fact that there is no outgoing message validation in the charger firmware didn't come as a surprise. Even commands that had proved stable in the past, such as reading configuration data, returned corrupted packets now. A charger reboot only made things worse. It turned out the charger couldn't negotiate an address any more. Instead of its serial number, it now sent different bytes, including an end of frame marker. A response, including its new "serial number" is not accepted by the charger, since it does not pass message validation. I suspect scanning the command space might have hit on a command for permanent configuration.
Backtracking, I noticed the new serial number is 4 numbers, an end of frame marker, followed by all zeroes. Since none of the requests carried any parameter, this could be the checksum, parity and end of frame marker from the request, padded with 0. The numbers match the command 42 request, which also happens to echo the (new) serial number. Unfortunately there is no way to change the serial number back, since the charger doesn't accept any other messages until after address negotiation. And I'm not even sure the serial number is the only thing that got accidentally overwritten.
With the charger unusable, I started scanning the command space on the communications module. No surprises there, but I have yet to brick it.
The contrast in robustness between the firmware and everything else surprised me. The electronics and mechanics of the charger are well thought-through. In contrast, the firmware only seems to care about the happy path. I understand this communications protocol was never intended for 3rd party access, but I find your lack of validation disturbing.
Obviously all findings have been added to the protocol documentation.
Everything in this project is done using free and open source software and hardware where possible.
All software is written using Vim, and runs on Python, on a Raspberry Pi. PIC reading is done with pickle.
In turn, the software, article, and documentation for this project are released into the public domain. You can find relevant files in the downloads directory of this article.
Exception to this is anything I didn't create myself, since it's not mine to give away. This includes the EVBox protocol, which obviously remain subject to the original licenses and copyrights.
At the start of this project I just wanted an EV charger. Unintentionally this turned into a reverse engineering project. Now I have an expensive paperweight, and am again in need of a charger. I did enjoy reversing the protocol, or at least until it bricked the charger. I hope the resulting documentation is of some use for the next person working on an EVBox charger.
For me the next step will be creating a new charger board to go inside the now useless wallbox. If anyone else would like to have a stab at the charger board, let me know and I'll ship it to you.