diff --git a/.gitmodules b/.gitmodules
index 7f3769d..8739090 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
[submodule "Shared/ws4py"]
path = Shared/ws4py
url = https://github.com/robot0nfire/ws4py.git
+[submodule "Shared/bottle"]
+ path = Shared/bottle
+ url = https://github.com/bottlepy/bottle.git
+[submodule "Shared/dashb0ard"]
+ path = Shared/dashb0ard
+ url = https://github.com/robot0nfire/dashb0ard.git
diff --git a/.no-fl0w b/.no-fl0w
deleted file mode 100644
index e69de29..0000000
diff --git a/README.md b/README.md
index a7231af..1f18318 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
# fl0w
fl0w is currently in the process of being refactored and revamped.
diff --git a/Server/Broadcast.py b/Server/Broadcast.py
deleted file mode 100644
index fb7d180..0000000
--- a/Server/Broadcast.py
+++ /dev/null
@@ -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__()
\ No newline at end of file
diff --git a/Server/Server.py b/Server/Server.py
index e058d29..5e36eb4 100644
--- a/Server/Server.py
+++ b/Server/Server.py
@@ -1,69 +1,35 @@
+from Meh import Config, Option, ExceptionInConfigError
+from Highway import Server, Route, DummyPipe
import Logging
-import Config
-from .Broadcast import Broadcast
-
-import json
import os
import subprocess
import re
import pwd
import platform
-import struct
from subprocess import Popen, PIPE
+from _thread import start_new_thread
from wsgiref.simple_server import make_server
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler
from ws4py.server.wsgiutils import WebSocketWSGIApplication
-from Highway import Server, Route, DummyPipe
+from bottle.bottle import route, run, static_file
+@route("/")
+def index():
+ return static_file("index.html", root="Shared/dashb0ard")
+
+@route("/static/")
+def static(filepath):
+ return static_file(filepath, root="Shared/dashb0ard/static")
+
class Info(Route):
def run(self, data, handler):
handler.send({"routes" : list(handler.routes.keys())}, "info")
-class Compile:
- HAS_MAIN = re.compile(r"\w*\s*main\(\)\s*(\{|.*)$")
-
- @staticmethod
- def is_valid_c_program(path):
- for line in open(path, "r").read().split("\n"):
- if Compile.HAS_MAIN.match(line):
- return True
- return False
-
-
- def __init__(self, source_path, binary_path):
- self.source_path = os.path.abspath(source_path) + "/"
- self.binary_path = os.path.abspath(binary_path) + "/"
- self.wallaby_library_avaliable = os.path.isfile("/usr/local/lib/libaurora.so") and os.path.isfile("/usr/local/lib/libdaylite.so")
- if not self.wallaby_library_avaliable:
- Logging.warning("Wallaby library not found. All Wallaby functions are unavaliable.")
- if platform.machine() != "armv7l":
- Logging.warning("Wrong processor architecture! Generated binaries will not run on Wallaby Controllers.")
-
-
- def compile(self, path, relpath, handler=None):
- if relpath.endswith(".c") and Compile.is_valid_c_program(path + relpath):
- name = "-".join(relpath.split("/")).rstrip(".c")
- full_path = self.binary_path + name
- if not os.path.exists(full_path):
- os.mkdir(full_path)
- error = True
- command = ["gcc", "-pipe", "-O0", "-lwallaby", "-I%s" % self.source_path, "-o", "%s" % full_path + "/botball_user_program", path + relpath]
- if not self.wallaby_library_avaliable:
- del command[command.index("-lwallaby")]
- p = Popen(command, stdout=PIPE, stderr=PIPE)
- error = False if p.wait() == 0 else True
- result = ""
- for line in p.communicate():
- result += line.decode()
- if handler != None:
- handler.send({"failed" : error, "returned" : result, "relpath" : relpath}, self.handler.reverse_routes[self])
-
-
class Subscribe(Route):
EDITOR = 1
@@ -76,13 +42,12 @@ class Subscribe(Route):
if "channel" in data:
if data["channel"] in Subscribe.CHANNELS:
handler.channel = data["channel"]
- handler.broadcast.add(handler, handler.channel)
if handler.debug:
Logging.info("'%s:%i' has identified as a %s client." % (handler.address, handler.port,
"Editor" if handler.channel == Subscribe.EDITOR else
"Controller" if handler.channel == Subscribe.WALLABY else
"Web" if handler.channel == Subscribe.WEB else
- "Unknown (will not subscribe to broadcast)"))
+ "Unknown"))
if "name" in data:
handler.name = data["name"]
handler.routes["peers"].push_changes(handler)
@@ -90,8 +55,8 @@ class Subscribe(Route):
class WhoAmI(Route):
def run(self, data, handler):
- handler.send({"id" : handler.id_,
- "user" : pwd.getpwuid(os.getuid()).pw_name},
+ handler.send({"id" : handler.id_,
+ "user" : pwd.getpwuid(os.getuid()).pw_name},
handler.reverse_routes[self])
@@ -119,7 +84,7 @@ class Peers(Route):
for channel in channels:
self.subscribe(handler, channel)
# Send on channels and on subscribe
- self.send_connected_peers(handler, channels)
+ self.send_connected_peers(handler, channels)
def send_connected_peers(self, handler, channels):
@@ -169,9 +134,8 @@ class Peers(Route):
class Handler(Server):
- def setup(self, routes, broadcast, websockets, debug=False):
+ def setup(self, routes, websockets, debug=False):
super().setup(routes, websockets, debug=debug)
- self.broadcast = broadcast
self.channel = None
self.name = "Unknown"
@@ -179,11 +143,9 @@ class Handler(Server):
def ready(self):
if self.debug:
Logging.info("Handler for '%s:%d' ready." % (self.address, self.port))
-
+
def closed(self, code, reason):
- if self.channel != None:
- self.broadcast.remove(self, self.channel)
if self.debug:
Logging.info("'%s:%d' disconnected." % (self.address, self.port))
self.routes["peers"].push_changes(self)
@@ -200,34 +162,31 @@ def folder_validator(folder):
CONFIG_PATH = "server.cfg"
-config = Config.Config()
-config.add(Config.Option("server_address", ("127.0.0.1", 3077)))
-config.add(Config.Option("debug", True, validator=lambda x: True if True or False else False))
-config.add(Config.Option("binary_path", "Binaries", validator=folder_validator))
-config.add(Config.Option("source_path", "Source", validator=folder_validator))
+
+config = Config()
+config.add(Option("fl0w_address", ("127.0.0.1", 3077)))
+config.add(Option("behem0th_address", ("127.0.0.1", 3078)))
+config.add(Option("dashb0ard_address", ("127.0.0.1", 8080)))
+config.add(Option("debug", True, validator=lambda x: True if True or False else False))
+config.add(Option("path", "Content", validator=folder_validator))
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)
-broadcast = Broadcast()
-# Populating broadcast channels with all channels defined in Subscribe.Channels
-for channel in Subscribe.CHANNELS:
- broadcast.add_channel(channel)
-
-compile = Compile(config.source_path, config.binary_path)
+#compile = Compile(config.source_path, config.binary_path)
-server = make_server(config.server_address[0], config.server_address[1],
+server = make_server(config.fl0w_address[0], config.fl0w_address[1],
server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler,
app=None)
server.initialize_websockets_manager()
server.set_app(WebSocketWSGIApplication(handler_cls=Handler,
- handler_args={"debug" : config.debug, "broadcast" : broadcast,
+ handler_args={"debug" : config.debug,
"websockets" : server.manager.websockets,
"routes" : {"info" : Info(),
"whoami" : WhoAmI(),
@@ -237,16 +196,21 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler,
"peers" : Peers(),
"sensor" : DummyPipe(),
"identify" : DummyPipe(),
- "list_programs" : DummyPipe(),
- "run_program" : DummyPipe(),
"std_stream" : DummyPipe(),
"stop_programs" : DummyPipe(),
"shutdown" : DummyPipe(),
- "reboot" : DummyPipe()}}))
+ "reboot" : DummyPipe(),
+ "output" : DummyPipe()}}))
try:
Logging.header("Server loop starting.")
+ start_new_thread(run, (), {"host" : config.dashb0ard_address[0],
+ "port" : config.dashb0ard_address[1], "quiet" : True})
+ Logging.info("Starting dashb0ard on 'http://%s:%d'" % (config.dashb0ard_address[0],
+ config.dashb0ard_address[1]))
+ Logging.info("Starting fl0w on 'ws://%s:%d'" % (config.fl0w_address[0],
+ config.fl0w_address[1]))
server.serve_forever()
except KeyboardInterrupt:
Logging.header("Gracefully shutting down server.")
diff --git a/Shared/Config.py b/Shared/Config.py
deleted file mode 100644
index 26a509d..0000000
--- a/Shared/Config.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/Shared/Disc0very.py b/Shared/Disc0very.py
deleted file mode 100644
index ed3aff9..0000000
--- a/Shared/Disc0very.py
+++ /dev/null
@@ -1,97 +0,0 @@
-from socket import *
-from random import getrandbits
-from random import choice
-from time import time
-from time import sleep
-import Utils
-import Logging
-from _thread import start_new_thread
-
-
-HEY = "HEY".encode()
-NAY = "NAY".encode()
-WHAT = "WHAT".encode()
-
-
-class MissingInterfaceError(TypeError):
- def __init__(self):
- super(TypeError, self).__init__("platform requires interface parameter.")
-
-
-class Disc0very:
- def __init__(self, port, interface=None, max_peers=32):
- self.port = port
- self.sock = socket(AF_INET, SOCK_DGRAM)
- self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
- self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
- self.sock.bind(("", self.port))
- self.ip_address = Utils.get_ip_address(interface)
- self.discovering = False
- self.enlisting = False
-
-
-
- # Not thread-safe if ran in parallel with __enlist
- def discover(self, time_out=1):
- self.sock.setblocking(False)
- end_time = time() + time_out
- servers = []
- self.sock.sendto(HEY, ('255.255.255.255', self.port))
- while time() < end_time:
- try:
- data, address = self.sock.recvfrom(512)
- address = address[0]
- # If enlisted peer responds with ip address
- if data not in (HEY, NAY) and address != self.ip_address and not data in servers:
- servers.append(data)
- # If another peer is currently discovering
- elif data == HEY and address != self.ip_address:
- self.sock.sendto(NAY, ('255.255.255.255', self.port))
- return self.discover(time_out=time_out)
- # If another peer gave up
- elif data == NAY and address != self.ip_address:
- return None
- except BlockingIOError:
- sleep((choice(range(1, 10)) / 2) / 10)
- if len(servers) == 0:
- return None
- elif len(servers) > 1:
- self.sock.sendto(WHAT, ('255.255.255.255', self.port))
- else:
- return servers[0]
-
-
- def enlist(self, interface, blocking=False):
- if blocking:
- self.__enlist(interface)
- else:
- start_new_thread(self.__enlist, (interface, ))
-
-
- # Not thread-safe if ran in parallel with discover
- # Interface should always be provided when using a Wallaby
- # because wlan0 and wlan1 have an IP address assigned
- def __enlist(self, interface=None):
- self.sock.setblocking(True)
- data = ""
- while True:
- try:
- data, address = self.sock.recvfrom(512)
- except BlockingIOError:
- sleep(0.1)
- if data == HEY:
- self.sock.sendto(self.ip_address.encode(), ('255.255.255.255', self.port))
- elif data == WHAT:
- Logging.error("Apparently more than one server is running. "
- "Investigating...")
- # Discover and if other server is found shutdown
-
-
-if __name__ == "__main__":
- disc0very = Disc0very(3077)
- server = disc0very.discover()
- if not server:
- print("enlisting")
- disc0very.enlist(None, blocking=True)
- else:
- print(server)
\ No newline at end of file
diff --git a/Shared/Highway.py b/Shared/Highway.py
index ffc8233..90c679f 100644
--- a/Shared/Highway.py
+++ b/Shared/Highway.py
@@ -330,10 +330,10 @@ class Shared:
data_repr = str(data).replace("\n", " ")
if len(data_repr) > 80:
data_repr = data_repr[:80] + "..."
- Logging.info("Received '%s' on route '%s': %s (%s:%d)" % (
- type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict",
- route, data_repr, self.address,
- self.port))
+ Logging.info("Received '%s' on route '%s': %s (%s:%d)" % (
+ type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict",
+ route, data_repr, self.address,
+ self.port))
try:
route = self.routes[route]
except:
diff --git a/Shared/Logging.py b/Shared/Logging.py
index 6bd0e4d..52d0ab9 100644
--- a/Shared/Logging.py
+++ b/Shared/Logging.py
@@ -112,7 +112,6 @@ def success(message):
if __name__ == "__main__":
- import sys
info("Hi!", color=UNDERLINE+BACKGROUND_GREEN+RED)
header("This is a header")
warning("This is a warning") # > stderr
diff --git a/Shared/Meh.py b/Shared/Meh.py
new file mode 100644
index 0000000..5d0611d
--- /dev/null
+++ b/Shared/Meh.py
@@ -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()
\ No newline at end of file
diff --git a/Shared/Utils.py b/Shared/Utils.py
index bae4483..0972417 100644
--- a/Shared/Utils.py
+++ b/Shared/Utils.py
@@ -6,6 +6,8 @@ import struct
import socket
import fcntl
import subprocess
+import urllib
+
class HostnameNotChangedError(PermissionError):
def __init__(self):
@@ -13,11 +15,11 @@ class HostnameNotChangedError(PermissionError):
class NotSupportedOnPlatform(OSError):
def __init__(self):
- super(OSError, self).__init__("feature not avaliable on OS")
+ super(OSError, self).__init__("feature not avaliable on OS")
class PlaybackFailure(OSError):
def __init__(self):
- super(OSError, self).__init__("audio playback failed")
+ super(OSError, self).__init__("audio playback failed")
def capture_trace():
@@ -38,7 +40,7 @@ def is_darwin():
def is_windows():
- return platform.uname().system == "Windows"
+ return platform.uname().system == "Windows"
def set_hostname(hostname):
@@ -82,6 +84,10 @@ def get_ip_address(ifname=None):
return ip_address
+def get_ip_from_url(url):
+ return urllib.parse.urlsplit(url).netloc.split(':')[0]
+
+
def play_sound(path):
if is_linux() or is_darwin():
try:
@@ -89,4 +95,4 @@ def play_sound(path):
except subprocess.CalledProcessError as e:
raise PlaybackFailure()
else:
- raise NotSupportedOnPlatform()
\ No newline at end of file
+ raise NotSupportedOnPlatform()
diff --git a/Shared/bottle b/Shared/bottle
new file mode 160000
index 0000000..5a6fc77
--- /dev/null
+++ b/Shared/bottle
@@ -0,0 +1 @@
+Subproject commit 5a6fc77c9c57501b08e5b4f9161ae07d277effa9
diff --git a/Shared/dashb0ard b/Shared/dashb0ard
new file mode 160000
index 0000000..cca5173
--- /dev/null
+++ b/Shared/dashb0ard
@@ -0,0 +1 @@
+Subproject commit cca517355923990423a2c744bf2bc81080090ff3
diff --git a/Sublime/fl0w/CompileHighlight.sublime-syntax b/Sublime/fl0w/CompileHighlight.sublime-syntax
deleted file mode 100644
index 3e997dc..0000000
--- a/Sublime/fl0w/CompileHighlight.sublime-syntax
+++ /dev/null
@@ -1,11 +0,0 @@
-%YAML 1.2
----
-# http://www.sublimetext.com/docs/3/syntax.html
-name: Compile Highlight
-file_extensions: []
-hidden: true
-scope: source.inspect
-contexts:
- main:
- - match: \b(warning|error)\b
- scope: keyword.control.c
\ No newline at end of file
diff --git a/Sublime/fl0w/Default (Linux).sublime-keymap b/Sublime/fl0w/Default (Linux).sublime-keymap
deleted file mode 100644
index 00b52dc..0000000
--- a/Sublime/fl0w/Default (Linux).sublime-keymap
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "keys" : ["f8"],
- "command" : "run"
- },
- {
- "keys" : ["f9"],
- "command" : "stop"
- },
- {
- "keys" : ["f10"],
- "command" : "sensor"
- }
-]
\ No newline at end of file
diff --git a/Sublime/fl0w/Default (OSX).sublime-keymap b/Sublime/fl0w/Default (OSX).sublime-keymap
deleted file mode 100644
index 00b52dc..0000000
--- a/Sublime/fl0w/Default (OSX).sublime-keymap
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "keys" : ["f8"],
- "command" : "run"
- },
- {
- "keys" : ["f9"],
- "command" : "stop"
- },
- {
- "keys" : ["f10"],
- "command" : "sensor"
- }
-]
\ No newline at end of file
diff --git a/Sublime/fl0w/Default (Windows).sublime-keymap b/Sublime/fl0w/Default (Windows).sublime-keymap
deleted file mode 100644
index 00b52dc..0000000
--- a/Sublime/fl0w/Default (Windows).sublime-keymap
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "keys" : ["f8"],
- "command" : "run"
- },
- {
- "keys" : ["f9"],
- "command" : "stop"
- },
- {
- "keys" : ["f10"],
- "command" : "sensor"
- }
-]
\ No newline at end of file
diff --git a/Sublime/fl0w/Shared b/Sublime/fl0w/Shared
deleted file mode 120000
index 8c1ab6a..0000000
--- a/Sublime/fl0w/Shared
+++ /dev/null
@@ -1 +0,0 @@
-../../Shared
\ No newline at end of file
diff --git a/Sublime/fl0w/SublimeMenu.py b/Sublime/fl0w/SublimeMenu.py
deleted file mode 100644
index 8ff2c59..0000000
--- a/Sublime/fl0w/SublimeMenu.py
+++ /dev/null
@@ -1,140 +0,0 @@
-FUNCTION = type(lambda: 1)
-
-class Input:
- def __init__(self, caption, initial_text="", on_done=None, on_change=None,
- on_cancel=None, kwargs={}):
- self.caption = caption
- self.initial_text = initial_text
- self.on_done = on_done
- self.on_change = on_change
- self.on_cancel = on_cancel
- self.kwargs = kwargs
-
-
- def wrapped_on_done(self, input_):
- if not self.on_done == None:
- self.on_done(input_, **self.kwargs)
-
-
- def wrapped_on_change(self, input_):
- if not self.on_change == None:
- self.on_change(input_, **self.kwargs)
-
-
- def wrapped_on_cancel(self):
- if not self.on_cancel == None:
- self.on_cancel(**self.kwargs)
-
-
- def invoke(self, window):
- window.show_input_panel(self.caption, self.initial_text,
- self.wrapped_on_done, self.wrapped_on_change,
- self.wrapped_on_cancel)
-
-
-class Entry:
- def __init__(self, name, description="", action=None, kwargs={},
- sub_menu=None, input=None):
- self.name = name
- self.description = description
- self.action = action
- self.kwargs = kwargs
- self.sub_menu = sub_menu
- self.input = input
-
- def __eq__(self, other):
- return self.__dict__ == other.__dict__
-
-
-class Menu:
- def __init__(self, selected_index=-1, on_highlight=None, subtitles=True):
- self.selected_index = selected_index
- self.on_highlight = on_highlight
- self.subtitles = subtitles
- self.entries = {}
- self.window = None
- self.back = None
-
- def invoke(self, window, back=None):
- self.window = window
- self.back = back
- entries = self.menu_entries
- if back:
- entries.insert(0, ["Back",
- "Back to previous menu"] if self.subtitles else ["Back"])
- window.show_quick_panel(entries, self._action,
- flags=0, selected_index=self.selected_index,
- on_highlight=self.on_highlight)
-
- def _action(self, entry_id):
- if entry_id != -1:
- if self.back:
- if entry_id != 0:
- entry = self.entries[entry_id - 1]
- else:
- self.back.invoke(self.window, back=self.back.back)
- return
- else:
- entry = self.entries[entry_id]
- if entry.action != None:
- entry.action(**entry.kwargs)
- if entry.input != None:
- entry.input.invoke(self.window)
- if type(entry.sub_menu) is FUNCTION:
- entry.sub_menu(entry).invoke(self.window, back=self)
- elif entry.sub_menu != None:
- entry.sub_menu.invoke(self.window, back=self)
-
-
- @property
- def menu_entries(self):
- entries = []
- for entry_id in self.entries:
- if self.subtitles:
- entries.append([self.entries[entry_id].name, self.entries[entry_id].description])
- else:
- entries.append([self.entries[entry_id].name])
- return entries
-
-
- 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, entry):
- if entry.__class__ == Entry:
- if len(self.entries) > 0:
- entry_id = tuple(self.entries.keys())[-1] + 1
- else:
- entry_id = 0
- self.entries[entry_id] = entry
- else:
- raise TypeError("invalid type supplied")
-
-
- def remove(self, entry):
- if entry.__class__ == Entry:
- if entry in self.entries.values():
- found_entry_id = None
- for entry_id in self.entries:
- if self.entries[entry_id] == entry:
- found_entry_id = entry_id
- if found_entry_id != None:
- del self.entries[entry_id]
- else:
- raise TypeError("invalid type supplied")
-
- def clear(self):
- self.entries = {}
diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py
deleted file mode 100644
index fa2ea18..0000000
--- a/Sublime/fl0w/fl0w.py
+++ /dev/null
@@ -1,749 +0,0 @@
-from sys import path
-import os
-from time import strftime
-from functools import partial
-import re
-
-fl0w_path = os.path.dirname(os.path.realpath(__file__))
-shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/"
-if fl0w_path not in path:
- path.append(fl0w_path)
-if shared_path not in path:
- path.append(shared_path)
-
-
-import sublime
-import sublime_plugin
-
-from Highway import Client, Route, Pipe, DummyPipe
-from Utils import get_hostname
-
-from SublimeMenu import *
-import Logging
-
-import webbrowser
-import threading
-from time import sleep
-import os
-
-CHANNEL = 1
-FL0W_STATUS = "fl0w"
-
-def plugin_unloaded():
- for window in windows:
- if hasattr(window, "fl0w") and window.fl0w.connected:
- window.fl0w.invoke_disconnect()
- for sensor_type in ("analog", "digital"):
- window.active_view().erase_phantoms(sensor_type)
-
-
-PARENTHESES_REGEX = re.compile("\((.*?)\)")
-STYLE_OPEN = ""
-STYLE_CLOSE = "
"
-
-ERROR_OPEN = ""
-ERROR_CLOSE = "
"
-
-windows = []
-views = []
-sensor_phantoms = []
-
-def set_status(status, window):
- window.active_view().set_status(FL0W_STATUS,
- "fl0w: %s" % status)
-
-class Target:
- def __init__(self, id_, name):
- self.id = id_
- self.name = name
-
-
-class Fl0wClient(Client):
- def setup(self, routes, fl0w, debug=False):
- super().setup(routes, debug=debug)
- self.fl0w = fl0w
-
-
- def ready(self):
- self.fl0w.connected = True
- if self.fl0w.debug:
- Logging.info("Connection ready!")
- # Enlist on editor channel
- self.send({"channel" : CHANNEL, "name" : get_hostname()}, "subscribe")
- # Subscribe to controller channel
- self.send({"subscribe" : [2]}, "peers")
-
-
- def closed(self, code, reason):
- self.fl0w.invoke_disconnect()
- if self.fl0w.debug:
- Logging.info("Connection closed: %s (%s)" % (reason, code))
-
-
- def peer_unavaliable(self, peer):
- sublime.error_message("The specifed controller is not connected anymore.")
- if self.fl0w.target.id == peer:
- self.fl0w.target = None
-
-
- class Info(Route):
- def run(self, data, handler):
- info = ""
- for key in data:
- info += "%s: %s\n" % (key.capitalize(), ", ".join(data[key]))
- sublime.message_dialog(info)
- handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu)
-
-
- class Sensor(Pipe):
- def run(self, data, peer, handler):
- handler.fl0w.subscriptions_lock.acquire()
- for sensor_phantom in handler.fl0w.subscriptions:
- sensor_phantom.update_sensor_values(data)
- handler.fl0w.subscriptions_lock.release()
-
-
- class Peers(Route):
- def start(self, handler):
- self.selected_action_menu = None
-
- def run(self, data, handler):
- handler.fl0w.controller_menu.clear()
- if handler.fl0w.target != None:
- if not handler.fl0w.target.id in data:
- handler.fl0w.target = None
- for id_ in data:
- action_menu = Menu()
- power_menu = Menu()
- utilities_menu = Menu()
- action_menu.id_ = id_
- action_menu.name = data[id_]["name"]
- action_menu += Entry("Set Target",
- "Set controller as target for program execution and sensor readouts",
- action=partial(self.set_target,
- handler, id_, data[id_]["name"]))
- action_menu += Entry("Run program",
- "Run a botball program on the controller",
- action=partial(handler.pipe, None, "list_programs", id_))
- action_menu += Entry("Stop programs",
- "Stop all currently running botball programs",
- action=partial(handler.pipe, None, "stop_programs", id_))
- utilities_menu += Entry("Set Name",
- "Sets the hostname of the selected controller",
- action=partial(lambda handler, id_: Input("New Hostname:",
- initial_text=data[id_]["name"],
- on_done=lambda hostname: handler.pipe(
- {"set" : hostname},
- "hostname", id_)).invoke(handler.fl0w.window), handler, id_))
- utilities_menu += Entry("Processes",
- "Lists processes currently running on controller",
- action=partial(handler.pipe, None, "processes", id_))
- utilities_menu += Entry("Identify",
- "Plays an identification sound on the controller.",
- action=partial(handler.pipe, None, "identify", id_))
- action_menu += Entry("Utilities", "Stuff you might need but probably won't",
- sub_menu=utilities_menu)
- power_menu += Entry("Shutdown",
- "Shutdown the controller",
- action=partial(handler.pipe, None, "shutdown", id_))
- power_menu += Entry("Reboot",
- "Reboot the controller",
- action=partial(handler.pipe, None, "reboot", id_))
- action_menu += Entry("Power", "Power related actions", sub_menu=power_menu)
- action_menu.back = handler.fl0w.controller_menu
- handler.fl0w.controller_menu += Entry(data[id_]["name"], id_, sub_menu=action_menu,
- action=self.set_selected_action_menu,
- kwargs={"selected_action_menu" : action_menu})
-
-
- def set_target(self, handler, peer, name):
- handler.fl0w.target = Target(peer, name)
- if handler.fl0w.debug:
- set_status("Target: %s" % peer, handler.fl0w.window)
-
-
- def set_selected_action_menu(self, selected_action_menu):
- self.selected_action_menu = selected_action_menu
-
-
- class Processes(Pipe):
- def run(self, data, peer, handler):
- view = handler.fl0w.window.new_file()
- view.set_name("Processes")
- view.settings().set("draw_indent_guides", False)
- for line in data:
- view.run_command("append", {"characters": line + "\n"})
- view.set_read_only(True)
-
-
- class ListPrograms(Pipe):
- def run(self, data, peer, handler):
- program_menu = Menu(subtitles=False)
- for program in data:
- program_menu += Entry(program,
- action=partial(self.run_program,
- handler, handler.routes["peers"].selected_action_menu.id_,
- program))
- program_menu.invoke(handler.fl0w.window,
- back=handler.routes["peers"].selected_action_menu)
-
- def run_program(self, handler, id_, program):
- handler.pipe(program, "run_program", id_)
-
-
- class RunProgram(Pipe):
- PROGRAM_NOT_FOUND = 1
-
- def run(self, data, peer, handler):
- if data == self.__class__.PROGRAM_NOT_FOUND:
- sublime.error_message("Program not found.")
-
-
- class StopPrograms(Pipe):
- NO_PROGRAMS_RUNNING = 1
-
- def run(self, data, peer, handler):
- if data == self.__class__.NO_PROGRAMS_RUNNING:
- sublime.error_message("No programs running.")
-
-
- class StdStream(Pipe):
- def start(self, handler):
- self.output_panels = {}
-
- self.lock = threading.RLock()
- self.buffer = {}
-
- self.handler = None
-
- self.fetcher = self.Fetcher(self.buffer, self.write_to_panel,
- self.lock)
- self.fetcher.start()
-
-
- def create_output_panel(self, window, peer):
- view = window.create_output_panel(peer)
- view.settings().set("draw_white_space", False)
- view.settings().set("draw_indent_guides", False)
- view.settings().set("gutter", False)
- view.settings().set("line_numbers", False)
- view.set_read_only(True)
- return view
-
-
- def run(self, data, peer, handler):
- self.handler = handler
- if type(data) is str:
- self.lock.acquire()
- # try/except is faster than an explicit if as long as the
- # condition is not met
- try:
- self.buffer[peer].append(data)
- except KeyError:
- self.buffer[peer] = []
- self.create_output_panel(handler.fl0w.window, peer)
- self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer)
- self.buffer[peer].append(data)
- self.lock.release()
- # Meta info comes in so infrequently that the conditional logic would
- # slow down the regular output streaming
- elif type(data) is dict:
- meta_text = ""
- if "exit_code" in data:
- meta_text += "Program finished with exit code: %d\n" % data["exit_code"]
- self.lock.acquire()
- # try/except is faster than an explicit if as long as the
- # condition is not met
- # function call is also slower
- try:
- self.buffer[peer].append(meta_text)
- except KeyError:
- self.buffer[peer] = []
- self.create_output_panel(handler.fl0w.window, peer)
- self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer)
- self.buffer[peer].append(meta_text)
- self.lock.release()
-
-
-
- def write_to_panel(self, text, peer):
- self.output_panels[peer].set_read_only(False)
- self.output_panels[peer].run_command("append", {"characters": text, "scroll_to_end" : True})
- self.output_panels[peer].set_read_only(True)
- self.handler.fl0w.window.run_command("show_panel", {"panel": "output.%s" % peer})
-
-
- # Sublime gets quite overwhelmed when faced with typical
- # "while (1) { printf(...)}" output.
- # That's why instead of directly writing to the view all received text
- # is bundled together after a fixed period of time.
- class Fetcher(threading.Thread):
- def __init__(self, buffer, write_to_panel, lock, push_rate=0.2):
- threading.Thread.__init__(self)
- self.buffer = buffer
- self.write_to_panel = write_to_panel
- self.lock = lock
- self.push_rate = push_rate
- self.daemon = True
-
- def run(self):
- while True:
- self.lock.acquire()
- for peer in self.buffer:
- if len(self.buffer[peer]) > 0:
- self.write_to_panel(
- "".join(self.buffer[peer]),
- peer)
- self.buffer[peer] = []
- self.lock.release()
- sleep(self.push_rate)
-
-
-class Fl0w:
- def __init__(self, window, debug=False):
- self.settings = sublime.load_settings("fl0w.sublime-settings")
- self.window = window
- self.folder = window.folders()[0]
- if self.folder != "/":
- self.folder = self.folder + "/"
-
- self.connected = False
-
- self.subscriptions = {}
- self.subscriptions_lock = threading.Lock()
- self._combined_subscriptions = {"analog" : [], "digital" : []}
-
- self._target = None
- self._debug = debug
-
-
- self.start_menu = Menu()
- self.start_menu += Entry("Connect", "Connect to a fl0w server",
- action=partial(Input("Address:Port (auto-connect nyi)",
- initial_text=self.settings.get("address", "127.0.0.1:3077"),
- on_done=self.invoke_connect).invoke, self.window))
- self.start_menu += Entry("About", "Information about fl0w",
- action=self.invoke_about)
-
- self.debug_menu = Menu(subtitles=False)
- self.debug_menu += Entry("On",
- action=lambda: self.set_debug(True))
- self.debug_menu += Entry("Off",
- action=lambda: self.set_debug(False))
-
-
- self.settings = Menu()
- self.settings += Entry("Debug", "Toggle debug mode",
- sub_menu=self.debug_menu)
-
-
- self.meta = Menu()
- self.meta += Entry("Info", "Server info",
- action=lambda: self.ws.send(None, "info"))
- self.meta_entry = Entry("Meta", "Debug information about fl0w",
- sub_menu=self.meta)
- if self.debug:
- self.main_menu += self.meta_entry
-
-
- self.main_menu = Menu()
- self.controller_menu = Menu()
- self.main_menu += Entry("Controllers", "All connected controllers",
- sub_menu=self.controller_menu)
- self.main_menu += Entry("Settings", "General purpose settings",
- sub_menu=self.settings)
- self.main_menu += Entry("Disconnect", "Disconnect from server",
- action=self.invoke_disconnect)
-
- # Patch all sensor phantom that have been created before a fl0w instance
- # was attached to the window
- for sensor_phantom in sensor_phantoms:
- if sensor_phantom.window.id() == self.window.id():
- sensor_phantom.fl0w = self
- if self.debug:
- Logging.info("Patched sensor phantom '%s'" % str(sensor_phatom))
-
-
- @property
- def target(self):
- return self._target
-
-
- @target.setter
- def target(self, target):
- if self.target != None:
- self.ws.pipe("unsubscribe", "sensor", self.target.id)
- self._target = target
- if target != None:
- set_status("Target: %s (%s)" % (target.name, target.id), self.window)
- if self.combined_subscriptions != {"analog" : [], "digital" : []}:
- self.ws.pipe({"subscribe" : self.combined_subscriptions}, "sensor", target.id)
- else:
- set_status("The target has become unavaliable.", self.window)
-
-
- @property
- def debug(self):
- return self._debug
-
-
- def set_debug(self, debug):
- self.debug = debug
-
-
- @debug.setter
- def debug(self, debug):
- if debug:
- self._debug = True
- if not self.meta_entry in self.main_menu.entries.values():
- self.main_menu += self.meta_entry
- else:
- self._debug = False
- self.main_menu -= self.meta_entry
- set_status("Debug set to %s" % self._debug, self.window)
-
-
- # Could be simplified because only one view can be active at any time.
- # This would definetly lead to some major performace improvements on
- # view switching and less unnecessary unsubscribes.
-
- # On the other hand it might be a good idea to leave it in and provide
- # an option to disable aggressive unsubscribes
- @property
- def combined_subscriptions(self):
- return self._combined_subscriptions
-
-
- @combined_subscriptions.setter
- def combined_subscriptions(self, combined_subscriptions_):
- if self.combined_subscriptions != combined_subscriptions_:
- self._combined_subscriptions = combined_subscriptions_
- if self.connected and self.target != None:
- self.ws.pipe("unsubscribe", "sensor", self.target.id)
- if combined_subscriptions_ != {"analog" : [], "digital" : []}:
- self.ws.pipe({"subscribe" : combined_subscriptions_}, "sensor",
- self.target.id)
-
-
- def subscribe(self, sensor_phatom, subscriptions):
- self.subscriptions_lock.acquire()
- self.subscriptions[sensor_phatom] = subscriptions
- self.subscriptions_lock.release()
- self.make_subscriptions()
-
-
-
- def unsubscribe(self, sensor_phantom):
- if sensor_phantom in self.subscriptions:
- """
- print("Lock will be aquired.")
- self.subscriptions_lock.acquire()
- print("Lock was aquired.")
- del self.subscriptions[sensor_phantom]
- print("Lock will be released.")
- self.subscriptions_lock.release()
- print("Lock was released.")
- """
- # Temporary solution, locking caused sublime to freeze
- # Could cause problems if lots (> 100) views are open.
- self.subscriptions[sensor_phantom] = {"analog" : [], "digital" : []}
- self.make_subscriptions()
-
-
- def make_subscriptions(self):
- combined_subscriptions = {"analog" : [], "digital" : []}
- for sensor_phantom in self.subscriptions:
- for sensor_type in ("analog", "digital"):
- combined_subscriptions[sensor_type] = list(
- set(combined_subscriptions[sensor_type]) |
- set(self.subscriptions[sensor_phantom][sensor_type])
- )
- self.combined_subscriptions = combined_subscriptions
-
-
- def run_program(self, path):
- if self.connected and self.target != None:
- relpath = os.path.relpath(path, self.folder)
- if os.path.isfile(self.folder + relpath):
- if self.debug:
- Logging.info("Running program '%s'" % relpath)
- self.ws.pipe(relpath.rstrip(".c"), "run_program", self.target.id)
-
-
- def stop_programs(self):
- if self.connected and self.target != None:
- self.ws.pipe(None, "stop_programs", self.target.id)
-
-
- def invoke_start_menu(self):
- self.start_menu.invoke(self.window)
-
-
- def invoke_main_menu(self):
- self.main_menu.invoke(self.window)
-
-
- def invoke_about(self):
- if sublime.ok_cancel_dialog("fl0w by @robot0nfire", "robot0nfire.com"):
- webbrowser.open("http://robot0nfire.com")
-
-
-
- def connect(self, address):
- try:
- self.ws = Fl0wClient(address)
- self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(),
- "processes" : Fl0wClient.Processes(),
- "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor(),
- "std_stream" : Fl0wClient.StdStream(), "run_program" : Fl0wClient.RunProgram(),
- "stop_programs" : Fl0wClient.StopPrograms()},
- self, debug=True)
- self.ws.connect()
- sublime.set_timeout_async(self.ws.run_forever, 0)
- set_status("Connection opened '%s'" % self.folder, self.window)
- self.connected = True
- self.settings.set("address", address)
- except OSError as e:
- sublime.error_message("Error during connection creation:\n %s" % str(e))
-
-
-
- def invoke_connect(self, address):
- # Will be removed once autoconnect works
- self.connect("ws://%s" % address)
-
-
- def invoke_disconnect(self):
- if self.connected:
- for sensor_phantom in sensor_phantoms:
- if sensor_phantom.window.id() == self.window.id():
- sensor_phantom.enabled = False
- self.target = None
- self.ws.close()
- set_status("Connection closed '%s'" % self.folder, self.window)
- self.connected = False
-
-
-class Fl0wCommand(sublime_plugin.WindowCommand):
- def run(self):
- valid_window_setup = True
- folder_count = len(self.window.folders())
- if folder_count > 1:
- sublime.error_message("Only one open folder per window is allowed.")
- valid_window_setup = False
- elif folder_count == 0:
- sublime.error_message("No folder open in window.")
- valid_window_setup = False
- if valid_window_setup:
- if not hasattr(self.window, "fl0w"):
- folder = self.window.folders()[0]
- files = os.listdir(folder)
- if not ".no-fl0w" in files:
- if not ".fl0w" in files:
- open(folder + "/.fl0w", 'a').close()
- self.window.fl0w = Fl0w(self.window)
- windows.append(self.window)
- self.window.fl0w.start_menu.invoke(self.window)
- else:
- self.window.fl0w = Fl0w(self.window)
- windows.append(self.window)
- self.window.fl0w.start_menu.invoke(self.window)
- else:
- sublime.error_message("fl0w can't be opened in your current directory (.no-fl0w file exists)")
- else:
- if not self.window.fl0w.connected:
- self.window.fl0w.invoke_start_menu()
- else:
- self.window.fl0w.invoke_main_menu()
- else:
- if hasattr(self.window, "fl0w"):
- sublime.error_message("Window setup was invalidated (Don't close or open any additional folders in a fl0w window)")
- self.window.fl0w.invoke_disconnect()
-
-
-class RunCommand(sublime_plugin.WindowCommand):
- def run(self):
- if hasattr(self.window, "fl0w"):
- if self.window.fl0w.connected:
- if self.window.fl0w.target == None:
- sublime.error_message("A target controller has to be set to "
- "run programs.")
- else:
- file_name = self.window.active_view().file_name()
- if file_name != None and file_name.endswith(".c"):
- self.window.fl0w.run_program(file_name)
- else:
- sublime.error_message("fl0w is not connected.")
- else:
- sublime.error_message("fl0w is not running in your current window.")
-
-class StopCommand(sublime_plugin.WindowCommand):
- def run(self):
- if hasattr(self.window, "fl0w"):
- if self.window.fl0w.connected:
- if self.window.fl0w.target == None:
- sublime.error_message("A target controller has to be set to "
- "stop programs.")
- else:
- self.window.fl0w.stop_programs()
- else:
- sublime.error_message("fl0w is not connected.")
- else:
- sublime.error_message("fl0w is not running in your current window.")
-
-
-class SensorCommand(sublime_plugin.WindowCommand):
- def run(self):
- if hasattr(self.window, "fl0w"):
- if self.window.fl0w.connected:
- if self.window.fl0w.target == None:
- sublime.error_message("A target controller has to be set to "
- "enable inline sensor readouts.")
- else:
- view_id = self.window.active_view().id()
- for view in views:
- if view.id() == view_id:
- view.sensor_phantom.enabled = not view.sensor_phantom.enabled
- view_file_name = view.file_name()
- if view_file_name == None:
- view_file_name = "untitled"
- set_status("%s sensor phantoms for '%s'." % (
- "Enabled" if view.sensor_phantom.enabled else "Disabled",
- view_file_name),
- self.window)
- else:
- sublime.error_message("fl0w is not connected.")
- else:
- sublime.error_message("fl0w is not running in your current window.")
-
-
-
-class SensorPhantom(sublime_plugin.ViewEventListener):
- def __init__(self, view):
- self.view = view
- self.view.sensor_phantom = self
- if not view in views:
- views.append(view)
- self.window = view.window()
-
- # Is patched by the fl0w instance that is in control of the same window
- self.fl0w = None
- self._enabled = False
-
- self.previously_enabled = False
-
- self._matches = {"analog" : [], "digital" : []}
-
- self.timeout_scheduled = False
- self.needs_update = False
-
- for window in windows:
- if hasattr(window, "fl0w"):
- self.fl0w = window.fl0w
- if not self in sensor_phantoms:
- sensor_phantoms.append(self)
-
-
- @property
- def enabled(self):
- return self._enabled
-
-
- @enabled.setter
- def enabled(self, enabled_):
- if enabled_:
- if self.fl0w != None:
- self.find_matches()
- self.fl0w.subscribe(self, self.subscriptions)
- self._enabled = True
- else:
- if self.fl0w != None:
- self.fl0w.unsubscribe(self)
- for sensor_type in ("analog", "digital"):
- self.view.erase_phantoms(sensor_type)
- self._enabled = False
-
-
- @property
- def matches(self):
- return self._matches
-
- @matches.setter
- def matches(self, matches_):
- if not matches_ == self.matches:
- self._matches = matches_
- self.fl0w.subscribe(self, self.subscriptions)
-
-
- @property
- def subscriptions(self):
- subscriptions_ = {"analog" : [], "digital" : []}
- for sensor_type in ("analog", "digital"):
- subscriptions_[sensor_type] = [sensor[0] for sensor in self.matches[sensor_type]]
- return subscriptions_
-
-
- def find_matches(self):
- matches = {"analog" : [], "digital" : []}
- # Don't do any calculations on 1MB or larger files
- if self.view.size() < 2**20:
- for method_name in ("analog", "digital"):
- candidates = self.view.find_all("%s\(\d*\)" % method_name)
- for candidate in candidates:
- line = self.view.substr(candidate)
- port_candidates = re.findall(PARENTHESES_REGEX, line)
- if len(port_candidates) == 1:
- if port_candidates[0].isnumeric():
- matches[method_name].append(
- (
- int(port_candidates[0]),
- sublime.Region(self.view.line(candidate.a).b)
- ))
- self.matches = matches
-
- # Called by fl0w instance
- def update_sensor_values(self, readouts):
- for sensor_type in ("analog", "digital"):
- self.view.erase_phantoms(sensor_type)
- for match in self.matches[sensor_type]:
- try:
- self.view.add_phantom(sensor_type, match[1],
- STYLE_OPEN + str(readouts[sensor_type][str(match[0])]) + STYLE_CLOSE,
- sublime.LAYOUT_INLINE)
- except KeyError:
- self.view.add_phantom(sensor_type, match[1],
- ERROR_OPEN + "!" + ERROR_CLOSE,
- sublime.LAYOUT_INLINE)
-
-
- def handle_timeout(self):
- self.timeout_scheduled = False
- if self.needs_update:
- self.needs_update = False
- self.find_matches()
-
-
- def on_modified(self):
- if self.enabled:
- if self.timeout_scheduled:
- self.needs_update = True
- else:
- sublime.set_timeout(lambda: self.handle_timeout(), 500)
- self.find_matches()
-
-
-
- def on_deactivated(self):
- self.previously_enabled = self.enabled
- if self.enabled:
- self.enabled = False
-
- def on_activated(self):
- if not self.enabled and self.previously_enabled:
- self.enabled = True
-
-
- def __del__(self):
- self.enabled = False
- if self in sensor_phantoms:
- del sensor_phantoms[sensor_phantoms.index(self)]
diff --git a/Sublime/fl0w/fl0w.sublime-commands b/Sublime/fl0w/fl0w.sublime-commands
deleted file mode 100644
index a2b89be..0000000
--- a/Sublime/fl0w/fl0w.sublime-commands
+++ /dev/null
@@ -1,6 +0,0 @@
-[
- {
- "caption": "fl0w: Menu",
- "command": "fl0w"
- }
-]
\ No newline at end of file
diff --git a/Sublime/fl0w/fl0w.sublime-settings b/Sublime/fl0w/fl0w.sublime-settings
deleted file mode 100644
index 51a7f3b..0000000
--- a/Sublime/fl0w/fl0w.sublime-settings
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "server_address": "", // Last server address
- "compression_level": 2
-}
diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py
index 24b5e99..7c85393 100644
--- a/Wallaby/Wallaby.py
+++ b/Wallaby/Wallaby.py
@@ -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,27 +10,24 @@ import sys
import subprocess
from random import randint
from _thread import start_new_thread
+from ctypes import cdll
import threading
+import json
+import re
+
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 + "/"
-
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)
+CONFIG_PATH = "wallaby.cfg"
-if not IS_WALLABY:
- Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.")
+config = Config()
+config.add(Option("server_address", "ws://127.0.0.1:3077"))
+config.add(Option("output_unbuffer", "stdbuf"))
+config.add(Option("identify_sound", "Wallaby/identify.wav",
+ validator=lambda sound: os.path.isfile(sound)))
class SensorReadout:
@@ -40,7 +37,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()
@@ -154,22 +151,11 @@ class SensorReadout:
class Identify(Pipe):
def run(self, data, peer, handler):
- Utils.play_sound(config.identify_sound)
- if handler.debug:
- Logging.success("I was identified!")
-
-
-class ListPrograms(Pipe):
- def run(self, data, peer, handler):
- programs = []
- if os.path.isdir(PATH):
- for program in os.listdir(PATH):
- if "botball_user_program" in os.listdir(PATH + program):
- programs.append(program)
- else:
- Logging.error("Harrogate folder structure does not exist. "
- "You broke something, mate.")
- handler.pipe(programs, handler.reverse_routes[self], peer)
+ try:
+ Utils.play_sound(config.identify_sound)
+ except Utils.PlaybackFailure:
+ Logging.error("Could not play identification sound")
+ Logging.success("I was identified!")
class StopPrograms(Pipe):
@@ -178,7 +164,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 +182,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:
@@ -252,6 +238,8 @@ class Sensor(Pipe):
self.sensor_readout = SensorReadout(handler)
+
+
class Shutdown(Pipe):
def run(self, data, peer, handler):
try:
@@ -281,21 +269,6 @@ class WhoAmI(Route):
handler.send(None, "whoami")
-class Hostname(Pipe):
- def run(self, data, peer, handler):
- if type(data) is dict:
- if "set" in data:
- try:
- Utils.set_hostname(str(data["set"]))
- except Utils.HostnameNotChangedError:
- if IS_WALLABY:
- Logging.error("Hostname change unsuccessful. "
- "Something is wrong with your Wallaby.")
- else:
- Logging.warning("Hostname change unsuccessful. "
- "This seems to be a dev-system, so don't worry too "
- "much about it.")
-
class Processes(Pipe):
def run(self, data, peer, handler):
@@ -304,43 +277,90 @@ class Processes(Pipe):
"processes", peer)
+class Output(Pipe):
+ def __init__(self):
+ self.content = []
+ self.handler = None
+ self.subscribers = []
+
+ self.escape_regex = re.compile(r"(\[\d+m?)")
+
+ self.stdout = Output.Std(Logging.stdout, self.push)
+ self.stderr = Output.Std(Logging.stderr, self.push)
+
+ Logging.stdout = self.stdout
+ Logging.stderr = self.stderr
+
+ def run(self, data, peer, handler):
+ if data == "subscribe":
+ self.subscribers.append(peer)
+ elif data == "unsubscribe":
+ self.unsubscribe(peer)
+
+
+ def start(self, handler):
+ self.handler = handler
+
+
+ def push(self, s):
+ s = self.escape_regex.sub("", s)
+ for peer in self.subscribers:
+ self.handler.pipe(s, "output", peer)
+
+
+ def unsubscribe(self, peer):
+ if peer in self.subscribers:
+ del self.subscribers[self.subscribers.index(peer)]
+
+ class Std:
+ def __init__(self, old_std, write_callback):
+ self.old_std = old_std
+ self.write_callback = write_callback
+
+
+ def write(self, s):
+ self.old_std.write(s)
+ self.write_callback(s)
+
+
+ def flush(self):
+ self.old_std.flush()
+
+
class Handler(Client):
def setup(self, routes, debug=False):
super().setup(routes, debug=debug)
+
def peer_unavaliable(self, peer):
if self.debug:
Logging.info("Unsubscribing '%s' from all sensor updates." % peer)
self.routes["sensor"].sensor_readout.unsubscribe_all(peer)
-
-
-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)))
-
-try:
- config = config.read_from_file(CONFIG_PATH)
-except FileNotFoundError:
- config.write_to_file(CONFIG_PATH)
- config = config.read_from_file(CONFIG_PATH)
+ self.routes["output"].unsubscribe(peer)
try:
- ws = Handler(config.server_address)
- # 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(),
- "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer),
+ config = config.load(CONFIG_PATH)
+except (IOError, ExceptionInConfigError):
+ config.dump(CONFIG_PATH)
+ config = config.load(CONFIG_PATH)
+
+
+try:
+ while 1:
+ ws = Handler(config.server_address)
+ # setup has to be called before the connection is established
+ ws.setup({"subscribe" : Subscribe(), "sensor" : Sensor(),
+ "identify" : Identify(), "whoami" : WhoAmI(),
"stop_programs" : StopPrograms(), "shutdown" : Shutdown(),
- "reboot" : Reboot()},
- debug=config.debug)
- ws.connect()
+ "reboot" : Reboot(), "output" : Output()},
+ debug=False)
+ try:
+ ws.connect()
+ break
+ except ConnectionRefusedError:
+ Logging.warning("Server not running... Retrying in 5s")
+ time.sleep(5)
ws.run_forever()
except KeyboardInterrupt:
- ws.close()
\ No newline at end of file
+ ws.close()
diff --git a/Wallaby/identify.wav b/Wallaby/identify.wav
index 4ab0ae3..8a444b1 100644
--- a/Wallaby/identify.wav
+++ b/Wallaby/identify.wav
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2c66eb1c4a17efbeaf27e2dae37d547886b447d72a75168fe0d439d699f25fcb
-size 75918
+oid sha256:81832bc5b34359bc87173dc234cbb30ea23aa541814809ad16eb1ba9bd7335af
+size 22858
diff --git a/fl0w.py b/fl0w.py
index a446800..bac79b4 100644
--- a/fl0w.py
+++ b/fl0w.py
@@ -1,3 +1,4 @@
from sys import path
-from os.path import abspath
-path.append(abspath("Shared"))
\ No newline at end of file
+from os.path import abspath, dirname, realpath, join
+
+path.append(join(abspath(dirname(realpath(__file__))), 'Shared'))