Watt Eye has a simple purpose - monitor pulses that comes from the home electric meter, measure the interval between the pulses and compute the real-time energy being consumed, broadcast that onto the network using UDP packets so that CouchCalendar has something to do and display, and publish the data to a web server, where it can be used (graphed or placed into a db).

Dependencies:   IniManager mbed HTTPClient SWUpdate StatisticQueue mbed-rtos NTPClient Watchdog SW_HTTPServer EthernetInterface TimeInterface

Features:

  • Reads the time between pulses (which the home electric meter emits as IR for each Watt consumed).
  • Once every 5 seconds, it broadcasts this via UDP to the network, so other nodes can listen to this real-time data.
  • Once every 5 minutes, it posts statistics to a web server for logging.
  • Once a day, it checks the web server to see if there is a SW update (and if so it downloads, installs, and activates it).
  • It syncs to a configured NTP server, but doesn't actually use this information for anything.
  • It hosts a web server, but this is not being used at this time.

So, this is a rather expensive piece of hardware to monitor a single pulse, and yet it is easy to imagine enhancing this:

  • Read the water meter in a similar manner.
  • Read the gas meter in a similar manner.

And even then, there will be many left-over port pins for other uses.

Revision:
0:a4887b672ac6
Child:
1:04ab0a3d07f1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.cpp	Sat Jul 26 19:51:33 2014 +0000
@@ -0,0 +1,526 @@
+#include "mbed.h"           // v83, RTOS 38
+#include "RawSerial.h"      // ?
+
+// My libs
+#include "TimeInterface.h"      // ver 3
+#include "HTTPClient.h"         // ver 0
+#include "IniManager.h"         // ver 9
+#include "SWUpdate.h"           // ver 17
+#include "Watchdog.h"           // ver 2
+#include "StatisticQueue.h"
+
+//#define WIFLY
+#define HW_ADAPTER SMART_BOARD  /* Which board are we compiling against? */
+
+#ifdef WIFLY
+#include "WiflyInterface.h"
+#else
+#include "EthernetInterface.h"  // ver 41
+#endif
+#include "EthStatus.h"
+
+#include "SW_HTTPServer.h"
+
+extern "C" void mbed_reset();
+
+//#define DEBUG "MAIN"
+#include <cstdio>
+#if (defined(DEBUG) && !defined(TARGET_LPC11U24))
+#define DBG(x, ...)  std::printf("[DBG %s %3d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
+#define WARN(x, ...) std::printf("[WRN %s %3d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
+#define ERR(x, ...)  std::printf("[ERR %s %3d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
+#define INFO(x, ...) std::printf("[INF %s %3d] "x"\r\n", DEBUG, __LINE__, ##__VA_ARGS__);
+#else
+#define DBG(x, ...)
+#define WARN(x, ...)
+#define ERR(x, ...)
+#define INFO(x, ...)
+#endif
+
+#define TIME_TO_CHECK_SW_UPDATE (60*60) /* once per hour */
+
+EthernetInterface eth;
+Mutex eth_mutex;
+
+Watchdog wd;
+
+RawSerial pc(USBTX, USBRX);
+LocalFileSystem local("local");
+INI ini;
+
+DigitalOut linkup(p26);
+DigitalOut linkdata(p25);
+
+TimeInterface ntp;
+HTTPClient http;
+
+// Keep a sample every 5 s for 5 minutes
+// 12 samples / min * 5 min => 60 samples
+#define SampleInterval_Sec 5
+#define SampleHistory_5m (60)
+StatisticQueue stats5s(SampleHistory_5m);
+
+// Keep 5 minute data for 1 day
+// 12 samples / hour * 24 hours => 288
+#define SampleInterval_Min 5
+#define SampleHistory_1d 288
+StatisticQueue stats5m(SampleHistory_1d);
+
+const char * PROG_INFO = "Watt Eye: " __DATE__ ", " __TIME__;
+const char * iniFile = "/local/WattEye.ini";
+
+
+DigitalOut PulseIndicator(LED1);
+DigitalOut UDPSendIndicator(LED2);
+DigitalOut URLSendIndicator(LED3);
+PwmOut signOfLife(LED4);
+
+InterruptIn event(p15);
+Timer timer;
+Timeout flash;
+
+typedef struct
+{
+    time_t todClock;
+    uint32_t tLastStart;
+    uint32_t tLastRise;
+    uint16_t Samples10s[30];    // Every 10s for 5 min
+    uint16_t Samples10sIndex;
+    uint16_t Samples5m[12*24];  // Every 5m for 1 day
+    uint16_t Samples5mIndex;
+    uint16_t Samples1d[365];    // Every 
+    uint16_t Samples1dIndex;
+} WattData;
+
+typedef struct
+{
+    float instantKW;
+    float averageKW;
+    uint32_t measuredCycles;
+} Atomic_t;
+
+Atomic_t PowerSnapshot;
+
+typedef struct
+{
+    bool init;
+    time_t startTimestamp;
+    uint64_t tStart;
+    uint64_t tLastRise;
+    uint64_t tStartSample;
+    uint32_t cycles;
+} RawSample_t;
+
+RawSample_t RawPowerSample;
+
+//uint64_t tElapsedFive;
+//uint32_t cycleFive;
+
+
+
+void SoftwareUpdateCheck(bool force = false)
+{
+    static time_t tLastCheck;
+    char url[100], name[10];
+    time_t tCheck = ntp.time();
+    
+    if (tCheck < tLastCheck)
+        force = true;  // guard against bad stuff that would prevent updates
+    
+    if ((tCheck - tLastCheck > TIME_TO_CHECK_SW_UPDATE) || force) {
+        tLastCheck = tCheck;
+        eth_mutex.lock();
+        pc.printf("SoftwareUpdateCheck\r\n");
+        if (ini.ReadString("SWUpdate", "url",   url, sizeof(url))
+        &&  ini.ReadString("SWUpdate", "name", name, sizeof(name))) {
+            //pc.printf("SW Check(%s,%s)\r\n", url, name);
+            SWUpdate_T su = SoftwareUpdate(url, name, DEFER_REBOOT);
+            if (SWUP_OK == su) {
+                eth_mutex.unlock();
+                pc.printf("  new software installed, restarting...\r\n");
+                Thread::wait(3000);
+                mbed_reset();
+            } else if (SWUP_SAME_VER == su) {
+                pc.printf("  no update available.\r\n");
+            } else {
+                pc.printf("  update failed %04X, http %d\r\n", su, SoftwareUpdateGetHTTPErrorCode());
+            }
+        } else {
+            pc.printf("  can't get info from ini file.\r\n");
+            eth_mutex.unlock();
+        }
+    }
+}
+
+void ShowIPAddress(bool show = true)
+{
+    char buf[16];
+    
+    if (show)
+        sprintf(buf, "%15s", eth.getIPAddress());
+    else
+        sprintf(buf, "%15s", "---.---.---.---");
+    pc.printf("Ethernet connected as %s\r\n", buf);
+}
+
+
+
+bool SyncToNTPServer(void)
+{
+    char url[100];
+    char tzone[10];
+    
+    if (ini.ReadString("Clock", "timeserver", url, sizeof(url))) {
+        ini.ReadString("Clock", "tzoffsetmin", tzone, sizeof(tzone), "0");
+        
+        time_t tls = ntp.get_timelastset();
+        //time_t tnow = ntp.time();
+        //int32_t tcr = ntp.get_cal();
+        eth_mutex.lock();
+        pc.printf("NTP update time from (%s)\r\n", url);
+        linkdata = true;
+        int32_t tzo_min = atoi(tzone);
+        ntp.set_tzo_min(tzo_min);
+        int res = ntp.setTime(url);
+        eth_mutex.unlock();
+        linkdata = false;
+        if (res == 0) {
+            time_t ctTime;
+            ctTime = ntp.timelocal();
+            pc.printf("   Time set to (UTC): %s\r\n", ntp.ctime(&ctTime));
+            return true;
+        } else {
+            pc.printf("Error %d\r\n", res);
+        }
+    } else {
+        pc.printf("no time server was set\r\n");
+    }
+    return false;
+}
+
+void TransmitEnergy(bool sendNow, float iKW, float min5s, float avg5s, float max5s, float min5m, float avg5m, float max5m)
+{
+    char url[100], dest[20], port[8];
+    char data[150];
+    char myID[50];
+    char fullurl[250];
+    bool bEU = ini.ReadString("Energy", "url",  url,  sizeof(url));
+    bool bDS = ini.ReadString("Energy", "dest", dest, sizeof(dest));
+    bool bPO = ini.ReadString("Energy", "port", port, sizeof(port));
+    bool bID = ini.ReadString("Node",   "id",   myID, sizeof(myID));
+    
+    if (bEU && bDS && bPO && bID) {
+        snprintf(data, 150, "ID=%s&iKW=%5.3f&min5s=%5.3f&avg5s=%5.3f&max5s=%5.3f&min5m=%5.3f&avg5m=%5.3f&max5m=%5.3f",
+            myID, iKW, min5s, avg5s, max5s, min5m, avg5m, max5m);
+        eth_mutex.lock();
+        // Send the UDP Broadcast, picked up by a listener
+        UDPSendIndicator = true;
+        UDPSocket bcast;
+        Endpoint ep;
+        int h = ep.set_address(dest, atoi(port));
+        int i = bcast.bind(atoi(port));
+        int j = bcast.set_broadcasting(true);
+        int k = bcast.sendTo(ep, data, strlen(data));
+        bcast.close();
+        UDPSendIndicator = false;
+        // On the 5-minute interval, post the data to a specified web server
+        if (sendNow && *url) {
+            //HTTPClient http;
+            char buf[50];
+            URLSendIndicator = true;
+            snprintf(fullurl, 250, "%s?%s", url, data);
+            pc.printf("Contacting %s\r\n", fullurl);
+            http.setMaxRedirections(3);
+            int x = http.get(fullurl, buf, sizeof(buf));
+            URLSendIndicator = false;
+            pc.printf("  return: %d\r\n", x);
+        }
+        eth_mutex.unlock();
+    }
+}
+
+
+/// ShowSignOfLife
+///
+/// Pulse an LED to indicate a sign of life of the program.
+/// This also has some moderate entertainment value.
+///
+void ShowSignOfLife()
+{
+#define PI 3.14159265359
+    static Timer activityTimer;
+    static unsigned int activityStart;
+    static bool init;
+    static int degrees = 0;
+    float v;
+
+    if (!init) {
+        activityTimer.start();
+        activityStart = (unsigned int) activityTimer.read_ms();
+        init = true;
+    }
+    if ((unsigned int)activityTimer.read_ms() - activityStart > 20) {
+
+        v = sin(degrees * PI / 180);
+        if (v < 0)
+            v = 0;
+        signOfLife = v;
+        degrees += 5;
+        activityStart = (unsigned int) activityTimer.read_ms();
+    }
+}
+
+void LedOff(void)
+{
+    PulseIndicator = 0;
+}
+
+void CheckConsoleInput(void)
+{
+    if (pc.readable()) {
+        int c = pc.getc();
+        switch (c) {
+            case 'r':
+                mbed_reset();
+                break;
+            case 's':
+                SoftwareUpdateCheck(true);
+                break;
+            case 't':
+                SyncToNTPServer();
+                break;
+            default:
+                pc.printf("unknown command '%c'\r\n", c);
+                pc.printf("Commands:\r\n"
+                          "  r = reset\r\n"
+                          "  s = software update check\r\n"
+                          "  t = time sync to NTP server\r\n"
+                          );
+                ShowIPAddress();
+                
+                break;
+        }
+    }
+}
+
+bool NetworkIsConnected(void)
+{
+#ifdef WIFLY
+    return eth.is_connected();
+#else
+    return get_link_status();
+#endif
+}
+
+void PulseRisingISR(void)
+{
+    uint64_t tNow = timer.read_us();
+
+    __disable_irq();
+    if (!RawPowerSample.init) {
+        RawPowerSample.init = true;
+        RawPowerSample.cycles = (uint32_t)-1;
+        RawPowerSample.tStart = tNow;
+        RawPowerSample.tLastRise = tNow;
+        RawPowerSample.startTimestamp = ntp.time();
+    }
+    RawPowerSample.cycles++;
+    RawPowerSample.tStartSample = RawPowerSample.tLastRise;
+    RawPowerSample.tLastRise = tNow;
+    __enable_irq();
+    PulseIndicator = 1;
+    flash.attach_us(&LedOff, 25000);
+}
+
+void RunPulseTask(void)
+{
+    static time_t timeFor5s = 0;
+    static time_t timeFor5m = 0;
+    static uint32_t lastCount = 0;
+    time_t timenow = ntp.time();
+    float iKW = 0.0f;
+    bool sendToWeb = false;
+
+    __disable_irq();
+    uint32_t elapsed = RawPowerSample.tLastRise - RawPowerSample.tStartSample;
+    uint32_t count = RawPowerSample.cycles;
+    __enable_irq();
+    
+    if (elapsed) {
+        // instantaneous, from this exact sample
+        iKW = (float)3600 * 1000 / elapsed;
+    }
+    if (timeFor5s == 0 || timenow < timeFor5s)  // startup or if something goes really bad
+        timeFor5s = timenow;
+    if (timeFor5m == 0 || timenow < timeFor5m)  // startup or if something goes really bad
+        timeFor5m = timenow;
+    
+    if ((timenow - timeFor5m) >= 60) { // 300) {
+        pc.printf(" tnow: %d, t5m: %d\r\n", timenow, timeFor5m);
+        sendToWeb = true;
+        timeFor5m = timenow;
+        stats5m.EnterItem(stats5s.Average());
+    }
+    if ((timenow - timeFor5s) >= 5) {
+        timeFor5s = timenow;
+        stats5s.EnterItem(iKW);
+        TransmitEnergy(sendToWeb, iKW, stats5s.Min(), stats5s.Average(), stats5s.Max(),
+            stats5m.Min(), stats5m.Average(), stats5m.Max());
+    }
+    if (count != lastCount) {
+        lastCount = count;
+        pc.printf("%8.3fs => %4.3f (%4.3f,%4.3f,%4.3f) iKW, (%4.3f,%4.3f,%4.3f) KW 5m\r\n",
+            (float)elapsed/1000000, 
+            iKW,
+            stats5s.Min(), stats5s.Average(), stats5s.Max(),
+            stats5m.Min(), stats5m.Average(), stats5m.Max());
+    }
+}
+
+/// SimplyDynamicPage1
+///
+/// This web page is generated dynamically as a kind of "bare minimum".
+/// It doesn't do much.
+///
+/// You can see in main how this page was registered.
+///
+HTTPServer::CallBackResults SuperSimpleDynamicPage(HTTPServer *svr, HTTPServer::CallBackType type, 
+    const char * path, const HTTPServer::namevalue *params, int paramcount)
+{
+    HTTPServer::CallBackResults ret = HTTPServer::ACCEPT_ERROR;
+    char contentlen[30];
+    char buf[500];
+    char linebuf[100];
+
+    switch (type) {
+        case HTTPServer::SEND_PAGE:
+            // This sample drops it all into a local buffer, computes the length,
+            // and passes that along as well. This can help the other end with efficiency.
+            strcpy(buf, "<html><head><title>Smart WattEye/title></head>\r\n");
+            strcat(buf, "<body>\r\n");
+            strcat(buf, "<h1>Smart WattEye</h1>\r\n");
+            strcat(buf, "<table>");
+            snprintf(linebuf, sizeof(linebuf), "<tr><td>Instantaneous</td><td align='right'>%5.3f.</td></tr>\r\n", 
+                PowerSnapshot.instantKW);
+            strcat(buf, linebuf);
+            snprintf(linebuf, sizeof(linebuf), "<tr><td>Average</td><td align='right'>%5.3f</td></tr>\r\n", 
+                PowerSnapshot.averageKW);
+            strcat(buf, linebuf);
+            snprintf(linebuf, sizeof(linebuf), "<tr><td>Total Cycles</td><td align='right'>%10u</td></tr>\r\n", 
+                PowerSnapshot.measuredCycles);
+            strcat(buf, linebuf);
+            strcat(buf, "</table>");
+            strcat(buf, "<a href='/'>back to main</a></body></html>\r\n");
+            sprintf(contentlen, "Content-Length: %d\r\n", strlen(buf));
+            // Now the actual header response
+            svr->header(200, "OK", "Content-Type: text/html\r\n", contentlen);
+            // and data are sent
+            svr->send(buf);
+            ret = HTTPServer::ACCEPT_COMPLETE;
+            break;
+        case HTTPServer::CONTENT_LENGTH_REQUEST:
+            ret = HTTPServer::ACCEPT_COMPLETE;
+            break;
+        case HTTPServer::DATA_TRANSFER:
+            ret = HTTPServer::ACCEPT_COMPLETE;
+            break;
+        default:
+            ret = HTTPServer::ACCEPT_ERROR;
+            break;
+    }
+    return ret;
+}
+
+
+int main()
+{
+    bool SensorStarted = false;
+    pc.baud(460800);
+    pc.printf("\r\n%s\r\n", PROG_INFO);
+
+    if (wd.WatchdogCausedReset()) {
+        pc.printf("**** Watchdog Event caused reset ****\r\n");
+    }
+    wd.Configure(30.0);   // nothing should take more than 30 s we hope.
+    ini.SetFile(iniFile);
+    // Thread bcThread(Scheduler_thread, NULL, osPriorityHigh);
+
+    // Now let's instantiate the web server - along with a few settings:
+    // the Wifly object, the port of interest (typically 80),
+    // file system path to the static pages,
+    // the maximum parameters per transaction (in the query string),
+    // the maximum number of dynamic pages that can be registered,
+    // the serial port back thru USB (for development/logging)
+    //HTTPServer svr(NULL, 80, "/Local/", 15, 30, 10, &pc);
+
+    // But for even more fun, I'm registering a few dynamic pages
+    // You see the handlers for in DynamicPages.cpp.
+    // Here you can see the path to place on the URL.
+    // ex. http://192.168.1.140/dyn
+    //svr.RegisterHandler("/dyn",  SuperSimpleDynamicPage);
+
+    
+    pc.printf("***\r\n");
+    pc.printf("Initializing network interface...\r\n");
+    
+    int res;
+    char ip[20], mask[20], gw[20];
+    bool bIP, bMask, bGW;
+    bIP = ini.ReadString("Network", "addr", ip, 20, "");
+    bMask = ini.ReadString("Network", "mask", mask, 20, "");
+    bGW = ini.ReadString("Network", "gate", gw, 20, "");
+    
+    if (bIP && bMask && bGW) {
+        res = eth.init(ip,mask,gw);
+    } else {
+        res = eth.init();
+    }
+    if (0 == res) { // Interface set
+        do {
+            pc.printf("Connecting to network...\r\n");
+            if (0 == eth.connect()) {
+                linkup = true;
+                ShowIPAddress(true);
+#ifdef WIFLY
+#else
+                int speed = get_connection_speed();
+                pc.printf("Connected at %d Mb/s\r\n", speed);
+#endif
+                SoftwareUpdateCheck(true);
+                SyncToNTPServer();  // we hope to have the right time of day now
+                wait(5);
+                if (!SensorStarted) {
+                    timer.start();
+                    timer.reset();
+                    event.rise(&PulseRisingISR);
+                    SensorStarted = true;
+                }
+                while (NetworkIsConnected()) {
+                    Thread::wait(5);
+                    linkdata = !linkdata;
+                    // Here's the real core of the main loop
+                    RunPulseTask();
+                    //svr.Poll();
+                    CheckConsoleInput();
+                    ShowSignOfLife();
+                    SoftwareUpdateCheck();
+                    wd.Service();
+                }
+                linkup = false;
+                pc.printf("lost connection.\r\n");
+                ShowIPAddress(false);
+                eth.disconnect();
+            }
+            else {
+                pc.printf("  ... failed to connect.\r\n");
+            }
+            CheckConsoleInput();
+        }
+        while (1);
+    }
+    else {
+        pc.printf("  ... failed to initialize, rebooting...\r\n");
+        mbed_reset();
+    }
+
+}