Sunday, November 25, 2018

Internet of things (IoT) temperature logger for the BeagleBone Black (Part 3)



click for Part 1
click for Part 2
click for Part 4

Introduction

In this post we will make our web-client display a graph of the live data being collected  by the BBB. This graph can be updated to use the latest data upon pressing a button.

Continuing on from the previous post we add the following:
  • Add a graph using Chart.js which can have its data updated using the JS fetch() function. For this the JSON data format will be used.
  • Ensure that only one instance of the server can be run in the same directory, so we don't inadvertently have multiple processes duplicating what is written to the database.
  • Have the server find the serial number of the W1 temperature sensor automatically.
  • Put the database logging back into the server, and make it such that the temperature is recorded every half hour with no option to change this interval or to turn the data recording on and off. If the server is running the data logging is on.
I have now tested the front end on  Google Chrome, Firefox and iOS Safari.

The Graph

Chart.js provides an API to make HTML graphs using JS. In our case the date will be required for the lower axis data and Chart.js makes use of Moment.js for this. The necessary  libraries are provided using CDN's in the HTML file. The Chart.js library is able to work with JSON data straightforwardly.

When the HTML is loaded our JS draw() function is called. This function asks our server for the file "data.json" using the fetch() function. Rather than write this file and then send it our server just sends what would be it's contents directly to the socket stream, the actual file "data.json" never exists. Finally our draw() function calls our drawChart() function and passes the JSON data to it. This function draws our graph.

The Chart.js documentation provides a means to alter or update the data in an existing chart. The chart is updated using our updateData() function which is called when the HTML button "Update graph data" is pushed. A picture of the resulting front end is shown directly below:

The front end rendered in Google Chrome
The front end has two buttons, the first of which is just what we had in the last post. The second button ("Update graph data") causes the graph to update when it is pressed. Every half hour another data point is added to the database, so if more data has been added since the graph was last updated it will be added.

The front end

The front end is in the files "graph.html" and "graph.js" which are provided by the server. The JS first waits for the "DOMContentLoaded" event, which occurs when the HTML has finished loading, and then calls the draw() function. The draw() function gets the JSON data from the server and then calls the drawChart() function which draws the graph on the web-page.
The "Update graph data" button on the web-page calls the updateData() function which reloads the JSON data from the server.

The Server

Single Instance

The server writes and locks a pid file to make sure it is not already running in the given directory. Another instance can be run in a different directory, which will have its own database file. This eliminates more than one instance of the server writing duplicate data to the same database file. The pid file is locked using Linux's C function flock(). When the program and all its child processes stop or are killed the lock will cease.

Finding the serial number

The probes driver directory, which depends explicitly upon its individual serial number is found using the Linux's C function scandir(). The directory we are looking for will start with "28-" with the number 28 signifying that the one-wire device is a temperature probe. If there is more than one temperature probe connected there will be additional directories starting with "28-". It has been arranged that the first probe directory found is used. If a temperature probe is not found a temperature of 100,000 degrees is returned.

Database logging

The database logging process is called very early on as a forked off child process. It logs the temperature to the database every half an hour. If the program is killed but this child process is still running the pid file lock will prevent the program from being started again in the same directory. The parent and all child processes must be killed before the program can be restarted.

The Code

https://github.com/wilstep/IoT-temperature-logger-for-the-BeagleBone-Black/releases/tag/1.2

or on the command line obtain with the command:

git clone -b '1.2' https://github.com/wilstep/IoT-temperature-logger-for-the-BeagleBone-Black.git

What's Next

I would like to add some input options to restrict the time range which the graph is plotted over, and allow the graph to be replotted upon adjusting this input. Then I'd like to add an option to download the currently plotted subset of data as a CSV file, but I haven't settled on how this will all work out or how to do it yet.

Wednesday, November 14, 2018

Internet of things (IoT) temperature logger for the BeagleBone Black (Part 2)

click for Part 1
click for Part 3
click for Part 4 

Introduction

In this post we will narrow things down to a simple project, which will form an important step towards our overall goal. We will make a web app that communicates with a stripped down C server on the BBB. This app will be able to request the temperature and display it, nothing more. The client will be coded using HTML and JavaScript, allowing the client side to be completely handled by a web-browser, regardless of which operating system is being used for the client and making it a true IoT device. I have only tested the front end on Firefox and Google Chrome.

HTTP headers

When a web-browser makes a request to a server it sends and expects headers. We need to make our server deal with this. It is easiest if our web-page comes from the same server address as our data does; then we don't need to worry about cross-origin resource sharing in our server's header.

A basic example of an HTTP header is:

HTTP/1.1 200 OK
Date: Mon 12 Nov 2017 05:23:02 GMT
Server: C_BBB

Content-Length: 1332
Content-type: text/html; charset=utf-8


