From 278caced99cf27a13f9422247a586922b0288f95 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Mon, 23 Jan 2017 09:36:47 +0100 Subject: [PATCH] Target now an object, locking for sensor readouts, re-organized controller menu, ... Added shutdown and reboot actions, added keyboard shortcut for stop_programs, ... Implemented buffered std_stream output for multiple controllers, sensor readout toggle now toggle view instead of window, ... Sensor phantoms now deactivated themselves when they aren't in focus --- Sublime/fl0w/fl0w.py | 273 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 217 insertions(+), 56 deletions(-) diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index ef24868..21aecab 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -22,6 +22,7 @@ from SublimeMenu import * import Logging import webbrowser +import threading from time import sleep import os @@ -44,12 +45,19 @@ 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) @@ -67,13 +75,15 @@ class Fl0wClient(Client): def closed(self, code, reason): - self.fl0w.connected = False + 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): @@ -87,8 +97,10 @@ class Fl0wClient(Client): 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): @@ -97,38 +109,61 @@ class Fl0wClient(Client): 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(lambda handler, id_: self.set_target(handler, id_), - handler, id_)) - action_menu += Entry("Programs", - "Lists all executable programs on the controller.", + "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(lambda handler, id_: handler.pipe(None, "list_programs", id_), + handler, id_)) + action_menu += Entry("Stop programs", + "Stop all currently running botball programs", + action=partial(lambda handler, id_: handler.pipe(None, "stop_programs", id_), handler, id_)) - action_menu += Entry("Set Name", + 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"], + 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.", + {"set" : hostname}, + "hostname", id_)).invoke(handler.fl0w.window), handler, id_)) + utilities_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", + utilities_menu += Entry("Identify", "Plays an identification sound on the controller.", action=partial(lambda handler, id_: handler.pipe(None, "identify", id_), handler, 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(lambda handler, id_: handler.pipe(None, "shutdown", id_), + handler, id_)) + power_menu += Entry("Reboot", + "Reboot the controller", + action=partial(lambda handler, id_: handler.pipe(None, "reboot", id_), + handler, 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): - handler.fl0w.target = peer + 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) @@ -152,18 +187,99 @@ class Fl0wClient(Client): program_menu = Menu(subtitles=False) for program in data: program_menu += Entry(program, - action=lambda handler: handler.pipe(program, - "run_program", - handler.routes["peers"].selected_action_menu.id_), - kwargs={"handler" : handler}) + 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) - class StdStream(Pipe): - def run(data, peer, handler): - pass + def run_program(self, handler, id_, program): + handler.pipe(program, "run_program", id_) + class StdStream(Pipe): + def start(self, handler): + self.output_panels = {} + + self.lock = threading.Lock() + 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() + """ + elif type(data) is dict: + # Bad solution, should instead be treated seperately + # while still being timed correctly. + meta_text = "" + if "exit_code" in data: + meta_text += "Program finished with exit code: %d\n" % data["exit_code"] + self.lock.acquire() + self.buffer.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): @@ -175,6 +291,7 @@ class Fl0w: self.connected = False self.subscriptions = {} + self.subscriptions_lock = threading.Lock() self._combined_subscriptions = {"analog" : [], "digital" : []} self._target = None @@ -234,11 +351,14 @@ class Fl0w: @target.setter def target(self, target): if self.target != None: - self.ws.pipe("unsubscribe", "sensor", self.target) + self.ws.pipe("unsubscribe", "sensor", self.target.id) self._target = target - set_status("Set target: %s" % target, self.window) - if self.combined_subscriptions != {"analog" : [], "digital" : []}: - self.ws.pipe({"subscribe" : self.combined_subscriptions_}, "sensor", 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 @@ -262,6 +382,9 @@ class Fl0w: 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. @property def combined_subscriptions(self): return self._combined_subscriptions @@ -272,20 +395,25 @@ class Fl0w: 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) + 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: + self.subscriptions_lock.acquire() del self.subscriptions[sensor_phantom] + self.subscriptions_lock.release() self.make_subscriptions() @@ -306,7 +434,12 @@ class Fl0w: if os.path.isfile(self.folder + relpath): if self.debug: Logging.info("Running program '%s'" % relpath) - self.ws.pipe(relpath, "run_program", self.target) + 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): @@ -328,7 +461,8 @@ 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(), "sensor" : Fl0wClient.Sensor()}, + "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor(), + "std_stream" : Fl0wClient.StdStream()}, self, debug=True) self.ws.connect() sublime.set_timeout_async(self.ws.run_forever, 0) @@ -349,6 +483,7 @@ class Fl0w: 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 @@ -407,47 +542,61 @@ class RunCommand(sublime_plugin.WindowCommand): 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 __init__(self, window): - super().__init__(window) - self.enabled = False - - 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) + 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: - sublime.error_message("fl0w is not connected.") + 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 running in your current window.") + sublime.error_message("fl0w is not connected.") 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) + 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 @@ -467,16 +616,17 @@ class SensorPhantom(sublime_plugin.ViewEventListener): @enabled.setter def enabled(self, enabled_): - self._enabled = 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.window.active_view().erase_phantoms(sensor_type) + self.view.erase_phantoms(sensor_type) + self._enabled = False @property @@ -547,6 +697,17 @@ class SensorPhantom(sublime_plugin.ViewEventListener): 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: