Remote temperature monitor using ZKit-ARM-1769

Remote temperature monitor is a device to push temperature sensor data from a remote location to a central server. The data is then made available through a web interface. This article shows how to build a simple remote temperature monitor.

This requires a microntroller board which is capable of reading a temperature sensor and sending the data to a central server over an IP network. The ZKit-ARM-1769 microncontroller is suitable for this project, since it has an I2C interface and Ethernet interface. Read on, to find out how to build a remote temperature monitor using the ZKit-ARM-1769.

Overview

The application running in the ZKit-ARM-1769 reads the temperature from the temperature sensor and pushes the data to a webserver, by invoking a callback URL. The central server stores the temperature data in a database. The data can be later queried and viewed from the web server.

Remote Temperature Block Diagram
Figure 1. Block Diagram

Hardware Components

  • ZKit-ARM-1769

  • Temperature Sensor Board

Hardware Setup

  • Connect the Temperature Sensor Board to ZKit-ARM-1769 board, through the I2C header.

  • Connect the ZKit-ARM-1769 board to the network, using an Ethernet cable.

Software Frameworks

NuttX

NuttX is a Unix like realtime operating system. NuttX has been ported to ZKit-ARM-1769. We will use the I2C and Socket API in NuttX to send the temperature to the server.

Flask

Flask is a web framework for Python based on Werkzeug, Jinja 2. Flask will be used to implement the central server to collect and display the data.

Software

We have following software components in this project

  • Reading the temperature.

  • Sending the temperature to the server.

  • Storing the temperature to database.

  • Displaying the temperature from database.

NuttX Application
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <nuttx/config.h>
#include <nuttx/i2c.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

/* I2C Device Address */
#define TEMP_SENSOR 0x48

/* Register Addresses */
#define LTHB    0x00

#define I2C_PORT 0
#define FREQUENCY 100000
#define ADDRESS_BITS 7
#define NET_DEVNAME "eth0"
#define PORT_NO 5000
#define IPADDR "172.16.0.10"
#define NETMASK "255.255.0.0"
#define GATEWAY_IP "172.16.0.2"
#define SERVER_IP "172.16.0.133"

static char buf[64];

struct i2c_dev_s *i2c_init(int port, uint32_t frequency, int addr, int nbits)
{
        uint32_t result;
        struct i2c_dev_s *i2c;

        i2c = up_i2cinitialize(I2C_PORT);
        if (i2c == NULL) {
                printf("nuttx-i2c: Error initializing I2C: %d\n", i2c);
                exit(0);
        }
        printf("nuttx-i2c: I2C Successfully initialized\n");

        I2C_SETFREQUENCY(i2c, frequency);
        result = I2C_SETADDRESS(i2c, addr, nbits);
        if (result != OK) {
                printf("nuttx-i2c: Wrong I2C Address.\n");
                exit(0);
        }
        return i2c;
}

int temperature_read(struct i2c_dev_s *i2c)
{
        int result;
        uint8_t lthb_reg = LTHB;
        uint8_t temp;

        result = I2C_WRITE(i2c, &lthb_reg, 1);
        if (result < 0) {
                printf("nuttx-i2c: Error Writing. Terminating\n");
                return 1;
        }

        result = I2C_READ(i2c, &temp, 1);
        if (result < 0) {
                printf("nuttx-i2c: Error Reading. Terminating\n");
                return 1;
        }

        return temp;
}

int network_setup(char *ipaddr, char *gatewayip, char *netmask)
{
        struct in_addr hostaddr;
        uint8_t mac[6];
        int ret;

        printf("Assigning MAC\n");
        mac[0] = 0x00;
        mac[1] = 0xe0;
        mac[2] = 0xde;
        mac[3] = 0xad;
        mac[4] = 0xbe;
        mac[5] = 0xef;
        ret = uip_setmacaddr(NET_DEVNAME, mac);
        if (ret < 0) {
                printf("Error setting MAC address\n");
                return -1;
        }

        /* Set up our host address */
        printf("Setup network addresses\n");
        ret = inet_pton(AF_INET, ipaddr, &hostaddr.s_addr);
        if (ret == 0) {
                printf("Invalid IPv4 dotted-decimal string\n");
                return -1;
        } else if (ret < 0) {
                printf("inet_pton: af argument unknown\n");
                return -1;
        }

        ret = uip_sethostaddr(NET_DEVNAME, &hostaddr);
        if (ret < 0) {
                printf("Error setting IP address\n");
                return -1;
        }

        /* Set up the default router address */
        ret = inet_pton(AF_INET, gatewayip, &hostaddr.s_addr);
        if (ret == 0) {
                printf("Invalid GATEWAY_IP dotted-decimal string\n");
                return -1;
        } else if (ret < 0) {
                printf("inet_pton: af argument unknown\n");
                return -1;
        }

        ret = uip_setdraddr(NET_DEVNAME, &hostaddr);
        if (ret < 0) {
                printf("Error setting GATEWAY IP address\n");
                return -1;
        }

        /* Setup the subnet mask */
        ret = inet_pton(AF_INET, netmask, &hostaddr.s_addr);
        if (ret == 0) {
                printf("Invalid NETMASK dotted-decimal string\n");
                return -1;
        } else if (ret < 0) {
                printf("inet_pton: af argument unknown\n");
                return -1;
        }

        ret = uip_setnetmask(NET_DEVNAME, &hostaddr);
        if (ret < 0) {
                printf("Error setting NETMASK\n");
                return -1;
        }

        return 0;
}

