Help monitoring 2 phases of AC current only ADS1115 & NodeMCU

Can definitely relate to that.
My ET career ran from 1974 to 2020. Now that it’s a “full time” hobby, I’m enjoying it more than ever.

And jumping on the bandwagon, I collected my 50 years of membership badge of the IEE (now the IET) a few years ago. :smiley: I joined in 1968 as a student apprentice.

No, I haven’t looked at it yet, but I will now, thank you, and I’ll checkout the Github source as well.

Thanks

Well some more progress today. I managed to get the ADS working properly with the ESP8266 and calibrated very closely to my Fluke clamp meter.

Now I need to ask for some help please with the code to make it 2 channel. I thought I had it ready to go, took about 20 minutes of compiling and making little changes then recompiling to where I had it compiled but then crashed when it was about to upload. I’ve put the code back to single channel so as not to muddle it up.

Could one of you wizzy Arduino code writers help me get this working with 2 channels please? I am not sure which variables need to be set for channel 2. I can read the ADS1115 channel 2 no problem, it’s the math functions that are confusing me. I can’t take credit for the math part of the code, as suggested, I found something that worked for the current measurement in single channel and used that.

Thank you.

Paul

#include <ESP8266WiFi.h>
#include <Wire.h>
#include <Adafruit_ADS1015.h>

Adafruit_ADS1115 ads;  

double offsetI;
double filteredI;
double sqI,sumI;
int16_t sampleI;
double Irms;
double squareRoot(double fg)  
{
  double n = fg / 2.0;
  double lstX = 0.0;
  while (n != lstX)
  {
    lstX = n;
    n = (n + fg / n) / 2.0;
  }
  return n;
}

double calcIrms(unsigned int Number_of_Samples)
{
    float multiplier = 0.02F;    
  for (unsigned int n = 0; n < Number_of_Samples; n++)
  {
    sampleI = ads.readADC_SingleEnded(0);

  //  Digital low pass filter extracts the 2.5 V or 1.65 V dc offset, 
  //  then subtract this - signal is now centered on 0 counts.
    offsetI = (offsetI + (sampleI-offsetI)/1024);
    filteredI = sampleI - offsetI;
    
    // Root-mean-square method current
    // 1) square current values
       sqI = filteredI * filteredI;
    // 2) sum 
       sumI += sqI;
  }
  
  Irms = squareRoot(sumI / Number_of_Samples)*multiplier; 

   //Reset accumulators
       sumI = 0;
//--------------------------------------------------------------------------------------       
 
  return Irms;
}

void setup() {
  Serial.begin(9600);
  delay(10);
  ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV
  ads.begin();
}

void loop() {
     
  double current = calcIrms(1480);
  Serial.print(Irms); 
  Serial.println("  ");
   
}

And in case anyone is interested… my ThingSpeak channel streaming real time data. Temp and oil P sensors are not connected, current is monitoring my power bar for my computer desk, lights etc. I’m feeding both current phases from channel 1 until I get 2 channels reading correctly in the ESP8266.

cheers

Thing Speak Generator Monitor

I prefer to teach you how to get to the answer, rather than telling you outright. So…

  1. What is the equivalent statement to read the second channel?
  2. Change calcIrms(...) so that you can tell it which channel to read, no.1 or no.2
  3. In loop extend it to call calcIrms(...) twice, once for each channel, save the result separately as (say) currentA & currentB for the A & B legs and then print the results.

Hint: to solve the puzzle of what you need to do for point no.2, you do for ‘readADC’ exactly like the way Number_of_Samples [ = 1480] gets from double current = calcIrms(1480); all the way to for (unsigned int n = 0; n < Number_of_Samples; n++) [as well as, not instead of Number_of_Samples].

If you struggle, come back with your best attempt.

Thank you Robert,

I agree, it’s the best way to learn. I will come at it with a fresh brain and try it again. I thought I had it ready to go once, I know how to retrieve the data from each pin and assign xxx1 and xxx2 for each input and variables, I just had a tough time figuring out what had to have 2 separate formulas for each channel and what could remain a common between the channels.

First cup of coffee is in hand, the fire is lit…and away we go.

More later.

Paul

1 Like

Ok, two cups of coffee later and some more hair loss, I’m back to it compiling as it did yesterday but crashing right at the end. Here’s my code and then the error message.

Thanks again. I am still right at the beginning of the code learning curve and understanding the C++ language, hardware is my strength. :face_with_diagonal_mouth:

#include <ESP8266WiFi.h>
#include <Wire.h>
#include <Adafruit_ADS1015.h>

