Initial commit

This commit is contained in:
Lynn Ochs 2021-03-08 21:54:21 +01:00
commit 230397f0da
12 changed files with 5257 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/sdcard.img

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "buildroot"]
path = buildroot
url = https://github.com/buildroot/buildroot.git

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
all: sdcard.img
sdcard.img: buildroot/.config root_overlay/root/.ssh/authorized_keys
make -C buildroot all
cp buildroot/output/images/sdcard.img sdcard.img
root_overlay/root/.ssh/authorized_keys: ~/.ssh/id_rsa.pub
cp ~/.ssh/id_rsa.pub root_overlay/root/.ssh/authorized_keys
buildroot/.config: config
ln -sf ../config buildroot/.config
clean:
make -C buildroot clean
rm -f sdcard.img
rm -f root_overlay/root/.ssh/authorized_keys
rm -f root_overlay/bin/poll_desfire
rm -f root_overlay/bin/test.py
menuconfig:
make -C buildroot menuconfig
.PHONY: all clean sdcard.img menuconfig

1
buildroot Submodule

@ -0,0 +1 @@
Subproject commit 21eb7775510d76163c2159bca14d3802283119e3

4875
config Normal file

File diff suppressed because it is too large Load Diff

BIN
root_overlay/bin/poll_desfire Executable file

Binary file not shown.

340
root_overlay/bin/test.py Normal file
View File