int app_main(void)
{
        int fd;
        int ret;
        int temperature;
        int len;
        struct i2c_dev_s *i2c;
        struct sockaddr_in addr;

        /* Initialize I2C interface */
        i2c = i2c_init(I2C_PORT, FREQUENCY, TEMP_SENSOR, ADDRESS_BITS);

        /* Setting up the network */
        ret = network_setup(IPADDR, GATEWAY_IP, NETMASK);
        if (ret < 0) {
                printf("nuttx-ethernet: Terminating!\n");
                return 1;
        }

        /* Creating the Socket */
        fd = socket(PF_INET, SOCK_STREAM, 0);
        if (fd < 0) {
                printf("nuttx-ethernet: Error creating socket:%d\n", errno);
                return 1;
        }

        /* Connect the socket to the server */
        addr.sin_family      = AF_INET;
        addr.sin_port        = htons(PORT_NO);
        inet_pton(AF_INET, (const char *)SERVER_IP, &addr.sin_addr.s_addr);
        printf("nuttx-ethernet: client: Connecting...\n");
        ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
        if (ret < 0) {
                printf("nuttx-ethernet: Error connecting socket:%d\n", errno);
                return 1;
        }

        ret = fcntl(fd, F_SETFL, O_NONBLOCK);
        if (ret < 0) {
                printf("nuttx-ethernet: Error setting non-block: %d\n", errno);
                return 1;
        }

        /* Write the data sent. */
        while (1) {
                temperature = temperature_read(i2c);
                snprintf(buf, sizeof(buf),
                         "GET /update?temperature=%d HTTP/1.1\r\n\r\n", temperature);
                len = strlen(buf);
                ret = send(fd, buf, len, 0);
                if (ret == -1) {
                        printf("nuttx-ethernet: error writing socket:%d\n",
                               errno);
                        goto error_out;
                } else if (ret == 0) {
                        break;
                }

                while (1) {
                        ret = read(fd, buf, sizeof(buf));
                        if (ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                                break;
                        } else if (ret == -1) {
                                printf("nuttx-ethernet: error reading socket: %d\n",
                                       errno);
                                return 1;
                        }
                }

                sleep(2);
        }

error_out:
        /* Close the connection. */
        printf("nuttx-ethernet: Closing the connection\n");
        close(fd);
        return 0;
}

Reading the Temperature

i2c_init() initializes the I2C interface and temperature_read() reads temperature from the Temperature Sensor Board. This is discussed in detail in the article Temperature Sensor Board With NuttX.

Sending the temperature to the Flask Server

To send the temperature to the server we need to setup the network parameters. This is done in network_setup().

  • For the purpose of this demo project, a bogus MAC address is configured, using uip_setmacaddr(). A real product will need a unique MAC address allocated by IANA (Internet Assigned Numbers Authoriy).

  • A free IP address in the network must be obtained from the local network administrator. The IP address is converted and assigned using inet_pton() and uip_sethostaddr().

  • The gateway is not if the central server and the ZKit-ARM-1769 are on the same network. But if this is not the case, the gateway IP should be obtained from the local network administrator. The IP address is converted and assigned using inet_pton() and uip_setdraddr().

  • The subnet mask of the current network is obtained from the local network administrator. The mask is converted and assigned using inet_pton() and uip_setnetmask().

In app_main(), the temperature data is sent to the server. The procedure is described below.

  • Setup the network interface.

  • Open a socket to the central web server, with the server’s IP address and port no.

  • In an infinite loop,

    • Read the temperature from the temperature sensor

    • Fire a HTTP GET request with the temperature data to the server. An example HTTP request:GET /update?temperature=40 HTTP/1.1

    • If any error break the loop

  • Close the connection