Adafruit_ADS1115 ads;  

double offsetI;
double filteredI;
double sqI,sumI;
int16_t sampleI1;
int16_t sampleI2;
double Irms1;
double Irms2;
double squareRoot(double fg)  
{
  double n = fg / 2.0;
  double lstX = 0.0;
  while (n != lstX)
  {
    lstX = n;
    n = (n + fg / n) / 2.0;
  }
  return n;
}

double calcIrms1(unsigned int Number_of_Samples);
double calcIrms2(unsigned int Number_of_Samples)
{
  /* Be sure to update this value based on the IC and the gain settings! */
  float multiplier = 0.02F;    /* ADS1115 @ +/- 4.096V gain (16-bit results) */
  for (unsigned int n = 0; n < Number_of_Samples; n++)
  {
    sampleI1 = ads.readADC_SingleEnded(0);
    sampleI2 = ads.readADC_SingleEnded(1);


  //  Digital low pass filter extracts the 2.5 V or 1.65 V dc offset, 
  //  then subtract this - signal is now centered on 0 counts.
    offsetI = (offsetI + (sampleI1-offsetI)/1024);
    filteredI = sampleI1 - offsetI;
    
    // Root-mean-square method current
    // 1) square current values
       sqI = filteredI * filteredI;
    // 2) sum 
       sumI += sqI;
  }
  
  Irms1 = squareRoot(sumI / Number_of_Samples)*multiplier; 
  Irms2 = squareRoot(sumI / Number_of_Samples)*multiplier; 
  
   //Reset accumulators
       sumI = 0;
//--------------------------------------------------------------------------------------       
 
  return Irms1;
  return Irms2;
}

void setup() {
  Serial.begin(9600);
  delay(10);
  ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV
  ads.begin();
}

void loop() {
     
  double current1 = calcIrms1(1480);
  double current2 = calcIrms2(1480);
  Serial.print(Irms1); 
  Serial.println("  ");
  Serial.print(Irms2); 
  Serial.println("  ");
   
}
Arduino: 1.8.8 (Windows 7), Board: "NodeMCU 1.0 (ESP-12E Module), 80 MHz, Flash, Disabled (new aborts on oom), Disabled, All SSL ciphers (most compatible), 32KB cache + 32KB IRAM (balanced), Use pgm_read macros for IRAM/PROGMEM, 4MB (FS:2MB OTA:~1019KB), 2, v2 Lower Memory, Disabled, None, Only Sketch, 115200"

c:/users/t500 daddy/appdata/local/arduino15/packages/esp8266/tools/xtensa-lx106-elf-gcc/3.1.0-gcc10.3-e5f9fec/bin/../lib/gcc/xtensa-lx106-elf/10.3.0/../../../../xtensa-lx106-elf/bin/ld.exe: sketch\Current_Monitor_2_Channel_Working.ino.cpp.o: in function `setup':

C:\Users\T500DA~1\AppData\Local\Temp\arduino_modified_sketch_298347/Current_Monitor_2_Channel_Working.ino:63: undefined reference to `_Z9calcIrms1j'