Note that a blank line must follow this header. For us the most important lines in the header are the first one and the Content-Length. The first line tells the client that the server is happy and to expect its request to follow the header. If the server can't find the request then it should send "HTTP/1.1 404 Not Found", which I'm sure we've all experienced. The length gives the number of bytes we are going to send in the message which follows.

We'll use this header to send our web-page to the client (our web-browser). Our web-page is in the file "web_client.html" which is placed in the same directory as our server. When the server is ready to call the write() function to reply to the client the following C function is called which does all the work in sending the header and the html file

void file_serv(int sock, char fn[])
{
    char buffer[512];
    int n, c;
    FILE *ifp;
   
    printf("Hi from file_serv %s\n", fn);
    ifp = fopen(fn, "r");
    fseek(ifp, 0L, SEEK_END);
    // get the size of the file in bytes, less the end of file character
    n = ftell(ifp) - 1;
    rewind(ifp);
    // Simple HTTP headers
    sprintf(buffer, "HTTP/1.1 200 OK\nServer: C_BBB\nContent-Length: %d", n);
    n = strlen(buffer);
    sprintf(buffer + n, "\nContent-Type: text/html; charset=utf-8\n\n");
    n = write(sock, buffer, strlen(buffer)); // send header to client
    if(n < 0) error("ERROR writing to socket");
    while(1){ // loop which sends the html file to the client
        n = 0;
        while((c = getc(ifp)) != EOF){
            buffer[n++] = c;
            if(n == 511) break;
        }
        if(n == 0) break; // end of file reached
        buffer[n] = '\0';
        n = write(sock, buffer, strlen(buffer));
        if (n < 0) error("ERROR writing to socket");
        if(n == 0) break; // The browser ain't buying what we're selling
    }
    fclose(ifp);
    printf("Bye from file_serv\n");
}


In this case we haven't put the date in the header, but it's not an issue because we won't be using this feature. This function is quite versatile, it will load any html file passed as an argument to it provided that it is compatible with the header. We will also use it to load the "favicon.ico" file, which is just a blank icon I downloaded and is used to keep the browser happy.

It is straightforward to make our server print the headers sent to it. When Firefox contacts our server, using the url localhost, it makes the request

GET / HTTP/1.1
Host: localhost:20006
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1


This is followed up with  the "favicon.ico" icon request with the GET line now "GET /favicon.ico HTTP/1.1" in the header.

