diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index 142aa9a..ef24868 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -23,7 +23,6 @@ import Logging import webbrowser from time import sleep -from _thread import start_new_thread import os CHANNEL = 1 @@ -41,12 +40,14 @@ PARENTHESES_REGEX = re.compile("\((.*?)\)") STYLE_OPEN = "" STYLE_CLOSE = "" +ERROR_OPEN = "" +ERROR_CLOSE = "" windows = [] sensor_phantoms = [] def set_status(status, window): - window.active_view().set_status(FL0W_STATUS, + window.active_view().set_status(FL0W_STATUS, "fl0w: %s" % status) class Fl0wClient(Client): @@ -86,7 +87,8 @@ class Fl0wClient(Client): class Sensor(Pipe): def run(self, data, peer, handler): - handler.fl0w.sensor_readouts = data + for sensor_phantom in handler.fl0w.subscriptions: + sensor_phantom.update_sensor_values(data) class Peers(Route): @@ -98,22 +100,26 @@ 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_)) - action_menu += Entry("Set Name", + 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_)) + 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"], + 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_)) - action_menu += Entry("Processes", - "Lists processes currently running on controller.", - action=partial(lambda handler: handler.pipe(None, "processes", id_), + action_menu += Entry("Processes", + "Lists processes currently running on controller.", + action=partial(lambda handler, id_: handler.pipe(None, "processes", id_), + handler, id_)) + action_menu += Entry("Identify", + "Plays an identification sound on the controller.", + action=partial(lambda handler, id_: handler.pipe(None, "identify", id_), handler, id_)) action_menu.back = handler.fl0w.controller_menu handler.fl0w.controller_menu += Entry(data[id_]["name"], id_, sub_menu=action_menu, @@ -123,7 +129,8 @@ class Fl0wClient(Client): def set_target(self, handler, peer): handler.fl0w.target = peer - set_status("Target: %s" % peer, handler.fl0w.window) + if handler.fl0w.debug: + set_status("Target: %s" % peer, handler.fl0w.window) def set_selected_action_menu(self, selected_action_menu): @@ -144,18 +151,20 @@ class Fl0wClient(Client): def run(self, data, peer, handler): program_menu = Menu(subtitles=False) for program in data: - program_menu += Entry(program, - action=lambda handler: handler.pipe(program, - "run_program", + program_menu += Entry(program, + action=lambda handler: handler.pipe(program, + "run_program", handler.routes["peers"].selected_action_menu.id_), kwargs={"handler" : handler}) - program_menu.invoke(handler.fl0w.window, + program_menu.invoke(handler.fl0w.window, back=handler.routes["peers"].selected_action_menu) class StdStream(Pipe): def run(data, peer, handler): pass + + class Fl0w: def __init__(self, window, debug=False): self.window = window @@ -164,98 +173,57 @@ class Fl0w: self.folder = self.folder + "/" self.connected = False - - self.required_readouts = {"analog" : [], "digital" : []} - self.sensor_readouts = {"analog" : {}, "digital" : {}} - + + self.subscriptions = {} + 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", + self.start_menu += Entry("Connect", "Connect to a fl0w server", action=self.invoke_connect) - self.start_menu += Entry("About", "Information about fl0w", + self.start_menu += Entry("About", "Information about fl0w", action=self.invoke_about) self.debug_menu = Menu(subtitles=False) - self.debug_menu += Entry("On", + self.debug_menu += Entry("On", action=lambda: self.set_debug(True)) - self.debug_menu += Entry("Off", + self.debug_menu += Entry("Off", action=lambda: self.set_debug(False)) self.settings = Menu() - self.settings += Entry("Debug", "Toggle debug mode", + self.settings += Entry("Debug", "Toggle debug mode", sub_menu=self.debug_menu) self.meta = Menu() - self.meta += Entry("Info", "Server info", + self.meta += Entry("Info", "Server info", action=lambda: self.ws.send(None, "info")) - self.meta_entry = Entry("Meta", "Debug information about fl0w", + 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", + self.main_menu += Entry("Settings", "General purpose settings", sub_menu=self.settings) - self.main_menu += Entry("Disconnect", "Disconnect from server", + 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 + # 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 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) + Logging.info("Patched sensor phantom '%s'" % str(sensor_phatom)) @property @@ -268,8 +236,9 @@ class Fl0w: 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) + set_status("Set target: %s" % target, self.window) + if self.combined_subscriptions != {"analog" : [], "digital" : []}: + self.ws.pipe({"subscribe" : self.combined_subscriptions_}, "sensor", target) @property @@ -293,6 +262,53 @@ class Fl0w: set_status("Debug set to %s" % self._debug, self.window) + @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) + self.ws.pipe({"subscribe" : combined_subscriptions_}, "sensor", + self.target) + + + def subscribe(self, sensor_phatom, subscriptions): + self.subscriptions[sensor_phatom] = subscriptions + self.make_subscriptions() + + + + def unsubscribe(self, sensor_phantom): + if sensor_phantom in self.subscriptions: + del self.subscriptions[sensor_phantom] + 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, "run_program", self.target) + + def invoke_start_menu(self): self.start_menu.invoke(self.window) @@ -311,8 +327,8 @@ class Fl0w: try: self.ws = Fl0wClient('ws://%s' % connect_details) self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(), - "processes" : Fl0wClient.Processes(), - "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor()}, + "processes" : Fl0wClient.Processes(), + "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor()}, self, debug=True) self.ws.connect() sublime.set_timeout_async(self.ws.run_forever, 0) @@ -330,6 +346,9 @@ class Fl0w: 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.ws.close() set_status("Connection closed '%s'" % self.folder, self.window) self.connected = False @@ -375,9 +394,18 @@ class Fl0wCommand(sublime_plugin.WindowCommand): 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) + 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 SensorCommand(sublime_plugin.WindowCommand): @@ -385,67 +413,89 @@ class SensorCommand(sublime_plugin.WindowCommand): 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) + def run(self): + if not self.enabled: + 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: + self.enabled = not self.enabled + for sensor_phantom in sensor_phantoms: + if sensor_phantom.window.id() == self.window.id(): + sensor_phantom.enabled = self.enabled + set_status("Enabled sensor phantoms.", self.window) + else: + sublime.error_message("fl0w is not connected.") + else: + sublime.error_message("fl0w is not running in your current window.") + else: + self.enabled = not self.enabled + for sensor_phantom in sensor_phantoms: + if sensor_phantom.window.id() == self.window.id(): + sensor_phantom.enabled = self.enabled + set_status("Disabled sensor phantoms.", self.window) class SensorPhantom(sublime_plugin.ViewEventListener): def __init__(self, view): self.view = 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.matches = {"analog" : [], "digital" : []} - self.new_matches = False - - self.required_readouts = {"analog" : [], "digital" : []} + self._matches = {"analog" : [], "digital" : []} self.timeout_scheduled = False self.needs_update = False for window in windows: if hasattr(window, "fl0w"): - window.fl0w.new_view_opened() + self.fl0w = window.fl0w 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, ()) + self.find_matches() + self.fl0w.subscribe(self, self.subscriptions) else: - self.required_readouts = {"analog" : [], "digital" : []} if self.fl0w != None: - self.fl0w.refresh_subscriptions() + self.fl0w.unsubscribe(self) for sensor_type in ("analog", "digital"): self.window.active_view().erase_phantoms(sensor_type) + @property + def matches(self): + return self._matches - def phantom_updater(self): - while self.enabled: - if self.fl0w.connected and self.fl0w.target != None: - self.update_sensor_values() - sleep(0.2) + @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): @@ -460,40 +510,25 @@ class SensorPhantom(sublime_plugin.ViewEventListener): 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 + ( + int(port_candidates[0]), + sublime.Region(self.view.line(candidate.a).b) + )) + self.matches = matches - - def handle_subscriptions(self): - ports = {} + # Called by fl0w instance + def update_sensor_values(self, readouts): for sensor_type in ("analog", "digital"): - # If new matches are found subscribe to them + self.view.erase_phantoms(sensor_type) 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])) + 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): @@ -502,20 +537,17 @@ class SensorPhantom(sublime_plugin.ViewEventListener): self.needs_update = False self.find_matches() + def on_modified(self): - if self.enabled and self.fl0w != None: + if self.enabled: 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): + + def __del__(self): self.enabled = False - - - - + if self in sensor_phantoms: + del sensor_phantoms[sensor_phantoms.index(self)]