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.
Diff: main.cpp
- Revision:
- 0:a4887b672ac6
- Child:
- 1:04ab0a3d07f1
diff -r 000000000000 -r a4887b672ac6 main.cpp --- /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(); + } + +}