We have launched a new Community! Please post any new forum topics there. This forum will remain available as an archive, but we hope you enjoy the new experience and feature set the latest NCD Community will offer!

Sample Particle Code for Current Monitor Board

I'm sharing my "Starter" Code for an Offboard 4-Channel Current Monitor Board from CE.

It includes comments on how to create a WebHook, log data to ThingSpeak.com, Notifications from IFTTT.com to your cell phone,  and remote functions to change the publish delay and reset your Particle device using Mobicle.io

At a minimum, You will need to update the API Write Key from YOUR ThingSpeak.com channel, and create a Webhook before you Flash this code:

This Code will Log the Current-Monitor Board's Channel #1 Amp Reading to your ThingSpeak.com Field #1,  Channel 2 Amps to ThingSpeak Field #2, etc.  

Code will send a Particle.Publish each time a pump changes run state (On/Off) for IFTTT notifications. 

Code will send update to ThinkSpeak every 20 minutes if no events have occured in the previous 20 minutes. 

I'm not a programmer, so please feel free to give any advice. 

    (Save and open in NotePad, turn WordWrap OFF before you copy/paste into Particle Web IDE)

ryan fontaine's picture

[reserved]

 

T

Thank you so much for sharing Ryan!

ryan fontaine's picture

Here is a screenshot of a graph that loads the data from ThingSpeak:

 

You can view real-time data for a Water Treatment Plant here :

http://www.mississippi.engineering/Decatur/Graph.html

 

The Source Code for the Graph webpage is here:

You will need to change channelNumber to your ThingSpeak Channel #.   Open Graph.txt in notepad and look for this:

IF your ThinkSpeak Channel is set to PRIVATE, you will also need to change the API Read Key. 

Save the file as Graph.html   

The webpage can be saved locally.  It doesn't have to be hosted on a webserver, because Graph.html pulls the data from ThingSpeak.com.

 

T

How has your experience been with ThingSpeak?  I've heard of it but never actually tried it out before.  Looks like a perfect solution for this applicaiton.

ryan fontaine's picture

I love ThingSpeak.   Create an account and a channel to try it out.  

I started out using the ThingSpeak Library, but the data usage was too high.  I cant explain why, but my data usage went way down by using a straight particle.publish and a webhook  (as my sample code in this thread).  

I would love some suggestions on the code.  I'm a Civil & Structural Engineer, not a EE or programmer.  

 

W

Ryan,

I had your code working yesterday. Today added relay code to firmware. Relays are working but the current is not publishing. Wondering if you could take a look and see if I'm missing some thing thanks.

 

// The Basis of this Code was take from:  https://github.com/ControlEverythingCommunity/PECMAC/blob/master/Particle/PECMAC125A.ino
// By Ryan Fontaine, P.E.

/* 
1.  Purchase CT Board from ControlEverything.com -   https://shop.controleverything.com/collections/energy-monitoring/products/4-channel-off-board-ac-current-monitor-for-particle-electron

2.  Create a free account at www.ThingSpeak.com

3. Create a Webhook manually at  https://console.particle.io/integrations/webhooks/create
    Click [CUSTOM JSON] at the Top Right
    Copy/Paste the following into the JSON window:
   
                        {
                            "event": "TEST",
                            "url": "https://api.thingspeak.com/update",
                            "requestType": "POST",
                            "form": {
                                "api_key": "{{k}}",
                                "field1": "{{1}}",
                                "field2": "{{2}}",
                                "field3": "{{3}}",
                                "field4": "{{4}}",
                                "field5": "{{5}}",
                                "field6": "{{6}}",
                                "field7": "{{7}}",
                                "field8": "{{8}}",
                                "lat": "{{a}}",
                                "long": "{{o}}",
                                "elevation": "{{e}}",
                                "status": "{{s}}"
                            },
                            "mydevices": true,
                            "noDefaults": true
                        }
                       

    You will change the "event" in the first line of the JSON to whatever event name you wish:
            The Example names the WebHook's event: "TEST",
    So you would change the CODE (for Photon or Electron) below under [Specific Values for each Installation] to :
            const char * eventName = "TEST"
    To match the WebHook event name.

*OR* Follow these instructions :   https://www.hackster.io/15223/thingspeak-particle-photon-using-webhooks-dbd96c

4.  Change the other values below,  under [Specific Values for each Installation] , per your requirements

5.  Flash Firmware to Photon/Electron

6.  If you want Notifications on your cell phone when a Pump Starts/Stops, then Create an IFTTT.com account. 
    The format is
    If THIS : type Particle
    for TRIGGER choose : New Event Published
    If (Event Name) : type PUMP
    is (Event Contents) : *Leave This BLANK*
    Device Name or ID : Choose your Particle Device from the DropDown list
    Click CREATE TRIGGER
    CLick THAT
    Search for NOTIFICATIONS  or SMS  (note: you can have unlimited notifications per month, but only 100 SMS text messages per month)  Notifications Recommended , & download the IFTTT app to your phone.
    Click CREATE TRIGGER. 

7.  Use Mobicle.IO to change the Publish Delay "On-The-Fly" without needing to Flash Firmware (see bottom of CODE).  Very Useful to "fine-tune" the data usage of ELECTRONS that are installed remotely.

8.  Use Mobicle.IO to remotely RESET the Particle Device if needed (see bottom of CODE)


*/

 

