Compare commits

...
This repository has been archived on 2025-06-04. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

33 commits

Author SHA1 Message Date
Philip Trauner
11a4c760d7 Wait for server to become avaliable 2017-04-24 14:25:17 +03:00
Philip Trauner
fc95b26c45 Handle playback failure 2017-04-22 19:56:04 +02:00
Philip Trauner
ae93513fb8 Removed Compile route and behem0th, updated dashb0ard path 2017-04-20 17:27:20 +02:00
Philip Trauner
90e15a8c14 Removed fl0w-proper bootstrapping code, unneeded routes 2017-04-20 17:26:05 +02:00
Philip Trauner
7fc4a0697b Removed selective debug suppression 2017-04-20 17:25:02 +02:00
Philip Trauner
d5af3524bc Upped dashb0ard version 2017-04-20 17:22:48 +02:00
Philip Trauner
7e299779c1 Reintroduced dashb0ard 2017-04-20 17:22:28 +02:00
Philip Trauner
1c872e1c6f Replaced identification sound 2017-04-20 16:50:34 +02:00
Philip Trauner
1d0253cb81 Removed Disc0very (obsolete) 2017-04-20 16:49:51 +02:00
Philip Trauner
fc8e117a93 Removed fl0w ignore file for Sublime Text 2017-04-20 11:09:51 +02:00
Philip Trauner
3468ea2cca Removed behem0th 2017-04-20 11:09:25 +02:00
Philip Trauner
75b3a24aec Removed Sublime Text plugin 2017-04-20 11:05:09 +02:00
Philip Trauner
9340ae44cc Removed temporary dashb0ard version 2017-04-20 11:04:23 +02:00
Christoph Heiss
4e611c33e9 Update behem0th 2017-03-22 19:39:14 +01:00
Christoph Heiss
e3f6d56d02 Remove superfluous whitespaces at the end of lines 2017-03-22 19:38:49 +01:00
Philip Trauner
526695e209 Added output streaming
stdout as well as stderr can now be subscribed to. ANSI control char filtering is still wip
2017-03-15 20:52:07 +01:00
Philip Trauner
1a918b5843 Added selective debug suppression
"pipe" can get quite noisy (main reason was recursion because of output)
2017-03-15 20:50:16 +01:00
Philip Trauner
3061d5569b Removed unnecessary import 2017-03-15 20:49:59 +01:00
Philip Trauner
ae81d646f6 Added output dummy pipe 2017-03-15 20:49:11 +01:00
Philip Trauner
a8ebee04c1 Move to Meh, create Harrogate user on first start, initial binary notifications 2017-03-03 20:25:29 +01:00
Philip Trauner
868505126e Updated fl0w image url 2017-03-02 22:34:22 +01:00
Philip Trauner
d147a81499 Serve dashb0ard and static content on startup 2017-03-02 22:24:39 +01:00
Philip Trauner
65d2715ea5 Temporarily added dashb1ard as a submodule 2017-03-02 22:23:53 +01:00
Philip Trauner
7c7412f88c Fixed sublime settings 2017-03-02 20:15:04 +01:00
Philip Trauner
dfdbf0634d Removed unused imports, moved to Meh, removed Broadcast, ...
Fixed behem0th path, disabled compile for now
2017-03-02 20:13:48 +01:00
Philip Trauner
f8112743b7 Removed Broadcast because it wasn't used anywhere
Piping got rid of broadcasts. That's good.
2017-03-02 20:12:56 +01:00
Philip Trauner
7f74a6b56f Replaced Config with Meh (better in every regard, basically)
Meh is a good config "engine". Config isn't.
2017-03-02 20:12:05 +01:00
Philip Trauner
2b97f0313f Added bottle.py to serve dashb0ard 2017-03-02 20:11:25 +01:00
Christoph Heiss
5467112a86 Update behem0th 2017-03-02 17:20:39 +01:00
Christoph Heiss
25f900e33c Sync correct path on Wallabys 2017-03-02 10:01:41 +01:00
Christoph Heiss
7f067f40e9 Initial integration of behem0th. 2017-03-01 21:56:29 +01:00
Christoph Heiss
5188457be5 Add behem0th as submodule 2017-03-01 21:53:34 +01:00
Christoph Heiss
048042d1b1 Fix appending of the 'Shared'-folder to the python search path.
It assumed that 'Shared' was in the current working directory, which
may not be always true.
2017-03-01 21:48:13 +01:00
25 changed files with 490 additions and 1379 deletions

6
.gitmodules vendored
View file

@ -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

View file

View file

@ -1,4 +1,4 @@
<img src="http://content.nicokratky.me/flow-logo/logo-horizontal.png" alt="fl0w logo" height="70">
<img src="https://cloud.githubusercontent.com/assets/9287847/23527929/2e61683c-ff98-11e6-8f3a-2ae440d63663.png" alt="fl0w logo" height="70">
# fl0w
fl0w is currently in the process of being refactored and revamped.

View file

@ -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__()

View file

@ -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/<filepath:path>")
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.")

View file

@ -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()

View file

@ -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)

View file

@ -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:

View file

@ -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

328
Shared/Meh.py Normal file
View 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()

View file

@ -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()
raise NotSupportedOnPlatform()

1
Shared/bottle Submodule

@ -0,0 +1 @@
Subproject commit 5a6fc77c9c57501b08e5b4f9161ae07d277effa9

1
Shared/dashb0ard Submodule

@ -0,0 +1 @@
Subproject commit cca517355923990423a2c744bf2bc81080090ff3

View file

@ -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

View file

@ -1,14 +0,0 @@
[
{
"keys" : ["f8"],
"command" : "run"
},
{
"keys" : ["f9"],
"command" : "stop"
},
{
"keys" : ["f10"],
"command" : "sensor"
}
]

View file

@ -1,14 +0,0 @@
[
{
"keys" : ["f8"],
"command" : "run"
},
{
"keys" : ["f9"],
"command" : "stop"
},
{
"keys" : ["f10"],
"command" : "sensor"
}
]

View file

@ -1,14 +0,0 @@
[
{
"keys" : ["f8"],
"command" : "run"
},
{
"keys" : ["f9"],
"command" : "stop"
},
{
"keys" : ["f10"],
"command" : "sensor"
}
]

View file

@ -1 +0,0 @@
../../Shared

View file

@ -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 = {}

View file

@ -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 = "<body><style>code { color: var(--orangish); }</style><code>"
STYLE_CLOSE = "</code></body>"
ERROR_OPEN = "<body><style>code { color: var(--redish); }</style><code>"
ERROR_CLOSE = "</code></body>"
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)]

View file

@ -1,6 +0,0 @@
[
{
"caption": "fl0w: Menu",
"command": "fl0w"
}
]

View file

@ -1,4 +0,0 @@
{
"server_address": "", // Last server address
"compression_level": 2
}

View file

@ -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()
ws.close()

BIN
Wallaby/identify.wav (Stored with Git LFS)

Binary file not shown.

View file

@ -1,3 +1,4 @@
from sys import path
from os.path import abspath
path.append(abspath("Shared"))
from os.path import abspath, dirname, realpath, join
path.append(join(abspath(dirname(realpath(__file__))), 'Shared'))