Browse Source

Initial commit

master
Mablr 1 year ago
commit
823a2eca71
Signed by: mablr GPG Key ID: 7568670EF499017A
9 changed files with 512 additions and 0 deletions
  1. +2
    -0
      .gitignore
  2. +101
    -0
      README.md
  3. +89
    -0
      arduino/stepper_serial_control/stepper_serial_control.ino
  4. +20
    -0
      rpi/cli/polux-cli.py
  5. +9
    -0
      rpi/requirements.txt
  6. +7
    -0
      rpi/webapp/polux.ini
  7. +183
    -0
      rpi/webapp/polux.py
  8. +22
    -0
      rpi/webapp/static/style.css
  9. +79
    -0
      rpi/webapp/templates/index.html

+ 2
- 0
.gitignore View File

@ -0,0 +1,2 @@
rpi/__pycache__
rpi/venv3

+ 101
- 0
README.md View File

@ -0,0 +1,101 @@
# Polux : système de poulallier autonome
Polux est un projet permettant la garde autonome et supervisée d'un poulallier.Il permet d'ouvrir le poulailler le matin afin que les poules puissent gambader en plein air, et de le fermer le soir pour mettre à l'abri les gallinacés des prédateurs nocturnes.
## Matériel
(Prix indicatifs)
- Raspberry pi 3 A+ : 21.63€
- Pi cam : 23.99€
- Carte SD : 9.59€
- Boîtier dérivation : 5.37€
- Rallonge : 12.17€
- Fiche électrique : 11.28€
- Moteur pas à pas Nema17 : 16.99€
- Arduino pro micro : 6.66€
- Driver A4988 : 1.99€
- Roulements à billes 688zz : 6.19€
- Fils de câblage : 13.90€
- Pâte à souder : 7.27€
- Tresse à dessouder : 2.77€
- Gaines thermo : 10.99 €
- Étain : 10.99€
## Mise en place
### Schéma électronique
*Bientôt disponible*
### Arduino
Récupérer la bibliotèque DHT.h de Adafruit, pour le capteur de température.
Compiler le code et téléverser le code du dossier `arduino` sur votre carte.
### Raspberry Pi
On considère que l'on débute avec une installation propre de Raspbian Lite.
#### Création d'un utilisateur dédié
```bash
adduser polux
adduser polux dialout # Pour avoir accès à l'arduino
```
#### Installation des paquets
```bash
apt update; apt upgrade -y; apt install -y nginx python3-venv
```
#### Environnement Python
```bash
su - polux
git clone ce_depot
cd polux/rpi
python3 -m virtualenv -p python3 venv3
. ./venv3/bin/activate
pip install -r requirements.txt
```
#### Déploiement de l'application
Copier cette configuration (en adaptant si nécessaire) dans le fichier `/etc/systemd/system/polux.service`
```
[Unit]
Description=Polux WebApp
After=network.target
[Service]
User=polux
Group=polux
WorkingDirectory=/home/polux/polux/rpi/webapp
Environment="PATH=/home/polux/polux/rpi/venv3/bin"
ExecStart=/home/polux/polux/rpi/venv3/bin/uwsgi -i polux.ini
[Install]
WantedBy=multi-user.target
```
Puis activer et lancer le nouveau service.
```bash
systemctl daemon-reload
systemctl start polux
systemctl enable polux
```
#### Configuration Nginx
Créer le fichier `/etc/nginx/sites-available/polux` (à adapter selon les besoins) :
```
server {
listen 80;
server_name 0.0.0.0;
client_max_body_size 200M;
location / {
auth_basic "Panneau de controle Polux : acces restreint";
auth_basic_user_file /etc/nginx/.htpasswd;
include uwsgi_params;
uwsgi_pass unix:/home/polux/polux/rpi/webapp/polux.sock;
}
}
```
```bash
cd /etc/nginx/sites-enabled/
ln -s ../sites-available/polux
nginx -t
systemctl reload nginx
```

+ 89
- 0
arduino/stepper_serial_control/stepper_serial_control.ino View File