@ -0,0 +1,340 @@
#!/usr/bin/python
import os, serial, socket, subprocess, select, datetime, sys
import paho.mqtt.client as mqcl
import argparse
OPEN_THRESHOLD = 80
CLOSED_THRESHOLD = 100
CLOSED_WANT = 160
parser = argparse.ArgumentParser()
parser.add_argument("--serial_port", default="/dev/serial/by-id/usb-Imaginaerraum.de_DoorControl_43363220195053573A002C0-if01")
parser.add_argument("--nfc_fifo", default="/tmp/nfc_fifo")
parser.add_argument("--control_socket", default="/tmp/nfc.sock")
parser.add_argument("--valid_tokens", default="/etc/door_tokens")
parser.add_argument("--log_file", default="/tmp/nfc.log")
parser.add_argument("--state_timeout", type=float, default=10)
parser.add_argument("--repeat_time", type=float, default=5)
parser.add_argument("--mqtt_host", default="10.10.21.2")
config = parser.parse_args()
mqttc = mqcl.Client()
def timestamp():
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Log data to stdout and the log file
def log(*args):
data = "%s %s" % (timestamp(), " ".join(str(i) for i in args))
lgf.write(data + "\n")
lgf.flush()
print(data)
def mqtt(topic, msg, persistent = True):
mqttc.publish("door/" + topic, msg, qos=2, retain=persistent)
# Opens the socket that can control the daemon
# Commands are TBD
def open_control_socket():
global control_socket
global config
global comm_channels
if os.path.exists(config.control_socket):
os.unlink(config.control_socket)
control_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
control_socket.bind(config.control_socket)
control_socket.listen(5)
comm_channels = []
# Opens the serial port to talk to the lock actuator
# Might return a failure. In that case, it will be tried again later
def open_serial_port():
global serial_port
global config
try:
serial_port = serial.Serial(config.serial_port, timeout=2)
except:
serial_port = None
pass
# Detected tokens are passed in through a FIFO
def open_nfc_fifo():
global nfc_fifo
global config
nfc_fifo = open(config.nfc_fifo, "r")
# Refresh valid tokens from the configured file
def read_valid_tokens():
global valid
global config
try:
log("Loading tokens")
valid = {}
lines =[ s.strip() for s in open(config.valid_tokens, "r").readlines() ]
for l in lines:
l = l.split(' ', 1)
if len(l) > 1:
if l[0] in valid:
log("Warning: Overwriting token %s" % (l[0],))
log("Got token: %s (%s)" % (l[0], l[1]))
valid[l[0]] = l[1]
else:
log("Got unnamed token: %s" % (l[0],))
valid[l[0]] = None
except:
valid = {}
log("Error reading token file")
raise
# Opens the log file for writing
def open_logfile():
global lgf
global config
lgf = open(config.log_file, "a+")
# Actions
IDLE, CLOSE, OPEN_THEN_CLOSE, OPEN, CLOSE_THEN_OPEN = range(5)
state_names = {
OPEN: "open",
CLOSE: "close"
}
action_names = {
IDLE: "idling",
OPEN: "waiting for open door",
CLOSE: "waiting for closed door",
OPEN_THEN_CLOSE: "waiting for open door, then closing again",
CLOSE_THEN_OPEN: "waiting for closed door, then opening again"
}
# Current door state
state = None
state_pos = 0
# Current target action
action = CLOSE
# Start time of the current action
start_time = None
# How often the action was repeated
repeats = 0
# Read the state of the door from the serial port
def poll_door_state():
global state
global state_pos
global serial_port
serial_port.reset_input_buffer()
serial_port.write(b"R")
data = serial_port.readline().strip().split()
if data[0] == b'pos:':
data = int(data[1])
if state_pos != data:
mqtt("position/value", data, True)
state_pos = data
changed = False
if data < OPEN_THRESHOLD and state != OPEN:
state = OPEN
changed = True
mqtt("state/value", "open", True)
elif data > CLOSED_THRESHOLD and state != CLOSE:
state = CLOSE
changed = True
mqtt("state/value", "closed", True)
return state
# Check the door state and send commands through a state machine
def handle_door_state():
global action, target, serial_port, start_time, repeats
# Commands associated with each target state
target_state_cmd = {
OPEN: b'O',
CLOSE: b'C'
}
old_state = state
# If no serial port, try to open it or return
if not serial_port:
try:
open_serial_port()
except:
return
# Get new state
poll_door_state()
# Idle + change = key?
if action == IDLE:
if state != old_state:
log("Door changed unexpectedly:", state_names[state])
start_time = datetime.datetime.now()
if start_time and (datetime.datetime.now() - start_time).total_seconds() >= config.state_timeout:
start_time = None
if state_pos >= CLOSED_THRESHOLD and state_pos < CLOSED_WANT:
log("Closing door a bit more")
serial_port.write(target_state_cmd[CLOSE])
return
# Target state, next action, timeout action
actions = {
OPEN: (OPEN, IDLE, CLOSE_THEN_OPEN ),
CLOSE: (CLOSE, IDLE, OPEN_THEN_CLOSE ),
OPEN_THEN_CLOSE: (OPEN, CLOSE, CLOSE ),
CLOSE_THEN_OPEN: (CLOSE, OPEN, OPEN )
}
if start_time == None:
start_time = datetime.datetime.now()
serial_port.write(target_state_cmd[actions[action][0]])
# Target state reached
if state == actions[action][0]:
# Select next action, reset start time and repetitions
action = actions[action][1]
start_time = datetime.datetime.now()
repeats = 0
# On idle, we're done
if action == IDLE:
log("Reached target position:", state_names[state])
return
# Execution time
t = (datetime.datetime.now() - start_time).total_seconds()
if t >= config.state_timeout:
# Timeout -> switch to timeout action
action = actions[action][2]
start_time = None
repeats = 0
log("Timeout. Switching to", action_names[action])
elif t >= (1 + repeats) * config.repeat_time:
# Repeat every couple of seconds
repeats += 1
serial_port.write(target_state_cmd[actions[action][0]])
log("Repeating command:", target_state_cmd[actions[action][0]])
def open_door():
global action
global start_time
log("Opening the door")
action = OPEN
start_time = None
mqtt("state/target", "open", True)
handle_door_state()
def close_door():
global action
global start_time
log("Closing the door")
action = CLOSE
start_time = None
mqtt("state/target", "closed", True)
handle_door_state()
def toggle_door_state():
global state, action
if state == CLOSE:
open_door()
else:
close_door()
def handle_nfc_token(token = None):
global valid
if not token:
token = nfc_fifo.readline()
if token == "":
open_nfc_fifo()
token = token.strip()
if token in valid:
name = valid[token]
if name:
name = "%s (%s)" % (token, name)
else:
name = token
log("Valid token: %s" % name)
toggle_door_state()
else:
log("Invalid token:", token)
mqtt("token/last_invalid", "%s;%s" % (timestamp(), token))
class LineBuffer(object):
def __init__(self, f, handler):
self.data = b''
self.f = f
self.handler = handler
def update(self):
data = self.f.recv(1024)
if not data:
return False
self.data += data
d = self.data.split(b'\n')
d, self.data = d[:-1], d[-1]
for i in d:
self.handler(self.f, i)
return True
def handle_cmd(comm, data):
cmd = data.decode('utf8').split()
cmd, args = cmd[0], cmd[1:]
log("Got command:", data)
send = lambda x: comm.send(x.encode('utf8'))
if cmd == 'fake':
log("Faking token", args[0])
send("Handling token\n")
handle_nfc_token(args[0])
elif cmd == 'open':
log("Control socket opening door")
send("Opening door")
open_door()
elif cmd == 'close':
log("Control socket closing door")
send("Closing door")
close_door()
elif cmd == 'rld':
log("Reloading tokens")
send("Reloading tokens")
read_valid_tokens()
elif cmd == 'stat':
send("Door status is %s, position is %d. Current action: %s (%g seconds ago)\n" % (
state_names.get(state, "None"),
state_pos,
action_names[action],
(datetime.datetime.now() - start_time).total_seconds()))
open_logfile()
read_valid_tokens()
open_nfc_fifo()
open_serial_port()
open_control_socket()
mqttc.on_connect = lambda client, userdata, flags, rc: log("Connected to mqtt host with result %s" % (str(rc), ))
mqttc.connect_async(config.mqtt_host, keepalive = 60)
mqttc.loop_start()
buffers = {}
while True:
readable = select.select([ nfc_fifo, control_socket ] + comm_channels, [], [], 1)[0]
for c in readable:
if c == nfc_fifo:
handle_nfc_token()
elif c == control_socket:
log("Got connection")
sock = control_socket.accept()[0]
buffers[sock] = LineBuffer(sock, handle_cmd)
comm_channels += [sock]
else:
if not buffers[c].update():
log("Lost connection")
del buffers[c]
comm_channels.remove(c)
handle_door_state()

