asso:si:si_garage

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentes Révision précédente
Prochaine révision
Révision précédente
asso:si:si_garage [2026/08/11 01:07] – grapsusasso:si:si_garage [2026/10/12 22:04] (Version actuelle) – La route locale est aussi appelée par le bouton garage du Pi de la porte (gatepi) nrineau
Ligne 1: Ligne 1:
-===== Système d'ouverture du garage =====+===== Système d'ouverture du garage et de la porte arrière =====
  
-Le système d'ouverture du garage écoute sur le port 4280 et attend une requête sur une URL secrète de la part de framateam. Suite à cette requête il ouvre le périphérique série et lui envoie une commande pour procéder à l'actionnement du moteur.+Le boîtier rose fixé près du garage contient un **Arduino Uno** (clone AZ-Delivery), une **télécommande 433 MHz** de la porte de garage et le **lecteur de badges RFID**. Il est branché en USB sur le serveur **Atom** (''192.168.42.1''), qui le pilote de deux façons :
  
-Pré-requis: +  * **Framateam (Mattermost)** : commandes slash dans les canaux autorisés → ouvrir le garage, ouvrir / fermer la porte arrière ; 
-<code> +  * **Badge RFID** : un badge de la liste ouvre le **garage** uniquement.
-apt install python3-flask python3-serial +
-adduser lov dialout +
-</code>+
  
-Code du serveur dans /home/lov/garage: +{{ :asso:si:garage_archify.png?direct&900 |Architecture : ouverture du garage et de la porte arrière (Archify)}}
-<code> +
-from flask import Flask +
-app = Flask(__name__) +
-import serial+
  
-ARDUINO = '/dev/serial/by-id/usb-Arduino__www.arduino.cc__Arduino_Uno_64938323131351213190-if00'+//Schéma réalisé avec [[https://github.com/tt-a1i/archify|Archify]] ; source : ''docs/boitier-garage/archify/garage.architecture.json'' dans le dépôt AdminLova.//
  
-@app.route('/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', methods=['POST']) +==== Électronique ====
-def open(): +
-    try: +
-        with serial.Serial(port=ARDUINO, baudrate=9600) as f: +
-            banner = f.readline().decode('ascii') +
-            if 'LOV' not in banner: +
-                return "didn't find the garage opener banner" +
-            f.write(b'O') +
-            f.flush() +
-            resp = f.readline().decode('ascii') +
-            return "\n".join((banner, resp)) +
-    except serial.serialutil.SerialException as e: +
-        return str(e)+
  
-if __name__ == '__main__': +{{ :asso:si:boitier_garage_fritzing.png?direct&700 |Schéma de câblage du boîtier (Fritzing)}}
-    app.run(host='0.0.0.0', port=4280) +
-</code>+
  
