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