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