Imagine you're trying to charge your new EV while out and about in your city- perhaps while you browse library books, or get a workout in at the community center. Perhaps you don't have charging at home and depend on public charging to make your green commute work. You arrive, plug in, and disaster- the fault light turns on or worse, the charger isn't powered on at all! This is reality for many EV drivers, and if you are already an EV driver you have surely been disappointed when arriving at a charger the latest PlugShare reports said was working only to discover that it is not. With over 26 million EVs expected to be on the road in the United States by 2030, this is a problem that will only get worse, especially as public infrastructure expands and demand for this public infrastructure increases.
Worse, many EV chargers like the one above aren't networked! If there isn't someone who's job it is to maintain these chargers, they'll rapidly stop working.
Fortunately, as many EV drivers know, often a charger will work again after a simple reset. Yes "have you turned it off and on again" isn't just for IT but automotive infrastructure, too!
What if we could make any EV charger smart? That's where Volt Valet comes in.
Using the AVR-IoT Cellular Mini we can make any EV charger smart. The key is making a sensor that reads signals from any EV charger- this means we need to make it understand signals from J1772 chargers, a spec that works across thousands of chargers and will make our solution universal for making smart predictive maintenance of chargers a reality. Each of these chargers emits signals to a car that indicates that it is ready to charge and how much power it can put out. It also indicates fault states. If we can devise a way to plug the charger into the AVR-IoT Cellular Mini, we can send signals to a city employee when something anomalous occurs!
Volt Valet, as you will learn below, accomplishes this task. It is able to:
- Interface with any J1772 EV charger by providing an inlet that any J1772 charging cable can plug into. This has an added benefit that it gives users a place to store the cable when done charging, rather than on the ground as is the case with many public charging stations.
- Read what's called the J1772 pilot signal, interpret it, and classify if an anomaly has occurred using machine learning
- Send an email to a city employee indicating the problem and potential required maintenance using Amazon's cloud over its cellular connection
It accomplishes this with a custom FeatherWing that uses a few simple components, a cloud stack that can be deployed with a single click, and just two easy to understand Arduino sketches. Ready to make your EV charger smart? Read on!
Prepare your workspace- you will need a soldering iron, your components (listed above), some wire strippers, and a plug nearby that can handle your chosen EV charger (I used the portable EV charger that came with my car, it can plug into any standard electrical outlet). Your SAE J1772 Inlet/Socket with cable does not yet need to be plugged into your EV charger- wait until later after we have described what each wire of the harness does before doing this for safety! The oscilloscope is optional, but it is useful for understanding how the J1772 FeatherWing sensor we will assemble below works!
Before we can assemble the FeatherWing, we must first assemble our development board. Remove the AVR-IoT Cellular Mini from its packaging. Find your short Feather male headers. Let's solder them on now, being sure to leave the longer pins facing up. You should find that they only fit into the holes of the AVR-IoT Cellular Mini one way. While the holes are arranged in a wave pattern, a little bit of force will make them fit. Your board should now look like this:
Now it is important that you take a moment and run through Microchip's excellent On-Boarding guide for the AVR-IoT Cellular Mini. The next steps will assume that the antenna has been attached, the SIM card registered and inserted into the device, and that you can reach a cellular provider from your location. Your final assembled development board should look like this:
Above is a pinout of the various pins inside your EV charger handle. These correspond, in a mirrored way, to the pins inside your J1772 inlet, or female connector. These pins also correspond with the wires coming from the back of the J1772 inlet, and should be labeled. For this guide, we will only be using the Control Pilot and Proximity Earth pins. Cut any excess copper from the other wires and wrap them with electrical tape. When the charger is connected later in this writeup, they will become energized. In my kit, a little exposed copper was left at the end of these wires. Safety first!
The first thing we will do is read the J1772 pilot signal. If you simply connect clips to your EV charger's Control Pilot (CP) pin to an oscilloscope however, you won't see anything! You actually need to "trick" the EV charger into thinking that a car has been connected. Looking at the J1772 spec, one of the easiest ways to simulate a car is with a diode in series with a 880 ohm load. We can see a similar design in this J1772 EV Charger Checker. In experience, the chargers are not very picky. I can pick two common resistor values in series to produce an adequate load, ones that you should have in a electronics kit or resistor booklet without much problem. I selected 150 and 680 ohm resistors in series- recall that resistors in series add, giving us an 830 ohm load, which is actually sufficient for our task. We will also select a common diode- 1N4148 which is also common in electronics and diodes kits. We will put these components between the CP pin and ground, giving us a circuit we can connect an oscilloscope to and study the pilot signal. Let's look at a schematic of what this circuit looks like:
I have also added a two-hole terminal block to the schematic, as ultimately we will be connecting the wire harness from your J1772 female adapter to read the pilot signal. Why two holes when so far we have only discussed CP? Looking at your wire harness you should also have a Protective Earth, or PE pin. This will provide our ground for our circuit! It will be tied to the FeatherWing and AVR-IoT ground. While it looks like a chip, the component on the right is actually your FeatherWing proto board! Let's look at how you can arrange the components on the protoboard:
I recommend you save soldering the headers on for last and just solder the above components to the protoboard for now. It makes it far easier to solder the through-hole components and trim off their leads. Use your jumper wires to connect any gaps between components, and be sure the second terminal block hole is connected to ground, as shown above. Your circuit should now look like this:
We can connect an oscilloscope probe like so, with the ground connected to our last resistor connected to the ground column and the probe connected to CP by connecting it to where our diode meets the terminal block, to begin viewing the J1772 signal:
Now, connect your EV charger to your J1772 adapter. Be careful- the line pins at the top of the J1772 connector are now hot! You will be plenty safe if you have wrapped them in electrical tape. View the circuit on your oscilloscope:
We are expecting a 1kHz +12V to -12V square wave. Excellent, we have it! But think of those voltages! They are much too high to read on the AVR-IoT board. We will need to moderate this signal, somehow.
What's a good way to limit the voltage of these square waves? What if we just trim it? In fact, using the same type of diode we used for our car simulator trickery (1N4148) above we can actually trim this signal to something the AVR-IoT board can safely read. In fact, what we are about to assemble is known as a diode clipper and is a common trimmer circuit. Let's look at the section about clipping both half cycles. Recall that we have both a positive and negative voltage component of this square wave, so we will need to trim both parts to get something reasonable for our development board. We will select a higher impedance resistor for our "R1" to limit the current to the diodes. Let's look at a schematic:
Here you can see the larger resistor, R3, is connected to a node between our two diodes. One diode points towards the 3V pin of the FeatherWIng, our Vcc. the other points towards this node. In this way, we have added a diode clipper similar to the simple one seen in the article above to our car simulator circuit. It can be easier to see how this fits together looking at the wiring diagram:
Note that we have tied our node to MOSI, or "MasterOut Slave In". This is a pin usually used for the Serial Peripheral Interface or SPI, however on the AVR-IoT Mini we can use it as a convenient input pin based on the geometry of the protoboard. In the next session, we will read the J1772 pilot signal using this pin using some simple software.
Solder this circuit together using your diodes, 22kOhm resistor, and jumper wires. You may have to join the disconnected pads of the protoboard together with solder- the pins in each column are not connected like they are on bread boards you may be familiar with. I found that this needed some patience and the need to turn my soldering iron up just a bit to get the solder more liquid.
We are done adding new components so now is also a good time to add your stacking headers. We will integrate our FeatherWing with the AVR-IoT board in the next section!
Your completed FeatherWing should look like this:
Let's check our output on the oscilloscope:
Much better! We have trimmed the voltage, without affecting the frequency or shape of the signal. We can now read this directly with the AVR-IoT Cellular Mini, which we will do with software described in the next section.
Place the completed FeatherWing onto the AVR-IoT Cellular Mini. The pins should only line up one way, where the board is fully populated. The final device will look like this:
The next thing we need to do is learn how we can read the J1772 pilot signal with our AVR-IoT Cellular Mini.
It is critical for this section that you install the Arduino IDE and then install the DxCore middleware and AVR-IoT Cellular libraries as described in Microchips' documentation. You should also configure your IDE as shown on this page. As you recall from the above schematics, we have connected our J1772 pilot signal input to MOSI, where the trimmed signal will arrive and be readable with this chosen input pin. Since we are just using this pin as input rather than for SPI, we will simply set it up as an input pin in our Arduino sketch. Looking at the pinout for the AVR-IoT Mini (page 19) we can see this corresponds to pin 19. The square wave we have been reading from the J1772 pilot signal uses what's called Pulse Width Modulation (PWM), where data is encoded in the duration of the pulse, or ON signal. The Arduino standard library gives us a convenient function for reading PWM signals called pulseIn(). It will read the duration of a pulse, high or low, on our chosen pin. Let's flash a simple program to our AVR-IoT Cellular Mini that simply prints the duration of the high signal and the duration of the low signal to the console. This simple code can be found as capture_pilot_wave_times.ino in the code repository at the bottom of this page, or can be pasted from here:
/**
* @brief This code shows how to read the J1772 pilot wave PWM waveform
*/
#include <Arduino.h>
#include <led_ctrl.h>
#include <log.h>
int pin = 19;
unsigned long durationHigh;
unsigned long durationLow;
void setup() {
LedCtrl.begin();
LedCtrl.startupCycle();
Log.begin(115200);
pinConfigure(PIN_PA4, PIN_DIR_INPUT);
}
void loop() {
durationHigh = pulseIn(PIN_PA4, HIGH);
durationLow = pulseIn(PIN_PA4, LOW);
Log.raw(String(durationHigh));
Log.raw(String(durationLow));
}
Note how we use Log instead of the more typical serial.printlin- this convenient logging function comes with the software you configured for your development software earlier, and will allow you to set different logging levels based on the severity of the message you wish to send. Here all messages are equally important, so we transmit them raw.
Let's now connect our J1772 inlet wire harness to our device. Connect CP to the terminal block input connected to our resistor/diode assembly. Connect PE to the terminal block input routed straight to ground. Press "upload" in the Arduino IDE. The sketch will be compiled and uploaded. Under "Tools" select "Serial Monitor". You should see output that looks similar to this:
You can also open the Serial Plotter under "Tools" to see that we are rapidly oscillating between a long LOW signal and a short HIGH signal:
However, you will notice that the durations HIGH and LOW aren't always the same. Additionally, we know that the pulse widths will change based on the status of the charger. For example, the charger could change the widths to indicate it can output less power than it is saying it can now. Hard coding a limit will be too inflexible for this application. We will need something more flexible.
Before we can work on a solution, we need a way to inform the user that the charger has faulted. Let's set up our cloud.
Setting up the Cloud Notification SystemWe need to receive emails whenever the charger changes states. Amazon Web Services makes this simple via their Simple Notifications Service which enables us to send email messages based on triggers. One such trigger could be our AVR-IoT Cellular Mini hitting an API endpoint set up through AWS' API Gateway. This may sound complicated, but the following diagram shows how simple this really is:
The API Gateway provides a web endpoint we can hit with our internet-connected device. It will then use a Simple Notification Service Topic, named after our chosen charger, to send an email to all our subscribers, which in this case will be you (or a city maintenance team!).
Clear as mud? Don't worry, I don't expect you to put this together by hand!
Click here to deploy the cloud notifications service to your Amazon account with zero code!
I have developed what's called a Cloudformation stack that describes the whole architecture above in human-readable YAML. You can check it out in the code repo at the bottom of this page!
After clicking the above link, you should be brought to the following screen:
Click "Next" at the bottom of the screen, since we're using the template from my link.
On the next screen, leave the Stack Name as "Volt Valet" and set the Charger Name. This can be anything you wish (such as the location of your charger). I chose City Hall. Lastly, set your email. Don't worry, it's going to be stored only in your Amazon cloud and used only for sending you emails from your deployed notification stack! Click "Next".
Scroll to the bottom of this page and click "Next".
Agree to the managed policy statement and click "Submit". Volt Valet will now take several minutes to deploy. When it is done, the "Outputs" menu will be filled in. Click "Outputs" (or refresh if it has been several minutes), and check for your ConnectURL and your TopicARN
Note these values for later, we're about to write our final software! We will paste these values in the appropriate locations of our code.
Navigate to your email. You should have received an email indicating that you have subscribed to your new SNS topic, like so (my charger in this example was called Community Center Charger):
Click "Confirm Subscription". This is an anti-spam measure that occurs with all new SNS subscriptions. Your screen will now look like this, confirming you will now receive emails from your charger:
As noted above, setting a hard PWM pulse in limit would be too inflexible. Even for the steady-state pilot signal we were reading these values varied greatly.
What we really want to know is when something anomalous occurs, that is something a measurable amount out of the ordinary.
Fortunately, there are many techniques in machine learning that allow you to write software that can detect anomalies! One of the simplest is K-means clustering.
The idea is quite simple. What if we could group our data into clusters, and quickly classify if a new data point belonged in a given cluster? That's what K-means lets us do, using a combination of clustering and a nearest-neighbors algorithm. Using nearest-neighbors, we can determine based on the points closest to a new data point what cluster it belongs in, with a given percent confidence that we are correct.
Fortunately, there is an Arduino library that implements K-means and nearest neighbors!
It is critical you install the Arduino KNN library before moving on, using this link.
Think back to our graph of the pulse durations above. We really have a simple machine learning algorithm to build- which of only two clusters does a new data point belong in? Anything that doesn't belong is an anomaly, and will trigger an email.
In fact, let's list what we will want to be notified for:
- The start time for when we started monitoring the EV charging station
- The PWM indicates that the charger is outputing an unexpected amount of power (the HIGH pulses will increase or decrease)
- The charger has been off for an anomalous amount of time (longer than it is possible for it to have been in use).
Since the charger docks with the inlet, we have no way of differentiating if the charger is being used or is off. We will set a reasonable limit- if no pulses have been seen for 8 hours, longer than is reasonable to be charging, we will assume that the charger needs to be checked in as it might be off.
Therefore our software needs to:
- Train a KNN classifier using some example data we collected above
- Determine if a new data point is outside our two clusters derived from the data above
- Notify the user if the data points fall outside the clusters OR if there have been no pulses for more than 8 hours
Let's examine the following code:
/**
* @brief KNN classifier for notifying a user about anomalous readings from their EV charger.
* server.
*/
#include <Arduino.h>
#include <http_client.h>
#include <led_ctrl.h>
#include <log.h>
#include <Arduino_KNN.h>
#include <lte.h>
#define NOTIFICATION_URL ""
int pin = 19;
unsigned long durationHigh;
unsigned long durationLow;
float threshold = 0.67;
// Create a new KNNClassifier, input values are array of floats
KNNClassifier anomKNN(1);
bool charging = false;
bool panic = false;
void setup() {
LedCtrl.begin();
LedCtrl.startupCycle();
Log.begin(115200);
pinConfigure(PIN_PA4, PIN_DIR_INPUT);
// Start LTE modem and connect to the operator
if (!Lte.begin()) {
Log.error(F("Failed to connect to operator"));
return;
}
Log.infof(F("Connected to operator: %s\r\n"), Lte.getOperator().c_str());
Log.raw("Building KNN anomaly detector for Volt Valet!");
Log.raw("\n");
Log.raw("Adding training data... ");
Log.raw("\n");
// add examples to KNN
float example1[] = { 815.0, 815.0 };
float example2[] = { 818.0, 818.0 };
float example3[] = { 820.0, 820.0 };
float example4[] = { 808.0, 808.0 };
float example5[] = { 802.0, 802.0 };
float example6[] = { 804.0, 804.0 };
float example7[] = { 803.0, 803.0 };
float example8[] = { 195.0, 195.0 };
float example9[] = { 194.0, 194.0 };
float example10[] = { 196.0, 196.0 };
float example11[] = { 198.0, 198.0 };
float example12[] = { 197.0, 197.0 };
float example13[] = { 188.0, 188.0 };
float example14[] = { 187.0, 187.0 };
float example15[] = { 191.0, 191.0 };
float example16[] = { 190.0, 190.0 };
float example17[] = { 772.0, 772.0 };
float example18[] = { 769.0, 769.0 };
float example19[] = { 770.0, 770.0 };
anomKNN.addExample(example1, 800); // add example for 800, or LOW
anomKNN.addExample(example2, 800); // add example for 800, or LOW
anomKNN.addExample(example3, 800); // add example for 800, or LOW
anomKNN.addExample(example4, 800); // add example for 800, or LOW
anomKNN.addExample(example5, 800); // add example for 800, or LOW
anomKNN.addExample(example6, 800); // add example for 800, or LOW
anomKNN.addExample(example7, 800); // add example for 800, or LOW
anomKNN.addExample(example17, 800); // add example for 800, or LOW
anomKNN.addExample(example18, 800); // add example for 800, or LOW
anomKNN.addExample(example19, 800); // add example for 800, or LOW
anomKNN.addExample(example8, 190); // add example for 190, or HIGH
anomKNN.addExample(example9, 190); // add example for 190, or HIGH
anomKNN.addExample(example10, 190); // add example for 190, or HIGH
anomKNN.addExample(example11, 190); // add example for 190, or HIGH
anomKNN.addExample(example12, 190); // add example for 190, or HIGH
anomKNN.addExample(example13, 190); // add example for 190, or HIGH
anomKNN.addExample(example14, 190); // add example for 190, or HIGH
anomKNN.addExample(example15, 190); // add example for 190, or HIGH
anomKNN.addExample(example16, 190); // add example for 190, or HIGH
// Get and print out the KNN count
Log.raw("\tanomKNN.getCount() = ");
Log.raw(String(anomKNN.getCount()));
Log.raw("\n");
if (!HttpClient.configure(NOTIFICATION_URL, 443, true)) {
Log.errorf(F("Failed to configure HTTPS for the domain %s\r\n"),NOTIFICATION_URL);
return;
}
HttpResponse response;
response = HttpClient.post("/prod/charger", "{\"message\": \"Started monitoring!\"}", "Content-Type: application/json", HttpClient.CONTENT_TYPE_APPLICATION_JSON);
Log.infof(F("POST - HTTP status code: %u, data size: %u\r\n"), response.status_code, response.data_size);
String body = HttpClient.readBody(response.data_size + 16);
if (body != "") {
Log.infof(F("Body: %s\r\n"), body.c_str());
}
Log.raw("Begin monitoring!\n");
}
void loop() {
bool lowanomaly = false;
bool highanomaly = false;
durationHigh = pulseIn(PIN_PA4, HIGH);
durationLow = pulseIn(PIN_PA4, LOW);
if (durationHigh > 0) {
charging = false;
panic = false;
float highinput[] = {1.0, durationHigh};
int classification = anomKNN.classify(highinput, 2); // classify input with K=2
float confidence = anomKNN.confidence();
if (classification != 190) {
highanomaly = true;
}
if (confidence < threshold) {
highanomaly = true;
}
}
if (durationLow > 0) {
charging = false;
panic = false;
float lowinput[] = {0.0, durationLow};
int classification = anomKNN.classify(lowinput, 2); // classify input with K=2
float confidence = anomKNN.confidence();
if (classification != 800) {
lowanomaly = true;
}
if (confidence < threshold) {
lowanomaly = true;
}
}
if (lowanomaly && highanomaly && !panic) {
panic = true;
HttpResponse response;
response = HttpClient.post("/prod/charger", "{\"message\": \"Charger has faulted!\"}", "Content-Type: application/json", HttpClient.CONTENT_TYPE_APPLICATION_JSON);
Log.infof(F("POST - HTTP status code: %u, data size: %u\r\n"),response.status_code,response.data_size);
delay(1UL * 60 * 60 * 1000); // Wait for an hour
}
if (durationHigh == 0 && durationLow == 0 && !charging) {
charging = true;
HttpResponse response;
response = HttpClient.post("/prod/charger", "{\"message\": \"Charger has begun charging\"}", "Content-Type: application/json", HttpClient.CONTENT_TYPE_APPLICATION_JSON);
Log.infof(F("POST - HTTP status code: %u, data size: %u\r\n"),
response.status_code,
response.data_size);
delay(8UL * 60 * 60 * 1000); // Wait up to 8 hours
}
if (durationHigh == 0 && durationLow == 0 && charging) {
charging = false;
// Fault condition!
HttpResponse response;
response = HttpClient.post("/prod/charger", "{\"message\": \"Charger may be off or cannot communicate with our cloud.\"}", "Content-Type: application/json", HttpClient.CONTENT_TYPE_APPLICATION_JSON);
Log.infof(F("POST - HTTP status code: %u, data size: %u\r\n"),response.status_code,response.data_size);
}
}
In summary, we have expanded the earlier code to:
- Build two clusters of HIGH and LOW data using points I chose from the earlier step where we printed the durations to the console (I recommend you use as much of your own data as possible). I set the names of the clusters as a value close to the median for each cluster. You can name the clusters whatever you wish. Note across the project we use K=2, that indicates we have two clusters.
- Connect to LTE and check if it can reach the Cloud Notification API we created earlier
- Email the user that we are starting monitoring
- Begin collecting pulses
- Check if a HIGH pulse is in the HIGH cluster
- Check if a LOW pulse is in the LOW cluster
- Notify the user if these pulses amount to anomalies (they fall outside the confidence bound at the top of the snippet which means we are 67% sure the value belongs in an existing cluster)
- Checks if there are no pulses. If so, notify the user that a charging session has begun and start waiting up to 8 hours to see if this situation persists. If so, notify the user that the charger should be checked on.
Marvelous! This simple code uses machine learning at the edge and our cloud API to send informative emails that can inform preventative maintenance of key green infrastructure!
There is one last step we have to do however before you flash this code to your AVR-IoT Mini and start monitoring your EV charger.
Provisioning the AVR-IoT Cellular Mini to Talk to AWS over HTTPSOut of the box the dev board will not talk to your new API because it uses HTTPS. As you may be aware, HTTPS encrypts web traffic. We need the certificate for our API baked into our dev board to make requests over HTTPS to our API. Our API will not respond to requests using the older, insecure HTTP. Fortunately the Provision.ino sketch provided with your development environment makes this relatively painless.
Flashing the final softwareUsing the KNN code from above, or directly using anomalydetector.ino from the repo linked here or at the bottom of the page, we can now flash the final software onto our dev board.
First, however, go back to the outputs from when you deployed the API. Copy the "ConnectURL" in its entirety. At the top of your Arduino sketch, paste it between the quotation marks for NOTIFICATION_URL.
#define NOTIFICATION_URL ""
You should now upload the sketch to your board. Turn on your charger and connect your board to a power source. You should receive an email that monitoring has begun!
You will also now start receiving anomaly emails, for example if you disconnect your charger now or begin charging your vehicle you will receive a charging email immediately and an anomaly one in 8 hours. If you can trigger a fault you will also receive an email. Happy monitoring!
SummaryVoltValet can interface with any J1772 EV charger. It can classify anomalous charger signals and determine if a charger has gone offline. It can alert a city employee for maintenance via email using a simple and secure cloud service. Volt Valet makes any EV charger smart, and can be a component that can make public EV charging more reliable as we move towards a more resilient and green future! Thanks to the AVR-IoT Mini, we can do all of this- machine learning, cloud connectivity, and signal analysis, on the edge in the field where public infrastructure already exists today.
Comments