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/10/10 17:45] – Mise à jour : électronique (schéma Fritzing, photos), serveur nginx/Flask, badges RFID, dépannage nrineauasso: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 6: Ligne 6:
   * **Badge RFID** : un badge de la liste ouvre le **garage** uniquement.   * **Badge RFID** : un badge de la liste ouvre le **garage** uniquement.
  
-<code> +{{ :asso:si:garage_archify.png?direct&900 |Architecture : ouverture du garage et de la porte arrière (Archify)}} 
-Framateam ──HTTP :4280──► nginx ──► server.py (Flask, 127.0.0.1:8001) ──USB série 9600──► Arduino ──► télécommande garage (D9) + 
-Badge ──► lecteur RFID USB ──► rfid.py ──► rfid.log ──► gate-service.sh (perm.list) ──┘          └──► télécommande serrure (D10 / D11) +//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.//
-</code>+
  
 ==== Électronique ==== ==== Électronique ====
Ligne 15: Ligne 14:
 {{ :asso:si:boitier_garage_fritzing.png?direct&700 |Schéma de câblage du boîtier (Fritzing)}} {{ :asso:si:boitier_garage_fritzing.png?direct&700 |Schéma de câblage du boîtier (Fritzing)}}
  
-Schéma Fritzing modifiable : {{:asso:si:boitier-garage.fzz|boitier-garage.fzz}}+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_vue_ensemble.jpg?300|Vue d'ensemble de l'intérieur du boîtier}}
Ligne 114: Ligne 113:
  
 ^ Route ^ Déclenchée par ^ Action ^ Contrôle ^ ^ Route ^ Déclenchée par ^ Action ^ Contrôle ^
-| ouverture locale (GET) | ''gate-service.sh'' (badge) | garage | aucun : réservé à l'usage local |+| 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 | | 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 | | 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 | | 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'' : Lancement au démarrage, dans ''/etc/rc.local'' :
  • asso/si/si_garage.1791654333.txt.gz
  • Dernière modification : 2026/10/10 17:45
  • de nrineau