Compare commits

...

5 Commits

4 changed files with 168 additions and 73 deletions

1
.gitignore vendored
View File

@ -103,3 +103,4 @@ ENV/
*.swo *.swo
card_data.json card_data.json
secrets.py

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

209
main.py
View File

@ -1,59 +1,90 @@
import os
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.INFO)
TEST = os.environ.get('TEST', False)
DEBUG = os.environ.get('DEBUG', False)
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
from queue import Empty from queue import Empty
import RPi.GPIO as GPIO
import os
import json import json
import requests import requests
import serial
import time import time
from signal import * from signal import *
DEBUG = os.environ.get('DEBUG', False) if not TEST:
import serial
import RPi.GPIO as GPIO
import secrets
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
VALID_PACKAGES = [
'Maker',
'Maker Plus',
'Maker Pro',
#'Storage bin rental',
#'Backyard Rental Spot',
'IndiCity Laser Space Rental',
#'Shipping container rental',
'Day Pass Holder',
'Access to everything 24/7',
'Barter Membership',
'Loft Member',
]
API_STATS = 'https://api.my.protospace.ca/stats/' TEST_PIPE = '/tmp/airlock'
API_DOOR = 'https://api.my.protospace.ca/door/' os.remove(TEST_PIPE)
API_SEEN = lambda x: 'https://api.my.protospace.ca/door/{}/seen/'.format(x)
API_MEMBERS = 'https://fabman.io/api/v1/members?limit=1000&embed=key&embed=activePackages&includeKeyToken=true'
ser = None ser = None
def unlock_door(): def unlock_door():
GPIO.output(RELAY_PIN, GPIO.HIGH) logging.info('Unlocking door...')
GPIO.output(RFID_EN_PIN, GPIO.HIGH)
time.sleep(OPEN_DURATION) if not TEST:
GPIO.output(RELAY_PIN, GPIO.HIGH)
GPIO.output(RFID_EN_PIN, GPIO.HIGH)
GPIO.output(RELAY_PIN, GPIO.LOW) time.sleep(OPEN_DURATION)
GPIO.output(RFID_EN_PIN, GPIO.LOW)
GPIO.output(RELAY_PIN, GPIO.LOW)
GPIO.output(RFID_EN_PIN, GPIO.LOW)
def lock_door_on_exit(*args): def lock_door_on_exit(*args):
logging.info('Exiting, locking door...') logging.info('Exiting, locking door...')
GPIO.output(RELAY_PIN, GPIO.LOW)
GPIO.output(RFID_EN_PIN, GPIO.LOW) if not TEST:
GPIO.output(RELAY_PIN, GPIO.LOW)
GPIO.output(RFID_EN_PIN, GPIO.LOW)
os._exit(0) os._exit(0)
def init(): def init():
global ser, cards global ser, cards
GPIO.setwarnings(False) if not TEST:
GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False)
GPIO.setup(RELAY_PIN, GPIO.OUT) GPIO.setmode(GPIO.BCM)
GPIO.output(RELAY_PIN, GPIO.LOW) GPIO.setup(RELAY_PIN, GPIO.OUT)
GPIO.setup(RFID_EN_PIN, GPIO.OUT) GPIO.output(RELAY_PIN, GPIO.LOW)
GPIO.output(RFID_EN_PIN, GPIO.LOW) GPIO.setup(RFID_EN_PIN, GPIO.OUT)
logging.info('GPIO initialized') GPIO.output(RFID_EN_PIN, GPIO.LOW)
logging.info('GPIO initialized')
ser = serial.Serial(port='/dev/ttyAMA0', baudrate=2400, timeout=0.1) if TEST:
logging.info('Serial initialized') os.mkfifo(TEST_PIPE)
logging.info('Test pipe initialized')
else:
ser = serial.Serial(port='/dev/ttyAMA0', baudrate=2400, timeout=0.1)
logging.info('Serial initialized')
for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM): for sig in (SIGABRT, SIGILL, SIGINT, SIGSEGV, SIGTERM):
signal(sig, lock_door_on_exit) signal(sig, lock_door_on_exit)
@ -63,83 +94,127 @@ def reader_thread(card_data_queue):
recent_scans = {} recent_scans = {}
with open(CARDS_FILE, 'r') as f: with open(CARDS_FILE, 'r') as f:
card_data = json.load(f) cards = json.load(f)
logging.info('Read {} card numbers from disk'.format(str(len(card_data)))) logging.info('Read {} cards from disk'.format(len(cards)))
while True: while True:
try: try:
card_data = card_data_queue.get_nowait() cards = card_data_queue.get_nowait()
except Empty: except Empty:
pass pass
card = ser.readline() if TEST:
with open(TEST_PIPE, 'r') as pipe:
card = pipe.readline()
else:
card = ser.readline()
if not card: continue if not card: continue
card = card.decode().strip() try:
if len(card) != 10: continue card = card.decode().strip()
except AttributeError:
card = card.strip()
except UnicodeDecodeError:
continue
if len(card) != 14: continue
# 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.info('Debounce skipping card scan')
continue continue
recent_scans[card] = now recent_scans[card] = now
logging.info('Read card: ' + card) logging.info('Read card: ' + card)
if card in card_data: if card in cards:
logging.info('Card recognized') logging.info('Card recognized')
else: else:
logging.info('Card not recognized, denying access') logging.info('Card not recognized, denying access')
continue continue
logging.info('DOOR ACCESS - Card: {} | Name: {}'.format( card_data = cards[card]
card, card_data[card],
)) logging.info('Card belongs to: %s', card_data['name'])
if not any(package in card_data['packages'] for package in VALID_PACKAGES):
logging.info('No valid packages found: %s', str(card_data['packages']))
continue
logging.info('DOOR ACCESS GRANTED - Card: %s | Name: %s', card, card_data['name'])
unlock_door() unlock_door()
try: #try:
res = requests.post(API_SEEN(card), timeout=2) # res = requests.post(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)))
# continue
def get_cards(card_data_queue):
try:
headers = {'Authorization': 'Bearer ' + secrets.FABMAN_API_KEY}
res = requests.get(API_MEMBERS, headers=headers, timeout=10)
res.raise_for_status()
res = res.json()
except BaseException as e:
logging.exception('Problem GETting Fabman API: {} - {}'.format(e.__class__.__name__, str(e)))
return
members = res
cards = {}
logging.info('Got {} members from API'.format(str(len(res))))
for member in members:
if member['state'] != 'active':
continue continue
packages = []
for member_packages in member['_embedded']['memberPackages']:
package = member_packages['_embedded']['package']
if package['state'] != 'active':
continue
packages.append(package['name'])
key = member['_embedded']['key']
if not key:
continue
if key['state'] != 'active':
continue
token = key['token']
name = '{} {} ({})'.format(member['firstName'], member['lastName'], member['memberNumber'])
cards[token] = dict(name=name, packages=packages)
logging.info('Processed {} cards'.format(len(cards)))
card_data_queue.put(cards)
logging.info('Writing data to file')
with open(CARDS_FILE, 'w') as f:
json.dump(cards, f, indent=4)
def update_thread(card_data_queue): def update_thread(card_data_queue):
last_card_change = None if not DEBUG: time.sleep(10)
while True: while True:
time.sleep(5) logging.info('Updating cards...')
get_cards(card_data_queue)
try: time.sleep(300)
res = requests.get(API_STATS, timeout=5)
res.raise_for_status()
res = res.json()
except BaseException as e:
logging.error('Problem GETting stats: {} - {}'.format(e.__class__.__name__, str(e)))
continue
if res['last_card_change'] == last_card_change:
continue
last_card_change = res['last_card_change']
logging.info('Cards changed, pulling update from API')
try:
res = requests.get(API_DOOR, timeout=5)
res.raise_for_status()
res = res.json()
except BaseException as e:
logging.error('Problem GETting door: {} - {}'.format(e.__class__.__name__, str(e)))
continue
logging.info('Got {} cards from API'.format(str(len(res))))
card_data_queue.put(res)
logging.info('Writing data to file')
with open(CARDS_FILE, 'w') as f:
json.dump(res, f)
def watchdog_thread(): def watchdog_thread():
while True: while True:

2
secrets.py.example Normal file
View File

@ -0,0 +1,2 @@
# Fabman API key
FABMAN_API_KEY = ''