Compare commits
33 commits
web_socket
...
core
Author | SHA1 | Date | |
---|---|---|---|
|
11a4c760d7 | ||
|
fc95b26c45 | ||
|
ae93513fb8 | ||
|
90e15a8c14 | ||
|
7fc4a0697b | ||
|
d5af3524bc | ||
|
7e299779c1 | ||
|
1c872e1c6f | ||
|
1d0253cb81 | ||
|
fc8e117a93 | ||
|
3468ea2cca | ||
|
75b3a24aec | ||
|
9340ae44cc | ||
|
4e611c33e9 | ||
|
e3f6d56d02 | ||
|
526695e209 | ||
|
1a918b5843 | ||
|
3061d5569b | ||
|
ae81d646f6 | ||
|
a8ebee04c1 | ||
|
868505126e | ||
|
d147a81499 | ||
|
65d2715ea5 | ||
|
7c7412f88c | ||
|
dfdbf0634d | ||
|
f8112743b7 | ||
|
7f74a6b56f | ||
|
2b97f0313f | ||
|
5467112a86 | ||
|
25f900e33c | ||
|
7f067f40e9 | ||
|
5188457be5 | ||
|
048042d1b1 |
25 changed files with 490 additions and 1379 deletions
6
.gitmodules
vendored
6
.gitmodules
vendored
|
@ -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
|
||||
|
|
0
.no-fl0w
0
.no-fl0w
|
@ -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.
|
||||
|
|
|
@ -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__()
|
116
Server/Server.py
116
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/<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.")
|
||||
|
|
118
Shared/Config.py
118
Shared/Config.py
|
@ -1,118 +0,0 @@
|
|||
from imp import load_source
|
||||
from os.path import isfile
|
||||
from marshal import dumps, loads
|
||||
from types import FunctionType
|
||||
|
||||
|
||||
class OptionDuplicateError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' already exists" % name)
|
||||
|
||||
|
||||
class OptionNotFoundError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' does not exist" % name)
|
||||
|
||||
|
||||
class NameMustBeStringError(Exception):
|
||||
def __init__(self):
|
||||
super(Exception, self).__init__("option names have to be strings")
|
||||
|
||||
|
||||
def make_value(value):
|
||||
if type(value) is str:
|
||||
value = '"%s"' % value
|
||||
elif type(value) in (list, tuple, dict):
|
||||
value = str(value)
|
||||
elif type(value) is FunctionType:
|
||||
value = dumps(value.__code__)
|
||||
return value
|
||||
|
||||
|
||||
class Option:
|
||||
def __init__(self, name, default_value, validator=None, comment=""):
|
||||
if not type(name) is str:
|
||||
raise NameMustBeStringError()
|
||||
self.name = name
|
||||
self.default_value = default_value
|
||||
self.validator = validator
|
||||
self.comment = comment
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, options=[], validation_failed=None, override_on_error=False):
|
||||
if type(options) in (list, tuple):
|
||||
for option in options:
|
||||
if not type(option) is Option:
|
||||
raise TypeError("all options must be of type Option")
|
||||
else:
|
||||
raise TypeError("options must be a list or tuple containing options of type Option")
|
||||
self.options = options
|
||||
self.validation_failed = validation_failed
|
||||
self.override_on_error = override_on_error
|
||||
|
||||
|
||||
def read_from_file(self, file):
|
||||
if isfile(file):
|
||||
config = load_source("config", file)
|
||||
error = False
|
||||
for option in self.options:
|
||||
# Make sure all options are avaliable
|
||||
if option.name not in dir(config):
|
||||
setattr(config, option.name, option.default_value)
|
||||
error = True
|
||||
else:
|
||||
# Make sure all validators pass
|
||||
if option.validator != None:
|
||||
value = getattr(config, option.name)
|
||||
if not option.validator(value):
|
||||
setattr(config, option.name, option.default_value)
|
||||
if self.validation_failed != None:
|
||||
self.validation_failed(option.name, value)
|
||||
error = True
|
||||
if self.override_on_error:
|
||||
if error:
|
||||
self.write_to_file(file)
|
||||
return config
|
||||
else:
|
||||
raise FileNotFoundError()
|
||||
|
||||
|
||||
def add(self, new_option):
|
||||
if type(new_option) is Option:
|
||||
for option in self.options:
|
||||
if new_option.name == option.name:
|
||||
raise OptionDuplicateError(option.name)
|
||||
self.options.append(new_option)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def remove(self, option):
|
||||
if option in self.options:
|
||||
del self.options[self.options.index(option)]
|
||||
else:
|
||||
raise OptionNotFoundError(option.name)
|
||||
|
||||
|
||||
def write_to_file(self, file):
|
||||
open(file, "w").write(self.get())
|
||||
|
||||
|
||||
def get(self):
|
||||
contains_function = False
|
||||
out = ""
|
||||
for option in self.options:
|
||||
value = make_value(option.default_value)
|
||||
if type(option.default_value) is FunctionType:
|
||||
if not contains_function:
|
||||
out = "from marshal import loads; from types import FunctionType\n\n" + out
|
||||
contains_function = True
|
||||
value = 'FunctionType(loads(%s), globals(), "%s")' % (value, option.name)
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.get()
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
|
@ -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
328
Shared/Meh.py
Normal file
|
@ -0,0 +1,328 @@
|
|||
from imp import load_source
|
||||
from os.path import isfile
|
||||
from sys import version
|
||||
|
||||
class OptionDuplicateError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' already exists" % name)
|
||||
|
||||
|
||||
class OptionNotFoundError(IndexError):
|
||||
def __init__(self, name):
|
||||
super(IndexError, self).__init__("'%s' does not exist" % name)
|
||||
|
||||
|
||||
class NameMustBeStringError(Exception):
|
||||
def __init__(self):
|
||||
super(Exception, self).__init__("option names have to be strings")
|
||||
|
||||
|
||||
class ValidationError(Exception):
|
||||
def __init__(self, option):
|
||||
super(Exception, self).__init__("invalid value for option '%s'" % option)
|
||||
|
||||
|
||||
class UnsupportedTypeError(TypeError):
|
||||
def __init__(self):
|
||||
super(TypeError, self).__init__("only list, tuple, dict, bytes, "
|
||||
"str, float, complex, int and bool are supported (same "
|
||||
"thing applies to list, dict and tuple contents)")
|
||||
|
||||
|
||||
class ExceptionInConfigError(Exception):
|
||||
"""
|
||||
Raised if an exception occurs while importing a config file.
|
||||
|
||||
IN: error (hint: error that occured on import)
|
||||
"""
|
||||
def __init__(self, error):
|
||||
self.error = error
|
||||
super(Exception, self).__init__("error occured during config import (%s)" %
|
||||
error.__class__.__name__)
|
||||
|
||||
def validate_value(value):
|
||||
type_value = type(value)
|
||||
if type_value in (list, tuple):
|
||||
for element in value:
|
||||
if not validate_value(element):
|
||||
return False
|
||||
elif type_value is dict:
|
||||
return validate_value(tuple(value.keys())) and validate_value(tuple(value.values()))
|
||||
elif type_value in (bytes, str, float, complex, int, bool):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def make_value(value):
|
||||
if validate_value(value):
|
||||
if type(value) is str:
|
||||
value = '"%s"' % value
|
||||
elif type(value) in (list, tuple, dict):
|
||||
value = str(value)
|
||||
return value
|
||||
else:
|
||||
raise UnsupportedTypeError()
|
||||
|
||||
|
||||
class _EditableConfig:
|
||||
"""
|
||||
Automatically created proxy class.
|
||||
|
||||
HINTS:
|
||||
_values: All options with their respective values
|
||||
_options: All Option instances that were originally added to
|
||||
the Config instance
|
||||
_file: Path to the config file
|
||||
_validation_failed: Optional function that's called on a validation error
|
||||
_debug: Debug mode on/off (obviously)
|
||||
"""
|
||||
def __init__(self, values, options, file, validation_failed=None, debug=False):
|
||||
self._values = values
|
||||
self._options = options
|
||||
self._file = file
|
||||
self._validation_failed = validation_failed
|
||||
self._debug = debug
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self._values:
|
||||
return self._values[name]
|
||||
else:
|
||||
raise AttributeError("config no attribute '%s'" % name)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name == "_values" or name not in self._values:
|
||||
self.__dict__[name] = value
|
||||
else:
|
||||
dump_required = False
|
||||
for option in self._options:
|
||||
if option.name == name:
|
||||
if validate_value(value):
|
||||
if option.validator != None:
|
||||
if option.validator(value):
|
||||
self._values[name] = value
|
||||
dump_required = True
|
||||
else:
|
||||
if self._validation_failed != None:
|
||||
self._validation_failed(option.name, value)
|
||||
else:
|
||||
raise ValidationError(option.name)
|
||||
else:
|
||||
self._values[name] = value
|
||||
dump_required = True
|
||||
else:
|
||||
raise UnsupportedTypeError()
|
||||
if dump_required:
|
||||
if self._debug:
|
||||
print("Rewriting config because the value of '%s' changed." % name)
|
||||
open(self._file, "w").write(self._dumps())
|
||||
|
||||
|
||||
def _dumps(self):
|
||||
out = ""
|
||||
for option in self._options:
|
||||
value = make_value(self._values[option.name])
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out.rstrip("\n")
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self._dumps()
|
||||
|
||||
|
||||
class Option:
|
||||
|
||||
def __init__(self, name, default_value, validator=None, comment=""):
|
||||
if not type(name) is str:
|
||||
raise NameMustBeStringError()
|
||||
if name.startswith("__"):
|
||||
raise InvalidOptionName()
|
||||
self._name = name
|
||||
self.default_value = default_value
|
||||
self.validator = validator
|
||||
self.comment = comment
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
if name.startswith("__"):
|
||||
raise InvalidOptionName()
|
||||
self._name = value
|
||||
|
||||
|
||||
def __eq__(self, other):
|
||||
if other.__class__ == Option:
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s = %s" % (self.name, str(self.default_value))
|
||||
|
||||
|
||||
class Config:
|
||||
"""
|
||||
The central element of Meh (TM).
|
||||
IN:
|
||||
options=[] (type: list, hint: a list of options)
|
||||
validation_failed=None (type: function, hint: function accepting two
|
||||
parameters that's called when a validation fails)
|
||||
|
||||
Example usage:
|
||||
from Meh import Config, Option
|
||||
config = Config()
|
||||
config.add(Option("number", 42, validator=lambda x: type(x) is int))
|
||||
|
||||
CONFIG_PATH = "awesome_config.cfg"
|
||||
try:
|
||||
config = config.load(CONFIG_PATH)
|
||||
except IOError:
|
||||
config.dump(CONFIG_PATH)
|
||||
config = config.load(CONFIG_PATH)
|
||||
|
||||
print(config.number)
|
||||
"""
|
||||
def __init__(self, options=[], validation_failed=None, debug=False):
|
||||
if type(options) in (list, tuple):
|
||||
for option in options:
|
||||
if not option.__class__ == Option:
|
||||
raise TypeError("all options must be of type Option")
|
||||
else:
|
||||
raise TypeError("options must be a list or tuple containing options of type Option")
|
||||
self.options = options
|
||||
self.validation_failed = validation_failed
|
||||
self.debug = debug
|
||||
self._iterator_index = 0
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
|
||||
def __next__(self):
|
||||
if self._iterator_index < len(self.options):
|
||||
self._iterator_index += 1
|
||||
return self.options[self._iterator_index - 1]
|
||||
self._iterator_index = 0
|
||||
raise StopIteration
|
||||
|
||||
|
||||
def load(self, file):
|
||||
"""
|
||||
Returns the actual read- and editable config
|
||||
IN: file (type: str, hint: should be a valid path)
|
||||
"""
|
||||
if isfile(file):
|
||||
try:
|
||||
config = load_source("config", file)
|
||||
except Exception as e:
|
||||
raise ExceptionInConfigError(e)
|
||||
option_missing = False
|
||||
values = {}
|
||||
for option in self.options:
|
||||
# Make sure all options are avaliable (validators aren't run in this case
|
||||
# because there are no values defined)
|
||||
if option.name not in dir(config):
|
||||
values[option.name] = option.default_value
|
||||
option_missing = True
|
||||
else:
|
||||
# Retrieve the option value
|
||||
value = getattr(config, option.name)
|
||||
# Make sure validator passes
|
||||
if option.validator != None:
|
||||
# If validation doesn't pass
|
||||
if not option.validator(value):
|
||||
# Resort to default value
|
||||
values[option.name] = option.default_value
|
||||
if self.validation_failed != None:
|
||||
self.validation_failed(option.name, value)
|
||||
else:
|
||||
raise ValidationError(option.name)
|
||||
option_missing = True
|
||||
# If validation passes
|
||||
else:
|
||||
values[option.name] = value
|
||||
else:
|
||||
values[option.name] = value
|
||||
if option_missing:
|
||||
self.dump(file)
|
||||
return _EditableConfig(values, self.options, file,
|
||||
validation_failed=self.validation_failed, debug=self.debug)
|
||||
else:
|
||||
error = "'%s' not found" % file
|
||||
raise FileNotFoundError(error) if version.startswith("3") else IOError(error)
|
||||
|
||||
|
||||
def __add__(self, other):
|
||||
try:
|
||||
self.add(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return self
|
||||
|
||||
|
||||
def __sub__(self, other):
|
||||
try:
|
||||
self.remove(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return self
|
||||
|
||||
|
||||
def add(self, option):
|
||||
"""
|
||||
Adds an option to a Config instance
|
||||
IN: option (type: Option)
|
||||
"""
|
||||
if option.__class__ == Option:
|
||||
for _option in self.options:
|
||||
if option.name == _option.name:
|
||||
raise OptionDuplicateError(_option.name)
|
||||
self.options.append(option)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def remove(self, option):
|
||||
"""
|
||||
Removes an option from a Config instance
|
||||
IN: option (type: Option)
|
||||
"""
|
||||
if option.__class__ == Option:
|
||||
if option in self.options:
|
||||
del self.options[self.options.index(option)]
|
||||
else:
|
||||
raise OptionNotFoundError(option.name)
|
||||
else:
|
||||
raise TypeError("invalid type supplied")
|
||||
|
||||
|
||||
def dump(self, file):
|
||||
"""
|
||||
Writes output of dumps() to the path provided
|
||||
IN: file (type: str, hint: should be a valid path)
|
||||
"""
|
||||
open(file, "w").write(self.dumps())
|
||||
|
||||
|
||||
def dumps(self):
|
||||
"""
|
||||
Returns contents of config file as string
|
||||
OUT: out (type: str, hint: config content)
|
||||
"""
|
||||
out = ""
|
||||
for option in self.options:
|
||||
value = make_value(option.default_value)
|
||||
out += "%s = %s%s\n" % (option.name, value,
|
||||
(" # %s" % option.comment) if option.comment else "")
|
||||
return out.rstrip("\n")
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return self.dumps()
|
|
@ -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
1
Shared/bottle
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 5a6fc77c9c57501b08e5b4f9161ae07d277effa9
|
1
Shared/dashb0ard
Submodule
1
Shared/dashb0ard
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit cca517355923990423a2c744bf2bc81080090ff3
|
|
@ -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
|
|
@ -1,14 +0,0 @@
|
|||
[
|
||||
{
|
||||
"keys" : ["f8"],
|
||||
"command" : "run"
|
||||
},
|
||||
{
|
||||
"keys" : ["f9"],
|
||||
"command" : "stop"
|
||||
},
|
||||
{
|
||||
"keys" : ["f10"],
|
||||
"command" : "sensor"
|
||||
}
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
[
|
||||
{
|
||||
"keys" : ["f8"],
|
||||
"command" : "run"
|
||||
},
|
||||
{
|
||||
"keys" : ["f9"],
|
||||
"command" : "stop"
|
||||
},
|
||||
{
|
||||
"keys" : ["f10"],
|
||||
"command" : "sensor"
|
||||
}
|
||||
]
|
|
@ -1,14 +0,0 @@
|
|||
[
|
||||
{
|
||||
"keys" : ["f8"],
|
||||
"command" : "run"
|
||||
},
|
||||
{
|
||||
"keys" : ["f9"],
|
||||
"command" : "stop"
|
||||
},
|
||||
{
|
||||
"keys" : ["f10"],
|
||||
"command" : "sensor"
|
||||
}
|
||||
]
|
|
@ -1 +0,0 @@
|
|||
../../Shared
|
|
@ -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 = {}
|
|
@ -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)]
|
|
@ -1,6 +0,0 @@
|
|||
[
|
||||
{
|
||||
"caption": "fl0w: Menu",
|
||||
"command": "fl0w"
|
||||
}
|
||||
]
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"server_address": "", // Last server address
|
||||
"compression_level": 2
|
||||
}
|
|
@ -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)
BIN
Wallaby/identify.wav
(Stored with Git LFS)
Binary file not shown.
5
fl0w.py
5
fl0w.py
|
@ -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'))
|
||||
|
|
Reference in a new issue