aboutsummaryrefslogtreecommitdiff
path: root/_posts/2017-08-11-simple-iot-application.md
diff options
context:
space:
mode:
authorMitja Felicijan <mitja.felicijan@gmail.com>2018-08-06 06:50:23 +0200
committerMitja Felicijan <mitja.felicijan@gmail.com>2018-08-06 06:50:23 +0200
commit48aafcc7ac34302a98649b19b67e9dd88f731f3c (patch)
treedb7733cf6fdfaa41bd765270e3437ae2a82bbe95 /_posts/2017-08-11-simple-iot-application.md
parent7c874cd09f4d0d6865f703fb9c5ced557159b26a (diff)
downloadmitjafelicijan.com-48aafcc7ac34302a98649b19b67e9dd88f731f3c.tar.gz
update
Diffstat (limited to '_posts/2017-08-11-simple-iot-application.md')
-rw-r--r--_posts/2017-08-11-simple-iot-application.md498
1 files changed, 498 insertions, 0 deletions
diff --git a/_posts/2017-08-11-simple-iot-application.md b/_posts/2017-08-11-simple-iot-application.md
new file mode 100644
index 0000000..699b146
--- /dev/null
+++ b/_posts/2017-08-11-simple-iot-application.md
@@ -0,0 +1,498 @@
1---
2layout: post
3title: "Simple IOT application supported by real-time monitoring and data history"
4description: "Develop simple IOT application with Arduino MKR1000 and Python"
5---
6
7I have been developing these kind of application for the better part of my last 5 years and people keep asking me how to approach developing such application and I will give a try explaining it here.
8
9IOT applications are really no different than any other kind of applications. We have data that needs to be collected and visualized in some form of tables or charts. The main difference here is that most of the times these data is collected by some kind of device foreign to developer that mainly operates in web domain. But fear not, it's not that different than writing some JavaScript.
10
11There are many devices able to transmit data via wireless or wired network by default but for the sake of example we will be using commonly known Arduino with wireless module already on the board → [Arduino MKR1000](https://store.arduino.cc/arduino-mkr1000).
12
13In order to make this little project as accessible to others as possible I will try to make it as inexpensive as possible. And by this I mean that I will avoid using hosted virtual servers and will be using my own laptop as a server. But you must buy Arduino MKR1000 to follow steps below. But if you would want to deploy this software I would suggest using [DigitalOcean](https://www.digitalocean.com) → smallest VPS is only $5 per month making this one of the most affordable option out there. Please notice that this software will not run on stock web hosting that only supports LAMP (Linux, Apache, MySQL, and PHP).
14
15_But before we begin please take notice that this is strictly experimental code and not well optimized and there are much better ways in handling some aspects of the application but that requires much deeper knowledge of technology that is not needed for an example like this._
16
17**Development steps**
18
191. Simple Python API that will receive and store incoming data.
202. Prototype C++ code that will read "sensor data" and transmit it to API.
213. Data visualization with charts → extends Python web application.
22
23Step 1. and 3. will share the same web application. One route will be dedicated to API and another to serving HTML with chart.
24
25Schema below represents what we will try to achieve and how different parts correlates to each other.
26
27![Overview](/files/simple-iot-application-overview.svg)
28
29
30
31## Simple Python API
32
33I have always been a fan of simplicity so we will be using [Bottle: Python Web Framework](https://bottlepy.org/docs/dev/). It is a single file web framework that seriously simplifies working with routes, templating and has built-in web server that satisfies our need in this case.
34
35First we need to install bottle package. This can be done by downloading ```bottle.py``` and placing it in the root of your application or by using pip software ```pip install bottle --user```.
36
37If you are using Linux or MacOS then Python is already installed. If you will try to test this on Windows please install [Python for Windows](https://www.python.org/downloads/windows/). There may be some problems with path when you will try to launch ```python webapp.py``` so please take care of this before you continue.
38
39
40
41### Basic web application
42
43Most basic bottle application is quite simple. Paste code below in ```webapp.py``` file and save.
44
45```python
46# -*- coding: utf-8 -*-
47
48import bottle
49
50# initializing bottle app
51app = bottle.Bottle()
52
53# triggered when / is accessed from browser
54# only accepts GET → no POST allowed
55@app.route("/", method=["GET"])
56def route_default():
57 return "howdy from python"
58
59# starting server on http://0.0.0.0:5000
60if __name__ == "__main__":
61 bottle.run(
62 app = app,
63 host = "0.0.0.0",
64 port = 5000,
65 debug = True,
66 reloader = True,
67 catchall = True,
68 )
69```
70
71To run this simple application you should open command prompt or terminal on your machine and go to the folder containing your file and type ```python webapp.py```. If everything goes ok then open your web browser and point it to ```http://0.0.0.0:5000```.
72
73If you would like change the port of your application (like port 80) and not use root to run your app this will present a problem. The TCP/IP port numbers below 1024 are privileged ports → this is a security feature. So in order of simplicity and security use a port number above 1024 like I have used port 5000.
74
75If this fails at any time please fix it before you continue, because nothing below will work otherwise.
76
77> We use 0.0.0.0 as default host so that this app is available over your local network. If you find your local ip (```ifconfig```) and try accessing this site with your phone (if on same network/router as your machine) this should work as well (example of such ip ```http://192.168.1.15:5000```). This is a must have because Arduino will be accessing this application to send it's data.
78
79
80
81### Web application security
82
83There is a lot to be said about security and is a topic of many books. Of course all this can not be written here but to just establish some basic security → you should always use SSL with your application. Some fantastic free certificates are available by [Let's Encrypt - Free SSL/TLS Certificates](https://letsencrypt.org). With SSL certificate installed you should then make use of HTTP headers and send your "API key" via a header. If your key is send via header then this key is encrypted by SSL and send encrypted over the network. Never send your api keys by GET parameter like ```http://example.com/?api_key=somekeyvalue```. The problem that this kind of sending presents is that this key is visible in logs and by network sniffers.
84
85There is a fantastic article describing some aspects about security: [11 Web Application Security Best Practices
86](https://www.keycdn.com/blog/web-application-security-best-practices/). Please check it out.
87
88
89
90### Simple API for writing data-points
91
92We will now be using boilerplate code from example above and extend it to be able to write data received by API to local storage. For example use I will use SQLite3 because it plays well with Python and can store quite large amount of data. I have been using it to collect gigabytes of data in a single database without any corruption or problems → your experience may vary.
93
94To avoid learning SQLite I will be using [Dataset: databases for lazy people](https://dataset.readthedocs.io/en/latest/index.html). This package abstracts SQL and simplifies writing and reading data from database. You should install this package with pip software ```pip install dataset --user```.
95
96Because API will use POST method I will be testing if code works correctly by using [Restlet Client for Google Chrome](https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm). This software also allows you to set headers → for basic security with API_KEY.
97
98To quickly generate passwords or API keys I usually use this nifty website [RandomKeygen](https://randomkeygen.com/).
99
100Copy and paste code below over your previous code in file ```webapp.py```.
101
102```python
103# -*- coding: utf-8 -*-
104
105import time
106import bottle
107import random
108import dataset
109
110# initializing bottle app
111app = bottle.Bottle()
112
113# connects to sqlite database
114# check_same_thread=False allows using it in multi-threaded mode
115app.config["dsn"] = dataset.connect("sqlite:///data.db?check_same_thread=False")
116
117# api key that will be used in Arduino code
118app.config["api_key"] = "JtF2aUE5SGHfVJBCG5SH"
119
120# triggered when /api is accessed from browser
121# only accepts POST → no GET allowed
122@app.route("/api", method=["POST"])
123def route_default():
124 status = 400
125 ts = int(time.time()) # current timestamp
126 value = bottle.request.body.read() # data from device
127 api_key = bottle.request.get_header("Api_Key") # api key from header
128
129 # outputs to console received data for debug reason
130 print ">>> {} :: {}".format(value, api_key)
131
132 # if api_key is correct and value is present
133 # then writes attribute to point table
134 if api_key == app.config["api_key"] and value:
135 app.config["dsn"]["point"].insert(dict(ts=ts, value=value))
136 status = 200
137
138 # we only need to return status
139 return bottle.HTTPResponse(status=status, body="")
140
141# starting server on http://0.0.0.0:5000
142if __name__ == "__main__":
143 bottle.run(
144 app = app,
145 host = "0.0.0.0",
146 port = 5000,
147 debug = True,
148 reloader = True,
149 catchall = True,
150 )
151```
152
153To run this simply go to folder containing python file and run ```python webapp.py``` from terminal. If everything goes ok you should have simple API available via POST method on /api route.
154
155After testing the service with Restlet Client you should be able to view your data in a database file ```data.db```.
156
157![REST settings example](/files/iot-rest-example.png)
158
159You can also check the contents of new database file by using desktop client for SQLite → [DB Browser for SQLite](http://sqlitebrowser.org/).
160
161![SQLite database example](/files/iot-sqlite-db.png)
162
163Table structure is as simple as it can be. We have ts (timestamp) and value (value from Arduino). As you can see timestamp is generated on API side. If you would happen to have atomic clock on Arduino it would be then better to generate and send timestamp with the value. This would be particularity useful if we would be collecting sensor data at a higher frequency and then sending this data in bulk to API.
164
165> If you will deploy this app with uWSGI and multi-threaded, use DSN (Data Source Name) url with ```?check_same_thread=False```.
166
167Ok, now that we have some sort of a working API with some basic security so unwanted people can not post data to your database can we proceed further and try to program Arduino to send data to API.
168
169
170
171## Sending data to API with Arduino MKR1000
172
173First of all you should have MKR1000 module and microUSB cable to proceed. If you have ever done any work with Arduino you should know that you also need [Arduino IDE](https://www.arduino.cc/en/Main/Software). On provided link you should be able to download and install IDE. Once that task is completed and you have successfully run blink example you should proceed to the next step.
174
175In order to use wireless capabilities of MKR1000 you need to first install [WiFi101 library](https://www.arduino.cc/en/Reference/WiFi101) in Arduino IDE. Please check before you install, you may already have it installed.
176
177Code below is a working example that sends data to API. Before you try to test your code make sure you have run Python web application. Then change settings for wifi, api endpoint and api_key. If by some reason code bellow doesn't work for you please leave a comment and I'll try to help.
178
179Once you have opened IDE and copied this code try to compile and upload it. Then open "Serial monitor" to see if any output is presented by Arduino.
180
181```c
182#include <WiFi101.h>
183
184// wifi settings
185char ssid[] = "ssid-name";
186char pass[] = "ssid-password";
187
188// api server enpoint
189char server[] = "192.168.6.22";
190int port = 5000;
191
192// api key that must be the same as the one in Python code
193String api_key = "JtF2aUE5SGHfVJBCG5SH";
194
195// frequency data is sent in ms - every 5 seconds
196int timeout = 1000 * 5;
197
198int status = WL_IDLE_STATUS;
199
200void setup() {
201
202 // initialize serial and wait for port to open:
203 Serial.begin(9600);
204 delay(1000);
205
206 // check for the presence of the shield
207 if (WiFi.status() == WL_NO_SHIELD) {
208 Serial.println("WiFi shield not present");
209 while (true);
210 }
211
212 // attempt to connect to wifi network
213 while (status != WL_CONNECTED) {
214 Serial.print("Attempting to connect to SSID: ");
215 Serial.println(ssid);
216 status = WiFi.begin(ssid, pass);
217 // wait 10 seconds for connection
218 delay(10000);
219 }
220
221 // output wifi status to serial monitor
222 Serial.print("SSID: ");
223 Serial.println(WiFi.SSID());
224
225 IPAddress ip = WiFi.localIP();
226 Serial.print("IP Address: ");
227 Serial.println(ip);
228
229 long rssi = WiFi.RSSI();
230 Serial.print("signal strength (RSSI):");
231 Serial.print(rssi);
232 Serial.println(" dBm");
233}
234
235void loop() {
236
237 WiFiClient client;
238
239 if (client.connect(server, port)) {
240
241 // I use random number generator for this example
242 // but you can use analog or digital inputs from arduino
243 String content = String(random(1000));
244
245 client.println("POST /api HTTP/1.1");
246 client.println("Connection: close");
247 client.println("Api-Key: " + api_key);
248 client.println("Content-Length: " + String(content.length()));
249 client.println();
250 client.println(content);
251
252 delay(100);
253 client.stop();
254 Serial.println("Data sent successfully ...");
255
256 } else {
257 Serial.println("Problem sending data ...");
258 }
259
260 // waits for x seconds and continue looping
261 delay(timeout);
262
263}
264```
265
266As seen from example you can notice that Arduino is generating random integer between [ 0 .. 1000 ]. You can easily replace this with a temperature sensor or any other kind of sensor.
267
268Now that we have API under the hood and Arduino is sending demo data we can now focus on data visualization.
269
270
271## Data visualization
272
273Before we continue we should examine our project folder structure. Currently we only have two files in our project:
274
275_simple-iot-app/_
276
277* _webapp.py_
278* _data.db_
279
280We will now add HTML template that will contain CSS and JavaScript code inline for the simplicity reason. And for the bottle framework to be able to scan root application folder for templates we will add ```bottle.TEMPLATE_PATH.insert(0, "./")``` in ```webapp.py```. By default bottle framework uses ```views/``` subfolder to store templates. This is not the ideal situation and if you will use bottle to develop web applications you should use native behavior and store templates in it's predefined folder. But for the sake of example we will over-ride this. Be careful to fully replace your code with new code that is provided below. Avoid partially replacing code in file :) Also new code for reading data-points is provided in Python example below.
281
282First we add new route to our web application. It should be trigger when browser hits root of application ```http://0.0.0.0:5000/```. This route will do nothing more than render ```frontend.html``` template. This is done by ```return bottle.template("frontend.html")```. Check code below to further examine how exactly this is done.
283
284Now we will expand ```/api``` route and use different methods to write or read data-points. For writing data-point we will use POST method and for reading points we will use GET method. GET method will return JSON object with latest readings and historical data.
285
286There is a fantastic JavaScript library for plotting time-series charts called [MetricsGraphics.js](https://www.metricsgraphicsjs.org) that is based on [D3.js](https://d3js.org/) library for visualizing data.
287
288Data schema required by MetricsGraphics.js → to achieve this we need to transform data from database into this format:
289
290```json
291[
292 {
293 "date": "2017-08-11 01:07:20",
294 "value": 933
295 },
296 {
297 "date": "2017-08-11 01:07:30",
298 "value": 743
299 }
300]
301```
302
303Web application is now complete and we only need ```frontend.html``` that we will develop now. If you would try to start web app now and go to root app this will return error because we don't have frontend.html yet.
304
305```python
306# -*- coding: utf-8 -*-
307
308import time
309import bottle
310import json
311import datetime
312import random
313import dataset
314
315# initializing bottle app
316app = bottle.Bottle()
317
318# adds root directory as template folder
319bottle.TEMPLATE_PATH.insert(0, "./")
320
321# connects to sqlite database
322# check_same_thread=False allows using it in multi-threaded mode
323app.config["db"] = dataset.connect("sqlite:///data.db?check_same_thread=False")
324
325# api key that will be used in Arduino code
326app.config["api_key"] = "JtF2aUE5SGHfVJBCG5SH"
327
328# triggered when / is accessed from browser
329# only accepts GET → no POST allowed
330@app.route("/", method=["GET"])
331def route_default():
332 return bottle.template("frontend.html")
333
334# triggered when /api is accessed from browser
335# accepts POST and GET
336@app.route("/api", method=["GET", "POST"])
337def route_default():
338
339 # if method is POST then we write datapoint
340 if bottle.request.method == "POST":
341 status = 400
342 ts = int(time.time()) # current timestamp
343 value = bottle.request.body.read() # data from device
344 api_key = bottle.request.get_header("Api-Key") # api key from header
345
346 # outputs to console recieved data for debug reason
347 print ">>> {} :: {}".format(value, api_key)
348
349 # if api_key is correct and value is present
350 # then writes attribute to point table
351 if api_key == app.config["api_key"] and value:
352 app.config["db"]["point"].insert(dict(ts=ts, value=value))
353 status = 200
354
355 # we only need to return status
356 return bottle.HTTPResponse(status=status, body="")
357
358 # if method is GET then we read datapoint
359 else:
360 response = []
361 datapoints = app.config["db"]["point"].all()
362
363 for point in datapoints:
364 response.append({
365 "date": datetime.datetime.fromtimestamp(int(point["ts"])).strftime("%Y-%m-%d %H:%M:%S"),
366 "value": point["value"]
367 })
368
369 bottle.response.content_type = "application/json"
370 return json.dumps(response)
371
372# starting server on http://0.0.0.0:5000
373if __name__ == "__main__":
374 bottle.run(
375 app = app,
376 host = "0.0.0.0",
377 port = 5000,
378 debug = True,
379 reloader = True,
380 catchall = True,
381 )
382```
383
384And now finally we can implement ```frontend.html```. Create file with this name and copy code below. When you are done you can start web application. Steps for this part are listed below the code.
385
386```html
387<!DOCTYPE html>
388<html>
389
390 <head>
391 <meta charset="utf-8">
392 <title>Simple IOT application</title>
393 </head>
394
395 <body>
396
397 <h1>Simple IOT application</h1>
398
399 <div class="chart-placeholder">
400 <div id="chart"></div>
401 </div>
402
403 <!-- application main script -->
404 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
405 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
406 <script src="https://cdnjs.cloudflare.com/ajax/libs/metrics-graphics/2.11.0/metricsgraphics.min.js"></script>
407 <script>
408 function fetch_and_render() {
409 d3.json("/api", function(data) {
410 data = MG.convert.date(data, "date", "%Y-%m-%d %H:%M:%S");
411 MG.data_graphic({
412 data: data,
413 chart_type: "line",
414 full_width: true,
415 height: 270,
416 target: document.getElementById("chart"),
417 x_accessor: "date",
418 y_accessor: "value"
419 });
420 });
421 }
422 window.onload = function() {
423 // initial call for rendering
424 fetch_and_render();
425
426 // updates chart every 5 seconds
427 setInterval(function() {
428 fetch_and_render();
429 }, 5000);
430 }
431 </script>
432
433 <!-- application styles -->
434 <style>
435 body {
436 font: 13px sans-serif;
437 padding: 20px 50px;
438 }
439 .chart-placeholder {
440 border: 2px solid #ccc;
441 width: 100%;
442 user-select: none;
443 }
444 /* chart styles */
445 .mg-line1-color {
446 stroke: red;
447 stroke-width: 2;
448 }
449 .mg-main-area, .mg-main-line {
450 fill: #fff;
451 }
452 .mg-x-axis line, .mg-y-axis line {
453 stroke: #b3b2b2;
454 stroke-width: 1px;
455 }
456 </style>
457
458 </body>
459
460</html>
461```
462
463Now the folder structure should look like:
464
465_simple-iot-app/_
466
467* _webapp.py_
468* _data.db_
469* _frontend.html_
470
471Ok, lets now start application and start feeding it data.
472
4731. ```python webapp.py```
4742. connect Arduino MKR1000 to power source
4753. open browser and go to ```http://0.0.0.0:5000```
476
477If everything goes well you should be seeing new data-points rendered on chart every 5 seconds.
478
479If you navigate to ```http://0.0.0.0:5000``` you should see rendered chart as shown on picture below.
480
481![Application output](/files/iot-app-output.png)
482
483Complete application with all the code is available for [download](/files/simple-iot-application.zip).
484
485
486
487## Conclusion
488
489I hope this clarifies some aspects of IOT application development. Of course this is a minimal example and is far from what can be done in real life with some further dive into other technologies.
490
491If you would like to continue exploring IOT world here are some interesting resources for you to examine:
492
493* [Reading Sensors with an Arduino](https://www.allaboutcircuits.com/projects/reading-sensors-with-an-arduino/)
494* [MQTT 101 – How to Get Started with the lightweight IoT Protocol](http://www.hivemq.com/blog/how-to-get-started-with-mqtt)
495* [Stream Updates with Server-Sent Events](https://www.html5rocks.com/en/tutorials/eventsource/basics/)
496* [Internet of Things (IoT) Tutorials](http://www.tutorialspoint.com/internet_of_things/)
497
498Any comment or additional ideas are welcomed in comments below.