Flask Application remote_temp.py
"""Flask application to receive, store and display temperature
updates."""

import sqlite3
from flask import Flask
from flask import request
from flask import g
from flask import render_template
from contextlib import closing
from time import mktime
from time import localtime
from time import strftime

DATABASE = '/tmp/test.db'
DEBUG = True
updatequery = "UPDATE temperature set temperature = ?, Time = ? WHERE Id = ?"

app = Flask(__name__)
app.config.from_object(__name__)


def init_db():
    """Initialise the database."""
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql', mode='r') as f:
            db.cursor().executescript(f.read())
            db.execute("INSERT INTO temperature VALUES(1,0,0)")
        db.commit()


def connect_db():
    """Establsih database connection"""
    return sqlite3.connect(app.config['DATABASE'])


@app.before_request
def before_request():
    """Establish database connection before http request."""
    g.db = connect_db()


@app.teardown_request
def teardown_request(exception):
    """Close the database connection."""
    db = getattr(g, 'db', None)
    if db is not None:
        db.close()


@app.route('/update', methods=['GET'])
def update():
    """Update the database with temperature and time."""
    temperature = request.args.get('temperature')
    g.db.execute(updatequery, [temperature, mktime(localtime()), 1])
    g.db.commit()
    return 'ok'


@app.route('/display')
def display():
    """Render a html file with temperature and time stamp."""
    cur = g.db.execute("select * from temperature")
    row = cur.fetchall()
    _, temp, time = row[0]
    dayname = strftime("%a", localtime(time))
    day = strftime("%d", localtime(time))
    year = strftime("%Y", localtime(time))
    month = strftime("%b", localtime(time))
    hrs = strftime("%H", localtime(time))
    mins = strftime("%M", localtime(time))
    sec = strftime("%S", localtime(time))
    entries = dict(temperature=temp,
                    dayname=dayname,
                    year=year,
                    month=month,
                    day=day,
                    hrs=hrs,
                    mins=mins,
                    sec=sec)
    return render_template("show_entries.html", entries=entries)


if __name__ == '__main__':
    init_db()
    app.run(host="0.0.0.0")

Storing the Temperature Data

In init_db(), the database is created with table name temperature and columns id, temperature and time. The SQL script is stored in a separate file schema.sql.

Database Creation Script schema.sql
drop table if exists temperature;
create table  temperature(
  id integer primary key autoincrement,
  temperature  integer not null,
  Time integer not null
);

The host argument in app.run(host="0.0.0.0"), configures the Flask application to listen on all network interfaces, so that server can be accessed from any system on the network.

The Flask application server listens for incoming HTTP requests. When the server receives a GET /update request, the update() funtion is invoked. In update(), the temperature data is extracted from the GET request and updated in the database.

The mapping from the GET request to the update() function is done using route decorator.

Displaying the Temperature Data

When the server receives a GET /display request, the display() function is invoked. In display(), the data is retreived from the database and rendered as a HTML page and returned back to the client.

The template files used for rendering the HTML page

Template show_entries.html
{% extends "layout.html"%}
{% block body %}
<ul class=entries>
       <li><h2>The temperature is recorded {{ entries.temperature }}<sup>o</sup> C at
              {{entries.hrs}}:{{entries.mins}}:{{entries.sec}} on {{entries.dayname}},{{entries.month}}
                    {{entries.day}} {{entries.year}}</h2>

</ul>
{% endblock %}
Template layout.html
<!doctype html>
<title></title>
<head>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
 <META HTTP-EQUIV="refresh" CONTENT="15"></head>
  <h1>Remote Temperature Monitoring Using ZKit-ARM-1769</h1>
  {% block body %}{% endblock %}
</div>

The templates are located as shown in the following directory structure.

/remote_temp.py
/templates
    /layout.html
    /show_entries.html

For more details, on templates, see Rendering HTML files using Flask

Running the Flask Application

Flask has a built-in webserver for development purpose. The webserver has several limitations. One limitation that concerns us is that, HTTP keep-alive is not supported. Once a request has been served the server closes the connection. But in our little project we would like to open a HTTP connection for the temperature update and use the same connection for further updates.

So instead we use Twisted, a production quality HTTP server that supports WSGI. Twisted have a command line utility called twistd.

Run the flask application using Twisted, using the following command.

twistd -n web --port 5000 --wsgi remote_temp.app

Credits

Thanks to Visual Pharm for the Thermometer Icon.