Compare commits

...

5 Commits

4 changed files with 57 additions and 22 deletions

View File

@ -4,13 +4,11 @@ Door controller for scanning Protospace member cards on the front and back doors
## Setup ## Setup
Ensure Pi user has read permissions to /dev/ttyACM0 (Pi user needs to be part of the dialout group).
Install dependencies: Install dependencies:
```text ```text
$ sudo apt update $ sudo apt update
$ sudo apt install python3 python3-pip python-virtualenv python3-virtualenv supervisor $ sudo apt install python3 python3-pip python3-virtualenv supervisor git
``` ```
Clone this repo: Clone this repo:
@ -21,16 +19,27 @@ $ sudo mv airlock/ /opt/
$ cd /opt/airlock $ cd /opt/airlock
``` ```
### Watchdog ### Hardware Access
For the watchdog to work, we need write access to `/dev/watchdog/`. Ensure Pi user has read permissions to `/dev/ttyACA0` and `/dev/watchdog`.
Configure `/etc/udev/rules.d/60-watchdog.rules`: Configure `/etc/udev/rules.d/local.rules`:
```text ```text
ACTION=="add", KERNEL=="dialout", MODE="0666"
ACTION=="add", KERNEL=="ttyACM0", MODE="0666"
ACTION=="add", KERNEL=="ttyAMA0", MODE="0666"
KERNEL=="watchdog", MODE="0666" KERNEL=="watchdog", MODE="0666"
``` ```
Also ensure `/boot/cmdline.txt` doesn't contain `console=serial0,115200`.
Then reboot:
```text
$ sudo reboot
```
### Main Script ### Main Script
Create a venv, activate it, and install: Create a venv, activate it, and install:
@ -53,6 +62,13 @@ Now you can run the script to test:
(env) $ DEBUG=true python main.py (env) $ DEBUG=true python main.py
``` ```
Copy and edit the settings file:
```text
(env) $ cp secrets.py.example secrets.py
(env) $ vim secrets.py
```
## Process management ## Process management
The script is kept alive with [supervisor](https://pypi.org/project/supervisor/). The script is kept alive with [supervisor](https://pypi.org/project/supervisor/).
@ -64,6 +80,7 @@ Configure `/etc/supervisor/conf.d/airlock.conf`:
user=pi user=pi
directory=/opt/airlock directory=/opt/airlock
command=/opt/airlock/env/bin/python -u main.py command=/opt/airlock/env/bin/python -u main.py
stopasgroup=true
stopsignal=INT stopsignal=INT
autostart=true autostart=true
autorestart=true autorestart=true

41
main.py
View File

@ -1,12 +1,14 @@
import os
DEBUG = os.environ.get('DEBUG', False)
import logging import logging
logging.basicConfig( logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO) level=logging.DEBUG if DEBUG else logging.INFO)
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
from queue import Empty from queue import Empty
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
import os
import json import json
import requests import requests
import serial import serial
@ -15,17 +17,12 @@ from signal import *
import secrets import secrets
DEBUG = os.environ.get('DEBUG', False)
RELAY_PIN = 17 RELAY_PIN = 17
RFID_EN_PIN = 27 RFID_EN_PIN = 27
CARDS_FILE = 'card_data.json' CARDS_FILE = 'card_data.json'
OPEN_DURATION = 4 OPEN_DURATION = 4
API_STATS = 'https://api.my.protospace.ca/stats/'
API_DOOR = 'https://api.my.protospace.ca/door/'
API_SEEN = lambda x: 'https://api.my.protospace.ca/door/{}/seen/'.format(x)
ser = None ser = None
def unlock_door(): def unlock_door():
@ -54,7 +51,7 @@ def init():
GPIO.output(RFID_EN_PIN, GPIO.LOW) GPIO.output(RFID_EN_PIN, GPIO.LOW)
logging.info('GPIO initialized') logging.info('GPIO initialized')
ser = serial.Serial(port='/dev/ttyAMA0', baudrate=2400, timeout=0.1) ser = serial.Serial(port='/dev/ttyAMA0', baudrate=secrets.BAUD_RATE, timeout=0.05)
logging.info('Serial initialized') logging.info('Serial initialized')
for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM): for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM):
@ -74,16 +71,28 @@ def reader_thread(card_data_queue):
except Empty: except Empty:
pass pass
card = ser.readline() ser.reset_input_buffer()
if not card: continue card = ser.read(100)
card = card.decode().strip() if not card:
if len(card) != 10: continue continue
logging.debug('Raw card read: %s', card)
try:
card = card.decode().strip()
except UnicodeDecodeError:
continue
card = card[1:11]
logging.debug('Card read string: %s', card)
# debounce card scans # debounce card scans
now = time.time() now = time.time()
if card in recent_scans: if card in recent_scans:
if now - recent_scans[card] < 5.0: if now - recent_scans[card] < 5.0:
logging.debug('Debouncing scan')
continue continue
recent_scans[card] = now recent_scans[card] = now
@ -102,7 +111,7 @@ def reader_thread(card_data_queue):
unlock_door() unlock_door()
try: try:
res = requests.post(API_SEEN(card), timeout=2) res = requests.post(secrets.API_SEEN(card), timeout=2)
res.raise_for_status() res.raise_for_status()
except BaseException as e: except BaseException as e:
logging.error('Problem POSTing seen: {} - {}'.format(e.__class__.__name__, str(e))) logging.error('Problem POSTing seen: {} - {}'.format(e.__class__.__name__, str(e)))
@ -115,13 +124,15 @@ def update_thread(card_data_queue):
time.sleep(5) time.sleep(5)
try: try:
res = requests.get(API_STATS, timeout=5) res = requests.get(secrets.API_STATS, timeout=5)
res.raise_for_status() res.raise_for_status()
res = res.json() res = res.json()
except BaseException as e: except BaseException as e:
logging.error('Problem GETting stats: {} - {}'.format(e.__class__.__name__, str(e))) logging.error('Problem GETting stats: {} - {}'.format(e.__class__.__name__, str(e)))
continue continue
logging.debug('Previous last change time: %s, current: %s', last_card_change, res['last_card_change'])
if res['last_card_change'] == last_card_change: if res['last_card_change'] == last_card_change:
continue continue
last_card_change = res['last_card_change'] last_card_change = res['last_card_change']
@ -130,7 +141,7 @@ def update_thread(card_data_queue):
try: try:
headers = {'Authorization': 'Bearer ' + secrets.DOOR_API_KEY} headers = {'Authorization': 'Bearer ' + secrets.DOOR_API_KEY}
res = requests.get(API_DOOR, headers=headers, timeout=5) res = requests.get(secrets.API_DOOR, headers=headers, timeout=5)
res.raise_for_status() res.raise_for_status()
res = res.json() res = res.json()
except BaseException as e: except BaseException as e:

View File

@ -3,5 +3,4 @@ chardet==3.0.4
idna==2.9 idna==2.9
pyserial==3.4 pyserial==3.4
requests==2.23.0 requests==2.23.0
RPi.GPIO==0.7.0
urllib3==1.25.8 urllib3==1.25.8

View File

@ -1,3 +1,11 @@
# Door cards API key # Door cards API key
# should be equal to the auth token value set in Spaceport # should be equal to the auth token value set in Spaceport
DOOR_API_KEY = '' DOOR_API_KEY = ''
# Door API routes
API_STATS = 'https://api.my.protospace.ca/stats/'
API_DOOR = 'https://api.my.protospace.ca/door/'
API_SEEN = lambda x: 'https://api.my.protospace.ca/door/{}/seen/'.format(x)
# Card reader settings
BAUD_RATE = 2400