A remote timer, equipped with a scheduling Web interface, controls a "DigitalOut".

Dependencies:   Timezone NTPClient

WebTimer

The WebTimer is an Mbed device enabling to remotely control a DigitalOut. It has a "calendar-like" user interface to edit the schedule - a table you can click on (or tap on a touch-screen device).
The program was ported to Mbed from the Tuxgraphics site. Thank you Guido for sharing!

  • When starting, it will print it's IP address over the USB serial link to the terminal screen. However, you can set a different static IP address if you modify the code in the main.cpp.
  • To connect to the WebTimer, type the IP address into the web browser edit box and hit ENTER.
  • When a cell is green then the timer keeps it's output ON during that period of time. If a cell is not green it means the output is OFF.
  • To select or deselect multiple cells you can click (keep down the button) and move the mouse over more cells.
  • The smallest time interval on this 24h timer is 15minutes.
  • To save the changes type in the password and hit ENTER or click on the Save button.
  • The default password is "secret". But you can change it by editing the source code in main.cpp.
OnDesktopOnPhone

main.cpp

Committer:
hudakz
Date:
2020-11-12
Revision:
2:6d02d3d314a2
Parent:
1:2d2517f82319
Child:
3:2862fe67dce0

File content as of revision 2:6d02d3d314a2:

/*********************************************
 * Author: Guido Socher
 * Copyright: GPL V2
 * See http://www.gnu.org/licenses/gpl.html
 *
 * 24h timer with web interface.
 * 
 * See http://tuxgraphics.org/electronics/
 *
 * Ported to mbed by Zoltan Hudak
 *********************************************/

#include "mbed.h"
#include "EthernetInterface.h"
#include "TCPSocket.h"
#include <stdio.h>
#include <string>
#include "websrv_help_functions.h"
#include "mbed_mktime.h"
#include "NTPClient.h"
#include "Timezone.h"

#define IP                 "192.168.1.181"
#define GATEWAY            "192.168.1.1"
#define NETMASK            "255.255.255.0"
#define PORT               80
#define STR_BUFFER_SIZE    40
#define NTP_SERVER         "europe.pool.ntp.org"

/**/
EthernetInterface*  net;
TCPSocket           server;
TCPSocket*          client;
char                httpBuf[1500];
char                httpHeader[256];
const int           OFF = 0;
const int           ON = 1;
const char          PASSWORD[] = "secret";  // Change as you like
DigitalOut          output(LED1);
char                strBuf[STR_BUFFER_SIZE + 1];
uint16_t            tablebitmap[6] = { 0 }; // each of the 96 bits represents one field in the time table
Thread              thread;
EventQueue          eventQueue;
tm                  tmNow;
DigitalOut          relay(LED1, 0);
NTPClient*          ntp;
TimeChangeRule      cest = { "CEST", Last, Sun, Mar, 2, 1 * 60 };   // Central European Summer Time = NTP + 1 hour
TimeChangeRule      cet = { "CET", Last, Sun, Oct, 2, 0 };          // Central European Time = NTP
Timezone            cetZone(cest, cet);                             // Central European Time Zone

/**
 * @brief
 * @note
 * @param
 * @retval
 */
