Compare commits
3 commits
core
...
web_socket
Author | SHA1 | Date | |
---|---|---|---|
|
2d58f095bd | ||
|
5dfeee5977 | ||
|
bf3f32c0d3 |
4 changed files with 372 additions and 190 deletions
|
@ -1,47 +0,0 @@
|
|||
class Broadcast:
|
||||
class ChannelError(IndexError):
|
||||
def __init__(self, channel):
|
||||
super(Broadcast.ChannelError, self).__init__("channel '%s' does not exist" % channel)
|
||||
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def broadcast(self, data, route, channel, exclude=[]):
|
||||
if channel in self.channels:
|
||||
for handler in self.channels[channel]:
|
||||
if not handler in exclude:
|
||||
handler.send(data, route)
|
||||
else:
|
||||
raise Broadcast.ChannelError(channel)
|
||||
|
||||
def remove(self, handler, channel):
|
||||
if channel in self.channels:
|
||||
if handler in self.channels[channel]:
|
||||
del self.channels[channel][self.channels[channel].index(handler)]
|
||||
else:
|
||||
raise Broadcast.ChannelError(channel)
|
||||
|
||||
def add(self, handler, channel):
|
||||
if channel in self.channels:
|
||||
if not handler in self.channels[channel]:
|
||||
self.channels[channel].append(handler)
|
||||
else:
|
||||
raise Broadcast.ChannelError(channel)
|
||||
|
||||
def add_channel(self, channel):
|
||||
self.channels[channel] = []
|
||||
|
||||
def remove_channel(self, channel):
|
||||
if channel in self.channels:
|
||||
del self.channels[channel]
|
||||
else:
|
||||
raise Broadcast.ChannelError(channel)
|
||||
|
||||
def __repr__(self):
|
||||
out = "Channels:\n"
|
||||
for channel in self.channels:
|
||||
out += "%s: %d socks\n" % (channel, len(self.channels[channel]))
|
||||
return out.rstrip("\n")
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
118
Shared/Config.py
118
Shared/Config.py
|
@ -1,118 +0,0 @@
|
|||
from imp import load_source
|
||||
from os.path import isfile
|
||||
from marshal import dumps, loads
|
||||
from types import FunctionType
|
||||
|
||||
|
||||
class OptionDuplicateError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' already exists" % name)
|
||||
|
||||
|
||||
class OptionNotFoundError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' does not exist" % name)
|
||||
|
||||
|
||||
class NameMustBeStringError(Exception):
|
||||
def __init__(self):
|
||||
super(Exception, self).__init__("option names have to be strings")
|
||||
|
||||
|
||||
def make_value(value):
|
||||
if type(value) is str:
|
||||
value = '"%s"' % value
|
||||
elif type(value) in (list, tuple, dict):
|
||||
value = str(value)
|
||||
elif type(value) is FunctionType:
|
||||
value = dumps(value.__code__)
|
||||
return value
|
||||
|
||||
|
||||
class Option:
|
||||
def __init__(self, name, default_value, validator=None, comment=""):
|
||||
if not type(name) is str:
|
||||
raise NameMustBeStringError()
|
||||
self.name = name
|
||||
self.default_value = default_value
|
||||
self.validator = validator
|
||||
self.comment = comment
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, options=[], validation_failed=None, override_on_error=False):
|
||||
if type(options) in (list, tuple):
|
||||
for option in options:
|
||||
if not type(option) is Option:
|
||||
raise TypeError("all options must be of type Option")
|
||||
else:
|
||||
raise TypeError("options must be a list or tuple containing options of type Option")
|
||||
self.options = options
|
||||
self.validation_failed = validation_failed
|
||||
self.override_on_error = override_on_error
|
||||
|
||||
|
||||
def read_from_file(self, file):
|
||||
if isfile(file):
|
||||
config = load_source("config", file)
|
||||
error = False
|
||||
for option in self.options:
|
||||
# Make sure all options are avaliable
|
||||
if option.name not in dir(config):
|
||||
setattr(config, option.name, option.default_value)
|
||||
error = True
|
||||
else:
|
||||
# Make sure all validators pass
|
||||
if option.validator != None:
|
||||
value = getattr(config, option.name)
|
||||
if not option.validator(value):
|
||||
setattr(config, option.name, option.default_value)
|
||||
if self.validation_failed != None:
|
||||
self.validation_failed(option.name, value)
|
||||
error = True
|
||||
if self.override_on_error:
|
||||
if error:
|
||||
self.write_to_file(file)
|
||||
return config
|
||||
else:
|
||||
raise FileNotFoundError()
|
||||
|
||||
|
||||
def add(self, new_option):
|
||||
if type(new_option) is Option:
|
||||
for option in self.options:
|
||||
if new_option.name == option.name:
|
||||
raise OptionDuplicateError(option.name)
|
||||
self.options.append(new_option)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def remove(self, option):
|
||||
if option in self.options:
|
||||
del self.options[self.options.index(option)]
|
||||
else:
|
||||
raise OptionNotFoundError(option.name)
|
||||
|
||||
|
||||
def write_to_file(self, file):
|
||||
open(file, "w").write(self.get())
|
||||
|
||||
|
||||
def get(self):
|
||||
contains_function = False
|
||||
out = ""
|
||||
for option in self.options:
|
||||
value = make_value(option.default_value)
|
||||
if type(option.default_value) is FunctionType:
|
||||
if not contains_function:
|
||||
out = "from marshal import loads; from types import FunctionType\n\n" + out
|
||||
contains_function = True
|
||||
value = 'FunctionType(loads(%s), globals(), "%s")' % (value, option.name)
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.get()
|
328
Shared/Meh.py
Normal file
328
Shared/Meh.py
Normal file
|
@ -0,0 +1,328 @@
|
|||
from imp import load_source
|
||||
from os.path import isfile
|
||||
from sys import version
|
||||
|
||||
class OptionDuplicateError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' already exists" % name)
|
||||
|
||||
|
||||
class OptionNotFoundError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' does not exist" % name)
|
||||
|
||||
|
||||
class NameMustBeStringError(Exception):
|
||||
def __init__(self):
|
||||
super(Exception, self).__init__("option names have to be strings")
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
def __init__(self, option):
|
||||
super(Exception, self).__init__("invalid value for option '%s'" % option)
|
||||
|
||||
|
||||
class UnsupportedTypeError(TypeError):
|
||||
def __init__(self):
|
||||
super(TypeError, self).__init__("only list, tuple, dict, bytes, "
|
||||
"str, float, complex, int and bool are supported (same "
|
||||
"thing applies to list, dict and tuple contents)")
|
||||
|
||||
|
||||
class ExceptionInConfigError(Exception):
|
||||
"""
|
||||
Raised if an exception occurs while importing a config file.
|
||||
|
||||
IN: error (hint: error that occured on import)
|
||||
"""
|
||||
def __init__(self, error):
|
||||
self.error = error
|
||||
super(Exception, self).__init__("error occured during config import (%s)" %
|
||||
error.__class__.__name__)
|
||||
|
||||
def validate_value(value):
|
||||
type_value = type(value)
|
||||
if type_value in (list, tuple):
|
||||
for element in value:
|
||||
if not validate_value(element):
|
||||
return False
|
||||
elif type_value is dict:
|
||||
return validate_value(tuple(value.keys())) and validate_value(tuple(value.values()))
|
||||
elif type_value in (bytes, str, float, complex, int, bool):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def make_value(value):
|
||||
if validate_value(value):
|
||||
if type(value) is str:
|
||||
value = '"%s"' % value
|
||||
elif type(value) in (list, tuple, dict):
|
||||
value = str(value)
|
||||
return value
|
||||
else:
|
||||
raise UnsupportedTypeError()
|
||||
|
||||
|
||||
class _EditableConfig:
|
||||
"""
|
||||
Automatically created proxy class.
|
||||
|
||||
HINTS:
|
||||
_values: All options with their respective values
|
||||
_options: All Option instances that were originally added to
|
||||
the Config instance
|
||||
_file: Path to the config file
|
||||
_validation_failed: Optional function that's called on a validation error
|
||||
_debug: Debug mode on/off (obviously)
|
||||
"""
|
||||
def __init__(self, values, options, file, validation_failed=None, debug=False):
|
||||
self._values = values
|
||||
self._options = options
|
||||
self._file = file
|
||||
self._validation_failed = validation_failed
|
||||
self._debug = debug
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self._values:
|
||||
return self._values[name]
|
||||
else:
|
||||
raise AttributeError("config no attribute '%s'" % name)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "_values" or name not in self._values:
|
||||
self.__dict__[name] = value
|
||||
else:
|
||||
dump_required = False
|
||||
for option in self._options:
|
||||
if option.name == name:
|
||||
if validate_value(value):
|
||||
if option.validator != None:
|
||||
if option.validator(value):
|
||||
self._values[name] = value
|
||||
dump_required = True
|
||||
else:
|
||||
if self._validation_failed != None:
|
||||
self._validation_failed(option.name, value)
|
||||
else:
|
||||
raise ValidationError(option.name)
|
||||
else:
|
||||
self._values[name] = value
|
||||
dump_required = True
|
||||
else:
|
||||
raise UnsupportedTypeError()
|
||||
if dump_required:
|
||||
if self._debug:
|
||||
print("Rewriting config because the value of '%s' changed." % name)
|
||||
open(self._file, "w").write(self._dumps())
|
||||
|
||||
|
||||
def _dumps(self):
|
||||
out = ""
|
||||
for option in self._options:
|
||||
value = make_value(self._values[option.name])
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out.rstrip("\n")
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self._dumps()
|
||||
|
||||
|
||||
class Option:
|
||||
|
||||
def __init__(self, name, default_value, validator=None, comment=""):
|
||||
if not type(name) is str:
|
||||
raise NameMustBeStringError()
|
||||
if name.startswith("__"):
|
||||
raise InvalidOptionName()
|
||||
self._name = name
|
||||
self.default_value = default_value
|
||||
self.validator = validator
|
||||
self.comment = comment
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
if name.startswith("__"):
|
||||
raise InvalidOptionName()
|
||||
self._name = value
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
if other.__class__ == Option:
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s = %s" % (self.name, str(self.default_value))
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
The central element of Meh (TM).
|
||||
IN:
|
||||
options=[] (type: list, hint: a list of options)
|
||||
validation_failed=None (type: function, hint: function accepting two
|
||||
parameters that's called when a validation fails)
|
||||
|
||||
Example usage:
|
||||
from Meh import Config, Option
|
||||
config = Config()
|
||||
config.add(Option("number", 42, validator=lambda x: type(x) is int))
|
||||
|
||||
CONFIG_PATH = "awesome_config.cfg"
|
||||
try:
|
||||
config = config.load(CONFIG_PATH)
|
||||
except IOError:
|
||||
config.dump(CONFIG_PATH)
|
||||
config = config.load(CONFIG_PATH)
|
||||
|
||||
print(config.number)
|
||||
"""
|
||||
def __init__(self, options=[], validation_failed=None, debug=False):
|
||||
if type(options) in (list, tuple):
|
||||
for option in options:
|
||||
if not option.__class__ == Option:
|
||||
raise TypeError("all options must be of type Option")
|
||||
else:
|
||||
raise TypeError("options must be a list or tuple containing options of type Option")
|
||||
self.options = options
|
||||
self.validation_failed = validation_failed
|
||||
self.debug = debug
|
||||
self._iterator_index = 0
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __next__(self):
|
||||
if self._iterator_index < len(self.options):
|
||||
self._iterator_index += 1
|
||||
return self.options[self._iterator_index - 1]
|
||||
self._iterator_index = 0
|
||||
raise StopIteration
|
||||
|
||||
|
||||
def load(self, file):
|
||||
"""
|
||||
Returns the actual read- and editable config
|
||||
IN: file (type: str, hint: should be a valid path)
|
||||
"""
|
||||
if isfile(file):
|
||||
try:
|
||||
config = load_source("config", file)
|
||||
except Exception as e:
|
||||
raise ExceptionInConfigError(e)
|
||||
option_missing = False
|
||||
values = {}
|
||||
for option in self.options:
|
||||
# Make sure all options are avaliable (validators aren't run in this case
|
||||
# because there are no values defined)
|
||||
if option.name not in dir(config):
|
||||
values[option.name] = option.default_value
|
||||
option_missing = True
|
||||
else:
|
||||
# Retrieve the option value
|
||||
value = getattr(config, option.name)
|
||||
# Make sure validator passes
|
||||
if option.validator != None:
|
||||
# If validation doesn't pass
|
||||
if not option.validator(value):
|
||||
# Resort to default value
|
||||
values[option.name] = option.default_value
|
||||
if self.validation_failed != None:
|
||||
self.validation_failed(option.name, value)
|
||||
else:
|
||||
raise ValidationError(option.name)
|
||||
option_missing = True
|
||||
# If validation passes
|
||||
else:
|
||||
values[option.name] = value
|
||||
else:
|
||||
values[option.name] = value
|
||||
if option_missing:
|
||||
self.dump(file)
|
||||
return _EditableConfig(values, self.options, file,
|
||||
validation_failed=self.validation_failed, debug=self.debug)
|
||||
else:
|
||||
error = "'%s' not found" % file
|
||||
raise FileNotFoundError(error) if version.startswith("3") else IOError(error)
|
||||
|
||||
|
||||
def __add__(self, other):
|
||||
try:
|
||||
self.add(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return self
|
||||
|
||||
|
||||
def __sub__(self, other):
|
||||
try:
|
||||
self.remove(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return self
|
||||
|
||||
|
||||
def add(self, option):
|
||||
"""
|
||||
Adds an option to a Config instance
|
||||
IN: option (type: Option)
|
||||
"""
|
||||
if option.__class__ == Option:
|
||||
for _option in self.options:
|
||||
if option.name == _option.name:
|
||||
raise OptionDuplicateError(_option.name)
|
||||
self.options.append(option)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def remove(self, option):
|
||||
"""
|
||||
Removes an option from a Config instance
|
||||
IN: option (type: Option)
|
||||
"""
|
||||
if option.__class__ == Option:
|
||||
if option in self.options:
|
||||
del self.options[self.options.index(option)]
|
||||
else:
|
||||
raise OptionNotFoundError(option.name)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def dump(self, file):
|
||||
"""
|
||||
Writes output of dumps() to the path provided
|
||||
IN: file (type: str, hint: should be a valid path)
|
||||
"""
|
||||
open(file, "w").write(self.dumps())
|
||||
|
||||
|
||||
def dumps(self):
|
||||
"""
|
||||
Returns contents of config file as string
|
||||
OUT: out (type: str, hint: config content)
|
||||
"""
|
||||
out = ""
|
||||
for option in self.options:
|
||||
value = make_value(option.default_value)
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out.rstrip("\n")
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.dumps()
|
|
@ -1,6 +1,6 @@
|
|||
from Highway import Route, Pipe, Client
|
||||
from Meh import Config, Option, ExceptionInConfigError
|
||||
import Logging
|
||||
import Config
|
||||
import Utils
|
||||
|
||||
import socket
|
||||
|
@ -10,29 +10,28 @@ import sys
|
|||
import subprocess
|
||||
from random import randint
|
||||
from _thread import start_new_thread
|
||||
from ctypes import cdll
|
||||
import threading
|
||||
import json
|
||||
|
||||
CHANNEL = 2
|
||||
IS_WALLABY = Utils.is_wallaby()
|
||||
PATH = "/home/root/Documents/KISS/bin/" if IS_WALLABY else (sys.argv[1] if len(sys.argv) > 1 else None)
|
||||
|
||||
PATH = os.path.abspath(PATH)
|
||||
|
||||
if PATH[-1] != "/":
|
||||
PATH = PATH + "/"
|
||||
|
||||
USERS_LOCATION = "/home/root/Documents/KISS/users.json"
|
||||
|
||||
LIB_WALLABY = "/usr/lib/libwallaby.so"
|
||||
WALLABY_PROGRAMS = "/root/Documents/KISS/bin/"
|
||||
|
||||
if not PATH:
|
||||
Logging.error("No path specified. (Necessary on simulated Wallaby controllers.)")
|
||||
exit(1)
|
||||
|
||||
if not IS_WALLABY:
|
||||
Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.")
|
||||
|
||||
|
||||
def get_users():
|
||||
if IS_WALLABY:
|
||||
return list(json.loads(open(USERS_LOCATION, "r").read()).keys())
|
||||
return ["Default User"]
|
||||
|
||||
|
||||
class SensorReadout:
|
||||
ANALOG = 1
|
||||
DIGITAL = 2
|
||||
|
@ -40,7 +39,7 @@ class SensorReadout:
|
|||
MODES = tuple(NAMED_MODES.keys())
|
||||
|
||||
|
||||
def __init__(self, handler, poll_rate=0.5):
|
||||
def __init__(self, handler, poll_rate=0.2):
|
||||
self.poll_rate = poll_rate
|
||||
self.handler = handler
|
||||
self.peer_lock = threading.Lock()
|
||||
|
@ -159,6 +158,7 @@ class Identify(Pipe):
|
|||
Logging.success("I was identified!")
|
||||
|
||||
|
||||
|
||||
class ListPrograms(Pipe):
|
||||
def run(self, data, peer, handler):
|
||||
programs = []
|
||||
|
@ -178,7 +178,7 @@ class StopPrograms(Pipe):
|
|||
def run(self, data, peer, handler):
|
||||
if handler.debug:
|
||||
Logging.info("Stopping all botball programs.")
|
||||
if subprocess.call(["killall", "botball_user_program"],
|
||||
if subprocess.call(["killall", "botball_user_program"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT):
|
||||
handler.pipe(self.__class__.NO_PROGRAMS_RUNNING, "stop_programs", peer)
|
||||
|
||||
|
@ -196,7 +196,7 @@ class RunProgram(Pipe):
|
|||
data = data + "/"
|
||||
path = "%s%s/botball_user_program" % (PATH, data)
|
||||
if os.path.isfile(path):
|
||||
program = subprocess.Popen(self.command + [path],
|
||||
program = subprocess.Popen(self.command + [path],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
start_new_thread(self.stream_stdout, (program, peer, handler))
|
||||
else:
|
||||
|
@ -313,21 +313,40 @@ class Handler(Client):
|
|||
Logging.info("Unsubscribing '%s' from all sensor updates." % peer)
|
||||
self.routes["sensor"].sensor_readout.unsubscribe_all(peer)
|
||||
|
||||
if IS_WALLABY:
|
||||
if not "fl0w" in get_users():
|
||||
json.loads(open(USERS_LOCATION, "r").read())["fl0w"] = {"mode" : "Advanced"}
|
||||
try:
|
||||
os.mkdir(FL0W_USER_PATH)
|
||||
except FileExistsError:
|
||||
pass
|
||||
PATH = FL0W_USER_PATH + "bin/"
|
||||
else:
|
||||
if len(sys.argv) == 2:
|
||||
if os.path.exists(sys.argv[1]):
|
||||
PATH = os.path.abspath(sys.argv[1])
|
||||
else:
|
||||
Logging.error("Location has to be provided in dev-env.")
|
||||
exit(1)
|
||||
|
||||
if PATH[-1] != "/":
|
||||
PATH = PATH + "/"
|
||||
|
||||
CONFIG_PATH = "wallaby.cfg"
|
||||
|
||||
config = Config.Config()
|
||||
config.add(Config.Option("server_address", "ws://127.0.0.1:3077"))
|
||||
config.add(Config.Option("debug", False, validator=lambda x: True if True or False else False))
|
||||
config.add(Config.Option("output_unbuffer", "stdbuf"))
|
||||
config.add(Config.Option("identify_sound", "Wallaby/identify.wav",
|
||||
validator=lambda x: os.path.isfile(x)))
|
||||
config = Config()
|
||||
config.add(Option("server_address", "ws://127.0.0.1:3077"))
|
||||
config.add(Option("debug", True, validator=lambda x: True if True or False else False))
|
||||
config.add(Option("output_unbuffer", "stdbuf"))
|
||||
config.add(Option("identify_sound", "Wallaby/identify.wav",
|
||||
validator=lambda sound: os.path.isfile(sound)))
|
||||
|
||||
|
||||
try:
|
||||
config = config.read_from_file(CONFIG_PATH)
|
||||
except FileNotFoundError:
|
||||
config.write_to_file(CONFIG_PATH)
|
||||
config = config.read_from_file(CONFIG_PATH)
|
||||
config = config.load(CONFIG_PATH)
|
||||
except (IOError, ExceptionInConfigError):
|
||||
config.dump(CONFIG_PATH)
|
||||
config = config.load(CONFIG_PATH)
|
||||
|
||||
|
||||
try:
|
||||
|
@ -335,7 +354,7 @@ try:
|
|||
# setup has to be called before the connection is established
|
||||
ws.setup({"subscribe" : Subscribe(), "hostname" : Hostname(),
|
||||
"processes" : Processes(), "sensor" : Sensor(),
|
||||
"identify" : Identify(), "list_programs" : ListPrograms(),
|
||||
"identify" : Identify(), "list_programs" : ListPrograms(),
|
||||
"whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer),
|
||||
"stop_programs" : StopPrograms(), "shutdown" : Shutdown(),
|
||||
"reboot" : Reboot()},
|
||||
|
|
Reference in a new issue