-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9df325b
Showing
10 changed files
with
268 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
**/.DS_Store | ||
env | ||
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |