iot-with-document-store-db

git clone git://git.codymlewis.com/iot-with-document-store-db.git
Log | Files | Refs | README

commit a1a99d04c5cedd7d63fce16b8981656ae7271698
Author: Cody Lewis <codymlewis@protonmail.com>
Date:   Thu, 16 May 2019 12:24:57 +1000

add README

Diffstat:
A.gitignore | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 24++++++++++++++++++++++++
Asrc/Functions.py | 26++++++++++++++++++++++++++
Asrc/Network.py | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awww/Server.py | 38++++++++++++++++++++++++++++++++++++++
Awww/static/images/background.jpg | 0
Awww/static/javascripts/index.js | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awww/static/stylesheets/style.css | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awww/templates/index.html | 22++++++++++++++++++++++
Awww/templates/node.html | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 724 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,134 @@ +tags +Session.vim +Secrets.py + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python + diff --git a/README.md b/README.md @@ -0,0 +1,24 @@ +# IoT with Document Store DB +A simulation of an Internet of things network using a centralized document store database. + +## Requirements +- CouchDB +- python +- NodeJS + +## Setup +Run +``` +cd www && npm install && cd .. +``` + +## Running +Start the simulation with +``` +cd src && python3 Network.py +``` +then start the web server with +``` +cd ../www && npm start +``` +and visit http://localhost:3000/ diff --git a/src/Functions.py b/src/Functions.py @@ -0,0 +1,26 @@ +''' +A few utility functions. + +Author: Cody Lewis +Date: 2019-03-30 +''' + + +def print_progress(current_epoch, total_epochs, progress_len=31, prefix="", suffix=""): + ''' + Print a progress bar about how far a process has went through it's epochs. + ''' + progress = int(100 * current_epoch / total_epochs) + + progress_bar_progress = int(progress_len * progress * 0.01) + if progress_bar_progress != 0: + unprogressed = progress_len - progress_bar_progress + else: + unprogressed = progress_len - 1 + progress_bar = "[" + progress_bar += "".join( + ["=" for _ in range(progress_bar_progress - 2)] + [">" if unprogressed > 0 else "="] + ) + progress_bar += "".join(["." for _ in range(unprogressed)]) + progress_bar += "]" + print(f"\r{prefix} {progress_bar} {progress}% {suffix}", end="\r") diff --git a/src/Network.py b/src/Network.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Simulate an Internet of things network, and send data on nodes to the server +# Author: Cody Lewis +# Date: 2019-05-16 + + +import random +import time +from json.encoder import JSONEncoder + +import requests + +import Secrets +import Functions + + +ADMIN_DB_ADDR = f"http://{Secrets.DB_USERNAME}:{Secrets.DB_PASSWORD}@127.0.0.1:5984/sensors" +DB_ADDR = f"http://127.0.0.1:5984/sensors" +THING_TYPES = ["Themometer", "Acnemometer", "Smart Light", "Pulse Oximeter"] +JSONENCODER = JSONEncoder() + + +class Thing: + ''' + A simulated thing in the network. + ''' + def __init__(self, id_num=0, x_val=0, y_val=0, type_val="Thing"): + self.__id = id_num + self.__x = x_val + self.__y = y_val + self.__type = type_val + self.__data = [] + self.rev = "" + + def get_type(self): + return self.__type + + def get_id(self): + return self.__id + + def get_json(self): + ''' + Get a JSON style string from the values contained in this + ''' + return JSONENCODER.encode(self.__get_vals()) + + def __get_vals(self): + ''' + Get a dictionary of the values in this + ''' + return { + "id": self.__id, + "x": self.__x, + "y": self.__y, + "type": self.__type, + "data": self.__data + } + + def add_data(self, data): + ''' + Add a row of data to this + ''' + self.__data.append(data) + + def get_data_json(self): + vals = self.__get_vals() + vals["_rev"] = self.rev + return JSONENCODER.encode(vals) + + +class Network: + ''' + The simulated network. + ''' + def __init__(self, total_nodes): + requests.put(f"{ADMIN_DB_ADDR}") + self.__nodes = [] + for i in range(total_nodes): + new_thing = Thing(i, random.randint(0, 255), random.randint(0, 255), THING_TYPES[i % len(THING_TYPES)]) + request = requests.put(f"{DB_ADDR}/{new_thing.get_id()}", data=new_thing.get_json()) + new_thing.rev = request.json()["rev"] + self.__nodes.append(new_thing) + Functions.print_progress(i + 1, total_nodes, prefix=f"{i + 1}/{total_nodes} created") + print() + + def run(self): + ''' + Run the network infinitely. + ''' + cur_time = 0, 0 + while True: + # chosen_node = self.__nodes[random.randint(0, len(self.__nodes) - 1)] + for node in self.__nodes: + node_type = node.get_type() + if node_type == "Themometer": + node.add_data({"time": cur_time, "temp": random.randint(15, 40)}) + elif node_type == "Acnemometer": + node.add_data({"time": cur_time, "speed": random.randint(0, 40), "pressure": random.randint(0, 20)}) + elif node_type == "Smart Light": + node.add_data({ + "time": cur_time, + "status": random.sample(["On", "Off"], 1)[0], + "usage": random.uniform(0, 1) + }) + elif node_type == "Pulse Oximeter": + node.add_data({"time": cur_time, "rate": random.randint(50, 170)}) + request = requests.put(f"{DB_ADDR}/{node.get_id()}", data=node.get_data_json()) + node.rev = request.json()["rev"] + print(f"\rAdded data for all nodes at time {cur_time}.", end="\r") + time.sleep(1) + cur_time += 1 + + def cleanup(self): + for index_node in enumerate(self.__nodes): + ok = False + while not ok: + rev = requests.get(f"{DB_ADDR}/{index_node[1].get_id()}").json()["_rev"] + request = requests.delete(f"{DB_ADDR}/{index_node[1].get_id()}?rev={rev}") + ok = request.json()["ok"] + Functions.print_progress(index_node[0] + 1, len(self.__nodes), prefix=f"{index_node[0] + 1}/{len(self.__nodes)} deleted") + print() + + +if __name__ == '__main__': + NUMBER_NODES = 10 + print(f"Creating a network with {NUMBER_NODES} nodes.") + NETWORK = Network(NUMBER_NODES) + try: + print("Running the network...") + NETWORK.run() + except KeyboardInterrupt: + print() + print("Cleaning up...") + NETWORK.cleanup() + print("bye.") diff --git a/www/Server.py b/www/Server.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from json.encoder import JSONEncoder +import requests + +from flask import Flask, render_template +app = Flask(__name__) + + +DB_ADDR = f"http://127.0.0.1:5984/sensors" + +JSONENCODER = JSONEncoder() + +# NODE_DATA = get_node_data() + + +@app.route("/") +def home(): + return render_template('index.html', title="Map") + + +@app.route("/nodes/<node_id>") +def show_node(node_id): + data = requests.get(f"{DB_ADDR}/{node_id}").json() + return render_template('node.html', title=f"{node_id}", data=data) + + +@app.route("/node-data") +def get_node_data(): + data = requests.post(f"{DB_ADDR}/_find", data=JSONENCODER.encode({ + "selector": { + "id": { + "$gt": None + } + } + }), headers={"Content-Type": "application/json"}).json() + return JSONENCODER.encode(data) diff --git a/www/static/images/background.jpg b/www/static/images/background.jpg Binary files differ. diff --git a/www/static/javascripts/index.js b/www/static/javascripts/index.js @@ -0,0 +1,112 @@ +var sensorMap = document.getElementById('sensor-map'); +var sensorMapCtx = sensorMap.getContext('2d'); +var frames = undefined; +var frameRate = 20; +var maxX = 65; +var maxY = 65; +var sensors = new Map(); +var canvasMap = createMap(); +window.addEventListener('resize', resizeCanvas, false); + +window.onload = () => { + constructMap(); +} + +sensorMap.addEventListener('click', (event) => { + var xDiv = (window.innerWidth * 0.8) / maxX; + var yDiv = (window.innerWidth * 0.8) / maxY; + var x = Math.floor(((event.pageX - sensorMap.offsetLeft) / (xDiv))); + var y = Math.floor(((event.pageY - sensorMap.offsetTop) / (yDiv))); + if (sensors[x] !== undefined && sensors[x][y] !== undefined) { + window.location = `/nodes/${sensors[x][y].id}`; + } +}); + +function createMap() { + var newMap = [] + for (var i = 0; i < maxX; ++i) { + var row = [] + for(var j = 0; j < maxY; ++j) { + if(Math.random() * 100 < 75) { + row[j] = "#0F0"; + } else { + row[j] = "#00F"; + } + } + newMap[i] = row; + } + return newMap; +} + +function constructMap() { + resizeCanvas(); + getAllSensors(); +} + +function resizeCanvas() { + sensorMap.width = window.innerWidth * 0.8; + sensorMap.height = window.innerHeight * 0.8; + if (frames !== undefined) { + clearInterval(frames); + } + frames = setInterval(() => { drawFrame(); }, frameRate); +} + +function getAllSensors() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var sensorsData = eval(`(${xhttp.responseText})`); + for (let i in sensorsData['docs']) { + sensor = sensorsData['docs'][i] + if (sensors[sensor.x] === undefined) { + sensors[sensor.x] = new Map(); + } + sensors[sensor.x][sensor.y] = new Thing(sensor.id, sensor.x, sensor.y, sensor.type); + } + } + }; + xhttp.open("GET", `/node-data`, true); + xhttp.send(); +} + +function drawFrame() { + var xDiv = (window.innerWidth * 0.8) / maxX; + var yDiv = (window.innerWidth * 0.8) / maxY; + for (var x = 0; x < maxX; ++x) { + for (var y = 0; y < maxY; ++y) { + sensorMapCtx.fillStyle = canvasMap[x][y]; + sensorMapCtx.fillRect(x * xDiv, y * yDiv, xDiv, yDiv); + if (sensors[x] !== undefined && sensors[x][y] !== undefined) { + sensorMapCtx.fillStyle = "#CCC"; + sensorMapCtx.fillRect(x * xDiv, y * yDiv, xDiv, yDiv); + } + } + } +} + +class Thing { + constructor(id, x, y, type) { + this.id = id; + this.x = x; + this.y = y; + this.type = type; + } + + getData() { + var data = null; + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + data = eval(`(${xhttp.responseText})`); + } + }; + xhttp.open("POST", `http://127.0.0.1:5984/sensors/${this.id}`, true); + xhttp.send(); + return sensors + } + + toString() { + return `Sensor: ${this.id}\nType: ${this.type}\nco-ordinates: ${this.x}, ${this.y}` + } +} diff --git a/www/static/stylesheets/style.css b/www/static/stylesheets/style.css @@ -0,0 +1,175 @@ +:root { + --primary-color: #5cff55; + --white: #fff; + --black: #000; + --default-color: #666; + --highlight-color: #333; +} + +h1 { + text-align: center; +} + +h2 { + text-align: center; +} + +body { + padding-top: 2%; + padding-left: 5%; + padding-right: 5%; + padding-bottom: 2%; + font: 16px "Lucida Grande", Helvetica, Arial, sans-serif; + background-color: var(--primary-color); + background-image: url("/static/images/background.jpg"); + background-repeat: no-repeat; + background-position: center; + background-size: cover; + background-attachment: fixed; +} + +div.root { + background: white; + overflow: hidden; +} + +div.menu { + width: 100%; + height: 40px; +} + +div.menu a { + color: var(--white); +} + +div.item { + background: var(--default-color); + padding: 1%; + height: 100%; + float: left; +} + +div.item:hover { + background: var(--highlight-color); +} + +div.card { + border: solid 1px var(--default-color); + padding: 2%; + margin: 1%; + width: 26%; + float: left; + overflow: hidden; +} + +div.card:hover { + border: solid 1px var(--black); +} + +div.card-img { + overflow: hidden; + width: 100%; + height: 250px; +} + +a { + color: #00B7FF; +} + +button.project { + margin: 3%; + width: 100%; + height: 40px; + background: var(--default-color); + color: white; + font-size: 16px; +} + +button.project:hover { + background: var(--highlight-color); +} + +.text-body { + padding-left: 18%; + padding-right: 18%; +} + +.float-left { + float: left; +} + +.float-right { + float: right; +} + +div.centre { + display: table; + margin: 0 auto; +} + +div.table { + overflow: hidden; + display: table; + width: 100%; +} + +aside.left { + width: 60%; +} + +aside.right { + width: 35%; + margin-top: 5%; +} + +.pad-left-5 { + padding-left: 5%; +} + +.tbl { + border-collapse: collapse; + width: 100%; +} + +th { + border-top: 1px solid black; + background-color: #4CAF50; + color: white; +} + +th, td { + border-bottom: 1px solid black; + padding: 15px; + padding-right: 50px; + text-align: center; +} + +tr:hover { + background-color: #EEE; +} + +/* Responsive css adapters */ +@media only screen and (max-width: 1200px) { + div.card { + float: none; + width: 93%; + margin: 0 auto; + } + + div.card-img { + height: 150px; + } +} + +@media only screen and (max-width: 600px) { + body { + margin-left: 0%; + margin-right: 0%; + padding-left: 0%; + padding-right: 0%; + } + .text-body { + padding-left: 10%; + padding-right: 10%; + } +} diff --git a/www/templates/index.html b/www/templates/index.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{title}} - Sensors</title> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="author" content="Cody Lewis" /> + <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" + integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous"> + <link rel='stylesheet' href='/static/stylesheets/style.css' /> + </head> + <body> + <div class="root"> + <h1>Sensors</h1> + <div class="centre"> + <canvas id="sensor-map"></canvas> + </div> + <script src="/static/javascripts/index.js"></script> + <br /> + </div> + </body> +</html> diff --git a/www/templates/node.html b/www/templates/node.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>{{title}} - Sensors</title> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="author" content="Cody Lewis" /> + <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" + integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous"> + <link rel='stylesheet' href='/static/stylesheets/style.css' /> + </head> + <body> + <div class="root"> + <a href="/" style="float:left"> + <span style="font-size: 96px; color: black; padding: 15px;"> + <i class="fas fa-chevron-left"></i> + </span> + </a> + <h1>Sensor {{title}}</h1> + <h2>{{data['type']}}</h2> + <table class="tbl"> + <tr> + <th>Time</th> + {% if data['type'] == "Themometer" %} + <th>Temperature (°C)</th> + {% elif data['type'] == "Acnemometer" %} + <th>Speed (m/s)</th> + <th>Pressure (HpA)</th> + {% elif data['type'] == "Smart Light" %} + <th>Status</th> + <th>Usage (kW/h)</th> + {% elif data['type'] == "Pulse Oximeter" %} + <th>Pulse Rate (bpm)</th> + {% endif %} + </tr> + {% for row in data['data'] %} + <tr> + <td>{{ row['time'] }}</td> + {% if data['type'] == "Themometer" %} + <td>{{ row['temp'] }}</td> + {% elif data['type'] == "Acnemometer" %} + <td>{{ row['speed'] }}</td> + <td>{{ row['pressure'] }}</td> + {% elif data['type'] == "Smart Light" %} + <td>{{ row['status'] }}</td> + <td>{{ row['usage'] }}</td> + {% elif data['type'] == "Pulse Oximeter" %} + <td>{{ row['rate'] }}</td> + {% endif %} + </tr> + {% endfor %} + </table> + <br /> + </div> + </body> +</html>