![](/media/cache/profiles/7c39a5f991bef4a1e34187677a65871d.jpg.50x50_q85.jpg)
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 isOFF
.
- 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 theSave
button. - The default password is "secret". But you can change it by editing the source code in
main.cpp
.
![]() | ![]() |
Diff: main.cpp
- Revision:
- 0:f78e57015038
- Child:
- 1:2d2517f82319
--- /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=/>[←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=/>[←Back]</a> <a href=/sw>[↻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)> </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=/>[↻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(); + } + } +}