There are a ton of Arduino-compatible IoT devices available on the market these days. Some with easy configurations and management, some with none at all. Personally, I prefer Particle devices for their easy-to-use, high security cloud. However, sometimes I don't need the hassle or security of managing access tokens and device IDs. And I know my potential users want to even know that stuff exists. What if I could manage the settings on the device itself and make it easily discoverable without having to check DHCP leases to figure out the device IP address? I can do just that by using Webduino and mDNS on the device itself!
mDNS (Multicast DNS) is a service that runs on a device to make it discoverable via local network DNS queries (that's the simple definition). It allows you to do something like ping mydevice.local
, and it will resolve mydevice.local
to an IP address on your local network. This will work from any modern operating system that supports mDNS, zeroconf, Bonjour, or avahi. Older versions of Windows may not support it inherently, but there are programs available to add support (most notably iTunes also installs Bonjour support).
Webduino is a simple web server library that allows you to run a web server on your Arduino (or compatible) microcontroller. There are also many forks available for a variety of different platforms. In my case, I will use the Webduino fork for Particle devices (Thanks Mat!).
mDNSFirst though, let's get mDNS set up. All of these examples are for Particle devices, but should be easily adapted for most other platforms. You'll want to start by including the mDNS library in your sketch. Inclusion of the library can vary depending on your build environment (web IDE vs Particle Dev vs CLI vs local toolchain).
#include "MDNS.h"
MDNS mdns
void setup() {
// Change "mydevice" to whatever you want to name your device
bool mdns_success = mdns.setHostname("mydevice");
if(mdns_success) {
// Change "MyDevice Config" to whatever you want to name
// the web server running on your device. This is purely
// ornamental and does not affect the actual web server.
mdns.addService("tcp", "http", 80, "MyDevice Config");
mdns.begin();
}
}
void loop() {
mdns.processQueries();
}
That was easy!
WebduinoThere are a couple of different approaches to using Webduino to manage your device. The first is to host all of the HTML locally on the device using EEPROM, flash, or some sort of external storage like an SD card. A second way is to host the HTML on a separate server but send data using HTTP POST or GET requests to the local device. I will illustrate both in this article.
Local HTMLIn this first example, we'll store all of the HTML on the device itself. To save room, I remove all of the unnecessary whitespace (spaces, tabs, and line breaks). This example also includes the mDNS code similar to above.
#include "MDNS.h"
MDNS mdns;
#define WEBDUINO_FAVICON_DATA ""
#define WEBDUINO_FAIL_MESSAGE ""
#include "WebServer.h"
WebServer webserver("", 80);
#define POST_NAME_LENGTH 32
#define POST_VALUE_LENGTH 32
// Our "settings"
String myValA = "B";
String myValB = "hello world";
bool myValC = true;
// All pages
P(Page_start) = "<!DOCTYPE html><html><head><title>My Device</title><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><meta charset=\"UTF-8\"></head><body>";
P(Page_css) = "<style type=\"text/css\">html,body{font-family:sans-serif;}fieldset{margin-left:auto;margin-right:auto;max-width:480px;border-radius:8px;}</style>";
P(Page_end) = "</body></html>";
// Form-specific
P(Form_css) = "<style type=\"text/css\">p{clear:both;width:100%;line-height:1.5em;}label{width:49%;text-align:right;display:inline-block;line-height:1.5em;float:left;margin-left:-1em;}select,input[type=\"text\"]{width:49%;text-align:left;float:right;}#c{text-align:left;float:left;margin-left:2em;}</style>";
P(Form_settings) = "<form method=\"POST\" action=\"save.html\"><fieldset><legend>MyDevice Config</legend><p><label for=\"a\">First Setting</label><select id=\"a\" name=\"a\"><option value=\"r\">Red</option><option value=\"g\">Green</option><option value=\"B\">Blue</option></select></p><p><label for=\"b\">Second Setting</label><input type=\"text\" id=\"b\" name=\"b\"></p><p><label for=\"c\">Third Setting</label><input type=\"checkbox\" id=\"c\" name=\"c\"></p><p> </p><p><label for=\"s\"> </label><input type=\"submit\" id=\"s\" value=\"Save\"></p></fieldset>";
P(Form_javascript1) = "<script type=\"text/javascript\">";
P(Form_javascript2) = "window.onload=function(){$a=document.querySelector('#a');$b=document.querySelector('#b');$c=document.querySelector('#c');$a.value=a;$b.value=b;if(c==1)$c.checked=\"checked\";}";
P(Form_javascript3) = "</script>";
// Save page
P(Save_fieldset) = "<fieldset><legend>MyDevice Config</legend><p>Your settings have been saved. You will be redirected in 5 seconds, or click <a href=\"/\">here</a> to continue.</p></fieldset>";
P(Save_redirect) = "<meta http-equiv=\"refresh\" content=\"5;URL=/\">";
// Fail page
P(Fail_message) = "<p>Not here. Go away.</p><p>ಠ_ಠ</p>";
// index.html
void web_index(WebServer &server, WebServer::ConnectionType type, char *, bool) {
server.httpSuccess();
server.printP(Page_start);
server.printP(Form_settings);
server.printP(Page_css);
server.printP(Form_css);
server.printP(Form_javascript1);
server.printP("var a=\""+String(myValA)+"\";");
server.printP("var b=\""+myValB+"\";");
server.printP("var c="+String(myValC ? 1 : 0)+";");
server.printP(Form_javascript2);
server.printP(Form_javascript3);
server.printP(Page_end);
}
// save.html
void web_save(WebServer &server, WebServer::ConnectionType type, char *, bool) {
URLPARAM_RESULT rc;
char name[POST_NAME_LENGTH];
char value[POST_VALUE_LENGTH];
server.httpSuccess();
server.printP(Page_start);
server.printP(Save_fieldset);
server.printP(Save_redirect);
server.printP(Page_css);
server.printP(Page_end);
// c has to be tracked a little differently since it's an HTML checkbox.
// If a checkbox is left unchecked, it simply does not get POSTed with the
// form data. So, by default this will be "0" unless we see it in the POST
// data, then we can set it to "1" if it's passed.
bool c = 0;
// Loop through POSTed data
while(server.readPOSTparam(name, POST_NAME_LENGTH, value, POST_VALUE_LENGTH)) {
// Because strings are easier to test/manipulate
String _name = String(name);
String _value = String(value);
if(_name.equals("a"))
myValA = _value;
else if(_name.equals("b"))
myValB = _value;
else if(_name.equals("c"))
c = 1;
}
myValC = c;
}
// Bad requests
void web_fail(WebServer &server, WebServer::ConnectionType type, char *, bool) {
server.httpFail();
server.printP(Page_start);
server.printP(Fail_message);
server.printP(Page_end);
}
void setup() {
bool mdns_success = mdns.setHostname("mydevice");
if(mdns_success) {
mdns.addService("tcp", "http", 80, "MyDevice Config");
mdns.begin();
}
webserver.setDefaultCommand(&web_index);
webserver.setFailureCommand(&web_fail);
webserver.addCommand("index.html", &web_index);
webserver.addCommand("save.html", &web_save);
webserver.begin();
}
void loop() {
mdns.processQueries();
char web_buff[64];
int web_len = 64;
webserver.processConnection(web_buff, &web_len);
}
All of that should produce something that looks like this in the browser if you go to http://mydevice.local
.
It's minimal, but there is still a fair amount of HTML, CSS, and JavaScript for styling and a reasonable user experience. However, it's still 1,361 bytes and seems to cause a little strangeness on the device after a while. Which leads to the next section...
Remote HTMLThe HTML for your device doesn't have to be hosted on the device itself. You can host it anywhere! It can be a simple Raspberry Pi on your local network, or even a web page out on the internet. All it will need to do is load the values from your device using something like AJAX and then save the data in the same way. Since the remote HTML will point to http://mydevice.local
for loading and saving of data, your data will still only stay on your wireless network, so it's as secure as your wifi network and does not go out to the public internet. You can even use a frame or iframe in the HTML hosted on your local device so that it appears that everything is happening on http://mydevice.local
!
Here's the updated sketch that will load the remote web page inside an iframe (so it still looks like it's on your device).
#include "MDNS.h"
MDNS mdns;
// The URL of the remote web page
#define REMOTE_URL "http://mypublicserver.com/mydevice_config/"
#define WEBDUINO_FAVICON_DATA ""
#define WEBDUINO_FAIL_MESSAGE ""
#include "WebServer.h"
WebServer webserver("", 80);
#define POST_NAME_LENGTH 32
#define POST_VALUE_LENGTH 32
// Our "settings"
String myValA = "B";
String myValB = "hello world";
bool myValC = true;
// index.html
void web_index(WebServer &server, WebServer::ConnectionType type, char *, bool) {
server.httpSuccess();
server.print("<!DOCTYPE html><html><head><title>MyDevice Config</title><style type=\"text/css\">html,body,iframe{position:absolute;top:0;right:0;bottom:0;left:0;border:0;width:99%;height:99%;}</style></head><body><iframe src=\""+String(REMOTE_URL)+"/index.html\"></iframe></body></html>");
}
// settings.json
void web_settings(WebServer &server, WebServer::ConnectionType type, char *, bool) {
server.httpSuccess("application/json");
server.print("\
{\
\"a\":"+myValA+",\
\"b\":"+myValB+",\
\"c\":"+String(myValC ? 1 : 0)+",\
}");
}
// save.html
void web_save(WebServer &server, WebServer::ConnectionType type, char *, bool) {
URLPARAM_RESULT rc;
char name[POST_NAME_LENGTH];
char value[POST_VALUE_LENGTH];
server.httpSeeOther(String(SETTINGS_URL)+"/index.html");
// c has to be tracked a little differently since it's an HTML checkbox.
// If a checkbox is left unchecked, it simply does not get POSTed with the
// form data. So, by default this will be "0" unless we see it in the POST
// data, then we can set it to "1" if it's passed.
bool c = 0;
// Loop through POSTed data
while(server.readPOSTparam(name, POST_NAME_LENGTH, value, POST_VALUE_LENGTH)) {
// Because strings are easier to test/manipulate
String _name = String(name);
String _value = String(value);
if(_name.equals("a"))
myValA = _value;
else if(_name.equals("b"))
myValB = _value;
else if(_name.equals("c"))
c = 1;
}
myValC = c;
}
void setup() {
bool mdns_success = mdns.setHostname("mydevice");
if(mdns_success) {
mdns.addService("tcp", "http", 80, "MyDevice Config");
mdns.begin();
}
webserver.setDefaultCommand(&web_index);
webserver.addCommand("save.html", &web_save);
webserver.addCommand("settings.json", &web_settings);
webserver.begin();
}
void loop() {
mdns.processQueries();
char web_buff[64];
int web_len = 64;
webserver.processConnection(web_buff, &web_len);
}
And here is the HTML hosted on the remote server:
<!DOCTYPE html>
<html>
<head>
<title>MyDevice Config</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta charset="UTF-8">
<style type="text/css">
html,
body {
font-family: sans-serif;
}
fieldset {
margin-left: auto;
margin-right: auto;
max-width: 480px;
border-radius: 8px;
}
p {
clear: both;
width: 100%;
line-height: 1.5em;
}
label {
width: 49%;
text-align: right;
display: inline-block;
line-height: 1.5em;
float: left;
margin-left: -1em;
}
select,
input[type="text"] {
width: 49%;
text-align: left;
float: right;
}
#c {
text-align: left;
float: left;
margin-left: 2em;
}
</style>
<!-- Download microajax.minified.js from https://code.google.com/archive/p/microajax/ -->
<script type="text/javascript" src="microajax.minified.js"></script>
<script type="text/javascript">
var $a, $b, $c;
var mA = microAjax;
window.onload = function() {
$a = document.querySelector('#a');
$b = document.querySelector('#b');
$c = document.querySelector('#c');
// Load our data
get_settings();
}
// Get the settings from mydevice.local
function get_settings() {
mA('http://mydevice.local/settings.json', function(res) {
var d = JSON.parse(res);
console.log('settings', d);
document.querySelector('#loading').style.display = 'none';
$a.value = d.a;
$b.value = d.b;
if(d.c==1)
$c.checked = "checked";
document.querySelector('#settings').style.display = 'block';
});
}
</script>
</head>
<body>
<p id="loading">Please wait. Loading settings . . .</p>
<form id="settings" method="POST" action="http://mydevice.local/save.html" style="display:none">
<fieldset>
<legend>MyDevice Config</legend>
<p>
<label for="a">First Setting</label>
<select id="a" name="a">
<option value="R">Red</option>
<option value="G">Green</option>
<option value="B">Blue</option>
</select>
</p>
<p>
<label for="b">Second Setting</label>
<input type="text" id="b" name="b">
</p>
<p>
<label for="c">Third Setting</label>
<input type="checkbox" id="c" name="c">
</p>
<p> </p>
<p>
<label for="s"> </label>
<input type="submit" id="s" value="Save">
</p>
</fieldset>
</form>
</body>
</html>
The code above is nearly identical to the original local HTML, but it requires much less space on the device and can be styled and customized as needed without having to flash new code to the device!
ConclusionThis is just a starting point with many different directions to go. I personally plan on extending this setup to use the Particle API JS library to provide things like automatic updates to devices, and even create setup wizards for things like word clocks so users can easily configure them for their network and other fun features.
Comments