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