Building a simple client/server IOT application

Stefanos Kouroupis - Jul 30 '19 - - Dev Community

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
);

Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Our setup() will initialize the following

  • serial
  • screen
  • temperature and humidity sensors
  • wifi
void setup()
{
    Serial.begin(115200);
    Screen.init();
    initSensors();
    initWiFi();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Reading the temperature

float readTemperature()
{
    ht_sensor->reset();

    float temperature = 0;
    ht_sensor->getTemperature(&temperature);

    return temperature;
}
Enter fullscreen mode Exit fullscreen mode

Reading the humidity

float readHumidity()
{
    ht_sensor->reset();

    float humidity = 0;
    ht_sensor->getHumidity(&humidity);

    return humidity;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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) + "\"}");
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . .