
This article covers the design and implementation of the arduino software for my Snake Tank Humidifier project.
- What + Why?
- How?
- Component Controllers
- Networking
- Utilities
- Constants
- Main Arduino File - climate_control.ino
What + Why?
The next major component of the Snake Tank Humidifier project is the software, which is responsible for controlling all of the electronics sub-systems and providing remote control of the system with network connectivity
Goals/Requirements
When designing the software for this project I wanted to ensure that the system could be monitored and updated remotely over the network, eliminating the need to change settings with code updates, and allowing for remote monitoring of the system status without a serial connection to a computer. In order to implement this functionality, there will be two separate code bases; one that runs on the arduino, and one for the mobile monitoring and control app. This article is focused on the code that runs on the arduino, the mobile app is discussed in the next article.
The arduino app consists of a few main components that control the electronics and provide network connectivity:
- Humidity controller
- Light controller
- Wifi Controller for connecting to a local WiFi network
- Remote time server connectivity
- Expose HTTP web server for the mobile app to communicate with
How?
Development Tools
The software is developed in C++ using the custom arduino compiler and libraries for interfacing with the board. For compilation and uploading to device, I use the Arduino IDE. The board used for this project (Arduino NANO 33 IoT) is a SAMD based board and requires the SAMD core to be installed in the Arduino IDE. For actually writing the code I use my editor of choice - VSCode - with some specific extensions for C++ and arduino development:
- Arduino
- The Arduino extension makes it easy to develop, build, deploy and debug your Arduino sketches in Visual Studio Code
- C/C++
- The C/C++ extension adds language support for C/C++ to Visual Studio Code, including features such as IntelliSense and debugging
- Code Spell Checker
- A basic spell checker that works well with camelCase code
External Dependencies
The software is designed to be pretty self-contained, but does still utilize a few external libraries to provide functionality that would be difficult to implement from scratch:
TODO: Dependencies info and links
Component Controllers
Each component controller is designed to encapsulate the functionality of an external piece of hardware connected to the main board via one or more control pins. They each expose different methods for interfacing, depending upon the device. DHT22, AtomizerController, and FanController are all traditional objects which can be instantiated multiple times. Alternatively, the HumidityController and LightController are static classes which cannot be instantiated, and function more like a namespace than a class - the reasons why are discussed in the implementation notes of each of those classes
DHT22
Responsible for interfacing with a DHT22 controller on a specific control pin and reading/exposing it’s temperature and humidity values.
CPP Header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef DHT22_H
#define DHT22_H
#include "Arduino.h"
#include "SimpleDHT.h"
// Represents a DHT22 sensor module
class DHT22 {
private:
byte pin;
SimpleDHT22 dht;
float temperature;
float humidity;
public:
DHT22(byte pin);
void updateValues();
float getTemperature();
float getHumidity();
};
#endif
Implementation
This class is designed to cache the temperature
and humidity
values whenever the void updateValues()
method is called, and make the most recent value available via getter methods: float getTemperature()
and float getHumidity()
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "Arduino.h"
#include "DHT22.h"
DHT22::DHT22(byte pin) : pin(pin), dht(pin) { };
// Read sensor and update stored values
void DHT22::updateValues() {
int err = SimpleDHTErrSuccess;
//update values
err = dht.read2(&temperature, &humidity, NULL);
//print any errors
if(err != SimpleDHTErrSuccess) {
Serial.print("Error reading DHT22 on pin:");
Serial.println(pin);
Serial.print(SimpleDHTErrCode(err));
Serial.print(", ");
Serial.println(SimpleDHTErrDuration(err));
}
};
// Return the last read temperature in degrees F
float DHT22::getTemperature() {
return temperature;
};
// Return the last read humidity as a percentage between 0-100
float DHT22::getHumidity() {
return humidity;
}
AtomizerController
Responsible for enabling/disabling the atomizer and tracking it’s current status via it’s control pin.
CPP Header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef ATOMIZER_CONTROLLER_H
#define ATOMIZER_CONTROLLER_H
#include "Arduino.h"
class AtomizerController {
private:
byte controlPin;
bool enabled;
public:
AtomizerController(byte pin);
bool isEnabled();
void enable();
void disable();
};
#endif
Implementation
This is a very simple class, with a constructor that takes a controlPin
parameter, an isEnabled()
that returns the value of an internal boolean flag, and has enable()
and disable()
methods that set the controlPin
to a digital HIGH
or LOW
to control an external transistor on the circuit board.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "Arduino.h"
#include "AtomizerController.h"
// Create new atomizer controller with specified control pin
AtomizerController::AtomizerController(byte pin) : controlPin(pin) {
pinMode(controlPin, OUTPUT);
enabled = false;
};
// Return true if the atomizer is enabled
bool AtomizerController::isEnabled() {
return enabled;
};
// Enable the atomizer
void AtomizerController::enable() {
digitalWrite(controlPin, HIGH);
enabled = true;
};
// Disable the atomizer
void AtomizerController::disable() {
digitalWrite(controlPin, LOW);
enabled = false;
};
FanController
Responsible for enabling/disabling the fans and tracking their current status via it’s control pin.
CPP Header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef FAN_CONTROLLER_H
#define FAN_CONTROLLER_H
#include "Arduino.h"
// Represents a DC fan controller
class FanController {
private:
byte controlPin;
bool enabled;
public:
FanController(byte pin);
bool isEnabled();
void enable();
void disable();
};
#endif
Implementation
This is another very simple class, implemented exactly the same way as the atomizer controller - with a constructor that takes a controlPin
parameter, an isEnabled()
that returns the value of an internal boolean flag, and has enable()
and disable()
methods that set the controlPin
to a digital HIGH
or LOW
to control an external transistor on the circuit board.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "Arduino.h"
#include "FanController.h"
// Create a new FanController with specified control pin
FanController::FanController(byte pin) : controlPin(pin) {
pinMode(controlPin, OUTPUT);
enabled = false;
};
// Return true if the fan controller is currently enabled
bool FanController::isEnabled() {
return enabled;
};
// Enable the fan controller
void FanController::enable() {
digitalWrite(controlPin, HIGH);
enabled = true;
};
// Disable the fan controller
void FanController::disable() {
digitalWrite(controlPin, LOW);
enabled = false;
}
HumidityController
Static class responsible for enabling/disabling the humidifier system, tying together the previous 3 classes internally to control each component. These files also contain the HumidityControllerSettings
class, used to manage the current configuration of the humidity controller. The HumidityController
class must be static in order for the TimeAlarms
library to call the update()
method on each update interval.
CPP Header
HumidityControllerSettings
is a simple struct with 4 properties: targetHumidity
(run humidifier until this humidity is reached), kickOnHumidity
(turn humidifier on after falling below this humidity), fanStopDelay
(time in seconds to run fans after atomizer is stopped - to clear remaining fog from reservoir), and updateInterval
(time in seconds to check humidity levels and update system status).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#ifndef HUMIDITY_CONTROLLER_H
#define HUMIDITY_CONTROLLER_H
#include "Arduino.h"
#include "Time.h"
#include "TimeAlarms.h"
#include "DHT22.h"
#include "DateTime.h"
#include "AtomizerController.h"
#include "FanController.h"
// Encapsulates settings for the HumidityController
struct HumidityControllerSettings {
float targetHumidity;
float kickOnHumidity;
int fanStopDelay;
int updateInterval;
HumidityControllerSettings(float target, float kickOn, int fanStop, int update);
};
// HumidityController class provides methods for controlling the sensors, atomizer, and fan system.
class HumidityController {
private:
static DHT22 sensorOne;
static DHT22 sensorTwo;
static AtomizerController atomizer;
static FanController fans;
static HumidityControllerSettings* settings;
static bool running;
static float average(float, float);
static void runHumidifier();
static void stopAtomizer();
static void stopFans();
public:
static void init(byte sensorOnePin, byte sensorTwoPin, byte atomizerPin,
byte fansPin, HumidityControllerSettings* s);
static void update();
static void controlStatus(bool&, bool&);
static void humidity(float&, float&, float&);
static void temperature(float&, float&, float&);
};
#endif
Implementation
HumidityController::init()
- Initialization of the humidity controller needs to be done using the static
init()
method, which takes 4 control pin arguments for the individual sub-controllers, and a pointer to aHumidityControllerSettings
object - using a pointer allows the object to be managed outside of theHumidityController
class. After initializing the sub-component modules, the TimeAlarms library is used to create a repeating timer that calls theupdate()
method with an interval ofsettings->updateInterval
seconds. HumidityController::update()
- This method is called once per loop every
settings->updateInterval
seconds, and performs an update of bothDHT22
sensors, calculates the average humidity, and enables/disables the atomizer and fans according to the calculated average humidity and the thresholds insettings->target
andsettings->kickOn
. When shutting down the system, the atomizer is turned off first, and then aAlarm.timerOnce()
is used to stop the fans after a specifiedfanStopDelay
. HumidityController::runHumidifier()
- This method enables both the atomizer and fan controllers.
HumidityController::stopAtomizer()
andHumidityController::stopFans()
- These methods disables the atomizer and fan controllers respectively.
HumidityController::average()
- Calculates and average of the two values
a
andb
. Note that ifa
orb
are equal to0
, then they will be discarded from the calculation. This provides a fail-safe option in the event of one of the sensors not functioning properly. HumidityController::controlStatus()
- Updates two boolean ref params -
aEnabled
andfEnabled
- with the status of the atomizer and fan controllers respectively HumidityController::temperature()
andHumidityController::humidity()
- Each method updates three floating point ref params -
avg
,one
, andtwo
- with the average and individual sensor values for temperature and humidity respectively
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include "Arduino.h"
#include "Time.h"
#include "TimeAlarms.h"
#include "DHT22.h"
#include "DateTime.h"
#include "AtomizerController.h"
#include "FanController.h"
#include "HumidityController.h"
// Create a new HumidityControllerSettings object with specified
// target humidity, kick on humidity, fan stop delay (s), and update interval (s)
HumidityControllerSettings::HumidityControllerSettings(float target, float kickOn, int fanStop, int update){
targetHumidity = target;
kickOnHumidity = kickOn;
fanStopDelay = fanStop;
updateInterval = update;
};
// static member initializers
DHT22 HumidityController::sensorOne = NULL;
DHT22 HumidityController::sensorTwo = NULL;
AtomizerController HumidityController::atomizer = NULL;
FanController HumidityController::fans = NULL;
HumidityControllerSettings* HumidityController::settings;
bool HumidityController::running = false;
// Initialize the humidity controller with provided sensor, atomizer and fan pins, and settings.
void HumidityController::init(byte sensorOnePin, byte sensorTwoPin, byte atomizerPin,
byte fansPin, HumidityControllerSettings* s) {
//init sensors
sensorOne = DHT22(sensorOnePin);
sensorTwo = DHT22(sensorTwoPin);
atomizer = AtomizerController(atomizerPin);
fans = FanController(fansPin);
//init settings and tracking vars
settings = s;
Serial.print("setting update interval: "); Serial.println(settings->updateInterval);
//set up update interval
Alarm.timerRepeat(settings->updateInterval, update);
};
// Update the humidity system status, called once per update interval
void HumidityController::update() {
// update sensor readings
sensorOne.updateValues();
sensorTwo.updateValues();
//check values against threshold
//float avgHumidity = average(sensorOne.getHumidity(), sensorTwo.getHumidity());
float avgHumidity, hum1, hum2;
humidity(avgHumidity, hum1, hum2);
if(avgHumidity < settings->kickOnHumidity) {
// start humidifier when kick-on humidity reached.
if(!running) {
runHumidifier();
running = true;
}
} else if(avgHumidity >= settings->targetHumidity) {
if(running) {
// stop atomizer when target humidity reached.
stopAtomizer();
//set up delayed fan stop timer
Alarm.timerOnce(settings->fanStopDelay, stopFans);
}
}
};
// Enable the humidifier and fans
void HumidityController::runHumidifier() {
Serial.println("Turning ON humidifier");
atomizer.enable();
fans.enable();
};
// Stop the atomizer system
void HumidityController::stopAtomizer() {
Serial.println("Turning OFF atomizer");
atomizer.disable();
};
// Stop the fan system
void HumidityController::stopFans() {
Serial.println("Turning OFF fans");
fans.disable();
running = false; // set running false so humidity check knows to start up again.
};
// Return the average humidity of the two DHT22 sensors
float HumidityController::average(float a, float b) {
// if one sensor is down, exclude it from the average
if(a == 0) {
a = b;
} else if(b == 0) {
b = a;
}
float avg = (a + b) / 2;
//print details about averages for debugging
Serial.print("avg: ");
Serial.print(avg);
Serial.print(" ( "); Serial.print(a);
Serial.print(" , "); Serial.print(b); Serial.println(" )");
return avg;
};
// Update the provided variables with the humidity system status
void HumidityController::controlStatus(bool& aEnabled, bool& fEnabled) {
aEnabled = atomizer.isEnabled();
fEnabled = fans.isEnabled();
};
void HumidityController::humidity(float &avg, float &one, float &two) {
Serial.print("Humidity - ");
one = sensorOne.getHumidity();
two = sensorTwo.getHumidity();
avg = average(one, two);
}
void HumidityController::temperature(float &avg, float &one, float &two) {
Serial.print("Temperature - ");
one = sensorOne.getTemperature();
two = sensorTwo.getTemperature();
avg = average(one, two);
}
LightController
Responsible for turning the day/night lights off/on via the relays attached to day/night control pins.
CPP Header
Similar to the HumidityControllerSettings
class, LightControllerSettings
is a simple struct with 2 properties: schedule
(a concrete instance of the abstract Schedule
class that returns a ScheduleEntry
for the current date/time - see Utilities > Scheduling) and updateInterval
(time in seconds to check humidity levels and update system status).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifndef LIGHT_CONTROLLER_H
#define LIGHT_CONTROLLER_H
#include "Arduino.h"
#include "Scheduling.h"
#include "DateTime.h"
#include "Time.h"
#include "TimeAlarms.h"
// Represents the LightControllerSettings status
struct LightControllerSettings {
int updateInterval;
Schedule* schedule;
LightControllerSettings(Schedule*, int);
};
// Represents the LightController class, provides methods for controlling the light system.
class LightController {
private:
static byte dayControlPin, nightControlPin;
static LightStatus status;
static LightControllerSettings* settings;
public:
static void init(byte dayControlPin, byte nightControlPin, LightControllerSettings*);
static void update();
static void enableLights(LightStatus);
static LightStatus getStatus();
static const char* getStatusString();
};
#endif
Implementation
LightController::init()
- Initialization of the light controller needs to be done using the static
init()
method, which takes 2 control pin arguments for the individual day/night relays, and a pointer to aLightControllerSettings
object - using a pointer allows the object to be managed outside of theLightController
class. After initializing the sub-component modules, the TimeAlarms library is used to create a repeating timer that calls theupdate()
method with an interval ofsettings->updateInterval
seconds. LightController::update()
- This method is called once per loop every
settings->updateInterval
seconds. It first gets the currentDate
andTime
(classes from Utilities > DateTime), and then requests aScheduleEntry
from thesettings->schedule
for the current date (see see Utilities > Scheduling). It then uses thisScheduleEntry
to determine the correct day/night status for the current time, and enables/disables voltage on the relevant control pins to actuate the connected relays. LightController::getStatus()
andLightController::getStatusString()
- Both methods return the current day/night status of the controller - one as a
DayNight
object (an enum from Utilities > Scheduling), and the other as a lowercase string representation. LightController::enableLights()
- Given a
DayNight
object (see Utilities > Scheduling), this method enables/disables the appropriate control pins and updates theLightController::status
variable with the new value.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "Arduino.h"
#include "Scheduling.h"
#include "DateTime.h"
#include "Time.h"
#include "TimeAlarms.h"
#include "LightController.h"
// Create new LightControllerSettings class with specified schedule and update interval
LightControllerSettings::LightControllerSettings(Schedule* s, int interval) : updateInterval(interval) {
schedule = s;
};
// static member initializers
byte LightController::dayControlPin;
byte LightController::nightControlPin;
LightStatus LightController::status;
LightControllerSettings* LightController::settings;
// Initialize LightController class with provided day and night pins, and settings
void LightController::init(byte dayPin, byte nightPin, LightControllerSettings* s) {
// init pins
dayControlPin = dayPin;
nightControlPin = nightPin;
// init settings
settings = s;
// configure pin modes
pinMode(dayControlPin, OUTPUT);
pinMode(nightControlPin, OUTPUT);
// set up update interval
Alarm.timerRepeat(settings->updateInterval, update);
};
// Update the light control system, called once per update interval
void LightController::update() {
Date today = Date(year(), (byte)month(), (byte)day());
Time now = Time((byte)hour(), (byte)minute(), (byte)second());
ScheduleEntry* sched = settings->schedule->getEntry(today);
LightStatus newStatus = sched->getLightStatus(now);
// if the status has changed, switch lights
if(status != newStatus) {
//print notification message
Serial.print("Switching lights from ");
Serial.print(status == DAY ? "day" : "night");
Serial.print(" to ");
Serial.println(newStatus == DAY ? "day." : "night.");
Serial.print("Date: ");
today.printSerial();
Serial.print("Time: ");
now.printSerial();
//enable appropriate lights
enableLights(newStatus);
}
};
// Return the day/night status of the light controller
LightStatus LightController::getStatus() {
return status;
};
// Return string representing the day/night status of the system
const char* LightController::getStatusString() {
return status == DAY ? "day" : "night";
};
// Enable the lights specified by 'newStatus'
void LightController::enableLights(LightStatus newStatus) {
//update light control pins
if(newStatus == DAY) {
digitalWrite(dayControlPin, HIGH);
digitalWrite(nightControlPin, LOW);
} else {
digitalWrite(dayControlPin, LOW);
digitalWrite(nightControlPin, HIGH);
}
//update saved status
status = newStatus;
}
Networking
The networking classes are responsible for providing network related functionality to the rest of the software.
NTPClient
Class that implements a basic NTP Client that sends requests over UDP and parses the incoming responses.
Note: Current implementation does not take into account daylight savings time, so the time is an hour off during half of the year and correct the other half. Future updates will add functionality to get DST info from the NTP server and update the time according to our current time zone.
NTP Protocol
The NTP protocol is fairly simple, and outlined in the image below. Each red box on the diagram represents one byte (8 bits) of the packet data. The numbers next to each row indicate the start and end indices for that row in the packetBuffer
variable. This diagram applies to both the request and response packets, as in the NTP protocol the packet format is the same for both. The relevant portions of the packet are outlined below:
- LI - Leap Indicator - 2 bits indicating the leap year/second status - currently using
3
for unsynchronized - VN - Version number - 3 bits indicating the protocol version - currently using version
4
- Mode - 3 bits indicating the mode - using
3
for client mode - Stratum - 8 bits indicating the type of clock we would like our time to be from - currently using
0
for unspecified, since we don’t have a need for high precision - Poll - 8 bits indicating the maximum time between successive NTP messages - not really relevant here, but defined to
6
- Precision - 8 bits indicating the system clock precision, in
seconds. To calculate this value requires several steps:
- First, we need to find the frequency of the clock - in this case
48 MHz
or48,000,000 Hz
. - Then we use the formula to take the inverse of that frequency and get the period (time between clock ticks):
- where is the clock frequency in and is the clock period in ticks/second. Evaluate the expression to get the following: seconds. - The NTP server expects an integer value
where evaluates to approximately the clock precision, so next we need to take the base-2 logarithm of this period with the following formula: . Evaluate that expression to get , so the nearest integer value (rounded down) is . - Since
is a signed integer - it has a negative sign - it should be represented in it’s two’s compliment binary representation. However, thebyte
elements that make up our buffer array are all unsigned 8-bit values, we could simply write-25
in our code and let the compiler automatically perform the two’s complement operation for us, but for the sake of clarity and not relying on the compiler, we’ll manually perform the operation and hard code the resulting value:- To convert to two’s compliment, we fist need to get the binary representation of 25:
- We then perform a binary complement operation, which swaps every
1
and0
in the number: - To complete the two’s-complement, we just need to add one to the complement:
- Then we just convert this value to hex:
- To convert to two’s compliment, we fist need to get the binary representation of 25:
- Root Delay - 32 bits not used by client
- Root Dispersion - 32 bits not used by client
- Reference Identifier - 4 bytes ASCII code, indicating the reference clock type. For Stratum 0, this is irrelevant
- Reference Timestamp - 64 bits indicating time request was sent by client. 32 bits of integer part, and 32 bits of decimal part. Not used
- Originate Timestamp - 64 bits indicating time request was received by server. 32 bits of integer part, and 32 bits of decimal part. Not used
- Receive Timestamp - 64 bits indicating time request was sent by server. 32 bits of integer part, and 32 bits of decimal part. Not used
- Transmit Timestamp - 64 bits indicating time request was received by client. 32 bits of integer part, and 32 bits of decimal part. This is the value we will use for our time determination
CPP Header
This file contains the definition for the NTPClient
class, as well as definitions of constants that are used internally:
NTP_DEFAULT_PORT = 8888;
- default local port that the underlying UDP instance will useNTP_DEFAULT_SERVER = "us.pool.ntp.org";
- default ntp server nameNTP_DEFAULT_TIMEZONE = -6;
- default time zoneNTP_PACKET_BUFFER_SIZE = 48;
- size of the internal request/response buffer in bytesNTP_REQUEST_PORT = 123;
- the remote port that NTP requests will be sent to.NTP_RESPONSE_WAIT_TIME = 1500;
- the maximum time in ms to wait for an NTP responseNTP_UNIX_TIME_OFFSET = 2208988800UL;
- constant representing the number of seconds between 1/1/1900 and 1/1/1970. Used to convert from UTC to Unix time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef NTPCLIENT_H
#define NTPCLIENT_H
#include "WiFiUdp.h"
//defaults
const u_int NTP_DEFAULT_PORT = 8888;
const char NTP_DEFAULT_SERVER[] = "us.pool.ntp.org";
const int NTP_DEFAULT_TIMEZONE = -6; // Central Standard Time = UTC-6:00
// constants
const int NTP_PACKET_BUFFER_SIZE = 48; // packet buffer size = 48 bytes
const byte NTP_REQUEST_PORT = 123; // NTP requests go to port 123
const byte NTP_RESPONSE_WAIT_TIME = 1500; // wait up to 1500ms for a response from the NTP server
const u_long NTP_UNIX_TIME_OFFSET = 2208988800UL; // number of seconds between 1/1/1900 and 1/1/1970
// Represents an NTPClient object
class NTPClient {
private:
WiFiUDP _udp;
u_int _udpPort;
String _server;
int _timeZone;
byte packetBuffer[NTP_PACKET_BUFFER_SIZE];
public:
//NTPClient(); // empty constructor to allow declaration before initialization
NTPClient(WiFiUDP); //udp only
NTPClient(WiFiUDP, int); //udp, timezone
NTPClient(WiFiUDP, String, int); //udp, server, timezone
NTPClient(WiFiUDP, String, u_int, int); //udp, server, port, timezone
void initUdp();
time_t getNTPTime();
void sendNTPRequestPacket(IPAddress&);
time_t receiveNTPResponsePacket();
};
#endif
Implementation
NTPClient::NTPClient(...)
- constructor- There are 4 overloaded constructors, that allow for creation of an object with default values. The only required value is a
WiFiUDP
instance (from the WiFiNINA module) that is used for sending/receiving web requests.settings->updateInterval
seconds. The other three valid signatures are:
NTPClient(WiFiUDP udp, int timeZone)
NTPClient(WiFiUDP udp, String server, int timeZone)
NTPClient(WiFiUDP udp, String server, u_int port, int timeZone)
NTPClient::initUDP()
- This method must be called after connecting the device to a WiFi network, and before sending/receiving any requests, and takes care of initializing the underlying
WiFiUDP
instance NTPClient::getNTPTime()
- This method returns the current time from a remote NTP server. First, it clears any incoming UDP requests to make sure we parse the right response. Next it takes care of making a DNS request to resolve an IP address from the ntp server url. Since the current implementation utilizes a public NTP server pool, this IP address is usually different, and depends on your region, etc. Once the IP address is resolved, we call the
sendNTPRequestPacket()
which builds and sends the NTP request to the remote server. Then we call, and return the result of, thereceiveNTPResponsePacket()
method which receives the response and returns atime_t
type variable with the time received from the server. NTPClient::sendNTPRequestPacket()
- This method creates and sends an NTP request packet to the specified
IPAddress
. The contents of the request are created inside apacketBuffer
- a byte array of size 48, with the structure shown in the diagram below. We build a packet according to this diagram, open a UDP connection to the remote server, write the bytes of the packet to the connection stream, close the stream, and return. NTPClient::receiveNTPResponsePacket()
- This method receives and parses a NTP response packet from the remote server. It waits a specified time (default 1500 ms) for a response to come in, returning
0
if a response is not received in time. If a response is received, the size is confirmed, and the response data is buffered intopacketBuffer
. This buffer can be shared between the send and receive methods since the request and response packet share the same format (see below). We parse the first 32 bits of the Transmit Timestamp section of the response packet into an unsigned long variable, convert it from UTC to Unix-style time, update the time based on the specified time-zone, and then return the value to the caller.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include "Arduino.h"
#include "NTPClient.h"
#include "WiFi.h"
#include "WiFiUdp.h"
#include "IPAddress.h"
#include "TimeLib.h"
// Create NTPCLient with specified udp instance and default port, server, and time zone
NTPClient::NTPClient(WiFiUDP udp)
: _udp(udp) {
_udpPort = NTP_DEFAULT_PORT;
_server = NTP_DEFAULT_SERVER;
_timeZone = NTP_DEFAULT_TIMEZONE;
};
// Create NTPCLient with specified udp instance and port, and default server and time zone
NTPClient::NTPClient(WiFiUDP udp, int timeZone)
: _udp(udp), _timeZone(timeZone) {
_udpPort = NTP_DEFAULT_PORT;
_server = NTP_DEFAULT_SERVER;
};
// Create NTPCLient with specified udp instance, port and server, and default time zone
NTPClient::NTPClient(WiFiUDP udp, String server, int timeZone)
: _udp(udp), _server(server), _timeZone(timeZone) {
_udpPort = NTP_DEFAULT_PORT;
};
// Create NTPCLient with specified udp instance, port and server, and time zone
NTPClient::NTPClient(WiFiUDP udp, String server, u_int port, int timeZone)
: _udp(udp), _server(server), _udpPort(port), _timeZone(timeZone) {
};
// Init NTPClient object
void NTPClient::initUdp() {
Serial.println("Starting UDP for NTPClient...");
_udp.begin(_udpPort);
};
// request the current time from an NTP server and return
time_t NTPClient::getNTPTime() {
Serial.println("clearing packets");
while(_udp.parsePacket() > 0) ; // clear previous incoming udp packets
Serial.println("...clear");
// get an IP address from the NTP server pool
IPAddress ntpServerIP;
WiFi.hostByName(_server.c_str(), ntpServerIP);
Serial.print("NTP server IP: ");
ntpServerIP.printTo(Serial); Serial.println();
// send NTP request
sendNTPRequestPacket(ntpServerIP);
// await response
return receiveNTPResponsePacket();
};
const byte NTP_LEAP_INDICATOR_BITS = 0b11000000; // LI: first 2 bits (3 = unsynchronized)
const byte NTP_VERSION_BITS = 0b00100000; // Version: next 3 bits(4 = current version)
const byte NTP_MODE_BITS = 0b00000011; // Mode: last 3 bits (3 = client)
// Send an NTP request packet to the specified IP address
// Packet format details: https://labs.apnic.net/?p=462
void NTPClient::sendNTPRequestPacket(IPAddress &addr) {
//fill packet buffer with 0's
memset(packetBuffer, 0, NTP_PACKET_BUFFER_SIZE);
// Initialize values needed to form NTP request
packetBuffer[0] = NTP_LEAP_INDICATOR_BITS | NTP_VERSION_BITS | NTP_MODE_BITS; // first byte: LI, Version, Mode
packetBuffer[1] = 0; // second byte: Stratum, or type of clock (0 = Unspecified)
packetBuffer[2] = 6; // third byte: Polling Interval (6 seconds)
packetBuffer[3] = 0xE7; // fourth byte: Peer Clock Precision (0xE7 = uint 231 = 11100111 = sint -25 = log_2(1/48,000,000) )
// 4 bytes of 0 for Root Delay - packetBuffer[4] ... packetBuffer[7]
// 4 bytes of 0 for Root Dispersion - packetBuffer[8] ... packetBuffer[11]
// 4 bytes of ASCII codes for Reference Identifier
// WHY THE FUCK is it "1N14" when that isn't a valid source according to the NTP protocol source list?
// https://forum.arduino.cc/t/udp-ntp-clients/95868
// doesn't seem to matter though, soo.... oh well, don't fix it if it ain't broke
packetBuffer[12] = 49; // 1
packetBuffer[13] = 0x4E; // N
packetBuffer[14] = 49; // 1
packetBuffer[15] = 52; // 4
//packet initialized, time to send it to the time-server.
_udp.beginPacket(addr, NTP_REQUEST_PORT);
_udp.write(packetBuffer, NTP_PACKET_BUFFER_SIZE);
_udp.endPacket();
Serial.println("NTP packet sent!");
};
// Receive an incoming NTP response packet
time_t NTPClient::receiveNTPResponsePacket() {
//set begin wait time
u_long beginWait = millis();
//continuously check for response until timed out
while(millis() - beginWait < NTP_RESPONSE_WAIT_TIME) {
//get response size
int size = _udp.parsePacket();
//check response size
if(size >= NTP_PACKET_BUFFER_SIZE) {
Serial.println("NTP Response packet received");
// read response into packet buffer
_udp.read(packetBuffer, NTP_PACKET_BUFFER_SIZE);
// parse 4 bytes from the response into a 32 bit unsigned integer (long)
// response time is a 64 bit value, with 32 bits each for the integer and decimal portions
// we don't need to be super accurate, and our time is stored as a non-decimal value
// so we only need to get the first 32 bits of the time in the response.
u_long secSince1900;
secSince1900 = (u_long)packetBuffer[40] << 24; // first byte, shifted 24 bits left, leaving room for 3 more
secSince1900 |= (u_long)packetBuffer[41] << 16; // second byte, shifted 16 bits left, leaving room for 2 more
secSince1900 |= (u_long)packetBuffer[42] << 8; // third byte, shifted 8 bits left, leaving room for 1 more
secSince1900 |= (u_long)packetBuffer[43]; // fourth and final byte, not shifted, since it's the last one.
//convert from UTC to unix time (seconds since 1970)
u_long unixTime = secSince1900 - NTP_UNIX_TIME_OFFSET;
//account for timezone
unixTime += _timeZone * SECS_PER_HOUR;
//return time to calling function
return unixTime;
}
}
// return 0 if no response received
Serial.println("No response received from NTP server");
return 0;
};
WebServer
The WebServer module is probably one of the more complex modules in this application. It’s a bare bones implementation of the HTTP protocol that wraps the WiFiServer
class from the WiFiNINA module. It handles detecting and parsing incoming web requests (parses HTTP method, query params, headers, and body content), and provides the WebRequest
and WebResponse
classes which allow us to process incoming requests as well as build and send responses.
CPP Header
This file contains the definitions for many internal constants:
- Server constants, such as default port, line buffer size, and line terminator character.
- Line mode status (request, header, body) for determining how to parse a specific line.
- Parser status (success, fail).
- Data size constants - determine the size of internal arrays and string buffers used during sending, receiving, and parsing.
- HTTP Status codes - string representations of common http status codes that I may use when sending a response. It also defines the following classes, discussed in more detail in the implementation notes below:
WebServer
WebRequest
WebResponse
QueryParam
HttpHeader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#ifndef WEB_SERVER_H
#define WEB_SERVER_H
#include "WiFiNINA.h"
// Server constants
const byte WS_DEFAULT_PORT = 80; // default to port 80
const int WS_LINE_BUFFER_SIZE = 512; // line buffer can hold up to 512 chars.
const char WS_LINE_TERMINATOR = '\n'; // line terminator character
// line mode status constants, determine how to process current line when parsing requests.
const byte LINE_MODE_REQUEST = 0; //Current line is a request
const byte LINE_MODE_HEADER = 1; //Current line is a header
const byte LINE_MODE_BODY = 2; //Current line is a body
// parser status constants
const byte PARSE_SUCCESS = 0; // Parsing succeeded
const byte PARSE_FAIL = 1; // Parsing failed
// data size constants - since I need to use char[] for these, I must define a fixed max size for each char[]
const byte REQ_METHOD_SIZE = 10; // Max size of request method string
const byte REQ_PATH_SIZE = 255; // Max size of request path string
const byte REQ_PARAMS_STR_SIZE = 255; // Max size of query params string
const byte REQ_VERSION_SIZE = 10; // Max size of request version
const byte REQ_HEADER_COUNT = 20; // Max number of headers a request can hold
const byte REQ_HEADER_NAME_SIZE = 50; // Max size of header name
const byte REQ_HEADER_VALUE_SIZE = 255; // Max size of header value
const byte REQ_QUERY_PARAMS_SIZE = 10; // Max number of query param objects in a single request
const int REQ_BODY_SIZE = 2048;// Max size of body value
// HTTP Status codes - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
const char HTTP_OK[] = "200 OK"; // Request succeeded
const char HTTP_BAD_REQUEST[] = "400 Bad Request"; // The server could not understand the request due to invalid syntax.
const char HTTP_NOT_FOUND[] = "404 Not Found"; // The server can not find the requested resource.
const char HTTP_LENGTH_REQUIRED[] = "411 Length Required"; // Server rejected the request because the Content-Length header field is not defined and the server requires it.
const char HTTP_PAYLOAD_TOO_LARGE[] = "413 Payload Too Large"; // Request entity is larger than limits defined by server
const char HTTP_URI_TOO_LONG[] = "414 URI Too Long"; // The URI requested by the client is longer than the server is willing to interpret.
const char HTTP_HEADER_TOO_LARGE[] = "431 Request Header Fields Too Large"; // The server is unwilling to process the request because its header fields are too large.
const char HTTP_SERVER_ERROR[] = "500 Internal Server Error"; // The server has encountered a situation it doesn't know how to handle.
// struct to hold HTTP Header key/value pairs
class HttpHeader {
public:
String key;
String value;
};
// represents a request query parameter
struct QueryParam {
String key; // param key
String value; // param value
};
// Represents a response to an incoming HTTP request
class WebResponse {
private:
int _currentHeaderIndex = 0;
public:
WiFiClient client;
String status;
String httpVersion;
String body;
HttpHeader headers[REQ_HEADER_COUNT];
int addHeader(HttpHeader); // add new header to header list
int addHeader(const char*, const char*); // add new header to header list with basic strings (key, value)
int addHeader(const char*, const long); // add numerical header, automatically parsing number to string
int addHeader(const char*, const float); // add floating point numerical header, auto parsing to string
int send(); // send the response to the client
};
// Represents an incoming HTTP request
class WebRequest {
public:
WiFiClient client;
String method;
String path;
QueryParam params[REQ_QUERY_PARAMS_SIZE];
String httpVersion;
String body;
HttpHeader headers[REQ_HEADER_COUNT];
WebResponse getResponse();
bool getHeader(String, HttpHeader&);
};
// Web Server class, provides methods for processing and replying to incoming requests.
class WebServer {
private:
WiFiServer _server; // WiFi server instance
byte _lineMode;
char _lineBuffer[WS_LINE_BUFFER_SIZE];
public:
WebServer();
WebServer(byte);
void listen(); // begin listening for incoming requests
int processIncomingRequest(WebRequest&); // process the next incoming request, return 1 when request found, -1 otherwise
void readLine(WiFiClient); // read the next line into the internal _lineBuffer
byte parseLineRequest(char*, char*, char*, char*); // parse buffered line as a request (src, method, path, params)
byte parseLineHeader(char*, char*); // parse buffered line as a header (key, value)
void parseQueryParams(char*, QueryParam*); // parse query params into provided array
};
#endif
Implementation
WebServer
The web server class is responsible for handling and parsing incoming HTTP requests
WebServer::WebServer()
- constructor- The web server constructor takes an optional
byte
parameter specifying the port to listen on. WebServer::listen()
- This method starts the underlying
WiFiServer
instance listening for incoming requests. WebServer::processIncomingRequest(WebRequest& req)
- This method is called once per application update loop (see Main Arduino File for details on main loop), and checks for an incoming request. If there is an incoming request available, the contents of the request are parsed line by line by the specialized parsing functions, and the reference parameter
req
is updated with the contents of the request. If the request parsing is successful the method returns1
to inform the caller of the success, if the parsing fails the method returns-1
to inform the caller of failure. WebServer::readLine(WiFiClient client)
- This is a helper method that reads the next available line from
client
into the internal_lineBuffer
variable, using theWS_LINE_TERMINATOR
to determine the end of the line. WebServer::parseLineRequest(char* method, char* path, char* params, char* version)
- This method is parses the first line of the request, which contains the method, path, and HTTP version. The supplied char buffers are updated with the text from the parsing results -
method
gets the HTTP method,path
the request path excluding any query parameters found,params
the full string of query params, andversion
the HTTP version text. WebServer::parseQueryParams(char* paramStr, QueryParam* dest)
- This method is responsible for further parsing the raw query param string into an array of
QueryParam
objects that can be stored in the finalWebRequest
object. We loop through the characters ofparamStr
, using akeyBuffer
andvalueBuffer
to store each char depending on whether the current char is part of a key or part of a value. We look for special characters?
,&
, and=
to determine this. The resulting params are added to theQueryParam[]
pointed to bydest
. WebServer::parseLineHeader(char* key, char* value)
- This method is responsible for parsing the contents of
_lineBuffer
as a header line, and settingkey
to the header name andvalue
to the header value.
WebRequest
The WebRequest
is responsible for wrapping all the properties of web request in a single object, and providing a method to get a WebResponse
object that can be used to respond.
WebRequest::getResponse()
- This method builds ad returns a
WebResponse
object that corresponds to thisWebRequest
. It copies over theclient
andhttpVersion
properties, and adds a default status of200 OK
as well as the following default headers:Content-Type text/plain
,Server: Arduino NANO 33 IoT - Snake Tank Controller
, andConnection: close
. WebRequest::getHeader(String name, HttpHeader& dest)
- This method is used to access a specific header from this request. The
HttpHeader
object header with the specifiedname
will be assigned to theHttpHeader
object referenced by thedest
parameter.
WebResponse
The WebResponse
is responsible for providing an interface to build a response to a specific web request, and providing a method to send that request to the remote server.
WebRequest::addHeader()
- The addHeader method is used to add an HTTP header to the response, and returns
1
on success, and-1
on failure (in the case that thisWebResponse
already has the maximum supported number of headers). There are four overloads of this method: one that accepts anHttpHeader
object directly, and three that accept two parameters:key
andvalue
. The two-parameter overloads all accept a C-string for thekey
, and one of the following types forvalue
:char*
- C-string value of headerfloat
- float value of header that will be converted to C-stringlong
- long value of header that will be converted to C-string
WebRequest::send()
- This method is responsible for sending the built response to the requesting client. It checks the client is still connected, calculates the size of
body
and generates acontent-length
header, and then serializes and sends the bytes of the response to the client. We then close the connection and return1
for success. If part of the process fails,-1
is returned to inform the caller.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#include "Arduino.h"
#include "WebServer.h"
#include "Regexp.h"
#include "WiFiNINA.h"
// ==== WebServer ====
//create WebServer with default port
WebServer::WebServer() : _server(WS_DEFAULT_PORT) { };
//create WebServer with custom port
WebServer::WebServer(byte port) : _server(port) { };
// begin listening to requests
void WebServer::listen() {
_server.begin();
};
// process next incoming request, updating provided WebRequest object.
int WebServer::processIncomingRequest(WebRequest& req) {
// get incoming client requests
WiFiClient client = _server.available();
// reset _lineMode
_lineMode = LINE_MODE_REQUEST;
if(client) { // ensure a client is connected
Serial.println("WebServer - processing new request: ");
int i = 0;
int headerIndex = 0;
int contentLength = 0;
while(client.connected() && i < 50) { // ensure connection still open
if(client.available()) { // ensure client stream is available
// parse request line
if(_lineMode == LINE_MODE_REQUEST) {
//read a new line into the buffer
readLine(client);
Serial.println("Processing request line...");
char method[REQ_METHOD_SIZE];
char path[REQ_PATH_SIZE];
char httpVersion[REQ_VERSION_SIZE];
char params[REQ_PARAMS_STR_SIZE];
// process request line
byte res = parseLineRequest(method, path, params, httpVersion);
if(res == PARSE_SUCCESS) {
//print results
Serial.println("Success!!!");
Serial.print("Method: "); Serial.println(method);
Serial.print("Path: "); Serial.println(path);
Serial.print("Params string: "); Serial.println(params);
Serial.print("Version: "); Serial.println(httpVersion);
//assign values on request
req.method = method;
req.path = path;
req.httpVersion = httpVersion;
parseQueryParams(params, req.params);
} else {
Serial.println("FAILED");
}
//set line mode to headers for next iteration
_lineMode = LINE_MODE_HEADER;
// parse header line
} else if (_lineMode == LINE_MODE_HEADER) {
//read a new line into the buffer
readLine(client);
Serial.println("Processing header line...");
char headerKey[REQ_HEADER_NAME_SIZE];
char headerValue[REQ_HEADER_VALUE_SIZE];
// if blank line, switch to body mode and continue loop
if(_lineBuffer[0] == '\r') {
Serial.println("Empty line, switching to body mode");
_lineMode = LINE_MODE_BODY;
continue;
}
//parse line
parseLineHeader(headerKey, headerValue);
//add header to request
HttpHeader h;
h.key = headerKey;
h.value = headerValue;
req.headers[headerIndex] = h;
headerIndex++;
// check for content length header and update local var
if(strcmp(headerKey, "Content-Length") == 0) {
contentLength = h.value.toInt();
Serial.print("Found Content-Length header: ");
Serial.println(contentLength);
}
//print parsed value
Serial.print("Added header - "); Serial.print(h.key); Serial.print(": "); Serial.println(h.value);
//parse body line
} else if (_lineMode == LINE_MODE_BODY) {
Serial.println("Processing body...");
char body[REQ_BODY_SIZE];
memset(body, 0, REQ_BODY_SIZE); // clear body buffer
client.readBytes(body, contentLength);
req.body = String(body);
Serial.println(req.body);
// return the parsed request object, for external handling, make sure to add client first
req.client = client;
return 1;
} // end if (_lineMode == ...)
} //end if (client.available())
i++;
Serial.print("End loop - i = "); Serial.println(i);
} //end while(client.connected())
} //end if (client)
return -1; // return fail if no incoming requests
};
// clear line buffer and read next line
void WebServer::readLine(WiFiClient client) {
memset(_lineBuffer, 0, WS_LINE_BUFFER_SIZE);
client.readBytesUntil(WS_LINE_TERMINATOR, _lineBuffer, WS_LINE_BUFFER_SIZE);
}
// parse the HTTP method, path, and query param strings from the current line in _lineBuffer
byte WebServer::parseLineRequest(char* method, char* path, char* params, char* version) {
// check matches
MatchState ms;
char* regexParams = "^(%u-) (%S-)%?(%S-) (HTTP.*)";
char* regexNoParams = "^(%u-) (%S-) (HTTP.*)";
int res;
int expectedMatches;
ms.Target(_lineBuffer);
if(String(_lineBuffer).indexOf('?') > 0){
res = ms.Match(regexParams);
expectedMatches = 4;
} else {
res = ms.Match(regexNoParams);
expectedMatches = 3;
}
// process results
switch(res) {
case REGEXP_MATCHED: //match
{ // enclosing scope for variables created in this branch of the switch
int matchCount = ms.level;
if(matchCount != expectedMatches) { // unexpected number of matches
Serial.print("Unexpected number of matches when parsing request line: expected = 4, actual = ");
Serial.println(matchCount);
break;
}
//get captured groups
if(expectedMatches == 4) {
ms.GetCapture(method, 0);
ms.GetCapture(path, 1);
ms.GetCapture(params, 2);
ms.GetCapture(version, 3);
} else {
ms.GetCapture(method, 0);
ms.GetCapture(path, 1);
ms.GetCapture(version, 2);
}
return PARSE_SUCCESS;
}
break;
case REGEXP_NOMATCH: //no match
Serial.print("No matches found...");
break;
default: //some sort of error
Serial.print("Error trying to match...");
}
// if we exit the switch statement before returning, that means there was a problem parsing.
return PARSE_FAIL;
};
// Parse query params from string 'paramStr' into the QueryParam array 'dest'
void WebServer::parseQueryParams(char* paramStr, QueryParam* dest) {
bool inKey = true;
char keyBuffer[REQ_PARAMS_STR_SIZE];
char valueBuffer[REQ_PARAMS_STR_SIZE];
QueryParam paramBuffer;
int bufferIndex = 0;
int paramIndex = 0;
//make sure buffers are empty to start
memset(keyBuffer, 0, REQ_PARAMS_STR_SIZE);
memset(valueBuffer, 0, REQ_PARAMS_STR_SIZE);
Serial.println("Parsing query params");
for(int i = 0; i < REQ_PARAMS_STR_SIZE; i++) {
char c = paramStr[i];
Serial.print("Current char: "); Serial.print(c);
//return if reached end of string
if(c == 0x00) {
Serial.println("..end");
//add last from buffer and return
dest[paramIndex] = QueryParam { String(keyBuffer), String(valueBuffer) };
return;
}
if(inKey) { // processing a key
Serial.print("..in key");
if(c == '=') {
Serial.println("..end of key");
// end of key
inKey = false;
bufferIndex = 0;
} else {
Serial.println("..add to buffer");
//add char to keyBuffer, and increment bufferIndex
keyBuffer[bufferIndex] = c;
bufferIndex++;
}
} else { // processing a value
Serial.print("..in value");
if(c == '&') {
Serial.println("..end of value");
// end of value
inKey = true;
//add QueryParam object
dest[paramIndex] = QueryParam { String(keyBuffer), String(valueBuffer) };
//clear buffers
memset(keyBuffer, 0x00, REQ_PARAMS_STR_SIZE);
memset(valueBuffer, 0x00, REQ_PARAMS_STR_SIZE);
//update indices
paramIndex++;
bufferIndex = 0;
} else {
Serial.println("..add to buffer");
//add char to valueBuffer and increment bufferIndex
valueBuffer[bufferIndex] = c;
bufferIndex++;
}
}
}
};
// parse an HTTP header from the current line in _lineBuffer
byte WebServer::parseLineHeader(char* key, char* value) {
MatchState ms;
ms.Target(_lineBuffer);
char res = ms.Match("^(.-): (.*)");
switch(res) {
case REGEXP_MATCHED:
{
int matchCount = ms.level;
if(matchCount != 2) {
Serial.print("Unexpected number of matches when parsing header line: expected = 2, actual = ");
Serial.println(matchCount);
break;
}
//get captured groups
ms.GetCapture(key, 0);
ms.GetCapture(value, 1);
}
break;
case REGEXP_NOMATCH:
Serial.println("No matches found...");
break;
default: //some sort of error
Serial.print("Error trying to match...");
}
// if we exit the switch statement before returning, that means there was a problem parsing.
return PARSE_FAIL;
};
// ==== WebRequest ====
//return a WebResponse object that can be used to reply to the incoming request
WebResponse WebRequest::getResponse() {
//create new response object
WebResponse res;
res.client = client;
res.httpVersion="HTTP/1.1";
//set up default status
res.status = HTTP_OK;
//set up default headers
res.addHeader("Content-Type", "text/plain");
res.addHeader("Server", "Arduino NANO 33 IoT - Snake Tank Controller");
res.addHeader("Connection", "close");
return res;
};
// Update 'dest' with header specified by 'name'
bool WebRequest::getHeader(String name, HttpHeader& dest) {
for(int i = 0; i < REQ_HEADER_COUNT; i++) {
HttpHeader curr = headers[i];
if(curr.key == name) {
dest = curr;
return true;
}
}
return false;
}
// ==== WebResponse ====
// Add new header with floating point value
int WebResponse::addHeader(const char* key, const float value) {
HttpHeader h { key, String(value, 10) };
return addHeader(h);
};
// Add new header with integer value
int WebResponse::addHeader(const char* key, const long value) {
String(value, 10);
char valueStr[32];
itoa(value, valueStr, 10);
HttpHeader h { key, valueStr };
return addHeader(h);
};
// Add a new header to the header list. Returns 1 for success, returns -1 if error.
int WebResponse::addHeader(const char* key, const char* value) {
HttpHeader h { key, value };
return addHeader(h);
};
// Add a new header to the header list. Returns 1 for success, returns -1 if error.
int WebResponse::addHeader(HttpHeader h) {
//make sure we have room
if(_currentHeaderIndex >= REQ_HEADER_COUNT) {
return -1; //return fail
}
//add header
headers[_currentHeaderIndex] = h;
_currentHeaderIndex += 1;
return 1; // return success
};
// Attempt to send the response to the requesting client. Returns -1 for fail, 1 for success.
int WebResponse::send() {
if(client.connected()) {
//send version and status line.
client.println(httpVersion + " " + status);
// calculate content-length header and add
int bodyLen = body.length();
if(bodyLen > 0) {
char bls[5];
itoa(bodyLen, bls, 10);
addHeader("Content-Length", bls);
} else {
addHeader("Content-Length", "0");
}
//send headers
for (int i = 0; i < _currentHeaderIndex; i++) {
HttpHeader h = headers[i];
client.println(h.key + ": " + h.value);
}
client.println(); // empty line to signify end of headers
client.print(body);// send body
delay(20); // wait for client to receive all data
// close connection and return success
client.stop();
return 1;
}
return -1; // client not connected.
}
Router
Provides a simple REST-ful(ish) HTTP router implementation. I have a decent amount of experience with the express.js library for NodeJS, so I took the inspiration for this routing library from that. This bare bones module provides the ability to define routes with any HTTP method and static path (static meaning it does not support parameterized routes, like /items/:id/
- yet), and callback function. It can then automatically route incoming requests to the appropriate callback function based on the method and path of the request.
CPP Header
This file contains the definitions for the Route
and Router
classes, the max number of routes supported by the router, and a custom type definition for route callback functions:
rest_callback_t
- defines a custom type representing a pointer to a function that accepts two parameters of typeWebRequest
andWebResponse
in that orderROUTER_MAX_ROUTES = 20;
- defines the maximum number of routes that a router instance can hold.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#ifndef ROUTER_H
#define ROUTER_H
#include "WebServer.h"
typedef void(*rest_callback_t)(WebRequest&, WebResponse&); // typedef for rest route callback function
const byte ROUTER_MAX_ROUTES = 20; // max number of routes that can be held by the router
// represents a REST route in the router
struct Route {
String method; // request method
String path; // request path
rest_callback_t callback;
};
// REST server router class
class Router {
private:
int _routeIndex; // current route index, used for adding new routes to the correct location.
public:
WebServer server; // the underlying WebServer instance
Route routes[ROUTER_MAX_ROUTES]; // list of routes currently set up
bool route(String, String, rest_callback_t); // add new route to the router (method, path, callback);
bool get(String, rest_callback_t); // helper to add new GET route
bool post(String, rest_callback_t); // helper to add new POST route
void handle(WebRequest&);
};
#endif
Implementation
Router::route(String method, String path, rest_callback_t cb)
- This method is the main method that allows for defining new routes, given the HTTP
method
, the requestpath
, and a pointer to the desiredcb
function. Router::get(String path, rest_callback_t cb)
- A shortcut for registering a new route that uses the HTTP
GET
method. Router::post(String path, rest_callback_t cb)
- A shortcut for registering a new route that uses the HTTP
POST
method. Router::handle(String path, rest_callback_t cb)
- This method is responsible for routing incoming requests to th correct handler/callback function. It looks through each registered route and finds one that matches the
method
andpath
. If a matching route is found, aWebResponse
object is created using theWebRequest::getResponse()
function, and the callback function is called with the existing request and new response parameter. If no matching route is found, the router automatically responds to the client with a404 Not Found
response.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "Router.h"
#include "WebServer.h"
//register a new route on the router
bool Router::route(String method, String path, rest_callback_t cb) {
if(_routeIndex < ROUTER_MAX_ROUTES) {
routes[_routeIndex] = Route { method, path, cb};
_routeIndex++;
}
return false;
};
//Helper to register new GET route
bool Router::get(String path, rest_callback_t cb) {
return route("GET", path, cb);
};
//Helper to register new POST route
bool Router::post(String path, rest_callback_t cb) {
return route("POST", path, cb);
};
// Handle an incoming WebRequest by calling the correct handler for the path
void Router::handle(WebRequest& req) {
// find matching route
for(int i = 0; i < _routeIndex; i++) {
Route r = routes[i];
if(r.method == req.method && r.path == req.path) {
WebResponse res = req.getResponse();
r.callback(req, res);
return;
}
}
Serial.println("No matching routes for request");
WebResponse res = req.getResponse();
res.status = HTTP_NOT_FOUND;
res.send();
};
WifiController
This class encapsulates the functionality of the WiFiNINA module, such as verifying and establishing wireless connections, in an easy to use static class.
CPP Header
This file contains the definitions for the WifiController
and WifiControllerSettings
classes:
- WifiControllerSettings - Struct representing wifi connection settings such as SSID and password
- WifiController - Static class providing methods for connecting to networks and monitoring/displaying network status
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#ifndef WIFI_CONTROLLER_H
#define WIFI_CONTROLLER_H
#include "Arduino.h"
#include "MacAddress.h"
// Represents settings for a WifiController object
struct WifiControllerSettings {
String ssid;
String password;
bool requireLatestFirmware;
int connectionCheckInterval;
WifiControllerSettings(String, String, bool, int);
};
// Provides methods for managing wifi connections
class WifiController {
private:
static String firmwareVersion;
static MacAddress macAddress;
static WifiControllerSettings* settings;
public:
static void init(WifiControllerSettings*);
static bool verifyModule();
static bool verifyFirmware();
static MacAddress getMacAddress();
static void connect();
static void checkConnectionStatus();
static void printAvailableNetworks();
static void printNetwork(byte);
static String statusToString(byte);
};
#endif
Implementation
- global
encryptionTypeToString(byte encryptionType)
- Global method that converts an encryption type code to the corresponding string. Returns
"Unknown"
for invalid encryption type. WifiController::init(WifiControllerSettings* s)
- This method is responsible for verifying the status of the wifi module and firmware, loading in the device mac address, connecting to the network specified in
settings
, displaying the connection status, and setting up a repeating timer to continually check for and resolve any connection issues. WifiController::statusToString(byte status)
- Helper method that converts a connection status code to a string. Returns
"N/A"
for invalid status. WifiController::verifyModule()
- Verify that the WiFiNINA module is properly installed on the device. Return false if installation issues found, true otherwise.
WifiController::verifyFirmware()
- Verify that the WiFiNINA firmware is properly installed on the device. Return false if installation issues found, true otherwise.
WifiController::WifiController::getMacAddress()
- Return a
MacAddress
object (see Networking > WifiData) representing the MAC address of the current device. WifiController::WifiController::connect()
- Connect to the WifiNetwork specified in
settings->ssid
andsettings->password
. Automatically retry connection up to three times upon connection failure, allowing 5 seconds between each attempt. WifiController::WifiController::checkConnectionStatus()
- Called once per
settings->connectionCheckInterval
ms, and responsible for checking the connection status, and reconnecting if status is notWL_CONNECTED
. WifiController::WifiController::printAvailableNetworks()
- Scan available networks to get count, and use the
printNetwork()
function to display details for each. WifiController::WifiController::printNetwork(byte i)
- Print the network details (ssid, mac, encryption type, channel, and signal strength) of the available wifi network identified by
i
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#include "Arduino.h"
#include "WiFi.h"
#include "TimeAlarms.h"
#include "MacAddress.h"
#include "WifiController.h"
// Convert an encryption type enum to a string
String encryptionTypeToString(byte encryptionType) {
switch(encryptionType) {
case ENC_TYPE_TKIP:
return "WPA";
break;
case ENC_TYPE_WEP:
return "WEP";
break;
case ENC_TYPE_CCMP:
return "WPA2";
break;
case ENC_TYPE_AUTO:
return "Auto";
break;
case ENC_TYPE_NONE:
return "Open";
break;
case ENC_TYPE_UNKNOWN:
return "Unknown";
break;
}
};
// Create instance of WifiControllerSettings with specified
// ssid, pass, 'requireLatestFw' and connection check interval
WifiControllerSettings::WifiControllerSettings(String ssid, String pass, bool requireLatestFw, int connCheckInterval)
: ssid(ssid), password(pass), requireLatestFirmware(requireLatestFw),
connectionCheckInterval(connCheckInterval) { }
// static member initializers
String WifiController::firmwareVersion;
MacAddress WifiController::macAddress = NULL;
WifiControllerSettings* WifiController::settings;
// Init WifiController module with specified settings
void WifiController::init(WifiControllerSettings* s) {
settings = s;
//modules
Serial.println("Checking module: ");
if(!verifyModule()){
Serial.println("Aborting WifiController.init()");
}
//firmware
Serial.println("Checking firmware: ");
bool fwCurrent = verifyFirmware();
if(settings->requireLatestFirmware && !fwCurrent) {
Serial.println("Aborting WifiController.init() - latest firmware required.");
}
//mac address
Serial.println("Getting MAC Address:");
macAddress = getMacAddress();
Serial.println(macAddress.toString());
//print network list
//printAvailableNetworks();
//connect to the network
connect();
//display network info
IPAddress address = WiFi.localIP();
address.printTo(Serial); Serial.println();
//set up timer to check connection status and attempt to resolve disconnects
Alarm.timerRepeat(settings->connectionCheckInterval, checkConnectionStatus);
};
// Convert wifi status enum value to string
String WifiController::statusToString(byte status) {
switch(status) {
case WL_NO_MODULE:
return "No module available";
case WL_IDLE_STATUS:
return "Idle";
case WL_NO_SSID_AVAIL:
return "No SSID available";
case WL_SCAN_COMPLETED:
return "Scan completed";
case WL_CONNECTED:
return "Connected";
case WL_CONNECT_FAILED:
return "Connect failed";
case WL_CONNECTION_LOST:
return "Connection lost";
case WL_DISCONNECTED:
return "Disconnected";
case WL_AP_CONNECTED:
return "AP Connected";
case WL_AP_FAILED:
return "AP Failed";
case WL_AP_LISTENING:
return "AP Listening";
default:
return "N/A";
}
};
// Verify the wifi capabilities on the device
bool WifiController::verifyModule() {
byte moduleStatus = WiFi.status();
if(moduleStatus == WL_NO_MODULE) {
Serial.println(" Communication with WiFi module failed. Not connecting..");
return false;
}
Serial.println(" Success - module status: " + statusToString(moduleStatus));
return true;
};
// Verify that the wifi firmware is up-to-date
bool WifiController::verifyFirmware() {
firmwareVersion = WiFi.firmwareVersion();
if(firmwareVersion < WIFI_FIRMWARE_LATEST_VERSION) {
Serial.println(" Firmware outdated: current(" + firmwareVersion
+ ") latest(" + WIFI_FIRMWARE_LATEST_VERSION + ")");
return false;
}
Serial.println(" Firmware up to date: current(" + firmwareVersion + ")");
return true;
};
// Return the devices MAC address
MacAddress WifiController::getMacAddress() {
byte macBytes[6];
WiFi.macAddress(macBytes);
MacAddress mac = MacAddress(macBytes);
return mac;
};
// Connect to the wifi network specified in the current settings
void WifiController::connect() {
Serial.println("Connecting to network: " + settings->ssid);
int ssidLen = settings->ssid.length()+1;
int passLen = settings->password.length()+1;
char ssid[ssidLen];
char pass[passLen];
settings->ssid.toCharArray(ssid, ssidLen);
settings->password.toCharArray(pass, passLen);
int attempts = 3;
while(WiFi.status() != WL_CONNECTED && attempts > 0) {
attempts--;
Serial.print("Wifi.begin("); Serial.print(ssid); Serial.print(", "); Serial.print(pass); Serial.println(")");
WiFi.begin(ssid, pass);
delay(5000);
Serial.println("Status: " + statusToString(WiFi.status()));
}
};
// Check the current network connection status
void WifiController::checkConnectionStatus() {
if(WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected - attempting to reconnect");
connect();
}
};
// Print a list of all currently available networks
void WifiController::printAvailableNetworks() {
Serial.println("Scanning networks...");
byte networkCount = WiFi.scanNetworks();
if(networkCount == -1) {
Serial.println("Unable to find any networks");
return;
}
Serial.print("Scan complete - ");
Serial.print(networkCount);
Serial.println(" networks found");
for(byte i = 0; i < networkCount; i++) {
printNetwork(i);
}
};
// Print the wifi network specified by 'byte'
void WifiController::printNetwork(byte i) {
Serial.print(i+1); Serial.print(" : ");
//print ssid
Serial.print("SSID: "); Serial.print(WiFi.SSID(i));
// print mac address of AP
byte bssid[6];
WiFi.BSSID(i, bssid);
MacAddress apMacAddr = MacAddress(bssid);
Serial.print("\tBSSID: "); Serial.print(apMacAddr.toString());
// print encryption type
Serial.print("\tEncryption: ");
Serial.println(encryptionTypeToString(WiFi.encryptionType(i)));
// print channel
Serial.print("\tChannel: "); Serial.print(WiFi.channel(i));
// print signal strength
Serial.print("\tSignal: "); Serial.print(WiFi.RSSI(i)); Serial.println(" dBm");
};
MacAddress
Struct representing a mac address, stored as a byte
array of size 6.
CPP Header
1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef WIFI_DATA_H
#define WIFI_DATA_H
#include "Arduino.h"
// Represents a MC address
struct MacAddress {
byte bytes[6];
MacAddress(byte[6]);
String toString();
};
#endif
Implementation
MacAddress::MacAddress(byte b[6])
- constructor- Create a new
MacAddress
object, storing the provided value ofb
in the localbytes
variable MacAddress::toString()
- Convert the address stored in
bytes
to a string, represented in hexadecimal format.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "MacAddress.h""
// Create instance of MacAddress
MacAddress::MacAddress(byte b[6]){
for(int i = 0; i < 5; i++) {
bytes[i] = b[i];
}
};
// Convert to string HEX representation of current mac address
String MacAddress::toString() {
String result;
for(int i = 5; i >= 0; i--) {
if(bytes[i] < 0xF) { //add leading zero if only one hex digit.
result += "0";
}
result += String(bytes[i], HEX); //print digit(s)
if(i > 0) { // add colon separators
result += ":";
}
}
return result;
};
routes
This field contains definitions of all the route handler callback functions used when defining routes (see Networking > Router). It also contains some global helper methods used inside the route handlers, and constants defining all the different header names used in the handlers.
CPP Header and Implementation
bool updateFixedSchedule(String body)
- helper method- Parse a request
body
to get aFixedSchedule
(see Utilities > Scheduling) and update the globallightControllerSchedule
(see climate_control.ino) with the new schedule. bool updateMonthlySchedule(String body)
- helper method- Parse a request
body
to get aMonthlySchedule
(see Utilities > Scheduling) and update the globallightControllerSchedule
(see climate_control.ino) with the new schedule. route_getTest
- Handles the
GET /test
route, used for testing the web server functionality. This handler prints the first 4 query params fromreq.params
, adds a new header calledHeader-Test
to the response, adds a test body contents, and sends response. route_getTime
- Handles the
GET /time
route, used by the mobile app to display the servers current time. This handler returns the time as response headers in two formats: as a raw UTC value, and as local time in individual component value (year, month, day, hours, minutes, seconds) route_getHumiditySettings
- Handles the
GET /humidity/settings
route, used by the mobile app to display the current humidity controller settings. This handler returns 4 values as headers: target, kick-on, fan stop delay, and update interval route_postHumiditySettings
- Handles the
POST /humidity/settings
route, used by the mobile app to update the current humidity controller settings. This handler parses optional parameters from the request headers (target, kick-on, fan stop delay, and update interval), updating aBitflag
depending which parameters were provided on the request. Then we use the bitflag to determine which values to update, adding a response header with the new value afterwards. Finally once all settings are updated, the response is sent. route_getHumidityStatus
- Handles the
GET /humidity/status
route, used by the mobile app to display the current status of the humidity controller. This handler returns the following values as response headers: average and individual temperature, average and individual humidity, and enabled/disabled status of the atomizer and fans. route_getLightStatus
- Handles the
POST /lights/status
route, used by the mobile app to display the current day/night light status. This handler returns a single header with a string representing the current light status - eitherday
ornight
. route_getLightSchedule
- Handles the
GET /lights/schedule
route, used by the mobile app to display the current light schedule. This handler returns a header with the schedule type (1
for fixed,2
for monthly), and body text containing oneScheduleEntry
per line in the format:D{HH:MM:SS} N{HH:MM:SS}
- one entry for fixed, 12 for monthly route_postLightSchedule
- Handles the
POST /lights/schedule
route, used by the mobile app to update the current light schedule. This handler accepts a header with the schedule type (1
for fixed,2
for monthly), and body text containing oneScheduleEntry
per line in the format:D{HH:MM:SS} N{HH:MM:SS}
- one entry for fixed, 12 for monthly. It uses theupdateFixedSchedule()
orupdateMonthlySchedule()
to update the schedule, and returns a response equivelant to that of theGET /lights/schedule
route with the newly updated schedule.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
#ifndef ROUTES_H
#define ROUTES_H
#include "Regexp.h"
#include "globals.h"
#include "WebServer.h"
#include "Bitflag.h"
#include "Lines.h"
// web server headers
const char* HEAD_TIME_UTC = "x-Time-UTC";
const char* HEAD_TIME_YEAR = "x-Time-Year";
const char* HEAD_TIME_MONTH = "x-Time-Month";
const char* HEAD_TIME_DAY = "x-Time-Day";
const char* HEAD_TIME_HOUR = "x-Time-Hour";
const char* HEAD_TIME_MINUTE = "x-Time-Minute";
const char* HEAD_TIME_SECOND = "x-Time-Second";
const char* HEAD_HUMIDITY_TARGET = "x-Humidity-Target";
const char* HEAD_HUMIDITY_KICK_ON = "x-Humidity-KickOn";
const char* HEAD_HUMIDITY_FAN_STOP = "x-Humidity-FanStopDelay";
const char* HEAD_HUMIDITY_UPDATE = "x-Humidity-UpdateInterval";
const char* HEAD_HUMIDITY_AVERAGE = "x-Humidity-Average";
const char* HEAD_HUMIDITY_ONE = "x-Humidity-SensorOne";
const char* HEAD_HUMIDITY_TWO = "x-Humidity-SensorTwo";
const char* HEAD_HUMIDITY_FANS = "x-Humidity-Fans";
const char* HEAD_HUMIDITY_ATOMIZER = "x-Humidity-Atomizer";
const char* HEAD_TEMP_AVERAGE = "x-Temperature-Average";
const char* HEAD_TEMP_ONE = "x-Temperature-SensorOne";
const char* HEAD_TEMP_TWO = "x-Temperature-SensorTwo";
const char* HEAD_LIGHT_MODE = "x-Light-Mode"; // current light mode (day/night)
const char* HEAD_LS_TYPE = "x-Light-Schedule-Type"; // light schedule type (fixed, monthly, etc.)
bool updateFixedSchedule(String body) {
Serial.println("Updating fixed schedule...");
char bodyChars[body.length()+1];
char sDayHour[2], sDayMin[2], sDaySec[2], sNightHour[2], sNightMin[2], sNightSec[2];
int dayHour, dayMin, daySec, nightHour, nightMin, nightSec;
MatchState ms;
// perform matching
memset(bodyChars,0,body.length()+1);
Serial.println(body);
Serial.println(body.length());
body.toCharArray(bodyChars, body.length()+1, 0);
ms.Target(bodyChars);
int res = ms.Match("D{(%d%d):(%d%d):(%d%d)}N{(%d%d):(%d%d):(%d%d)}", 0); // (%d%d):(%d%d):(%d%d)
Serial.println(res);
Serial.println(ms.level);
if(res != REGEXP_MATCHED) {
Serial.print("error matching: "); Serial.println(res);
return false;
}
if(ms.level != 6) {
Serial.println("not enough matches");
return false;
}
// capture matches
ms.GetCapture(sDayHour, 0);
ms.GetCapture(sDayMin, 1);
ms.GetCapture(sDaySec, 2);
ms.GetCapture(sNightHour, 3);
ms.GetCapture(sNightMin, 4);
ms.GetCapture(sNightSec, 5);
// convert to ints
dayHour = String(sDayHour).toInt();
dayMin = String(sDayMin).toInt();
daySec = String(sDaySec).toInt();
nightHour = String(sNightHour).toInt();
nightMin = String(sNightMin).toInt();
nightSec = String(sNightSec).toInt();
// update schedule
Serial.println("updating");
lightControllerSchedule = new FixedSchedule(
new ScheduleEntry(
Time(dayHour, dayMin, daySec),
Time(nightHour, nightMin, nightSec)
)
);
return true;
};
bool updateMonthlySchedule(String body) {
Serial.println("Updating monthly schedule");
char line[LINE_SIZE];
char sDayHour[2], sDayMin[2], sDaySec[2], sNightHour[2], sNightMin[2], sNightSec[2];
int dayHour, dayMin, daySec, nightHour, nightMin, nightSec;
MatchState ms;
ScheduleEntry* entries[LINE_COUNT];
Lines lines = Lines::split(body.c_str());
lines.printLines();
for(int i = 0; i < LINE_COUNT; i++) {
// get line and match
Serial.print("Checking line: "); Serial.println(i);
lines.getLine(line, i);
Serial.println(line);
ms.Target(line);
char res = ms.Match("D{(%d%d):(%d%d):(%d%d)}N{(%d%d):(%d%d):(%d%d)}", 0);
if(res != REGEXP_MATCHED) {
Serial.print("error matching: "); Serial.println(res);
return false;
}
if(ms.level != 6) {
Serial.println("not enough matches");
return false;
}
// capture matches
ms.GetCapture(sDayHour, 0);
ms.GetCapture(sDayMin, 1);
ms.GetCapture(sDaySec, 2);
ms.GetCapture(sNightHour, 3);
ms.GetCapture(sNightMin, 4);
ms.GetCapture(sNightSec, 5);
// convert to ints
dayHour = String(sDayHour).toInt();
dayMin = String(sDayMin).toInt();
daySec = String(sDaySec).toInt();
nightHour = String(sNightHour).toInt();
nightMin = String(sNightMin).toInt();
nightSec = String(sNightSec).toInt();
entries[i] = new ScheduleEntry(Time(dayHour, dayMin, daySec), Time(nightHour, nightMin, nightSec));
}
lightControllerSchedule = new MonthlySchedule(entries);
return true;
};
void route_getTest(WebRequest& req, WebResponse& res) {
for(int i = 0; i < 4; i++) {
Serial.print("params["); Serial.print(i); Serial.print("].key = "); Serial.println(req.params[i].key);
Serial.print("params["); Serial.print(i); Serial.print("].value = "); Serial.println(req.params[i].value);
}
res.addHeader("Header-Test", "Test header");
res.body="Test body contents";
res.send();
};
void route_getTime(WebRequest& req, WebResponse& res){
res.addHeader(HEAD_TIME_UTC, (long)now());
res.addHeader(HEAD_TIME_YEAR, (long)year());
res.addHeader(HEAD_TIME_MONTH, (long)month());
res.addHeader(HEAD_TIME_DAY, (long)day());
res.addHeader(HEAD_TIME_HOUR, (long)hour());
res.addHeader(HEAD_TIME_MINUTE, (long)minute());
res.addHeader(HEAD_TIME_SECOND, (long)second());
res.send();
};
void route_getHumiditySettings(WebRequest& req, WebResponse& res){
res.addHeader(HEAD_HUMIDITY_TARGET, humidityControllerSettings->targetHumidity);
res.addHeader(HEAD_HUMIDITY_KICK_ON, humidityControllerSettings->kickOnHumidity);
res.addHeader(HEAD_HUMIDITY_FAN_STOP, (long)(humidityControllerSettings->fanStopDelay));
res.addHeader(HEAD_HUMIDITY_UPDATE, (long)(humidityControllerSettings->updateInterval));
res.send();
};
void route_postHumiditySettings(WebRequest& req, WebResponse& res){
HttpHeader target, kickOn, fanStop, updateInterval;
Bitflag valuesProvided; // bitflag to indicate which values should be updated
//get relevant headers and set flag bits when found
if (req.getHeader(HEAD_HUMIDITY_TARGET, target)) {
valuesProvided.setBit(BIT_HUM_TARGET);
}
if (req.getHeader(HEAD_HUMIDITY_KICK_ON, kickOn)) {
valuesProvided.setBit(BIT_HUM_KICK_ON);
}
if (req.getHeader(HEAD_HUMIDITY_FAN_STOP, fanStop)) {
valuesProvided.setBit(BIT_HUM_FAN_STOP);
}
if (req.getHeader(HEAD_HUMIDITY_UPDATE, updateInterval)) {
valuesProvided.setBit(BIT_HUM_UPDATE);
}
if(!valuesProvided.checkAny()) {
//return 400 BadRequest unless at least one new value provided
res.status = HTTP_BAD_REQUEST;
res.body = "No update values provided, unable to process request.";
} else {
if(valuesProvided.checkBit(BIT_HUM_TARGET)) {
//update target humidity
Serial.println("Updating target humidity");
humidityControllerSettings->targetHumidity = target.value.toFloat();
res.addHeader(HEAD_HUMIDITY_TARGET, humidityControllerSettings->targetHumidity);
}
if(valuesProvided.checkBit(BIT_HUM_KICK_ON)) {
//update kickon humidity
Serial.println("Updating kickon humidity");
humidityControllerSettings->kickOnHumidity = kickOn.value.toFloat();
res.addHeader(HEAD_HUMIDITY_KICK_ON, humidityControllerSettings->kickOnHumidity);
}
if(valuesProvided.checkBit(BIT_HUM_FAN_STOP)) {
//update fan stop delay
Serial.println("Updating fan stop delay");
humidityControllerSettings->fanStopDelay = fanStop.value.toInt();
res.addHeader(HEAD_HUMIDITY_FAN_STOP, (long)(humidityControllerSettings->fanStopDelay));
}
if(valuesProvided.checkBit(BIT_HUM_UPDATE)) {
//update humidity update check interval
Serial.println("Updating humidity check interval");
humidityControllerSettings->fanStopDelay = updateInterval.value.toInt();
res.addHeader(HEAD_HUMIDITY_UPDATE, (long)(humidityControllerSettings->updateInterval));
}
}
res.send();
};
void route_getHumidityStatus(WebRequest& req, WebResponse& res){
float avgTemp, tempOne, tempTwo;
float avgHum, humOne, humTwo;
bool fansEnabled, atomizerEnabled;
HumidityController::controlStatus(fansEnabled, atomizerEnabled);
HumidityController::humidity(avgHum, humOne, humTwo);
HumidityController::temperature(avgTemp, tempOne, tempTwo);
res.addHeader(HEAD_TEMP_AVERAGE, avgTemp);
res.addHeader(HEAD_TEMP_ONE, tempOne);
res.addHeader(HEAD_TEMP_TWO, tempTwo);
res.addHeader(HEAD_HUMIDITY_AVERAGE, avgHum);
res.addHeader(HEAD_HUMIDITY_ONE, humOne);
res.addHeader(HEAD_HUMIDITY_TWO, humTwo);
res.addHeader(HEAD_HUMIDITY_FANS, fansEnabled ? "enabled" : "disabled");
res.addHeader(HEAD_HUMIDITY_ATOMIZER, atomizerEnabled ? "enabled" : "disabled");
res.send();
};
void route_getLightStatus(WebRequest& req, WebResponse& res){
res.addHeader(HEAD_LIGHT_MODE, LightController::getStatusString());
res.send();
};
void route_getLightSchedule(WebRequest& req, WebResponse& res){
//add type header
int schedTypeCode = lightControllerSchedule->getScheduleType();
int bodySize = 25;
res.addHeader(HEAD_LS_TYPE, (long)schedTypeCode);
Serial.println(schedTypeCode);
if(schedTypeCode == ScheduleType::MONTHLY) {
bodySize = 12 * 25; // upgrade body size to hold 12 lines
}
//get body string
char body[bodySize];
memset(body, 0, bodySize);
lightControllerSchedule->toString(body);
// add body to request
res.body = body;
Serial.println(res.body);
//send response
res.send();
};
void route_postLightSchedule(WebRequest& req, WebResponse& res){
HttpHeader schedType;
bool validSchedProvided = false;
int schedTypeCode = 0;
Serial.println(req.body);
// check schedule type header is valid
if(req.getHeader(HEAD_LS_TYPE, schedType)) {
schedTypeCode = schedType.value.toInt();
validSchedProvided = Schedule::validScheduleType(schedTypeCode);
}
if(validSchedProvided) {
bool updateSucceeded = false;
// check which type we're updating to.
switch(schedTypeCode) {
case ScheduleType::FIXED:
{
Serial.println("fixed");
// update fixed schedule
updateSucceeded = updateFixedSchedule(req.body);
break;
}
case ScheduleType::MONTHLY:
{
Serial.println("monthly");
// update monthly schedule
updateSucceeded = updateMonthlySchedule(req.body);
break;
}
}
if(updateSucceeded) {
Serial.println("Success updating schedule");
char body[300];
memset(body, 0, 300);
lightControllerSchedule->toString(body);
Serial.println(body);
} else {
Serial.println("Failed to update schedule");
res.status = HTTP_SERVER_ERROR;
}
// send response
res.send();
} else {
// return bad formatted request
res.status = HTTP_BAD_REQUEST;
res.body = "Invalid request, unable to parse";
res.send();
}
}
#endif
Utilities
Bitflag
Class that implements a basic Bit Flag/Bit Field algorithm. Bit flags are a memory compact way of storing multiple boolean values (flags) as the individual bits in a single variable. You use the bitwise OR operator |
to set bits, and the bitwise AND operator &
to check if individual bits are set.
CPP Header
This file contains the definitions for the Bitflag
class. The bits are stored on a private variable _bits
, and methods are provided to interact with the bits. Since this class is only currently used for one purpose - determining which values are requesting to be updated from the POST humidity/settings
route (see Networking > routes) - it also defines unique bit constants, each corresponding to a specific field. If the flag for a field is set, the request handler knows to update that field with the provided value.
BIT_HUM_TARGET = 0b1000;
- humidity target fieldBIT_HUM_KICK_ON = 0b0100;
- humidity kick on fieldBIT_HUM_FAN_STOP = 0b0010;
- humidity fan stop fieldBIT_HUM_UPDATE = 0b0001;
- humidity update field
Note: The current flag only need up to 4 bits, so we could use a byte or smaller variable type, but to allow for extensibility, the
_bit
field and all method arguments acceptu_int
allowing this class to work with up to 32 bits.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef BITFLAG_H
#define BITFLAG_H
#include "Arduino.h"
const byte BIT_HUM_TARGET = 0b1000; // Humidity target percent flag
const byte BIT_HUM_KICK_ON = 0b0100; // Humidity kick on percent flag
const byte BIT_HUM_FAN_STOP = 0b0010; // Humidity fan stop delay flag
const byte BIT_HUM_UPDATE = 0b0001; // Humidity update interval flag
struct Bitflag {
private:
u_int _bits = 0b0000;
public:
void setBit(u_int toSet);
bool checkBit(u_int toCheck);
bool checkAny();
};
#endif
Implementation
Bitflag::setBit(const u_int toSet)
- This method sets the bit(s) specified by
toSet
to1
in the local_bits
variable. Bitflag::checkBit(const u_int toCheck)
- This method checks if an individual bit specified by
toCheck
is1
in the local_bits
variable. If bit is set returns true, otherwise returns false. Bitflag::checkAny()
- This is a helper method to easily determine if any bits have been set yet. Returns true if the value of
_bits
is non-zero, false otherwise.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "Bitflag.h"
// Set the bit(s) specified by 'toSet'
void Bitflag::setBit(const u_int toSet) {
_bits |= toSet;
};
// Return true if 'toCheck' is set
bool Bitflag::checkBit(const u_int toCheck) {
return _bits & toCheck;
};
// Return true if any bits are set
bool Bitflag::checkAny() {
return _bits != 0;
};
DateTime
This module provides a Date
and Time
class, as well as a Comparable<T>
interface that they both implement.
CPP Header
This file contains the definitions for the three classes (Comparable<T>
, Date
, and Time
):
- **Comparable
** - interface that defines a single virtual method, `virtual int compare(T);`, which allows for comparing two objects, for example `a.compare(b)` will return 1 if `a > b`, 0 if `a = b`, and -1 if `a < b` - Date - represents a date with
year
,month
, andday
properties - Time - represents a time with
hours
minute
, andsecond
properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#ifndef DATE_TIME_COMPARABLE_H
#define DATE_TIME_COMPARABLE_H
#include "Arduino.h"
// Size of a null-terminated time string (in format HH:MM:SS)
#define TIME_STR_SIZE 9
// Size of a null-terminated time component string (digit pair)
#define TIME_COMP_SIZE 3
template <class T>
struct Comparable {
virtual int compare(T) = 0;
};
// Represents a Date object with day, month, and year
struct Date : Comparable<Date> {
byte day, month;
int year;
Date(byte, byte, int);
Date(int, byte, byte);
virtual int compare(Date);
int compareYear(Date);
int compareMonth(Date);
int compareDay(Date);
void printSerial();
};
// Represents a Time object with hours, minutes, and seconds
struct Time : Comparable<Time> {
byte hours, minutes, seconds;
Time(byte, byte, byte);
virtual int compare(Time);
int compareHours(Time);
int compareMinutes(Time);
int compareSeconds(Time);
void toString(char*);
void printSerial();
};
#endif
Implementation
compareWholeNumber(byte first, byte second)
- helper method- compares
first
andsecond
, returning 1 iffirst > second
, 0 iffirst = second
, and -1 iffirst < second
compareWholeNumber(int first, int second)
- helper method- compares
first
andsecond
, returning 1 iffirst > second
, 0 iffirst = second
, and -1 iffirst < second
Date::Date(...)
- constructor- The Date class has two constructors, allowing you to pass either
year, month, day
, orday, month, year
Date::compare(Date other)
- Compare
this
toother
returning1
,0
, or-1
depending which date is earlier. Each date component is compared individually in order of it’s magnitude - firstyear
is compared, thenmonth
, thenday
. Date::compareDay(Date other)
- Compare
this.day
toother.day
returning1
,0
, or-1
depending which date is earlier. Date::compareMonth(Date other)
- Compare
this.month
toother.month
returning1
,0
, or-1
depending which date is earlier. Date::compareYear(Date other)
- Compare
this.year
toother.year
returning1
,0
, or-1
depending which date is earlier. Date::printSerial()
- Print this date object to serial interface using
Serial.write()
Time::Time(byte hours, byte minutes, byte seconds)
- constructor- The Time class only has one constructor that takes
hours
,minutes
, andseconds
in that order. Time::compare(Time other)
- Compare
this
toother
returning1
,0
, or-1
depending which time is earlier. Each time component is compared individually in order of it’s magnitude - firsthour
is compared, thenminute
, thensecond
. Time::compareHours(Time other)
- Compare
this.hours
toother.hours
returning1
,0
, or-1
depending which is earlier. Time::compareMinutes(Time other)
- Compare
this.minutes
toother.minutes
returning1
,0
, or-1
depending which is earlier. Time::compareSeconds(Time other)
- Compare
this.seconds
toother.seconds
returning1
,0
, or-1
depending which is earlier. Time::toString(char *dest)
- Set the value of
dest
to the string representation of this time object in the formatHH:MM:SS
, adding padding zeros where necessary. Time::printSerial()
- Print this date object to serial interface using
Serial.write()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include "Arduino.h"
#include "DateTime.h"
// Compare to whole number bytes. Returns -1 if first < second,
// 1 if first > second, and 0 if they are equal.
int compareWholeNumber(byte first, byte second) {
if(first < second) {
return -1;
} else if(first > second) {
return 1;
}
return 0;
};
// Compare to whole number integers. Returns -1 if first < second,
// 1 if first > second, and 0 if they are equal.
int compareWholeNumber(int first, int second) {
if(first < second) {
return -1;
} else if(first > second) {
return 1;
}
return 0;
};
// Create new Date object with specified day, month, and year
Date::Date(byte day, byte month, int year) : day(day), month(month), year(year) { };
// Create new Date object with specified year, month, and date
Date::Date(int year, byte month, byte day) : year(year), month(month), day(day) { };
// Compare to another Date object.
// Returns:
// -1 if this is before other
// 0 if this is equal to other
// 1 if this is after other
int Date::compare(Date other) {
int cYear, cMonth, cDay;
cYear = compareYear(other);
cMonth = compareMonth(other);
cDay = compareDay(other);
if(cYear == 0) {
if(cMonth == 0) {
if(cDay == 0) {
return 0;
} else return cDay;
} else return cMonth;
} else return cYear;
};
// Compare 'day' of this Date to the 'day' of another
int Date::compareDay(Date other) {
return compareWholeNumber(day, other.day);
};
// Compare 'month' of this Date to the 'month' of another
int Date::compareMonth(Date other) {
return compareWholeNumber(month, other.month);
};
// Compare 'year' of this Date to the 'year' of another
int Date::compareYear(Date other) {
return compareWholeNumber(year, other.year);
};
// Print the Date object to Serial in the format YY-MM-DD
void Date::printSerial() {
Serial.print(year); Serial.print("-");
Serial.print(month); Serial.print("-");
Serial.println(day);
};
// Create new Time object with specified hours, minutes, and seconds
Time::Time(byte hours, byte minutes, byte seconds) : hours(hours), minutes(minutes), seconds(seconds) { };
// Compare to another Time object.
// Returns:
// -1 if this is before other
// 0 if this is equal to other
// 1 if this is after other
int Time::compare(Time other) {
int cHours, cMinutes, cSeconds;
cHours = compareHours(other);
cMinutes = compareMinutes(other);
cSeconds = compareSeconds(other);
if(cHours == 0) {
if(cMinutes == 0) {
if(cSeconds == 0) {
return 0;
} else return cSeconds;
} else return cMinutes;
} else return cHours;
};
// Compare 'hours' of this to 'hours' of other
int Time::compareHours(Time other) {
return compareWholeNumber(hours, other.hours);
};
// Compare 'minutes' of this to 'minutes' of other
int Time::compareMinutes(Time other) {
return compareWholeNumber(minutes, other.minutes);
};
// Compare 'seconds' of this to 'seconds' of other
int Time::compareSeconds(Time other) {
return compareWholeNumber(seconds, other.seconds);
};
// Convert this Time object to a string, and assign to 'dest'
void Time::toString(char *dest) {
// init char[]s for values
char digitBuffer[TIME_COMP_SIZE];
//add hours
memset(digitBuffer, 0, 3); // clear digit buffer
itoa(hours, digitBuffer, 10); // parse int into digit buffer
if(hours > 9){ //two digits, no need for extra zero
memcpy(dest, digitBuffer, 2);
} else { //one digit, need to pad leading zero
dest[0] = '0';
memcpy(dest+1, digitBuffer, 1);
}
dest[2] = ':';
//add min
memset(digitBuffer, 0, 3);
itoa(minutes, digitBuffer, 10);
if(minutes > 9){ //two digits, no need for extra zero
memcpy(dest+3, digitBuffer, 2);
} else { //one digit, need to pad leading zero
dest[3] = '0';
memcpy(dest+4, digitBuffer, 1);
}
dest[5] = ':';
//add sec
memset(digitBuffer, 0, 3);
itoa(seconds, digitBuffer, 10);
if(seconds > 9){ //two digits, no need for extra zero
memcpy(dest+6, digitBuffer, 2);
} else { //one digit, need to pad leading zero
dest[6] = '0';
memcpy(dest+7, digitBuffer, 1);
}
}
// Print Time object to serial in format hh:mm:ss
void Time::printSerial() {
Serial.print(hours); Serial.print(":");
Serial.print(minutes); Serial.print(":");
Serial.println(seconds);
};
lines
this module provides a class Lines
that represents a collection of strings (lines). This is used when parsing the body of HTTP requests for included light schedule data. This simplifies the process of having multidimensional string arrays, which is much more of a pain in C++ than other languages I’m used to.
CPP Header and Implementation
LINE_COUNT
- This represents the max number of lines that can be stored ina single
Lines
instance. LINE_SIZE
- This represents the max number of characters that can be stored in a single line (this includes the terminating NULL, so the effective size is really
LINE_SIZE - 1
). Lines::count()
- Returns the number of lines currently stored in this object.
Lines::addLine(const char* line)
- Store a string as a new line of text into this object.
LLines::getLine(char* dest, int index)
- Copy the string stored in line
index
to thedest
string. Lines::split(const char* inStr)
- Split the string in
inStr
into individual lines, using\n
as the line delimiter, and return a newLines
object containing them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#ifndef LINES_H
#define LINES_H
#define LINE_COUNT 12
#define LINE_SIZE 25
//class representing a list of strings (char [])
struct Lines {
private:
int _lineIndex = 0;
public:
char lines[LINE_COUNT][LINE_SIZE];
int count();
bool addLine(const char*);
void getLine(char*, int);
void printLines();
static Lines split(const char*);
};
// return number of lines added to list
int Lines::count() {
return _lineIndex + 1;
}
// copy 'line' into next empty line
bool Lines::addLine(const char* line) {
if(strlen(line) > LINE_SIZE-1) {
return false;
}
if(count() > LINE_COUNT){
return false;
}
strcpy(lines[_lineIndex], line);
_lineIndex++;
}
// update 'dest' to value of line at 'index'
void Lines::getLine(char* dest, int index) {
strcpy(dest, lines[index]);
}
// print line list to Serial
void Lines::printLines() {
for(int i = 0; i < count(); i++) {
char l[LINE_SIZE];
getLine(l, i);
Serial.println(l);
}
}
// split a target string 'inStr' into lines and return new Lines object
Lines Lines::split(const char* inStr) {
Serial.println("Splitting lines...");
//vars
Lines l;
char lineBuff[LINE_SIZE];
int bufferIndex = 0;
int inLen = strlen(inStr);
// clean line buffer before beginning
memset(lineBuff, 0, LINE_SIZE);
// loop through each char in input, adding lines as we go
for(int i = 0; i <inLen; i++) {
char c = inStr[i];
if(c == '\r') { // don't add to buffer
//skip
} else if(c == '\n') { // add line and reset buffer
Serial.println("adding line: ");
Serial.println(lineBuff);
l.addLine(lineBuff);
memset(lineBuff, 0, LINE_SIZE);
bufferIndex = 0;
} else { // add char to buffer
lineBuff[bufferIndex] = c;
bufferIndex++;
}
}
if(strlen(lineBuff) > 0) {
// add anything left in the buffer as the final line, to handle no newline at end of str ing
l.addLine(lineBuff);
}
return l;
}
#endif
Scheduling
This module provides a number of classes that work together to provide extensible light scheduling functionality for the application.
CPP Header
This file defines two enums, ScheduleType
and LightStatus
, and several classes:
- ScheduleEntry - this class defines a day and night start time for a specific date/time range, and logic to determine the status for a specific time.
- Schedule - this is an abstract class that each schedule implementation inherits from
- FixedSchedule - concrete
Schedule
implementation that provides a single fixed entry no matter the date. - FixedSchedule - concrete
Schedule
implementation that provides a different entry for each month, 12 in total.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#ifndef SCHEDULING_H
#define SCHEDULING_H
#include "Arduino.h"
#include "DateTime.h"
// Represents a lighting schedule type
enum ScheduleType {
FIXED = 1,
MONTHLY = 2,
};
// Represents a day/night status
enum LightStatus { DAY, NIGHT };
//schedule entry class
struct ScheduleEntry {
Time dayStart;
Time nightStart;
ScheduleEntry(Time, Time);
LightStatus getLightStatus(Time);
void toString(char*);
};
//abstract Schedule class
struct Schedule {
virtual int getScheduleType();
virtual ScheduleEntry* getEntry(Date) = 0;
virtual void toString(char*);
static bool validScheduleType(int i);
};
//fixed schedule implementation
struct FixedSchedule : public Schedule {
ScheduleEntry* entry;
FixedSchedule(ScheduleEntry*);
int getScheduleType();
ScheduleEntry* getEntry(Date);
void toString(char*);
};
//monthly schedule implementation
struct MonthlySchedule : public Schedule {
ScheduleEntry* schedules[12];
MonthlySchedule(ScheduleEntry*[12]);
int getScheduleType();
ScheduleEntry* getEntry(Date);
void toString(char*);
};
//TODO: other schedule implementations
#endif
Implementation
ScheduleEntry::ScheduleEntry(Time dayStart, Time nightStart)
- constructor- Create a new schedule entry with the specified
dayStart
andnightStart
times. LightStatus ScheduleEntry::getLightStatus(Time t)
- Calculate and return the
LightStatus
for a specific timet
using thecompare()
method to comparet
withdayStart
andnightStart
- ift < dayStart || t >= nightStart
returnsLightStatus.NIGHT
, ift >= dayStart && t < nightStart
returnsLightStatus.DAY
. void ScheduleEntry::toString(char* dest)
- Set the value of
dest
to a string representation of the current schedule entry in the formD{<dayStart>}N{<nightStart>}
where<dayStart>
and<nightStart>
are strings in the formatHH:MM:SS
. This is used as the schedule entry representation when sending or updating the light settings over the network. bool Schedule::validScheduleType(int i)
- This is a helper method to ensure that
i
represents a validScheduleType
- returns true ifi == ScheduleType.FIXED || i == ScheduleType.MONTHLY
FixedSchedule::FixedSchedule(ScheduleEntry* entry)
- constructor- Create a fixed schedule with the specified
entry
ScheduleEntry* FixedSchedule::getEntry(Date d)
- override ofSchedule::getEntry(Date d)
- Return the
ScheduleEntry
associated with the specified dated
. Fixed schedule always returns the same value since it only has one entry. int FixedSchedule::getScheduleType()
- Returns the
ScheduleType
value associated with this schedule implementation:ScheduleType::FIXED
void FixedSchedule::toString(char* dest)
- Set value of
dest
to the string representation of this schedule. Used to send/update schedules over the network. This implementation is represented by one line, the value ofthis->entry.toString()
MonthlySchedule::MonthlySchedule(ScheduleEntry* sched[12])
- constructor- Create a monthly schedule with the twelve specified entries
sched
, one for each month, assigned to the local variableschedules
.sched[0]
= Jan,sched[1]
= Feb, and so on. ScheduleEntry* MonthlySchedule::getEntry(Date d)
- override ofSchedule::getEntry(Date d)
- Return the
ScheduleEntry
associated with the specified dated
. Monthly schedule returns theentry
associated with the current month ofd
. int MonthlySchedule::getScheduleType()
- Returns the
ScheduleType
value associated with this schedule implementation:ScheduleType::MONTHLY
void MonthlySchedule::toString(char* dest)
- Set value of
dest
to the string representation of this schedule. Used to send/update schedules over the network. This implementation is represented by twelve lines, one for eachScheduleEntry
inschedules
. Each line will have the value ofthis->schedules[i].toString()
wherei
is the month,0
through11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#include "Arduino.h"
#include "DateTime.h"
#include "Scheduling.h"
// Create new ScheduleEntry with specified 'dayStart' and 'nightStart'
ScheduleEntry::ScheduleEntry(Time dayStart, Time nightStart)
: dayStart(dayStart), nightStart(nightStart) { };
// Return the correct LightStatus value for the specified time
LightStatus ScheduleEntry::getLightStatus(Time t) {
Serial.print("Current time: ");
t.printSerial();
Serial.print("Day start time: ");
dayStart.printSerial();
Serial.print("Night start time: ");
nightStart.printSerial();
// if time lt dayStart OR gte nightStart, night
if(t.compare(dayStart) < 0 || t.compare(nightStart) >= 0) {
Serial.println("getLightStatus = NIGHT");
return NIGHT;
}
// if time gte dayStart AND lt nightStart, day
if(t.compare(dayStart) >= 0 && t.compare(nightStart) < 0) {
Serial.println("getLightStatus = DAY");
return DAY;
}
// default to DAY if for some reason neither test matches.
// Also log error, since this should not happen.
Serial.println("ERROR: ScheduleEntry.getLightStatus() did not match either day or night. Defaulting to DAY.");
Serial.print("Current time: ");
t.printSerial();
Serial.print("Day start time: ");
dayStart.printSerial();
Serial.print("Night start time: ");
nightStart.printSerial();
return DAY;
};
// Convert the schedule entry into a serialized string representation for sending in web responses
void ScheduleEntry::toString(char* dest) {
char timeBuffer[9];
//get day start time
memset(timeBuffer, 0, 9);
dayStart.toString(timeBuffer);
//add day start time
strcat(dest, "D{");
strcat(dest, timeBuffer);
//get night start time
memset(timeBuffer, 0, 9);
nightStart.toString(timeBuffer);
//add night start time
strcat(dest, "}N{");
strcat(dest, timeBuffer);
strcat(dest, "}");
}
// Return true if 'i' is a valid schedule type
bool Schedule::validScheduleType(int i) {
if(i >= ScheduleType::FIXED && i <= ScheduleType::MONTHLY) {
return true;
}
return false;
};
// Create new fixed schedule
FixedSchedule::FixedSchedule(ScheduleEntry* entry) {
this->entry = entry;
};
// Get the ScheduleEntry corresponding to date 'd'
ScheduleEntry* FixedSchedule::getEntry(Date d) {
//ignores the date passed and always returns the same entry
return entry;
};
// Get the schedule type
int FixedSchedule::getScheduleType() {
return ScheduleType::FIXED;
}
// Convert the schedule into a serialized string representation for sending in web responses
void FixedSchedule::toString(char* dest) {
entry->toString(dest);
};
//MonthlySchedule implementation
MonthlySchedule::MonthlySchedule(ScheduleEntry* sched[12]) {
for(int i=0; i<12; i++) {
schedules[i] = sched[i];
}
};
// Get the ScheduleEntry corresponding to date 'd'
ScheduleEntry* MonthlySchedule::getEntry(Date d) {
return schedules[d.month-1];
};
// Get the schedule type
int MonthlySchedule::getScheduleType() {
return ScheduleType::MONTHLY;
}
// Convert the schedule into a serialized string representation for sending in web responses
void MonthlySchedule::toString(char* dest) {
ScheduleEntry* entry;
char lineBuffer[24];
for(int i=0; i<12; i++) {
memset(lineBuffer, 0, 24);
entry = schedules[i];
entry->toString(lineBuffer);
strcat(dest, lineBuffer);
strcat(dest, "\r\n");
}
};
Constants
secrets
secrets.h
defines the SSID and password for the wifi network the device should connect to. It should NOT be checked in to source control with any passwords in it.
CPP Header
This file simply defines two constant char arrays:
1
2
const char SECRET_SSID[]="";
const char SECRET_PASS[]="";
globals
globals.h
contains the definitions for all global variables that need to be accessible throughout various parts of the program. This includes the settings for the controller classes, as well as network classes. This class is used to ensure that the globals can be included in any files where they are needed to accessed, and includes compiler directives to ensure that they are only defined a single time.
CPP Header
This file defines several variables for the controller classes. These are defined as globals so that they can be read by the controller classes and updated by the web server route handlers.
humidityControllerSettings
- current settings for theHumidityController
classlightControllerSettings
- current settings for theLightController
classwifiControllersettings
- current settings for theWifiController
classlightControllerSchedule
- current schedule for theLightController
class
It also defines some networking variables. These are defined as globals to allow them to be accessed by both the main event loop and the routes.h
and other networking related classes.
udp
- an instance ofWiFiUDP
used byntp
ntp
- an instance ofNTPClient
, used to send and receive NTP timeserver
- an instance ofWebServer
for processing incoming HTTP requestsrouter
- an instance ofRouter
for routing incoming HTTP requests to the right handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef GLOBALS_H
#define GLOBALS_H
#include "WiFiNINA.h"
#include "HumidityController.h"
#include "LightController.h"
#include "WifiController.h"
#include "NTPClient.h"
#include "WebServer.h"
#include "Router.h"
// global vars
HumidityControllerSettings* humidityControllerSettings;
LightControllerSettings* lightControllerSettings;
WifiControllerSettings* wifiControllersettings;
Schedule* lightControllerSchedule;
WiFiUDP udp;
NTPClient ntp = NTPClient(udp);
WebServer server;
Router router;
#endif
Main Arduino File - climate_control.ino
This file is the entry point for the program that ties together all the other code in the project and is responsible for starting up the application and performing actions during the main program loop.
CPP Header
- Control pins
- First, we define some control pins that will be provided to the controller modules.
- Defaults
- Next we define a bunch of settings defaults that will be used to initialize the settings of the various controller modules.
- Global functions
- Then we declare some global functions that will be implemented later on, so they can be referenced in the
setup()
andloop()
functions void setup()
- The setup function is responsible for initializing all of the various controller modules and networking functionality. First, it waits for a serial connection, timing out after two seconds if no serial connection is available. Next we setup default settings and call the
init()
method for the each of the humidity, light, and wifi controllers. Then we initialize the globalNTPClient
and set the sync provider for the Time library. Then, after printing the current date and time, we finally initialize the globalWebServer
and register the routes void loop()
- The loop function checks for incoming web requests, and responds to them if there are any, and then calls the
Alarm.delay()
function of the TimeAlarms library which calls all of our controllers repeating timer functions, and waits for a certain time before exiting the loop function so it can run again. The delay is used to prevent the loop function from being called too often, increasing CPU load and heat. time_t timeProvider()
- Wrapper function that can be passed to
setSyncProvider()
function of Time library. Calls the ntp.getNTPTime() function and returns it’s return value. void registerRoutes()
- Registers all the routes defined in routes.h with the global
router
object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include <Time.h>
#include <TimeAlarms.h>
#include <SPI.h>
#include <WiFiNINA.h>
#include "secrets.h"
#include "globals.h"
#include "routes.h"
#include "HumidityController.h"
#include "LightController.h"
#include "WifiController.h"
// control pins
const byte PIN_DHT22_ONE = 2;
const byte PIN_DHT22_TWO = 4;
const byte PIN_CTRL_ATOMIZER = 6;
const byte PIN_CTRL_FANS = 8;
const byte PIN_RELAY_DAY = 10;
const byte PIN_RELAY_NIGHT = 12;
// settings defaults
const float HUMIDITY_TARGET_DEFAULT = 90.0; // target 90% humidity
const float HUMIDITY_KICKON_DEFAULT = 75.0; // start when below 75% humidity
const int HUMIDITY_FAN_STOP_DEFAULT = 20; // fans run for 15 seconds after atomizer stops
const int HUMIDITY_UPDATE_DEFAULT = 2; // update humidity every 10 seconds by default
const Time LIGHT_DAY_START_DEFAULT = Time(07,00,00); //day start 7am
const Time LIGHT_NIGHT_START_DEFAULT = Time(19,00,00); //night start 7pm
const int LIGHT_UPDATE_DEFAULT = 1 * SECS_PER_MIN; // update lights every 1 minutes by default
const bool WIFI_REQUIRE_LATEST_FIRMWARE = false;
const int WIFI_CONNECTION_CHECK_INTERVAL = 10 * SECS_PER_MIN;
// global functions
time_t timeProvider();
void registerRoutes();
// Arduino setup functions
void setup()
{
int serialStart = millis();
//init serial
Serial.begin(115200);
while (!Serial && millis() - serialStart < 2000) {
delay(500);
}
// set up humidity controller (sensors, atomizer and fan control)
Serial.println("==========Initializing Humidity Controller==========");
humidityControllerSettings = new HumidityControllerSettings(
HUMIDITY_TARGET_DEFAULT,
HUMIDITY_KICKON_DEFAULT,
HUMIDITY_FAN_STOP_DEFAULT,
HUMIDITY_UPDATE_DEFAULT
);
HumidityController::init(
PIN_DHT22_ONE,
PIN_DHT22_TWO,
PIN_CTRL_ATOMIZER,
PIN_CTRL_FANS,
humidityControllerSettings
);
// set up light controller (timer and relays)
Serial.println("==========Initializing Light Controller==========");
lightControllerSchedule = new FixedSchedule(
new ScheduleEntry(
LIGHT_DAY_START_DEFAULT,
LIGHT_NIGHT_START_DEFAULT
)
);
lightControllerSettings = new LightControllerSettings(
lightControllerSchedule,
LIGHT_UPDATE_DEFAULT
);
LightController::init(
PIN_RELAY_DAY,
PIN_RELAY_NIGHT,
lightControllerSettings
);
// set up wifi connection
Serial.println("==========Initializing WiFi==========");
wifiControllersettings = new WifiControllerSettings(
SECRET_SSID,
SECRET_PASS,
WIFI_REQUIRE_LATEST_FIRMWARE,
WIFI_CONNECTION_CHECK_INTERVAL
);
WifiController::init(wifiControllersettings);
// set up NTP provider
Serial.println("==========Initializing NTP==========");
ntp.initUdp();
setSyncProvider(timeProvider);
setSyncInterval(5 * SECS_PER_MIN); //update every 5 min
// print date and time on startup
Date today = Date(year(), (byte)month(), (byte)day());
Time now = Time((byte)hour(), (byte)minute(), (byte)second());
Serial.println("Date: "); today.printSerial();
Serial.println("Time: "); now.printSerial();
// start web server
Serial.println("==========Initializing Web Server==========");
server = WebServer(); // initialize web server with no port defaults to port 80.
Serial.println("Registering routes...");
registerRoutes();
Serial.println("...done.");
server.listen();
Serial.println("Server Listening...");
};
// Arduino loop function
void loop()
{
//process incoming http requests each loop
WebRequest req;
if(server.processIncomingRequest(req) == 1) {
Serial.println("Responding to request...");
router.handle(req);
}
//check alarms twice per second
Alarm.delay(500);
};
// Time provider functions
time_t timeProvider() {
return ntp.getNTPTime();
};
// Register routes for the web server
void registerRoutes() {
// test route for testing query params
router.get("/test", route_getTest);
//get server time
router.get("/time", route_getTime);
// get humidity status - humidity values and enabled status of fans and atomizers
router.get("/humidity/status", route_getHumidityStatus);
// get humidity settings
router.get("/humidity/settings", route_getHumiditySettings);
// update humidity settings
router.post("/humidity/settings", route_postHumiditySettings);
// get light status (day/night)
router.get("/lights/status", route_getLightStatus);
// get light schedule
router.get("/lights/schedule", route_getLightSchedule);
// update light schedule
router.post("/lights/schedule", route_postLightSchedule);
};