Tuesday, January 14, 2014

Wireless Whole House Power Monitor - Part 2 - Software

Here's the sketch for the Wireless Whole House Power Monitor.  About once per second, 1500 measurements are taken across four channels, connected to both AC line voltage phases, and two AC current clamps around the main conductors.  These readings are formatted into ASCII text and transmitted, for example:

"MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

Here's the sketch.  Note, this is before calibration, which I'll discuss in my next post.

Moteino_Main_Tx.ino


const char* NAME = "Moteino_Main_Tx ";
const char* VERSION = "0.7 ";
const char* DATE = "1/9/2014 ";

// Moteino_Main_Tx (using LowPowerLab RFM12b library)
// www.mikesmicromania.com
// LED on D9
// Phase 1 current sense to A1
// Phase 2 current sense to A2
// Phase 1 voltage sense to A3
// Phase 2 voltage sense to A4
// 60 Hz INT to D3 (INT1)
// Serial and Tx output @ 1Hz: ASCII string, e.g.
// "MAIN 1500 N 120.1 V1 3.2 A1 349.7 W1 119.5 V2 8.3 A2 919.3 W2"

#include <PString.h>   //arduiniana.org/libraries/PString/
#include <RFM12B.h>    //www.lowpowerlab.com

// cal factors for converting raw A2D values to volts, amps, watts
// initial default values map 1 to 1 output raw A2D values 0 to 1023
const float CFrawV1lo = 110.0; // raw A2D value
const float CFrawV1hi = 130.0;
const float CFcalV1lo = 110.0; // scaled Volts
const float CFcalV1hi = 130.0; 

const float CFrawV2lo = 110.0; // raw A2D value
const float CFrawV2hi = 130.0;
const float CFcalV2lo = 110.0; // scaled Volts
const float CFcalV2hi = 130.0; 

const float CFrawA1lo = 2.3;  // raw A2D value
const float CFrawA1hi = 9.7;
const float CFcalA1lo = 2.3;  // scaled Amps
const float CFcalA1hi = 9.7;  

const float CFrawA2lo = 2.3;  // raw A2D value
const float CFrawA2hi = 9.7; 
const float CFcalA2lo = 2.3;  // scaled Amps 
const float CFcalA2hi = 9.7;  

// radio settings
const int NODEID    =  3; // this node ID
const int GATEWAYID =  1; // send to node 1: Moteino Rx R-Pi
const int NETWORKID = 99; // Rx must be on same group to receive
const int myCS = 10;      // RFM12B chip select, SPI SS. 
                          // Pin 9 on my UNO shields. 10 on JeeNodes, Moteinos.

RFM12B radio;             // object, instance of class RFM12B 

// HW pin assignments
const int LED    = 9; // activity LED, 1 Hz
const int LEDBIT = 1; // activity LED, PORTB
const int A1SENS = 1; // phase 1 current: A1
const int A2SENS = 2; // phase 2 current: A2
const int V1SENS = 3; // phase 1 voltage: A3
const int V2SENS = 4; // phase 2 voltage: A4
const int HzSENS = 3; // IRQ  = D3 (Red wire from Q3 collector, 60Hz)

// global constants
const int HzINT  = 1; // INT1
const int MaxN  = 65; // max # readings (good down to 48 Hz)
const int NAVG = 30;

void setup () { 
  radio.Initialize(NODEID, RF12_433MHZ, NETWORKID, 0, 8, RF12_2v75, myCS);
  pinMode(HzSENS, INPUT);      // D3 is IRQ1, active low 
  digitalWrite(HzSENS, HIGH);  // enable internal pullup
  pinMode(LED, OUTPUT);        // D9 drives LED to ground
  digitalWrite(LED, LOW);      // LOW = off
  // speed up ADC to minimize phase error measuring voltage and current
  // arduino cookbook 2nd ed, pg 624
  bitClear(ADCSRA, ADPS0);
  bitClear(ADCSRA, ADPS1);
  bitSet(ADCSRA, ADPS2);  
  Serial.begin(115200);
  Serial.print(NAME);
  Serial.print(VERSION);
  Serial.println(DATE); 
}

volatile int unsigned measState; // state machine, clocked by line frequency