If we append ?tt onto the URL (e.g. http://localhost:20006/?tt) then the GET line becomes: "GET /?tt HTTP/1.1". So this is what we will use to send a request to our server from the client side.

The client

The client code is small enough to reproduce here

<!DOCTYPE html>
<html>
<body>
  
<h1>Temerature Reader</h1>
<p id="mydata"></p>
<button onclick="displayResult()">Update</button>
  
<script>
    const url=document.location.origin + '?data';
   
    displayResult();
   
    function displayResult(){
        fetch(url).then(function(response){
            if(response.ok){
                response.text().then(function(myText){
                    str = myText;
                    document.getElementById("mydata").innerHTML = myText;
                });
            }else{
                console.log('Network request failed with response ' +
                         response.status + ': ' + response.statusText);
            }
        });    
    }

</script>

</body>
</html>

This code uses the JS fetch() function to contact the server whenever the "Update" button is pushed. A picture of the working client is shown directly below:


When the Update button is pressed the server is contacted, and the text line containing the time and temperature is returned and displayed as shown.

The Code 


The code can be found at https://github.com/wilstep/IOT-temperature-logger-for-the-BeagleBone-Black/tree/1.1 The relevant code is located in the subdirectory "web-trial". Compile on the BBB by issuing the command "make" and then run with the command

wserver x

where x is the port number (20000 is not a bad choice). Once the server is running you can access it from a browser. In my case when running on port 20000 the URL "http://beaglebone.local:20009/" is all that's needed.

What's next

In the next instalment we will continue with the client and the server by adding functionality. We will aim to display a graph of the temperature on the client.

Friday, November 9, 2018

Internet of things (IoT) temperature logger for the BeagleBone Black (Part 1)

click for Part 2
click for Part 3
click for Part 4 

Introduction

In this series I am going to make a network based temperature logger using a one wire temperature sensor (this is a digital sensor and readily available online) and a BeagleBone Black (BBB). At this early stage I plan to have fairly minimal functionality, with a server running on the BBB which periodically measures the temperature, and then access this over the internet using a client on another machine.

The server will be written in C and make use of SQLite for the database. When it is set to run it will log the temperature at a regular interval, simultaneously recording the temperature and the time of measurement. The client will probably be written to run in a web browser using HTML with ReactJS. The client will be able to set some parameters in the server, such as turning on or off the data collection (of the temperature at a regular interval) and changing the interval between measurements. The client will also be able to obtain data from the server, plot a graph of it and download it to a file.

In this entry I will concentrate on getting a fairly complete working version of the server, and use a very crude client written in C to test its functionality.

Setting up the One Wire Sensor

To begin with it is necessary to connect and configure the one wire sensor on the BBB. This is easy to do if you have a new enough version of the Linux kernel, and I strongly recommend updating if you are having trouble. I am using Debian Stretch with kernel 4.14.67, for which everything worked very smoothly. Once you have a new enough system you can follow the simple instructions on this page https://github.com/beagleboard/linux/issues/142 and you should be in good shape.

The probe has three wires, a positive, a negative, and a signal. The signal needs to be configured with a pull up resistor. The probe is nominally 5V but you need to use 3.3V on the BBB or risk frying it. Because of this lower voltage I used a 3.3k resistor instead of the recommended 4.7k. The probe's accuracy is not sensitive to this, so it doesn't matter much.

Each probe has its own serial number and this will effect the name of the directory where the driver file is located. In my case I have

/sys/bus/w1/devices/28-041750bdd9ff

where the 28- is the signature for the temperature probe and the 041750bdd9ff is particular to my individual probe. If you navigate into your version of this directory you may issue the command

cat w1_slave 

In my case this produced

40 01 4b 46 7f ff 0c 10 bc : crc=bc YES
40 01 4b 46 7f ff 0c 10 bc t=20000

where the YES signifies a successful temperature read and the 20000 gives 20000/1000 = 20 degrees Celsius.

The Server

As a starting point for the server I used the simple bare bones version given here http://www.linuxhowtos.org/C_C++/socket.htm This server makes use of the Linux fork() function to spawn off child processes, so that it can handle multiple client requests at once. The functionality I have added requires a major expansion of this code.

As it stands the function of my server depends upon the first character of the stream sent from the client when it initialises communication. When the server is started it does nothing until some instruction is provided from the client. Valid first characters are a, b, c, d & e: providing the following functions
  1.  Starts the server measuring the temperature and logging it to the database with a period that has been determined elsewhere.
  2. Stops the server from measuring, provided it is currently measuring.
  3. Reads the data from the data base
  4. Changes the period for measurement. The initial period is set to 1 minute
  5. Shuts the server program down completely
Upon starting the server it checks to see whether the database file (temperature.db) exists, if it doesn't it writes an empty initial SQLite3 database with a table called tbl1, which has two columns (time, temperature) by issuing the following SQLite command:  

create table tbl1(time varchar(23), temperature real).

Lets start the server on port 20001 by first compiling it on the BBB (you will need to change line 170 of server.c to match the directory belonging to your particular probe serial number) and then typing:

server 20001&

at the command prompt in the directory where the server program has been placed. We can then run the client program on another machine, or on the BBB itself. I ran it from a local Linux machine where the BBB can be accessed by the name beaglebone.local. So the client was run providing the following response:

client beaglebone.local 20001
Please enter the message: a
Temperature monitoring started 


So upon running the client I was prompted for a message, which I entered as simply the single character a.  The client then responds that: "Temperature monitoring started". Next we run the client again:

client beaglebone.local 20001
Please enter the message: d30
delay time set to 30


and this time we entered 'd30' which results in the server's response "delay time set to 30". After some time various entries will build up in the data base. These can be read by the client:

client beaglebone.local 20001
Please enter the message: c
8
2018-11-08T13:38, 18.8
2018-11-08T13:39, 18.8
2018-11-08T14:00, 18.9
2018-11-08T14:30, 19.3
2018-11-08T15:00, 19.5
2018-11-08T15:30, 19.8
2018-11-08T16:00, 20.0
2018-11-08T16:30, 20.1
database output done 


In this case we responded with the character c and the server then responded with the database entries recorded so far. The first line from the server gives us the integer 8 which is the number of measurements returned. When the measurements were started the default value for the period of 1 minute was set, then some 3 plus minutes later this was changed to 30 minutes and the measuring continued. This was done in the afternoon when the temperature of the room I was in was increasing. Note that the server automatically synchronises the measurements to coincide with the turn of the hour. If an integer value which is not a factor of 60 (7 for example) is chosen for the period, then the first measurement of the next hour will be shifted forward to fall on the turn.

The Code


If you wish to obtain the code used in this project you can download if from

https://github.com/wilstep/IOT-temperature-logger-for-the-BeagleBone-Black/tree/1.0

Keep in mind that the server needs to be compiled on the BBB and that the client needs to be compiled where it will be used. This could be on the same BBB or another Linux computer. It might compile on OSX using gcc but I haven't tried it.

What's Next?


In the next instalment I will focus on improving the client to provide a GUI interface which plots a graph. The server will probably be tweaked a bit too; for now I would like to make it detect the temperature probe's serial number automatically, add an enumeration type for the response to the client's input and perhaps reference the time periods to the turn of the day rather than the turn of the hour.

Go to part 2