--- /dev/null
+__pycache__
--- /dev/null
+files = *.py tuerd tryshell tyshell
+target = /opt/tuer/
+
+all: install
+.PHONY: install
+
+install:
+ cp -v $(files) $(target)
--- /dev/null
+from libtuer import ThreadFunction, logger
+import RPi.GPIO as GPIO
+import time
+
+class Actor:
+ CMD_BUZZ = 0
+ CMD_UNLOCK = 1
+ CMD_LOCK = 2
+ CMD_GREEN_ON = 3
+ CMD_GREEN_OFF = 4
+ CMD_RED_ON = 5
+ CMD_RED_OFF = 6
+
+ class CMD():
+ def __init__(self, name, pin, tid, todo):
+ self.name = name
+ self.pin = pin
+ self.tid = tid
+ self.todo = todo
+ # don't do the GPIO setup here, the main init did not yet run
+
+ def execute(self):
+ logger.info("Actor: Running command %s" % self.name)
+ for (value, delay) in self.todo:
+ if value is not None:
+ logger.debug("Actor: Setting pin %d to %d" % (self.pin, value))
+ GPIO.output(self.pin, value)
+ if delay > 0:
+ time.sleep(delay)
+
+ CMDs = {
+ CMD_UNLOCK: CMD("unlock", pin=12, tid=0, todo=[(True, 0.3), (False, 0.1)]),
+ CMD_LOCK: CMD("lock", pin=16, tid=0, todo=[(True, 0.3), (False, 0.1)]),
+ CMD_BUZZ: CMD("buzz", pin=22, tid=1, todo=[(True, 2.5), (False, 0.1)]),
+ CMD_GREEN_ON: CMD("green on", pin=23, tid=2, todo=[(True, 0)]),
+ CMD_GREEN_OFF: CMD("green off", pin=23, tid=2, todo=[(False, 0)]),
+ CMD_RED_ON: CMD("red on", pin=26, tid=2, todo=[(True, 0)]),
+ CMD_RED_OFF: CMD("red off", pin=26, tid=2, todo=[(False, 0)]),
+ }
+
+ def __init__(self):
+ # launch threads, all running the "_execute" method
+ self.threads = {}
+ for cmd in Actor.CMDs.values():
+ GPIO.setup(cmd.pin, GPIO.OUT)
+ GPIO.output(cmd.pin, False)
+ if not cmd.tid in self.threads:
+ self.threads[cmd.tid] = ThreadFunction(self._execute, "Actor TID %d" % cmd.tid)
+
+ def _execute(self, cmd):
+ Actor.CMDs[cmd].execute()
+
+ def act(self, cmd):
+ # dispatch command to correct thread
+ self.threads[Actor.CMDs[cmd].tid](cmd)
+
+ def stop(self):
+ for thread in self.threads.values():
+ thread.stop()
-import logging, logging.handlers, syslog, os
+import logging, logging.handlers, os, time, queue, threading, subprocess
+import traceback, smtplib
+import email.mime.text, email.utils
+
+# Logging configuration
+syslogLevel = logging.INFO
+mailLevel = logging.CRITICAL # must be "larger" than syslog level!
+mailAddress = ['post+tuer'+'@'+'ralfj.de', 'vorstand@lists.hacksaar.de']
+printLevel = logging.DEBUG
+
+# Mail logging handler
+def sendeMail(subject, text, receivers, sender='sphinx@hacksaar.de', replyTo=None):
+ assert isinstance(receivers, list)
+ if not len(receivers): return # nothing to do
+ # construct content
+ msg = email.mime.text.MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')
+ msg['Subject'] = subject
+ msg['Date'] = email.utils.formatdate(localtime=True)
+ msg['From'] = sender
+ msg['To'] = ', '.join(receivers)
+ if replyTo is not None:
+ msg['Reply-To'] = replyTo
+ # put into envelope and send
+ s = smtplib.SMTP('localhost')
+ s.sendmail(sender, receivers, msg.as_string())
+ s.quit()
# logging function
class Logger:
def __init__ (self):
- import __main__ as main
- self.name = os.path.basename(main.__file__)
- self.logger = logging.getLogger(self.name)
- self.logger.setLevel(logging.INFO)
- self.handler = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_LOCAL0)
- self.logger.addHandler(self.handler)
- def log (self, what):
- thestr = "%s[%d]: %s" % (self.name,os.getpid(),what)
- print (thestr)
- self.logger.info(thestr)
+ self.syslog = logging.getLogger("tuerd")
+ self.syslog.setLevel(logging.DEBUG)
+ self.syslog.addHandler(logging.handlers.SysLogHandler(address = '/dev/log',
+ facility = logging.handlers.SysLogHandler.LOG_LOCAL0))
+
+ def _log (self, lvl, what):
+ thestr = "%s[%d]: %s" % ("tuerd", os.getpid(), what)
+ # console log
+ if lvl >= printLevel:
+ print(thestr)
+ # syslog
+ if lvl >= syslogLevel:
+ self.syslog.log(lvl, thestr)
+ # mail log
+ if lvl >= mailLevel and mailAddress is not None:
+ sendeMail('Kritischer Türfehler', what, mailAddress)
+
+ def debug(self, what):
+ self._log(logging.DEBUG, what)
+ def info(self, what):
+ self._log(logging.INFO, what)
+ def warning(self, what):
+ self._log(logging.WARNING, what)
+ def error(self, what):
+ self._log(logging.ERROR, what)
+ def critical(self, what):
+ self._log(logging.CRITICAL, what)
logger = Logger()
-def log (what):
- logger.log(what)
-
+# run a command asynchronously and log the return value if not 0
+# prefix must be a string identifying the code position where the call came from
+def fire_and_forget (cmd, log, prefix):
+ def _fire_and_forget ():
+ with open("/dev/null", "w") as fnull:
+ retcode = subprocess.call(cmd, stdout=fnull, stderr=fnull)
+ if retcode is not 0:
+ logger.error("%sReturn code %d at command: %s" % (prefix,retcode,str(cmd)))
+ t = threading.Thread(target=_fire_and_forget)
+ t.start()
# Threaded callback class
class ThreadFunction():
_CALL = 0
_TERM = 1
- def __init__(self, f):
+ def __init__(self, f, name):
+ self.name = name
self._f = f
self._q = queue.Queue()
self._t = threading.Thread(target=self._thread_func)
while True:
(cmd, data) = self._q.get()
# run command
- if cmd == _CALL:
- self._f(*data)
- elif cmd == _TERM:
+ if cmd == ThreadFunction._CALL:
+ try:
+ self._f(*data)
+ except Exception as e:
+ logger.critical("ThreadFunction: Got exception out of handler thread %s: %s" % (self.name, str(e)))
+ logger.debug(traceback.format_exc())
+ elif cmd == ThreadFunction._TERM:
assert data is None
break
else:
- raise NotImplementedError("Command %d does not exist" % cmd)
+ logger.error("ThreadFunction: Command %d does not exist" % cmd)
def __call__(self, *arg):
- self._q.put((self._CALL, arg))
+ self._q.put((ThreadFunction._CALL, arg))
+
+ def stop(self):
+ self._q.put((ThreadFunction._TERM, None))
+ self._t.join()
+
+# Thread timer-repeater class: Call a function every <sleep_time> seconds
+class ThreadRepeater():
+ def __init__(self, f, sleep_time, name):
+ self.name = name
+ self._f = f
+ self._stop = False
+ self._sleep_time = sleep_time
+ self._t = threading.Thread(target=self._thread_func)
+ self._t.start()
+
+ def _thread_func(self):
+ while True:
+ if self._stop:
+ break
+ try:
+ self._f()
+ except Exception as e:
+ logger.critical("ThreadRepeater: Got exception out of handler thread %s: %s" % (self.name, str(e)))
+ logger.debug(traceback.format_exc())
+ time.sleep(self._sleep_time)
def stop(self):
- self._q.put((_TERM, None))
+ self._stop = True
self._t.join()
--- /dev/null
+import RPi.GPIO as GPIO
+from collections import namedtuple
+from libtuer import ThreadRepeater, logger
+from statemachine import StateMachine
+
+class PinsState():
+ pass
+
+class PinWatcher():
+ def __init__(self, pin, histlen):
+ GPIO.setup(pin, GPIO.IN)
+ assert histlen > 1 # otherwise our logic goes nuts...
+ self.pin = pin
+ self._histlen = histlen
+ # state change detection
+ self.state = None
+ self._newstate = None # != None iff we are currently seeing a state change
+ self._newstatelen = 0 # only valid if newstate != None
+
+ def read(self):
+ curstate = GPIO.input(self.pin)
+ assert curstate in (0, 1)
+ if curstate != self.state:
+ # the state is about to change
+ if curstate == self._newstate:
+ # we already saw this new state
+ self._newstatelen += 1
+ if self._newstatelen >= self._histlen:
+ # we saw it often enough to declare it the new state
+ self.state = curstate
+ self._newstate = None
+ return True
+ else:
+ # now check for how long we see this new state
+ self._newstate = curstate
+ self._newstatelen = 1
+ else:
+ # old state is preserved
+ self._newstate = None
+ return False
+
+class PinsWatcher():
+ def __init__(self, state_machine):
+ self._pins = {
+ 'bell_ringing': PinWatcher(18, 2),
+ 'door_closed': PinWatcher(8, 4),
+ 'door_locked': PinWatcher(10, 4),
+ 'space_active': PinWatcher(24, 4),
+ }
+ self._sm = state_machine
+
+ # start a thread doing the work
+ self._t = ThreadRepeater(self._read, 0.02, name="PinsWatcher")
+
+ def _read(self):
+ saw_change = False
+ for name in self._pins.keys():
+ pin = self._pins[name]
+ if pin.read():
+ saw_change = True
+ logger.debug("Pin %s changed to %d" % (name, pin.state))
+ if not saw_change:
+ return None
+ # create return object
+ pinsState = PinsState()
+ for name in self._pins.keys():
+ setattr(pinsState, name, self._pins[name].state)
+ # send it to state machine
+ self._sm.callback(StateMachine.CMD_PINS, pinsState)
+
+ def stop(self):
+ self._t.stop()
+++ /dev/null
-#!/usr/bin/python3
-import time, socket, atexit
-import queue, threading, select
-from libtuer import log, ThreadFunction
-import RPi.GPIO as GPIO
-GPIO.setmode(GPIO.BOARD)
-atexit.register(GPIO.cleanup)
-
-tuerSock = "/run/tuer.sock"
-ringPin = 18
-
-
-# Main classes
-class PinWatcher():
- def __init__(self, pin, histlen):
- GPIO.setup(pin, GPIO.IN)
- assert histlen > 1 # otherwise our logic goes nuts...
- self._pin = pin
- self._histlen = histlen
- # state change detection
- self._state = None
- self._newstate = None # != None iff we are currently seeing a state change
- self._newstatelen = 0 # only valid if newstate != None
- # start state change handler thread
- self._callback = ThreadFunction(self.callback)
- self.stop = self._callback.stop
-
- def read(self):
- curstate = GPIO.input(self._pin)
- assert curstate in (0, 1)
- if curstate != self._state:
- # the state is about to change
- if curstate == self._newstate:
- # we already saw this new state
- self._newstatelen += 1
- if self._newstatelen >= self._histlen:
- self._callback(self._state, curstate) # send stuff to the other thread
- self._state = curstate
- self._newstate = None
- else:
- # now check for how long we see this new state
- self._newstate = curstate
- self._newstatelen = 1
- else:
- # old state is preserved
- self._newstate = None
-
-class RingWatcher(PinWatcher):
- def __init__(self):
- super().__init__(ringPin, 2)
- self.last1Event = None
-
- def callback(self, oldstate, newstate):
- if oldstate is None:
- return # ignore the very first state change
- # now (oldstate, newstate) is either (0, 1) or (1, 0)
- if newstate:
- self.last1Event = time.time()
- elif self.last1Event is not None:
- # how long was this pressed?
- timePressed = time.time() - self.last1Event
- log("Ring button pressed for",timePressed)
- if timePressed >= 1.5 and timePressed <= 3:
- self.buzz()
-
- def buzz(self):
- log("Opening door")
- # talk with tuerd
- s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- s.connect(tuerSock)
- s.send(b'buzz')
- s.close()
-
-# MAIN PROGRAM
-pins = [
- RingWatcher(),
-]
-
-try:
- log("entering loop")
- while True:
- for pin in pins:
- pin.read()
- time.sleep(0.02)
-except KeyboardInterrupt:
- for pin in pins:
- pin.stop()
--- /dev/null
+from libtuer import ThreadFunction, logger, fire_and_forget
+from actor import Actor
+import os, random, time
+
+# logger.{debug,info,warning,error,critical}
+
+def play_sound (what):
+ try:
+ soundfiles = os.listdir(SOUNDS_DIRECTORY+what)
+ except FileNotFoundError:
+ logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
+ return
+ soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
+ fire_and_forget ([SOUNDS_PLAYER,soundfile], logger.error, "StateMachine: ")
+
+
+# StateUnlocking constants
+OPEN_REPEAT_TIMEOUT = 7
+OPEN_REPEAT_NUMBER = 3
+
+# StateLocking constants
+CLOSE_REPEAT_TIMEOUT = 7
+CLOSE_REPEAT_NUMBER = 3
+
+# StateAboutToOpen constants
+ABOUTOPEN_NERVLIST = [(5, lambda : play_sound("flipswitch")), (5, lambda:play_sound("flipswitch")), (0, lambda:logger.warning("Space open but switch not flipped for 10 seconds")),\
+ (10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (0, lambda:logger.error("Space open but switch not flipped for 30 seconds")),\
+ (10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (6, lambda:play_sound("flipswitch")), (4, lambda:logger.critical("Space open but switch not flipped for 60 seconds")),
+ (59*60, lambda:logger.critical("Space open but switch not flipped for one hour"))]
+
+# Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it
+# ALso the time we wait after the door was opend, till we assume something went wrong and start nerving
+LEAVE_TIMEOUT = 20
+
+# play_sound constants
+SOUNDS_DIRECTORY = "/opt/tuer/sounds/"
+SOUNDS_PLAYER = "/usr/bin/mplayer"
+
+
+class Nerver():
+ # A little utility class used to run the nervlists. A nervlist is a list of (n, f) tuples where f() is run after n seconds.
+ # If f returns something, that's also returned by nerv.
+ def __init__(self, nervlist):
+ self.nervlist = list(nervlist)
+ self.last_event_time = time.time()
+
+ def nerv(self):
+ if len(self.nervlist):
+ (wait_time, f) = self.nervlist[0]
+ now = time.time()
+ time_gone = now-self.last_event_time
+ # check if the first element is to be triggered
+ if time_gone >= wait_time:
+ self.nervlist = self.nervlist[1:] # "pop" the first element, but do not modify original list
+ self.last_event_time = now
+ return f()
+
+
+class StateMachine():
+ # commands you can send
+ CMD_PINS = 0
+ CMD_BUZZ = 1
+ CMD_UNLOCK = 2
+ CMD_WAKEUP = 3
+ CMD_LAST = 4
+
+ class State():
+ def __init__(self, state_machine, nervlist = None):
+ self.state_machine = state_machine
+ self._nerver = None if nervlist is None else Nerver(nervlist)
+ def handle_pins_event(self):
+ pass # one needn't implement this
+ def handle_buzz_event(self,arg): # this shouldn't be overwritten
+ self.actor().act(Actor.CMD_BUZZ)
+ arg("200 okay: buzz executed")
+ def handle_cmd_unlock_event(self,arg):
+ if arg is not None:
+ arg("412 Precondition Failed: The current state (%s) cannot handle the UNLOCK command. Try again later." % self.__class__.__name__)
+ def handle_wakeup_event(self):
+ if self._nerver is not None:
+ return self._nerver.nerv()
+ def on_leave(self):
+ pass
+ def pins(self):
+ return self.state_machine.pins
+ def old_pins(self):
+ return self.state_machine.old_pins
+ def actor(self):
+ return self.state_machine.actor
+ def handle_event(self,ev,arg): # don't override
+ if ev == StateMachine.CMD_PINS:
+ return self.handle_pins_event()
+ elif ev == StateMachine.CMD_BUZZ:
+ return self.handle_buzz_event(arg)
+ elif ev == StateMachine.CMD_UNLOCK:
+ return self.handle_cmd_unlock_event(arg)
+ elif ev == StateMachine.CMD_WAKEUP:
+ return self.handle_wakeup_event()
+ else:
+ raise Exception("Unknown command number: %d" % ev)
+
+ class AbstractNonStartState(State):
+ def handle_pins_event(self):
+ if self.pins().door_locked != (not self.pins().space_active):
+ self.actor().act(Actor.CMD_RED_ON)
+ else:
+ self.actor().act(Actor.CMD_RED_OFF)
+ return super().handle_pins_event()
+
+ class AbstractLockedState(AbstractNonStartState):
+ '''A state with invariant "The space is locked", switching to StateAboutToOpen when the space becomes unlocked'''
+ def __init__(self, sm, nervlist = None):
+ super().__init__(sm, nervlist)
+ self.actor().act(Actor.CMD_GREEN_OFF)
+ def handle_pins_event(self):
+ if not self.pins().door_locked:
+ logger.info("Door unlocked, space is about to open")
+ return StateMachine.StateAboutToOpen(self.state_machine)
+ if not self.old_pins().space_active and self.pins().space_active:
+ logger.info("Space toggled to active while it was closed - unlocking the door")
+ return StateMachine.StateUnlocking(self.state_machine)
+ return super().handle_pins_event()
+
+ class AbstractUnlockedState(AbstractNonStartState):
+ '''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
+ def __init__(self, sm, nervlist = None):
+ super().__init__(sm, nervlist)
+ self.actor().act(Actor.CMD_GREEN_ON)
+ def handle_pins_event(self):
+ if self.pins().door_locked:
+ logger.info("Door locked, closing space")
+ if self.pins().space_active:
+ logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
+ play_sound("manual_lock")
+ return StateMachine.StateZu(self.state_machine)
+ return super().handle_pins_event()
+
+ class StateStart(State):
+ def handle_pins_event(self):
+ pins = self.pins()
+ if not (pins.door_locked is None or pins.door_closed is None or pins.space_active is None or pins.bell_ringing is None):
+ logger.info("All sensors got a value, switching to a proper state")
+ if pins.door_locked:
+ return StateMachine.StateZu(self.state_machine)
+ else:
+ return StateMachine.StateAboutToOpen(self.state_machine)
+ return super().handle_pins_event()
+
+ class StateZu(AbstractLockedState):
+ def handle_cmd_unlock_event(self,callback):
+ return StateMachine.StateUnlocking(self.state_machine, callback)
+
+ class StateUnlocking(AbstractLockedState):
+ def __init__(self,sm,callback=None):
+ # construct a nervlist
+ nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_UNLOCK)) for t in range(OPEN_REPEAT_NUMBER)]
+ nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
+ super().__init__(sm,nervlist)
+ self.callbacks=[callback]
+ # TODO: can we send "202 processing: Trying to unlock the door" here? Are the callbacks multi-use?
+ self.actor().act(Actor.CMD_UNLOCK)
+ def notify(self, did_it_work):
+ s = "200 okay: door unlocked" if did_it_work else ("500 internal server error: Couldn't unlock door with %d tries à %f seconds" % (OPEN_REPEAT_NUMBER,OPEN_REPEAT_TIMEOUT))
+ for cb in self.callbacks:
+ if cb is not None:
+ cb(s)
+ def on_leave(self):
+ self.notify(not self.pins().door_locked)
+ def handle_cmd_unlock_event(self,callback):
+ # TODO: 202 notification also here if possible
+ self.callbacks.append(callback)
+ def could_not_open(self):
+ logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER)
+ return StateMachine.StateZu(self.state_machine)
+
+ class AbstractStateWhereUnlockingIsRedundant(AbstractUnlockedState):
+ def handle_cmd_unlock_event(self, callback):
+ callback("299 redundant: Space seems to be already open. Still processing your request tough.")
+ logger.info("StateMachine: Received UNLOCK command in %s. This should not be necessary." % self.__class__.__name__)
+ self.actor().act(Actor.CMD_UNLOCK)
+
+ class StateAboutToOpen(AbstractStateWhereUnlockingIsRedundant):
+ def __init__(self, sm):
+ super().__init__(sm, ABOUTOPEN_NERVLIST)
+ def handle_pins_event(self):
+ pins = self.pins()
+ if pins.space_active:
+ logger.info("Space activated, opening procedure completed")
+ return StateMachine.StateAuf(self.state_machine)
+ return super().handle_pins_event()
+
+ class StateAuf(AbstractStateWhereUnlockingIsRedundant):
+ def __init__(self,sm):
+ nervlist = [(24*60*60, lambda: logger.critical("Space is now open for 24h. Is everything all right?"))]
+ super().__init__(sm, nervlist)
+ self.last_buzzed = None
+ def handle_pins_event(self):
+ pins = self.pins()
+ if pins.bell_ringing and not self.old_pins().bell_ringing:
+ # someone just pressed the bell
+ logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
+ self.actor().act(Actor.CMD_BUZZ)
+ if not pins.space_active:
+ logger.info("StateMachine: space switch turned off - starting leaving procedure")
+ return StateMachine.StateAboutToLeave(self.state_machine)
+ return super().handle_pins_event()
+
+ class StateLocking(AbstractUnlockedState):
+ def __init__(self,sm):
+ # construct a nervlist
+ nervlist = [(CLOSE_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_LOCK)) for t in range(CLOSE_REPEAT_NUMBER)]
+ nervlist += [(CLOSE_REPEAT_TIMEOUT, self.could_not_close)]
+ super().__init__(sm, nervlist)
+ if self.pins().door_closed: # this should always be true, but just to be sure...
+ self.actor().act(Actor.CMD_LOCK)
+ def handle_pins_event(self):
+ pins = self.pins()
+ if not pins.door_closed:
+ # TODO play a sound? This shouldn't happen, door was opened while we are locking
+ logger.warning("StateMachine: door manually opened during locking")
+ return StateMachine.StateAboutToOpen(self.state_machine)
+ # TODO do anything here if the switch is activated now?
+ return super().handle_pins_event()
+ def handle_cmd_unlock_event(self,callback):
+ callback("409 conflict: The sphinx is currently trying to lock the door. Try again later.")
+ def could_not_close(self):
+ logger.critical("StateMachine: Couldn't close door after %d tries. Going back to StateAboutToOpen." % CLOSE_REPEAT_NUMBER)
+ return StateMachine.StateAboutToOpen(self.state_machine)
+
+ class StateAboutToLeave(AbstractUnlockedState):
+ def __init__(self, sm):
+ nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateLocking(self.state_machine))]
+ super().__init__(sm, nervlist)
+ def handle_pins_event(self):
+ if not self.pins().door_closed:
+ return StateMachine.StateLeaving(self.state_machine)
+ if self.pins().space_active:
+ logger.info("Space re-activated, cancelling leaving procedure")
+ return StateMachine.StateAuf(self.state_machine)
+ return super().handle_pins_event()
+
+ class StateLeaving(AbstractUnlockedState):
+ def __init__(self, sm):
+ nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateAboutToOpen(self.state_machine))]
+ super().__init__(sm, nervlist)
+ def handle_pins_event(self):
+ if self.pins().door_closed:
+ logger.info("The space was left, locking the door")
+ return StateMachine.StateLocking(self.state_machine)
+ if self.pins().space_active:
+ logger.info("Space re-activated, cancelling leaving procedure")
+ return StateMachine.StateAuf(self.state_machine)
+ return super().handle_pins_event()
+
+ def __init__(self, actor):
+ self.actor = actor
+ self.callback = ThreadFunction(self._callback, name="StateMachine")
+ self.current_state = StateMachine.StateStart(self)
+ self.pins = None
+ self.old_pins = None
+
+ def stop (self):
+ self.callback.stop()
+
+ def _callback(self, cmd, arg=None):
+ # update pins
+ if cmd == StateMachine.CMD_PINS:
+ self.pins = arg
+ # handle stuff
+ newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state
+ self.old_pins = self.pins
+ while newstate is not None:
+ assert isinstance(newstate, StateMachine.State), "I should get a state"
+ self.current_state.on_leave()
+ logger.debug("StateMachine: Doing state transition %s -> %s" % (self.current_state.__class__.__name__, newstate.__class__.__name__))
+ self.current_state = newstate
+ newstate = self.current_state.handle_event(StateMachine.CMD_PINS, self.pins)
--- /dev/null
+#!/usr/bin/python3
+
+import readline
+import socket
+
+tuerSock = "/run/tuer.sock"
+
+while True:
+ command = input("# ") # EOFError used to exit
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ s.connect(tuerSock)
+ s.send(command.encode())
+ data = s.recv(64)
+ s.close()
+ print(str(data))
#!/usr/bin/python3
-import time, socket, os, stat, atexit, errno, struct, pwd
-from libtuer import log
-from datetime import datetime
import RPi.GPIO as GPIO
-SO_PEERCRED = 17 # DO - NOT - TOUCH
+import statemachine, actor, pins, tysock, waker
+from libtuer import logger
+import argparse
+
+# Parse arguments
+parser = argparse.ArgumentParser(description='Run a door')
+parser.add_argument("-d", "--debug",
+ action="store_true", dest="debug",
+ help="Don't send emails")
+args = parser.parse_args()
+if args.debug:
+ import libtuer
+ libtuer.mailAddress = []
+
+# initialize GPIO stuff
GPIO.setmode(GPIO.BOARD)
-atexit.register(GPIO.cleanup)
-#tmp
-def recv_timeout(conn, size, time):
- (r, w, x) = select.select([conn], [], [], time)
- if len(r):
- assert r[0] == conn
- return conn.recv(size)
- return None
-
-# ******** definitions *********
-# send to client for information but don't care if it arrives
-def waynesend (conn, what):
- try:
- conn.send(what)
- except:
- log("Couldn't send %s" % str(what))
-
-# for command not found: do nothing with the pins and send a "0" to the client
-def doNothing (conn):
- log ("doing nothing")
- waynesend(conn,b"0")
+# bring 'em all up
+the_actor = actor.Actor()
+the_machine = statemachine.StateMachine(the_actor)
+the_socket = tysock.TySocket(the_machine)
+the_pins = pins.PinsWatcher(the_machine)
+the_waker = waker.Waker(the_machine)
+
+# we do the socket accept thing in the main thread
+try:
+ the_socket.accept()
+except KeyboardInterrupt:
+ # this is what we waited for!
+ logger.info("Got SIGINT, terminating...")
pass
-# delete a file, don't care if it did not exist in the first place
-def forcerm(name):
- try:
- os.unlink (name)
- except OSError as e:
- # only ignore error if it was "file didn't exist"
- if e.errno != errno.ENOENT:
- raise
-
-# commands: on a pin do a series of timed on/off switches
-class Pinoutput:
- # name is for logging and also used for mapping command names to instances of this class
- # actionsanddelays is a list of pairs: (bool to set on pin, delay in seconds to wait afterwards)
- def __init__ (self, name, pinnumber, actionsanddelays):
- self.name = name
- self.pin = pinnumber
- self.todo = actionsanddelays
- GPIO.setup(pinnumber, GPIO.OUT)
- log ("Pin %d set to be an output pin for %s." % (pinnumber,name))
- # actually send the signal to the pins
- def __call__ (self, conn):
- for (value,delay) in self.todo:
- GPIO.output(self.pin, value)
- # log ("%s: Pin %d set to %s." % (self.name,self.pin,str(value)))
- time.sleep(delay)
- # notify success
- log
- waynesend(conn,b"1")
-
-# ******** configuration *********
-
-tuergroupid = 1005
-socketname = "/run/tuer.sock"
-pinlist = [Pinoutput("open", 12, [(True, 0.3), (False, 5.0)]),
- Pinoutput("close", 16, [(True, 0.3), (False, 5.0)]),
- Pinoutput("buzz", 22, [(True, 2.0), (False, 0.1)])]
-
-
-# ******** main *********
-# convert list of pin objects to dictionary for command lookup
-pindict = {}
-for pin in pinlist:
- pindict[pin.name.encode()] = pin
-
-# create socket
-sock = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM)
-# delete old socket file and don't bitch around if it's not there
-forcerm(socketname)
-# bind socket to file name
-sock.bind (socketname)
-# allow only users in the tuergroup to write to the socket
-os.chown (socketname, 0, tuergroupid)
-os.chmod (socketname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)
-# listen to the people, but not too many at once
-sock.listen(1)
-
-# shutdown handling
-def shutdown():
- log("Shutting down")
- sock.close()
- forcerm(socketname)
-atexit.register(shutdown)
-
-# main loop
-# FIXME: DoS by opening socket but not sending data, because this loop is single threaded; maybe settimeout helps a bit.
-while True:
- # accept connections
- conn, addr = sock.accept()
- try:
- # get peer information
- (pid, uid, gid) = (struct.unpack('3i', conn.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, struct.calcsize('3i'))))
- # get some data from the client (enough to hold any valid command)
- data = conn.recv (32)
- # log the command
- log("received command from %s (uid %d): %s" % (pwd.getpwuid(uid).pw_name,uid, str(data)))
- # lookup the command, if it's not in the dict, use the doNothing function instead
- # and execute the looked up command or doNothing with the connection, so it can respond to the client
- pindict.get(data,doNothing)(conn)
- log("done")
- # close connection cleanly
- conn.close()
- except Exception as e:
- log("Something went wrong: %s\n...continuing." % str(e))
+# bring 'em all down
+the_waker.stop()
+the_pins.stop()
+the_machine.stop()
+the_actor.stop()
+# shutdown GPIO stuff
+GPIO.cleanup()
import sys
import subprocess
import socket
+import pwd
+import grp
+import traceback
+from collections import namedtuple
tuerSock = "/run/tuer.sock"
pass
import atexit
atexit.register(readline.write_history_file, histfile)
-atexit.register(print, "Bye")
# available commands
def helpcmd(c):
- print("Available commands: %s" % ", ".join(sorted(commands.keys())))
+ if (len(c) > 1):
+ print(commands.get(c[1],(None,'Can\'t find help for command %s'%(c[1]))).helpstring)
+ else:
+ print("Available commands: %s" % ", ".join(sorted(commands.keys())))
+ print("Use 'help command' to get more information on the command 'command'")
def extcmd(cmd):
def run(c):
def sendcmd(addr, cmd):
def run(c):
- print("Running %s..." % (cmd))
+ print("206 Sending command %s..." % (cmd))
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(addr)
+ s.settimeout(60.0)
s.send(cmd.encode())
- data = s.recv(4)
+ data = s.recv(256)
s.close()
- print("...done")
- if data != b'1':
- print("Received unexpected answer %s" % str(data))
+ print(data.decode('utf-8'))
return run
def exitcmd(c):
- sys.exit(0)
+ print("Bye")
+ return True
+
+def whocmd(c):
+ for n in grp.getgrnam("tuer").gr_mem:
+ p = pwd.getpwnam(n)
+ print (p.pw_name, " - ", p.pw_gecos)
+
+def alias (cmds, aliases):
+ for newname, oldname in aliases.items():
+ cmds[newname] = cmds[oldname]
+ return cmds
+
+CmdEntry = namedtuple('CmdEntry','function helpstring')
+
+commands = alias({
+ 'exit': CmdEntry(exitcmd, 'Quits this shell'),
+ 'help': CmdEntry(helpcmd, 'Helps you getting to know the available commands'),
+ 'unlock': CmdEntry(sendcmd(tuerSock, 'unlock'), 'Will try to unlock the apartment door'),
+ 'buzz': CmdEntry(sendcmd(tuerSock, 'buzz'), 'Will buzz the buzzer for the street door'),
+ 'who': CmdEntry(whocmd, 'Shows the list of people, who are allowed to control this system'),
+},{
+ # aliases
+ 'open': 'unlock',
+})
-commands = {
- 'exit': exitcmd,
- 'help': helpcmd,
- 'open': sendcmd(tuerSock, 'open'),
- 'close': sendcmd(tuerSock, 'close'),
- 'buzz': sendcmd(tuerSock, 'buzz'),
-}
+def complete_command(cmd):
+ '''returns a list of commands (as strings) starting with cmd'''
+ return list(filter(lambda x: x.startswith(cmd), commands.keys()))
+readline.set_completer(lambda cmd, num: (complete_command(cmd)+[None])[num]) # wrap complete_command for readline's weird completer API
+readline.parse_and_bind("tab: complete") # run completion on tab
# input loop
print("Welcome to tyshell. Use help to see what you can do.")
break
command = shlex.split(command)
if not len(command): continue
- # execute command
- if command[0] in commands:
+ # find suiting commands
+ if command[0] in commands: # needed in case a complete command is a prefix of another one
+ cmdoptions = [command[0]]
+ else:
+ cmdoptions = complete_command(command[0])
+ # check how many we found
+ if len(cmdoptions) == 0: # no commands fit prefix
+ print("Command %s not found. Use help." % command[0])
+ elif len(cmdoptions) == 1: # exactly one command fits (prefix)
try:
- commands[command[0]](command)
+ res = commands[cmdoptions[0]].function(command)
+ if res: break
except Exception as e:
print("Error while executing %s: %s" % (command[0], str(e)))
- else:
- print("Command %s not found. Use help." % command[0])
+ #print(traceback.format_exc())
+ else: # multiple commands fit the prefix
+ print("Ambiguous command prefix, please choose one of the following:")
+ print("\t", " ".join(cmdoptions))
+ # TODO: put current "command[0]" into the shell for the next command, but such that it is deletable with backspace
--- /dev/null
+import socket, os, stat, struct, pwd, errno
+from statemachine import StateMachine
+from libtuer import logger
+SO_PEERCRED = 17 # DO - NOT - TOUCH
+
+tuergroupid = 1005
+socketname = "/run/tuer.sock"
+
+# send to client for information but don't care if it arrives
+def waynesend (conn, what):
+ try:
+ conn.send(what.encode())
+ except:
+ pass # we do not care
+
+# delete a file, don't care if it did not exist in the first place
+def forcerm(name):
+ try:
+ os.unlink (name)
+ except OSError as e:
+ # only ignore error if it was "file didn't exist"
+ if e.errno != errno.ENOENT:
+ raise
+
+# the class doing the actual work
+class TySocket():
+ CMDs = {
+ b'buzz': StateMachine.CMD_BUZZ,
+ b'unlock': StateMachine.CMD_UNLOCK,
+ }
+
+ def __init__(self, sm):
+ self._sm = sm
+ # create socket
+ self._sock = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM)
+ # delete old socket file and don't bitch around if it's not there
+ forcerm(socketname)
+ # bind socket to file name
+ self._sock.bind (socketname)
+ # allow only users in the tuergroup to write to the socket
+ os.chown (socketname, 0, tuergroupid)
+ os.chmod (socketname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP)
+ # listen to the people, but not too many at once
+ self._sock.listen(1)
+
+ def _answer(self, conn):
+ def answer(msg):
+ # this is called in another thread, so it should be quick and not touch the TySocket
+ waynesend(conn, msg)
+ conn.close()
+ return answer
+
+ def accept(self):
+ '''Handles incoming connections and keyboard events'''
+ self._sock.settimeout(None)
+ while True:
+ # accept connections
+ conn, addr = self._sock.accept()
+ conn.settimeout(0.1)
+ try:
+ # get peer information
+ (pid, uid, gid) = struct.unpack('3i', conn.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, struct.calcsize('3i')))
+ # get some data from the client (enough to hold any valid command)
+ data = conn.recv (32)
+ # log the command
+ logger.info("TySocket: Received command from %s (uid %d): %s" % (pwd.getpwuid(uid).pw_name, uid, str(data)))
+ # lookup the command, send it to state machine
+ if data in self.CMDs:
+ self._sm.callback(self.CMDs[data], self._answer(conn))
+ # _answer will be called, and it will close the connection
+ else:
+ waynesend(conn, 'Command not found')
+ conn.close()
+ except KeyboardInterrupt:
+ raise # forward Ctrl-C to the outside
+ except Exception as e:
+ logger.critical("TySocket: Something went wrong: %s" % str(e))
--- /dev/null
+from libtuer import ThreadRepeater
+from statemachine import StateMachine
+
+class Waker():
+ def __init__(self, sm):
+ self._sm = sm
+ self._t = ThreadRepeater(self._wake, 0.5, name="Waker")
+
+ def _wake(self):
+ self._sm.callback(StateMachine.CMD_WAKEUP)
+
+ def stop(self):
+ self._t.stop()