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
Diff: main.cpp
- 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(); + } +}