//      [ Specific Values for each Installation ]
//////////////////////////////////////////////////////
const char * eventName = "TEST";                    //  This must match the name of the event you chose for the WebHook in Step #3
int publish_delay =          30000;                 //  Multiply # of Seconds * 1000. This is how often the AMPS will be updated on ThingSpeak.com while a pump is running. Very Important to data usage for an ELECTRON. Start with 180000 after initial bench testing..
int minCurrent =                20;                 //  MUST MULTIPLY BY A FACTOR OF 10.  Example: using 20 will eleminate any AMP Reading that is less than 2 AMPS to avoid noise on the Graph.
int spikeCurrent =             50000;                 //  MUST MULTIPLY BY A FACTOR OF 10.  Example: using 500 will eleminate any AMP Reading that is OVER 50 AMPS. This doesn't show Startup SPIKES for large Inductive Loads, true running amps will be read 10 seconds later.
                                                    //  If you dont want the minCurrent and spikeCurrent Functionality, set to 1 and 1500 respectively
//ThingSpeak Channel Info                           // 
const char * myWriteAPIKey = "XXXXXXXXXXXXXXXX";    //  From your ThinkSpeak Account Info (API KEYS tab)
//////////////////////////////////////////////////////

// There is no need to change anything else if you are just getting started.


#include "NCD4Relay.h"
#include  <spark_wiring_i2c.h>
#include <application.h>
#define AddrCurrent 0x2A    // I²C  Address for Current Monitor
#define AddrRelay 0x20      // Address for Relay
#define CT_delay 10000      //  10,000 = 10 seconds.  The CT Current Monitor Data Sheet suggests only reading the Board every 8-10 seconds

 

NCD4Relay relayController;

SYSTEM_MODE(AUTOMATIC);

int triggerRelay(String command);

bool tripped[4];

int debugTrips[4];

int minTrips = 5;

SYSTEM_THREAD(ENABLED);  

char msg[256];

unsigned long now = millis();           // Used as Time to compare the Ellapsed time for various readings
unsigned int lastPublish = 0;           // Previous Publish to ThingSpeak
unsigned long nowCT = millis();         // Latest CT Current-Monitor Reading
unsigned int lastCT = millis();         // Previous CT Current-Monitor Reading

//  Variables Req'd for I²C  CT Current-Monitor Board
int msb1 = 0, msb = 0, lsb = 0;
unsigned int data[36]; 
int typeOfSensor = 0;
int maxCurrent = 0;
int noOfChannel = 0;
double current = 0;
double amps = 0;

//  These 4 Floats will hold the values to send to ThinkSpeak.com for data logging to the Cloud :
float CH1, CH2, CH3, CH4;      // Pump/Motor AMPS   


