diff options
Diffstat (limited to 'public/simple-iot-application.html')
| -rwxr-xr-x | public/simple-iot-application.html | 472 |
1 files changed, 0 insertions, 472 deletions
diff --git a/public/simple-iot-application.html b/public/simple-iot-application.html deleted file mode 100755 index b242397..0000000 --- a/public/simple-iot-application.html +++ /dev/null | |||
| @@ -1,472 +0,0 @@ | |||
| 1 | <!doctype html><html lang=en-us><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=generator content="JBMAFP - github.com/mitjafelicijan/jbmafp"><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."><meta name=author content="Mitja Felicijan"><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>:root{--border-color:gainsboro;--border-size:2px;--link-color:blue;--bg-color:#eee}*::selection{background:var(--link-color);color:#fff}*::-moz-selection{background:var(--link-color);color:#fff}*::-webkit-selection{background:var(--link-color);color:#fff}body{padding:2.5rem;max-width:1900px;background:#fff;font-family:sans-serif;line-height:1.35rem;font-size:16px}hr{border:0;border-bottom:var(--border-size)solid var(--border-color);margin-block-start:1.5rem}a{color:var(--link-color);text-decoration:none}h1,h2,h3{line-height:initial}h1{font-size:xx-large}footer{margin-block-start:2rem}cap{text-transform:capitalize}blockquote{font-style:italic}table{max-width:100%;border:var(--border-size)solid var(--border-color);border-collapse:separate;border-spacing:0}table thead tr th{border-bottom:var(--border-size)solid var(--border-color);text-align:left}table th,table td{padding:.5em .8em}ul.list li{padding:.2em 0}ul{line-height:1.35em}pre{text-wrap:nowrap;overflow-x:auto;padding:0 1em;border:var(--border-size)solid var(--border-color)}code{padding:0 3px;font-size:14px;border:0;background:var(--bg-color)}pre code{line-height:1.3em;background:#fff}pre,code,pre *,code *{font-family:monospace}figure{margin-inline-start:0;margin-inline-end:0}figcaption{width:800px;max-width:100%;text-align:center}figcaption p{margin:.3em 0 1.5em;font-style:italic}img,video,audio{width:800px;max-width:100%;border:var(--border-size)solid var(--border-color);padding:.5em}header nav{display:flex;gap:.9rem}article iframe{margin:0!important}audio::-webkit-media-controls-enclosure{border-radius:0}@media only screen and (max-width:600px){body{padding:.5em;word-wrap:break-word}header nav{gap:.7rem}header nav .hob{display:none}a{word-wrap:break-word}img,video,audio{padding:0}}</style><header><nav class=main itemscope itemtype=http://schema.org/SiteNavigationElement role=navigation aria-label="Main navigation"><a href=/>Home</a> | ||
| 2 | <a href=/#posts>Posts</a> | ||
| 3 | <a href=/#notes>Notes</a> | ||
| 4 | <a href=/#sideprojects class=hob>Side Projects</a> | ||
| 5 | <a href=/vault.html>Vault</a> | ||
| 6 | <a href=https://github.com/mitjafelicijan target=_blank>Code</a> | ||
| 7 | <a href=/mitjafelicijan.pgp.pub.txt target=_blank class=hob>PGP</a> | ||
| 8 | <a href=/curriculum-vitae.html>CV</a> | ||
| 9 | <a href=/index.xml target=_blank class=hob>RSS</a></nav></header><main role=main><article itemtype=http://schema.org/Article><h1 itemtype=headline>Simple IOT application supported by real-time monitoring and data history</h1><p><cap>post</cap>, Aug 11, 2017 on <a href=https://mitjafelicijan.com>Mitja Felicijan's blog</a><div><h2 id=initial-thoughts>Initial thoughts</h2><p>I have been developing these kind of application for the better part of my last | ||
| 10 | 5 years and people keep asking me how to approach developing such application | ||
| 11 | and I will give a try explaining it here.<p>IOT applications are really no different than any other kind of applications. | ||
| 12 | We have data that needs to be collected and visualized in some form of tables or | ||
| 13 | charts. The main difference here is that most of the times these data is | ||
| 14 | collected by some kind of device foreign to developer that mainly operates in | ||
| 15 | 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 | ||
| 16 | default but for the sake of example we will be using commonly known Arduino with | ||
| 17 | wireless module already on the board → <a href=https://store.arduino.cc/arduino-mkr1000>Arduino | ||
| 18 | MKR1000</a>.<p>In order to make this little project as accessible to others as possible I will | ||
| 19 | try to make it as inexpensive as possible. And by this I mean that I will avoid | ||
| 20 | using hosted virtual servers and will be using my own laptop as a server. But | ||
| 21 | you must buy Arduino MKR1000 to follow steps below. But if you would want to | ||
| 22 | deploy this software I would suggest using | ||
| 23 | <a href=https://www.digitalocean.com>DigitalOcean</a> → smallest VPS is only per month | ||
| 24 | making this one of the most affordable option out there. Please notice that this | ||
| 25 | software will not run on stock web hosting that only supports LAMP (Linux, | ||
| 26 | Apache, MySQL, and PHP).<p>But before we begin please take notice that this is strictly experimental code | ||
| 27 | and not well optimized and there are much better ways in handling some aspects | ||
| 28 | of the application but that requires much deeper knowledge of technology that is | ||
| 29 | 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 | ||
| 30 | to API and another to serving HTML with chart.<p>Schema below represents what we will try to achieve and how different parts | ||
| 31 | correlates to each other.<figure><img src=/posts/iot-application/simple-iot-application-overview.svg alt=Overview></figure><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 | ||
| 32 | Framework</a>. It is a single file web framework | ||
| 33 | that seriously simplifies working with routes, templating and has built-in web | ||
| 34 | server that satisfies our need in this case.<p>First we need to install bottle package. This can be done by downloading | ||
| 35 | <code>bottle.py</code> and placing it in the root of your application or by using pip | ||
| 36 | software <code>pip install bottle --user</code>.<p>If you are using Linux or MacOS then Python is already installed. If you will | ||
| 37 | try to test this on Windows please install <a href=https://www.python.org/downloads/windows/>Python for | ||
| 38 | Windows</a>. There may be some problems | ||
| 39 | with path when you will try to launch <code>python webapp.py</code> so please take care | ||
| 40 | 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 | ||
| 41 | <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> | ||
| 42 | </span></span><span style=display:flex><span> | ||
| 43 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 44 | </span></span><span style=display:flex><span> | ||
| 45 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 46 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 47 | </span></span><span style=display:flex><span> | ||
| 48 | </span></span><span style=display:flex><span><span style=color:green># triggered when / is accessed from browser</span> | ||
| 49 | </span></span><span style=display:flex><span><span style=color:green># only accepts GET → no POST allowed</span> | ||
| 50 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/"</span>, method=[<span style=color:#a31515>"GET"</span>]) | ||
| 51 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 52 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> <span style=color:#a31515>"howdy from python"</span> | ||
| 53 | </span></span><span style=display:flex><span> | ||
| 54 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 55 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 56 | </span></span><span style=display:flex><span> bottle.run( | ||
| 57 | </span></span><span style=display:flex><span> app = app, | ||
| 58 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 59 | </span></span><span style=display:flex><span> port = 5000, | ||
| 60 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 61 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 62 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 63 | </span></span><span style=display:flex><span> ) | ||
| 64 | </span></span></code></pre><p>To run this simple application you should open command prompt or terminal on | ||
| 65 | 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 | ||
| 66 | <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 | ||
| 67 | root to run your app this will present a problem. The TCP/IP port numbers below | ||
| 68 | 1024 are privileged ports → this is a security feature. So in order of | ||
| 69 | 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 | ||
| 70 | below will work otherwise.<p>We use 0.0.0.0 as default host so that this app is available over your local | ||
| 71 | network. If you find your local ip <code>ifconfig</code> and try accessing this site | ||
| 72 | with your phone (if on same network/router as your machine) this should work as | ||
| 73 | well (example of such ip <code>http://192.168.1.15:5000</code>). This is a must have | ||
| 74 | 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 | ||
| 75 | all this can not be written here but to just establish some basic security → you | ||
| 76 | should always use SSL with your application. Some fantastic free certificates | ||
| 77 | are available by <a href=https://letsencrypt.org>Let's Encrypt - Free SSL/TLS | ||
| 78 | Certificates</a>. With SSL certificate installed you | ||
| 79 | should then make use of HTTP headers and send your "API key" via a header. If | ||
| 80 | your key is send via header then this key is encrypted by SSL and send encrypted | ||
| 81 | over the network. Never send your api keys by GET parameter like | ||
| 82 | <code>http://example.com/?api_key=somekeyvalue</code>. The problem that this kind of | ||
| 83 | 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 | ||
| 84 | Application Security Best | ||
| 85 | Practices</a>. Please | ||
| 86 | 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 | ||
| 87 | SQLite3 because it plays well with Python and can store quite large amount of | ||
| 88 | able to write data received by API to local storage. For example use I will use | ||
| 89 | data. I have been using it to collect gigabytes of data in a single database | ||
| 90 | 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 | ||
| 91 | people</a>. This package | ||
| 92 | abstracts SQL and simplifies writing and reading data from database. You should | ||
| 93 | 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 | ||
| 94 | using <a href=https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm>Restlet Client for Google | ||
| 95 | Chrome</a>. | ||
| 96 | 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 | ||
| 97 | <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> | ||
| 98 | </span></span><span style=display:flex><span> | ||
| 99 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> time | ||
| 100 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 101 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> random | ||
| 102 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> dataset | ||
| 103 | </span></span><span style=display:flex><span> | ||
| 104 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 105 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 106 | </span></span><span style=display:flex><span> | ||
| 107 | </span></span><span style=display:flex><span><span style=color:green># connects to sqlite database</span> | ||
| 108 | </span></span><span style=display:flex><span><span style=color:green># check_same_thread=False allows using it in multi-threaded mode</span> | ||
| 109 | </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>) | ||
| 110 | </span></span><span style=display:flex><span> | ||
| 111 | </span></span><span style=display:flex><span><span style=color:green># api key that will be used in Arduino code</span> | ||
| 112 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"api_key"</span>] = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span> | ||
| 113 | </span></span><span style=display:flex><span> | ||
| 114 | </span></span><span style=display:flex><span><span style=color:green># triggered when /api is accessed from browser</span> | ||
| 115 | </span></span><span style=display:flex><span><span style=color:green># only accepts POST → no GET allowed</span> | ||
| 116 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/api"</span>, method=[<span style=color:#a31515>"POST"</span>]) | ||
| 117 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 118 | </span></span><span style=display:flex><span> status = 400 | ||
| 119 | </span></span><span style=display:flex><span> ts = int(time.time()) <span style=color:green># current timestamp</span> | ||
| 120 | </span></span><span style=display:flex><span> value = bottle.request.body.read() <span style=color:green># data from device</span> | ||
| 121 | </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> | ||
| 122 | </span></span><span style=display:flex><span> | ||
| 123 | </span></span><span style=display:flex><span> <span style=color:green># outputs to console received data for debug reason</span> | ||
| 124 | </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) | ||
| 125 | </span></span><span style=display:flex><span> | ||
| 126 | </span></span><span style=display:flex><span> <span style=color:green># if api_key is correct and value is present</span> | ||
| 127 | </span></span><span style=display:flex><span> <span style=color:green># then writes attribute to point table</span> | ||
| 128 | </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: | ||
| 129 | </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)) | ||
| 130 | </span></span><span style=display:flex><span> status = 200 | ||
| 131 | </span></span><span style=display:flex><span> | ||
| 132 | </span></span><span style=display:flex><span> <span style=color:green># we only need to return status</span> | ||
| 133 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.HTTPResponse(status=status, body=<span style=color:#a31515>""</span>) | ||
| 134 | </span></span><span style=display:flex><span> | ||
| 135 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 136 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 137 | </span></span><span style=display:flex><span> bottle.run( | ||
| 138 | </span></span><span style=display:flex><span> app = app, | ||
| 139 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 140 | </span></span><span style=display:flex><span> port = 5000, | ||
| 141 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 142 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 143 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 144 | </span></span><span style=display:flex><span> ) | ||
| 145 | </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 | ||
| 146 | available via POST method on /api route.<p>After testing the service with Restlet Client you should be able to view your | ||
| 147 | data in a database file <code>data.db</code>.<figure><img src=/posts/iot-application/iot-rest-example.png alt="REST settings example"></figure><p>You can also check the contents of new database file by using desktop client | ||
| 148 | for SQLite → <a href=http://sqlitebrowser.org/>DB Browser for SQLite</a>.<figure><img src=/posts/iot-application/iot-sqlite-db.png alt="SQLite database example"></figure><p>Table structure is as simple as it can be. We have ts (timestamp) and value | ||
| 149 | (value from Arduino). As you can see timestamp is generated on API side. If you | ||
| 150 | would happen to have atomic clock on Arduino it would be then better to generate | ||
| 151 | and send timestamp with the value. This would be particularity useful if we | ||
| 152 | would be collecting sensor data at a higher frequency and then sending this data | ||
| 153 | in bulk to API.<p>If you will deploy this app with uWSGI and multi-threaded, use DSN (Data Source | ||
| 154 | 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 | ||
| 155 | unwanted people can not post data to your database can we proceed further and | ||
| 156 | 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 | ||
| 157 | you have ever done any work with Arduino you should know that you also need | ||
| 158 | <a href=https://www.arduino.cc/en/Main/Software>Arduino IDE</a>. On provided link you | ||
| 159 | should be able to download and install IDE. Once that task is completed and you | ||
| 160 | 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 | ||
| 161 | <a href=https://www.arduino.cc/en/Reference/WiFi101>WiFi101 library</a> in Arduino IDE. | ||
| 162 | 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 | ||
| 163 | your code make sure you have run Python web application. Then change settings | ||
| 164 | for wifi, api endpoint and api_key. If by some reason code bellow doesn't work | ||
| 165 | 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. | ||
| 166 | 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> | ||
| 167 | </span></span></span><span style=display:flex><span><span style=color:#00f></span> | ||
| 168 | </span></span><span style=display:flex><span><span style=color:green>// wifi settings | ||
| 169 | </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>; | ||
| 170 | </span></span><span style=display:flex><span><span style=color:#2b91af>char</span> pass[] = <span style=color:#a31515>"ssid-password"</span>; | ||
| 171 | </span></span><span style=display:flex><span> | ||
| 172 | </span></span><span style=display:flex><span><span style=color:green>// api server enpoint | ||
| 173 | </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>; | ||
| 174 | </span></span><span style=display:flex><span><span style=color:#2b91af>int</span> port = 5000; | ||
| 175 | </span></span><span style=display:flex><span> | ||
| 176 | </span></span><span style=display:flex><span><span style=color:green>// api key that must be the same as the one in Python code | ||
| 177 | </span></span></span><span style=display:flex><span><span style=color:green></span>String api_key = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span>; | ||
| 178 | </span></span><span style=display:flex><span> | ||
| 179 | </span></span><span style=display:flex><span><span style=color:green>// frequency data is sent in ms - every 5 seconds | ||
| 180 | </span></span></span><span style=display:flex><span><span style=color:green></span><span style=color:#2b91af>int</span> timeout = 1000 * 5; | ||
| 181 | </span></span><span style=display:flex><span> | ||
| 182 | </span></span><span style=display:flex><span><span style=color:#2b91af>int</span> status = WL_IDLE_STATUS; | ||
| 183 | </span></span><span style=display:flex><span> | ||
| 184 | </span></span><span style=display:flex><span><span style=color:#2b91af>void</span> setup() { | ||
| 185 | </span></span><span style=display:flex><span> | ||
| 186 | </span></span><span style=display:flex><span> <span style=color:green>// initialize serial and wait for port to open: | ||
| 187 | </span></span></span><span style=display:flex><span><span style=color:green></span> Serial.begin(9600); | ||
| 188 | </span></span><span style=display:flex><span> delay(1000); | ||
| 189 | </span></span><span style=display:flex><span> | ||
| 190 | </span></span><span style=display:flex><span> <span style=color:green>// check for the presence of the shield | ||
| 191 | </span></span></span><span style=display:flex><span><span style=color:green></span> <span style=color:#00f>if</span> (WiFi.status() == WL_NO_SHIELD) { | ||
| 192 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"WiFi shield not present"</span>); | ||
| 193 | </span></span><span style=display:flex><span> <span style=color:#00f>while</span> (true); | ||
| 194 | </span></span><span style=display:flex><span> } | ||
| 195 | </span></span><span style=display:flex><span> | ||
| 196 | </span></span><span style=display:flex><span> <span style=color:green>// attempt to connect to wifi network | ||
| 197 | </span></span></span><span style=display:flex><span><span style=color:green></span> <span style=color:#00f>while</span> (status != WL_CONNECTED) { | ||
| 198 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"Attempting to connect to SSID: "</span>); | ||
| 199 | </span></span><span style=display:flex><span> Serial.println(ssid); | ||
| 200 | </span></span><span style=display:flex><span> status = WiFi.begin(ssid, pass); | ||
| 201 | </span></span><span style=display:flex><span> <span style=color:green>// wait 10 seconds for connection | ||
| 202 | </span></span></span><span style=display:flex><span><span style=color:green></span> delay(10000); | ||
| 203 | </span></span><span style=display:flex><span> } | ||
| 204 | </span></span><span style=display:flex><span> | ||
| 205 | </span></span><span style=display:flex><span> <span style=color:green>// output wifi status to serial monitor | ||
| 206 | </span></span></span><span style=display:flex><span><span style=color:green></span> Serial.print(<span style=color:#a31515>"SSID: "</span>); | ||
| 207 | </span></span><span style=display:flex><span> Serial.println(WiFi.SSID()); | ||
| 208 | </span></span><span style=display:flex><span> | ||
| 209 | </span></span><span style=display:flex><span> IPAddress ip = WiFi.localIP(); | ||
| 210 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"IP Address: "</span>); | ||
| 211 | </span></span><span style=display:flex><span> Serial.println(ip); | ||
| 212 | </span></span><span style=display:flex><span> | ||
| 213 | </span></span><span style=display:flex><span> <span style=color:#2b91af>long</span> rssi = WiFi.RSSI(); | ||
| 214 | </span></span><span style=display:flex><span> Serial.print(<span style=color:#a31515>"signal strength (RSSI):"</span>); | ||
| 215 | </span></span><span style=display:flex><span> Serial.print(rssi); | ||
| 216 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>" dBm"</span>); | ||
| 217 | </span></span><span style=display:flex><span>} | ||
| 218 | </span></span><span style=display:flex><span> | ||
| 219 | </span></span><span style=display:flex><span><span style=color:#2b91af>void</span> loop() { | ||
| 220 | </span></span><span style=display:flex><span> WiFiClient client; | ||
| 221 | </span></span><span style=display:flex><span> | ||
| 222 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> (client.connect(server, port)) { | ||
| 223 | </span></span><span style=display:flex><span> | ||
| 224 | </span></span><span style=display:flex><span> <span style=color:green>// I use random number generator for this example | ||
| 225 | </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 | ||
| 226 | </span></span></span><span style=display:flex><span><span style=color:green></span> String content = String(random(1000)); | ||
| 227 | </span></span><span style=display:flex><span> | ||
| 228 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"POST /api HTTP/1.1"</span>); | ||
| 229 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Connection: close"</span>); | ||
| 230 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Api-Key: "</span> + api_key); | ||
| 231 | </span></span><span style=display:flex><span> client.println(<span style=color:#a31515>"Content-Length: "</span> + String(content.length())); | ||
| 232 | </span></span><span style=display:flex><span> client.println(); | ||
| 233 | </span></span><span style=display:flex><span> client.println(content); | ||
| 234 | </span></span><span style=display:flex><span> | ||
| 235 | </span></span><span style=display:flex><span> delay(100); | ||
| 236 | </span></span><span style=display:flex><span> client.stop(); | ||
| 237 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"Data sent successfully ..."</span>); | ||
| 238 | </span></span><span style=display:flex><span> | ||
| 239 | </span></span><span style=display:flex><span> } <span style=color:#00f>else</span> { | ||
| 240 | </span></span><span style=display:flex><span> Serial.println(<span style=color:#a31515>"Problem sending data ..."</span>); | ||
| 241 | </span></span><span style=display:flex><span> } | ||
| 242 | </span></span><span style=display:flex><span> | ||
| 243 | </span></span><span style=display:flex><span> <span style=color:green>// waits for x seconds and continue looping | ||
| 244 | </span></span></span><span style=display:flex><span><span style=color:green></span> delay(timeout); | ||
| 245 | </span></span><span style=display:flex><span>} | ||
| 246 | </span></span></code></pre><p>As seen from example you can notice that Arduino is generating random integer | ||
| 247 | between [ 0 .. 1000 ]. You can easily replace this with a temperature sensor or | ||
| 248 | any other kind of sensor.<p>Now that we have API under the hood and Arduino is sending demo data we can now | ||
| 249 | focus on data visualization.<h2 id=data-visualization>Data visualization</h2><p>Before we continue we should examine our project folder structure. Currently we | ||
| 250 | 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 | ||
| 251 | for the simplicity reason. And for the bottle framework to be able to scan root | ||
| 252 | 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> | ||
| 253 | subfolder to store templates. This is not the ideal situation and if you will | ||
| 254 | use bottle to develop web applications you should use native behavior and store | ||
| 255 | templates in it's predefined folder. But for the sake of example we will | ||
| 256 | over-ride this. Be careful to fully replace your code with new code that is | ||
| 257 | provided below. Avoid partially replacing code in file :) Also new code for | ||
| 258 | 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 | ||
| 259 | hits root of application <code>http://0.0.0.0:5000/</code>. This route will do nothing | ||
| 260 | 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 | ||
| 261 | exactly this is done.<p>Now we will expand <code>/api</code> route and use different methods to write or read | ||
| 262 | data-points. For writing data-point we will use POST method and for reading | ||
| 263 | points we will use GET method. GET method will return JSON object with latest | ||
| 264 | readings and historical data.<p>There is a fantastic JavaScript library for plotting time-series charts called | ||
| 265 | <a href=https://www.metricsgraphicsjs.org>MetricsGraphics.js</a> that is based on | ||
| 266 | <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 | ||
| 267 | transform data from database into this format:<pre tabindex=0 style=background-color:#fff><code><span style=display:flex><span>[ | ||
| 268 | </span></span><span style=display:flex><span> { | ||
| 269 | </span></span><span style=display:flex><span> "date": <span style=color:#a31515>"2017-08-11 01:07:20"</span>, | ||
| 270 | </span></span><span style=display:flex><span> "value": 933 | ||
| 271 | </span></span><span style=display:flex><span> }, | ||
| 272 | </span></span><span style=display:flex><span> { | ||
| 273 | </span></span><span style=display:flex><span> "date": <span style=color:#a31515>"2017-08-11 01:07:30"</span>, | ||
| 274 | </span></span><span style=display:flex><span> "value": 743 | ||
| 275 | </span></span><span style=display:flex><span> } | ||
| 276 | </span></span><span style=display:flex><span>] | ||
| 277 | </span></span></code></pre><p>Web application is now complete and we only need <code>frontend.html</code> that we | ||
| 278 | will develop now. If you would try to start web app now and go to root app this | ||
| 279 | 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> | ||
| 280 | </span></span><span style=display:flex><span> | ||
| 281 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> time | ||
| 282 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> bottle | ||
| 283 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> json | ||
| 284 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> datetime | ||
| 285 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> random | ||
| 286 | </span></span><span style=display:flex><span><span style=color:#00f>import</span> dataset | ||
| 287 | </span></span><span style=display:flex><span> | ||
| 288 | </span></span><span style=display:flex><span><span style=color:green># initializing bottle app</span> | ||
| 289 | </span></span><span style=display:flex><span>app = bottle.Bottle() | ||
| 290 | </span></span><span style=display:flex><span> | ||
| 291 | </span></span><span style=display:flex><span><span style=color:green># adds root directory as template folder</span> | ||
| 292 | </span></span><span style=display:flex><span>bottle.TEMPLATE_PATH.insert(0, <span style=color:#a31515>"./"</span>) | ||
| 293 | </span></span><span style=display:flex><span> | ||
| 294 | </span></span><span style=display:flex><span><span style=color:green># connects to sqlite database</span> | ||
| 295 | </span></span><span style=display:flex><span><span style=color:green># check_same_thread=False allows using it in multi-threaded mode</span> | ||
| 296 | </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>) | ||
| 297 | </span></span><span style=display:flex><span> | ||
| 298 | </span></span><span style=display:flex><span><span style=color:green># api key that will be used in Arduino code</span> | ||
| 299 | </span></span><span style=display:flex><span>app.config[<span style=color:#a31515>"api_key"</span>] = <span style=color:#a31515>"JtF2aUE5SGHfVJBCG5SH"</span> | ||
| 300 | </span></span><span style=display:flex><span> | ||
| 301 | </span></span><span style=display:flex><span><span style=color:green># triggered when / is accessed from browser</span> | ||
| 302 | </span></span><span style=display:flex><span><span style=color:green># only accepts GET → no POST allowed</span> | ||
| 303 | </span></span><span style=display:flex><span>@app.route(<span style=color:#a31515>"/"</span>, method=[<span style=color:#a31515>"GET"</span>]) | ||
| 304 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 305 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.template(<span style=color:#a31515>"frontend.html"</span>) | ||
| 306 | </span></span><span style=display:flex><span> | ||
| 307 | </span></span><span style=display:flex><span><span style=color:green># triggered when /api is accessed from browser</span> | ||
| 308 | </span></span><span style=display:flex><span><span style=color:green># accepts POST and GET</span> | ||
| 309 | </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>]) | ||
| 310 | </span></span><span style=display:flex><span><span style=color:#00f>def</span> route_default(): | ||
| 311 | </span></span><span style=display:flex><span> | ||
| 312 | </span></span><span style=display:flex><span> <span style=color:green># if method is POST then we write datapoint</span> | ||
| 313 | </span></span><span style=display:flex><span> <span style=color:#00f>if</span> bottle.request.method == <span style=color:#a31515>"POST"</span>: | ||
| 314 | </span></span><span style=display:flex><span> status = 400 | ||
| 315 | </span></span><span style=display:flex><span> ts = int(time.time()) <span style=color:green># current timestamp</span> | ||
| 316 | </span></span><span style=display:flex><span> value = bottle.request.body.read() <span style=color:green># data from device</span> | ||
| 317 | </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> | ||
| 318 | </span></span><span style=display:flex><span> | ||
| 319 | </span></span><span style=display:flex><span> <span style=color:green># outputs to console recieved data for debug reason</span> | ||
| 320 | </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) | ||
| 321 | </span></span><span style=display:flex><span> | ||
| 322 | </span></span><span style=display:flex><span> <span style=color:green># if api_key is correct and value is present</span> | ||
| 323 | </span></span><span style=display:flex><span> <span style=color:green># then writes attribute to point table</span> | ||
| 324 | </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: | ||
| 325 | </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)) | ||
| 326 | </span></span><span style=display:flex><span> status = 200 | ||
| 327 | </span></span><span style=display:flex><span> | ||
| 328 | </span></span><span style=display:flex><span> <span style=color:green># we only need to return status</span> | ||
| 329 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> bottle.HTTPResponse(status=status, body=<span style=color:#a31515>""</span>) | ||
| 330 | </span></span><span style=display:flex><span> | ||
| 331 | </span></span><span style=display:flex><span> <span style=color:green># if method is GET then we read datapoint</span> | ||
| 332 | </span></span><span style=display:flex><span> <span style=color:#00f>else</span>: | ||
| 333 | </span></span><span style=display:flex><span> response = [] | ||
| 334 | </span></span><span style=display:flex><span> datapoints = app.config[<span style=color:#a31515>"db"</span>][<span style=color:#a31515>"point"</span>].all() | ||
| 335 | </span></span><span style=display:flex><span> | ||
| 336 | </span></span><span style=display:flex><span> <span style=color:#00f>for</span> point <span style=color:#00f>in</span> datapoints: | ||
| 337 | </span></span><span style=display:flex><span> response.append({ | ||
| 338 | </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>), | ||
| 339 | </span></span><span style=display:flex><span> <span style=color:#a31515>"value"</span>: point[<span style=color:#a31515>"value"</span>] | ||
| 340 | </span></span><span style=display:flex><span> }) | ||
| 341 | </span></span><span style=display:flex><span> | ||
| 342 | </span></span><span style=display:flex><span> bottle.response.content_type = <span style=color:#a31515>"application/json"</span> | ||
| 343 | </span></span><span style=display:flex><span> <span style=color:#00f>return</span> json.dumps(response) | ||
| 344 | </span></span><span style=display:flex><span> | ||
| 345 | </span></span><span style=display:flex><span><span style=color:green># starting server on http://0.0.0.0:5000</span> | ||
| 346 | </span></span><span style=display:flex><span><span style=color:#00f>if</span> __name__ == <span style=color:#a31515>"__main__"</span>: | ||
| 347 | </span></span><span style=display:flex><span> bottle.run( | ||
| 348 | </span></span><span style=display:flex><span> app = app, | ||
| 349 | </span></span><span style=display:flex><span> host = <span style=color:#a31515>"0.0.0.0"</span>, | ||
| 350 | </span></span><span style=display:flex><span> port = 5000, | ||
| 351 | </span></span><span style=display:flex><span> debug = <span style=color:#00f>True</span>, | ||
| 352 | </span></span><span style=display:flex><span> reloader = <span style=color:#00f>True</span>, | ||
| 353 | </span></span><span style=display:flex><span> catchall = <span style=color:#00f>True</span>, | ||
| 354 | </span></span><span style=display:flex><span> ) | ||
| 355 | </span></span></code></pre><p>And now finally we can implement <code>frontend.html</code>. Create file with this name | ||
| 356 | and copy code below. When you are done you can start web application. Steps for | ||
| 357 | 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> | ||
| 358 | </span></span><span style=display:flex><span><html> | ||
| 359 | </span></span><span style=display:flex><span> | ||
| 360 | </span></span><span style=display:flex><span> <head> | ||
| 361 | </span></span><span style=display:flex><span> <meta charset=<span style=color:#a31515>"utf-8"</span>> | ||
| 362 | </span></span><span style=display:flex><span> <title>Simple IOT application</title> | ||
| 363 | </span></span><span style=display:flex><span> </head> | ||
| 364 | </span></span><span style=display:flex><span> | ||
| 365 | </span></span><span style=display:flex><span> <body> | ||
| 366 | </span></span><span style=display:flex><span> | ||
| 367 | </span></span><span style=display:flex><span> <h1>Simple IOT application</h1> | ||
| 368 | </span></span><span style=display:flex><span> | ||
| 369 | </span></span><span style=display:flex><span> <div class=<span style=color:#a31515>"chart-placeholder"</span>> | ||
| 370 | </span></span><span style=display:flex><span> <div id=<span style=color:#a31515>"chart"</span>></div> | ||
| 371 | </span></span><span style=display:flex><span> </div> | ||
| 372 | </span></span><span style=display:flex><span> | ||
| 373 | </span></span><span style=display:flex><span> <span style=color:green><!-- application main script --></span> | ||
| 374 | </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> | ||
| 375 | </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> | ||
| 376 | </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> | ||
| 377 | </span></span><span style=display:flex><span> <script> | ||
| 378 | </span></span><span style=display:flex><span> <span style=color:#00f>function</span> fetch_and_render() { | ||
| 379 | </span></span><span style=display:flex><span> d3.json(<span style=color:#a31515>"/api"</span>, <span style=color:#00f>function</span>(data) { | ||
| 380 | </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>); | ||
| 381 | </span></span><span style=display:flex><span> MG.data_graphic({ | ||
| 382 | </span></span><span style=display:flex><span> data: data, | ||
| 383 | </span></span><span style=display:flex><span> chart_type: <span style=color:#a31515>"line"</span>, | ||
| 384 | </span></span><span style=display:flex><span> full_width: <span style=color:#00f>true</span>, | ||
| 385 | </span></span><span style=display:flex><span> height: 270, | ||
| 386 | </span></span><span style=display:flex><span> target: document.getElementById(<span style=color:#a31515>"chart"</span>), | ||
| 387 | </span></span><span style=display:flex><span> x_accessor: <span style=color:#a31515>"date"</span>, | ||
| 388 | </span></span><span style=display:flex><span> y_accessor: <span style=color:#a31515>"value"</span> | ||
| 389 | </span></span><span style=display:flex><span> }); | ||
| 390 | </span></span><span style=display:flex><span> }); | ||
| 391 | </span></span><span style=display:flex><span> } | ||
| 392 | </span></span><span style=display:flex><span> window.onload = <span style=color:#00f>function</span>() { | ||
| 393 | </span></span><span style=display:flex><span> <span style=color:green>// initial call for rendering | ||
| 394 | </span></span></span><span style=display:flex><span><span style=color:green></span> fetch_and_render(); | ||
| 395 | </span></span><span style=display:flex><span> | ||
| 396 | </span></span><span style=display:flex><span> <span style=color:green>// updates chart every 5 seconds | ||
| 397 | </span></span></span><span style=display:flex><span><span style=color:green></span> setInterval(<span style=color:#00f>function</span>() { | ||
| 398 | </span></span><span style=display:flex><span> fetch_and_render(); | ||
| 399 | </span></span><span style=display:flex><span> }, 5000); | ||
| 400 | </span></span><span style=display:flex><span> } | ||
| 401 | </span></span><span style=display:flex><span> </script> | ||
| 402 | </span></span><span style=display:flex><span> | ||
| 403 | </span></span><span style=display:flex><span> <span style=color:green><!-- application styles --></span> | ||
| 404 | </span></span><span style=display:flex><span> <style> | ||
| 405 | </span></span><span style=display:flex><span> body { | ||
| 406 | </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>; | ||
| 407 | </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>; | ||
| 408 | </span></span><span style=display:flex><span> } | ||
| 409 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>chart-placeholder</span> { | ||
| 410 | </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; | ||
| 411 | </span></span><span style=display:flex><span> <span style=color:#00f>width</span>: 100<span style=color:#2b91af>%</span>; | ||
| 412 | </span></span><span style=display:flex><span> <span style=color:#00f>user-select</span>: <span style=color:#00f>none</span>; | ||
| 413 | </span></span><span style=display:flex><span> } | ||
| 414 | </span></span><span style=display:flex><span> <span style=color:green>/* chart styles */</span> | ||
| 415 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>mg-line1-color</span> { | ||
| 416 | </span></span><span style=display:flex><span> stroke: <span style=color:#00f>red</span>; | ||
| 417 | </span></span><span style=display:flex><span> stroke-width: 2; | ||
| 418 | </span></span><span style=display:flex><span> } | ||
| 419 | </span></span><span style=display:flex><span> .<span style=color:#2b91af>mg-main-area</span>, .<span style=color:#2b91af>mg-main-line</span> { | ||
| 420 | </span></span><span style=display:flex><span> fill: #fff; | ||
| 421 | </span></span><span style=display:flex><span> } | ||
| 422 | </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 { | ||
| 423 | </span></span><span style=display:flex><span> stroke: #b3b2b2; | ||
| 424 | </span></span><span style=display:flex><span> stroke-width: 1<span style=color:#2b91af>px</span>; | ||
| 425 | </span></span><span style=display:flex><span> } | ||
| 426 | </span></span><span style=display:flex><span> </style> | ||
| 427 | </span></span><span style=display:flex><span> | ||
| 428 | </span></span><span style=display:flex><span> </body> | ||
| 429 | </span></span><span style=display:flex><span> | ||
| 430 | </span></span><span style=display:flex><span></html> | ||
| 431 | </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 | ||
| 432 | every 5 seconds.<p>If you navigate to <code>http://0.0.0.0:5000</code> you should see rendered chart as | ||
| 433 | shown on picture below.<figure><img src=/posts/iot-application/iot-app-output.png alt="Application output"></figure><p>Complete application with all the code is available for | ||
| 434 | <a href=/posts/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 | ||
| 435 | this is a minimal example and is far from what can be done in real life with | ||
| 436 | some further dive into other technologies.<p>If you would like to continue exploring IOT world here are some interesting | ||
| 437 | 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></article></main><section><hr><h2>Posts from blogs I follow around the net</h2><ul><li><a href=https://utcc.utoronto.ca/~cks/space/blog/linux/NFSv4ServerLockClients target=_blank rel=noopener>Finding which NFSv4 client owns a lock on a Linux NFS(v4) server</a> — <a href=https://utcc.utoronto.ca/~cks/space/blog/>Chris's Wiki :: blog</a><div>A while back I wrote an entry about finding which NFS client owns | ||
| 438 | a lock on a Linux NFS server, which turned | ||
| 439 | out to be specific to NFS v3 (which I really should have seen coming, | ||
| 440 | since it involved NLM and lockd). Finding the NFS v4 client that | ||
| 441 | owns a lock is, depending on your perspective, either simpl…<li><a href=http://www.landley.net/notes-2023.html#28-10-2023 target=_blank rel=noopener>October 28, 2023</a> — <a href=http://www.landley.net/notes-2023.html>Rob Landley's Blog Thing for 2023</a><div>Oh good grief, two of my least favorite licensing people, Larry Rosen | ||
| 442 | and Bradley Kuhn, are interacting on the OSI's license-discuss | ||
| 443 | list where the're doing | ||
| 444 | bad computer history and insisting that a guy Larry Rosen | ||
| 445 | coincidentally interviewed for a book years ago is clearly the origin of | ||
| 446 | somethin…<li><a href="http://offbeatpursuit.com:80/blog/?id=25" target=_blank rel=noopener>A fix by any other name</a> — <a href=http://offbeatpursuit.com:80/blog/>WLOG - blog</a><div>tags: | ||
| 447 | i2c, plan9 | ||
| 448 | Another month, another file system. | ||
| 449 | Well, if you can’t fix it in software, fix it in hardware (looking at | ||
| 450 | you, bme680, we’re not | ||
| 451 | done yet). The show must go on, as they say, and I would like my | ||
| 452 | experiments to go on. | ||
| 453 | So a “new” addition to the environmental sensor family connected to | ||
| 454 | the h…<li><a href=https://mirzapandzo.com/next-image-url-parameter-is-valid-but-upstream-response-is-invalid target=_blank rel=noopener>Next/Image "url" parameter is valid but upstream response is invalid</a> — <a href=https://mirzapandzo.com/>Mirza Pandzo's Blog</a><div>Getting "url" parameter is valid but upstream response is invalid error with Next/Image on WSL2<li><a href=https://drewdevault.com/2023/10/13/Going-off-script.html target=_blank rel=noopener>Going off-script</a> — <a href=https://drewdevault.com>Drew DeVault's blog</a><div>There is a phenomenon in society which I find quite bizarre. Upon our entry to | ||
| 455 | this mortal coil, we are endowed with self-awareness, agency, and free will. | ||
| 456 | Each of the 8 billion members of this human race represents a unique person, a | ||
| 457 | unique worldview, and a unique agency. Yet, many of us have the sam…<li><a href=https://szymonkaliski.com/writing/2023-10-02-building-a-diy-pen-plotter/ target=_blank rel=noopener>Building a DIY Pen Plotter</a> — <a href=http://github.com/dylang/node-rss>Szymon Kaliski</a><div>This article documents my learnings from designing and building a DIY Pen Plotter during the summer of 2023. | ||
| 458 | My ultimate goal is to build my…<li><a href=https://neil.computer/notes/chart-of-accounts-for-startups-and-saas-companies/ target=_blank rel=noopener>Chart of Accounts for Startups and SaaS Companies</a> — <a href=https://neil.computer/>Neil Panchal</a><div>Accounting is fundamental to starting a business. You need to have a basic understanding of accounting principles and essential bookkeeping. I had to learn it. There was no choice. For filing taxes, your CPA is going to ask you for an Income Statement (also known as P/L statement). If<li><a href=https://journal.valeriansaliou.name/deploy-a-nomad-cluster-on-alpine-linux-with-vultr/ target=_blank rel=noopener>Deploy a Nomad Cluster on Alpine Linux with Vultr</a> — <a href=https://journal.valeriansaliou.name/>Valerian Saliou</a><div>After spending countless hours trying to understand how to deploy my apps on Kubernetes for the first time to host Mirage, an AI API service that I run, I ended up making myself a promise that the next app I work on would be using a more productive & simpler<li><a href=https://jcs.org/2023/10/25/wifi_da target=_blank rel=noopener>BlueSCSI Wi-Fi Desk Accessory 1.0 Released</a> — <a href=https://jcs.org/>joshua stein</a><div>BlueSCSI Wi-Fi Desk Accessory | ||
| 459 | 1.0 has been released: | ||
| 460 | wifi_da-1.0.sit | ||
| 461 | (StuffIt 3 archive) | ||
| 462 | SHA256: ccfc9d27dd5da7412d10cef73b81119a1fec3848e4d1d88ff652a07ffdc6a69aSHA1: ff124972f202ceda6d7fa4788110a67ccda6a13a | ||
| 463 | This is the initial public release of my BlueSCSI Wi-Fi Desk Accessory for | ||
| 464 | classic MacOS.<li><a href=https://michael.stapelberg.ch/posts/2023-10-25-my-all-flash-zfs-network-storage-build/ target=_blank rel=noopener>My 2023 all-flash ZFS NAS (Network Storage) build</a> — <a href=https://michael.stapelberg.ch/>Michael Stapelbergs Website</a><div>For over 10 years now, I run two self-built NAS (Network Storage) devices which serve media (currently via Jellyfin) and run daily backups of all my PCs and servers. | ||
| 465 | In this article, I describe my goals, which hardware I picked for my new build (and why) and how I set it up. | ||
| 466 | Design Goals | ||
| 467 | I use my netw…</ul><p>Generated with <a href=https://git.sr.ht/~sircmpwn/openring target=_blank rel=noopener>openring</a>.</section><footer><hr><p><big><strong>Want to comment or have something to add?</strong></big><p>You can write me an email | ||
| 468 | at <a href=mailto:mitja.felicijan@gmail.com>mitja.felicijan@gmail.com</a> or | ||
| 469 | catch up with me <a href=https://telegram.me/mitjafelicijan target=_blank>on Telegram</a>.<hr><p>This website does not track you. Content is made available under the <a href=https://creativecommons.org/licenses/by/4.0/ target=_blank rel=noreferrer>CC BY 4.0 license</a> unless | ||
| 470 | specified otherwise. Blog is also available as <a href=/index.xml target=_blank>RSS feed</a>.</footer><script> | ||
| 471 | window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); }; | ||
| 472 | </script><script defer src=/_vercel/insights/script.js></script> \ No newline at end of file | ||