6
root_overlay/bin/watcher Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
while true; do
start-stop-daemon -S -m -p /tmp/poll_desfire.pid -x /bin/poll_desfire -b -- /root/key /tmp/nfc_fifo
start-stop-daemon -v -p /tmp/fcpy.pid -S -b -m -v -x /usr/bin/python -- /bin/test.py
sleep 30
done

View File

@ -0,0 +1,2 @@
wpa_supplicant -Dnl80211 -iwlan0 -c/etc/wpa_supplicant/wpa_supplicant.conf -B
dhcpcd wlan0

View File

@ -0,0 +1,3 @@
mkfifo /var/nfc_fifo
start-stop-daemon -S -m -p /tmp/watcher.pid -x /bin/watcher -b

View File

@ -0,0 +1,2 @@
device.name="reader"
device.connstring="pn532_uart:/dev/serial/by-id/usb-Imaginaerraum.de_DoorControl_43363220195053573A002C0-if03"

View File

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5pAEc3dFnR7zDtdIaH3ICBQincnxs7HlIi+JEfl3M23S/wkQASE9hDpNwAF/CRu9u2cjnXgHNqVG0TkKyjWgfXk9tTpyVBNw8QkHQ75TpbKsuhWQm/LgYb874j4+2pDGxLLdVVuY1c73vNQqkHL0xS3aG/dtvCc7XMNg5VRWdwL3WPhkmuBQK7JYUkUnf+dZWS91oJWZHd/OEU/H9p147UGw3ffLQ9H+IKxl3pvEMKz8Aeca1sDtSS0Z2LXzh3au5KynMR6KHBX9QUM3Bkoy20QqnNZz/sQX04NrOigoPDOFRlOaeIXeZvecZRP7LnTy7JviWFylMwziwzEJslDLN apo@mae