void loop () {
  //Serial.println("loop()");
  int V_1[MaxN], A_1[MaxN], V_2[MaxN], A_2[MaxN];  // waveform arrays
  int KAW_N = 0;
  float KAW_V1=0, KAW_A1=0, KAW_V2=0, KAW_A2=0, KAW_W1=0, KAW_W2=0;
  for(int i = 0; i < NAVG; i++){
    // acquire voltage and current samples
    int n = 0;
    if(i > 0)
      measState = 1; // 1 is partial half cycle
    else
      measState = 0; // first attachInterrupt take longer
    
    attachInterrupt(HzINT,AC60HZ,FALLING);
    while(measState <= 1);  // skip cycle 0 and 1
    //digitalWrite(LED,HIGH);
    //bitSet(PORTB, LEDBIT);
    while((measState <= 2) && (n < MaxN)) { // measure 1 cycle
      //digitalWrite(LED,HIGH);
      //bitSet(PORTB, LEDBIT);     // 0
      A_1[n] = analogRead(A1SENS); // 16.8 us
      V_1[n] = analogRead(V1SENS); // 16.8 us
      A_2[n] = analogRead(A2SENS); // 16.8 us
      V_2[n] = analogRead(V2SENS); // 16.8 us
      //digitalWrite(LED,LOW);
      //bitClear(PORTB, LEDBIT);   // 68 us
      n++;
      // time difference between voltage and current readings is about 20.4 us
      // 16.8 us is about 0.44 degrees at 60 Hz, good enough!
      // target 50 quad-readings over 1 cycle @ 60Hz
      // total reading time: 4 * 16.8 us * 50 = 3.4 ms
      // total dead time: (1/60) - 3.4 ms = 13.2666 ms
      // dead time between readings, for 50 readings: 
      // 13.2666 ms / 50 = 265 us
      delayMicroseconds(265); // fine tuned for 50 readings @ 60 Hz
    } 
    //digitalWrite(LED,LOW);
    //bitClear(PORTB, LEDBIT);
    detachInterrupt(HzINT);
    KAW_N += n; // number of readings
    // compute offset levels
    float V1Sum=0, A1Sum=0, V2Sum=0, A2Sum=0, W1Sum=0, W2Sum=0;
    for(int i=0; i<n; i++){
      V1Sum += V_1[i];
      A1Sum += A_1[i];  
      V2Sum += V_2[i];
      A2Sum += A_2[i];   
    }
    
    float aV1, aA1, aV2, aA2; // averages = DC mean values
    aV1 = V1Sum/n;
    aA1 = A1Sum/n;
    aV2 = V2Sum/n;
    aA2 = A2Sum/n;
    
    V1Sum = A1Sum = V2Sum = A2Sum = 0; // compute std.dev, AC RMS
    for(int i=0; i<n; i++){
      V1Sum += (V_1[i]-aV1)*(V_1[i]-aV1); // dV1^2
      A1Sum += (A_1[i]-aA1)*(A_1[i]-aA1); // dA1^2
      V2Sum += (V_2[i]-aV2)*(V_2[i]-aV2); // dV2^2
      A2Sum += (A_2[i]-aA2)*(A_2[i]-aA2); // dA2^2
      W1Sum += (V_1[i]-aV1)*(A_1[i]-aA1); // dV1*dA1
      W2Sum += (V_2[i]-aV2)*(A_2[i]-aA2); // dV2*dA2
    }
     
    KAW_V1 += sqrt(V1Sum/n);  // root(mean(V1^2))
    KAW_A1 += sqrt(A1Sum/n);  // root(mean(A1^2))
    KAW_V2 += sqrt(V2Sum/n);  // root(mean(V2^2))
    KAW_A2 += sqrt(A2Sum/n);  // root(mean(A2^2))
    KAW_W1 += W1Sum/n;        // mean(sum(dV1[i]*dA1[i]))
    KAW_W2 += W2Sum/n;        // mean(sum(dV2[i]*dA2[i]))
  }
  
  KAW_V1 /= NAVG; // average all measurements
  KAW_A1 /= NAVG;
  KAW_V2 /= NAVG;
  KAW_A2 /= NAVG;
  KAW_W1 /= NAVG;
  KAW_W2 /= NAVG;
  
// apply cal factors
  // voltage
  KAW_V1 = map(KAW_V1, CFrawV1lo, CFrawV1hi, CFcalV1lo, CFcalV1hi); 
  KAW_V2 = map(KAW_V2, CFrawV2lo, CFrawV2hi, CFcalV2lo, CFcalV2hi); 
  
  // current
  KAW_A1 = map(KAW_A1, CFrawA1lo, CFrawA1hi, CFcalA1lo, CFcalA1hi);  
  KAW_A2 = map(KAW_A2, CFrawA2lo, CFrawA2hi, CFcalA2lo, CFcalA2hi);  
    
  // power
  KAW_W1 = map(KAW_W1, CFrawV1lo*CFrawA1lo, CFrawV1hi*CFrawA1hi, CFcalV1lo*CFcalA1lo, CFcalV1hi*CFcalA1hi);
  KAW_W2 = map(KAW_W2, CFrawV2lo*CFrawA2lo, CFrawV2hi*CFrawA2hi, CFcalV2lo*CFcalA2lo, CFcalV2hi*CFcalA2hi);
  
  // prevent negative values (e.g. no load current, plus noise)
  KAW_A1 = max(KAW_A1, 0);
  KAW_W1 = max(KAW_W1, 0);  
  KAW_A2 = max(KAW_A2, 0);
  KAW_W2 = max(KAW_W2, 0);  
  
  // format Tx string, e,g.
  // "Main 1500 N 120.1 V1 3.0 A1 300 W1 120.1 V2 3.0 A2 300 W2"
  char buffer[67];
  PString str(buffer, sizeof(buffer));
  str.print("Main ");
  str.print(KAW_N);
  str.print(" N ");
  str.print(KAW_V1);
  str.print(" V1 ");
  str.print(KAW_A1);
  str.print(" A1 ");
  str.print(KAW_W1);
  str.print(" W1 ");
  str.print(KAW_V2);
  str.print(" V2 ");
  str.print(KAW_A2);
  str.print(" A2 ");
  str.print(KAW_W2);
  str.print(" W2 ");
  
  //Serial.println(str);
  radio.Send(GATEWAYID, str, str.length());
  
  // flash LED so we know we're alive
  //digitalWrite(LED,HIGH);
  bitSet(PORTB, LEDBIT);   
  delay(10);
  bitClear(PORTB, LEDBIT); 
  //digitalWrite(LED,LOW);  // save power
}

void AC60HZ(){ 
  //digitalWrite(LED,HIGH);
  //bitSet(PORTB, LEDBIT);
  measState++;
  //Serial.print("measState:");
  //Serial.println(measState);
  //digitalWrite(LED,LOW);
  //bitClear(PORTB, LEDBIT);
}

// overload Arduino integer map() function to support floats, doubles
double map(double x, double in_min, double in_max, double out_min, double out_max)
{
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}