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

main.cpp

Committer:
WiredHome
Date:
2019-03-03
Revision:
10:ca0c1db6d933
Parent:
9:2c96e69b6035

File content as of revision 10:ca0c1db6d933:

//
// WattEye - Version 2
//
/// 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
#include "EthernetInterface.h"  // ver 56 - https://os.mbed.com/users/WiredHome/code/EthernetInterface/
#include "SW_HTTPServer.h"      // ver 58
#include "TimeInterface.h"      // ver 25
#include "SWUpdate.h"           // ver 26
#include "SW_String.h"          // ver 2
#include "SSDP.h"               // ver 7
#include "Watchdog.h"           // ver 6
#include "IniManager.h"         // v20

#include "StatisticQueue.h"
#include "PowerData.h"

#include "WebPages.h"           // Private handler for web queries
#include "SignOfLife.h"         // LED effects

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


RawSerial pc(USBTX, USBRX);
EthernetInterface eth;
DigitalOut linkup(p26);
DigitalOut linkdata(p25);

Watchdog wd;
bool WDEventOccurred = false;

TimeInterface ntp(&eth);
time_t ntpSyncd;            // time of the last sync
bool ntpUpdateCheck = true;         // scheduled check on startup

time_t lastboottime;
bool swUpdateCheck = true;          // scheduled check on startup

INI ini;
LocalFileSystem local("local");             // some place to hold settings and maybe the static web pages
const char * Server_Root = "/local";

// public for the WebPages handler to see
//
const char * BUILD_DATE = __DATE__ " " __TIME__;
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";

HTTPClient http;
DigitalOut PulseIndicator(LED1);
DigitalOut UDPSendIndicator(LED2);
DigitalOut URLSendIndicator(LED3);


// 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.timelocal();
    time_t tNow = ntp.timelocal();

    //eth_mutex.lock();
#define ONE_DAY (24 * 60 * 60)
    if (force || (tNow - tstart) > ONE_DAY) {
        pc.printf(" SoftwareUpdateCheck: %s\r\n", ntp.ctime(&tNow));
        tstart = tNow;
        swUpdateCheck = true;
        if (INI::INI_SUCCESS == ini.ReadString("SWUpdate", "url",   url, sizeof(url))
                &&  INI::INI_SUCCESS == ini.ReadString("SWUpdate", "name", name, sizeof(name))) {
            linkdata = true;
            pc.printf("  url: %s\r\n", url);
            pc.printf(" name: %s\r\n", name);
            SWUpdate_T su = SoftwareUpdate(url, name, DEFER_REBOOT);
            if (SWUP_OK == su) {
                pc.printf("  update installed, rebooting...\r\n");
                Thread::wait(3000);
                mbed_reset();
            } else if (SWUP_SAME_VER == su) {
                pc.printf("  no update available.\r\n");
                swUpdateCheck = false;
            } else {
                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();
    }
}

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.
///
void SyncToNTPServer(bool force)
{
    char url[100];
    char tzone[10];
    char dstFlag[5];    // off,on,auto
    char dstStart[12];  // mm/dd,hh:mm
    char dstStop[12];   // mm/dd,hh:mm
    static time_t tlast = 0;
    time_t tnow = ntp.timelocal();

    if (((tnow - tlast) > (60*60*24)) || force) {
        printf("SyncToNTPServer\r\n");
        if (INI::INI_SUCCESS == ini.ReadString("Clock", "timeserver", url, sizeof(url))) {
            ini.ReadString("Clock", "tzoffsetmin", tzone, sizeof(tzone), "0");
            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) */
                ntp.set_dst(dstStart,dstStop);
            }
            ntp.set_tzo_min(tzo_min);
            linkdata = true;
            int res = ntp.setTime(url);
            //printf("  NTP (release ethernet)\r\n");
            if (res == 0) {
                time_t ctTime;
                ctTime = ntp.timelocal();
                ntpSyncd = ntp.get_timelastset();;
                tlast = ctTime;
                printf("   Time set to (UTC): %s\r\n", ntp.ctime(&ctTime));
                printf("            ntpSyncd: %s (UTC)\r\n", ntp.ctime(&ntpSyncd));
                ntpUpdateCheck = false;
            } else {
                ntpSyncd = 0;
                printf("Error return from setTime(%s) %d\r\n", url, res);
            }
            linkdata = false;
        } else {
            ntpSyncd = 0;
            printf("no time server was set\r\n");
        }
    }
}


/// This function monitors the USB serial interface for activity
/// from a connected user.
///
/// It offers a tiny bit of help if an unrecognized command is entered.
///
void CheckConsoleInput(void)
{
    static Timer timer;

    if (pc.readable()) {
        int c = pc.getc();
        switch (c) {
            case 'r':
                mbed_reset();
                break;
            case 's':
                swUpdateCheck = true;
                break;
            case 't':
                ntpUpdateCheck = true;
                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=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.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=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("Commands:\r\n"
                          "  r = reset\r\n"
                          "  s = software update check\r\n"
                          "  t = time sync to NTP server\r\n"
                         );
                ShowIPAddress();

                break;
        }
    }
}



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();
    if (wd.WatchdogCausedReset()) {
        pc.printf("**** Watchdog Event caused reset at %s ****\r\n", ntp.ctime(&lastboottime));
        WDEventOccurred = true;
    }
    wd.Configure(25);       // very generous, but this is a network appliance, so a bit less deterministic.

    ini.SetFile(iniFile, 2);

    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))) {
        initResult = eth.init(ip,nm,gw);    // use Fixed
    } else {
        initResult = eth.init();    // use DHCP
    }
    if (initResult) {
        // Failed to init ...
        pc.printf("  ... failed to initialize, rebooting...\r\n");
        wait_ms(5000);
        mbed_reset();
    } else {
        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.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);
                svr.RegisterHandler("/", RootPage);
                svr.RegisterHandler("/info", InfoPage);
                svr.RegisterHandler("/software", SoftwarePage);
                svr.RegisterHandler("/reboot", RebootPage);
                svr.RegisterHandler("/setup.xml", Setup_xml);
                SSDP ssdp(myID, eth.getMACAddress(), eth.getIPAddress(), Server_Port);

                wait(5);
                if (!SensorStarted) {
                    timer.start();
                    timer.reset();
                    event.rise(&PulseRisingISR);
                    SensorStarted = true;
                    printf("Sensor started\r\n");
                }
                while (eth.is_connected()) {
                    wd.Service();
                    time_t tNow = ntp.timelocal();
                    CheckConsoleInput();
                    RunPulseTask();
                    svr.Poll();         // Web Server: non-blocking, but also not deterministic
                    ShowSignOfLife();
                    SyncToNTPServer(ntpUpdateCheck);
                    SoftwareUpdateCheck(swUpdateCheck);
                    // Any other work can happen here
                    // ...
                    Thread::yield();
                }
                linkup = false;
                linkdata = false;
                pc.printf("lost connection.\r\n");
                ShowIPAddress(false);
                eth.disconnect();
            } else {
                pc.printf("  ... failed to connect.\r\n");
            }
        } 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();
    }
}