@ -0,0 +1,89 @@
#include "DHT.h"
#define DHTPIN 15
#define DHTTYPE DHT11
#define DirPin 2
#define StepPin 3
#define SPR 200
#define disableStep 9
#define rotCount 11
// Inspired by examples found just here : https://lastminuteengineers.com/a4988-stepper-motor-driver-arduino-tutorial/ and adafruit documentation for DHT library.
DHT dht(DHTPIN, DHTTYPE);
bool DoorOpened = false;
void setup()
{
Serial.begin(9600);
// Make pins as Outputs
pinMode(StepPin, OUTPUT);
pinMode(DirPin, OUTPUT);
pinMode(disableStep, OUTPUT);
digitalWrite(disableStep, HIGH); //disable stepper
dht.begin();
}
void loop()
{
if (Serial.available() > 0) {
String cmd = Serial.readStringUntil('\n');
if (cmd == "close") {
if (DoorOpened) {
digitalWrite(disableStep, LOW); //enable stepper
digitalWrite(DirPin, HIGH); // defines the direction to clockwise
// Pulse the step pin
for(int x = 0; x < rotCount*SPR; x++) {
digitalWrite(StepPin, HIGH);
delayMicroseconds(1000);
digitalWrite(StepPin, LOW);
delayMicroseconds(1000);
}
digitalWrite(disableStep, HIGH); //disable stepper
DoorOpened = false;
Serial.println("Porte Fermée");
Serial.println("#");
}else{
Serial.println("Déjà fermé");
Serial.println("#");
}
}
if (cmd == "open") {
if (!DoorOpened) {
digitalWrite(disableStep, LOW); //enable stepper
digitalWrite(DirPin, LOW); // defines the direction to counterclockwise
// Pulse the step pin
for(int x = 0; x < rotCount*SPR; x++) {
digitalWrite(StepPin, HIGH);
delayMicroseconds(1000);
digitalWrite(StepPin, LOW);
delayMicroseconds(1000);
}
digitalWrite(disableStep, HIGH); //disable stepper
DoorOpened = true;
Serial.println("Porte Ouverte");
Serial.println("#");
}else{
Serial.println("Déjà ouvert");
Serial.println("#");
}
}
if (cmd == "state") {
if (DoorOpened) {
Serial.println("Porte Ouverte");
}else{
Serial.println("Porte Fermée");
}
Serial.println("#");
}
if (cmd == "temp") {
float h = dht.readHumidity();
float t = dht.readTemperature();
float hic = dht.computeHeatIndex(t, h, false);
Serial.print("Humidity: ");
Serial.print(h);
Serial.print("% Temperature: ");
Serial.print(hic);
Serial.println(F("°C "));
Serial.println("#");
}
}
}

+ 20
- 0
rpi/cli/polux-cli.py View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import serial
import time
import sys
availableCmd = ['state','close','open','temp']
if __name__ == '__main__':
ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1)
ser.flush()
cmd = str(sys.argv[1])
if cmd in availableCmd:
ser.write((cmd+"\n").encode('utf-8'))
while True: # Emulating DoWhile
rep = ser.readline().decode('utf-8').rstrip()
if rep == "#":
break
print(rep)
else:
print(cmd + " is not an available command")
print("Please choose in this list: " + " ".join(availableCmd))

+ 9
- 0
rpi/requirements.txt View File

@ -0,0 +1,9 @@
click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
picamera==1.13
pyserial==3.4
uWSGI==2.0.19.1
Werkzeug==1.0.1

+ 7
- 0
rpi/webapp/polux.ini View File

@ -0,0 +1,7 @@
[uwsgi]
module = polux:app
master = true
socket = polux.sock
chmod-socket = 666
vacuum = true
die-on-term = true