// Arrays
static int oldVal[5] = {0,0,0,0,0}; // Previous Sensor Values to Compare if Run State has Changed - equality checks are safer with Integers, so we will multiply by 10 for all sensor values, But divide by 10 before publishing each value
static int nowVal[5] = {0,0,0,0,0}; // Most Recent Sensor Values * 10   
static int Flag[3] = {0,0,0};       // A "0" Value means nothing to update.  Flag[1] = 1 requests to Publish AMPS after the Publish Delay, Flag[2] = 1 requests an immediate Publish (no Delay - used for Pump Start/Stop Events)
      

//  Remote Reset Function, used to RE-SET the Photon/Electron using www.Mobicle.io
#define DELAY_BEFORE_REBOOT 2000
unsigned int rebootDelayMillis = DELAY_BEFORE_REBOOT;
unsigned long rebootSync = millis();
bool resetFlag = false;


void setup() {
   
  // Register Particle Function to allow user to remotely Change the Publish Delay.  
  Particle.function("setDelay", setPublishDelay);
  Particle.function("controlRelay", triggerRelay);
    relayController.setAddress(0,0,0);

  // Set I²C  Current Monitor Board  variable
  Particle.variable("i2cdevice", "PECMAC125A");
  Particle.variable("typeOfSensor", typeOfSensor);
  Particle.variable("maxCurrent", maxCurrent);
  Particle.variable("noOfChannel", noOfChannel);

//  Remote Reset Function   
  Particle.function("reset",cloudResetFunction);

  // I²C  setup for the CT Current-Monitor Motherboard
  // Initialise I²C  communication as MASTER
  Wire.begin();
  // Initialise Serial Communication, set baud rate = 9600
  Serial.begin(9600);

  // Setup the Current Monitor
  // Start I²C  transmission
  Wire.beginTransmission(AddrCurrent);
  // Command header byte-1
  Wire.write(0x92);
  // Command header byte-2
  Wire.write(0x6A);
  // Command 2 is used to read no of sensor type, Max current, No. of channel
  Wire.write(0x02);
  // Reserved
  Wire.write(0x00);
  // Reserved
  Wire.write(0x00);
  // Reserved
  Wire.write(0x00);
  // Reserved
  Wire.write(0x00);
  // CheckSum
  Wire.write(0xFE);
  // Stop I²C  transmission
  Wire.endTransmission();
  // Request 6 bytes of data
   Wire.requestFrom(AddrCurrent, 6);
  // Read 6 bytes of data
  if (Wire.available() == 6){
    data[0] = Wire.read();
    data[1] = Wire.read();
    data[2] = Wire.read();
    data[3] = Wire.read();
    data[4] = Wire.read();
    data[5] = Wire.read();
   }
  typeOfSensor = data[0];
  maxCurrent = data[1];
  noOfChannel = data[2];
  delay(200); 
}

void loop() {
   
   
     int status = relayController.readAllInputs();
    int a = 0;
    for(int i = 1; i < 9; i*=2){
        if(status & i){
            debugTrips[a]++;
            if(debugTrips[a] >= minTrips){
                if(!tripped[a]){
                    tripped[a] = true;
                    //set input trip event to true
                    String eventName = "Input_";
                    eventName+=(a+1);
                    Particle.publish(eventName, "ON");
                    Serial.print("eventName: ");
                    Serial.println(eventName);
                    Serial.print("eventContents: ");
                    Serial.println("ON");
                }
            }
        }else{
            debugTrips[a] = 0;
            if(tripped[a]){
                tripped[a] = false;
                //set input trip event to false
                String eventName = "Input_";
                eventName+=(a+1);
                Particle.publish(eventName, "OFF");
                Serial.print("eventName: ");
                Serial.println(eventName);
                Serial.print("eventContents: ");
                Serial.println("OFF");
            }
        }
        a++;
    }
}

