diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index 228272b..1044681 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -1,8 +1,8 @@ from sys import path import os -import _thread 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/" @@ -12,18 +12,19 @@ if shared_path not in path: path.append(shared_path) -from watchdog.observers import Observer -from watchdog.events import FileSystemEventHandler - import sublime import sublime_plugin from Highway import Client, Route, Pipe, DummyPipe +from Utils import get_hostname from SublimeMenu import * import Logging import webbrowser +from time import sleep +from _thread import start_new_thread +import os CHANNEL = 1 FL0W_STATUS = "fl0w" @@ -32,8 +33,21 @@ 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 = "
"
+
+
+windows = []
+sensor_phantoms = []
+
+def set_status(status, window):
+ window.active_view().set_status(FL0W_STATUS,
+ "fl0w: %s" % status)
class Fl0wClient(Client):
def setup(self, routes, fl0w, debug=False):
@@ -45,6 +59,9 @@ class Fl0wClient(Client):
self.fl0w.connected = True
if self.fl0w.debug:
Logging.info("Connection ready!")
+ # Enlist on editor channel
+ self.send({"channel" : 1, "name" : get_hostname()}, "subscribe")
+ # Subscribe to controller channel
self.send({"subscribe" : [2]}, "peers")
@@ -67,6 +84,11 @@ class Fl0wClient(Client):
handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu)
+ class Sensor(Pipe):
+ def run(self, data, peer, handler):
+ handler.fl0w.sensor_readouts = data
+
+
class Peers(Route):
def start(self, handler):
self.selected_action_menu = None
@@ -76,10 +98,14 @@ class Fl0wClient(Client):
for id_ in data:
action_menu = Menu()
action_menu.id_ = id_
+ action_menu += Entry("Set Target",
+ "Set controller as target for program execution and sensor readouts.",
+ action=partial(lambda handler, id_: self.set_target(handler, id_),
+ handler, id_))
action_menu += Entry("Programs",
"Lists all executable programs on the controller.",
action=partial(lambda handler, id_: handler.pipe(None, "list_programs", id_),
- handler, id_))
+ handler, id_))
action_menu += Entry("Set Name",
"Sets the hostname of the selected controller",
action=partial(lambda handler, id_: Input("New Hostname:", initial_text=data[id_]["name"],
@@ -95,6 +121,10 @@ class Fl0wClient(Client):
kwargs={"selected_action_menu" : action_menu})
+ def set_target(self, handler, peer):
+ handler.fl0w.target = peer
+ set_status("Target: %s" % peer, handler.fl0w.window)
+
def set_selected_action_menu(self, selected_action_menu):
self.selected_action_menu = selected_action_menu
@@ -122,13 +152,23 @@ class Fl0wClient(Client):
program_menu.invoke(handler.fl0w.window,
back=handler.routes["peers"].selected_action_menu)
+ class StdStream(Pipe):
+ def run(data, peer, handler):
class Fl0w:
def __init__(self, window, debug=False):
- self.connected = False
self.window = window
self.folder = window.folders()[0]
+ if self.folder != "/":
+ self.folder = self.folder + "/"
+
+ self.connected = False
+
+ self.required_readouts = {"analog" : [], "digital" : []}
+ self.sensor_readouts = {"analog" : {}, "digital" : {}}
+
+ self._target = None
self._debug = debug
@@ -144,28 +184,93 @@ class Fl0w:
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:
+ 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.",
+ 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)
-
+
+ self.new_view_opened()
+
+
+ def new_view_opened(self):
+ for phantom in sensor_phantoms:
+ if phantom.window.id() is self.window.id() and phantom.fl0w != self:
+ phantom.fl0w = self
+ if self.debug:
+ Logging.info("Patched phantom '%s'" % str(phantom))
+
+
+ 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):
+ self.ws.pipe(relpath, "run_program", self.target)
+
+
+ def refresh_subscriptions(self):
+ required_readouts = {}
+ subscribe_count = 0
+ subscribe = {}
+ # Iterate over all phantoms
+ for phantom in sensor_phantoms:
+ # One fl0w instance per window
+ if phantom.window.id() is self.window.id() and phantom.fl0w == self:
+ for sensor_type in ("analog", "digital"):
+ required_readouts[sensor_type] = []
+ subscribe[sensor_type] = []
+ for port in phantom.required_readouts[sensor_type]:
+ if port not in required_readouts[sensor_type]:
+ required_readouts[sensor_type].append(port)
+ if port not in self.required_readouts[sensor_type]:
+ subscribe[sensor_type].append(port)
+ subscribe_count += 1
+ unsubscribe_count = 0
+ unsubscribe = {}
+ for sensor_type in ("analog", "digital"):
+ unsubscribe[sensor_type] = []
+ for port in self.required_readouts[sensor_type]:
+ if port not in required_readouts[sensor_type]:
+ unsubscribe[sensor_type].append(port)
+ unsubscribe_count += 1
+ self.required_readouts = required_readouts
+ if unsubscribe_count != 0:
+ self.ws.pipe({"unsubscribe" : unsubscribe}, "sensor", self.target)
+ if subscribe_count != 0:
+ self.ws.pipe({"subscribe" : subscribe}, "sensor", self.target)
+
+
+ @property
+ def target(self):
+ return self._target
+
+
+ @target.setter
+ def target(self, target):
+ if self.target != None:
+ self.ws.pipe("unsubscribe", "sensor", self.target)
+ self._target = target
+ if self.required_readouts != {"analog" : [], "digital" : []}:
+ self.ws.pipe({"subscribe" : self.required_readouts}, "sensor", target)
+
@property
def debug(self):
@@ -185,8 +290,7 @@ class Fl0w:
else:
self._debug = False
self.main_menu -= self.meta_entry
- self.window.active_view().set_status(FL0W_STATUS,
- "Debug set to %s" % self._debug)
+ set_status("Debug set to %s" % self._debug, self.window)
def invoke_start_menu(self):
@@ -208,12 +312,12 @@ class Fl0w:
self.ws = Fl0wClient('ws://%s' % connect_details)
self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(),
"processes" : Fl0wClient.Processes(),
- "list_programs" : Fl0wClient.ListPrograms()},
+ "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor()},
self, debug=True)
self.ws.connect()
sublime.set_timeout_async(self.ws.run_forever, 0)
- self.window.active_view().set_status(FL0W_STATUS,
- "Connection opened '%s'" % self.folder)
+ set_status("Connection opened '%s'" % self.folder, self.window)
+ self.connected = True
except OSError as e:
sublime.error_message("Error during connection creation:\n %s" % str(e))
@@ -227,11 +331,8 @@ class Fl0w:
def invoke_disconnect(self):
if self.connected:
self.ws.close()
- self.window.active_view().set_status(FL0W_STATUS,
- "Connection closed '%s'" % self.folder)
-
-
-
+ set_status("Connection closed '%s'" % self.folder, self.window)
+ self.connected = False
class Fl0wCommand(sublime_plugin.WindowCommand):
@@ -271,5 +372,150 @@ class Fl0wCommand(sublime_plugin.WindowCommand):
self.window.fl0w.invoke_disconnect()
+class RunCommand(sublime_plugin.WindowCommand):
+ def run(self):
+ if hasattr(self.window, "fl0w"):
+ file_name = self.window.active_view().file_name()
+ if file_name != None and file_name.endswith(".c"):
+ self.window.fl0w.run_program(file_name)
+
+
+class SensorCommand(sublime_plugin.WindowCommand):
+ def __init__(self, window):
+ super().__init__(window)
+ self.enabled = False
+
+ def run(self):
+ self.enabled = not self.enabled
+ for sensor_phantom in sensor_phantoms:
+ sensor_phantom.enabled = self.enabled
+ set_status("%s sensor phantoms." % ("Enabled" if self.enabled else "Disabled"),
+ self.window)
+
+
+
+class SensorPhantom(sublime_plugin.ViewEventListener):
+ def __init__(self, view):
+ self.view = view
+ self.window = view.window()
+
+ self.fl0w = None
+ self._enabled = False
+
+ self.matches = {"analog" : [], "digital" : []}
+ self.new_matches = False
+
+ self.required_readouts = {"analog" : [], "digital" : []}
+
+ self.timeout_scheduled = False
+ self.needs_update = False
+
+ for window in windows:
+ if hasattr(window, "fl0w"):
+ window.fl0w.new_view_opened()
+ if not self in sensor_phantoms:
+ sensor_phantoms.append(self)
+
+ self.find_matches()
+
+
+ @property
+ def enabled(self):
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, enabled_):
+ self._enabled = enabled_
+ if enabled_:
+ if self.fl0w != None:
+ self.handle_subscriptions()
+ # Proper way doesn't seem to work after reloading
+ # sublime.set_timeout_async(self.phantom_updater, 0)
+ start_new_thread(self.phantom_updater, ())
+ else:
+ self.required_readouts = {"analog" : [], "digital" : []}
+ if self.fl0w != None:
+ self.fl0w.refresh_subscriptions()
+ for sensor_type in ("analog", "digital"):
+ self.window.active_view().erase_phantoms(sensor_type)
+
+
+
+ def phantom_updater(self):
+ while self.enabled:
+ if self.fl0w.connected and self.fl0w.target != None:
+ self.update_sensor_values()
+ sleep(0.2)
+
+
+ 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)))
+ if matches != self.matches:
+ self.matches = matches
+ self.new_matches = True
+
+
+ def handle_subscriptions(self):
+ ports = {}
+ for sensor_type in ("analog", "digital"):
+ # If new matches are found subscribe to them
+ for match in self.matches[sensor_type]:
+ if match[0] not in self.required_readouts[sensor_type]:
+ self.required_readouts[sensor_type].append(match[0])
+ # If a subscription is not needed anymore
+ # Fetch all required ports
+ ports[sensor_type] = [match[0] for match in self.matches[sensor_type]]
+ for required_readout in self.required_readouts[sensor_type]:
+ if required_readout not in ports[sensor_type]:
+ del self.required_readouts[sensor_type][self.required_readouts[sensor_type].index(required_readout)]
+ if self.fl0w.target != None:
+ self.fl0w.refresh_subscriptions()
+
+
+ def update_sensor_values(self):
+ if self.fl0w.target != None:
+ 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(self.fl0w.sensor_readouts[sensor_type][str(match[0])]) + STYLE_CLOSE,
+ sublime.LAYOUT_INLINE)
+ except KeyError:
+ Logging.warning("Unable to retrieve for %s:%i." % (sensor_type, match[0]))
+
+
+ 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 and self.fl0w != None:
+ if self.timeout_scheduled:
+ self.needs_update = True
+ else:
+ sublime.set_timeout(lambda: self.handle_timeout(), 500)
+ self.find_matches()
+ if self.new_matches:
+ self.handle_subscriptions()
+ self.new_matches = False
+
+ def __del__(self):
+ self.enabled = False
+
+
+
-windows = []