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 = []