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.

main.cpp

Committer:
WiredHome
Date:
2019-03-02
Revision:
4:0a7567195e4b
Parent:
3:5c3ba12d155b

File content as of revision 4:0a7567195e4b:

#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 51
#endif

#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);
            case ' ':
            case '\r':
            case '\n':
                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)
{
    return eth.is_connected();
}

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;
        timeFor5s = timeFor5m = timenow;
        stats5s.EnterItem(iKW);
        stats5m.EnterItem(stats5s.Average());
        TransmitEnergy(true, iKW, stats5s.Min(), stats5s.Average(), stats5s.Max(),
            stats5m.Min(), stats5m.Average(), stats5m.Max());
    } else if ((timenow - timeFor5s) >= 5) {
        sendToWeb = true;
        timeFor5s = timenow;
        stats5s.EnterItem(iKW);
        TransmitEnergy(false, iKW, stats5s.Min(), stats5s.Average(), stats5s.Max(),
            stats5m.Min(), stats5m.Average(), stats5m.Max());
    }
    if (sendToWeb) { // 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(HTTPServer::OK, "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);
                int speed = eth.get_connection_speed();
                pc.printf("Connected at %d Mb/s\r\n", speed);
                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();
    }

}