int triggerRelay(String command){
    if(command.equalsIgnoreCase("turnonallrelays")){
        relayController.turnOnAllRelays();
        return 1;
    }
    if(command.equalsIgnoreCase("turnoffallrelays")){
        relayController.turnOffAllRelays();
        return 1;
    }
    if(command.startsWith("setBankStatus:")){
        int status = command.substring(14).toInt();
        if(status < 0 || status > 255){
            return 0;
        }
        Serial.print("Setting bank status to: ");
        Serial.println(status);
        relayController.setBankStatus(status);
        Serial.println("done");
        return 1;
    }
    //Relay Specific Command
    int relayNumber = command.substring(0,1).toInt();
    Serial.print("relayNumber: ");
    Serial.println(relayNumber);
    String relayCommand = command.substring(1);
    Serial.print("relayCommand:");
    Serial.print(relayCommand);
    Serial.println(".");
    if(relayCommand.equalsIgnoreCase("on")){
        Serial.println("Turning on relay");
        relayController.turnOnRelay(relayNumber);
        Serial.println("returning");
        return 1;
    }
    if(relayCommand.equalsIgnoreCase("off")){
        relayController.turnOffRelay(relayNumber);
        return 1;
    }
    if(relayCommand.equalsIgnoreCase("toggle")){
        relayController.toggleRelay(relayNumber);
        return 1;
    }
    if(relayCommand.equalsIgnoreCase("momentary")){
        relayController.turnOnRelay(relayNumber);
        delay(300);
        relayController.turnOffRelay(relayNumber);
        return 1;
    }
    return 0;
   
   
//  Read all channels of Current-Monitor   //  The CT's measure the AMPS on each channel.  # of Channels depends on the Actual Current Monitor Board Purchased.  
//  This code works for up to 4 Channels, but you can easily use a 6 channel CT Board and increase the oldVal and nowVal Arrays and add the CH's for the Particle.Publish lines. 
nowCT = millis();

if (abs(nowCT - lastCT) > CT_delay)  {    // The intention is to only read the Current Monitor every 10 seconds, per the Specs. 
    for (int j = 1; j < noOfChannel + 1; j++) {
    // Start I²C  Transmission
    Wire.beginTransmission(AddrCurrent);
    // Command header byte-1
    Wire.write(0x92);
    // Command header byte-2
    Wire.write(0x6A);
    // Command 1
    Wire.write(0x01);
    // Start Channel No.
    Wire.write(j);
    // End Channel No.
    Wire.write(j);
    // Reserved
    Wire.write(0x00);
    // Reserved
    Wire.write(0x00);
    // CheckSum
    Wire.write((0x92 + 0x6A + 0x01 + j + j + 0x00 + 0x00) & 0xFF);
    // Stop I²C  Transmission
    Wire.endTransmission();
    delay(1000);
    // Request 3 bytes of data
    Wire.requestFrom(AddrCurrent, 3);
    // Read 3 bytes of data
    // msb1, msb, lsb
    msb1 = Wire.read();
    msb = Wire.read();
    lsb = Wire.read();
    current = (msb1 * 65536) + (msb * 256) + lsb;
    // Convert the data to ampere
    amps = current / 1000;
   
    // the For Loop cycles through each Channel of the CT Board.  Store each AMP reading next.
    nowVal[j] = round(amps * 800) ;  // Multiply the AMPS * 10 and store the value in the Array for that particular Channel (CH1 - CH4).  This converts the AMP reading to an integer, we will "recover" the decimal place later.

    if (nowVal[j]  > spikeCurrent){ //  Want to eleminate the SPIKE during Startup.  The CT will recognize the TRUE Running Amps 10 seconds (CT_Delay) later during the next read and store that value instead. 
        nowVal[j] = 0;              // set to 0 and read the True Running Amps on the Next read in 10 seconds.
    }
    if (nowVal[j]  < minCurrent) {  //The CT's may bounch around 0-2 amps during "No-Load". 
        nowVal[j] = 0;              //  Dont want to send 0.XX - 2.00 Amps to ThingSpeak, so delete these low values, or "noise".
       
        if (oldVal[j]  > minCurrent) {   // If the previous AMP reading showed the Pump to be Running, but Now it is NOT Running- So the PUMP just "STOPPED".
            Particle.publish("PUMP", "Pump # " + String(j) + " Stopped"   , PRIVATE);   // Comment Out this line if you do Not Want Start/Stop Notifications.  Use IFTTT.com if you do. 
            Flag[2]  = 1;           // Will REQUEST an immediate full publish - with no publish Delay Check for Flag[2] == 1. 
        }
    }   
    //  Same as above, but checking to see if the Pump JUST "STARTED".
    if (nowVal[j]  > minCurrent) {  //The CT's bounch around 0 amps.  Dont want to send 0.XX - 2.00 Amps to ThingSpeak
        Flag[1]  = 1;               // Flag[1] is used for ANY CT measurement above the minimum threshold - Requests a Full Publish after the Publish Delay Is Met.  This is to log normal runtime AMPS at the interval publish_delay.
       
            if (oldVal[j]  < minCurrent) { // a PUMP is running now but wasn't on previous check, then it just "STARTED".
                Particle.publish("PUMP", "Pump # " + String(j) + " Started"   , PRIVATE);   // Comment Out this line if you do Not Want Start/Stop Notifications.  Use IFTTT.com if you do.
                Flag[2]  = 1;       // Will REQUEST an immediate full publish - with no publish Delay Check for Flag[2] == 1
            }
    }   
    oldVal[j] =  nowVal[j];         // Set the Old Value to the most recent Value for the Next Comparison to determine if the Pump is Starting or Stopping.
    }
    lastCT = nowCT;                 // Since we've sucessfully read all the Channels of the CT Current Monitor Board, update the TIME for the Last Read.  We want to wait 10 seconds before another CT Current Monitor Board Read. 
}

//  ALL READINGS ARE COMPLETE, SO UPDATE THE CHANNEL Float Values used for logging data to www.THINGSPEAK.COM
CH1 = (nowVal[1] / 10.0);           //  Divide each Integer Value by 10 to recover the 1 Decimal Place precision
CH2 = (nowVal[2] / 10.0);
CH3 = (nowVal[3] / 10.0);
CH4 = (nowVal[4] / 10.0);

// Check Flag[2];  requests a publish when a pump has started/stopped.  No Publish Delay (other than ThingSpeak.com 15 sec).  Allows the "normal" publish delay to be longer for updating the AMPS during a Pump Run.
if (Flag[2] == 1) {               
    now = millis();
    if (abs(now - lastPublish) > 30000) {  //  15 Seconds is the fastest Update Rate for a ThingSpeak Channel.
   
    //  Build the Particle Publish String
    //  The next 3 lines are the same as this:   snprintf(msg, sizeof(msg), "{\"1\":\"%.1f\",\"2\":\"%.1f\",\"3\":\"%.1f\",\"4\":\"%.1f\",\"k\":\"%s\"}", CH1, CH2, CH3, CH4, myWriteAPIKey);   just formmatted for easier reading.
        snprintf(msg, sizeof(msg),
        "{\"1\":\"%.1f\"    ,   \"2\":\"%.1f\"  ,   \"3\":\"%.1f\"  ,   \"4\":\"%.1f\"  ,   \"k\":\"%s\"}",
                CH1         ,       CH2         ,       CH3         ,        CH4        ,   myWriteAPIKey);   
   
//  Particle.publish(eventName, msg, PRIVATE);              // Publish with Acknowledgement
    Particle.publish(eventName, msg, PRIVATE, NO_ACK);      // Publish with NO Acknowledgement to save Electron Data
   
    lastPublish = now;
    Flag[1] = 0;                    // Reset Flag- just published.
    Flag[2] = 0;                    // Reset Flag- just published.
    }
}

// Check Flag[1],  this is the normal publish during runtime
if (Flag[1] == 1) {
    now = millis();
    if (abs(now - lastPublish) > publish_delay) {  //  Wont publish more often than publish_delay. 
       
        snprintf(msg, sizeof(msg),
        "{\"1\":\"%.1f\"    ,   \"2\":\"%.1f\"  ,   \"3\":\"%.1f\"  ,   \"4\":\"%.1f\"  ,   \"k\":\"%s\"}",
                CH1         ,       CH2         ,       CH3         ,        CH4        ,   myWriteAPIKey);
   
     // Particle.publish(eventName, msg, PRIVATE);          // Publish with Acknowledgement
        Particle.publish(eventName, msg, PRIVATE, NO_ACK);  // Publish with NO Acknowledgement to save Electron Data
      
        lastPublish = now;
        Flag[1] = 0;                // Reset Flag- just published.
        Flag[2] = 0;                // Reset Flag- just published.
    }
}

// FAILSAFE every 20 MINUTES                                   
// The ELECTRON will Handshake the Cellular Tower every 22-23 minutes if no data has been sent, which uses 70+ Bytes of Data Overhead, So we will publish actual Data every 20 minutes and not waste Cellular data with the empty "Ping" every 22 minutes.
now = millis();
if (abs(now - lastPublish) > 1200000) {
   
        snprintf(msg, sizeof(msg),
        "{\"1\":\"%.1f\"    ,   \"2\":\"%.1f\"  ,   \"3\":\"%.1f\"  ,   \"4\":\"%.1f\"  ,   \"k\":\"%s\"}",
                CH1         ,       CH2         ,       CH3         ,        CH4        ,   myWriteAPIKey);

    Particle.publish(eventName, msg, PRIVATE);          // Publish with Acknowledgement since this is a 20 minute failsafe "Check-in"
   
    lastPublish = now;  
    Flag[1] = 0;  // Reset Flag- just published.       
    Flag[2] = 0;  // Reset Flag- just published.
}

//  Remote Reset Function, Use Mobicle.IO to remotely RE-SET the Photon/Electron easily.  Create a Button that Sends TRUE argument to the Function
    if ((resetFlag) && (millis() - rebootSync >=  rebootDelayMillis)) {
        Particle.publish("Debug","Remote Reset Initiated",300,PRIVATE);
        System.reset();
    }
}  // End LOOP

 

