Arduino MKR WiFi
The first edition of this book was reviewed by someone who subsequently built what was described as a motorised skylight opener/closer. I thought that a project like that would sit very well on one of the new Arduino MKR WIFI 1010 boards as they come with Wi-Fi and Bluetooth connectivity built in.
We could start with a spec for a real world installation and then model that on a breadboard to work up some controlling code and then add a touch of HTML to interact with the prototype from a web browser.
I suspect that it would be a good idea to fit limit switches to detect when the skylight or window was fully open or closed as that should restrict the direction any motor should run in and could be useful feedback to any remote display.
For motive power, if you were tackling this project for real, you would probably aspire to a linear actuator which normally means running a 12 volt DC motor although some models use a stepper motor. On a budget, you might consider some sort of worm drive and gearing attached to whatever you could lay your hands on. A recycled windscreen wiper motor could be used perhaps as that has an inbuilt worm screw that could be mated with some additional gears to get a long enough movement although some of the wiper motor wiring would need to be subverted to override things like “self-parking”. You could also consider a very high torque servo although positioning it and protecting it from weather might be an extra problem. Thinking about it, a servo could be a good choice for a regular window with a vertical hinge.
The more obvious motor choices would mean we were looking to control a DC motor with useful information coming in from two limit switches. Now there are not a lot of shields designed for the MKR board format at the time of writing so it looked necessary to take a small step back in pre-packaged motor control. If the motor does not draw too much in the way of amps then the Toshiba’s TB6612FNG dual motor driver could be just the ticket and this has the advantage that it can be controlled with a logic voltage range of 2.7 to 5.5 volts so would suite the 3.3 volt logic supplied by the MKR WIFI 1010. The controller can manage DC motor voltages up to 13.5 volts. There are several small format and very low cost boards about featuring this motor driver. One note of caution, these boards can get very hot when active and this would need to be taken into account when testing or when designing an enclosure – a heat sink might be advisable.
Superficially, this project felt like it should use a software state machine. There are “states” for open, closed, part open, closing and opening. However some further though had me realise that there was nothing happening in three of those states and the two states where a motor would be running were only differentiated by the motor direction. It proved in the end, that the device state could be happily represented by a single variable.
Most unusually for an Arduino project, we can give some thought to a user interface. What can we tell the user about the current state? What commands would the user want to see executed by our device?
Motor Control
The hardware connection choices to the MKR 1010 board can be quite flexible. External interrupts are available on 8 pins which gives us lots of options for the limit switches. We have enough digital pins to manage the motor control and Pulse Width Modulation (PWM) can be applied to 12 pins so picking one to manage the motor speed should not present problems. Obviously, there is a fair amount of overlap between functions on individual pins as this is not a board bristling with them but there are more than enough for our immediate needs.
The motor control board pin-out
The incoming +ve feed for the motors should be connected to VM. VCC is for the logic and is probably best fed from the Arduino board to match the control voltage level. The two sets A01/A02 and B01/B02 are the feeds to the motors. All of the ground connections are linked. For high current motors you would probably choose to directly solder the motor voltage hook-ups to the board. As the board can control two motors, there are two sets of logic level connections that can be hooked up to an Arduino. AIN1 and AIN2 control motor A direction with PWMA being fed a pulse width modulated signal to control the motor speed. There is a matching B set to control motor B. The STBY (standby) pin must be set high for either or both motors to be fed power.
I wired up a simple test rig for the code development as shown in the next diagram. At this initial stage I only linked one push to make button as shown but that was all I needed to test the external interrupt code along with digital and PWM output.
This diagram assumes that the Arduino board will be powered through the USB connection and that the trial DC motor will be fed power through the breadboard power supply (part) shown.
Logic level links in addition to VCC are:
MKR 1010 | Motor Controller |
D4 | PWMA |
D3 | AIN2 |
D2 | AIN1 |
D1 | STBY |
D0 | Push switch |
Sparkfun publish an open source library that you can use to manage these motor controller boards and that is probably a good choice although I have included the full code here as it is both straightforward and short.
A program for some Initial tests to check motor control follows starting with the global variables and setup() function. There are some lines commented out that we will be using so please include them now.
const int PWMA = 4; const int AIN2 = 3; const int AIN1 = 2; const int STBY = 1; const int INTPIN = 0; int motorASpeed = 255; //volatile bool stopA = false; void setup() { Serial.begin(115200); delay(1000); pinMode(PWMA, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(AIN1, OUTPUT); pinMode(STBY, OUTPUT); pinMode(INTPIN, INPUT_PULLUP); //attachInterrupt(digitalPinToInterrupt(INTPIN), setStopA, LOW); test1(); }
Then the motor control functions; forward, brake, stop and reverse. Of course, motor direction is a bit arbitrary for the first runs. Once the required directions have been established then the motor connections may need to be swapped or minor changes made to the code.
void motorAForward() { Serial.println("Forward"); analogWrite(PWMA, motorASpeed); digitalWrite(AIN1, HIGH); digitalWrite(AIN2, LOW); digitalWrite(STBY, HIGH); } void motorABrake() { Serial.println("Brake"); digitalWrite(AIN1, HIGH); digitalWrite(AIN2, HIGH); analogWrite(PWMA, 0); } void motorAStop() { Serial.println("Stop"); digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW); analogWrite(PWMA, 0); digitalWrite(STBY, LOW); // would also stop motor B } void motorABack() { Serial.println("Backward"); analogWrite(PWMA, motorASpeed); digitalWrite(AIN1, LOW); digitalWrite(AIN2, HIGH); digitalWrite(STBY, HIGH); }
I ran a sequence of tests adjusting the value of motorASpeed with the motor under a reasonable load. I wanted to establish a suitable value for a slow (but not too slow) running speed.
void test1() { motorAForward(); delay(2000); motorABrake(); delay(500); motorAStop(); delay(1000); motorABack(); delay(2000); motorABrake(); delay(500); motorAStop(); }
Adding an External Interrupt
Then I added the lines (previously shown commented out) for the volatile bool stopA, the external interrupt on pin 0 and then added an ISR.
void setStopA() { stopA = true; }
Then added code to the loop() function to monitor that Boolean value.
void loop() { if(stopA) { motorAStop(); Serial.println("Limiter Stop"); stopA = false; } }
With the call in setup() to test1() replaced by a call to just motorAForward() by itself, testing the external interrupt from a future limit switch can then follow. Upload the changes and give the prototype limit switch a trial.
First steps with WiFi
So far so good, now let’s look at some code that reaches out to the ESP32 WiFi module. We need to include the WiFiNINA library that you might need to install using the “manage libraries” option under the Tools menu. So include that library into a new program.
#include <WiFiNINA.h>
template<class T> inline Print &operator<<(Print &obj, T arg) {
obj.print(arg); return obj;}
void setup() {
Serial.begin(115200);
while (!Serial) {}
Serial.println("Scanning available WiFi access...");
listWiFi();
}
void listWiFi() {
int wifiCount = WiFi.scanNetworks();
for(int i = 0; i < wifiCount; i++) {
Serial << i << ' ' << "SSID: " << WiFi.SSID(i) << '\n';
}
if(wifiCount < 1) {
Serial.println("No WiFi services found");
}
}
void loop() {
}
The scanNetworks() static method is exposed by the WiFi object and the code above lists the content of the resulting SSID name array, assuming that one or more WiFi SSID is identified.
Now we can try connecting to your WiFi. A useful tool for Windows that you might like to download is the “Advanced IP Scanner”. This is an easy way to list devices connected to your local area network and to see their IP address although we will get the Arduino to display its IP address via the Serial Monitor.
The online web based Arduino IDE has a neat way of hiding sensitive data like your local SSID and password separate from the rest of the code base which we can emulate. Start a new program and then add a new tab and name it Secret.h. That tab can then have the following code added – filling in your WiFi network values.
const char SECRET_SSID[] = "Your-SSID"; const char SECRET_PASSWORD[] = "Your-Password";
Now a short program to use those values to connect to the local network.
#include <WiFiNINA.h>
#include "Secret.h"
const char* mySSID = SECRET_SSID;
const char* myPass = SECRET_PASSWORD;
template<class T> inline Print &operator<<(Print &obj, T arg) {
obj.print(arg); return obj;}
void setup() {
Serial.begin(115200);
while (!Serial) {}
Serial.println("Connecting to WiFi...");
connectToWiFi();
}
void connectToWiFi() { while (WiFi.begin(mySSID, myPass) != WL_CONNECTED) {
delay(500);
}
Serial.println("WiFi connected");
Serial << "Local IP: " << WiFi.localIP() << '\n';
}
You could also check the signal strength and MAC address with the following lines added to the connectToWiFi() function and then an extra function (called showMACAddress()).
Serial << "Signal strength: " << WiFi.RSSI() << '\n'; byte macAdd[6]; WiFi.macAddress(macAdd); Serial.print("MAC Address: "); showMACAddress(macAdd);
void showMACAddress(byte add[]) { for(int i = 5; i >= 0; i--) { if(add[i] < 16) {Serial.print('0');} // padd hex Serial.print(add[i], HEX); if(i > 0) { Serial.print(':'); } } Serial.println(); }
A signal strength between 0 (zero) and -70 (minus 70) would be good with values nearer to zero being better.
The Internet
That’s a good start – now can we reach out to the Internet? An obvious thing to try would be setting the real time clock (RTC) on the MKR 1010 board. We might consider adding time based commands to our project. The RTC is a good foundation for timing events on this board where timer interrupts are a little less easy to program than they are on the original Arduino family.
We need to add #include statements for <WiFiUdp.h> and <RTCZero.h>. The WiFiUpd files will be located in the WiFiNINA folder so will already be available. You might need to use the “manage libraries” menu option to install the RTCZero library. Before I added the required code I used the Internet to locate the IP Address of a local (read UK) NTP server. Note how the IP address is passed into the constructor for a new IPAddress class instance.
I added a fresh tab in the IDE and named it “ReadInternetTime” without an extension. This meant that the compiler would treat the code on that tab as if it were part of the code on the main tab. I only did this to make the new functions easier to find and review. The new code in that new tab started with some definitions.
unsigned int localPort = 2390; // local port to listen for packets IPAddress timeServer(143,210,16,201); // 0.uk.pool.ntp.org note commas const int NTP_PACKET_SIZE = 48; // NTP time stamp in the first 48 bytes byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold packets const int GMT_ADJUST = 1 * 60 * 60; //time zone adjustment (BST) unsigned long epoch;
Note the line setting a GMT_ADJUST value. In this sample code I have set an adjustment of plus one hour for British Summer Time (BST) but if you live in a different time zone then you will have to set a positive or negative (or zero) adjustment of your own.
Then we have a function getCurrentTime().
void getCurrentTime() { int pingCount = 0, maxPings = 6; do { epoch = getLinuxEpoch(); pingCount++; } while ((epoch == 0) && (pingCount <= maxPings)); if (pingCount > maxPings) { Serial.println("NTP did not respond"); } else { rtc.setEpoch(epoch + GMT_ADJUST); Serial << "Time value: " << rtc.getEpoch() << '\n'; } }
That function makes several attempts, by calling getLinuxEpoch(), to contact the NTP server and that function calls sendNTPPacket() to send the request via a WiFiUDP class instance.
Now is a good time to add instances of the RTCZero class and WiFiUPD to the code, then add two new function calls to the startup() function. Then add getLinuxEpoch().
RTCZero rtc; WiFiUDP Udp; void setup() { Serial.begin(115200); while (!Serial) {} Serial.println("Connecting to WiFi..."); connectToWiFi(); rtc.begin(); // start the clock getCurrentTime(); showTime(); }
unsigned long getLinuxEpoch() { Udp.begin(localPort); sendNTPPacket(timeServer); // send an NTP packet to NTP server // wait a bit delay(1000); if ( Udp.parsePacket() ) { Serial.println("NTP time received"); // read data into buffer Udp.read(packetBuffer, NTP_PACKET_SIZE); // read 48 bytes unsigned long highWord = word(packetBuffer[40], packetBuffer[41]); unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]); // combine the four bytes into a long integer // should be seconds since Jan 1 1900 unsigned long ntpTime = highWord << 16 | lowWord; Udp.stop(); // now adjust NTP time to Linux time which starts 70 years later const unsigned long seventyYears = 2208988800UL; return (ntpTime - seventyYears); } else { Udp.stop(); return 0; } }
If we upload and run this code then, as long as everything went to plan, our Arduino has now sent and received an Internet packet. Not quite the world wide web (yet) but we are getting there. Don’t try this at work though as your company network security people will probably have blocked all of the inbound ports and you won’t get a reply from any NTP server.
As we have gone to all that trouble to read the time then we should display the time and date from the RTC.
void showTime() { Serial << rtc.getDay() << '/' << rtc.getMonth() << "/20" << rtc.getYear() << " " << rtc.getHours() << ':' << rtc.getMinutes() << ':' << rtc.getSeconds() << '\n'; }
As you will have noted, the RTC deals in seconds. While it does not meet the need for sub second time interrupts it can have “alarms” set for regular interrupts at intervals of minutes, hours, days, months, years and at precisely set future dates and times. We included the RTC here to trial the capacity of the MKR 1010 to chat to distant Internet servers but while we are passing we could try setting an alarm before moving on.
Add a couple of global variables at the start of the program.
volatile unsigned long rtcEpoch = 0; unsigned long lastEpoch = 0;
Then a new function called setAlarm() and then alarmCall() to act as an ISR.
void setAlarm(){ rtc.setAlarmSeconds(5); rtc.attachInterrupt(alarmCall); rtc.enableAlarm(rtc.MATCH_SS); // match the alarm time every minute } void alarmCall() { rtcEpoch = rtc.getEpoch(); }
Add a call to setAlarm() to the end of the startup() function and then add the following to the loop() function.
void loop() { if(rtcEpoch != lastEpoch) { lastEpoch = rtcEpoch; showTime(); } }
When you now run the program, the RTC will be set to the current time and then the RTC alarm will trigger once every minute. When I ran this, the time had advanced to six seconds after the minute by the time that the showTime() function had extracted the seconds value from the RTC but it was reasonably clear that the alarm had called the interrupt while the set alarm time was correct.
Why not look for the RTCZero.h file in the Arduino libraries folder on your PC and review the available alarm options? They all work in a similar way by calling the RTCZero.enableAlarm() method with the appropriate mask from the AlarmMatch enum.
Sending and receiving individual data packets via the Internet is a good start but it is now time to join Sir Tim Berners-Lee’s World Wide Web and take the opportunity to persuade our MKR Arduino to present a User Interface (UI) to the world.
An HTML User Interface
Why not start once again with a new program. Then create a new Secret.h tab and copy the two lines from the same tab that was part of the previous program. We can also copy/paste the connectToWiFi() function as well. The program should then start as follows.
#define BUFF_SIZE 81
#include <WiFiNINA.h>
#include "Secret.h"
const char* mySSID = SECRET_SSID;
const char* myPass = SECRET_PASSWORD;
const char pageRequest[] = "GET / HTTP/1.1";
WiFiServer server(80); // port 80
char buffer[BUFF_SIZE];
const long timeOut = 1000;
unsigned long waitStart;
int pageCount = 0;
WiFiClient client;
template<class T> inline Print &operator<<(Print &obj, T arg) {
obj.print(arg); return obj;}
You will see that an instance of WiFiServer has been created to listen on port 80 which is the default for access using a web browser using HTTP. The WiFiClient class instance will manage communications with any browser that connects to our server. The function of the other new variables will become clear as we develop the code.
The << operator for Print is going to prove useful when we talk to the WiFiClient class instance as you will see shortly.
The setup() function is short and sweet. It starts the server instance once the board has connected to WiFi.
void setup() { Serial.begin(115200); while (!Serial) {} connectToWiFi(); server.begin(); // start the server }
I trimmed my version of the connectToWiFi() function so that it only showed the IP address – which is a value we are going to be making use of.
void connectToWiFi() { while (WiFi.begin(mySSID, myPass) != WL_CONNECTED) { delay(500); } Serial << "WiFi connected\n"; Serial << "Local IP: " << WiFi.localIP() << '\n'; }
The management of the browser connection is based upon code located in the loop() function which looks rather lengthy but breaks down into nice logical steps.
void loop() { client = server.available(); if(client) { int charCount = clearBuffer(); Serial << "Browser connected\n"; waitStart = millis(); while (client.connected()) { if(client.available()) { char c = client.read(); Serial << c; if(c == '\n' || c == '\r'){ if(strncmp(pageRequest, buffer, 14) == 0) { sendHTML(); break; } else { charCount = clearBuffer(); waitStart = millis(); } } else { if(charCount < 79) { buffer[charCount] = c; charCount++; } } } else { // trigger timeout when browser silent if((unsigned long)millis() - waitStart >= timeOut) { break; } } } client.flush(); client.stop(); Serial << "Browser disconnected\n"; } }
The code starts by checking if there is a client waiting to connect using the server.available() method. If the WiFiClient instance is created and not null then we have a connection and can proceed. The code then clears the buffer that will later allow the code to react to some of the incoming browser requests and then sets the waitStart variable to the value of millis(). This will allow our code to drop a browser connection if it stops being active but fails to disconnect. We then have a while loop that is active while the client is connected.
The first action within the while loop is to check if the client has incoming characters that can be read. If there are, then they are read in turn. Each character is echoed to the Serial Monitor as it is interesting to see just how much information flows to the server from a browser when it connects.
The character is then checked to see if it is a line termination character. If it is, then the buffer content (if any) is checked to see if it holds a “get request” that we might be interested in. At this stage we are just checking for a match to our pageRequest string. If there is a match then the HTML page content is sent to the browser by the sendHTML() function. If HTML is sent then the while loop is exited and the connection closed. If the buffer content is not something we are looking for then it is cleared and the elapsed time check millis() value reset. Any other character is added to the buffer (although there is a check to avoid overflows).
If there are no characters waiting to be read then the millis() value is checked to see if the wait time has been exceeded. If it has, then the while loop is exited to again close the connection.
The clearBuffer() function uses memset to set all of the array values to zero.
int clearBuffer() { memset(buffer, 0, BUFF_SIZE); return 0; }
The sendHTML() function sends some fairly primitive page data and HTML to the browser via the WiFiClient class. We will improve on this a bit later.
Note that out << print operator works with the WiFiClient in just the same way as it works with the Serial class because both use the Print object in a similar manner.
void sendHTML() {
Serial << "\nSending HTML\n";
// HTTP preamble first
client << "HTTP/1.1 200 OK\n";
client << "Content-Type: text/html\n";
client << "Connection: close\n\n"; // extra linefeed important
// now the HTML forming the web page
client << "<!DOCTYPE HTML>\n";
client << "<html>\n";
client << "Hello from Arduino MKR 1010 Server<br />\n";
pageCount++;
client << "Page served Counter: " << pageCount << '\n';
client << "</html>\n";
}
If you are not familiar with even the basics of HTML then I recommend doing an Internet search for an introductory lesson. At this stage a fairly concise run around HTML will do but if you want to be thorough then I recommend the materials maintained by Mozilla at https://developer.mozilla.org/en-US/docs/Web.
Now it is time to give the program a run and try connecting a web browser to the Arduino based server. Run the program and take note of the IP address displayed in the Serial Monitor window. Open a new browser tab and type HTTP:// followed by the IP address into the browser address bar, then hit return. My value was http://192.168.0.52 and yours will probably be similar. The browser should display something like:
Take a look at the Serial Monitor output to see how the conversation between the web browser and the server progressed. There should be a lot to see.
Now use the refresh button on the browser window to make a second set of connections to the server. You should see that the “Page Served Counter” increments each time you initiate a page refresh and therefore a new set of connections.
We now have the foundation for a web based User Interface (UI) so we had better try making it a bit more interactive.
You may have noticed your browser indicating that the connection to your Arduino board was “Not Secure”. This is because many modern browsers now warn their users if a given connection is not encrypted using the Secure Sockets Layer (SSL) protocol. At the time of writing, it looks like efforts are being made to enable the server to interact with browsers using SSL so it is worth checking to see if an update has enabled this.
Try amending the sendHTML() function by replacing the line that displayed the “page served” counter with this one:
client << "<input type=\"button\" onclick=\"location.href=\'/A\';\" value=\"Click Me\" />\n";
This line includes a lot of backslashes that are used to escape the single and double quote marks included within the string. The intention is to send the following HTML line to the browser:
<input type="button" onclick="location.href='/A';" value="Click Me" />
That line creates an HTML button and attaches a smidgeon of JavaScript to any click event to make a fresh request to the server. Try uploading the program, then again connecting with your browser and then clicking the button created by the new HTML.
If you watch the Serial Monitor you will see a slightly different GET request sent by the browser when the button is clicked. You should see something like ”GET /A HTTP/1.1” and this will be repeated several times as the browser attempts to get a response from the server. The /A of course, comes from the response to the click event and represents a request for the server to send the content of a notional page named A that it expects our board to be able to serve. We can, of course spot that request and generate any response we choose – perhaps after starting or stopping our skylight manipulating motor.
If you take a look at the browser address bar you will see that the address has now changed and now has the suffix “/A”. This might be seen as a disadvantage with this approach as some of the inner workings of the UI get exposed there. The alternatives might include using POST rather than GET but that would complicate the HTML or we could use a technology called Ajax and a lot more JavaScript. While those alternatives are excellent solutions for commercial projects I suspect they are beyond the intended scope of this chapter. So maybe we should proceed with the simplest strategy that works. That of course leaves lots of scope to further explore the options in any project of a similar nature you might care to undertake.
Skylight automation
We now have drafts for the component parts of a software program controlling our prototype hardware sketched out. The chief complication that I see in drawing it all together is managing the motor and running the UI – both from within the loop() function.
This is the time to start writing the final program and to re-visit the test rig with the motor and its control board. Also time to hook the second push to make button up to (maybe) pin A1 on the Arduino board to stand in place of the second limit switch.
I broke the program code into several sections, each on a different tab in the Arduino IDE. I did this just to organise the code to make it easier to quickly locate a given function. You can, of course, just type everything into the same tab but I would recommend breaking it up. The first new tab was called Secret.h and I copied the two lines into this from previous programs. I then added a tab called MotorControl (no extension). I copied and pasted most of the code here from an earlier program in this chapter although there are some name changes and a new function to set the motor control pins.
const int PWMA = 4; const int AIN2 = 3; const int AIN1 = 2; const int STBY = 1; byte motorSpeed = 180; void setMotorPins() { pinMode(PWMA, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(AIN1, OUTPUT); pinMode(STBY, OUTPUT); }
void motorOpen() { analogWrite(PWMA, motorSpeed); digitalWrite(AIN1, HIGH); digitalWrite(AIN2, LOW); digitalWrite(STBY, HIGH); } void motorBrake() { digitalWrite(AIN1, HIGH); digitalWrite(AIN2, HIGH); analogWrite(PWMA, 0); } void motorStop() { digitalWrite(AIN1, LOW); digitalWrite(AIN2, LOW); analogWrite(PWMA, 0); digitalWrite(STBY, LOW); } void motorClose() { analogWrite(PWMA, motorSpeed); digitalWrite(AIN1, LOW); digitalWrite(AIN2, HIGH); digitalWrite(STBY, HIGH); }
The next new tab I called LimitSwitches and there I added most of the code associated with them.
Starting with the definitions for the pin numbers and then an enum and variables to hold the limit switch states. Note how the enum is defined as a byte (8 bit integer) rather than a default int value.
const int LIMIT_CLOSED = 0; const int LIMIT_OPEN = 16; // Pin A1 enum SwitchStates: int8_t {OPEN, CLOSED}; // switch open or closed byte closedLimitState = OPEN; volatile byte newClosedState = OPEN; byte openLimitState = OPEN; volatile byte newOpenState = OPEN;
Then we have a function called setupLimitSwitches() which sets the pins and interrupts. This is followed by the two ISRs.
void setupLimitSwitches() { pinMode(LIMIT_CLOSED, INPUT_PULLUP); pinMode(LIMIT_OPEN, INPUT_PULLUP); attachInterrupt(LIMIT_CLOSED, setClosedLimit, CHANGE); attachInterrupt(LIMIT_OPEN, setOpenLimit, CHANGE); initLimitStates(); } void setClosedLimit() { newClosedState = (digitalRead(LIMIT_CLOSED) == LOW) ? CLOSED : OPEN; } void setOpenLimit() { newOpenState = (digitalRead(LIMIT_OPEN) == LOW) ? CLOSED : OPEN; }
As the limit switches can have one of two states we have moved on from just setting a Boolean and we now have a volatile byte variable to record the change detected by the pin.
The function made a call to initLimitSwitches() which is a function that is used to detect the initial start state. We need to know if the skylight is open, closed or just part open before anything much else can happen.
void initLimitStates() { newClosedState = closedLimitState = (digitalRead(LIMIT_CLOSED) == LOW) ? CLOSED : OPEN; newOpenState = openLimitState = (digitalRead(LIMIT_OPEN) == LOW) ? CLOSED : OPEN; if(closedLimitState == CLOSED) { skylightState = FULLY_CLOSED; } else if(openLimitState == CLOSED) { skylightState = FULLY_OPEN; } else { skylightState = PART_OPEN; // as it can't be moving yet } }
Now we need a function to check for changes to the limit switch state and to trigger any appropriate responses.
void checkLimitStates() { if(newOpenState != openLimitState) { if(newOpenState == CLOSED) { motorStop(); skylightState = FULLY_OPEN; if(Serial) { Serial << "Stopped in fully open position\n"; } } openLimitState = newOpenState; } if(newClosedState != closedLimitState) { if(newClosedState == CLOSED) { motorStop(); skylightState = FULLY_CLOSED; if(Serial) { Serial << "Stopped in fully closed position\n"; } } closedLimitState = newClosedState; } }
I called my next new tab “ManageUI” and put the code for clearing the buffer used to store incoming chars from the browser and for generating an HTML response for all of the browser requests.
void clearBuffer() { memset(buffer, 0, BUFF_SIZE); charCount = 0; }
The clearBuffer() function was slightly changed but the sendHTML() function introduces several new items. We can take this function in three slices. The first slice sends the data preceding the HTML.
void sendHTML() { client << "HTTP/1.1 200 OK\n"; client << "Content-Type: text/html\n"; if(skylightState == OPENING || skylightState == CLOSING) { client << "Refresh: 3\n"; // ask for refresh at 3 second intervals } client << "Connection: close\n\n"; // extra linefeed important
That section introduces a new if statement that asks the browser to refresh the page at three second intervals if the motor is opening or closing the skylight – that is, if the state is likely to change when a limit switch is triggered.
The next sections adds a <head> tag to the HTML and includes a page title and a <meta> line to improve the rendering of the HTML on devices such as your phone or tablet. After the <head> tag is closed we have a <body> tag where we lay out the UI. To avoid asking you to read up about CSS (assuming you have no prior experience of that) I have embedded some style instructions within the HTML elements.
client << "<!DOCTYPE HTML>\n";
client << "<html>\n<head>\n";
client << "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
client << "<title>Skylight Automation</title>\n</head>\n";
client << "<body><div style=\"width:100%; height: 100%; ";
client << "background-color: antiquewhite;\">\n";
client << "<div style=\"text-align: center; border: 3px solid blue;\">\n";
client << "<h1>Automated Skylight</h1><br />\n";
client << "Your skylight is currently: ";
client << stateText[skylightState];
client << "<br /><br />\n";
The final section of this function adds three buttons. The new element here is that we can disable any buttons that we don’t want the user to click based upon the current skylight state. All we have to do is add the word “disabled” to the content of the tag. Note that the three buttons send different characters as part of any get request when they are clicked.
client << "<input type=\"button\" value=\"Open\" onclick=\"location.href=\'/O';\" ";
if(skylightState != FULLY_CLOSED && skylightState != PART_OPEN) {
client << " disabled ";
}
client << " />\n";
client << "<input type=\"button\" value=\"Close\" onclick=\"location.href=\'/C';\" ";
if(skylightState != FULLY_OPEN && skylightState != PART_OPEN) {
client << " disabled ";
}
client << " />\n";
client << "<input type=\"button\" value=\"Stop\" onclick=\"location.href=\'/S';\" ";
if(skylightState != OPENING && skylightState != CLOSING) {
client << " disabled ";
}
client << " />\n";
client << "<br /><br /></div></div></body></html>\n";
}
After wrapping up that function we can turn our attention to the main program tab that starts with quite a few global items.
#define BUFF_SIZE 81
#define BUFF_LIMIT 80
#include <WiFiNINA.h>
#include "Secret.h"
char buffer[BUFF_SIZE];
const long timeOut = 500;
unsigned long waitStart;
int charCount;
enum SkylightStates: int8_t {FULLY_OPEN, FULLY_CLOSED, PART_OPEN, OPENING, CLOSING};
char stateText[][13] = {"fully open", "fully closed",
"part open", "opening", "closing"};
byte skylightState;
const char* mySSID = SECRET_SSID;
const char* myPass = SECRET_PASSWORD;
const char getRequest[] = "GET /";
WiFiServer server(80); // port 80 is standard
WiFiClient client;
template<class T> inline Print &operator<<(Print &obj, T arg) {
obj.print(arg); return obj;}
We then have a setup() function followed by our now familiar connectToWiFi(). The setup() calls setMotorPins() and setupLimitSwitches() to complete the setup of the board pins and to establish the initial skylight state.
After connecting to wifi the server is started, listening on port 80.
void setup() { Serial.begin(115200); while (!Serial) {} setMotorPins(); setupLimitSwitches(); connectToWiFi(); server.begin(); // start the server } void connectToWiFi() { while (WiFi.begin(mySSID, myPass) != WL_CONNECTED) { delay(500); } Serial << "WiFi connected\n"; Serial << "Local IP: " << WiFi.localIP() << '\n'; }
All of the rest of the code is within the loop() function. Here we make checks on the limit switches if the motor is running while listening for get requests from any attached browser. There are four requests that we respond to – an initial request and then any related to button clicks. We can differentiate between the get requests from the different buttons by inspecting the sixth character in the buffer (character following “GET /”).
void loop() { if(skylightState == OPENING || skylightState == CLOSING) { checkLimitStates(); // check for interrupts } client = server.available(); if(client) { waitStart = millis(); clearBuffer(); while (client.connected()) { if(client.available()) { char c = client.read(); if(c == '\n' || c == '\r'){ if(strncmp(getRequest, buffer, 5) == 0) { switch(buffer[5]) { case ' ': // initial get request sendHTML(); break; case 'O': // open button clicked if(skylightState != OPENING && skylightState != FULLY_OPEN){ skylightState = OPENING; motorOpen(); } sendHTML(); break; case 'C': // close button clicked if(skylightState != CLOSING && skylightState != FULLY_CLOSED) { skylightState = CLOSING; motorClose(); } sendHTML(); break; case 'S': // stop button motorStop(); skylightState = PART_OPEN; // probably checkLimitStates(); //to make sure sendHTML(); break; } } break; // escape while loop to close connection } else { if(charCount <= BUFF_LIMIT) { buffer[charCount] = c; charCount++; } } } else { if((unsigned long)millis() - waitStart >= timeOut) {break;} } if(skylightState == OPENING || skylightState == CLOSING) { checkLimitStates(); } } client.flush(); client.stop(); } }
Uploading and running this program should mean that your browser now sees a web page like:
If your motor is hooked up to power then you should be able to run it forwards and backwards using the web UI Open and Close buttons. The stop button should stop the motor if you have not already triggered one of the limit switches.
Your tests should include starting the program with one or other of the push to make buttons pressed to simulate the skylight states of fully open or closed. You can also use those switches to stop the motor when it is running. The enabled buttons on the UI should reflect the current device state.
If you have a “smart phone” or tablet, try pointing the browser on that device to the same IP address. The UI should resize itself automatically to any smaller browser window. Now you can control your skylight from your portable device of choice.
I found that my home router always connected a given device using a consistent IP address. So much so that when I tried to request a different address from code on the Arduino, the request was ignored. If yours is the same, then this consistency is just what you need as it means that you can set a browser “bookmark” to connect to any given HTML enabled Arduino project.
What could be added? You might consider temperature sensors to compare the inside temperature with outdoors and then maybe use the RTC alarm feature to automatically open the skylight between certain hours if the temperature levels and difference met certain criteria. Would that require a rain detector to be fully effective? Would an electromagnetic “latch” to lock the skylight closed be a good idea? If we went for a latch, would we need a relay board or could we use the other “channel” on the motor controller?
Running a device such as this on a touch sensitive screen might suggest that the buttons should be revised to allow the owner to use touch to start and stop opening or closing the skylight. With some CSS skills the control buttons could be upgraded as well. Interacting with an Arduino might well start to look very cool indeed.
Internet Services
One area that we have not explored via the WiFi link is posting data to or receiving data from a Web service. Keeping within the spirit of the skylight automation project, I elected to add a current weather forecast to the HTML user interface. Here I (as author) need to strike a balance though. Most of the code I used to parse the data returned from the service is particular to that service and of no general value. I will therefore concentrate on the key components of communicating with any Web resource and try and generalise the potential issues. A complete code listing is available on the web site if you want to review it.
You might have in mind to develop programs to allow one or more Arduino to collect data and to send that data to a web service where it can be consolidated and analysed. Alternately, you might want to receive controlling data from a web server or (as in my test case) consume data from a private or public service. There are many public data services and a good number can be accessed for free although usually with some restrictions on the total data volume. As an example, Google makes available geolocation and mapping facilities through publicly available Web Services. After signing up for a particular service you will probably be supplied with a unique “key” used to sign and authenticate each data request.
Interaction with Web Services will normally be carried out using HTTP or secure HTTPS. We can use an instance of the WiFiClient class to connect to Web Services – and indeed any public Web page. The only real difference between connecting to a web service and connecting to a web page is what the remote server sends back. A Web page is returned as HTML while a Web Service will send data usually encoded as XML or JSON. XML looks a lot like HTML (they are both mark-up “languages”) and JSON is designed to be consumed by JavaScript where it can be transformed into a JavaScript object which is something like a class.
Connect to a web page
Start a new program and then we can use that to connect to a web page that I can be reasonably sure will exist when you try this.
Add that Secret.h tab again and copy the content with your WiFi credentials. You can also copy a connectToWiFi() function and our << Print operator as well. The setup() function just calls connectToWiFi() and a new function connectToResource() after starting the Serial connection to the Serial Monitor.
#include <WiFiNINA.h>
#include "Secret.h"
const char* mySSID = SECRET_SSID;
const char* myPass = SECRET_PASSWORD;
template<class T> inline Print &operator<<(Print &obj, T arg) {
obj.print(arg); return obj;}
void setup() {
Serial.begin(115200);
while (!Serial) {}
connectToWiFi();
connectToResource();
}
The connectToResource() function follows.
void connectToResource() { WiFiClient client; const char* host = "www.arduino.cc"; const int httpPort = 443; char* uri = "/reference/en/language/variables/data-types/stringobject/"; if (!client.connectSSL(host, httpPort)) { Serial << "connection failed\n"; return; } client << "GET " << uri << " HTTP/1.1\r\n"; client << "Host: " << host << "\r\n"; client << "Connection: close\r\n\r\n"; // two line terminators unsigned long timeOut = millis(); while(!client.available()) { if((unsigned long) millis() - timeOut > 5000) { Serial << "Client timeout\n"; } } while(client.available() ) { char c = client.read(); Serial << c; } Serial << "\nThats All Folks\n"; }
The code is intended to connect to the Arduino web site and so www.arduino.cc is set as the host name. The URI is a specific web page on that domain. In this instance we are going to connect using SSL so the port number is 443 rather than 80.
The WiFiClient instance uses the client.connectSSL() method to connect to the host using the set port. If the page was only available using HTTP then we would have used client.connect() and port 80. Once the connection is made then the code needs to tell the remote server what it wants. This is where the URI comes into play and four lines (the last blank) are sent to the server as shown. You can see that this includes the GET request that we saw sent from our local browser to our Arduino based server.
The code then waits for a response with a timeout if none arrives within 5 seconds. When the response arrives, then the code simply echoes the characters to the Serial Monitor. Scrolling back, you will be able to see the preamble from the server and then the HTML that would be used by a browser to render a web page. You can tweak the URI value to get the Arduino site server to send you any other available page.
Web service URI
Connecting to a Web Service is no different. You would include any data to be sent to the service as part of the URI. As an example, the URI used to request a weather forecast from the Met Office Web Service includes data in two ways.
/public/data/val/wxfcs/all/xml/310133?res=3hourly&key=MyAPIKey
It starts off like a request for a web page. Then we see “/xml” that tells this service that I want XML and not JSON. This is followed by “/310133” which is the Met Office location code for Shrewsbury in Shropshire. Then we see a question mark which is the way of marking the start of data items specific to a request. Here we see “res=3hourly” which designates the type of forecast wanted and this is followed by an ampersand and “key=” and my prearranged API key for this service.
Of course, accessing any given web service is going to require sending correctly named data items that will be specific to the service and data. However the format is likely to be very similar. If you are just sending data then the response is likely to be little more than an acknowledgement. Dealing with longer responses might require some coding effort.
A sample from the XML received from the Met Office web service I used is shown below. This was preceded by a lot of other general and specific information sent by the server in response to the request. The way the XML is shown here might lead you to assume that it was presented as a sequence of lines but in fact the characters were received in a single undivided stream. This would normally be the case and makes processing XML data on an Arduino and using C more challenging than it might be.
<SiteRep>
<script id="tinyhippos-injected"/>
<Wx>
<Param name="F" units="C">Feels Like Temperature</Param>
<Param name="G" units="mph">Wind Gust</Param>
<Param name="H" units="%">Screen Relative Humidity</Param>
<Param name="T" units="C">Temperature</Param>
<Param name="V" units="">Visibility</Param>
<Param name="D" units="compass">Wind Direction</Param>
<Param name="S" units="mph">Wind Speed</Param>
<Param name="U" units="">Max UV Index</Param>
<Param name="W" units="">Weather Type</Param>
<Param name="Pp" units="%">Precipitation Probability</Param>
</Wx>
<DV dataDate="2019-09-03T10:00:00Z" type="Forecast">
<Location i="310133" lat="52.7066" lon="-2.7513" name="SHREWSBURY" country="ENGLAND" continent="EUROPE" elevation="66.0">
<Period type="Day" value="2019-09-04Z">
<Rep D="SW" F="15" G="20" H="88" Pp="54" S="9" T="16" V="VG" W="12" U="0">0</Rep>
<Rep D="W" F="14" G="20" H="86" Pp="11" S="9" T="15" V="VG" W="7" U="0">180</Rep>
<Rep D="W" F="11" G="20" H="80" Pp="6" S="9" T="13" V="VG" W="3" U="1">360</Rep>
<Rep D="W" F="12" G="27" H="65" Pp="6" S="13" T="14" V="GO" W="3" U="2">540</Rep>
<Rep D="W" F="12" G="31" H="55" Pp="14" S="18" T="16" V="EX" W="7" U="4">720</Rep>
<Rep D="W" F="13" G="34" H="60" Pp="16" S="18" T="16" V="EX" W="7" U="2">900</Rep>
<Rep D="WNW" F="12" G="27" H="67" Pp="36" S="16" T="14" V="VG" W="7" U="1">1080</Rep>
<Rep D="NW" F="10" G="29" H="73" Pp="11" S="16" T="13" V="VG" W="7" U="0">1260</Rep>
</Period>
Part of the trick is to find a way to ignore the preamble and only start processing the characters returned by the Web Service once the XML (or what have you) is reached.
There are C/C++ libraries for parsing XML to extract the data held within tags and as attributes of tags.
You may have noticed that the
To implement the weather forecast as part of my automated skylight prototype I also had to set and use the real time clock (RTC).
The RTC was used to get the date and the time (in minutes) of the next three hourly forecast.
My code then used these values to identify the correct XML
The result looks not untypical for late summer in North Shropshire.
Program size was still only 34980 bytes which is 13.34% of the total program space available on this device. I could keep adding new features in code for a long time yet.