+ 183
- 0
rpi/webapp/polux.py View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
""" Pi camera part was written by Miguel Grinberg, https://github.com/miguelgrinberg/flask-video-streaming, MIT Licence """
from flask import Flask, render_template, redirect, request, Response
import serial
import os, fnmatch
import io
import time
from picamera import PiCamera
import threading
availableCmd = ['state','close','open','temp']
app = Flask(__name__)
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
class CameraEvent(object):
"""An Event-like class that signals all active clients when a new frame is
available.
"""
def __init__(self):
self.events = {}
def wait(self):
"""Invoked from each client's thread to wait for the next frame."""
ident = get_ident()
if ident not in self.events:
# this is a new client
# add an entry for it in the self.events dict
# each entry has two elements, a threading.Event() and a timestamp
self.events[ident] = [threading.Event(), time.time()]
return self.events[ident][0].wait()
def set(self):
"""Invoked by the camera thread when a new frame is available."""
now = time.time()
remove = None
for ident, event in self.events.items():
if not event[0].isSet():
# if this client's event is not set, then set it
# also update the last set timestamp to now
event[0].set()
event[1] = now
else:
# if the client's event is already set, it means the client
# did not process a previous frame
# if the event stays set for more than 5 seconds, then assume
# the client is gone and remove it
if now - event[1] > 5:
remove = ident
if remove:
del self.events[remove]
def clear(self):
"""Invoked from each client's thread after a frame was processed."""
self.events[get_ident()][0].clear()
class BaseCamera(object):
thread = None # background thread that reads frames from camera
frame = None # current frame is stored here by background thread
last_access = 0 # time of last client access to the camera
event = CameraEvent()
def __init__(self):
"""Start the background camera thread if it isn't running yet."""
if BaseCamera.thread is None:
BaseCamera.last_access = time.time()
# start background frame thread
BaseCamera.thread = threading.Thread(target=self._thread)
BaseCamera.thread.start()
# wait until frames are available
while self.get_frame() is None:
time.sleep(0)
def get_frame(self):
"""Return the current camera frame."""
BaseCamera.last_access = time.time()
# wait for a signal from the camera thread
BaseCamera.event.wait()
BaseCamera.event.clear()
return BaseCamera.frame
@staticmethod
def frames():
""""Generator that returns frames from the camera."""
raise RuntimeError('Must be implemented by subclasses.')
@classmethod
def _thread(cls):
"""Camera background thread."""
print('Starting camera thread.')
frames_iterator = cls.frames()
for frame in frames_iterator:
BaseCamera.frame = frame
BaseCamera.event.set() # send signal to clients
time.sleep(0)
# if there hasn't been any clients asking for frames in
# the last 10 seconds then stop the thread
if time.time() - BaseCamera.last_access > 10:
frames_iterator.close()
print('Stopping camera thread due to inactivity.')
break
BaseCamera.thread = None
class Camera(BaseCamera):
@staticmethod
def frames():
with PiCamera() as camera:
# let camera warm up
time.sleep(2)
stream = io.BytesIO()
camera.resolution = (320, 240)
camera.vflip = True
for _ in camera.capture_continuous(stream, 'jpeg',
use_video_port=True):
# return current frame
stream.seek(0)
yield stream.read()
# reset stream for next frame
stream.seek(0)
stream.truncate()
def findSerialDev(pattern):
for root, dirs, files in os.walk('/dev'):
for name in files:
if fnmatch.fnmatch(name, pattern):
return os.path.join(root, name)
def serialWrite(cmd):
rep = ""
ser.write((cmd+"\n").encode('utf-8'))
while True: # Emulating DoWhile
buf = ser.readline().decode('utf-8').rstrip()
if buf == "#":
break
rep += buf
return rep
@app.route('/')
def index():
return redirect("/polux", code=302)
@app.route('/polux', methods=['GET','POST'])
def polux():
error = None
feedback = None
if request.method == 'POST':
if request.form['cmd'] in availableCmd:
feedback = serialWrite(request.form['cmd'])
else:
error = "Invalid command."
return render_template('index.html', feedback=feedback, error=error)
def gen(camera):
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed')
def video_feed():
return Response(gen(Camera()), mimetype='multipart/x-mixed-replace; boundary=frame')
ser = serial.Serial(findSerialDev('ttyACM*'), 9600, timeout=1)
if __name__ == '__main__':
app.run()

+ 22
- 0
rpi/webapp/static/style.css
File diff suppressed because it is too large
View File


+ 79
- 0
rpi/webapp/templates/index.html View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="fr" dir="ltr">
<head>
<meta charset="utf-8">
<title>Polux</title>
<link rel="stylesheet" href='../static/style.css' />
</head>
<body>
<main>
<div class="center">
<h1 class="center">Panneau de contrôle Polux</h1>
</div>
<div class="flex one two-1200">
<div>
<article class="card">
<header>
<h2>Commandes</h2>
</header>
<footer>
<form action="/polux" method="post">
<label>
<input type='radio' name="cmd" id="state" value="state">
<span class="checkable">État</span>
</label><br>
<label>
<input type='radio' name="cmd" id="open" value="open">
<span class="checkable">Ouvrir</span>
</label><br>
<label>
<input type='radio' name="cmd" id="close" value="close">
<span class="checkable">Fermer</span>
</label><br>
<label>
<input type='radio' name="cmd" id="temp" value="temp">
<span class="checkable">Conditions Météo</span>
</label><br>
<input type="submit" value="OK">
</form>
</footer>
</article>
</div>
{% if feedback != None %}
<div>
<article class="card">
<header>
<h2 class="success">Retour commande</h2>
</header>
<footer>
<pre>{{ feedback }}</pre>
</footer>
</article>
</div>
{% endif %}
{% if error != None %}
<div>
<article class="card">
<header>
<h2 class="error">Erreurs</h2>
</header>
<footer>
<pre>{{ error }}</pre>
</footer>
</article>
</div>
{% endif %}
<div class="full">
<article class="card">
<header>
<h2>Vidéosurveillance</h2>
</header>
<footer>
<img class="cctv" alt="cctv" src="{{ url_for('video_feed') }}">
</footer>
</article>
</div>
</div>
</main>
</body>
</html>

Loading…
Cancel
Save