//  Remote Reset Function
int cloudResetFunction(String command) {
       resetFlag = true;
       rebootSync=millis();
       return 0;           
}

 

//Funciton to change the Publish Delay remotely, without needing to reflash the Firmware.  Use Mobicle.IO
int setPublishDelay(String command) {
//Particle.functions  take a string as an argument
publish_delay = ( command.toInt() * 60000 );
// Use Mobicle.Io
// Select Device, then Create a Button to send the # of MINUTES as an Argument to the setPublishDelay FUNCTION. 
// Create as Many buttons as you need, or you can type the Argument direcly into the Function.
// Value must be an Integer....1,2,3, etc in Minutes.
}

 

 

ryan fontaine's picture

The first problem is your Loop() is closed 37 lines after it starts.  That means you only have 37 lines of code that gets repeated. 

Double Click the "{"  on the  line

 void loop() {

That would originally highlight all my sample code until you get to the line:

}  // End LOOP

But your pasted relay code has closed the Loop } too soon.   It appears you have pasted a FUNCTION for the Relay that needs to go after the main Loop(), not inside Loop().

I'm very new to programming, so anybody else will be able to better explain this but I'll try.

Definitions, Global Variables, etc are placed in the Code BEFORE Setup().

Anything that you want to run only once is placed inside Setup().

