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

161
main.py
View File

@ -1,32 +1,55 @@
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():
logging.info('Unlocking door...')
if not TEST:
GPIO.output(RELAY_PIN, GPIO.HIGH) GPIO.output(RELAY_PIN, GPIO.HIGH)
GPIO.output(RFID_EN_PIN, GPIO.HIGH) GPIO.output(RFID_EN_PIN, GPIO.HIGH)
@ -37,13 +60,17 @@ def unlock_door():
def lock_door_on_exit(*args): def lock_door_on_exit(*args):
logging.info('Exiting, locking door...') logging.info('Exiting, locking door...')
if not TEST:
GPIO.output(RELAY_PIN, GPIO.LOW) GPIO.output(RELAY_PIN, GPIO.LOW)
GPIO.output(RFID_EN_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
if not TEST:
GPIO.setwarnings(False) GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
GPIO.setup(RELAY_PIN, GPIO.OUT) GPIO.setup(RELAY_PIN, GPIO.OUT)
@ -52,6 +79,10 @@ def init():
GPIO.output(RFID_EN_PIN, GPIO.LOW) GPIO.output(RFID_EN_PIN, GPIO.LOW)
logging.info('GPIO initialized') logging.info('GPIO initialized')
if TEST:
os.mkfifo(TEST_PIPE)
logging.info('Test pipe initialized')
else:
ser = serial.Serial(port='/dev/ttyAMA0', baudrate=2400, timeout=0.1) ser = serial.Serial(port='/dev/ttyAMA0', baudrate=2400, timeout=0.1)
logging.info('Serial initialized') logging.info('Serial initialized')
@ -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
if TEST:
with open(TEST_PIPE, 'r') as pipe:
card = pipe.readline()
else:
card = ser.readline() card = ser.readline()
if not card: continue if not card: continue
try:
card = card.decode().strip() card = card.decode().strip()
if len(card) != 10: continue 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:
# res = requests.post(API_SEEN(card), timeout=2)
# res.raise_for_status()
#except BaseException as e:
# logging.error('Problem POSTing seen: {} - {}'.format(e.__class__.__name__, str(e)))
# continue
def get_cards(card_data_queue):
try: try:
res = requests.post(API_SEEN(card), timeout=2) headers = {'Authorization': 'Bearer ' + secrets.FABMAN_API_KEY}
res.raise_for_status() res = requests.get(API_MEMBERS, headers=headers, timeout=10)
except BaseException as e:
logging.error('Problem POSTing seen: {} - {}'.format(e.__class__.__name__, str(e)))
continue
def update_thread(card_data_queue):
last_card_change = None
while True:
time.sleep(5)
try:
res = requests.get(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.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
if res['last_card_change'] == last_card_change: packages = []
continue
last_card_change = res['last_card_change']
logging.info('Cards changed, pulling update from API') for member_packages in member['_embedded']['memberPackages']:
package = member_packages['_embedded']['package']
try: if package['state'] != 'active':
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 continue
logging.info('Got {} cards from API'.format(str(len(res)))) packages.append(package['name'])
card_data_queue.put(res)
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') logging.info('Writing data to file')
with open(CARDS_FILE, 'w') as f: with open(CARDS_FILE, 'w') as f:
json.dump(res, f) json.dump(cards, f, indent=4)
def update_thread(card_data_queue):
if not DEBUG: time.sleep(10)
while True:
logging.info('Updating cards...')
get_cards(card_data_queue)
time.sleep(300)
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 = ''