diff options
Diffstat (limited to 'public/simple-iot-application.html')
| -rwxr-xr-x | public/simple-iot-application.html | 439 |
1 files changed, 0 insertions, 439 deletions
diff --git a/public/simple-iot-application.html b/public/simple-iot-application.html deleted file mode 100755 index 2027c1c..0000000 --- a/public/simple-iot-application.html +++ /dev/null | |||
| @@ -1,439 +0,0 @@ | |||
| 1 | <!doctype html><html lang=en-us><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><link href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL69vf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAv76+/8LBwQkAAAAAAAAAAAAAAAC+vb3/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAL+9vf/Bv78JAAAAAAAAAAAAAAAAu7q6/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC7ubr/vr29CAAAAAAAAAAAy8nJAZ6foP8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnqGj/6GipAoAAAAAHLjU/xcXHf/BwsL/I8XY/yPK3v8XGiD/IbjL/yPF2f8XGiD/Fxkf/yLF2f8gnK3/Fxog/62ztv8fwNf/FRcd/x271v8mz93/GRsi/xkXHf8p097/GiIp/xobIv8p0t3/KdPe/xocIv8fYmr/KNPe/xoZH/8aHCL/J87c/xy81/8VFxz/IsPZ/8zS0/8XGiD/Ir/R/yPH2/8XGiD/Fxkf/yPH2/8dd4T/GBog/yPJ3f8jyNr/uru9/xcUGv8cudb/EhITDKi5vRKlvMP/RUpOERwcHRAdOj4QHTk8EBwdHRAdNTgQHTo/EBwcHRAcHB0QSGduEKW4vf+koqQfHzg+EBqz0ewSFRv7EyMr/xq51vsTERb7ExUb+xq41fsau9j7ExUb+xiPp/sZudb7ExUb+xMVG/sZuNX/GKvI/BIUGfMdvdn/IrfL/xcaIP8n1eb/J9Dh/xkcIf8ZGR7/J8/f/xxCSv8ZGyH/J9Dg/ybQ4P8ZHCL/FSQs/yPK3/8UExj/GE1b/ybS5P8ZGB7/Ghwj/ynW5P8p2Ob/Ghwi/yWrtv8p1eH/Ghwi/xocIv8p1uT/J8XT/xkcIv8m1un/Hb7d/xUYH/8hzOr/HtHu/xcaIf8XGB//I8vi/xgxOv8XGSD/I8rg/yPK4P8XGiD/GUFL/yPP6f8SERj/Fhkh/x3A4f8AAAAAJ2f9/ydr//8mZPH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlYu38J2v//ydo/f8AAAAAAAAAAAd8/fkFqf//Iob8sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMY39awWr//8FfP3/AAAAAAAAAAAFm/7/SfD//wR+/f8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOB/f9B7v//BaX+/wAAAAAAAAAAQ878SAyZ/v9n1v4KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADu9v8DDJb+/z3N/XgAAAAA3/sAAN/7AADf+wAA3/sAAAAAAAAAAAAAAAAAAN/7AAAAAAAAAAAAAAAAAAAAAAAAj/EAAI/5AACP8QAA3/sAAA==" rel=icon type=image/x-icon><title>Simple IOT application supported by real-time monitoring and data history</title><meta name=description content="Initial thoughtsI have been developing these kind of application for the better part of my last5 years and people keep asking me how to approach developing such applicationand I will give a try explaining it here."><link rel=alternate type=application/rss+xml title="Mitja Felicijan's posts" href=https://mitjafelicijan.com/index.xml><link rel=alternate type=application/rss+xml title="Mitja Felicijan's notes" href=https://mitjafelicijan.com/notes.xml><style>body{padding:1rem;max-width:760px;background:#fff;font-family:times new roman,Times,serif;line-height:1.35rem}hr{margin-block-start:1.5rem}h1,h2,h3{line-height:initial}footer{margin-block-start:3rem}table{max-width:100%;border-collapse:separate;border-spacing:2px;border:1px solid #000;border-left:1px solid #999;border-top:1px solid #999}blockquote{font-style:italic}table thead{background:#eee}td,th{border:1px solid #000;padding:4px;border-right:1px solid #999;border-bottom:1px solid #999;text-align:left}pre{text-wrap:nowrap;overflow-x:auto;margin-block-start:1.5rem;margin-block-end:1.5rem;padding:.5rem 0;border-top:1px solid #000;border-bottom:1px solid #000}pre code{line-height:1.3em}pre,code,pre *,code *{font-family:monospace;font-size:initial!important}img,video,audio{max-width:100%}header{display:flex;flex-direction:row;gap:3rem}nav{display:flex;gap:.75rem}.pstatus-orange{background:gold}.pstatus-green{background:#9acd32}.pstatus-red{background:#cd5c5c}@media only screen and (max-width:600px){header{flex-direction:column;gap:1rem}a{word-wrap:break-word}}</style><header><nav class=main><a href=/>Home</a> | ||
| 2 | <a href=https://git.mitjafelicijan.com/ target=_blank>Git</a> | ||
| 3 | <a href=https://files.mitjafelicijan.com/ target=_blank>Files</a> | ||
| 4 | <a href=/mitjafelicijan.pgp.pub.txt target=_blank>PGP</a> | ||
| 5 | <a href=/curriculum-vitae.html>CV</a> | ||
| 6 | <a href=/index.xml target=_blank>RSS</a></nav></header><main><div><h1>Simple IOT application supported by real-time monitoring and data history</h1><p>Aug 11, 2017<div><h2 id=initial-thoughts>Initial thoughts</h2><p>I have been developing these kind of application for the better part of my last | ||
| 7 | 5 years and people keep asking me how to approach developing such application | ||
| 8 | and I will give a try explaining it here.<p>IOT applications are really no different than any other kind of applications. | ||
| 9 | We have data that needs to be collected and visualized in some form of tables or | ||
| 10 | charts. The main difference here is that most of the times these data is | ||
| 11 | collected by some kind of device foreign to developer that mainly operates in | ||
| 12 | web domain. But fear not, it's not that different than writing some JavaScript.<p>There are many devices able to transmit data via wireless or wired network by | ||
| 13 | default but for the sake of example we will be using commonly known Arduino with | ||
| 14 | wireless module already on the board → <a href=https://store.arduino.cc/arduino-mkr1000>Arduino | ||
| 15 | MKR1000</a>.<p>In order to make this little project as accessible to others as possible I will | ||
| 16 | try to make it as inexpensive as possible. And by this I mean that I will avoid | ||
| 17 | using hosted virtual servers and will be using my own laptop as a server. But | ||
| 18 | you must buy Arduino MKR1000 to follow steps below. But if you would want to | ||
| 19 | deploy this software I would suggest using | ||
| 20 | <a href=https://www.digitalocean.com>DigitalOcean</a> → smallest VPS is only per month | ||
| 21 | making this one of the most affordable option out there. Please notice that this | ||
| 22 | software will not run on stock web hosting that only supports LAMP (Linux, | ||
| 23 | Apache, MySQL, and PHP).<p>But before we begin please take notice that this is strictly experimental code | ||
| 24 | and not well optimized and there are much better ways in handling some aspects | ||
| 25 | of the application but that requires much deeper knowledge of technology that is | ||
| 26 | not needed for an example like this.<p><strong>Development steps</strong><ol><li>Simple Python API that will receive and store incoming data.<li>Prototype C++ code that will read "sensor data" and transmit it to API.<li>Data visualization with charts → extends Python web application.</ol><p>Step 1. and 3. will share the same web application. One route will be dedicated | ||
| 27 | to API and another to serving HTML with chart.<p>Schema below represents what we will try to achieve and how different parts | ||
| 28 | correlates to each other.<p><img src=/assets/iot-application/simple-iot-application-overview.svg alt=Overview><h2 id=simple-python-api>Simple Python API</h2><p>I have always been a fan of simplicity so we will be using <a href=https://bottlepy.org/docs/dev/>Bottle: Python Web | ||
| 29 | Framework</a>. It is a single file web framework | ||
| 30 | that seriously simplifies working with routes, templating and has built-in web | ||
| 31 | server that satisfies our need in this case.<p>First we need to install bottle package. This can be done by downloading | ||
| 32 | <code>bottle.py</code> and placing it in the root of your application or by using pip | ||
| 33 | software <code>pip install bottle --user</code>.<p>If you are using Linux or MacOS then Python is already installed. If you will | ||
| 34 | try to test this on Windows please install <a href=https://www.python.org/downloads/windows/>Python for | ||
| 35 | Windows</a>. There may be some problems | ||
| 36 | with path when you will try to launch <code>python webapp.py</code> so please take care | ||
| 37 | of this before you continue.<h3 id=basic-web-application>Basic web application</h3><p>Most basic bottle application is quite simple. Paste code below in | ||
| 38 | <code>webapp.py</code> file and save.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:green># -*- coding: utf-8 -*-</span> | ||
| 39 | </span></span><span style=display:flex><span> | ||
| 40 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 41 | </span></span><span style=display:flex><span> | ||
| 42 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 43 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 44 | </span></span><span style=display:flex><span> | ||
| 45 | </span></span><span style=display:flex><span><span style=color:green># triggered when / is accessed from browser</span> | ||
| 46 | </span></span><span style=display:flex><span><span style=color:green># only accepts GET → no POST allowed</span> | ||
| 47 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/"</span>, method=[<span style=color:#a31515>"GET"</span>]) | ||
| 48 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 49 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> <span style=color:#a31515>"howdy from python"</span> | ||
| 50 | </span></span><span style=display:flex><span> | ||
| 51 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 52 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 53 | </span></span><span style=display:flex><span> bottle.run( | ||
| 54 | </span></span><span style=display:flex><span> app = app, | ||
| 55 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 56 | </span></span><span style=display:flex><span> port = 5000, | ||
| 57 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 58 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 59 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 60 | </span></span><span style=display:flex><span> ) | ||
| 61 | </span></span></code></pre><p>To run this simple application you should open command prompt or terminal on | ||
| 62 | your machine and go to the folder containing your file and type <code>python webapp.py</code>. If everything goes ok then open your web browser and point it to | ||
| 63 | <code>http://0.0.0.0:5000</code>.<p>If you would like change the port of your application (like port 80) and not use | ||
| 64 | root to run your app this will present a problem. The TCP/IP port numbers below | ||
| 65 | 1024 are privileged ports → this is a security feature. So in order of | ||
| 66 | simplicity and security use a port number above 1024 like I have used port 5000.<p>If this fails at any time please fix it before you continue, because nothing | ||
| 67 | below will work otherwise.<p>We use 0.0.0.0 as default host so that this app is available over your local | ||
| 68 | network. If you find your local ip <code>ifconfig</code> and try accessing this site | ||
| 69 | with your phone (if on same network/router as your machine) this should work as | ||
| 70 | well (example of such ip <code>http://192.168.1.15:5000</code>). This is a must have | ||
| 71 | because Arduino will be accessing this application to send it's data.<h3 id=web-application-security>Web application security</h3><p>There is a lot to be said about security and is a topic of many books. Of course | ||
| 72 | all this can not be written here but to just establish some basic security → you | ||
| 73 | should always use SSL with your application. Some fantastic free certificates | ||
| 74 | are available by <a href=https://letsencrypt.org>Let's Encrypt - Free SSL/TLS | ||
| 75 | Certificates</a>. With SSL certificate installed you | ||
| 76 | should then make use of HTTP headers and send your "API key" via a header. If | ||
| 77 | your key is send via header then this key is encrypted by SSL and send encrypted | ||
| 78 | over the network. Never send your api keys by GET parameter like | ||
| 79 | <code>http://example.com/?api_key=somekeyvalue</code>. The problem that this kind of | ||
| 80 | sending presents is that this key is visible in logs and by network sniffers.<p>There is a fantastic article describing some aspects about security: <a href=https://www.keycdn.com/blog/web-application-security-best-practices/>11 Web | ||
| 81 | Application Security Best | ||
| 82 | Practices</a>. Please | ||
| 83 | check it out.<h3 id=simple-api-for-writing-data-points>Simple API for writing data-points</h3><p>We will now be using boilerplate code from example above and extend it to be | ||
| 84 | SQLite3 because it plays well with Python and can store quite large amount of | ||
| 85 | able to write data received by API to local storage. For example use I will use | ||
| 86 | data. I have been using it to collect gigabytes of data in a single database | ||
| 87 | without any corruption or problems → your experience may vary.<p>To avoid learning SQLite I will be using <a href=https://dataset.readthedocs.io/en/latest/index.html>Dataset: databases for lazy | ||
| 88 | people</a>. This package | ||
| 89 | abstracts SQL and simplifies writing and reading data from database. You should | ||
| 90 | install this package with pip software <code>pip install dataset --user</code>.<p>Because API will use POST method I will be testing if code works correctly by | ||
| 91 | using <a href=https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm>Restlet Client for Google | ||
| 92 | Chrome</a>. | ||
| 93 | This software also allows you to set headers → for basic security with API_KEY.<p>To quickly generate passwords or API keys I usually use this nifty website | ||
| 94 | <a href=https://randomkeygen.com/>RandomKeygen</a>.<p>Copy and paste code below over your previous code in file <code>webapp.py</code>.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:green># -*- coding: utf-8 -*-</span> | ||
| 95 | </span></span><span style=display:flex><span> | ||
| 96 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> time | ||
| 97 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 98 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> random | ||
| 99 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> dataset | ||
| 100 | </span></span><span style=display:flex><span> | ||
| 101 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 102 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 103 | </span></span><span style=display:flex><span> | ||
| 104 | </span></span><span style=display:flex><span><span style=color:green># connects to sqlite database</span> | ||
| 105 | </span></span><span style=display:flex><span><span style=color:green># check_same_thread=False allows using it in multi-threaded mode</span> | ||
| 106 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"dsn"</span>] = dataset.connect(<span style=color:#a31515>"sqlite:///data.db?check_same_thread=False"</span>) | ||
| 107 | </span></span><span style=display:flex><span> | ||
| 108 | </span></span><span style=display:flex><span><span style=color:green># api key that will be used in Arduino code</span> | ||
| 109 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"api_key"</span>] = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span> | ||
| 110 | </span></span><span style=display:flex><span> | ||
| 111 | </span></span><span style=display:flex><span><span style=color:green># triggered when /api is accessed from browser</span> | ||
| 112 | </span></span><span style=display:flex><span><span style=color:green># only accepts POST → no GET allowed</span> | ||
| 113 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/api"</span>, method=[<span style=color:#a31515>"POST"</span>]) | ||
| 114 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 115 | </span></span><span style=display:flex><span> status = 400 | ||
| 116 | </span></span><span style=display:flex><span> ts = int(time.time()) <span style=color:green># current timestamp</span> | ||
| 117 | </span></span><span style=display:flex><span> value = bottle.request.body.read() <span style=color:green># data from device</span> | ||
| 118 | </span></span><span style=display:flex><span> api_key = bottle.request.get_header(<span style=color:#a31515>"Api_Key"</span>) <span style=color:green># api key from header</span> | ||
| 119 | </span></span><span style=display:flex><span> | ||
| 120 | </span></span><span style=display:flex><span> <span style=color:green># outputs to console received data for debug reason</span> | ||
| 121 | </span></span><span style=display:flex><span> print <span style=color:#a31515>">>> </span><span style=color:#a31515>{}</span><span style=color:#a31515> :: </span><span style=color:#a31515>{}</span><span style=color:#a31515>"</span>.format(value, api_key) | ||
| 122 | </span></span><span style=display:flex><span> | ||
| 123 | </span></span><span style=display:flex><span> <span style=color:green># if api_key is correct and value is present</span> | ||
| 124 | </span></span><span style=display:flex><span> <span style=color:green># then writes attribute to point table</span> | ||
| 125 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> api_key == app.config[<span style=color:#a31515>"api_key"</span>] <span style=color:#00f>and</span> value: | ||
| 126 | </span></span><span style=display:flex><span> app.config[<span style=color:#a31515>"dsn"</span>][<span style=color:#a31515>"point"</span>].insert(dict(ts=ts, value=value)) | ||
| 127 | </span></span><span style=display:flex><span> status = 200 | ||
| 128 | </span></span><span style=display:flex><span> | ||
| 129 | </span></span><span style=display:flex><span> <span style=color:green># we only need to return status</span> | ||
| 130 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.HTTPResponse(status=status, body=<span style=color:#a31515>""</span>) | ||
| 131 | </span></span><span style=display:flex><span> | ||
| 132 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 133 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 134 | </span></span><span style=display:flex><span> bottle.run( | ||
| 135 | </span></span><span style=display:flex><span> app = app, | ||
| 136 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 137 | </span></span><span style=display:flex><span> port = 5000, | ||
| 138 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 139 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 140 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 141 | </span></span><span style=display:flex><span> ) | ||
| 142 | </span></span></code></pre><p>To run this simply go to folder containing python file and run <code>python webapp.py</code> from terminal. If everything goes ok you should have simple API | ||
| 143 | available via POST method on /api route.<p>After testing the service with Restlet Client you should be able to view your | ||
| 144 | data in a database file <code>data.db</code>.<p><img src=/assets/iot-application/iot-rest-example.png alt="REST settings example"><p>You can also check the contents of new database file by using desktop client | ||
| 145 | for SQLite → <a href=http://sqlitebrowser.org/>DB Browser for SQLite</a>.<p><img src=/assets/iot-application/iot-sqlite-db.png alt="SQLite database example"><p>Table structure is as simple as it can be. We have ts (timestamp) and value | ||
| 146 | (value from Arduino). As you can see timestamp is generated on API side. If you | ||
| 147 | would happen to have atomic clock on Arduino it would be then better to generate | ||
| 148 | and send timestamp with the value. This would be particularity useful if we | ||
| 149 | would be collecting sensor data at a higher frequency and then sending this data | ||
| 150 | in bulk to API.<p>If you will deploy this app with uWSGI and multi-threaded, use DSN (Data Source | ||
| 151 | Name) url with <code>?check_same_thread=False</code>.<p>Ok, now that we have some sort of a working API with some basic security so | ||
| 152 | unwanted people can not post data to your database can we proceed further and | ||
| 153 | try to program Arduino to send data to API.<h2 id=sending-data-to-api-with-arduino-mkr1000>Sending data to API with Arduino MKR1000</h2><p>First of all you should have MKR1000 module and microUSB cable to proceed. If | ||
| 154 | you have ever done any work with Arduino you should know that you also need | ||
| 155 | <a href=https://www.arduino.cc/en/Main/Software>Arduino IDE</a>. On provided link you | ||
| 156 | should be able to download and install IDE. Once that task is completed and you | ||
| 157 | have successfully run blink example you should proceed to the next step.<p>In order to use wireless capabilities of MKR1000 you need to first install | ||
| 158 | <a href=https://www.arduino.cc/en/Reference/WiFi101>WiFi101 library</a> in Arduino IDE. | ||
| 159 | Please check before you install, you may already have it installed.<p>Code below is a working example that sends data to API. Before you try to test | ||
| 160 | your code make sure you have run Python web application. Then change settings | ||
| 161 | for wifi, api endpoint and api_key. If by some reason code bellow doesn't work | ||
| 162 | for you please leave a comment and I'll try to help.<p>Once you have opened IDE and copied this code try to compile and upload it. | ||
| 163 | Then open "Serial monitor" to see if any output is presented by Arduino.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:#00f>#include</span> <span style=color:#00f><WiFi101.h></span><span style=color:#00f> | ||
| 164 | </span></span></span><span style=display:flex><span><span style=color:#00f></span> | ||
| 165 | </span></span><span style=display:flex><span><span style=color:green>// wifi settings | ||
| 166 | </span></span></span><span style=display:flex><span><span style=color:green></span><span style=color:#2b91af>char</span> ssid[] = <span style=color:#a31515>"ssid-name"</span>; | ||
| 167 | </span></span><span style=display:flex><span><span style=color:#2b91af>char</span> pass[] = <span style=color:#a31515>"ssid-password"</span>; | ||
| 168 | </span></span><span style=display:flex><span> | ||
| 169 | </span></span><span style=display:flex><span><span style=color:green>// api server enpoint | ||
| 170 | </span></span></span><span style=display:flex><span><span style=color:green></span><span style=color:#2b91af>char</span> server[] = <span style=color:#a31515>"192.168.6.22"</span>; | ||
| 171 | </span></span><span style=display:flex><span><span style=color:#2b91af>int</span> port = 5000; | ||
| 172 | </span></span><span style=display:flex><span> | ||
| 173 | </span></span><span style=display:flex><span><span style=color:green>// api key that must be the same as the one in Python code | ||
| 174 | </span></span></span><span style=display:flex><span><span style=color:green></span>String api_key = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span>; | ||
| 175 | </span></span><span style=display:flex><span> | ||
| 176 | </span></span><span style=display:flex><span><span style=color:green>// frequency data is sent in ms - every 5 seconds | ||
| 177 | </span></span></span><span style=display:flex><span><span style=color:green></span><span style=color:#2b91af>int</span> timeout = 1000 * 5; | ||
| 178 | </span></span><span style=display:flex><span> | ||
| 179 | </span></span><span style=display:flex><span><span style=color:#2b91af>int</span> status = WL_IDLE_STATUS; | ||
| 180 | </span></span><span style=display:flex><span> | ||
| 181 | </span></span><span style=display:flex><span><span style=color:#2b91af>void</span> setup() { | ||
| 182 | </span></span><span style=display:flex><span> | ||
| 183 | </span></span><span style=display:flex><span> <span style=color:green>// initialize serial and wait for port to open: | ||
| 184 | </span></span></span><span style=display:flex><span><span style=color:green></span> Serial.begin(9600); | ||
| 185 | </span></span><span style=display:flex><span> delay(1000); | ||
| 186 | </span></span><span style=display:flex><span> | ||
| 187 | </span></span><span style=display:flex><span> <span style=color:green>// check for the presence of the shield | ||
| 188 | </span></span></span><span style=display:flex><span><span style=color:green></span> <span style=color:#00f>if</span> (WiFi.status() == WL_NO_SHIELD) { | ||
| 189 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"WiFi shield not present"</span>); | ||
| 190 | </span></span><span style=display:flex><span> <span style=color:#00f>while</span> (true); | ||
| 191 | </span></span><span style=display:flex><span> } | ||
| 192 | </span></span><span style=display:flex><span> | ||
| 193 | </span></span><span style=display:flex><span> <span style=color:green>// attempt to connect to wifi network | ||
| 194 | </span></span></span><span style=display:flex><span><span style=color:green></span> <span style=color:#00f>while</span> (status != WL_CONNECTED) { | ||
| 195 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"Attempting to connect to SSID: "</span>); | ||
| 196 | </span></span><span style=display:flex><span> Serial.println(ssid); | ||
| 197 | </span></span><span style=display:flex><span> status = WiFi.begin(ssid, pass); | ||
| 198 | </span></span><span style=display:flex><span> <span style=color:green>// wait 10 seconds for connection | ||
| 199 | </span></span></span><span style=display:flex><span><span style=color:green></span> delay(10000); | ||
| 200 | </span></span><span style=display:flex><span> } | ||
| 201 | </span></span><span style=display:flex><span> | ||
| 202 | </span></span><span style=display:flex><span> <span style=color:green>// output wifi status to serial monitor | ||
| 203 | </span></span></span><span style=display:flex><span><span style=color:green></span> Serial.print(<span style=color:#a31515>"SSID: "</span>); | ||
| 204 | </span></span><span style=display:flex><span> Serial.println(WiFi.SSID()); | ||
| 205 | </span></span><span style=display:flex><span> | ||
| 206 | </span></span><span style=display:flex><span> IPAddress ip = WiFi.localIP(); | ||
| 207 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"IP Address: "</span>); | ||
| 208 | </span></span><span style=display:flex><span> Serial.println(ip); | ||
| 209 | </span></span><span style=display:flex><span> | ||
| 210 | </span></span><span style=display:flex><span> <span style=color:#2b91af>long</span> rssi = WiFi.RSSI(); | ||
| 211 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"signal strength (RSSI):"</span>); | ||
| 212 | </span></span><span style=display:flex><span> Serial.print(rssi); | ||
| 213 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>" dBm"</span>); | ||
| 214 | </span></span><span style=display:flex><span>} | ||
| 215 | </span></span><span style=display:flex><span> | ||
| 216 | </span></span><span style=display:flex><span><span style=color:#2b91af>void</span> loop() { | ||
| 217 | </span></span><span style=display:flex><span> WiFiClient client; | ||
| 218 | </span></span><span style=display:flex><span> | ||
| 219 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> (client.connect(server, port)) { | ||
| 220 | </span></span><span style=display:flex><span> | ||
| 221 | </span></span><span style=display:flex><span> <span style=color:green>// I use random number generator for this example | ||
| 222 | </span></span></span><span style=display:flex><span><span style=color:green></span> <span style=color:green>// but you can use analog or digital inputs from arduino | ||
| 223 | </span></span></span><span style=display:flex><span><span style=color:green></span> String content = String(random(1000)); | ||
| 224 | </span></span><span style=display:flex><span> | ||
| 225 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"POST /api HTTP/1.1"</span>); | ||
| 226 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Connection: close"</span>); | ||
| 227 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Api-Key: "</span> + api_key); | ||
| 228 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Content-Length: "</span> + String(content.length())); | ||
| 229 | </span></span><span style=display:flex><span> client.println(); | ||
| 230 | </span></span><span style=display:flex><span> client.println(content); | ||
| 231 | </span></span><span style=display:flex><span> | ||
| 232 | </span></span><span style=display:flex><span> delay(100); | ||
| 233 | </span></span><span style=display:flex><span> client.stop(); | ||
| 234 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"Data sent successfully ..."</span>); | ||
| 235 | </span></span><span style=display:flex><span> | ||
| 236 | </span></span><span style=display:flex><span> } <span style=color:#00f>else</span> { | ||
| 237 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"Problem sending data ..."</span>); | ||
| 238 | </span></span><span style=display:flex><span> } | ||
| 239 | </span></span><span style=display:flex><span> | ||
| 240 | </span></span><span style=display:flex><span> <span style=color:green>// waits for x seconds and continue looping | ||
| 241 | </span></span></span><span style=display:flex><span><span style=color:green></span> delay(timeout); | ||
| 242 | </span></span><span style=display:flex><span>} | ||
| 243 | </span></span></code></pre><p>As seen from example you can notice that Arduino is generating random integer | ||
| 244 | between [ 0 .. 1000 ]. You can easily replace this with a temperature sensor or | ||
| 245 | any other kind of sensor.<p>Now that we have API under the hood and Arduino is sending demo data we can now | ||
| 246 | focus on data visualization.<h2 id=data-visualization>Data visualization</h2><p>Before we continue we should examine our project folder structure. Currently we | ||
| 247 | only have two files in our project:<p><em>simple-iot-app/</em><ul><li><em>webapp.py</em><li><em>data.db</em></ul><p>We will now add HTML template that will contain CSS and JavaScript code inline | ||
| 248 | for the simplicity reason. And for the bottle framework to be able to scan root | ||
| 249 | application folder for templates we will add <code>bottle.TEMPLATE_PATH.insert(0, "./")</code> in <code>webapp.py</code>. By default bottle framework uses <code>views/</code> | ||
| 250 | subfolder to store templates. This is not the ideal situation and if you will | ||
| 251 | use bottle to develop web applications you should use native behavior and store | ||
| 252 | templates in it's predefined folder. But for the sake of example we will | ||
| 253 | over-ride this. Be careful to fully replace your code with new code that is | ||
| 254 | provided below. Avoid partially replacing code in file :) Also new code for | ||
| 255 | reading data-points is provided in Python example below.<p>First we add new route to our web application. It should be trigger when browser | ||
| 256 | hits root of application <code>http://0.0.0.0:5000/</code>. This route will do nothing | ||
| 257 | more than render <code>frontend.html</code> template. This is done by <code>return bottle.template("frontend.html")</code>. Check code below to further examine how | ||
| 258 | exactly this is done.<p>Now we will expand <code>/api</code> route and use different methods to write or read | ||
| 259 | data-points. For writing data-point we will use POST method and for reading | ||
| 260 | points we will use GET method. GET method will return JSON object with latest | ||
| 261 | readings and historical data.<p>There is a fantastic JavaScript library for plotting time-series charts called | ||
| 262 | <a href=https://www.metricsgraphicsjs.org>MetricsGraphics.js</a> that is based on | ||
| 263 | <a href=https://d3js.org/>D3.js</a> library for visualizing data.<p>Data schema required by MetricsGraphics.js → to achieve this we need to | ||
| 264 | transform data from database into this format:<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>[ | ||
| 265 | </span></span><span style=display:flex><span> { | ||
| 266 | </span></span><span style=display:flex><span> "date": <span style=color:#a31515>"2017-08-11 01:07:20"</span>, | ||
| 267 | </span></span><span style=display:flex><span> "value": 933 | ||
| 268 | </span></span><span style=display:flex><span> }, | ||
| 269 | </span></span><span style=display:flex><span> { | ||
| 270 | </span></span><span style=display:flex><span> "date": <span style=color:#a31515>"2017-08-11 01:07:30"</span>, | ||
| 271 | </span></span><span style=display:flex><span> "value": 743 | ||
| 272 | </span></span><span style=display:flex><span> } | ||
| 273 | </span></span><span style=display:flex><span>] | ||
| 274 | </span></span></code></pre><p>Web application is now complete and we only need <code>frontend.html</code> that we | ||
| 275 | will develop now. If you would try to start web app now and go to root app this | ||
| 276 | will return error because we don't have frontend.html yet.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:green># -*- coding: utf-8 -*-</span> | ||
| 277 | </span></span><span style=display:flex><span> | ||
| 278 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> time | ||
| 279 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 280 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> json | ||
| 281 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> datetime | ||
| 282 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> random | ||
| 283 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> dataset | ||
| 284 | </span></span><span style=display:flex><span> | ||
| 285 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 286 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 287 | </span></span><span style=display:flex><span> | ||
| 288 | </span></span><span style=display:flex><span><span style=color:green># adds root directory as template folder</span> | ||
| 289 | </span></span><span style=display:flex><span>bottle.TEMPLATE_PATH.insert(0, <span style=color:#a31515>"./"</span>) | ||
| 290 | </span></span><span style=display:flex><span> | ||
| 291 | </span></span><span style=display:flex><span><span style=color:green># connects to sqlite database</span> | ||
| 292 | </span></span><span style=display:flex><span><span style=color:green># check_same_thread=False allows using it in multi-threaded mode</span> | ||
| 293 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"db"</span>] = dataset.connect(<span style=color:#a31515>"sqlite:///data.db?check_same_thread=False"</span>) | ||
| 294 | </span></span><span style=display:flex><span> | ||
| 295 | </span></span><span style=display:flex><span><span style=color:green># api key that will be used in Arduino code</span> | ||
| 296 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"api_key"</span>] = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span> | ||
| 297 | </span></span><span style=display:flex><span> | ||
| 298 | </span></span><span style=display:flex><span><span style=color:green># triggered when / is accessed from browser</span> | ||
| 299 | </span></span><span style=display:flex><span><span style=color:green># only accepts GET → no POST allowed</span> | ||
| 300 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/"</span>, method=[<span style=color:#a31515>"GET"</span>]) | ||
| 301 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 302 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.template(<span style=color:#a31515>"frontend.html"</span>) | ||
| 303 | </span></span><span style=display:flex><span> | ||
| 304 | </span></span><span style=display:flex><span><span style=color:green># triggered when /api is accessed from browser</span> | ||
| 305 | </span></span><span style=display:flex><span><span style=color:green># accepts POST and GET</span> | ||
| 306 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/api"</span>, method=[<span style=color:#a31515>"GET"</span>, <span style=color:#a31515>"POST"</span>]) | ||
| 307 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 308 | </span></span><span style=display:flex><span> | ||
| 309 | </span></span><span style=display:flex><span> <span style=color:green># if method is POST then we write datapoint</span> | ||
| 310 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> bottle.request.method == <span style=color:#a31515>"POST"</span>: | ||
| 311 | </span></span><span style=display:flex><span> status = 400 | ||
| 312 | </span></span><span style=display:flex><span> ts = int(time.time()) <span style=color:green># current timestamp</span> | ||
| 313 | </span></span><span style=display:flex><span> value = bottle.request.body.read() <span style=color:green># data from device</span> | ||
| 314 | </span></span><span style=display:flex><span> api_key = bottle.request.get_header(<span style=color:#a31515>"Api-Key"</span>) <span style=color:green># api key from header</span> | ||
| 315 | </span></span><span style=display:flex><span> | ||
| 316 | </span></span><span style=display:flex><span> <span style=color:green># outputs to console recieved data for debug reason</span> | ||
| 317 | </span></span><span style=display:flex><span> print <span style=color:#a31515>">>> </span><span style=color:#a31515>{}</span><span style=color:#a31515> :: </span><span style=color:#a31515>{}</span><span style=color:#a31515>"</span>.format(value, api_key) | ||
| 318 | </span></span><span style=display:flex><span> | ||
| 319 | </span></span><span style=display:flex><span> <span style=color:green># if api_key is correct and value is present</span> | ||
| 320 | </span></span><span style=display:flex><span> <span style=color:green># then writes attribute to point table</span> | ||
| 321 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> api_key == app.config[<span style=color:#a31515>"api_key"</span>] <span style=color:#00f>and</span> value: | ||
| 322 | </span></span><span style=display:flex><span> app.config[<span style=color:#a31515>"db"</span>][<span style=color:#a31515>"point"</span>].insert(dict(ts=ts, value=value)) | ||
| 323 | </span></span><span style=display:flex><span> status = 200 | ||
| 324 | </span></span><span style=display:flex><span> | ||
| 325 | </span></span><span style=display:flex><span> <span style=color:green># we only need to return status</span> | ||
| 326 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.HTTPResponse(status=status, body=<span style=color:#a31515>""</span>) | ||
| 327 | </span></span><span style=display:flex><span> | ||
| 328 | </span></span><span style=display:flex><span> <span style=color:green># if method is GET then we read datapoint</span> | ||
| 329 | </span></span><span style=display:flex><span> <span style=color:#00f>else</span>: | ||
| 330 | </span></span><span style=display:flex><span> response = [] | ||
| 331 | </span></span><span style=display:flex><span> datapoints = app.config[<span style=color:#a31515>"db"</span>][<span style=color:#a31515>"point"</span>].all() | ||
| 332 | </span></span><span style=display:flex><span> | ||
| 333 | </span></span><span style=display:flex><span> <span style=color:#00f>for</span> point <span style=color:#00f>in</span> datapoints: | ||
| 334 | </span></span><span style=display:flex><span> response.append({ | ||
| 335 | </span></span><span style=display:flex><span> <span style=color:#a31515>"date"</span>: datetime.datetime.fromtimestamp(int(point[<span style=color:#a31515>"ts"</span>])).strftime(<span style=color:#a31515>"%Y-%m-</span><span style=color:#a31515>%d</span><span style=color:#a31515> %H:%M:%S"</span>), | ||
| 336 | </span></span><span style=display:flex><span> <span style=color:#a31515>"value"</span>: point[<span style=color:#a31515>"value"</span>] | ||
| 337 | </span></span><span style=display:flex><span> }) | ||
| 338 | </span></span><span style=display:flex><span> | ||
| 339 | </span></span><span style=display:flex><span> bottle.response.content_type = <span style=color:#a31515>"application/json"</span> | ||
| 340 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> json.dumps(response) | ||
| 341 | </span></span><span style=display:flex><span> | ||
| 342 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 343 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 344 | </span></span><span style=display:flex><span> bottle.run( | ||
| 345 | </span></span><span style=display:flex><span> app = app, | ||
| 346 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 347 | </span></span><span style=display:flex><span> port = 5000, | ||
| 348 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 349 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 350 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 351 | </span></span><span style=display:flex><span> ) | ||
| 352 | </span></span></code></pre><p>And now finally we can implement <code>frontend.html</code>. Create file with this name | ||
| 353 | and copy code below. When you are done you can start web application. Steps for | ||
| 354 | this part are listed below the code.<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span><span style=color:#00f><!DOCTYPE html></span> | ||
| 355 | </span></span><span style=display:flex><span><html> | ||
| 356 | </span></span><span style=display:flex><span> | ||
| 357 | </span></span><span style=display:flex><span> <head> | ||
| 358 | </span></span><span style=display:flex><span> <meta charset=<span style=color:#a31515>"utf-8"</span>> | ||
| 359 | </span></span><span style=display:flex><span> <title>Simple IOT application</title> | ||
| 360 | </span></span><span style=display:flex><span> </head> | ||
| 361 | </span></span><span style=display:flex><span> | ||
| 362 | </span></span><span style=display:flex><span> <body> | ||
| 363 | </span></span><span style=display:flex><span> | ||
| 364 | </span></span><span style=display:flex><span> <h1>Simple IOT application</h1> | ||
| 365 | </span></span><span style=display:flex><span> | ||
| 366 | </span></span><span style=display:flex><span> <div class=<span style=color:#a31515>"chart-placeholder"</span>> | ||
| 367 | </span></span><span style=display:flex><span> <div id=<span style=color:#a31515>"chart"</span>></div> | ||
| 368 | </span></span><span style=display:flex><span> </div> | ||
| 369 | </span></span><span style=display:flex><span> | ||
| 370 | </span></span><span style=display:flex><span> <span style=color:green><!-- application main script --></span> | ||
| 371 | </span></span><span style=display:flex><span> <script src=<span style=color:#a31515>"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"</span>></script> | ||
| 372 | </span></span><span style=display:flex><span> <script src=<span style=color:#a31515>"https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"</span>></script> | ||
| 373 | </span></span><span style=display:flex><span> <script src=<span style=color:#a31515>"https://cdnjs.cloudflare.com/ajax/libs/metrics-graphics/2.11.0/metricsgraphics.min.js"</span>></script> | ||
| 374 | </span></span><span style=display:flex><span> <script> | ||
| 375 | </span></span><span style=display:flex><span> <span style=color:#00f>function</span> fetch_and_render() { | ||
| 376 | </span></span><span style=display:flex><span> d3.json(<span style=color:#a31515>"/api"</span>, <span style=color:#00f>function</span>(data) { | ||
| 377 | </span></span><span style=display:flex><span> data = MG.convert.date(data, <span style=color:#a31515>"date"</span>, <span style=color:#a31515>"%Y-%m-%d %H:%M:%S"</span>); | ||
| 378 | </span></span><span style=display:flex><span> MG.data_graphic({ | ||
| 379 | </span></span><span style=display:flex><span> data: data, | ||
| 380 | </span></span><span style=display:flex><span> chart_type: <span style=color:#a31515>"line"</span>, | ||
| 381 | </span></span><span style=display:flex><span> full_width: <span style=color:#00f>true</span>, | ||
| 382 | </span></span><span style=display:flex><span> height: 270, | ||
| 383 | </span></span><span style=display:flex><span> target: document.getElementById(<span style=color:#a31515>"chart"</span>), | ||
| 384 | </span></span><span style=display:flex><span> x_accessor: <span style=color:#a31515>"date"</span>, | ||
| 385 | </span></span><span style=display:flex><span> y_accessor: <span style=color:#a31515>"value"</span> | ||
| 386 | </span></span><span style=display:flex><span> }); | ||
| 387 | </span></span><span style=display:flex><span> }); | ||
| 388 | </span></span><span style=display:flex><span> } | ||
| 389 | </span></span><span style=display:flex><span> window.onload = <span style=color:#00f>function</span>() { | ||
| 390 | </span></span><span style=display:flex><span> <span style=color:green>// initial call for rendering | ||
| 391 | </span></span></span><span style=display:flex><span><span style=color:green></span> fetch_and_render(); | ||
| 392 | </span></span><span style=display:flex><span> | ||
| 393 | </span></span><span style=display:flex><span> <span style=color:green>// updates chart every 5 seconds | ||
| 394 | </span></span></span><span style=display:flex><span><span style=color:green></span> setInterval(<span style=color:#00f>function</span>() { | ||
| 395 | </span></span><span style=display:flex><span> fetch_and_render(); | ||
| 396 | </span></span><span style=display:flex><span> }, 5000); | ||
| 397 | </span></span><span style=display:flex><span> } | ||
| 398 | </span></span><span style=display:flex><span> </script> | ||
| 399 | </span></span><span style=display:flex><span> | ||
| 400 | </span></span><span style=display:flex><span> <span style=color:green><!-- application styles --></span> | ||
| 401 | </span></span><span style=display:flex><span> <style> | ||
| 402 | </span></span><span style=display:flex><span> body { | ||
| 403 | </span></span><span style=display:flex><span> <span style=color:#00f>font</span>: 13<span style=color:#2b91af>px</span> <span style=color:#00f>sans-serif</span>; | ||
| 404 | </span></span><span style=display:flex><span> <span style=color:#00f>padding</span>: 20<span style=color:#2b91af>px</span> 50<span style=color:#2b91af>px</span>; | ||
| 405 | </span></span><span style=display:flex><span> } | ||
| 406 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>chart-placeholder</span> { | ||
| 407 | </span></span><span style=display:flex><span> <span style=color:#00f>border</span>: 2<span style=color:#2b91af>px</span> <span style=color:#00f>solid</span> #ccc; | ||
| 408 | </span></span><span style=display:flex><span> <span style=color:#00f>width</span>: 100<span style=color:#2b91af>%</span>; | ||
| 409 | </span></span><span style=display:flex><span> <span style=color:#00f>user-select</span>: <span style=color:#00f>none</span>; | ||
| 410 | </span></span><span style=display:flex><span> } | ||
| 411 | </span></span><span style=display:flex><span> <span style=color:green>/* chart styles */</span> | ||
| 412 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>mg-line1-color</span> { | ||
| 413 | </span></span><span style=display:flex><span> stroke: <span style=color:#00f>red</span>; | ||
| 414 | </span></span><span style=display:flex><span> stroke-width: 2; | ||
| 415 | </span></span><span style=display:flex><span> } | ||
| 416 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>mg-main-area</span>, .<span style=color:#2b91af>mg-main-line</span> { | ||
| 417 | </span></span><span style=display:flex><span> fill: #fff; | ||
| 418 | </span></span><span style=display:flex><span> } | ||
| 419 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>mg-x-axis</span> line, .<span style=color:#2b91af>mg-y-axis</span> line { | ||
| 420 | </span></span><span style=display:flex><span> stroke: #b3b2b2; | ||
| 421 | </span></span><span style=display:flex><span> stroke-width: 1<span style=color:#2b91af>px</span>; | ||
| 422 | </span></span><span style=display:flex><span> } | ||
| 423 | </span></span><span style=display:flex><span> </style> | ||
| 424 | </span></span><span style=display:flex><span> | ||
| 425 | </span></span><span style=display:flex><span> </body> | ||
| 426 | </span></span><span style=display:flex><span> | ||
| 427 | </span></span><span style=display:flex><span></html> | ||
| 428 | </span></span></code></pre><p>Now the folder structure should look like:<p><em>simple-iot-app/</em><ul><li><em>webapp.py</em><li><em>data.db</em><li><em>frontend.html</em></ul><p>Ok, lets now start application and start feeding it data.<ol><li><code>python webapp.py</code><li>connect Arduino MKR1000 to power source<li>open browser and go to <code>http://0.0.0.0:5000</code></ol><p>If everything goes well you should be seeing new data-points rendered on chart | ||
| 429 | every 5 seconds.<p>If you navigate to <code>http://0.0.0.0:5000</code> you should see rendered chart as | ||
| 430 | shown on picture below.<p><img src=/assets/iot-application/iot-app-output.png alt="Application output"><p>Complete application with all the code is available for | ||
| 431 | <a href=/assets/iot-application/simple-iot-application.zip>download</a>.<h2 id=conclusion>Conclusion</h2><p>I hope this clarifies some aspects of IOT application development. Of course | ||
| 432 | this is a minimal example and is far from what can be done in real life with | ||
| 433 | some further dive into other technologies.<p>If you would like to continue exploring IOT world here are some interesting | ||
| 434 | resources for you to examine:<ul><li><a href=https://www.allaboutcircuits.com/projects/reading-sensors-with-an-arduino/>Reading Sensors with an Arduino</a><li><a href=http://www.hivemq.com/blog/how-to-get-started-with-mqtt>MQTT 101 – How to Get Started with the lightweight IoT Protocol</a><li><a href=https://www.html5rocks.com/en/tutorials/eventsource/basics/>Stream Updates with Server-Sent Events</a><li><a href=http://www.tutorialspoint.com/internet_of_things/>Internet of Things (IoT) Tutorials</a></ul><p>Any comment or additional ideas are welcomed in comments below.</div></div></main><footer><hr><div><h3>Want to comment or have something to add?</h3>You can write me an email at | ||
| 435 | <a href=mailto:m@mitjafelicijan.com>m@mitjafelicijan.com</a> or catch up | ||
| 436 | with me | ||
| 437 | <a href=https://telegram.me/mitjafelicijan target=_blank>on Telegram</a>.</div><hr><p>This website does not track you. Content is made available under | ||
| 438 | the <a href=https://creativecommons.org/licenses/by/4.0/ target=_blank rel=noreferrer>CC BY 4.0 license</a> unless specified | ||
| 439 | otherwise. Blog feed is available as <a href=/index.xml target=_blank>RSS feed</a>.</footer><script src=https://cdn.usefathom.com/script.js data-site=XHQARKXP defer></script> \ No newline at end of file | ||