Any code that you want repeated is placed inside Loop().

Any functions that are called by Loop() are seperate little "mini" programs.  They must be placed after the Loop() is closed. 

My sample code has two functions.  They are both after the main loop is Closed.  I think functions could go Before the loop also, but it appears everybody places at the end of the code. 

}  // End LOOP


int cloudResetFunction(String command) {
       resetFlag = true;
       rebootSync=millis();
       return 0;           
}

int setPublishDelay(String command) {
publish_delay = ( command.toInt() * 60000 );
}

 

Take a look at your original Relay Code by itself (or the original sample code).  Find where the Loop is terminated (doubleclick the "{" like before).

When you copy/paste the Relay code into another program, just put the Relay code in the same places it originates from (Before Setup, Inside Setup, Inside Loop, or After Loop). 

That will get you much closer.   Your Relay functions should be placed at the bottom of my sample code. 

Any click VERIFY everytime after you paste new code.  It's much easier for a newbie like me to only debug one change at a time.  

 

 

 

Kristian Jenset's picture

Hi Ryan

Thanx you for sharing you code and html. I got it all working. But I have som questions;

-It is possible, and easy for you, to also additionally add an webhook til ubidots.com in your code?

-If I want to use the 12-channel board do I need to do much editing of the code?

 

Regards

Kristian