uint8_t verifyPassword(char* str)
{
    if (strncmp(PASSWORD, str, strlen(PASSWORD)) == 0)
        return(1);
    else
        return(0);
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void setHeaderToHttp200ok(void)
{
    strcpy(httpHeader, "HTTP/1.0 200 OK");
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void addHttpDataUint8(uint8_t i)
{
    sprintf(strBuf, "%i", i);  // convert integer to string
    if (i < 10)
        strcat(httpBuf, "0");
    strcat(httpBuf, strBuf);
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void addHttpDataUint16(uint16_t ui)
{
    uint8_t i = 6;
    if (ui == 0) {
        strcat(httpBuf, "0");
        return;
    }

    // convert a 16bit unsigned integer into a printable string
    strBuf[i] = '\0';
    while (ui && i) {
        i--;
        strBuf[i] = (char)((ui % 10 & 0xf)) + 0x30;
        ui = ui / 10;
    }

    strcat(httpBuf, &strBuf[i]);
}

/**
 * @brief   Check the tablebitmap
 * @note
 * @param
 * @retval  0 there is no change compared to previous minute and current state is off
 *          1 the on/off state changed from on to off
 *          2 the on/off state changed from off to on
 *          3 there is no change compared to previous minute and current state is on
 */

uint8_t checkTableBitmap(uint8_t h, uint8_t m)
{
    uint8_t tablepage, bitpos;
    uint8_t curr = 0;
    uint8_t prev = 0;
    tablepage = h / 4;  // which table page is the current
    if (tablepage > 5)
        tablepage = 5;  // bounds check in case of garbage time
    bitpos = ((h % 4) * 60 + m) / 15;
    if (bitpos > 15)
        bitpos = 15;    // bounds check in case of garbage time
    if (tablebitmap[tablepage] & (1 << bitpos)) {
        curr = 1;
    }

    // now check what the previous value was
    if (m > 0) {
        m--;
    }
    else {
        m = 59;
        if (h > 0) {
            h--;
        }
        else {
            h = 23;
        }
    }

    tablepage = h / 4;  // which table page is the current
    if (tablepage > 5)
        tablepage = 5;  // bounds check in case of garbage time
    bitpos = ((h % 4) * 60 + m) / 15;
    if (bitpos > 15)
        bitpos = 15;    // bounds check in case of garbage time
    if (tablebitmap[tablepage] & (1 << bitpos)) {
        prev = 1;
    }

    if (prev == curr && curr == 0)
        return(0);
    if (prev == curr && curr == 1)
        return(3);
    if (curr == 1 && prev == 0)
        return(2);
    return(1);
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void checkRelayStatus(void)
{
    uint8_t i;
    i = checkTableBitmap(tmNow.tm_hour, tmNow.tm_min);
    if (i == 2 || i == 3) {
        relay = ON;
    }
    else {
        relay = OFF;
    }
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void printPageSetClock(void)
{
    strcpy(httpHeader, "HTTP/1.0 200 OK");
    memset(httpBuf, 0, sizeof(httpBuf));
    strcpy(httpBuf, "<meta name=viewport content=\"width=device-width\">\n");
    /*$off*/
    strcat
    (
        httpBuf,
        "<a href=/>[&#8592;Back]</a>\n"
        "<h2>Set clock</h2>\n<pre>"
        "<form  action=cc method=get>\n"
        "<blockquote>"
        "<input type=radio name=mt value=0 checked>Automatically by NTP server<br>"
        "<input type=radio name=mt value=1>Manually to"
        "</blockquote>"
        "time :<input type=text name=tv id=tf size=10>\n\n"
        "passw:<input type=password name=pw size=10><input type=submit value=\"set time\">\n"
        "</form>\n"
        "<script>\n"
        "d=new Date();\n"
        "e=document.getElementById(\"tf\");\n"
        "mo=d.getMonth()+1;\n"
        "e.value=d.getFullYear()+\"-\"+mo+\"-\"+d.getDate()+\"-\"+d.getHours()+\":\"+d.getMinutes();\n"
        "</script>\n"
        "</pre><hr>\n"
    );
    /*$on*/
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void printPageRelay(void)
{
    setHeaderToHttp200ok();
    memset(httpBuf, 0, sizeof(httpBuf));
    strcpy(httpBuf, "<meta name=viewport content=\"width=device-width\">\n");
    strcat(httpBuf, "<a href=/>[&#8592;Back]</a> <a href=/sw>[&#8635;Refresh]</a>\n""<h2>Switch</h2>\n""<pre>state: ");
    if (relay == ON) {
        strcat(httpBuf, "<font color=#00AA00>ON</font>");
    }
    else {
        strcat(httpBuf, "OFF");
    }

    strcat(httpBuf, "\n<form action=/cr method=get>");
    strcat(httpBuf, "<input type=hidden name=sw value=");
    if (relay == ON) {
        strcat(httpBuf, "0>\npassw: <input type=password size=10 name=pw>");
        strcat(httpBuf, "<input type=submit value=\"switch off\"></form>\n");
    }
    else {
        strcat(httpBuf, "1>\npassw: <input type=password size=10 name=pw>");
        strcat(httpBuf, "<input type=submit value=\"switch on\"></form>\n");
    }

    strcat(httpBuf, "</pre><hr>\n");
}

/**
 * @brief
 * @note    In the function fill we encode all the 96 fiels into 6 numbers
 *          with 16 bit. Every bit represents a table box. If the box was
 *          green then the bit is 1 otherwise zero.
 * @param
 * @retval
 */
void printJavaScriptFunctions(void)
{
    setHeaderToHttp200ok();

    //
    /*$off*/
    memset(httpBuf, 0, sizeof(httpBuf));
    strcpy
    (
        httpBuf,
        "var col=\"\";\n"
        "var cc=0;\n"
        "var e,i,j;\n"
        "function dw(s){document.writeln(s)}\n"
        "function mc(e){if (cc) e.style.backgroundColor=col}\n"
        "function wr(t,i){dw(\"<tr><td>\"+t+\"</td><td id=t\"+i+\" onmousedown=cbg(this) onmouseover=mc(this)>&nbsp;</td></tr>\")}\n"
        "function dwh(s){dw(\"<table border=1><tr><td>Time</td><td>green=on<br>white=off</td></tr>\")}\n"
        "function cbg(e)\n"
        "{\n"
            "cc=1;\n"
            "col=e.style.backgroundColor ? \"\" : \"#7AFA7A\";\n"
            "e.style.backgroundColor=col;\n"
        "}\n"
        "function sc(){cc=0}\n"
        "function fill(ff){\n"
            "var b=new Array();\n"
            "for(i=0;i<6;i++){\n"
            "b[i]=0;\n"
            "for(j=0;j<16;j++){\n"
                "e=document.getElementById(\"t\"+(i*16+j));\n"
                "if (e.style.backgroundColor)\n"
                    "b[i]|=1<<j;\n"
                "}\n"
            "}\n"
            "ff.elements[0].value=b[0] +\"-\"+b[1]+\"-\"+b[2]+\"-\"+b[3]+\"-\"+b[4]+\"-\"+b[5]+\"-\";\n"
        "}\n"
        "function it(a)\n"
        "{\n"
            "for(i=0;i<6;i++){\n"
                "for(j=0;j<16;j++){\n"
                    "e=document.getElementById(\"t\"+(i*16+j));\n"
                    "if (a[i]&(1<<j))\n"
                    "e.style.backgroundColor=\"#7AFA7A\";\n"
                "}\n"
            "}\n"
        "}"
    );
    /*$on*/
}

/**
 * @brief   This is the top level main web page
 * @note
 * @param
 * @retval
 */
void printPageMain(void)
{
    setHeaderToHttp200ok();
    memset(httpBuf, 0, sizeof(httpBuf));
    strcpy
    (
        httpBuf,
        "<meta name=viewport content=\"width=device-width\">\n"
        "<a href=/sw>[Switch]</a> <a href=/sc>[Set clock]</a> <a href=/>[&#8635;Refresh]</a>\n"
    );
    /*$on*/
    strcat(httpBuf, "<h2><pre>Watering Timer<br>");

    strcat(httpBuf, "Output: ");
    if (relay == ON) {
        strcat(httpBuf, "<font color=#00AA00>ON</font>");
    }
    else {
        strcat(httpBuf, "OFF");
    }

    //strcat(httpBuf, "\n");
    strcat(httpBuf, "   Time: ");
    addHttpDataUint8(tmNow.tm_hour);
    strcat(httpBuf, ":");
    addHttpDataUint8(tmNow.tm_min);
    strcat(httpBuf, "</pre></h2>\n");
    /*$off*/
    strcat
    (
        httpBuf,
        //"<br>Click table fields to change.\n"
        "<script src=p1.js></script>\n"
        "<script>\n"
        "dw(\"<table onmouseup=sc()>\");\n"
        "dw(\"<tr><td>\");\n"
        "dwh();\n"
        "var i,t;\n"
        "for(i=0;i<48;i++){\n"
            "t=\"\";\n"
            "if (i%4==0) t=i/4+\":00\";\n"
            "wr(t,i);\n"
        "}\n"
        "dw(\"</table></td><td>\");\n"
        "dwh();\n"
        "for(i=48;i<96;i++){\n"
            "t=\"\";\n"
            "if (i%4==0) t=i/4+\":00\";\n"
            "wr(t,i);\n"
        "}\n"
        "dw(\"</table></td></tr></table>\");\n"
        "dw(\"1 cell=15min\");\n"
    );
    /*$on*/
    // initialize the table with the current settings
    strcat(httpBuf, "it(new Array(");
    addHttpDataUint16(tablebitmap[0]);

    int i = 1;
    while (i < 6) {
        strcat(httpBuf, ",");
        addHttpDataUint16(tablebitmap[i]);
        i++;
    }

    strcat(httpBuf, "));\n");
    /*$off*/
    strcat(
        httpBuf,
        "</script>\n"
        "<br><br>\n"
        "<form onsubmit=fill(this) action=st method=get>\n"
        "<input type=hidden name=tt>\n"
        "passw: <input type=password size=10 name=pw>\n"
        "<input type=submit value=save>\n"
        "</form>\n"
    );
    /*$on*/
}

// takes a string of the form command/Number and analyse it (e.g "cc?tv=8%3A15&pw=secret HTTP/1.1")

// The first char of the url ('/') is already removed.
int8_t analyseUrl(char* str)
{
    int8_t  i;
    int8_t  j;
    uint8_t n;

    if (str[0] == ' ') {
        printPageMain();
        return(1);
    }

    if (strncmp("sc", str, 2) == 0) {
        printPageSetClock();
        return(1);
    }

    if (strncmp("sw", str, 2) == 0) {
        printPageRelay();
        return(1);
    }

    if (strncmp("p1.js", str, 5) == 0) {
        printJavaScriptFunctions();
        return(2);
    }

    // save the tablebitmap
    if (strncmp("st", str, 2) == 0) {

        // save the tablebitmap
        if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"pw")) {
            urldecode(strBuf);
            if (verifyPassword(strBuf)) {
                if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"tt")) {
                    urldecode(strBuf);
                    strBuf[37] = '\0';  // max length

                    // gStrbuf is a string like this: 11-58-0-0-0-0-
                    // no space allowed
                    if (!isdigit(strBuf[0])) {
                        return(0);
                    }

                    n = 0;
                    i = 0;
                    j = 0;
                    while (strBuf[i] && n < 6) {
                        if (strBuf[i] == '-') {
                            strBuf[i] = '\0';
                            tablebitmap[n] = atol(&strBuf[j]);
                            n++;
                            j = i + 1;
                        }

                        i++;
                    }

                    checkRelayStatus();
                    printPageMain();
                    return(1);          // main page
                }
            }
        }

        return(-1);
    }

    // change clock
    if (strncmp("cc", str, 2) == 0) {
        if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"pw")) {
            urldecode(strBuf);
            if (verifyPassword(strBuf)) {
                if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"mt")) {
                    urldecode(strBuf);
                    if (strBuf[0] == '0') {
                        ntp->setTime(NTP_SERVER, 123, 3000);  // set clock
                        set_time(cetZone.toLocal(rtc_read()));                          // daylight saving time
                    }
                    else
                    if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"tv")) {
                        urldecode(strBuf);
                        strBuf[16] = '\0';

                        // strBuf is a string like: 2020-10-21-11:58
                        // no space allowed
                        if (!isdigit(strBuf[0])) {
                            return(0);
                        }

                        char*   token = strBuf;
                        char*   delim;

                        delim = strchr(token, '-');
                        *delim = '\0';
                        tmNow.tm_year = atoi(token) - 1900;

                        token = delim + 1;
                        delim = strchr(token, '-');
                        *delim = '\0';
                        tmNow.tm_mon = atoi(token) - 1;

                        token = delim + 1;
                        delim = strchr(token, '-');
                        *delim = '\0';
                        tmNow.tm_mday = atoi(token);

                        token = delim + 1;
                        delim = strchr(token, ':');
                        *delim = '\0';
                        tmNow.tm_hour = atoi(token);

                        token = delim + 1;
                        tmNow.tm_min = atoi(token);

                        tmNow.tm_isdst = 1;                         // apply daylight saving
                        time_t  seconds;

                        _rtc_maketime(&tmNow, &seconds, RTC_FULL_LEAP_YEAR_SUPPORT);
                        set_time(seconds);
                    }

                    time_t  seconds = rtc_read();
                    _rtc_localtime(seconds, &tmNow, RTC_FULL_LEAP_YEAR_SUPPORT);
                    checkRelayStatus();
                    printPageMain();
                    return(1);                                      // main page
                }

                return(0);
            }
        }

        return(-1);
    }

    i = 0;                      // switch on or off

    // change relay state FORM
    if (strncmp("cr", str, 2) == 0) {
        if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"sw")) {
            if (strBuf[0] == '1') {
                i = 1;
            }

            if (find_key_val(str, strBuf, STR_BUFFER_SIZE, (char*)"pw")) {
                urldecode(strBuf);
                if (verifyPassword(strBuf)) {
                    if (i)
                        relay = ON;
                    else
                        relay = OFF;

                    printPageMain();
                    return(1);  // main page
                }
            }

            return(-1);
        }

        return(0);
    }

    return(0);
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void sendHttp(TCPSocket* client, char* header, char* content, bool js = false)
{
    char    content_length[10] = { };

    sprintf(content_length, "%u\r\n", strlen(content));

    if (js) {
        strcat(header, "Content-Type: application/x-javascript\r\n\r\n");
    }
    else {
        strcat(header, "\r\nContent-Type: text/html\r\n");
        strcat(header, "Content-Length: ");
        strcat(header, content_length);
        strcat(header, "Pragma: no-cache\r\n");
        strcat(header, "Connection: About to close\r\n\r\n");
    }

    char    c = content[0];
    memmove(httpBuf + strlen(header), httpBuf, strlen(content));    // make room for the header
    strcpy(httpBuf, header);                                        // copy the header on front of the content
    httpBuf[strlen(header)] = c;
    client->send((uint8_t*)httpBuf, strlen(httpBuf));
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
void onTick()
{
    time_t  seconds = rtc_read();
#ifdef DEBUG
    printf("Seconds since January 1, 1970 = %llu s\r\n", seconds);
#endif
    _rtc_localtime(seconds, &tmNow, RTC_FULL_LEAP_YEAR_SUPPORT);
    checkRelayStatus();
}

/**
 * @brief
 * @note
 * @param
 * @retval
 */
int main(void)
{
    printf("\r\nStarting..\r\n");

    eventQueue.call_every(1s, onTick);
    thread.start(callback(&eventQueue, &EventQueue::dispatch_forever));

    //net = NetworkInterface::get_default_instance();
    net = new EthernetInterface();

    //net->set_network("192.168.1.181","255.255.255.0","192.168.1.1");  // use static IP address, netmask, gateway
    if (!net) {
        printf("Error! No network inteface found.\n");
        return 0;
    }

    //net->set_network (IP, NETMASK, GATEWAY);  // include this to use static IP address
    nsapi_size_or_error_t   r = net->connect();
    if (r != NSAPI_ERROR_OK) {
        printf("Error! net->connect() returned: %d\n", r);
        return r;
    }

    // Show the network address
    SocketAddress   addr;
    net->get_ip_address(&addr);
    printf("IP address: %s\n", addr.get_ip_address() ? addr.get_ip_address() : "None");
    net->get_netmask(&addr);
    printf("Netmask: %s\n", addr.get_ip_address() ? addr.get_ip_address() : "None");
    net->get_gateway(&addr);
    printf("Gateway: %s\n", addr.get_ip_address() ? addr.get_ip_address() : "None");

    // Create NTP connection
    ntp = new NTPClient(*net);
    ntp->setTime(NTP_SERVER, 123, 3000);              // set clock
    set_time(cetZone.toLocal(rtc_read()));                          // daylight saving time

    /* Open the server on ethernet stack */
    server.open(net);

    /* Bind the HTTP port (TCP 80) to the server */
    server.bind(PORT);

    /* Listen for clients */
    server.listen();

    //listening for http GET request
    while (true) {
        client = server.accept();
        if (client)
        {
#ifdef DEBUG
            client->getpeername(&addr);
            printf("Connection succeeded!\n\rIP: %s\n\r", addr.get_ip_address());
#endif
            client->recv(httpBuf, 1500);
            if (strncmp(httpBuf, "GET", 3) != 0) {
                setHeaderToHttp200ok();
                strcpy(httpBuf, "<h1>200 OK</h1>");
                sendHttp(client, httpHeader, httpBuf);
            }
            else {
                int cmd = analyseUrl((char*) &httpBuf[5]);  // "GET [this is the tex to be analysed]"

                switch (cmd) {
                    case -1:
                        strcpy(httpHeader, "HTTP/1.0 401 Unauthorized");
                        strcpy(httpBuf, "<h1>401 Unauthorized</h1>");
                        sendHttp(client, httpHeader, httpBuf);
                        break;

                    case 0:
                        strcpy(httpHeader, "HTTP/1.0 404 Page Not Found");
                        strcpy(httpBuf, "<h1>404 Page Not Found</h1>");
                        sendHttp(client, httpHeader, httpBuf);
                        break;

                    case 1:
                        sendHttp(client, httpHeader, httpBuf);
                        break;

                    case 2:
                        sendHttp(client, httpHeader, httpBuf, true);
                        break;
                }
            }

            client->close();
        }
    }
}