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