Commit 8b136f42 authored by Ralf's avatar Ralf

start implementing the new all-great tuerd

parent df7d5268
files = libtuer.py tuerd ringd tyshell
files = *.py tuerd tryshell tyshell
target = /opt/tuer/
all: install
......
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:
......@@ -9,17 +9,25 @@ 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
......@@ -36,12 +44,15 @@ class ThreadFunction():
(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))
......@@ -49,3 +60,23 @@ class ThreadFunction():
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()
#!/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)
from collections import namedtuple
from libtuer import ThreadRepeater, logger
from statemachine import StateMachine
tuerSock = "/run/tuer.sock"
ringPin = 18
class PinsState():
pass
# 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.pin = pin
self._histlen = histlen
# state change detection
self._state = None
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)
curstate = GPIO.input(self.pin)
assert curstate in (0, 1)
if curstate != self._state:
# the state is about to change
......@@ -34,9 +26,10 @@ class PinWatcher():
# 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
# 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
......@@ -44,44 +37,35 @@ class PinWatcher():
else:
# old state is preserved
self._newstate = None
return False
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()
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 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()
def stop():
self._t.stop()
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()
......@@ -16,7 +16,6 @@ except IOError:
pass
import atexit
atexit.register(readline.write_history_file, histfile)
atexit.register(print, "Bye")
# available commands
def helpcmd(c):
......@@ -34,16 +33,16 @@ def sendcmd(addr, cmd):
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,
......@@ -68,11 +67,13 @@ while True:
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
......
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
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()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment