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
Revision:
0:f78e57015038
Child:
1:2d2517f82319
diff -r 000000000000 -r f78e57015038 main.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/main.cpp	Wed Nov 11 16:56:02 2020 +0000
@@ -0,0 +1,720 @@
+/*********************************************
+ * 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);
+}
+
+/* check the tablebitmap and return:
+ * 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
+ * @param
+ * @retval
+ */
+void printJavaScriptFunctions(void)
+{
+    setHeaderToHttp200ok();
+
+    //
+    // 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.
+    /*$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, "  ");
+    //    strcat(httpBuf, "Date:");
+    //    addHttpDataUint16(tmNow.tm_year + 1900);
+    //    strcat(httpBuf, "-");
+    //    addHttpDataUint8(tmNow.tm_mon + 1);
+    //    strcat(httpBuf, "-");
+    //    addHttpDataUint8(tmNow.tm_mday);
+    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 row=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();
+        }
+    }
+}