-files = libtuer.py tuerd ringd tyshell
+files = *.py tuerd tryshell tyshell
target = /opt/tuer/
all: install
--- /dev/null
+from libtuer import ThreadFunction, logger
+import RPi.GPIO as GPIO
+
+class Actor:
+ CMD_BUZZ = 0
+ CMD_OPEN = 1
+ CMD_CLOSE = 2
+
+ CMDs = {
+ CMD_BUZZ: ("buzz", 12, [(True, 0.3), (False, 2.0)]),
+ CMD_OPEN: ("open", 16, [(None, 0.2), (True, 0.3), (False, 1.0)]),
+ CMD_CLOSE: ("close", 22, [(None, 0.2), (True, 0.3), (False, 1.0)]),
+ }
+
+ def __init__(self):
+ self.act = ThreadFunction(self._act)
+ for (name, pin, todo) in self.CMDs.values():
+ GPIO.setup(pin, GPIO.OUT)
+
+ def _act(self, cmd):
+ if cmd in self.CMDs:
+ (name, pin, todo) = self.CMDs[cmd]
+ logger.info("Actor: Running command %s" % name)
+ for (value, delay) in todo:
+ if value is not None:
+ GPIO.output(pin, value)
+ time.sleep(delay)
+ else:
+ logger.error("Actor: Gut unknown command %d" % cmd)
+
+ def stop(self):
+ pass
-import logging, logging.handlers, syslog, os
+import logging, logging.handlers, os, time, queue, threading
# logging function
class Logger:
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):
+
+ def log (self, lvl, what):
thestr = "%s[%d]: %s" % (self.name,os.getpid(),what)
print (thestr)
- self.logger.info(thestr)
+ self.logger.log(lvl, thestr)
+
+ 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)
-
-
# Threaded callback class
class ThreadFunction():
_CALL = 0
(cmd, data) = self._q.get()
# run command
if cmd == _CALL:
- self._f(*data)
+ try:
+ self._f(*data)
+ except Exception:
+ logger.error("ThreadFunction: Got exception out of handler thread: %s" % str(e))
elif cmd == _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))
def stop(self):
self._q.put((_TERM, None))
self._t.join()
+
+# Thread timer-repeater class: Call a function every <sleep_time> seconds
+class ThreadRepeater():
+ def __init__(self, f, sleep_time):
+ 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():
+ while True:
+ if self._stop:
+ break
+ self._f()
+ time.sleep(sleep_time)
+
+ def stop(self):
+ 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, 5),
+ 'door_locked': PinWatcher(9, 5),
+ 'space_active': PinWatcher(10, 5),
+ }
+ self._sm = state_machine
+
+ # start a thread doing the work
+ self._t = ThreadRepeater(self._read, 0.02)
+
+ def _read():
+ saw_change = False
+ for name in self.pins.keys():
+ pin = pins[name]
+ if pin.read():
+ saw_change = True
+ logger.debug("Pin %s changed to %d" % (name, pin.state)
+ if not saw_change: return
+ # 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._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
+from actor import Actor
+
+# logger.{debug,info,warning,error,critical}
+
+# StateOpening constants
+OPEN_REPEAT_TIMEOUT = 8
+OPEN_REPEAT_NUMBER = 3
+
+def play_sound (what):
+ print ("I would now play the sound %s... IF I HAD SOUNDS!" % what)
+
+# StateAboutToOpen constants
+ABOUTOPEN_NERVLIST = [(5, lambda : play_sound("heydrückdenknopf.mp3")), (10, lambda:play_sound("alterichmeinsernst.mp3"))]
+# TODO: erzeuge mehr nerv
+
+class StateMachine():
+ # commands you can send
+ CMD_PINS = 0
+ CMD_BUZZ = 1
+ CMD_OPEN = 2
+ CMD_WAKEUP = 3
+ CMD_LAST = 4
+
+ class State():
+ def __init__(self, state_machine):
+ self.state_machine = state_machine
+ self.time_entered = time.time()
+ self.theDict = None
+ 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_open_event(self,arg):
+ if arg is not None:
+ arg("412 Precondition Failed: The current state (%s) cannot handle the OPEN event" % self.__class__.__name__)
+ def handle_wakeup_event(self):
+ pass # one needn't implement this
+ def pins(self):
+ return self.state_machine.pins
+ def actor(self):
+ return self.state_machine.actor
+ def handle_event(self,ev,arg):
+ if arg is CMD_PINS:
+ self.handle_pins_event()
+ elif arg is CMD_BUZZ:
+ self.handle_buzz_event(arg)
+ elif arg is CMD_OPEN:
+ self.handle_open_event(arg)
+ elif arg is CMD_WAKEUP:
+ self.handle_wakeup_event()
+ else:
+ raise Exception("Unknown command number: %d" % ev)
+
+ class StateStart(State):
+ def __init__(self, sm):
+ State.__init__(self,sm)
+ def handle_pins_event(self):
+ thepins = self.pins()
+ for pin in thepins:
+ if pin is None:
+ return None
+ if thepins.door_locked:
+ return StateZu
+ else:
+ return StateAuf
+
+ class StateZu(State):
+ def __init__(self,sm):
+ State.__init__(self,sm)
+ def handle_pins_event(self):
+ pins = self.pins()
+ if not pins.door_locked:
+ return StateAboutToOpen(self.state_machine)
+ def handle_open_event(self,callback):
+ return StateOpening(callback,self.state_machine)
+
+ class StateOpening(State):
+ def __init__(self,callback,sm):
+ State.__init__(self,sm)
+ self.callbacks=[callback]
+ self.tries = 0
+ self.actor().act(Actor.CMD_OPEN)
+ def notify(self, did_it_work):
+ s = "200 okay: door open" if did_it_work else ("500 internal server error: Couldn't open 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 handle_pins_event(self):
+ pins = self.pins()
+ if not pins.door_locked:
+ self.notify(True)
+ return StateAboutToOpen(self.state_machine)
+ def handle_open_event(self,callback):
+ self.callbacks.append(callback)
+ def handle_wakeup_event(self):
+ over = time.time() - self.time_entered
+ nexttry = (self.tries+1) * OPEN_REPEAT_TIMEOUT
+ if over > nexttry:
+ if self.tries < OPEN_REPEAT_NUMBER:
+ self.actor().act(Actor.CMD_OPEN)
+ self.tries += 1
+ else:
+ #TODO: LOG ERROR und EMAIL an Admins
+ self.notify(False)
+ return StateZu(self.state_machine)
+
+ class StateAboutToOpen(State):
+ def __init__(self, sm):
+ State.__init__(sm)
+ def handle_pins_event(self):
+ pins = self.pins()
+ if pins.door_locked:
+ return StateZu(self.state_machine)
+ elif pins.space_active:
+ return StateAuf(self.state_machine)
+ else:
+ over = time.time() - self.time_entered
+ # TODO: Nerv
+ logger.debug("AboutToOpen since %f seconds. TODO: nerv the user" % over)
+ # TODO
+
+ class StateAuf(State):
+ #TODO
+ pass
+
+ class StateClosing(State):
+ #TODO
+ pass
+
+ class StateAboutToLeave(State):
+ #TODO
+ pass
+
+ class StateLeaving(State):
+ #TODO
+ pass
+
+ def __init__(self, actor):
+ self.actor = actor
+ self.callback = ThreadFunction(self._callback)
+ self.current_state = None
+ self.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
+ while newstate is not None:
+ logger.info("StateMachine: new state = %s" % newstate.__class__.__name__)
+ self.current_state = newstate
+ newstate = self.current_state.handle_event(StateMachine.CMD_PINS, self.pins)
#!/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
-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
+import statemachine, actor, pins, tysock, waker
-# ******** 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))
+# initialize GPIO stuff
+GPIO.setmode(GPIO.BOARD)
-# 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!
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_socket.stop()
+the_machine.stop()
+the_actor.stop()
+# shutdown GPIO stuff
+GPIO.cleanup()
pass
import atexit
atexit.register(readline.write_history_file, histfile)
-atexit.register(print, "Bye")
# available commands
def helpcmd(c):
print("Running %s..." % (cmd))
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(addr)
+ s.settimeout(10.0)
s.send(cmd.encode())
data = s.recv(4)
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
commands = {
'exit': exitcmd,
cmdoptions = [command[0]]
else:
cmdoptions = list(filter(lambda x: command[0].startswith(x), commands.keys()))
+ # 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[cmdoptions[0]](command)
+ res = commands[cmdoptions[0]](command)
+ if res: break
except Exception as e:
print("Error while executing %s: %s" % (command[0], str(e)))
else: # multiple commands fit the prefix
--- /dev/null
+import socket, os, stat
+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'open': StateMachine.CMD_OPEN,
+ }
+
+ 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.error("TySocket: Something went wrong: %s" % str(e))
+
+ def stop(self):
+ pass
--- /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)
+
+ def _wake(self):
+ self._sm.callback(StateMachine.CMD_WAKEUP)
+
+ def stop(self):
+ self._t.stop()