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:
- 2016-01-18
- Revision:
- 3:5c3ba12d155b
- Parent:
- 2:649d91b93824
- Child:
- 4:0a7567195e4b
File content as of revision 3:5c3ba12d155b:
#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(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); 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(); } }