From 9df325b73e2a81730ef503655a6ea0f9b674a7bb Mon Sep 17 00:00:00 2001 From: Ryder Damen Date: Thu, 1 Jul 2021 00:39:22 -0400 Subject: [PATCH] init --- .gitignore | 3 ++ Dockerfile | 7 ++++ Makefile | 22 ++++++++++++ README.md | 35 +++++++++++++++++++ docker-compose.yaml | 20 +++++++++++ scripts/install.sh | 13 +++++++ src/AirQualityMonitor.py | 28 +++++++++++++++ src/app.py | 75 ++++++++++++++++++++++++++++++++++++++++ src/requirements.txt | 6 ++++ src/templates/index.html | 59 +++++++++++++++++++++++++++++++ 10 files changed, 268 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 docker-compose.yaml create mode 100644 scripts/install.sh create mode 100644 src/AirQualityMonitor.py create mode 100644 src/app.py create mode 100644 src/requirements.txt create mode 100644 src/templates/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5c2f00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +**/.DS_Store +env +*.pyc diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..930dffe --- /dev/null +++ b/Dockerfile @@ -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" ] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5a7f272 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c26afe --- /dev/null +++ b/README.md @@ -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. diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..a61512f --- /dev/null +++ b/docker-compose.yaml @@ -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" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..711c48c --- /dev/null +++ b/scripts/install.sh @@ -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 diff --git a/src/AirQualityMonitor.py b/src/AirQualityMonitor.py new file mode 100644 index 0000000..f8d6018 --- /dev/null +++ b/src/AirQualityMonitor.py @@ -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)] diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..cb8391b --- /dev/null +++ b/src/app.py @@ -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'))) diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..6f32d05 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,6 @@ +sds011==0.2.0 +ipython +flask +redis +APScheduler==3.7.0 +flask-cors \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..aef2be5 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,59 @@ + + + + + + + + Raspberry Pi Air Quality Monitor + + + + + +
+
+
+

Raspberry Pi Air Quality Monitor

+
+
+
+
+ +
+
+
+ + + + + + + + + + \ No newline at end of file