Initial inline sensor readout support, initial program execution
Inline sensor readouts work, but the code is ugly and it wastes resources. Initial program execution with keybinding is also included.
This commit is contained in:
parent
da4fc67b8b
commit
8bf83b655a
1 changed files with 266 additions and 20 deletions
|
@ -1,8 +1,8 @@
|
||||||
from sys import path
|
from sys import path
|
||||||
import os
|
import os
|
||||||
import _thread
|
|
||||||
from time import strftime
|
from time import strftime
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
import re
|
||||||
|
|
||||||
fl0w_path = os.path.dirname(os.path.realpath(__file__))
|
fl0w_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/"
|
shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/"
|
||||||
|
@ -12,18 +12,19 @@ if shared_path not in path:
|
||||||
path.append(shared_path)
|
path.append(shared_path)
|
||||||
|
|
||||||
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
from Highway import Client, Route, Pipe, DummyPipe
|
from Highway import Client, Route, Pipe, DummyPipe
|
||||||
|
from Utils import get_hostname
|
||||||
|
|
||||||
from SublimeMenu import *
|
from SublimeMenu import *
|
||||||
import Logging
|
import Logging
|
||||||
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
from time import sleep
|
||||||
|
from _thread import start_new_thread
|
||||||
|
import os
|
||||||
|
|
||||||
CHANNEL = 1
|
CHANNEL = 1
|
||||||
FL0W_STATUS = "fl0w"
|
FL0W_STATUS = "fl0w"
|
||||||
|
@ -32,8 +33,21 @@ def plugin_unloaded():
|
||||||
for window in windows:
|
for window in windows:
|
||||||
if hasattr(window, "fl0w") and window.fl0w.connected:
|
if hasattr(window, "fl0w") and window.fl0w.connected:
|
||||||
window.fl0w.invoke_disconnect()
|
window.fl0w.invoke_disconnect()
|
||||||
|
for sensor_type in ("analog", "digital"):
|
||||||
|
window.active_view().erase_phantoms(sensor_type)
|
||||||
|
|
||||||
|
|
||||||
|
PARENTHESES_REGEX = re.compile("\((.*?)\)")
|
||||||
|
STYLE_OPEN = "<body><style>code { color: var(--orangish); }</style><code>"
|
||||||
|
STYLE_CLOSE = "</code></body>"
|
||||||
|
|
||||||
|
|
||||||
|
windows = []
|
||||||
|
sensor_phantoms = []
|
||||||
|
|
||||||
|
def set_status(status, window):
|
||||||
|
window.active_view().set_status(FL0W_STATUS,
|
||||||
|
"fl0w: %s" % status)
|
||||||
|
|
||||||
class Fl0wClient(Client):
|
class Fl0wClient(Client):
|
||||||
def setup(self, routes, fl0w, debug=False):
|
def setup(self, routes, fl0w, debug=False):
|
||||||
|
@ -45,6 +59,9 @@ class Fl0wClient(Client):
|
||||||
self.fl0w.connected = True
|
self.fl0w.connected = True
|
||||||
if self.fl0w.debug:
|
if self.fl0w.debug:
|
||||||
Logging.info("Connection ready!")
|
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")
|
self.send({"subscribe" : [2]}, "peers")
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,6 +84,11 @@ class Fl0wClient(Client):
|
||||||
handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu)
|
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):
|
class Peers(Route):
|
||||||
def start(self, handler):
|
def start(self, handler):
|
||||||
self.selected_action_menu = None
|
self.selected_action_menu = None
|
||||||
|
@ -76,10 +98,14 @@ class Fl0wClient(Client):
|
||||||
for id_ in data:
|
for id_ in data:
|
||||||
action_menu = Menu()
|
action_menu = Menu()
|
||||||
action_menu.id_ = id_
|
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",
|
action_menu += Entry("Programs",
|
||||||
"Lists all executable programs on the controller.",
|
"Lists all executable programs on the controller.",
|
||||||
action=partial(lambda handler, id_: handler.pipe(None, "list_programs", id_),
|
action=partial(lambda handler, id_: handler.pipe(None, "list_programs", id_),
|
||||||
handler, id_))
|
handler, id_))
|
||||||
action_menu += Entry("Set Name",
|
action_menu += Entry("Set Name",
|
||||||
"Sets the hostname of the selected controller",
|
"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"],
|
||||||
|
@ -95,6 +121,10 @@ class Fl0wClient(Client):
|
||||||
kwargs={"selected_action_menu" : action_menu})
|
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):
|
def set_selected_action_menu(self, selected_action_menu):
|
||||||
self.selected_action_menu = selected_action_menu
|
self.selected_action_menu = selected_action_menu
|
||||||
|
@ -122,13 +152,23 @@ class Fl0wClient(Client):
|
||||||
program_menu.invoke(handler.fl0w.window,
|
program_menu.invoke(handler.fl0w.window,
|
||||||
back=handler.routes["peers"].selected_action_menu)
|
back=handler.routes["peers"].selected_action_menu)
|
||||||
|
|
||||||
|
class StdStream(Pipe):
|
||||||
|
def run(data, peer, handler):
|
||||||
|
|
||||||
|
|
||||||
class Fl0w:
|
class Fl0w:
|
||||||
def __init__(self, window, debug=False):
|
def __init__(self, window, debug=False):
|
||||||
self.connected = False
|
|
||||||
self.window = window
|
self.window = window
|
||||||
self.folder = window.folders()[0]
|
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
|
self._debug = debug
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,28 +184,93 @@ class Fl0w:
|
||||||
self.debug_menu += Entry("Off",
|
self.debug_menu += Entry("Off",
|
||||||
action=lambda: self.set_debug(False))
|
action=lambda: self.set_debug(False))
|
||||||
|
|
||||||
|
|
||||||
self.settings = Menu()
|
self.settings = Menu()
|
||||||
self.settings += Entry("Debug", "Toggle debug mode",
|
self.settings += Entry("Debug", "Toggle debug mode",
|
||||||
sub_menu=self.debug_menu)
|
sub_menu=self.debug_menu)
|
||||||
|
|
||||||
|
|
||||||
self.meta = Menu()
|
self.meta = Menu()
|
||||||
self.meta += Entry("Info", "Server info",
|
self.meta += Entry("Info", "Server info",
|
||||||
action=lambda: self.ws.send(None, "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)
|
sub_menu=self.meta)
|
||||||
if self.debug:
|
if self.debug:
|
||||||
self.main_menu += self.meta_entry
|
self.main_menu += self.meta_entry
|
||||||
|
|
||||||
|
|
||||||
self.main_menu = Menu()
|
self.main_menu = Menu()
|
||||||
self.controller_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)
|
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)
|
sub_menu=self.settings)
|
||||||
self.main_menu += Entry("Disconnect", "Disconnect from server",
|
self.main_menu += Entry("Disconnect", "Disconnect from server",
|
||||||
action=self.invoke_disconnect)
|
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
|
@property
|
||||||
def debug(self):
|
def debug(self):
|
||||||
|
@ -185,8 +290,7 @@ class Fl0w:
|
||||||
else:
|
else:
|
||||||
self._debug = False
|
self._debug = False
|
||||||
self.main_menu -= self.meta_entry
|
self.main_menu -= self.meta_entry
|
||||||
self.window.active_view().set_status(FL0W_STATUS,
|
set_status("Debug set to %s" % self._debug, self.window)
|
||||||
"Debug set to %s" % self._debug)
|
|
||||||
|
|
||||||
|
|
||||||
def invoke_start_menu(self):
|
def invoke_start_menu(self):
|
||||||
|
@ -208,12 +312,12 @@ class Fl0w:
|
||||||
self.ws = Fl0wClient('ws://%s' % connect_details)
|
self.ws = Fl0wClient('ws://%s' % connect_details)
|
||||||
self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(),
|
self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(),
|
||||||
"processes" : Fl0wClient.Processes(),
|
"processes" : Fl0wClient.Processes(),
|
||||||
"list_programs" : Fl0wClient.ListPrograms()},
|
"list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor()},
|
||||||
self, debug=True)
|
self, debug=True)
|
||||||
self.ws.connect()
|
self.ws.connect()
|
||||||
sublime.set_timeout_async(self.ws.run_forever, 0)
|
sublime.set_timeout_async(self.ws.run_forever, 0)
|
||||||
self.window.active_view().set_status(FL0W_STATUS,
|
set_status("Connection opened '%s'" % self.folder, self.window)
|
||||||
"Connection opened '%s'" % self.folder)
|
self.connected = True
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
sublime.error_message("Error during connection creation:\n %s" % str(e))
|
sublime.error_message("Error during connection creation:\n %s" % str(e))
|
||||||
|
|
||||||
|
@ -227,11 +331,8 @@ class Fl0w:
|
||||||
def invoke_disconnect(self):
|
def invoke_disconnect(self):
|
||||||
if self.connected:
|
if self.connected:
|
||||||
self.ws.close()
|
self.ws.close()
|
||||||
self.window.active_view().set_status(FL0W_STATUS,
|
set_status("Connection closed '%s'" % self.folder, self.window)
|
||||||
"Connection closed '%s'" % self.folder)
|
self.connected = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Fl0wCommand(sublime_plugin.WindowCommand):
|
class Fl0wCommand(sublime_plugin.WindowCommand):
|
||||||
|
@ -271,5 +372,150 @@ class Fl0wCommand(sublime_plugin.WindowCommand):
|
||||||
self.window.fl0w.invoke_disconnect()
|
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 = []
|
|
||||||
|
|
Reference in a new issue