Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
ryderdamen committed Jul 1, 2021
0 parents commit 9df325b
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**/.DS_Store
env
*.pyc
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.8
WORKDIR /code
COPY src/requirements.txt .
RUN pip install -r requirements.txt
COPY src .
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
PI_IP_ADDRESS=10.0.0.172
PI_USERNAME=pi

.PHONY: run
run:
@docker-compose up

.PHONY: install
install:
@cd scripts && bash install.sh

.PHONY: copy
copy:
@rsync -a $(shell pwd) --exclude env $(PI_USERNAME)@$(PI_IP_ADDRESS):/home/$(PI_USERNAME)

.PHONY: shell
shell:
@ssh $(PI_USERNAME)@$(PI_IP_ADDRESS)

.PHONY: build
build:
@docker-compose build
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Raspberry Pi Air Quality Monitor
A simple air quality monitoring service for the Raspberry Pi.

## Installation
Clone the repository and run the following:
```bash
make install
```

## Running
To run, use the run command:
```bash
make run
```

## Architecture
This project uses python, flask, docker-compose and redis to create a simple web server to display the latest historical values from the sensor.

## Example Data
Some example data you can get from the sensor includes the following:

```json
{
"device_id": 13358,
"pm10": 10.8,
"pm2.5": 4.8,
"timestamp": "2021-06-16 22:12:13.887717"
}
```

The sensor reads two particulate matter (PM) values.

PM10 is a measure of particles less than 10 micrometers, whereas PM 2.5 is a measurement of finer particles, less than 2.5 micrometers.

Different particles are from different sources, and can be hazardous to different parts of the respiratory system.
20 changes: 20 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "3.4"
services:
redis:
image: redis
volumes:
- ./data/redis:/data
web:
build: .
image: pi-air-quality-monitor
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
environment:
- REDIS_HOST=redis
- PORT=8000
volumes:
- ./src:/code
depends_on:
- "redis"
ports:
- "8000:8000"
13 changes: 13 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
# install.sh

cd ../

sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-dev libffi-dev libssl-dev

curl -sSL https://get.docker.com | sh
sudo usermod -aG docker ${USER}
sudo pip3 install docker-compose
sudo systemctl enable docker
newgrp docker
28 changes: 28 additions & 0 deletions src/AirQualityMonitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
import os
import time
from sds011 import SDS011
import redis

redis_client = redis.StrictRedis(host=os.environ.get('REDIS_HOST'), port=6379, db=0)


class AirQualityMonitor():

def __init__(self):
self.sds = SDS011(port='/dev/ttyUSB0')
self.sds.set_working_period(rate=1)

def get_measurement(self):
return {
'time': int(time.time()),
'measurement': self.sds.read_measurement(),
}

def save_measurement_to_redis(self):
"""Saves measurement to redis db"""
redis_client.lpush('measurements', json.dumps(self.get_measurement(), default=str))

def get_last_n_measurements(self, n=10):
"""Returns the last n measurements in the list"""
return [json.loads(x) for x in redis_client.lrange('measurements', -n, -1)]
75 changes: 75 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import os
import time
from flask import Flask, request, jsonify, render_template
from AirQualityMonitor import AirQualityMonitor
from apscheduler.schedulers.background import BackgroundScheduler
import redis
import atexit
from flask_cors import CORS, cross_origin



app = Flask(__name__)
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
aqm = AirQualityMonitor()

scheduler = BackgroundScheduler()
scheduler.add_job(func=aqm.save_measurement_to_redis, trigger="interval", seconds=60)
scheduler.start()
atexit.register(lambda: scheduler.shutdown())


def reconfigure_data(measurement):
"""Reconfigures data for chart.js"""
current = int(time.time())
measurement = measurement.reverse()
return {
'labels': [int((current - (x['time'])) / 60) for x in measurement],
'pm10': {
'label': 'pm10',
'data': [x['measurement']['pm10'] for x in measurement],
'backgroundColor': '#cc0000',
'borderColor': '#cc0000',
'borderWidth': 3,
},
'pm2': {
'label': 'pm2.5',
'data': [x['measurement']['pm2.5'] for x in measurement],
'backgroundColor': '#42C0FB',
'borderColor': '#42C0FB',
'borderWidth': 3,
},
}

@app.route('/')
def index():
"""Index page for the application"""
context = {
'historical': reconfigure_data(aqm.get_last_n_measurements()),
}
return render_template('index.html', context=context)


@app.route('/api/')
@cross_origin()

def api():
"""Returns historical data from the sensor"""
context = {
'historical': reconfigure_data(aqm.get_last_n_measurements()),
}
return jsonify(context)


@app.route('/api/now/')
def api_now():
"""Returns latest data from the sensor"""
context = {
'current': aqm.get_measurement(),
}
return jsonify(context)


if __name__ == "__main__":
app.run(debug=True, use_reloader=False, host='0.0.0.0', port=int(os.environ.get('PORT', '8000')))
6 changes: 6 additions & 0 deletions src/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
sds011==0.2.0
ipython
flask
redis
APScheduler==3.7.0
flask-cors
59 changes: 59 additions & 0 deletions src/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Raspberry Pi Air Quality Monitor</title>

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

</head>
<body>
<div class="container">
<div class="row">
<div class="col-12">
<h1>Raspberry Pi Air Quality Monitor</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<canvas id="historicalChart" width="400" height="200"></canvas>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.3.2/chart.min.js" integrity="sha512-VCHVc5miKoln972iJPvkQrUYYq7XpxXzvqNfiul1H4aZDwGBGC0lq373KNleaB2LpnC2a/iNfE5zoRYmB4TRDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>

$.getJSON('http://10.0.0.172:8000/api/', function(data) {
var ctx = document.getElementById('historicalChart').getContext('2d');
var historicalChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.historical.labels,
datasets: [data.historical.pm10, data.historical.pm2]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
})


</script>



</body>
</html>

0 comments on commit 9df325b

Please sign in to comment.