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