X10 Server - IOT device to leverage a collection of old X10 devices for home automation and lighting control.

Dependencies:   IniManager mbed HTTPClient SWUpdate mbed-rtos Watchdog X10 SW_HTTPServer SW_String EthernetInterface TimeInterface SSDP

X10 Server

See the X10 Server Nodebook page

Revision:
10:ca0c1db6d933
Parent:
9:2c96e69b6035
--- a/main.cpp	Sun Mar 03 00:26:40 2019 +0000
+++ b/main.cpp	Sun Mar 03 23:41:27 2019 +0000
@@ -1,9 +1,9 @@
 //
-// X10 Server - Version 2
+// WattEye - Version 2
 //
-/// If you've wired the CM17a to the same mbed pins, you should be able to simply
-/// install this application, and then install a properly configured ini file
-/// on the local file system.
+/// WattEye monitors a simple input pin, measures the time between pulses, and translates
+/// that into a unit of power. The pulses are generated by the whole-house electric meter
+/// and may be generated by an IR Emitter.
 ///
 
 #include "mbed.h"               // ver 120; mbed-rtos ver 111
@@ -15,15 +15,29 @@
 #include "SSDP.h"               // ver 7
 #include "Watchdog.h"           // ver 6
 #include "IniManager.h"         // v20
-#include "X10.h"                // v1
-#include "X10Server.h"          // v1
+
+#include "StatisticQueue.h"
+#include "PowerData.h"
 
 #include "WebPages.h"           // Private handler for web queries
 #include "SignOfLife.h"         // LED effects
 
 extern "C" void mbed_reset();
 
-X10Interface cm17a(p5,p12);    // cm17a.ParseCommand(buf);
+//#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
+
 
 RawSerial pc(USBTX, USBRX);
 EthernetInterface eth;
@@ -47,30 +61,90 @@
 // public for the WebPages handler to see
 //
 const char * BUILD_DATE = __DATE__ " " __TIME__;
-const char * PROG_NAME = "X10-Server-2";
-char * My_Name = "X10-Server-2";
+const char * PROG_NAME = "WattEye-2";
+char * My_Name = "WattEye-2";
+const char * PROG_INFO = "WattEye-2      Build " __DATE__ " " __TIME__;
 const char * My_SerialNum = "0000001";
 int Server_Port = 80;
 // end public information
 
+const char * iniFile = "/local/WattEye.ini";
 
-const char * iniFile = "/local/X10svr.ini";
-
+HTTPClient http;
+DigitalOut PulseIndicator(LED1);
+DigitalOut UDPSendIndicator(LED2);
+DigitalOut URLSendIndicator(LED3);
 
 
-/// This function uses the settings in the .ini file to check for
-/// and install, a software update that might be available.
-///
-void SoftwareUpdateCheck(bool force)
+// 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);
+
+char url[100], dest[20], port[8];
+char myID[50];
+
+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;
+
+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;
+
+void PulseRisingISR(void);
+void RunPulseTask(void);
+void TransmitEnergy(bool sendNow, float iKW, float min5s, float avg5s, float max5s, float min5m, float avg5m, float max5m);
+void ShowRawSample();
+
+void ShowRawSample() {
+    printf("Sample:\r\n");
+    printf("  Sample Start: %s\r\n", ntp.ctime(&RawPowerSample.startTimestamp));
+    printf("        tStart: %llu\r\n", RawPowerSample.tStart);
+    printf("     tLastRise: %llu\r\n", RawPowerSample.tLastRise);
+    printf("  tStartSample: %llu\r\n", RawPowerSample.tStartSample);
+    printf("        cycles: %ul\r\n", RawPowerSample.cycles);
+}
+
+
+void SoftwareUpdateCheck(bool force = false)
 {
     char url[100], name[10];
-    static time_t tstart = ntp.time();
-    time_t tNow = ntp.time();
-    
+    static time_t tstart = ntp.timelocal();
+    time_t tNow = ntp.timelocal();
+
     //eth_mutex.lock();
-    #define ONE_DAY (24 * 60 * 60)
+#define ONE_DAY (24 * 60 * 60)
     if (force || (tNow - tstart) > ONE_DAY) {
-        pc.printf("SoftwareUpdateCheck at %s (UTC)\r\n", ntp.ctime(&tNow));
+        pc.printf(" SoftwareUpdateCheck: %s\r\n", ntp.ctime(&tNow));
         tstart = tNow;
         swUpdateCheck = true;
         if (INI::INI_SUCCESS == ini.ReadString("SWUpdate", "url",   url, sizeof(url))
@@ -87,19 +161,32 @@
                 pc.printf("  no update available.\r\n");
                 swUpdateCheck = false;
             } else {
-                pc.printf("  update failed %04X - %s\r\n", su, 
-                    SoftwareUpdateGetHTTPErrorMsg(SoftwareUpdateGetHTTPErrorCode()));
+                pc.printf("  update failed %04X - %s\r\n", su,
+                          SoftwareUpdateGetHTTPErrorMsg(SoftwareUpdateGetHTTPErrorCode()));
                 Thread::wait(1000);
+                swUpdateCheck = false;
             }
             linkdata = false;
         } else {
             pc.printf("  can't get info from ini file.\r\n");
             swUpdateCheck = false;
         }
-    //eth_mutex.unlock();
+        //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);
+}
+
+
 
 /// This function syncs the node to a timeserver, if one
 /// is configured in the .ini file.
