diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-02-23 10:35:22 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2024-02-23 10:35:22 +0100 |
| commit | 4abcce013c9ee3053badf2abda77190233066676 (patch) | |
| tree | 450de7e8fed3c3c7501a9d2e2eb60a676bdfa09e /_posts/posts/2017-08-11-simple-iot-application.md | |
| parent | cdf50cb2e3051200c6ea0628c318d66220b7d1a1 (diff) | |
| download | mitjafelicijan.com-4abcce013c9ee3053badf2abda77190233066676.tar.gz | |
Testing thoughts page
Diffstat (limited to '_posts/posts/2017-08-11-simple-iot-application.md')
| -rw-r--r-- | _posts/posts/2017-08-11-simple-iot-application.md | 608 |
1 files changed, 608 insertions, 0 deletions
diff --git a/_posts/posts/2017-08-11-simple-iot-application.md b/_posts/posts/2017-08-11-simple-iot-application.md new file mode 100644 index 0000000..b552e8f --- /dev/null +++ b/_posts/posts/2017-08-11-simple-iot-application.md | |||
| @@ -0,0 +1,608 @@ | |||
| 1 | --- | ||
| 2 | title: Simple IOT application supported by real-time monitoring and data history | ||
| 3 | permalink: /simple-iot-application.html | ||
| 4 | date: 2017-08-11T12:00:00+02:00 | ||
| 5 | layout: post | ||
| 6 | type: post | ||
| 7 | draft: false | ||
| 8 | --- | ||
| 9 | |||
| 10 | ## Initial thoughts | ||
| 11 | |||
| 12 | I have been developing these kind of application for the better part of my last | ||
| 13 | 5 years and people keep asking me how to approach developing such application | ||
| 14 | and I will give a try explaining it here. | ||
| 15 | |||
| 16 | IOT applications are really no different than any other kind of applications. | ||
| 17 | We have data that needs to be collected and visualized in some form of tables or | ||
| 18 | charts. The main difference here is that most of the times these data is | ||
| 19 | collected by some kind of device foreign to developer that mainly operates in | ||
| 20 | web domain. But fear not, it's not that different than writing some JavaScript. | ||
| 21 | |||
| 22 | There are many devices able to transmit data via wireless or wired network by | ||
| 23 | default but for the sake of example we will be using commonly known Arduino with | ||
| 24 | wireless module already on the board → [Arduino | ||
| 25 | MKR1000](https://store.arduino.cc/arduino-mkr1000). | ||
| 26 | |||
| 27 | In order to make this little project as accessible to others as possible I will | ||
| 28 | try to make it as inexpensive as possible. And by this I mean that I will avoid | ||
| 29 | using hosted virtual servers and will be using my own laptop as a server. But | ||
| 30 | you must buy Arduino MKR1000 to follow steps below. But if you would want to | ||
| 31 | deploy this software I would suggest using | ||
| 32 | [DigitalOcean](https://www.digitalocean.com) → smallest VPS is only per month | ||
| 33 | making this one of the most affordable option out there. Please notice that this | ||
| 34 | software will not run on stock web hosting that only supports LAMP (Linux, | ||
| 35 | Apache, MySQL, and PHP). | ||
| 36 | |||
| 37 | But before we begin please take notice that this is strictly experimental code | ||
| 38 | and not well optimized and there are much better ways in handling some aspects | ||
| 39 | of the application but that requires much deeper knowledge of technology that is | ||
| 40 | not needed for an example like this. | ||
| 41 | |||
| 42 | **Development steps** | ||
| 43 | |||
| 44 | 1. Simple Python API that will receive and store incoming data. | ||
| 45 | 2. Prototype C++ code that will read "sensor data" and transmit it to API. | ||
| 46 | 3. Data visualization with charts → extends Python web application. | ||
| 47 | |||
| 48 | Step 1. and 3. will share the same web application. One route will be dedicated | ||
| 49 | to API and another to serving HTML with chart. | ||
| 50 | |||
| 51 | Schema below represents what we will try to achieve and how different parts | ||
| 52 | correlates to each other. | ||
| 53 | |||
| 54 | {:loading="lazy"} | ||
| 55 | |||
| 56 | ## Simple Python API | ||
| 57 | |||
| 58 | I have always been a fan of simplicity so we will be using [Bottle: Python Web | ||
| 59 | Framework](https://bottlepy.org/docs/dev/). It is a single file web framework | ||
| 60 | that seriously simplifies working with routes, templating and has built-in web | ||
| 61 | server that satisfies our need in this case. | ||
| 62 | |||
| 63 | First 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 | ||
| 65 | software ```pip install bottle --user```. | ||
| 66 | |||
| 67 | If you are using Linux or MacOS then Python is already installed. If you will | ||
| 68 | try to test this on Windows please install [Python for | ||
| 69 | Windows](https://www.python.org/downloads/windows/). There may be some problems | ||
| 70 | with path when you will try to launch ```python webapp.py``` so please take care | ||
| 71 | of this before you continue. | ||
| 72 | |||
| 73 | ### Basic web application | ||
| 74 | |||
| 75 | Most basic bottle application is quite simple. Paste code below in | ||
| 76 | ```webapp.py``` file and save. | ||
| 77 | |||
| 78 | ```python | ||
| 79 | # -*- coding: utf-8 -*- | ||
| 80 | |||
| 81 | import bottle | ||
| 82 | |||
| 83 | # initializing bottle app | ||
| 84 | app = bottle.Bottle() | ||
| 85 | |||
| 86 | # triggered when / is accessed from browser | ||
| 87 | # only accepts GET → no POST allowed | ||
| 88 | @app.route("/", method=["GET"]) | ||
| 89 | def route_default(): | ||
| 90 | return "howdy from python" | ||
| 91 | |||
| 92 | # starting server on http://0.0.0.0:5000 | ||
| 93 | if __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 | |||
| 104 | To run this simple application you should open command prompt or terminal on | ||
| 105 | your machine and go to the folder containing your file and type ```python | ||
| 106 | webapp.py```. If everything goes ok then open your web browser and point it to | ||
| 107 | ```http://0.0.0.0:5000```. | ||
| 108 | |||
| 109 | If you would like change the port of your application (like port 80) and not use | ||
| 110 | root to run your app this will present a problem. The TCP/IP port numbers below | ||
| 111 | 1024 are privileged ports → this is a security feature. So in order of | ||
| 112 | simplicity and security use a port number above 1024 like I have used port 5000. | ||
| 113 | |||
| 114 | If this fails at any time please fix it before you continue, because nothing | ||
| 115 | below will work otherwise. | ||
| 116 | |||
| 117 | We use 0.0.0.0 as default host so that this app is available over your local | ||
| 118 | network. If you find your local ip ```ifconfig``` and try accessing this site | ||
| 119 | with your phone (if on same network/router as your machine) this should work as | ||
| 120 | well (example of such ip ```http://192.168.1.15:5000```). This is a must have | ||
| 121 | because Arduino will be accessing this application to send it's data. | ||
| 122 | |||
| 123 | ### Web application security | ||
| 124 | |||
| 125 | There is a lot to be said about security and is a topic of many books. Of course | ||
| 126 | all this can not be written here but to just establish some basic security → you | ||
| 127 | should always use SSL with your application. Some fantastic free certificates | ||
| 128 | are available by [Let's Encrypt - Free SSL/TLS | ||
| 129 | Certificates](https://letsencrypt.org). With SSL certificate installed you | ||
| 130 | should then make use of HTTP headers and send your "API key" via a header. If | ||
| 131 | your key is send via header then this key is encrypted by SSL and send encrypted | ||
| 132 | over the network. Never send your api keys by GET parameter like | ||
| 133 | ```http://example.com/?api_key=somekeyvalue```. The problem that this kind of | ||
| 134 | sending presents is that this key is visible in logs and by network sniffers. | ||
| 135 | |||
| 136 | There is a fantastic article describing some aspects about security: [11 Web | ||
| 137 | Application Security Best | ||
| 138 | Practices](https://www.keycdn.com/blog/web-application-security-best-practices/). Please | ||
| 139 | check it out. | ||
| 140 | |||
| 141 | ### Simple API for writing data-points | ||
| 142 | |||
| 143 | We will now be using boilerplate code from example above and extend it to be | ||
| 144 | SQLite3 because it plays well with Python and can store quite large amount of | ||
| 145 | able to write data received by API to local storage. For example use I will use | ||
| 146 | data. I have been using it to collect gigabytes of data in a single database | ||
| 147 | without any corruption or problems → your experience may vary. | ||
| 148 | |||
| 149 | To avoid learning SQLite I will be using [Dataset: databases for lazy | ||
| 150 | people](https://dataset.readthedocs.io/en/latest/index.html). This package | ||
| 151 | abstracts SQL and simplifies writing and reading data from database. You should | ||
| 152 | install this package with pip software ```pip install dataset --user```. | ||
| 153 | |||
| 154 | Because API will use POST method I will be testing if code works correctly by | ||
| 155 | using [Restlet Client for Google | ||
| 156 | Chrome](https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm). | ||
| 157 | This software also allows you to set headers → for basic security with API_KEY. | ||
| 158 | |||
| 159 | To quickly generate passwords or API keys I usually use this nifty website | ||
| 160 | [RandomKeygen](https://randomkeygen.com/). | ||
| 161 | |||
| 162 | Copy and paste code below over your previous code in file ```webapp.py```. | ||
| 163 | |||
| 164 | ```python | ||
| 165 | # -*- coding: utf-8 -*- | ||
| 166 | |||
| 167 | import time | ||
| 168 | import bottle | ||
| 169 | import random | ||
| 170 | import dataset | ||
| 171 | |||
| 172 | # initializing bottle app | ||
| 173 | app = bottle.Bottle() | ||
| 174 | |||
| 175 | # connects to sqlite database | ||
| 176 | # check_same_thread=False allows using it in multi-threaded mode | ||
| 177 | app.config["dsn"] = dataset.connect("sqlite:///data.db?check_same_thread=False") | ||
| 178 | |||
| 179 | # api key that will be used in Arduino code | ||
| 180 | app.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"]) | ||
| 185 | def 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 | ||
| 204 | if __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 | |||
| 215 | To run this simply go to folder containing python file and run ```python | ||
| 216 | webapp.py``` from terminal. If everything goes ok you should have simple API | ||
| 217 | available via POST method on /api route. | ||
| 218 | |||
| 219 | After testing the service with Restlet Client you should be able to view your | ||
| 220 | data in a database file ```data.db```. | ||
| 221 | |||
| 222 | {:loading="lazy"} | ||
| 223 | |||
| 224 | You can also check the contents of new database file by using desktop client | ||
| 225 | for SQLite → [DB Browser for SQLite](http://sqlitebrowser.org/). | ||
| 226 | |||
| 227 | {:loading="lazy"} | ||
| 228 | |||
| 229 | Table 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 | ||
| 231 | would happen to have atomic clock on Arduino it would be then better to generate | ||
| 232 | and send timestamp with the value. This would be particularity useful if we | ||
| 233 | would be collecting sensor data at a higher frequency and then sending this data | ||
| 234 | in bulk to API. | ||
| 235 | |||
| 236 | If you will deploy this app with uWSGI and multi-threaded, use DSN (Data Source | ||
| 237 | Name) url with ```?check_same_thread=False```. | ||
| 238 | |||
| 239 | Ok, now that we have some sort of a working API with some basic security so | ||
| 240 | unwanted people can not post data to your database can we proceed further and | ||
| 241 | try to program Arduino to send data to API. | ||
| 242 | |||
| 243 | ## Sending data to API with Arduino MKR1000 | ||
| 244 | |||
| 245 | First of all you should have MKR1000 module and microUSB cable to proceed. If | ||
| 246 | you 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 | ||
| 248 | should be able to download and install IDE. Once that task is completed and you | ||
| 249 | have successfully run blink example you should proceed to the next step. | ||
| 250 | |||
| 251 | In 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. | ||
| 253 | Please check before you install, you may already have it installed. | ||
| 254 | |||
| 255 | Code below is a working example that sends data to API. Before you try to test | ||
| 256 | your code make sure you have run Python web application. Then change settings | ||
| 257 | for wifi, api endpoint and api_key. If by some reason code bellow doesn't work | ||
| 258 | for you please leave a comment and I'll try to help. | ||
| 259 | |||
| 260 | Once you have opened IDE and copied this code try to compile and upload it. | ||
| 261 | Then open "Serial monitor" to see if any output is presented by Arduino. | ||
| 262 | |||
| 263 | ```c | ||
| 264 | #include <WiFi101.h> | ||
| 265 | |||
| 266 | // wifi settings | ||
| 267 | char ssid[] = "ssid-name"; | ||
| 268 | char pass[] = "ssid-password"; | ||
| 269 | |||
| 270 | // api server enpoint | ||
| 271 | char server[] = "192.168.6.22"; | ||
| 272 | int port = 5000; | ||
| 273 | |||
| 274 | // api key that must be the same as the one in Python code | ||
| 275 | String api_key = "JtF2aUE5SGHfVJBCG5SH"; | ||
| 276 | |||
| 277 | // frequency data is sent in ms - every 5 seconds | ||
| 278 | int timeout = 1000 * 5; | ||
| 279 | |||
| 280 | int status = WL_IDLE_STATUS; | ||
| 281 | |||
| 282 | void 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 | |||
| 317 | void 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 | |||
| 346 | As seen from example you can notice that Arduino is generating random integer | ||
| 347 | between [ 0 .. 1000 ]. You can easily replace this with a temperature sensor or | ||
| 348 | any other kind of sensor. | ||
| 349 | |||
| 350 | Now that we have API under the hood and Arduino is sending demo data we can now | ||
| 351 | focus on data visualization. | ||
| 352 | |||
| 353 | ## Data visualization | ||
| 354 | |||
| 355 | Before we continue we should examine our project folder structure. Currently we | ||
| 356 | only have two files in our project: | ||
| 357 | |||
| 358 | _simple-iot-app/_ | ||
| 359 | |||
| 360 | * _webapp.py_ | ||
| 361 | * _data.db_ | ||
| 362 | |||
| 363 | We will now add HTML template that will contain CSS and JavaScript code inline | ||
| 364 | for the simplicity reason. And for the bottle framework to be able to scan root | ||
| 365 | application folder for templates we will add ```bottle.TEMPLATE_PATH.insert(0, | ||
| 366 | "./")``` in ```webapp.py```. By default bottle framework uses ```views/``` | ||
| 367 | subfolder to store templates. This is not the ideal situation and if you will | ||
| 368 | use bottle to develop web applications you should use native behavior and store | ||
| 369 | templates in it's predefined folder. But for the sake of example we will | ||
| 370 | over-ride this. Be careful to fully replace your code with new code that is | ||
| 371 | provided below. Avoid partially replacing code in file :) Also new code for | ||
| 372 | reading data-points is provided in Python example below. | ||
| 373 | |||
| 374 | First we add new route to our web application. It should be trigger when browser | ||
| 375 | hits root of application ```http://0.0.0.0:5000/```. This route will do nothing | ||
| 376 | more than render ```frontend.html``` template. This is done by ```return | ||
| 377 | bottle.template("frontend.html")```. Check code below to further examine how | ||
| 378 | exactly this is done. | ||
| 379 | |||
| 380 | Now we will expand ```/api``` route and use different methods to write or read | ||
| 381 | data-points. For writing data-point we will use POST method and for reading | ||
| 382 | points we will use GET method. GET method will return JSON object with latest | ||
| 383 | readings and historical data. | ||
| 384 | |||
| 385 | There 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 | |||
| 389 | Data schema required by MetricsGraphics.js → to achieve this we need to | ||
| 390 | transform 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 | |||
| 405 | Web application is now complete and we only need ```frontend.html``` that we | ||
| 406 | will develop now. If you would try to start web app now and go to root app this | ||
| 407 | will return error because we don't have frontend.html yet. | ||
| 408 | |||
| 409 | ```python | ||
| 410 | # -*- coding: utf-8 -*- | ||
| 411 | |||
| 412 | import time | ||
| 413 | import bottle | ||
| 414 | import json | ||
| 415 | import datetime | ||
| 416 | import random | ||
| 417 | import dataset | ||
| 418 | |||
| 419 | # initializing bottle app | ||
| 420 | app = bottle.Bottle() | ||
| 421 | |||
| 422 | # adds root directory as template folder | ||
| 423 | bottle.TEMPLATE_PATH.insert(0, "./") | ||
| 424 | |||
| 425 | # connects to sqlite database | ||
| 426 | # check_same_thread=False allows using it in multi-threaded mode | ||
| 427 | app.config["db"] = dataset.connect("sqlite:///data.db?check_same_thread=False") | ||
| 428 | |||
| 429 | # api key that will be used in Arduino code | ||
| 430 | app.config["api_key"] = "JtF2aUE5SGHfVJBCG5SH" | ||
| 431 | |||
| 432 | # triggered when / is accessed from browser | ||
| 433 | # only accepts GET → no POST allowed | ||
| 434 | @app.route("/", method=["GET"]) | ||
| 435 | def 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"]) | ||
| 441 | def 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 | ||
| 477 | if __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 | |||
| 488 | And now finally we can implement ```frontend.html```. Create file with this name | ||
| 489 | and copy code below. When you are done you can start web application. Steps for | ||
| 490 | this 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 | |||
| 569 | Now the folder structure should look like: | ||
| 570 | |||
| 571 | _simple-iot-app/_ | ||
| 572 | |||
| 573 | * _webapp.py_ | ||
| 574 | * _data.db_ | ||
| 575 | * _frontend.html_ | ||
| 576 | |||
| 577 | Ok, lets now start application and start feeding it data. | ||
| 578 | |||
| 579 | 1. ```python webapp.py``` | ||
| 580 | 2. connect Arduino MKR1000 to power source | ||
| 581 | 3. open browser and go to ```http://0.0.0.0:5000``` | ||
| 582 | |||
| 583 | If everything goes well you should be seeing new data-points rendered on chart | ||
| 584 | every 5 seconds. | ||
| 585 | |||
| 586 | If you navigate to ```http://0.0.0.0:5000``` you should see rendered chart as | ||
| 587 | shown on picture below. | ||
| 588 | |||
| 589 | {:loading="lazy"} | ||
| 590 | |||
| 591 | Complete application with all the code is available for | ||
| 592 | [download](/assets/posts/iot-application/simple-iot-application.zip). | ||
| 593 | |||
| 594 | ## Conclusion | ||
| 595 | |||
| 596 | I hope this clarifies some aspects of IOT application development. Of course | ||
| 597 | this is a minimal example and is far from what can be done in real life with | ||
| 598 | some further dive into other technologies. | ||
| 599 | |||
| 600 | If you would like to continue exploring IOT world here are some interesting | ||
| 601 | resources 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 | |||
| 608 | Any comment or additional ideas are welcomed in comments below. | ||
