This commit is contained in:
Ryder Damen 2021-07-01 00:39:22 -04:00
commit 9df325b73e
No known key found for this signature in database
GPG key ID: A0BB4C56DC7699FF
10 changed files with 268 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
**/.DS_Store
env
*.pyc

7
Dockerfile Normal file
View file

@ -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
Makefile Normal file
View file

@ -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
README.md Normal file
View file

@ -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
docker-compose.yaml Normal file
View file

@ -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
scripts/install.sh Normal file
View file

@ -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
src/AirQualityMonitor.py Normal file
View file

@ -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
src/app.py Normal file
View file

@ -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
src/requirements.txt Normal file
View file

@ -0,0 +1,6 @@
sds011==0.2.0
ipython
flask
redis
APScheduler==3.7.0
flask-cors

59
src/templates/index.html Normal file
View file

@ -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/bootstrap@5.0.2/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/bootstrap@5.0.2/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>