c:/users/t500 daddy/appdata/local/arduino15/packages/esp8266/tools/xtensa-lx106-elf-gcc/3.1.0-gcc10.3-e5f9fec/bin/../lib/gcc/xtensa-lx106-elf/10.3.0/../../../../xtensa-lx106-elf/bin/ld.exe: sketch\Current_Monitor_2_Channel_Working.ino.cpp.o:(.text.loop+0x18): undefined reference to `_Z9calcIrms1j'

collect2.exe: error: ld returned 1 exit status
exit status 1
Error compiling for board NodeMCU 1.0 (ESP-12E Module).

You’re duplicating far, far too much.

Very little.

Almost everything.

Here’s my take on point 2:

double calcIrms(unsigned int Number_of_Samples, byte inputChannel)
// A byte is unsigned, 8 bits wide. You'll only ever need 2 bits here (inputs 0-3)
{
  float multiplier = 0.02F;    
  for (unsigned int n = 0; n < Number_of_Samples; n++)
  {
    sampleI = ads.readADC_SingleEnded(inputChannel);

    //  Digital low pass filter extracts the 2.5 V or 1.65 V dc offset, 
    //  then subtract this - signal is now centered on 0 counts.
    offsetI = (offsetI + (sampleI-offsetI)/1024);
    filteredI = sampleI - offsetI;
    
    // Root-mean-square method current
    // 1) square current values
    sqI = filteredI * filteredI;
    // 2) sum 
    sumI += sqI;
  }
  
  Irms = squareRoot(sumI / Number_of_Samples)*multiplier; 

  //  Reset accumulators
  sumI = 0;
  //--------------------------------------------------------------------------------------       
 
  return Irms;
}

Compare that with your original in post no.11, NOT the crashing version.

Only when you understand how and why this will work, look at point no.3

[How far from UTC are you? I’m off to cook a meal, back in a couple of hours.]

Thank you Robert!

It’s 9:48am here, enjoy your dinner.

I’ll go back to it shortly, I need to clean the workbench so I can build this into a box once I get the code working…hopefully today. :slight_smile:

Cheers

Silly question, but I’ve been bitten by this one…

Is your “board” selection set to NodeMCU?

e.g.

If you get back to this by 4pm your time, I’ll probably still be around. Otherwise, it’s tomorrow.

Meanwhile, I’ll try to point out and explain the things you got wrong, and the things I’d do differently.

1. You wrote that you wanted to use emonLib. The way this works is it samples a channel (both voltage and current if you want power, or just the current if you only want current) for a fixed period of time. In the case of power, voltage is available so it can count mains half-cycles and start and stop as close as it can to a zero crossing. In the case of current only, you have to calculate (or measure) and give it the number of samples to record so that you’re as close as you can be to a whole number of mains cycles, otherwise, the rms average will be wrong because of the extra part-cycle – and you can’t define the starting or ending points, so the error is essentially random. Having sampled one channel it switches to the next, and so on. When it’s done all, it dispatches the data to wherever it’s needed. Then the sketch waits (the way we used it) for 9 s or so and then tells it to get the samples for all the channels again.

2. Now, looking at loop(...), you think you’ve got two functions to get the current. In fact you haven’t, because you’ve both declared and defined only one – double calcIrms2(unsigned int Number_of_Samples) { ... }. You declared double calcIrms1(unsigned int Number_of_Samples); but that’s all, there is no definition of what it does – there is no pair of curly brackets with code between them that defines how it does anything.
This is what "undefined reference to _Z9calcIrms1j "is trying to tell you (but the name has got mangled).

3. Inside calcIrms2(...), you’re trying to sample both samples sequentially. This would be OK, but (a) it’s not how emonLib works and more seriously, calcIrms2(...) is defined as returning a double – A double, one, not two, so

  return Irms1;
  return Irms2;

is illegal because you can never return two separate things from a function. To do this, you’d have to either use global variables (these are values that exist for all time all over the sketch, which is messy and generally not recommended) or you’d need to use an array or pointers, and that’s probably too advanced for you at the moment.

The other, and best, reason for not doing both legs in parallel is your sample rate from the ADS1115 is slow, 860 samples per second, to you that’s 14.33 samples per mains cycle. If you’re doing two channels, that’s only 7.167 samples per cycle. You’ll be in trouble if there’s anything above the third harmonic in your current wave. If you have a current wave like this spiky current waveform (Washing machine brushless d.c. motor on spin by @dBC

Phase measurement and correction in IoTaWatt - #17 by dBC)

or this most distorted channel made up of a boat load of CFLs:

(from The importance of continuous sampling Red is the current, also thanks to @dBC)
Just consider how much of the wave you’ll sample, or not sample, with only 14 - let alone just 7 sampling points - on each cycle.

1 Like

Hi Bill,

Yes I have run into that before in a “seniors moment” but not this time. ha! Thanks for the thought though.

Robert, thank you for the detailed reply, I haven’t read it yet, I had a bit of a mental meltdown and realized I shouldn’t be learning Arduino code right now with so much to do before winter hits. It’s the sort of thing I should be doing in the dead of winter when there is 3’ of snow on the ground and there’s nothing to do. I really need to take a course or get a book and really learn C++, it’s not something I think I can pickup and make useful right now. I have been successful in the past with various projects but that was more cut and pasting things into a sketch and making it work, I actually have never really written complex code from scratch, there’s my confession, I’m a hardware engineer.

I will let you know when I make more progress on this, it could be a few months or if I get things done that need doing, I may get back on it right away, either way, I WILL figure it out, come hell or high blood pressure.

Cheers and thank you both so much for the guidance.

1 Like

Bookmark this thread in your browser, or click on your icon a couple of times till you come to a page with “Summary” and “Activity” on it, where some/all your posts are listed, and find it that way.

If you’re away for a while, I’ll post my version in full here. It’s not tested, but I’m bound to lose it if I don’t.

#include <ESP8266WiFi.h>
#include <Wire.h>
#include <Adafruit_ADS1015.h>

Adafruit_ADS1115 ads;  

double offsetI;
double filteredI;
double sqI,sumI;
int16_t sampleI;
double Irms;
double squareRoot(double fg)  
{
  double n = fg / 2.0;
  double lstX = 0.0;
  while (n != lstX)
  {
    lstX = n;
    n = (n + fg / n) / 2.0;
  }
  return n;
}

double calcIrms(unsigned int Number_of_Samples, byte inputChannel)
// A byte is unsigned, 8 bits wide. You'll only ever need 2 bits here (inputs 0-3)
{
  float multiplier = 0.02F;    
  for (unsigned int n = 0; n < Number_of_Samples; n++)
  {
    sampleI = ads.readADC_SingleEnded(inputChannel);

    //  Digital low pass filter extracts the 2.5 V or 1.65 V dc offset, 
    //  then subtract this - signal is now centered on 0 counts.
    offsetI = (offsetI + (sampleI-offsetI)/1024);
    filteredI = sampleI - offsetI;
    
    // Root-mean-square method current
    // 1) square current values
    sqI = filteredI * filteredI;
    // 2) sum 
    sumI += sqI;
  }
  
  Irms = squareRoot(sumI / Number_of_Samples)*multiplier; 

  //  Reset accumulators
  sumI = 0;
  //--------------------------------------------------------------------------------------       
 
  return Irms;
}

void setup() {
  Serial.begin(9600);
  delay(10);
  ads.setGain(GAIN_ONE);        // 1x gain   +/- 4.096V  1 bit = 2mV      0.125mV
  ads.begin();
}

void loop() {
     
  double IrmsA = calcIrms(1480, 0);
  double IrmsB = calcIrms(1480, 1);     // I'm guessing this is the second channel
  Serial.print(IrmsA); 
  Serial.print("  ");
  Serial.print(IrmsB); 
  Serial.println(" "); 
}

/**********************************************************************
 
On a practical level, you will need to change "1480" to a value which gives you 
the number of samples as close as possible to a whole number of mains cycles, 
and about 200 ms worth, because if you have an "overhang" of say ¼-cycle, you'll 
have an error which varies according to where you catch the mains cycle - unless 
you have so many cycles that the error is negligible.

**********************************************************************/

I’ve got a list of books & on-line resources.

1 Like

Thanks Robert,

I’m over the anxious moment having achieved one hardware project, putting a fan on the wood stove. Ha! I’ll probably get back on it tonight. :crazy_face: while it’s still fresh in my brain, waiting for a month or more I’d be back at SQ1.

Here is my most successful hardware project and really where I learned about Arduino and figured out all the code required to make it work. As you can see, it’s a real hardware project. Hope it’s okay to post non energy subject matter here. Also, if you go from that video to my main page you can see part 1 of the Telecine machine as well as the generator I am building this 2 phase monitor for.

Paul’s Telecine project

PS I am not going to read your last post with the code yet, I’m going to study post 19 for a while and see if I can figure it out myself. It’s just started raining again so I can’t complete any outdoor projects for now, time for more code writing.

Thank you.

Paul

Good morning gentlemen,

I spent a further 3 hours working on the code, trying to get the 2 channels reading correctly then finally gave in and I looked at Roberts code…oh man, how simple is that? I was changing all the code within the {} loop, obviously the wrong loop to modify. Robert, your code works perfectly, I thank you. I spent another hour trying to figure out why it works, but couldn’t. If you have time, I would appreciate a brief explanation, and then point me to your recommended online learning resources please.

So now that it’s working I want to go through the code and ensure the ADS1115 is working at its highest speed of 860s/s. I have the 1015 on the way but want to learn how the libraries interact with the sketch, I should be able to figure this out. I also find that there are several different libraries for the ADS1115/1015 which I am also going to play with.

The current measurements are very accurate, at least for me, from .5A up to 12A, reads within .1A of my Fluke 333 clamp meter, and all data is being passed to Thingspeak successfully.

Thank you both for getting me this far, I’m going to continue tweaking and playing to ensure it’s 100%.

cheers

Paul

1 Like

I told you you were changing too much :wink: I did try to tell you a few posts ago:

OK, a function in C (or C++, except it’s called a “method” there when it’s part of a ‘class’) can be invoked with no parameters, or many. When it’s done its job, it can return one or no value.

double squareRoot(double fg)
{
  ...
  return n;
}

When you invoke it, you give it a number to work with, a double precision floating point variable. Outside of the function, that variable can be called anything. Inside, the value of the variable is assigned to fg. You can do what you like with it inside the function, it has absolutely no effect outside. (If you’ve got an fg outside the function, it is NOT the same variable! The two can exist entirely separately. If you want to learn more, the words are “scope of variables”.)
When the function has done its thing, it (the function itself) has the value of n, so you can use it wherever you can use a variable (or a constant).
Inside the function, you use fg in exactly the same way as any variable anywhere in a program, but it only exists inside the function. You can’t know anything about it outside.

So, when I wrote

followed by

what I was trying to tell you was to look at the 1480 in double current = calcIrms(1480); and work out how it uses that number to read 1480 samples inside calcIrms.
The answer is of course the parameter Number_of_Samples is assigned the value 1480 when you call the function, and so in the line
for (unsigned int n = 0; n < Number_of_Samples; n++)
Number_of_Samples is replaced by the number 1480, and it does the stuff inside the curly brackets 1480 times (from n = 0 to n = 1479), making n bigger by one (n++) each time.
Is it now clear what I did? I changed the 0 in ads.readADC_SingleEnded(0); into a variable so that I could vary it :dizzy_face:, and then made that variable a parameter in the function call so that I could feed the channel number into the function from outside.

And the rest was hopefully obvious (now, if not then).

You should have noticed I wrote above “When the function has done its thing, it (the function itself) has the value of n, so you can use it wherever you can use a variable (or a constant).”

Can you see anything wrong with writing this at the end of loop():

  Serial.print(calcIrms(1480, 0)); 
  Serial.print("  ");
  Serial.print(calcIrms(1480, 1)); 
  Serial.println(" "); 

The correct answer is NO, calcIrms(...)will get the samples and calculate the current, and return the value to Serial.print, which will print it. If you want the value again though, you can’t have it because it’s gone. That’s the only reason I created IrmsA & IrmsB because I reckoned you’d want the values again somewhere.

A function that doesn’t return a value is called a “void function” and it has to do something elsewhere to be useful. Quite often, functions that do something elsewhere, like writing to a disc or to EEPROM, do in fact return a value that will indicate success or failure.
You declare a void function with the keyword “void”, like this for setting values in emonLibDB:
void EmonLibDB_set_vInput(uint8_t input, double _amplitudeCal, double _phase) {... }
because there’s not a lot I can do even to mark an error - the emonTx4 can’t easily answer back and report it. You can’t use a void function like I suggested in Serial.print, it makes no sense (and the compiler would probably complain). It has to be a statement on its own.

1 Like

This could be part of my problem, the old brain just isn’t what it used to be.

Robert, thank you so much for the detailed explanation, I am going to study it again and try to understand exactly why it works. My son is here for the weekend so I will get back to this tomorrow evening. I referred to the Arduino reference section a few times to figure out some functions and terms etc… I’m going to search for a good online resource so I can learn much more about Arduino’s C++.

Thank you again!

Paul

1 Like

I think I might have beaten you to it, so I’m not buying that as an excuse :innocent:

Sorry, I forgot it, here’s the reading list:

My bible for C is of course “Kernigan & Ritchie” http://www.amazon.co.uk/C-Programming-Language-2nd/dp/0131103628/ref=sr_1_3?s=books&ie=UTF8&qid=1292502807&sr=1-3. This is the standard text book. The normal place I point people at who want to move up to C++ is C++ In Action: Industrial Strength Programming Techniques - Free Computer, Programming, Mathematics, Technical Books, Lecture Notes and Tutorials and that assumes you know C. Because the Arduino environment normally uses a very small subset of the language, neither are the best place for a beginner to start, nor are many of the other on-line tutorials. I’d still suggest you have both of those, and maybe Bruce Eckel’s “Thinking in C++”

http://engineering.nyu.edu/gk12/amps-cbri/pdf/ArduinoBooks/Arduino%20Programming%20Notebook.pdf does look to be a good starting place for a beginner, though I’ve not checked through it in detail. It does not go as far as classes and methods that are used here. For that, you need “Cpp In Action…” or one of the references below.

Learn C++ (Codecademy): Learn C++ | Codecademy

Object Oriented Programming with C++ - CodeProject (I’ve not been through this.)

O’Reilly Arduino Cookbook by Michael Margolis ISBN 978-1-449-31387-6 It’s pretty much ANSI C rather than C++ though.

Thank you for all those references, as I know NOTHING about C other than it’s the third letter of the alphabet and what I have picked up playing with Arduino in the last 3 years, I think this may be more my speed. Ha! :rofl:

I’ll review your recommendations and get ready to study this winter. Thanks Robert.

image