-<code>+Schéma Fritzing modifiable : {{:asso:si:boitier_garage_fritzing.zip|boitier_garage_fritzing.zip}} (contient ''boitier-garage.fzz'', à ouvrir avec Fritzing) 
 + 
 +{{ :asso:si:boitier_garage_vue_ensemble.jpg?300|Vue d'ensemble de l'intérieur du boîtier}} 
 +{{ :asso:si:boitier_garage_arduino.jpg?300|Arduino Uno AZ-Delivery, résistances sur D9–D11}} 
 +{{ :asso:si:boitier_garage_telecommande.jpg?300|Télécommande 433 MHz HXQ920BT-4 alimentée par l'Arduino}} 
 + 
 +^ Broche Arduino ^ Fonction ^ Fonctionnement ^ 
 +| D9  | Bouton de la télécommande du garage | Mise à la masse pendant 800 ms = appui (ouvre **ou** ferme) | 
 +| D10 | Télécommande de la serrure : **ouvrir** | Impulsion 5 V de 2,5 s | 
 +| D11 | Télécommande de la serrure : **fermer** | Impulsion 5 V de 2,5 s | 
 +| USB | Liaison avec l'Atom | Série 9600 bauds, puce CH340 | 
 + 
 +La télécommande du garage (PCB vert HXQ920BT-4) n'a plus de pile : elle est alimentée par l'Arduino. Les boutons de la télécommande de la serrure sont court-circuités par D10 / D11. 
 + 
 +Anciennement, un servomoteur appuyait physiquement sur le bouton de la télécommande ; il a été remplacé par la commande électrique de D9 (code servo conservé en commentaire ci-dessous). 
 + 
 +==== Firmware Arduino (LOV garage opener v3) ==== 
 + 
 +Protocole : un caractère sur le port série (9600 bauds). ''O'' = garage, ''E'' = ouvrir la porte arrière, ''L'' = fermer la porte arrière. Tout autre caractère est ignoré. Au démarrage, l'Arduino affiche ''LOV garage opener v3''. 
 + 
 +<code cpp>
 #include <Servo.h> #include <Servo.h>
  
Ligne 45: Ligne 44:
   pinMode(11, OUTPUT);   pinMode(11, OUTPUT);
   digitalWrite(11, LOW);   digitalWrite(11, LOW);
- 
  
   Serial.begin(9600);   Serial.begin(9600);
Ligne 62: Ligne 60:
     c = Serial.read();     c = Serial.read();
     if (c == 'O') {     if (c == 'O') {
-      //Code for activating the servo of that simulates a click on the real life remote of the garagedoor 
       Serial.println("opening garage...");       Serial.println("opening garage...");
       /*servo.attach(9);       /*servo.attach(9);
Ligne 94: Ligne 91:
 </code> </code>
  
-Pour lancer au boot du serveur dans rc.local:+Vérifié le 10/10/2026 : ce code correspond au firmware présent sur la carte. 
 + 
 +==== Serveur (Atom) ==== 
 + 
 +Pré-requis :
 <code> <code>
-sudo -u lov -g lov /usr/bin/python3 server.py 2>&1 >/var/log/garage.log &+apt install python3-flask python3-serial python3-evdev inotify-tools tmux nginx 
 +adduser lov dialout
 </code> </code>
 +
 +Fichiers dans ''/home/lov/garage'' :
 +
 +^ Fichier ^ Rôle ^
 +| ''server.py'' | Serveur Flask (127.0.0.1:8001) qui envoie ''O'' / ''E'' / ''L'' à l'Arduino (''/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0'') |
 +| ''gate-service.sh'' | Surveille ''rfid.log'', cherche le badge dans ''perm.list'' et ouvre le garage |
 +| ''rfid.py'' | Lit le lecteur RFID USB et ajoute chaque badge lu à ''rfid.log'' |
 +| ''perm.list'' | Badges autorisés, une ligne ''ID pseudo'' |
 +| ''rfid.log'' | Journal de tous les badges passés |
 +
 +**nginx** écoute sur le port **4280** et ne transmet au serveur Flask que les URL secrètes (voir ''/etc/nginx/sites-enabled/default'' sur l'Atom — ne pas les recopier sur le wiki) :
 +
 +^ Route ^ Déclenchée par ^ Action ^ Contrôle ^
 +| ouverture locale (GET) | ''gate-service.sh'' (badge de l'atom) et bouton garage du [[asso:si:serrure|Pi de la porte]] | garage | aucun : réservé au réseau local |
 +| garage (POST) | commande slash Framateam | garage | canal garage ou canal porte |
 +| entrer (POST) | commande slash Framateam | ouvre la porte arrière + message « X a ouvert le local » | canal porte |
 +| sortir (POST) | commande slash Framateam | ferme la porte arrière + message « X a fermé le local » | canal porte |
 +
 +==== Code du serveur (server.py) ====
 +
 +Version en service sur l'atom (''/home/lov/garage/server.py''). Les chemins d'URL et les identifiants de canaux Framateam sont **secrets** : ils sont remplacés ci-dessous par ''<chemin-…>'' et ''<id-canal-…>''. Les vraies valeurs sont dans le fichier sur l'atom.
 +
 +<code python>
 +from flask import Flask, request, jsonify
 +app = Flask(__name__)
 +import serial
 +
 +ARDUINO = '/dev/serial/by-id/usb-1a86_USB_Serial-if00-port0'
 +GARAGE_CHANNEL = '<id-canal-garage>'
 +PORTE_CHANNEL = '<id-canal-porte>'
 +
 +def send_command(c):
 +    try:
 +        with serial.Serial(port=ARDUINO, baudrate=9600) as f:
 +            banner = f.readline().decode('ascii')
 +            if 'LOV' not in banner:
 +                return "didn't find the garage opener banner"
 +            f.write(c)
 +            f.flush()
 +            resp = f.readline().decode('ascii')
 +            return "\n".join((banner, resp))
 +    except serial.serialutil.SerialException as e:
 +        return str(e)
 +
 +# uncondtionnally open the garage (for local usage)
 +@app.route('/<chemin-local>', methods=['GET'])
 +def local_open():
 +    return send_command(b'O')
 +
 +@app.route('/<chemin-garage>', methods=['POST'])
 +def open():
 +    user_name = request.form.get('user_name', 'unknown user')
 +    channel_id = request.form.get('channel_id', '')
 +    if channel_id != GARAGE_CHANNEL and channel_id != PORTE_CHANNEL:
 +        return "this is not the command you are looking for"
 +    return send_command(b'O')
 +
 +@app.route('/<chemin-entrer>', methods=['POST'])
 +def enter():
 +    user_name = request.form.get('user_name', 'unknown user')
 +    channel_id = request.form.get('channel_id', '')
 +    if channel_id != PORTE_CHANNEL:
 +        return "this is not the command you are looking for"
 +    r = send_command(b'E')
 +    return jsonify({
 +        "response_type": "ephemeral",
 +        "text": r,
 +        "extra_responses": [{
 +            "response_type": "in_channel",
 +            "text": "%s a ouvert le local" % user_name,
 +            "channel_id": "<id-canal-annonces>"
 +        }]
 +    })
 +
 +@app.route('/<chemin-sortir>', methods=['POST'])
 +def leave():
 +    user_name = request.form.get('user_name', 'unknown user')
 +    channel_id = request.form.get('channel_id', '')
 +    if channel_id != PORTE_CHANNEL:
 +        return "this is not the command you are looking for"
 +    r = send_command(b'L')
 +    return jsonify({
 +        "response_type": "ephemeral",
 +        "text": r,
 +        "extra_responses": [{
 +            "response_type": "in_channel",
 +            "text": "%s a fermé le local" % user_name,
 +            "channel_id": "<id-canal-annonces>"
 +        }]
 +    })
 +
 +if __name__ == '__main__':
 +    app.run(host='127.0.0.1', port=8001)
 +</code>
 +
 +Fonctionnement :
 +  * ''send_command(c)'' ouvre le port série de l'Arduino. L'ouverture **redémarre l'Arduino**, qui envoie sa bannière ''LOV garage opener v3'' : le serveur la lit pour vérifier qu'il parle bien au boîtier, envoie la lettre (''O'', ''E'' ou ''L'') puis lit la réponse (''opening …''). Compter environ 2 secondes par commande.
 +  * ''local_open'' (GET) : ouvre le garage **sans aucune vérification**. Elle est appelée par le lecteur de badges de l'atom et par le bouton garage du [[asso:si:serrure|Raspberry Pi de la porte]] (gatepi).
 +  * ''open'', ''enter'', ''leave'' (POST) : appelées par les commandes slash de Framateam. Mattermost envoie ''user_name'' et ''channel_id'' ; la commande n'est acceptée que depuis les canaux prévus.
 +  * ''enter'' et ''leave'' répondent en JSON : un message visible seulement par l'auteur (''ephemeral'') et une annonce publique « X a ouvert / fermé le local » dans le canal d'annonces.
 +  * Le serveur n'écoute que sur ''127.0.0.1:8001'' : c'est **nginx** (port 4280) qui le rend joignable, uniquement pour les quatre chemins prévus.
 +
 +==== Code du lecteur de badges (gate-service.sh) ====
 +
 +<code bash>
 +#!/bin/bash
 +
 +cd $(dirname "$0")
 +LOG=./rfid.log
 +
 +(while true ; do python3 rfid.py ; sleep 1 ; done) &
 +
 +validate_id() {
 + awk "/^$1 / { print \$2 }" ./perm.list
 +}
 +
 +while true; do
 + inotifywait -qq -e modify "$LOG"
 + date
 + l=$(tail -n 1 "$LOG")
 + if ! egrep -q '^[0-9A-Z]{8}' <<< "$l" ; then
 + echo "invalid read: $l"
 + continue
 + fi
 + c=$(cut -d ' ' -f 2 <<< "$l")
 + name=$(validate_id "$c")
 + if [ -z "$name" ] ; then
 + echo "unauthorized code $l"
 + continue
 + fi
 + echo "identified $name, opening garage..."
 + curl http://192.168.42.1:4280/<chemin-local> || true
 +done
 +</code>
 +
 +Fonctionnement :
 +  * lance ''rfid.py'' en boucle (il redémarre tout seul si le lecteur est débranché puis rebranché) ;
 +  * ''rfid.py'' lit le lecteur USB, qui se comporte comme un clavier, convertit le numéro du badge au format de l'ancien lecteur (8 caractères) et ajoute une ligne à ''rfid.log'' ;
 +  * à chaque nouvelle ligne de ''rfid.log'' (surveillée par ''inotifywait''), le script cherche l'identifiant dans ''perm.list'' ;
 +  * si le badge est autorisé, il appelle la route locale du serveur (''<chemin-local>'') : le garage s'ouvre.
 +
 +Lancement au démarrage, dans ''/etc/rc.local'' :
 +<code>
 +tmux new -s garage -d 'sudo -u lov -g lov /usr/bin/python3 /home/lov/garage/server.py'
 +tmux new -s rfid -d /home/lov/garage/gate-service.sh
 +</code>
 +
 +Pour voir ce qui se passe : ''sudo tmux attach -t garage'' ou ''sudo tmux attach -t rfid'' (sortir avec ''Ctrl-B'' puis ''D'').
 +
 +==== Ajouter un badge ====
 +
 +  - Passer le badge sur le lecteur.
 +  - Sur l'Atom : ''tail ~/garage/rfid.log'' → noter le 2e champ (8 caractères, ex. ''A1B2C3D4'').
 +  - Ajouter la ligne ''<ID> <pseudo>'' dans ''~/garage/perm.list''. C'est pris en compte immédiatement.
 +
 +==== Dépannage ====
 +
 +  * ''didn't find the garage opener banner'' : l'Arduino ne répond pas → vérifier le câble USB, puis ''screen /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 9600'' doit afficher la bannière.
 +  * ''could not open port'' : Arduino débranché (''ls /dev/serial/by-id/'').
 +  * Le badge ne fait rien : lecteur débranché (''lsusb''), ou badge absent de ''perm.list''.
 +  * L'Arduino répond ''done'' mais la porte ne bouge pas : vérifier la télécommande (alimentation, bouton) — ''done'' veut dire « impulsion envoyée », pas « porte ouverte ».
 +
 +Documentation technique détaillée (rétro-ingénierie, schéma, sauvegarde du firmware) : dépôt AdminLova, ''docs/boitier-garage/''.
 +
  • asso/si/si_garage.1786410435.txt.gz
  • Dernière modification : 2026/08/11 01:07
  • de grapsus