Building a simple client/server IOT application
Stefanos Kouroupis
Posted on July 30, 2019
A while ago I run into an MXChip. The MXChip is similar to an arduino, but with a lots of sensors pre build on it and designed specifically for the cloud.
Its components include Wifi, OLED display, headphone, microphone, and it also has the following sensors, temperature, humidity, motion and pressure.
So I got one and took it for a test drive. The first application I build was a simple http client/server.
Basically the MXChip will acts as a client sending sensor readings in a regular interval to the the server (API written in NodeJS).
To keep things simple I'll be using SQLite. Everything will be stored in a single table with the following schema.
create table TimeSeries
(
id integer
constraint TimeSeries_pk
primary key autoincrement,
temperature numeric,
humidity numeric,
date text,
location text,
timestamp numeric
);
The NodeJS API is nothing more than a single endpoint supporting OPTIONS, POST and GET
- POST: adds a record to the db
- GET: retrieves records between two timestamps
On tutorials I tend use as less dependencies as possible. This one only depends on sqlite3.
import * as http from 'http';
import * as sqlite3 from 'sqlite3';
import * as url from 'url';
const URLSearchParams = url.URLSearchParams;
const hostname = '0.0.0.0';
const sqlite = sqlite3.verbose();
const port = 3000;
var db = new sqlite.Database('./timedb.sqlite');
http.createServer((req: any, res: any) => {
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
if (req.method === 'POST') {
let body = '';
req.on('data', (chunk: any) => {
body += chunk.toString();
});
req.on('end', () => {
try {
db.serialize(() => {
const data = JSON.parse(body);
db.run("INSERT INTO TimeSeries (temperature, humidity, date, location, timestamp) VALUES (?, ? ,?, ?, ?)", [
data.temperature,
data.humidity,
new Date().toUTCString(),
data.location,
Date.now() / 1000 | 0
]);
});
} catch (error) {
console.log(error);
}
console.log(new Date().toUTCString());
console.log(body);
res.end('ok');
});
} else if (req.method === 'GET') {
const search_params = new URLSearchParams(req.url.split('?')[1]);
let from: any = search_params.get('from');
let to: any = search_params.get('to');
const now: Date = new Date as unknown as Date;
if (to === null) {
to = (now as unknown as number) / 1000 | 0;
}
if (from === null) {
from = now.setHours(now.getHours() - 24) / 1000 | 0;
}
try {
db.serialize(() => {
db.all("SELECT * FROM TimeSeries WHERE timestamp > ? AND timestamp < ?", [from, to], (err, rows) => {
res.end(JSON.stringify(rows));
});
});
} catch (error) {
console.log(error);
}
}
}).listen(port, hostname, () => {
console.log("server start at port 3000");
});
When it comes to arduino you could write an application in any language you prefer as far as it can be compiled for the platform. My language of choice is C++.
For those that are not familiar with arduino development a basic file structure has the following functions
- setup : everything that has with initialization and setting initial values, goes here
- loop : a function that runs consecutively and continuously and allow the program to adapt and respond.
Our include
and global variables
are:
#include "AZ3166WiFi.h"
#include "Arduino.h"
#include "http_client.h"
#include "Sensor.h"
#include "SystemTickCounter.h"
#include "RGB_LED.h"
static char buffInfo[128]; // buffer for the screen
static RGB_LED rgbLed; // our led
static volatile uint64_t msReadEnvData = 0; // stores current tick of executed loop
#define READ_ENV_INTERVAL 120000 // how often loop will run properly
static HTS221Sensor *ht_sensor; // sensors
static DevI2C *ext_i2c; // SPI
static bool hasWifi = false; // wifi on/off
static bool begin = false; // avoid race condition
Our setup()
will initialize the following
- serial
- screen
- temperature and humidity sensors
- wifi
void setup()
{
Serial.begin(115200);
Screen.init();
initSensors();
initWiFi();
}
Initializing the sensors we need to communicate with them through the Serial Peripheral Interface(DevI2C)
void initSensors()
{
ext_i2c = new DevI2C(D14, D15);
if (ext_i2c == NULL)
{
Screen.print(0, "Error \r\n ext_i2c");
}
// temperature and humidity
ht_sensor = new HTS221Sensor(*ext_i2c);
if (ht_sensor == NULL)
{
Screen.print(0, "Error \r\n ht_sensor");
}
ht_sensor->init(NULL);
ht_sensor->reset();
}
And then we need to connect to wifi. Setting up the wifi, is really easy on the MXChip as it stores persistently the SSID and password on first setup of the device. So the code we need is minimal.
void initWiFi()
{
if (WiFi.begin() == WL_CONNECTED)
{
IPAddress ip = WiFi.localIP();
Screen.print(1, ip.get_address());
hasWifi = true;
}
else
{
Screen.print(1, "No Wi-Fi");
}
}
Then we move to our main function the loop()
void loop()
{
if (hasWifi)
{
// get current tick
uint64_t ms = SystemTickCounterRead() - msReadEnvData;
if (!begin)
{
if (ms < READ_ENV_INTERVAL)
{
return;
}
}
begin = true;
// get readings
float temperature = readTemperature();
float humidity = readHumidity();
// display the values, because its cool
displayLines("Leicester", "Temp:" + String(temperature), "Hum: " + String(humidity));
// update the tick to track loop full execution
msReadEnvData = SystemTickCounterRead();
// switch on rgb led while posting data (visual feedback)
rgbLed.setColor(185, 24, 23);
// POST sensor data
sendData(temperature, humidity);
// turn off rgb led
rgbLed.turnOff();
}
}
Reading the temperature
float readTemperature()
{
ht_sensor->reset();
float temperature = 0;
ht_sensor->getTemperature(&temperature);
return temperature;
}
Reading the humidity
float readHumidity()
{
ht_sensor->reset();
float humidity = 0;
ht_sensor->getHumidity(&humidity);
return humidity;
}
A handy helper function to print to the all screen lines at once (MXChip has 3)
void displayLines(String line1, String line2, String line3)
{
char screenBuff[128];
line1.toCharArray(screenBuff, 128);
Screen.print(0, screenBuff);
line2.toCharArray(screenBuff, 128);
Screen.print(1, screenBuff);
line3.toCharArray(screenBuff, 128);
Screen.print(2, screenBuff);
}
And finally we need to POST our data to the API (as JSON)
void sendData(float temp, float humidity)
{
httpRequest(HTTP_POST, "http://192.168.1.128:3000/", "{\"location\":\"Earth\",\"humidity\":\"" + String(humidity) + "\",\"temperature\":\"" + String(temp) + "\"}");
}
Http_Request/Http_Response function
const Http_Response *httpRequest(http_method method, String url, String body)
{
Screen.print(3, "Sending Data");
char urlBuf[48];
url.toCharArray(urlBuf, 48);
HTTPClient *httpClient = new HTTPClient(method, urlBuf);
httpClient->set_header("Content-Type", "application/json"); // required for posting data in the body
char bodyBuf[256];
body.toCharArray(bodyBuf, 256);
const Http_Response *result = httpClient->send(bodyBuf, strlen(bodyBuf));
if (result == NULL)
{
Screen.print(1, "Failed");
char errorBuf[10];
String(httpClient->get_error()).toCharArray(errorBuf, 10);
Screen.print(1, errorBuf);
return result;
}
Screen.print(3, "Success");
String(result->body).toCharArray(buffInfo, 128);
Screen.print(3, buffInfo);
Serial.print(result->status_code);
Serial.print(result->status_message);
delete httpClient;
return result;
}
Posted on July 30, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.