My last post covered how I with a logic analyzer successfully (but not in the safest way) sniffed the SPI-transmission on the device and managed to decipher the data. And then how I used an Arduino Nano to do the sniffing for me with an interrupt driven approach. The Arduino translated the bits and bytes to power and voltage values that it continuously printed on the PC's Serial monitor via an USB-cable.
The next obvious step in this project is to get the energy meter to transmit the data wirelessly. Not only would this mean that I could grab data from several energy meters simultaneously, but it also creates a safe way of debugging the device since my previous setup (which I abandoned after reading some of the comments on Hackaday.com) meant connecting the mains neutral to the computers ground (do not try this at home!!!)
Today's post will cover how I managed to get an Arduino pro mini (3.3V) to sniff the energy meters SPI and transmit the data wirelessly with a nRF24L01+ to an Arduino Nano connected to a computer. The neat thing with this setup is that the Arduino pro mini and the nRF fits perfectly within the casing of the energy meter, and are both driven by the internal power that charges the rechargeable battery that you'll find in the meter.
The total cost of components for each energy meter is:
Energy meter £7.97
Arduino Mini 3.3 V £2.58
nRF24L01+ £0.99 (choose the black + version)
A capacitor + cables pretty much free, say £1
which comes to a total of £12.54 per unit!
Plus one computer connected node, or a raspberry pi with a nRF24L01 to receive the transmissions from all the energy meters.
Arduino and RF24.h
As mentioned in previous blog-posts I have worked quite allot with the nRF24L01 and AVR's, writing my own libraries and so on. But since this is my first Arduino-project I wanted to learn how to import and use other peoples libraries. Since I am still a newbie in programming, I always have to learn everything from scratch, and nothing ever works as supposed to when I try to follow a tutorial...
I started out by setting up two Arduino Nanos according to this guide and running the example code at the bottom of that page, and as expected - it didn't work.
Debugging
1. Do I have a working SPI-connection?
I had followed the explanation in the guide i linked to above, on how to install the RF24.h-library and it all seamed to work, I definitely had a working SPI-connection between the Arduino and the nRF since I could print out all the registers, and modifying them also worked...
Took me a while to figure out that I had to download and include the file "printf.h" and call "printf_begin();" to print out data from the nRF to the serial monitor... and why, why, why are they using the "protected" attribute for many of the functions??? For example get_status is a very useful debug-function and should not be protected!
2. Am I using the correct hardware setup?
As always I hadn't used the exact components as the instructions said, in my drawer I could only find one 47 uF 24 V and one 100 uF 16 V capacitors. Well in the guide it sais:
"Connect a .3.3 uF to 10 uF (MicroFarad) capacitor directly on the module from +3.3V to Gnd (Watch + and - !) [Some users say 10 uF or more..]"
And I read 10 uF or more....
Anyways, I thought that this should work, since I have used these capacitors before on the nRF, but not with this library and not with Arduino...
3. I found a working code
Yay, suddenly i found a working code, the "pingpair" example that comes with the NRF.h-library worked with my setup!
4. Why does only Pingpair-code work?
I started stripping the Pingpair-code of its components, and found that my setup was only working when it got into receiving mode in between every transmission... strange, but keeping in mind that I was using bigger capacitors I figured it had something to do with the capacitors not have time enough to recharge/discharge.
I think this is what happened:
When the Arduino calls the "Write" function, the library runs the "startWrite"-function which powers up the nRF and has a predefined delay of 150 us (wait for the nRF to power up) before it starts transmitting the data.
It turned out that this delay was not enough for my setup, so when I changed this to 1000 us (1 ms), the code worked like a charm even without the receiving functions in between! To do this modification, I opened the RF24.cpp-file (in the arduino Sketchbook libraries folder) with notepad, changed the delay, and saved the file.
Arduino Mini
Just as I managed to get a working Arduino code for the nRF, two Arduino Mini 3.3 V landed in my postbox, awesome timing!
Here's a pic of when I have soldered an nRF to the Arduino and uploading code with the USB to COM adapter via a breadboard (out of sight) so the colored cables does not match the pins...
Here you have the wire diagram:
I started off by just uploading a simple transmission code on the Arduino mini, that would send a dummy byte once a second to confirm a working setup. I soldered everything together, plugged the meter into an outlet and kept my fingers crossed as I watched the Arduino-Serial monitor connected to the receiving unit, and.... noting (of course!)
After several hours of debugging, (I thought I had burnt the nRF by hooking it up directly to the power-meters VCC-pin) It turned out had I accidentally uploaded a non-working code to the Arduino Mini.... FAIL!!!
I had cleaned up my working code, and shifted some parts around, it seems like you can't open the writing/reading-pipes on the nRF before you set the datarate/payloadsize/PALevel...
Anyway, the setup I found working is grabbing the power directly from the battery (3.6 V soldered to the underside of the PCB) rather than the VCC-cable (which i think is a bit flaky, and is connected to the RAW-pin on the Arduino, which has a built in 3,3 V regulator. I'm not sure this modification is necessary, but it ensures that the nRF and the Arduino gets 3.3 V. (The pictures above are not connected to RAW, but the Arduino's 3.3 V)
Here's whats transmitted to the computer connected Arduino and printed in the Serial monitor:
I have spent some time looking at ways to store and plot this data in the cloud. One interesting approach is to send the data to a website like http://emoncms.org/ and use there ready to use energy-viewing graphs. With the help of a small python-script I wrote, I can easily send the data from my raspberry pi-server to my account on there website where I have the option of making graphs.
I have also been working in the program called "Processing" which is pretty much the same as Arduino-IDE but for graphing and doing stuff with the data.
I have so far programmed a working graph that shows the data output from up to 6 wireless energy meters, it also logs the data with timestamp to a csv-file. See the processing-code at the bottom.
You might wounder why the red and green line (nr 1) sometimes differ? That's because I have the meter to transmit the average=red and the maximum=green power readings on every third power reading. This interval is easily changed by typing 1:1 or 1:4 to change the interval on meter 1 from to 1 or to 4 readings in between transmissions.
I had a hard time figuring out how to transmit this new interval from the computer nrf to the meter, because if I in between every transmission changed the nRF to a receiver, the power consumption would cause the whole meter to stop working.
This time I had to do some serious thinking, and I ended up using the possibility to alter the autoAck-payload. A function that normally sends an "OK" from the receiving nrf back to the transmitting nRF to confirm the transmission. I found out how to alter this value to send something like a "2" instead, or some other integer that would be a new interval. This was exactly what I needed, See the code at the bottom!
Her's the working Arduino code, first for the Energy Meter then for the computer connected receiver-Arduino. At the very bottom you have the processing code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Reads voltage and power from China-style energy meter and sends it to nRF24L01+. | |
Collecting data from meter by eavesdropping on the MOSI-line (master in slave out) | |
between the energy monitoring chip (ECH1560) and the main processor | |
Meter Arduino | |
GND (Brown) GND (2nd from the end of the meter-board) | |
VCC (3.6V) RAW (Connect to battery "+" under meter-board) | |
CLK (Green) D2 (INT0) (5th from the end of the meter-board) | |
SDO (Blue-white) D5 (6th from the end of the meter-board) | |
nRF24L01+ Ardiono | |
GND (square) GND | |
VCC 3,3V (mini = VCC) | |
CE 9 | |
CSV 10 | |
SCK 13 | |
MOSI 11 | |
MISO 12 | |
*/ | |
#include <SPI.h> | |
#include "nRF24L01.h" | |
#include "RF24.h" | |
#include "printf.h" | |
//NRF-setup: | |
#define CE_PIN 9 | |
#define CSN_PIN 10 | |
//Energy meter setup: | |
const int CLKPin = 2; // Pin connected to CLK (D2 & INT0) | |
const int MISOPin = 5; // Pin connected to MISO (D5) | |
// Set up nRF24L01 radio on SPI bus plus pins 9 & 10 | |
RF24 radio(CE_PIN, CSN_PIN); // Create a Radio | |
// Radio pipe addresses for 1 node to communicate. | |
//const uint64_t PipeAddress = 0xAABBCCDD11LL; //5-byte address "LL" is for long | |
const uint64_t pipes[] = { 0xAABBCCDD11LL}; //Transmitting address (Last 2 numbers are number on energy-meter) | |
//EnergyMeter-variables | |
//All variables that are changed in the interrupt function must be volatile to make sure changes are saved. | |
volatile int Ba = 0; //Store MISO-byte 1 | |
volatile int Bb = 0; //Store MISO-byte 2 | |
volatile int Bc = 0; //Store MISO-byte 2 | |
float U = 0; //voltage | |
float P = 0; //power | |
float ReadData[3] = {0, 0, 0}; //Array to hold mean values of [U,I,P] | |
int AverageMax = 5; | |
volatile long CountBits = 0; //Count read bits | |
volatile int Antal = 0; //number of read values (to calculate mean) | |
volatile long ClkHighCount = 0; //Number of CLK-highs (find start of a Byte) | |
volatile boolean inSync = false; //as long as we ar in SPI-sync | |
volatile boolean NextBit = true; //A new bit is detected | |
void SendToComputer(void); //initiate transmitt-function | |
void setup(void) | |
{ | |
//debug over Serial | |
Serial.begin(57600); | |
//Setting up interrupt ISR on D2 (INT0), trigger function "CLK_ISR()" when INT0 (CLK)is rising | |
attachInterrupt(0, CLK_ISR, RISING); | |
//Set the CLK-pin (D5) to input | |
pinMode(CLKPin, INPUT); | |
//Set the MISO-pin (D5) to input | |
pinMode(MISOPin, INPUT); | |
// Setup and configure nrf radio see: http://maniacbug.github.io/RF24/classRF24.html | |
radio.begin(); | |
// | |
//********* Optional settings for nrf below see: http://maniacbug.github.io/RF24/classRF24.html | |
// | |
// increase the delay between retries & # of retries | |
radio.setRetries(15,15); | |
// reduce the payload size. (3 floates = 3*4=12 Bytes) | |
radio.setPayloadSize(12); | |
//Set speed of transmission (250kbps only works on the "+"-version!!! slower increases range! | |
radio.setDataRate(RF24_250KBPS); | |
//Set Power Amplifier (PA) level to high to increase range! | |
radio.setPALevel(RF24_PA_HIGH); | |
//Automatically receive data when sending something | |
radio.enableAckPayload(); | |
// | |
//********* End of optional settings********* | |
// | |
//Addresses has to be set last! | |
//Transmitter address | |
radio.openWritingPipe(pipes[0]); | |
//I don't have to set an receiver address, because the auto ack will automatically receive an payload if i need to send anything to this meter! | |
} | |
void loop(void) | |
{ | |
//do nothing until the CLK-interrupt occures and sets inSync=true | |
if(inSync == true){ | |
CountBits = 0; //CLK-interrupt increments CountBits when new bit is received | |
while(CountBits<40){} //skip the uninteresting 5 first bytes | |
CountBits=0; | |
Ba=0; | |
Bb=0; | |
while(CountBits<24){ //Loop through the next 3 Bytes (6-8) and save byte 6 and 7 in Ba and Bb | |
if(NextBit == true){ //when rising edge on CLK is detected, NextBit = true in in interrupt. | |
if(CountBits < 9){ //first Byte/8 bits in Ba | |
Ba = (Ba << 1); //Shift Ba one bit to left and store MISO-value (0 or 1) (see http://arduino.cc/en/Reference/Bitshift) | |
//read MISO-pin, if high: make Ba[0] = 1 | |
if(digitalRead(MISOPin)==HIGH){ | |
Ba |= (1<<0); //changes first bit of Ba to "1" | |
} //doesn't need "else" because BaBb[0] is zero if not changed. | |
NextBit=false; //reset NextBit in wait for next CLK-interrupt | |
} | |
else if(CountBits < 17){ //bit 9-16 is byte 7, stor in Bb | |
Bb = Bb << 1; //Shift Ba one bit to left and store MISO-value (0 or 1) | |
//read MISO-pin, if high: make Ba[0] = 1 | |
if(digitalRead(MISOPin)==HIGH){ | |
Bb |= (1<<0); //changes first bit of Bb to "1" | |
} | |
NextBit=false; //reset NextBit in wait for next CLK-interrupt | |
} | |
} | |
} | |
if(Bb!=3){ //if bit Bb is not 3, we have reached the important part, U is allready in Ba and Bb and next 8 Bytes will give us the Power. | |
Antal += 1; //increment for mean value calculations | |
//Voltage = 2*Ba+Bb/255 | |
U=2.0*((float)Ba+(float)Bb/255.0); | |
//Power: | |
CountBits=0; | |
while(CountBits<40){}//Start reading the next 8 Bytes by skipping the first 5 uninteresting ones | |
CountBits=0; | |
Ba=0; | |
Bb=0; | |
Bc=0; | |
while(CountBits<24){ //store byte 6, 7 and 8 in Ba and Bb & Bc. | |
if(NextBit == true){ | |
if(CountBits < 9){ | |
Ba = (Ba << 1); //Shift Ba one bit to left and store MISO-value (0 or 1) | |
//read MISO-pin, if high: make Ba[0] = 1 | |
if(digitalRead(MISOPin)==HIGH){ | |
Ba |= (1<<0); //changes first bit of Ba to "1" | |
} | |
NextBit=false; | |
} | |
else if(CountBits < 17){ | |
Bb = Bb << 1; //Shift Ba one bit to left and store MISO-value (0 or 1) | |
//read MISO-pin, if high: make Ba[0] = 1 | |
if(digitalRead(MISOPin)==HIGH){ | |
Bb |= (1<<0); //changes first bit of Bb to "1" | |
} | |
NextBit=false; | |
} | |
else{ | |
Bc = Bc << 1; //Shift Bc one bit to left and store MISO-value (0 or 1) | |
//read MISO-pin, if high: make Bc[0] = 1 | |
if(digitalRead(MISOPin)==HIGH){ | |
Bc |= (1<<0); //changes first bit of Bc to "1" | |
} | |
NextBit=false; | |
} | |
} | |
} | |
//Power = (Ba*255+Bb)/2 | |
P=((float)Ba*255+(float)Bb+(float)Bc/255.0)/2; | |
//Voltage mean | |
ReadData[0] = (U+ReadData[0]*((float)Antal-1))/(float)Antal; | |
//Power mean | |
ReadData[1] = (P+ReadData[2]*((float)Antal-1))/(float)Antal; | |
//Power max | |
if(P>ReadData[2]){ | |
ReadData[2] = P; | |
} | |
//every 5th read-out ~every 5th second, we send the mean value data to the nrf24l01-transmitter | |
if(Antal>=AverageMax){ | |
//Send the data to the computer | |
bool ok = radio.write( ReadData, sizeof(ReadData) ); | |
if (ok){ | |
Antal=0; //reset mean-value counter after successfull transmission ~10s | |
ReadData[0]=0; | |
ReadData[1]=0; | |
ReadData[2]=0; | |
//This is an awesome feature, if "radio.enableAckPayload()" is enabled, then we may have got somethig in return that we can use: | |
//did we get any data in return when sending the bytes?? | |
if ( radio.isAckPayloadAvailable() ) | |
{ | |
radio.read(&AverageMax,sizeof(AverageMax)); | |
} | |
} | |
} | |
inSync=false; //reset sync variable to make sure next reading is in sync. | |
} | |
if(Bb==0){ //If Bb is not 3 or something else than 0, something is wrong! | |
inSync=false; | |
Serial.println("Disconnected"); | |
} | |
} | |
} | |
//Function that triggers whenever CLK-pin is rising (goes high) | |
void CLK_ISR(){ | |
//if we are trying to find the sync-time (CLK goes high for 1-2ms) | |
if(inSync==false){ | |
ClkHighCount = 0; | |
//Register how long the ClkHigh is high to evaluate if we are at the part wher clk goes high for 1-2 ms | |
while(digitalRead(CLKPin)==HIGH){ | |
ClkHighCount += 1; | |
delayMicroseconds(30); //can only use delayMicroseconds in an interrupt. | |
} | |
//if the Clk was high between 1 and 2 ms than, its a start of a SPI-transmission | |
if(ClkHighCount >= 33 && ClkHighCount <= 67){ | |
inSync = true; | |
} | |
} | |
else{ //we are in sync and logging CLK-highs | |
//increment an integer to keep track of how many bits we have read. | |
CountBits += 1; | |
NextBit = true; | |
} | |
} | |
// vim:cin:ai:sts=2 sw=2 ft=cpp |
Here's the code for the receiving Arduino (connected to computer via USB)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <SPI.h> | |
#include "nRF24L01.h" | |
#include "RF24.h" | |
#include "printf.h" //Activate for NRF-debug (printf_begin(); in setup() followed by radio.printDetails();) | |
#define CE_PIN 9 | |
#define CSN_PIN 10 | |
// | |
// Hardware configuration | |
// | |
// Set up nRF24L01 radio on SPI bus plus pins 9 & 10 | |
RF24 radio(CE_PIN, CSN_PIN); // Create a Radio | |
// Radio pipe addresses 1 node to communicate. | |
//const uint64_t Address = 0xAABBCC22LL; //5-byte address "LL" is for long (if more than one node:) | |
//const uint64_t pipes[2] = { 0xAABBCC11LL, 0xAABBCC22LL }; //Pipes 1-5 must share the first 4 bytes. Only the least significant byte should be unique! | |
const uint64_t pipes[3] = { 0xBABBCCDD00LL, //Writing pipe (not used) | |
0xAABBCCDD11LL, //Meter A's writing pipe | |
0xAABBCCDD22LL //Meter B's writing pipe | |
}; //First letter is address to energ-meter | |
float ReadData[3] = {0, 0, 0}; | |
uint8_t TempStatus; | |
uint8_t CH; | |
uint8_t data[2]; | |
void setup(void) | |
{ | |
Serial.begin(57600); | |
//printf_begin(); | |
Serial.println("Start: "); | |
// | |
// Setup and configure rf radio | |
radio.begin(); | |
// | |
//********* Optional settings for nrf below see: http://maniacbug.github.io/RF24/classRF24.html | |
// | |
// increase the delay between retries & # of retries | |
radio.setRetries(15,15); | |
// reduce the payload size. (3 floates = 3*4=12 Bytes) | |
radio.setPayloadSize(12); | |
//Set speed of transmission (250kbps only works on the "+"-version!!! slower increases range! | |
radio.setDataRate(RF24_250KBPS); | |
//Set Power Amplifier (PA) level to high to increase range! | |
radio.setPALevel(RF24_PA_HIGH); | |
//Awesome feature that lets us send a package in return when receiving something without changing to transmitter! | |
radio.enableAckPayload(); | |
// | |
//********* End of optional settings********* | |
// | |
//Addresses has to be set last! | |
//radio.openWritingPipe(pipes[0]); | |
radio.openReadingPipe(1,pipes[1]); | |
radio.openReadingPipe(2,pipes[2]); | |
//Start listening for incoming data | |
radio.startListening(); | |
// | |
// Dump the configuration of the rf unit for debugging | |
// | |
//read_register(CONFI); //read out the status register to decide which channel that's transmitting data //change RF24.h in libraries (remove "public:", "protected:" and change "private:" to "public:) to use this function! | |
//Serial.println(radio.get_status(),BIN); | |
//Serial.println(radio.read_register(RF_SETUP),BIN); | |
Serial.println("Type: 2:3 to change the intervall of meter 2 to ~3s"); | |
//radio.writeAckPayload( 1, &AverageMax, sizeof(AverageMax) ); | |
//radio.writeAckPayload( 2, &AverageMax, sizeof(AverageMax) ); | |
} | |
void loop(void) | |
{ | |
// if data is received to the nRF | |
if ( radio.available() ) | |
{ | |
// Dump the payloads until we've gotten everything | |
bool done = false; | |
while (!done) | |
{ | |
Serial.println(" "); | |
//Serial.println(radio.get_status(),BIN); | |
// read out the status register to decide which channel that's transmitting data | |
//change RF24.h in libraries (remove "public:", "protected:" and change "private:" to "public:) to use this function! | |
CH = 0; | |
CH = ((radio.get_status() & 0b00001110) >> 1); //extract bit 1-3 (the channels) to CH and move them one step to right to get it in decimal | |
// Fetch the payload, and see if this was the last one. | |
done = radio.read( ReadData, sizeof(ReadData) ); | |
//Print out details | |
Serial.print("Energy meter: "); | |
Serial.println(CH); | |
//When meter reads 0W it sometimes shows ~6000W | |
if(ReadData[2]<4000){ | |
Serial.print(ReadData[0],0); | |
Serial.println(" V"); | |
Serial.print(ReadData[1],1); | |
Serial.println(" W"); | |
Serial.print(ReadData[2],2); | |
Serial.println(" W-Max"); | |
} | |
else{ | |
Serial.print(ReadData[0],0); | |
Serial.println(" V"); | |
Serial.print(0.0,0); | |
Serial.println(" W"); | |
} | |
delay(20); | |
//Reset STATUS-register: | |
//radio.write_register(STATUS, _BV(RX_DR) | _BV(TX_DS) | _BV(MAX_RT) ); | |
} | |
} | |
} | |
//Interrupt ocures whenever you type anything in the serial-monitor. | |
//To change the data sending interval of an energy meter, in the serial monitor type: | |
//2:3 | |
//to change the channel 2-energy meter to ~3s interval. | |
void serialEvent() { | |
while (Serial.available()) { | |
// Reads the the bytes until it reaches an non-integer value ":" | |
int CH = Serial.parseInt(); | |
byte skipp = Serial.read(); | |
int AverageMax = Serial.parseInt(); | |
if(Serial.available()==false && CH >0 && CH < 6 && AverageMax >0 && AverageMax <10000){ | |
// Add an ack packet for the next time around | |
radio.writeAckPayload( CH, &AverageMax, sizeof(AverageMax) ); | |
Serial.print("Intervall on meter: "); | |
Serial.print(CH); | |
Serial.print(" will change to: ~"); | |
Serial.print(AverageMax); | |
Serial.println("s on next transmission!"); | |
} | |
else{ | |
while (Serial.available()) { | |
skipp = Serial.read(); | |
} | |
Serial.println("Wrong format! Type: 2:3 to change the intervall of meter 2 to ~3s"); | |
} | |
} | |
} | |
// vim:cin:ai:sts=2 sw=2 ft=cpp |
And finally the Processing code that can be used to read and write to the computer connected Arduino through its serial connection (over USB)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import processing.serial.*; | |
Serial myPort; // Create object from Serial class | |
String val; // Data received from the serial port | |
//print to csv-file | |
PrintWriter output; | |
long xPos = 1; // horizontal position of the graph | |
//int xLast[] = new int[6]; | |
int yPos = 0; | |
int xMax = 600; | |
int yMax = 600; | |
int vmax = 250; | |
int pmax = 500; | |
int newPmax=0; | |
long lastupdatedXvalue = 0; | |
int pmmax = pmax; | |
int Meter = 1; | |
int IntValue=0; | |
float Last_V = 0.0; | |
float Last_P = 0.0; | |
float Last_Pmax = 0.0; | |
boolean Error = false; | |
float curVal=0; | |
int curElevation=0; | |
String SerialString = ""; | |
String typing = ""; | |
int RGB[][] = new int[][] { | |
{ | |
255, 0, 0, | |
255, 0, 0, | |
0, 150, 0, | |
} | |
, | |
{ | |
255, 220, 230, | |
255, 0, 0, | |
170, 25, 255, | |
} | |
}; | |
//ArrayList<ArrayList<Float>> V = new ArrayList<ArrayList<Float>>(6); | |
TwoDArrayList V; | |
TwoDArrayList P; | |
TwoDArrayList PMax; | |
TwoDArrayList xVals = new TwoDArrayList(); | |
movingLabels Label[][] = new movingLabels[2][6]; | |
//ArrayList<ArrayList<Float>> P = new ArrayList<ArrayList<Float>>(5); | |
//ArrayList<ArrayList<Float>> PMax = new ArrayList<ArrayList<Float>>(5); | |
//ArrayList P = new ArrayList(); | |
//ArrayList PMax = new ArrayList(); | |
String label; | |
float[] markers; | |
String portName = ""; | |
void setup() | |
{ | |
// List all the available serial ports | |
println(Serial.list()); | |
//Sets current Serial-port to the arduino port (the last in the list!?) | |
portName = Serial.list()[Serial.list().length-1]; //change the "Serial.list().length-1" to a 1 or 2 etc. to match your port | |
//Create Serial object to use for comunication with the Arduino with the correct baudrate (Energy meter=57600) | |
myPort = new Serial(this, portName, 57600); | |
//create a csv-file and folder with timestamp-name to stop it from overwriting files... | |
output = createWriter("Logged Data/EnergyMeter_" + year() + "-" +month() + "-" + day() + "_" + hour() + "." + minute() + ".csv"); | |
//create new instances of a nested arraylist-object (see tab "Arraylist") | |
V = new TwoDArrayList(); | |
P = new TwoDArrayList(); | |
PMax = new TwoDArrayList(); | |
// set the window size: | |
size(xMax, yMax); | |
// set inital background to gray | |
background(0); | |
//A serialEvent() is generated when a newline character is received : | |
myPort.bufferUntil('\n'); | |
xPos=0; | |
drawTitle(); | |
//Label[2]=new movingLabels(xPos, 200.0, Integer.toString(xPos) + " V"); | |
} | |
void draw() | |
{ | |
//Wait for a serial event to trigger... | |
} | |
//Interrupt that triggers whenever data is received on the serial port. | |
void serialEvent (Serial myPort) { | |
//if error occures, do not stop code... | |
try { | |
// get the Serial-ASCII string: | |
String inString= myPort.readStringUntil('\n'); | |
println(inString); //debug | |
if (inString != null) { | |
// trim off any whitespace before and after the data (if exists) | |
inString = trim(inString); | |
//The data comes in 4 separate transmissions, first energy meter number, then V then W than W-Max | |
//now check which one we are receiving: | |
if (inString.indexOf("Energy")>=0) //If the String "Energy" is found, its the Energy meter number "Energy Meter: x" | |
{ | |
//Remove the "Energy Meter: " to get the float containing the number | |
//print("Mätare: "); | |
inString = inString.replaceAll("Energy meter: ", ""); | |
Meter = Integer.parseInt(inString.trim());//Float.parseFloat(inString); | |
//increment x-value | |
xPos=millis()/100; //10 lines per second | |
//Setts the actual value, before fitting it into the graph. | |
} else if (inString.indexOf(" V")>=0 && V.getSize(Meter)==PMax.getSize(Meter) && Meter<7 && Meter>0) //If the String " V" is found, its the voltage string: "xxx V" | |
{ | |
//Remove the " V" to get the float containing the data | |
inString = inString.replaceAll(" V", ""); | |
IntValue = Integer.parseInt(inString.trim()); | |
if (IntValue>0) { | |
Last_V = float(inString); | |
ReDraw(); | |
//If we've allready filled 3/4th of the plot-window, start removing data so we dont crash into the wall. | |
if (xPos >= xMax-100) | |
{ | |
//ReDraw(); | |
} | |
xVals.addArr(Meter, (float)xPos); | |
stroke(RGB[Meter-1][0], RGB[Meter-1][1], RGB[Meter-1][2]); | |
Error=false; | |
//If this is a valid reading IntValue>200 (~230 expected) to skip error-values | |
if (IntValue>200) | |
{ | |
//voltage converted to a scale from (0 to vmax) as (0 to height of the graph*0,8) | |
curVal=map(IntValue, 0, vmax, 0, height*0.8); | |
//Add the value to the V-object | |
V.addArr(Meter, curVal); | |
} else if (V.getSize(Meter)>0) //Error and not first valueA | |
{ | |
Error=true; | |
curVal=V.getLast(Meter); | |
V.addArr(Meter, V.getLast(Meter)); | |
} else //Error and first value =>put a zero as first recorded value | |
{ | |
curVal=0; | |
V.addArr(Meter, curVal); | |
} | |
//Check if this is the first added value? | |
if (V.getSize(Meter)==1) | |
{ | |
//Draw the starting line from current xPos and y=V(0) to itself (make a dot) | |
line(xPos, height-V.getArr(Meter, 0), xPos, height-V.getArr(Meter, 0)); | |
//Create a label-object for the line that hoovers 10px over line (first index of "Label" is 0=V,1=Pmax,2=P, secound index is Meter) | |
Label[0][Meter]=new movingLabels(xPos, height-V.getArr(Meter, 0)-10, inString + " V"); | |
} else { | |
//Draw a line from the second last xVals-value to the new xPos-value (last xVals-value) | |
line(xVals.getLast(Meter, 1), height-V.getLast(Meter, 1), xVals.getLast(Meter), height-V.getLast(Meter)); | |
//print the label: | |
Label[0][Meter].drawText(xPos-10, height-curVal-10, Meter + String.format(": %.0f", Last_V)+"V"); | |
} | |
//output.println("V"); | |
} | |
} | |
//now do the same with W-Max and W (note the "" for string and '' for char! | |
else if (inString.indexOf("W-Max")>=0 && PMax.getSize(Meter)<P.getSize(Meter)) // Max power "xx.x W-Max" | |
{ | |
inString = inString.replaceAll(" W-Max", ""); | |
Last_Pmax = float(inString); | |
stroke(RGB[Meter-1][6], RGB[Meter-1][7], RGB[Meter-1][8]); | |
//skip error-values | |
if (Last_Pmax<4000 && Error == false) | |
{ | |
curVal= map(Last_Pmax, 0, pmmax, 0, height*0.8); | |
//Add the voltage-float converted to a scale from 0 to vmax as 0 to height of the graph*0,8 | |
PMax.addArr(Meter, curVal); | |
} else if (PMax.getSize(Meter)>0) { | |
Error=true; | |
curVal=PMax.getLast(Meter); | |
PMax.addArr(Meter, PMax.getLast(Meter)); | |
} else { | |
Error=true; | |
curVal=0; | |
PMax.addArr(Meter, curVal); | |
} | |
//Check if this is the first added value? | |
if (PMax.getSize(Meter)==1) { | |
//Draw the starting line from x=10 and y=V(0) to itself (make a dot) | |
line(xPos, height-PMax.getArr(Meter, 0), xPos, height-curVal); | |
} else { | |
//Draw a line from the last value to the new value | |
line(xVals.getLast(Meter, 1), height-PMax.getLast(Meter, 1), xVals.getLast(Meter), height-PMax.getLast(Meter)); | |
} | |
//output.println("Wmax"); | |
//Print all the values to a csv-file (see output-tab) | |
PrintToCSVFile(); | |
} else if (inString.indexOf(" W")>=0 && inString.indexOf("Max")<0 && P.getSize(Meter)<V.getSize(Meter)) //Average Power "xx.x W" | |
{ | |
inString = inString.replaceAll(" W", ""); | |
Last_P = float(inString); | |
stroke(RGB[Meter-1][3], RGB[Meter-1][4], RGB[Meter-1][5]); | |
//skip error-values either caught here, or by the voltage-readings. | |
if (Last_P<=4000 && Error == false) | |
{ | |
curVal=map(Last_P, 0, pmax, 0, height*0.8); | |
//Add the voltage-float converted to a scale from 0 to vmax as 0 to height of the graph*0,8 | |
P.addArr(Meter, curVal); | |
//Add kWh | |
P.addkWh(Meter, Last_P); | |
} else if (P.getSize(Meter)>0) | |
{ | |
Error=true; | |
curVal=P.getLast(Meter); | |
P.addArr(Meter, P.getLast(Meter)); | |
} else //this is the first reading, and it is wiered. | |
{ | |
Error=true; | |
curVal=0; | |
P.addArr(Meter, curVal); | |
} | |
//Check if this is the first added value? | |
if (P.getSize(Meter)==1) { | |
//Draw the starting line from x=10 and y=V(0) to itself (make a dot) | |
line(xPos, height-P.getArr(Meter, 0), xPos, height-curVal); | |
Label[1][Meter]=new movingLabels(xPos, height-curVal-10, "W");//Float.toString(P.getLast(Meter)) + " V"); | |
} else { | |
//Draw a line from the last value to the new value | |
line(xVals.getLast(Meter, 1), height-P.getLast(Meter, 1), xVals.getLast(Meter), height-P.getLast(Meter)); | |
Label[1][Meter].drawText(xPos+5, height-curVal-10, Meter + String.format(".\n%.1f", Last_P)+"W\n" + String.format("%.4f", P.getkWh(Meter)) + "kWh");//Float.toString(P.getLast(Meter)) + " W"); | |
} | |
//output.println("W"); | |
} | |
} | |
} | |
catch(RuntimeException e) { | |
print("Här: "); | |
println(e); | |
delay(1000); | |
myPort = new Serial(this, portName, 57600); | |
} | |
} | |
//Function that triggers when a key is pressed, that sends data to the Arduino over serial. | |
void keyPressed() { | |
// If the return key is pressed, save the String, do something with it, clear it | |
if (key == '\n' ) { | |
//type eg: 2:1 to set the transmission intervall to 1s on the 2'nd meter. | |
if (SerialString.indexOf(":")>=0) { //see if the string contained ":" | |
//Send string to Arduino: | |
myPort.write(SerialString); | |
} else if (SerialString.indexOf("z")>=0) { //if you want to change the zoom on the power-readings, type | |
//example: type "z1800" to set it to pmax=1800W => screen height | |
try { | |
//extract the number-part from the string and try and convert it to an integer: | |
newPmax=int(SerialString.replaceAll("z", "")); | |
ReDraw(); | |
} | |
catch(Exception e) { | |
//newPmax=0; | |
} | |
} | |
//redraws the whole graph. | |
//ReDraw(); | |
// clear the string | |
SerialString = ""; | |
} else { | |
// Each character typed by the user is added to the end of the String variable. | |
SerialString = SerialString + key; | |
drawTitle(); | |
//The string is printed on the screen everytime the Label is uated... | |
} | |
//ReDraw(); | |
} | |
//******************************************************New Tab here****************************************************// | |
/*This class is creating nested arraylists | |
Theloat in my case, but can be used by all objects | |
To create an nested arraylist: | |
//Create a public variable-list | |
TwoDArrayList = X; | |
//In code, make a new instance of the list | |
X = new TwoDArrayList(); | |
//Then | |
X.addArr(2,34); //Adds "34" to the last position of array 2 | |
X.setArr(2,3,34); //Sets "34" to array 2, position 3. | |
X.getArr(2,3); //Returns value from array 2, position 3. | |
*/ | |
class TwoDArrayList<T> extends ArrayList<ArrayList<T>> { | |
long lastX[] = new long[6]; | |
float kWh[][]= new float[6][2]; | |
public void setLastX(int index, long _lastX) { | |
lastX[index]=_lastX; | |
} | |
public long getLastX(int index) { | |
if (lastX[index]==0) { | |
lastX[index]= xMax-100; | |
} | |
return lastX[index]; | |
} | |
public void addkWh(int index, float Pmedel) { | |
//E=P*dt | |
kWh[index][0]+=Pmedel*(millis()-kWh[index][1])/(1000*3600)/1000; // /ms /s per h /kW in W | |
kWh[index][1]=millis(); | |
} | |
public float getkWh(int index) { | |
return kWh[index][0]; | |
} | |
//Add element on last position of inner list "index" | |
public void addArr(int index, T element) { | |
while (index >= this.size ()) { | |
this.add(new ArrayList<T>()); | |
} | |
this.get(index).add(element); | |
} | |
//Add element on specific position of index and index2 | |
public void setArr(int index, int index2, T element) { | |
while (index >= this.size ()) { | |
this.add(new ArrayList<T>()); | |
} | |
ArrayList<T> inner = this.get(index); | |
while (index2 >= inner.size ()) { | |
inner.add(null); | |
} | |
inner.set(index2, element); | |
} | |
//return value from index position | |
public float getArr(int index, int index2) { | |
return (Float)this.get(index).get(index2); | |
} | |
//return size from index position | |
public int getSize(int index) { | |
if (this.isEmpty()) { | |
return 0; | |
} else if (index>this.size()-1) { | |
return 0; | |
} else if (this.get(index).isEmpty()) { | |
return 0; | |
} else { | |
return (Integer)this.get(index).size(); | |
} | |
} | |
//return last value without offset | |
public float getLast(int index) { | |
return (Float)this.get(index).get(this.get(index).size() -1); | |
} | |
//return last value with offset | |
public float getLast(int index, int offset) { | |
return (Float)this.get(index).get(this.get(index).size() -1 -offset); | |
} | |
// | |
public void Remove(int index, int index2) { | |
if (this.get(index).isEmpty()==false) { | |
this.get(index).remove(index2); | |
} | |
} | |
} | |
//******************************************************New Tab here****************************************************// | |
void drawTitle() { | |
//background(0); | |
fill(255, 255, 255); | |
textAlign(LEFT); | |
String title = "Energy Meter"; | |
textSize(20); | |
text(title, 20, 20); | |
textSize(15); | |
text("Zoom: " + pmax + " W", 20, 50); | |
text(SerialString, 250, 20); | |
} | |
void drawLabel(int x, int y, String val, String unit) { | |
fill(255, 255, 255); | |
textSize(10); | |
textAlign(LEFT); | |
String title = val + ' ' + unit; | |
text(title, x, y); | |
} | |
class movingLabels { | |
float x, y, xOld, yOld; | |
String s; | |
String sOld=""; | |
movingLabels (float _x, float _y, String _text) { | |
//This function is the 'constructor'. | |
//It will get called when you create a new | |
//movingText object | |
x = _x; //Set x to the first argument | |
y = _y; //Set y to the second argument | |
s = _text; //Set our string to the 3rd argument | |
} | |
public void drawText(float _x, float _y, String _text) { | |
s=_text; | |
x=_x; | |
y=_y; | |
textSize(10); | |
textAlign(LEFT); | |
//Start by writing over the old text with black: | |
fill(0); | |
for (int i=0; i<10; i++) { | |
//text(sOld, xOld, yOld); | |
} | |
textSize(10); | |
//Now print the new text in white | |
fill(255, 255, 255); | |
textLeading(10); //enable new line '\n' | |
text(s, x, y); | |
sOld=s; | |
xOld=x; | |
yOld=y; | |
drawTitle(); | |
} | |
} | |
void YLabel() { | |
fill(255, 255, 255); | |
textSize(10); | |
textAlign(RIGHT); | |
stroke(255,255,255); | |
strokeWeight(1); | |
int Intervall = pmax/20; | |
int HuvudLinje = Intervall*5; | |
for (float v = 0; v <= pmax; v += Intervall) { | |
if (v % Intervall == 0) { // If a tick mark | |
float y = map(v, 0, pmax, height,height*0.2); | |
//=map(Last_P, 0, pmax, 0, height*0.8); | |
if (v % HuvudLinje == 0) { // If a major tick mark | |
float textOffset = textAscent()/2; // Center vertically | |
if (v == 0) { | |
textOffset = 0; // Align by the bottom | |
} else if (v == height*0.8) { | |
//textOffset = textAscent(); // Align by the top | |
} | |
text(floor(v), 20, y + textOffset); | |
line(35, y, width, y); // Draw major tick | |
//line(plotX1-2, y+9, plotX1, y+9); //line to draw midle lines | |
} else { | |
line(40, y, 45, y); // Draw minor tick | |
} | |
} | |
} | |
} | |
//******************************************************New Tab here****************************************************// | |
void PrintToCSVFile() { | |
//String to export to csv-file with timestamp: | |
//csv-files opened in Swedish Excel automatically separate colums by: ';', in english version it's: ',' | |
String Temp = year() + "-" + month() + "-" + day() + ";"; | |
Temp += hour() + ":" + minute() + ":" + second() + ";"; | |
//jump some colums to separate the different meters: | |
for (int i=1; i<Meter; i++) { | |
Temp += ";;;;;;;"; | |
} | |
Temp += "Energy Meter: " + Meter + ";"; | |
Temp += String.format("%.0f", Last_V) + ";V;"; | |
Temp += String.format("%.1f", Last_P) + ";W;"; | |
Temp += String.format("%.1f", Last_Pmax) + ";W"; | |
//put the string in output-buffer | |
output.println(Temp); | |
//Execute the writing to the file... | |
output.flush(); | |
} | |
//Not used, but a way of adding separators. | |
String PrintData(float Data, int Col) { | |
String TempStr = ""; | |
for (int i=0; i<Col; i++) { | |
TempStr = TempStr + ";"; | |
} | |
return TempStr + String.format("%.1f", Data); | |
} | |
//******************************************************New Tab here****************************************************// | |
void ReDraw() { | |
//if a secound meter starts after we've hit the wall, its empty! | |
if (V.getSize(Meter)>0) { | |
// set inital background=clear screen | |
background(0); | |
//save current meter to remember which one to add data to later. | |
int currMeter = Meter; | |
//put the string in output-buffer | |
//output.println("getlastX: ;" + xVals.getLastX(Meter) + ";Xpos: ;" + xPos); | |
//find the offset from last recorded value: | |
long Xoffset=xPos-lastupdatedXvalue;//xVals.getLastX(Meter); | |
lastupdatedXvalue=xPos; | |
//loop through all the meters and if they contain data; meve them back one dX | |
for (int j=1; j<7; j++) { | |
Meter=j; | |
//Check if Meter is not empty | |
if (V.getSize(Meter)>0) { | |
//Remove values that have passed the screen on the rest of the meters | |
while (xVals.getArr (Meter, 0)<-100) { | |
V.Remove(Meter, 0); | |
P.Remove(Meter, 0); | |
PMax.Remove(Meter, 0); | |
xVals.Remove(Meter, 0); | |
} | |
if (xPos>= xMax-100 && newPmax==0) { | |
//Uppdate all the xVals on j-meter. | |
for (int i=0; i<xVals.getSize (Meter); i++) | |
{ | |
xVals.setArr(Meter, i, xVals.getArr(Meter, i) - Xoffset); | |
} | |
} | |
if (newPmax>0) { | |
for (int i=0; i<P.getSize (Meter); i++) | |
{ // curVal=map(P, 0, pmax, 0, height*0.8); | |
P.setArr(Meter, i, P.getArr(Meter, i)*pmax/newPmax); | |
PMax.setArr(Meter, i, PMax.getArr(Meter, i)*pmmax/newPmax); | |
} | |
} | |
//Set line-color to pink for plotting "V" | |
stroke(RGB[Meter-1][0], RGB[Meter-1][1], RGB[Meter-1][2]); | |
//Uppdate all the V-values | |
for (int i=0; i<V.getSize (Meter)-1; i++) | |
{ | |
line(xVals.getArr(Meter, i), height-V.getArr(Meter, i), xVals.getArr(Meter, i+1), height-V.getArr(Meter, i+1)); | |
} | |
//Set line-color to pink for plotting "W-Max" | |
stroke(RGB[Meter-1][3], RGB[Meter-1][4], RGB[Meter-1][5]); | |
for (int i=0; i<P.getSize (Meter)-1; i++) | |
{ | |
line(xVals.getArr(Meter, i), height-P.getArr(Meter, i), xVals.getArr(Meter, i+1), height-P.getArr(Meter, i+1)); | |
} | |
//Set line-color to pink for plotting "W" | |
stroke(RGB[Meter-1][6], RGB[Meter-1][7], RGB[Meter-1][8]); | |
for (int i=0; i<PMax.getSize (Meter)-1; i++) | |
{ | |
line(xVals.getArr(Meter, i), height-PMax.getArr(Meter, i), xVals.getArr(Meter, i+1), height-PMax.getArr(Meter, i+1)); | |
} | |
//If this isn't the meter that'll get a new value, draw the lables | |
if (Meter!=currMeter) { | |
//Label[0][Meter].drawText(xVals.getLast(Meter), height-V.getLast(Meter)-10, Meter + String.format(": %.0f", map(V.getLast(Meter), 0, height*0.8, 0, vmax))+"V"); | |
Label[1][Meter].drawText(xVals.getLast(Meter)+5, height-P.getLast(Meter)-10, Meter + String.format(".\n%.1f", | |
map(P.getLast(Meter), 0, height*0.8, 0, pmax))+"W\n" + String.format("%.4f",P.getkWh(Meter)) + "kWh"); | |
} | |
} | |
} | |
Meter=currMeter; | |
if (newPmax>0) { | |
pmax=newPmax; | |
pmmax=newPmax; | |
newPmax=0; | |
} | |
} | |
//Save the current time | |
xVals.setLastX(Meter, xPos); | |
if (xPos> xMax-100) { | |
//X-value for newest value that'll be added bellow! | |
xPos = xMax-100; | |
} | |
YLabel(); | |
} |