@@ -112,7 +199,7 @@
     char dstStart[12];  // mm/dd,hh:mm
     char dstStop[12];   // mm/dd,hh:mm
     static time_t tlast = 0;
-    time_t tnow = ntp.time();
+    time_t tnow = ntp.timelocal();
 
     if (((tnow - tlast) > (60*60*24)) || force) {
         printf("SyncToNTPServer\r\n");
@@ -121,15 +208,15 @@
             ini.ReadString("Clock", "dst", dstFlag, sizeof(dstFlag), "0");
             ini.ReadString("Clock", "dststart", dstStart, sizeof(dstStart), "");
             ini.ReadString("Clock", "dststop", dstStop, sizeof(dstStop), "");
-            
+
             printf("NTP update time from (%s)\r\n", url);
             int32_t tzo_min = atoi(tzone);
-            
+
             if (strcmp(dstFlag,"on") == 0) {
                 ntp.set_dst(1);
             } else if (strcmp(dstFlag, "off") == 0) {
                 ntp.set_dst(0);
-            } else /* if (strcmp(dstFlag, "auto") == 0) */ {
+            } else { /* if (strcmp(dstFlag, "auto") == 0) */
                 ntp.set_dst(dstStart,dstStop);
             }
             ntp.set_tzo_min(tzo_min);
@@ -138,11 +225,11 @@
             //printf("  NTP (release ethernet)\r\n");
             if (res == 0) {
                 time_t ctTime;
-                ctTime = ntp.time();
+                ctTime = ntp.timelocal();
                 ntpSyncd = ntp.get_timelastset();;
                 tlast = ctTime;
                 printf("   Time set to (UTC): %s\r\n", ntp.ctime(&ctTime));
-                printf("            ntpSyncd: %s\r\n", ntp.ctime(&ntpSyncd));
+                printf("            ntpSyncd: %s (UTC)\r\n", ntp.ctime(&ntpSyncd));
                 ntpUpdateCheck = false;
             } else {
                 ntpSyncd = 0;
@@ -165,14 +252,9 @@
 void CheckConsoleInput(void)
 {
     static Timer timer;
-    static bool test = false;
-    static bool toggle = false;
-    static char buf[80];
-    static int i;
 
     if (pc.readable()) {
         int c = pc.getc();
-        test = false;
         switch (c) {
             case 'r':
                 mbed_reset();
@@ -183,114 +265,48 @@
             case 't':
                 ntpUpdateCheck = true;
                 break;
-            case 'x':
-                pc.printf("x10>");
-                i = 0;
-                do {
-                    c = pc.getc();
-                    pc.putc(c);
-                    if (c == '\x08') {  // <bs>
-                        if (i < 0) {
-                            pc.printf("\r\n");
-                            break;
-                        }
-                    } else if (c == '\r') {
-                        buf[i++] = '\0';
-                        printf("Shell Command: '%s'\r\n", buf);
-                        cm17a.ParseCommand(buf);
-                        break;
-                    } else {
-                        buf[i++] = c;
-                    }
-                } while(1);
-                break;
-            case 'z':
-                pc.printf("X10 test mode enabled\r\n");
-                test = true;
-                timer.start();
-                break;
             case '@':
                 pc.printf("Sample '%s' file.\r\n", iniFile);
                 pc.printf("[SWUpdate]\r\n");
                 pc.printf("url=http://192.168.1.201/mbed/\r\n");
-                pc.printf("name=X10svr\r\n");
+                pc.printf("name=WattEye\r\n");
                 pc.printf("[Clock]\r\n");
                 pc.printf("timeserver=time.nist.gov\r\n");
                 pc.printf("tzoffsetmin=-300\r\n");
                 pc.printf("[IP]\r\n");
-                pc.printf("ip=192.168.1.203\r\n");
+                pc.printf("ip=192.168.1.204\r\n");
                 pc.printf("nm=255.255.254.0\r\n");
                 pc.printf("gw=192.168.1.1\r\n");
                 pc.printf("[Node]\r\n");
-                pc.printf("id=X10Server-01\r\n");
+                pc.printf("id=WattEye-01\r\n");
                 pc.printf(";hint: Use either the fixed IP or the Node\r\n");
                 break;
             case '?':
+                ShowRawSample();
+                // break;
             default:
                 pc.printf("\r\n\r\n");
                 if (c > ' ' && c != '?')
                     pc.printf("unknown command '%c'\r\n", c);
-                pc.printf("IP: %s\r\n", eth.getIPAddress());
-                pc.printf("  Last Boot %s %s\r\n", 
-                    ntp.ctime(&lastboottime),
-                    (WDEventOccurred) ? "(WD event)" : "");
-                pc.printf("\r\n");
-                pc.printf("Valid commands:\r\n");
-                pc.printf("  r - reset\r\n");
-                pc.printf("  s - software update\r\n");
-                pc.printf("  t - time server sync\r\n");
-                pc.printf("  x <House><Unit> <cmd> | x # | /XXXX\r\n");
-                pc.printf("    a-p  House\r\n");
-                pc.printf("      1 - 16 Unit\r\n");
-                pc.printf("      1=On,0=Off,+#=Bright,-#=Dim (#=1 to 6)\r\n");
-                pc.printf("      ex: x a1 1 a3 +2\r\n");
-                pc.printf("    # = set baud rate\r\n");
-                pc.printf("    /XXXX send hex code XXXX\r\n");
-                pc.printf("  z - x10 test mode (toggles a1 on/off)\r\n");
-                pc.printf("  @ - Show a sample '%s' file.\r\n", iniFile);
-                pc.printf("\r\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;
         }
-    } else {
-        if (test) {
-            if (timer.read_ms() > 1000) {
-                timer.reset();
-                pc.printf("  Test Mode: Sending a1 %d\r\n", toggle);
-                if (toggle) {
-                    cm17a.ParseCommand("a1 1");
-                } else {
-                    cm17a.ParseCommand("a1 0");
-                }
-                toggle = !toggle;
-            }
-        }
     }
 }
 
 
-/// This handler is registered for callbacks from the X10server.
-/// 
-/// It has only the simple responsibility of passing the command
-/// forward to the CM17a driver. As a useful side-effect, it
-/// blinks the Network interface data LED.
-/// 
-void x10Handler(char * buffer, int size)
-{
-    time_t ctTime;
-    
-    ctTime = ntp.time();
-    linkdata = true;
-    pc.printf("X10 (%6s)     %s (UTC)\r\n", buffer, ntp.ctime(&ctTime));
-    cm17a.ParseCommand(buffer);
-    wait_ms(100);
-    linkdata = false;
-}
-
-
 
 int main()
 {
     char ip[20],nm[20],gw[20];
+    bool SensorStarted = false;
+
     pc.baud(460800);
     pc.printf("\r\n%s Build %s\r\n", PROG_NAME, BUILD_DATE);
     lastboottime = ntp.timelocal();
@@ -304,10 +320,10 @@
 
     pc.printf("Initializing network interface...\r\n");
     int initResult;
-        
+
     if (INI::INI_SUCCESS == ini.ReadString("IP", "ip",   ip, sizeof(ip))
-    &&  INI::INI_SUCCESS == ini.ReadString("IP", "nm",   nm, sizeof(nm))
-    &&  INI::INI_SUCCESS == ini.ReadString("IP", "gw",   gw, sizeof(gw))) {
+            &&  INI::INI_SUCCESS == ini.ReadString("IP", "nm",   nm, sizeof(nm))
+            &&  INI::INI_SUCCESS == ini.ReadString("IP", "gw",   gw, sizeof(gw))) {
         initResult = eth.init(ip,nm,gw);    // use Fixed
     } else {
         initResult = eth.init();    // use DHCP
@@ -318,56 +334,53 @@
         wait_ms(5000);
         mbed_reset();
     } else {
-        char * nn = (char *)malloc(33);
-        if (!nn)
-            error("no mem for network name");
-        ini.ReadString("Node", "id", nn, 32, "Name Me");
-        pc.printf("Name: %s\r\n", nn);
-        eth.setName(nn);
-
-        char * port = (char *)malloc(33);
-        uint16_t portNum = 10630;               // X10 Listener Port
-        if (!port)
-            error("no mem for port");
-        if (INI::INI_SUCCESS == ini.ReadString("IP", "port", port, sizeof(port)))
-            portNum = atoi(port);
+        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 (INI::INI_SUCCESS == bEU && INI::INI_SUCCESS == bDS && INI::INI_SUCCESS == bPO && INI::INI_SUCCESS == bID) {
+            pc.printf("Node   %s\r\n", myID);
+            pc.printf("URL    %s\r\n", url);
+            pc.printf("port   %s\r\n", port);
+            pc.printf("dest   %s\r\n", dest);
+            Server_Port = atoi(port);
+        }
+        eth.setName(myID);
 
         do {
             pc.printf("Connecting to network...\r\n");
             if (0 == eth.connect()) {
                 wd.Service();
                 linkup = true;
-                time_t tstart = ntp.time();
+                time_t tstart = ntp.timelocal();
                 int speed = eth.get_connection_speed();
 
                 pc.printf("Ethernet Connected at: %d Mb/s\r\n", speed);
                 pc.printf("                   IP: %15s\r\n", eth.getIPAddress());
 
-                HTTPServer svr(Server_Port, Server_Root, 15, 30, 20, 50, &pc);
+                HTTPServer svr(Server_Port, Server_Root);
                 svr.RegisterHandler("/", RootPage);
                 svr.RegisterHandler("/info", InfoPage);
                 svr.RegisterHandler("/software", SoftwarePage);
                 svr.RegisterHandler("/reboot", RebootPage);
                 svr.RegisterHandler("/setup.xml", Setup_xml);
-                SSDP ssdp(My_Name, eth.getMACAddress(), eth.getIPAddress(), Server_Port);
+                SSDP ssdp(myID, eth.getMACAddress(), eth.getIPAddress(), Server_Port);
 
-                pc.printf("  X10 Server started %s (UTC)\r\n", ntp.ctime(&tstart));
-                X10Server x10svr(&x10Handler, portNum);
-
+                wait(5);
+                if (!SensorStarted) {
+                    timer.start();
+                    timer.reset();
+                    event.rise(&PulseRisingISR);
+                    SensorStarted = true;
+                    printf("Sensor started\r\n");
+                }
                 while (eth.is_connected()) {
-                    static time_t tLastSec;
-
                     wd.Service();
                     time_t tNow = ntp.timelocal();
                     CheckConsoleInput();
-                    x10svr.poll();      
+                    RunPulseTask();
                     svr.Poll();         // Web Server: non-blocking, but also not deterministic
-                    ShowSignOfLife(1);
-                    ShowSignOfLife(2);
-                    if (tNow != tLastSec) {
-                        pc.printf("time is %s\r\n", ntp.ctime(&tNow));
-                        tLastSec = tNow;
-                    }
+                    ShowSignOfLife();
                     SyncToNTPServer(ntpUpdateCheck);
                     SoftwareUpdateCheck(swUpdateCheck);
                     // Any other work can happen here
@@ -377,6 +390,7 @@
                 linkup = false;
                 linkdata = false;
                 pc.printf("lost connection.\r\n");
+                ShowIPAddress(false);
                 eth.disconnect();
             } else {
                 pc.printf("  ... failed to connect.\r\n");
@@ -384,3 +398,116 @@
         } while (1);
     }
 }
+
+
+void LedOff(void)
+{
+    PulseIndicator = 0;
+}
+
+
+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.timelocal();
+    }
+    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.timelocal();
+    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());
+    }
+}
+
+void TransmitEnergy(bool sendNow, float iKW, float min5s, float avg5s, float max5s, float min5m, float avg5m, float max5m)
+{
+    char data[150];
+    char fullurl[250];
+
+    if (*url) {
+        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();
+        linkdata = true;
+        // Send the UDP Broadcast, picked up by a listener
+        UDPSendIndicator = true;
+        UDPSocket bcast;
+        Endpoint ep;
+        int h = ep.set_address(dest, Server_Port);
+        int i = bcast.bind(Server_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);
+        }
+        linkdata = false;
+        //eth_mutex.unlock();
+    }
+}