def __init__(self, console, default_node, nodes): self.default_node = default_node self.nodes = nodes for node in nodes: if node != default_node: ClientAsync.aw(node.lock())
def process_events(self, on_event_data=None, all_nodes=False): """Listen to events sent by the program running on the robot and process them until _exit is received. Argument: on_event_data -- func(node, event_name) called when new data is received """ exit_received = None # or exit code once received def on_event_received(node, event_name, event_data): if self.output_enabled: if event_name == "_exit": nonlocal exit_received exit_received = event_data[0] elif event_name == "_print": print_id = event_data[0] print_format, print_num_args = print_statements[print_id] print_args = tuple(event_data[1 : 1 + print_num_args]) print_str = print_format % print_args print(print_str) else: if len(event_data) > 0: if node.id_str not in self.event_data_dict: self.event_data_dict[node.id_str] = {} if event_name not in self.event_data_dict[node.id_str]: self.event_data_dict[node.id_str][event_name] = [] self.event_data_dict[node.id_str][event_name].append(event_data) if on_event_data is not None: on_event_data(node, event_name) def wake(): return exit_received is not None self.client.clear_event_received_listeners() self.client.add_event_received_listener(on_event_received) try: if all_nodes: for node in self.client.nodes: ClientAsync.aw(node.watch(events=True)) else: ClientAsync.aw(self.node.watch(events=True)) ClientAsync.aw(self.client.sleep(wake=wake)) self.stop_program(self.node, discard_output=True) if exit_received: print(f"Exit, status={exit_received}") finally: if all_nodes: for node in self.client.nodes: ClientAsync.aw(node.watch(events=False)) else: ClientAsync.aw(self.node.watch(events=False)) self.client.clear_event_received_listeners()
def sleep(t): """Wait for some time. Argument: t -- time to wait in seconds """ # send and flush all variables which might have been changed self.send_variables(self.var_set) # wait ClientAsync.aw(self.client.sleep(t)) # fetch all variables which might be used self.fetch_variables(self.var_got, node_flush=False)
async def start(zeroconf=None, tdm_addr=None, tdm_port=None, password=None, debug=0, **kwargs): """Start the connection with the Thymio and variable synchronization. Arguments: tdm_addr - TDM address as a string (default: localhost) tdm_port - TDM TCP port number (default: standard or provided by zeroconf) password - TDM password (default: None, not necessary for local TDM) robot_id - robot node id (default: any) robot_name - robot name (default: any) zeroconf - True to find TDM with zeroconf (default: automatic) """ client = ClientAsync(zeroconf=zeroconf, tdm_addr=tdm_addr, tdm_port=tdm_port, password=password, debug=debug) node = await client.wait_for_node(**kwargs) await node.lock() global _interactive_console _interactive_console = TDMConsole(local_var=get_ipython().user_ns) await _interactive_console.init(client, node) # configure ipython ip = get_ipython() ip.events.register("pre_run_cell", _pre_run_cell) ip.events.register("post_run_cell", _post_run_cell)
def on_nodes_changed(nodes): nodes = [ node for node in ClientAsync.filter_nodes( nodes, node_id=self.node_id, node_name=self.node_name) if node.status != self.client.NODE_STATUS_DISCONNECTED ] self.node = nodes[0] if len(nodes) > 0 else None if self.node is None: self.clear_variables() self.set_title() self.info_mode["text"] = "" else: self.info_mode["text"] = { self.client.NODE_STATUS_UNKNOWN: "No robot", self.client.NODE_STATUS_CONNECTED: "Robot connected", self.client.NODE_STATUS_AVAILABLE: "Observe", self.client.NODE_STATUS_BUSY: "Observe (robot busy)", self.client.NODE_STATUS_READY: "Control", self.client.NODE_STATUS_DISCONNECTED: "Robot disconnected", }[self.node.status] # new node, set it up by starting coroutine self.start_co = self.init_prog() # disable menu Control if busy if self.node.status in { self.client.NODE_STATUS_AVAILABLE, self.client.NODE_STATUS_READY }: self.robot_menu.entryconfig("Control", state="normal") else: self.robot_menu.entryconfig("Control", state="disabled")
def connect(self): def on_nodes_changed(nodes): nodes = [ node for node in ClientAsync.filter_nodes( nodes, node_id=self.node_id, node_name=self.node_name) if node.status != self.client.NODE_STATUS_DISCONNECTED ] self.node = nodes[0] if len(nodes) > 0 else None if self.node is None: self.clear_variables() self.set_title() self.info_mode["text"] = "" else: self.info_mode["text"] = { self.client.NODE_STATUS_UNKNOWN: "No robot", self.client.NODE_STATUS_CONNECTED: "Robot connected", self.client.NODE_STATUS_AVAILABLE: "Observe", self.client.NODE_STATUS_BUSY: "Observe (robot busy)", self.client.NODE_STATUS_READY: "Control", self.client.NODE_STATUS_DISCONNECTED: "Robot disconnected", }[self.node.status] # new node, set it up by starting coroutine self.start_co = self.init_prog() # disable menu Control if busy if self.node.status in { self.client.NODE_STATUS_AVAILABLE, self.client.NODE_STATUS_READY }: self.robot_menu.entryconfig("Control", state="normal") else: self.robot_menu.entryconfig("Control", state="disabled") def on_variables_changed(node, variables): if self.edited_variable is None: for name in variables: if variables[name] is not None: self.add_variable(name, variables[name]) self.client = ClientAsync(zeroconf=self.zeroconf, tdm_addr=self.tdm_addr, tdm_port=self.tdm_port, password=self.password, debug=self.debug) self.client.on_nodes_changed = on_nodes_changed self.client.add_variables_changed_listener(on_variables_changed) # schedule communication self.after(100, self.run)
async def list(zeroconf=None, tdm_addr=None, tdm_port=None, password=None, robot_id=None, robot_name=None, timeout=5): """Display a list of all the robots. Arguments: tdm_addr - TDM address as a string (default: as in start()) tdm_port - TDM TCP port number (default: as in start()) password - TDM password (default: None, not necessary for local TDM) robot_id - robot id to restrict the output (default: any) robot_name - robot name to restrict the output (default: any) timeout - time to obtain at least one node (default: 5s) zeroconf - True to find TDM with zeroconf (default: automatic) """ with (ClientAsync(zeroconf=zeroconf, tdm_addr=tdm_addr, tdm_port=tdm_port, password=password) if zeroconf is not None or tdm_addr is not None or tdm_port is not None or _interactive_console is None else _interactive_console.client) as client: for _ in range(1 if timeout < 0.1 else int(timeout / 0.1)): client.process_waiting_messages() if len(client.nodes) > 0: break await client.sleep(0.1) for node in client.filter_nodes(client.nodes, node_id=robot_id, node_name=robot_name): print(f"id: {node.id_str}") if "group_id_str" in node.props and node.props[ "group_id_str"] is not None: print(f"group id: {node.props['group_id_str']}") if "name" in node.props: print(f"name: {node.props['name']}") if "status" in node.props: status_str = { ClientAsync.NODE_STATUS_UNKNOWN: "unknown", ClientAsync.NODE_STATUS_CONNECTED: "connected", ClientAsync.NODE_STATUS_AVAILABLE: "available", ClientAsync.NODE_STATUS_BUSY: "busy", ClientAsync.NODE_STATUS_READY: "ready", ClientAsync.NODE_STATUS_DISCONNECTED: "disconnected", }[node.status] print(f"status: {node.status} ({status_str})") if "capabilities" in node.props: print(f"cap: {node.props['capabilities']}") if "fw_version" in node.props: print(f"firmware: {node.props['fw_version']}") print()
def stop_program(self, node, discard_output=False): with self.lock_robots({node}) as nodes_l: output_enabled_orig = self.output_enabled self.output_enabled = not discard_output try: error = ClientAsync.aw(node.stop()) if error is not None: raise Exception(f"Error {error['error_code']}") finally: self.output_enabled = output_enabled_orig
def main(argv=None): tdm_addr = None tdm_port = None password = None robot_id = None robot_name = None if argv is not None: try: arguments, values = getopt.getopt(argv[1:], "", [ "help", "password="******"robotid=", "robotname=", "tdmaddr=", "tdmport=", ]) except getopt.error as err: print(str(err)) sys.exit(1) for arg, val in arguments: if arg == "--help": help() sys.exit(0) elif arg == "--password": password = val elif arg == "--robotid": robot_id = val elif arg == "--robotname": robot_name = val elif arg == "--tdmaddr": tdm_addr = val elif arg == "--tdmport": tdm_port = int(val) with ClientAsync(tdm_addr=tdm_addr, tdm_port=tdm_port, password=password) as client: async def co_init(): with await client.lock(node_id=robot_id, node_name=robot_name) as node: interactive_console = TDMConsole(user_functions={ "get_client": lambda: client, "get_node": lambda: node, }) await interactive_console.init(client, node) interactive_console.interact() client.run_async_program(co_init)
def run_node(node): """Compile, configure node, load and start program on a node. Return True if the node requires waiting. """ print_statements[node] = [] events = [] if language == "python": # transpile from Python to Aseba transpiler = self.transpile(src, import_thymio) src_aseba = transpiler.get_output() print_statements[node] = transpiler.print_format_strings if len(print_statements[node]) > 0: events.append(("_print", 1 + transpiler.print_max_num_args)) if transpiler.has_exit_event: events.append(("_exit", 1)) for event_name in transpiler.events_in: events.append((event_name, transpiler.events_in[event_name])) for event_name in transpiler.events_out: events.append((event_name, transpiler.events_out[event_name])) if len(events) > 0: events = ClientAsync.aw(node.filter_out_vm_events(events)) if len(events) > 0: ClientAsync.aw(node.register_events(events)) elif language == "aseba": src_aseba = src else: raise Exception(f"Unsupported language {language}") error = ClientAsync.aw(node.compile(src_aseba)) if error is not None: raise Exception(error["error_msg"]) node.send_set_scratchpad(src_aseba) wait_for_node = wait if wait is None: # default: wait if there are events to receive wait_for_node = len(events) > 0 if wait_for_node: ClientAsync.aw(node.watch(events=True, vm_state=True)) error = ClientAsync.aw(node.run()) if error is not None: raise Exception(f"Error {error['error_code']}") return wait_for_node
sys.exit(0) elif arg == "--debug": debug = int(val) elif arg == "--password": password = val elif arg == "--robotid": robot_id = val elif arg == "--robotname": robot_name = val elif arg == "--tdmaddr": tdm_addr = val elif arg == "--tdmport": tdm_port = int(val) with ClientAsync(tdm_addr=tdm_addr, tdm_port=tdm_port, password=password, debug=debug) as client: for _ in range(50): client.process_waiting_messages() if len(client.nodes) > 0: break sleep(0.1) for node in client.filter_nodes(client.nodes, node_id=robot_id, node_name=robot_name): print(f"id: {node.id_str}") if "group_id_str" in node.props and node.props[ "group_id_str"] is not None: print(f"group id: {node.props['group_id_str']}")
#!/usr/bin/env python3 # This file is part of tdmclient. # Copyright 2021 ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, # Miniature Mobile Robots group, Switzerland # Author: Yves Piguet # # SPDX-License-Identifier: BSD-3-Clause from tdmclient import ClientAsync if __name__ == "__main__": with ClientAsync(debug=0) as client: thymio_program = """ leds.top = [0, 0, 32] leds.bottom.left = [32, 0, 0] leds.bottom.right = [0, 32, 0] """ async def prog(): await client.wait_for_status(client.NODE_STATUS_AVAILABLE) node = client.first_node() print(node.id_str) await node.lock_node() await client.wait_for_status(client.NODE_STATUS_READY) error = await node.compile(thymio_program) if error is not None: print(f"Compilation error: {error['error_msg']}") else:
# This file is part of tdmclient. # Copyright 2021 ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, # Miniature Mobile Robots group, Switzerland # Author: Yves Piguet # # SPDX-License-Identifier: BSD-3-Clause from tdmclient import ClientAsync if __name__ == "__main__": with ClientAsync() as client: async def prog(): with await client.lock() as node: while True: str = input("System sound id (0-8 or exit): ") if str == "exit": break try: i = int(str) error = await node.compile(f"call sound.system({i})") if error is not None: print(f"Compilation error: {error['error_msg']}") else: await node.watch(events=True) error = await node.run() if error is not None: print(f"Error {error['error_code']}") except ValueError: print("Unexpected value")
def run_program(self, src, nodes=None, language="aseba", wait=False, import_thymio=True): if nodes is None: nodes = [self.node] running_nodes = set() # exit_received[node] = exit code once received exit_received = {} # print_statements[node][print_id] = (print_format, print_num_args) print_statements = {} def on_event_received(node, event_name, event_data): if self.output_enabled: if event_name == "_exit": exit_received[node] = event_data[0] if event_data[0]: exit_str = f"Exit, status={event_data[0]}" if len(nodes) > 1: # multiple nodes: add prefix exit_str = f"[R{nodes.index(node)}] " + exit_str print(exit_str) self.stop_program(node, discard_output=True) running_nodes.remove(node) elif event_name == "_print": print_id = event_data[0] print_format, print_num_args = print_statements[node][print_id] print_args = tuple(event_data[1 : 1 + print_num_args]) print_str = print_format % print_args if len(nodes) > 1: # multiple nodes: add prefix print_str = f"[R{nodes.index(node)}] " + print_str print(print_str) else: if len(event_data) > 0: if node.id_str not in self.event_data_dict: self.event_data_dict[node.id_str] = {} if event_name not in self.event_data_dict[node.id_str]: self.event_data_dict[node.id_str][event_name] = [] self.event_data_dict[node.id_str][event_name].append(event_data) def on_vm_state_changed(node, state, line, error, error_msg): if error != ClientAsync.ERROR_NO_ERROR: exit_received[node] = f"vm error {error}" if error_msg: print(f"{error_msg} (line {line}{' in Aseba' if language != 'aseba' else ''})") def run_node(node): """Compile, configure node, load and start program on a node. Return True if the node requires waiting. """ print_statements[node] = [] events = [] if language == "python": # transpile from Python to Aseba transpiler = self.transpile(src, import_thymio) src_aseba = transpiler.get_output() print_statements[node] = transpiler.print_format_strings if len(print_statements[node]) > 0: events.append(("_print", 1 + transpiler.print_max_num_args)) if transpiler.has_exit_event: events.append(("_exit", 1)) for event_name in transpiler.events_in: events.append((event_name, transpiler.events_in[event_name])) for event_name in transpiler.events_out: events.append((event_name, transpiler.events_out[event_name])) if len(events) > 0: events = ClientAsync.aw(node.filter_out_vm_events(events)) if len(events) > 0: ClientAsync.aw(node.register_events(events)) elif language == "aseba": src_aseba = src else: raise Exception(f"Unsupported language {language}") error = ClientAsync.aw(node.compile(src_aseba)) if error is not None: raise Exception(error["error_msg"]) node.send_set_scratchpad(src_aseba) wait_for_node = wait if wait is None: # default: wait if there are events to receive wait_for_node = len(events) > 0 if wait_for_node: ClientAsync.aw(node.watch(events=True, vm_state=True)) error = ClientAsync.aw(node.run()) if error is not None: raise Exception(f"Error {error['error_code']}") return wait_for_node self.reset_sync_var() self.client.clear_event_received_listeners() self.client.add_event_received_listener(on_event_received) self.client.add_vm_state_changed_listener(on_vm_state_changed) wait_for_nodes = False with self.lock_robots(nodes) as nodes_l: # transpile, compile, load, set scratchpad, and run for node in nodes_l: wait_for_node = run_node(node) wait_for_nodes = wait_for_nodes or wait_for_node running_nodes.add(node) # wait until all nodes have exited if wait_for_nodes: try: def wake(): # True when all nodes have exited return len(exit_received) >= len(nodes) ClientAsync.aw(self.client.sleep(wake=wake)) finally: # stop nodes still running for node in running_nodes: self.stop_program(node, discard_output=True) self.client.clear_event_received_listeners() self.client.clear_vm_state_changed_listener()
async def watch(timeout=-1, zeroconf=None, tdm_addr=None, tdm_port=None, password=None, robot_id=None, robot_name=None): """Display the robot variables with live updates until the timeout elapses or the execution is interrupted. Arguments: timeout -- amount of time until updates stop zeroconf -- True to use find TDM with zeroconf (default: automatic) password - TDM password (default: None, not necessary for local TDM) tdm_addr -- address of the tdm tdm_port -- port of the tdm (default: connection established by start(), or from zeroconf) robot_id ID -- robot specified by id (default: first robot) robot_name NAME -- robot specified by name (default: first robot) """ import IPython.display def var_dict_to_md(variable_dict): md = "| Variable | Value |\n| --- | --- |\n" md += "\n".join( [f"| {name} | {variable_dict[name]} |" for name in variable_dict]) return md async def watch_node(client, node): variable_dict = node.var def variables_changed_listener(node, variable_update_dict): nonlocal variable_dict variable_dict = dict( sorted({ **variable_dict, **variable_update_dict }.items())) IPython.display.clear_output(wait=True) md = var_dict_to_md(variable_dict) IPython.display.display(IPython.display.Markdown(md)) node.add_variables_changed_listener(variables_changed_listener) variables_changed_listener(node, node.var) try: await client.sleep() except: # avoid long exception message with stack trace print("Interrupted") finally: IPython.display.clear_output(wait=True) node.remove_variables_changed_listener(variables_changed_listener) if _interactive_console is not None: await watch_node(_interactive_console.client, _interactive_console.node) else: with ClientAsync(zeroconf=zeroconf, tdm_addr=tdm_addr, tdm_port=tdm_port, password=password) as client: await client.wait_for_status_set({ ClientAsync.NODE_STATUS_AVAILABLE, ClientAsync.NODE_STATUS_BUSY }) node = client.first_node(node_id=robot_id, node_name=robot_name) await node.watch(variables=True) await watch_node(client, node)
class VariableTableWindow(tk.Tk): def __init__(self, zeroconf=None, tdm_addr=None, tdm_port=None, password=None, node_id=None, node_name=None, language=None, debug=0): super(VariableTableWindow, self).__init__() self.geometry("800x600") self.program_path = None self.program_src = "" self.language = language or "aseba" self.zeroconf = zeroconf self.tdm_addr = tdm_addr self.tdm_port = tdm_port self.password = password self.node_id = node_id self.node_name = node_name self.debug = debug # menus accelerator_key = "Cmd" if sys.platform == "darwin" else "Ctrl" bind_key = "Command" if sys.platform == "darwin" else "Control" menubar = tk.Menu(self) self.config(menu=menubar) self.bind("<" + bind_key + "-q>", lambda event: self.quit()) file_menu = tk.Menu(menubar, tearoff=False) file_menu.add_command(label="New", command=self.new, accelerator=accelerator_key + "-N") self.bind("<" + bind_key + "-n>", lambda event: self.new()) file_menu.add_separator() file_menu.add_command(label="Open", command=self.open, accelerator=accelerator_key + "-O") self.bind("<" + bind_key + "-o>", lambda event: self.open()) file_menu.add_command(label="Save", command=lambda: self.save(self.program_path), accelerator=accelerator_key + "-S") self.bind("<" + bind_key + "-s>", lambda event: self.save(self.program_path)) file_menu.add_command(label="Save As...", command=lambda: self.save(None), accelerator=accelerator_key + "-Shift-") self.bind("<" + bind_key + "-S>", lambda event: self.save(None)) if sys.platform != "darwin": file_menu.add_separator() file_menu.add_command(label="Quit", command=self.quit, accelerator=accelerator_key + "-Q") menubar.add_cascade(label="File", menu=file_menu) def send_event_to_focused_widget(event_id): widget = self.focus_get() if widget is not None: widget.event_generate(event_id) edit_menu = tk.Menu(menubar, tearoff=False) edit_menu.add_command( label="Cut", command=lambda: send_event_to_focused_widget("<<Cut>>")) edit_menu.add_command( label="Copy", command=lambda: send_event_to_focused_widget("<<Copy>>")) edit_menu.add_command( label="Paste", command=lambda: send_event_to_focused_widget("<<Paste>>")) menubar.add_cascade(label="Edit", menu=edit_menu) view_menu = tk.Menu(menubar, tearoff=False) self.view_var = tk.IntVar() view_menu.add_radiobutton(label="Variables", variable=self.view_var, value=1, command=self.set_view_variables, accelerator=accelerator_key + "-Shift-V") self.bind("<" + bind_key + "-V>", lambda event: self.set_view_variables()) view_menu.add_radiobutton(label="Program", variable=self.view_var, value=2, command=self.set_view_program, accelerator=accelerator_key + "-Shift-P") self.bind("<" + bind_key + "-P>", lambda event: self.set_view_program()) menubar.add_cascade(label="View", menu=view_menu) self.view_var.set(1) self.robot_menu = tk.Menu(menubar, tearoff=False) lock_node_var = tk.BooleanVar() self.robot_menu.add_checkbutton(label="Control", variable=lock_node_var, accelerator=accelerator_key + "-L") self.bind("<" + bind_key + "-l>", lambda event: lock_node_var.set(not lock_node_var.get())) lock_node_var.trace_add( "write", lambda var, index, mode: self.lock_node(lock_node_var.get())) self.robot_menu.add_separator() self.robot_menu.add_command(label="Run", command=self.run_program, state="disabled", accelerator=accelerator_key + "-R") self.bind("<" + bind_key + "-r>", lambda event: self.run_program()) self.robot_menu.add_command(label="Stop", command=self.stop_program, state="disabled", accelerator="Escape") self.bind("<Escape>", lambda event: self.stop_program()) self.robot_menu.add_separator() self.language_var = tk.IntVar() self.robot_menu.add_radiobutton( label="Aseba", variable=self.language_var, value=1, command=lambda: self.set_language("aseba")) self.robot_menu.add_radiobutton( label="Python", variable=self.language_var, value=2, command=lambda: self.set_language("py")) self.language_var.set(1) menubar.add_cascade(label="Robot", menu=self.robot_menu) # main layout: info at bottom (one line), scrollable main content above self.main_content = tk.Frame(self) self.main_content.pack(fill=tk.BOTH, expand=True) status_frame = tk.Frame(self, height=1) status_frame.pack(side=tk.BOTTOM, fill=tk.X) self.info_mode = tk.Label(status_frame, anchor="w", bg="#fff", fg="#666", width=16) # char units self.info_mode.pack(side=tk.LEFT) self.info_error = tk.Label(status_frame, anchor="e", bg="#fff", fg="#666") self.info_error.pack(side=tk.LEFT, fill=tk.X, expand=True) # variables self.canvas = None self.scrollbar = None self.frame = None # program self.text_program = None # key=name, value={"widget": w, "value": v} self.variables = {} self.edited_variable = None self.client = None self.node = None self.locked = False self.start_co = None self.set_title() def set_title(self): name = self.node.props["name"] if self.node is not None else "No robot" if self.client is not None and self.client.tdm_addr is not None: name += f" (TDM: {self.client.tdm_addr}:{self.client.tdm_port})" if self.text_program is not None: name += " - " name += os.path.basename( self.program_path ) if self.program_path is not None else f"Untitled.{self.language}" self.title(name) def set_view_variables(self): self.view_var.set(1) self.remove_program_view() self.create_variable_view() self.set_title() def set_view_program(self): self.view_var.set(2) self.remove_variable_view() self.create_program_view() self.set_title() def set_language(self, language): self.language_var.set({ "aseba": 1, "py": 2, }[language]) self.language = language self.set_title() def new(self): self.remove_program_view() self.program_src = "" self.program_path = None self.set_view_program() def open(self): path = filedialog.askopenfilename( filetypes=[("All", ".aseba .py"), ("Aseba", ".aseba"), ("Python", ".py")]) if path: with open(path, encoding="utf-8") as f: self.remove_program_view() self.program_src = f.read() self.program_path = path self.set_view_program() self.text_program.edit_modified(False) self.set_language("py" if os.path.splitext(path)[1] == ".py" else "aseba") def save(self, path): if path is None: path = filedialog.asksaveasfilename(filetypes=[ ("Aseba", ".aseba"), ], defaultextension="." + self.language) if path: with open(path, "wb") as f: self.program_src = self.text_program.get("1.0", "end") f.write(bytes(self.program_src, "utf-8")) self.text_program.edit_modified(False) self.program_path = path def run_src(self, src_aseba): async def run_a(): error = await self.node.compile(src_aseba) if error is not None: self.error_msg = error["error_msg"] self.info_error["text"] = self.error_msg else: error = await self.node.run() if error is not None: self.error_msg = f"Run error {error['error_code']}" self.info_error["text"] = self.error_msg else: self.error_msg = None self.info_error["text"] = "OK" self.client.run_async_program(run_a) def run_program(self): if self.locked and self.text_program is not None: self.program_src = self.text_program.get("1.0", "end") if self.language == "py": try: aseba_src = ATranspiler.simple_transpile(self.program_src) except Exception as e: self.error_msg = str(e) self.info_error["text"] = self.error_msg return else: aseba_src = self.program_src self.run_src(aseba_src) def stop_program(self): async def stop_a(): error = await self.node.stop() if error is not None: self.error_msg = f"Stop error {error['error_code']}" self.info_error["text"] = self.error_msg if self.locked: self.client.run_async_program(stop_a) async def init_prog(self): await self.client.wait_for_status_set( { self.client.NODE_STATUS_AVAILABLE, self.client.NODE_STATUS_BUSY, self.client.NODE_STATUS_READY }, node_id=self.node_id, node_name=self.node_name) self.node = self.client.first_node(node_id=self.node_id, node_name=self.node_name) self.set_title() await self.node.watch(variables=True) def lock_node(self, locked): if locked: self.node.send_lock_node() self.robot_menu.entryconfig("Run", state="normal") self.robot_menu.entryconfig("Stop", state="normal") else: self.node.send_unlock_node() self.robot_menu.entryconfig("Run", state="disabled") self.robot_menu.entryconfig("Stop", state="disabled") self.locked = locked def remove_variable_view(self): if self.canvas is not None: self.canvas.destroy() self.canvas = None self.frame = None self.scrollbar.destroy() self.scrollbar = None def create_variable_row(self, name, value): f = tk.Frame(self.frame) f.pack(fill=tk.X, expand=True) title = name + (f"[{len(value)}]" if len(value) > 1 else "") l = tk.Label(f, text=title, anchor="w", width=25) l.pack(side=tk.LEFT) text = ", ".join([str(item) for item in value]) v = tk.Label(f, text=text, anchor="w") v.pack(side=tk.LEFT, fill=tk.X, expand=True) v.bind("<Button-1>", lambda e: self.begin_editing(name)) self.variables[name] = { "widget": f, "vwidget": v, "ewidget": None, "value": value, "text": text, } def create_variable_view(self): if self.frame is None: self.canvas = tk.Canvas(self.main_content) self.canvas.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) self.scrollbar = tk.Scrollbar(self.main_content, orient="vertical", command=self.canvas.yview) self.scrollbar.pack(side=tk.RIGHT, fill="y") self.canvas.configure(yscrollcommand=self.scrollbar.set) self.canvas.bind( "<Configure>", lambda event: self.canvas.configure( scrollregion=self.canvas.bbox("all"))) self.canvas.bind( "<Enter>", lambda event: self.canvas.bind_all( "<MouseWheel>", lambda event: self.canvas.yview_scroll( -event.delta // 120, "units"))) self.canvas.bind( "<Leave>", lambda event: self.canvas.unbind_all("<MouseWheel>")) self.frame = tk.Frame(self.canvas) self.frame.bind( "<Configure>", lambda event: self.canvas.configure( scrollregion=self.canvas.bbox("all"))) self.canvas.create_window((0, 0), window=self.frame, anchor="nw") for name in self.variables: self.create_variable_row(name, self.variables[name]["value"]) def add_variable(self, name, value): if self.view_var.get() == 1 and value is not None: if name not in self.variables: self.create_variable_view() self.create_variable_row(name, value) else: text = ", ".join([str(item) for item in value]) v = self.variables[name] v["text"] = text v["vwidget"]["text"] = text else: # just remember value self.variables[name] = {"value": value} def clear_variables(self): self.end_editing(cancel=True) for name in self.variables: self.variables[name]["widget"].destroy() self.variables = {} self.remove_variable_view() def begin_editing(self, name): if self.node.status != self.client.NODE_STATUS_READY or not self.end_editing( keep_editing_on_error=True): return self.edited_variable = name v = self.variables[name] entry = tk.Entry(v["vwidget"]) v["ewidget"] = entry entry.insert(0, v["text"]) entry.place(x=0, y=0, anchor="nw", relwidth=1, relheight=1) entry.bind("<Return>", lambda e: self.end_editing(keep_editing_on_error=True)) entry.bind("<Escape>", lambda e: self.end_editing(cancel=True)) entry.focus_set() def end_editing(self, cancel=False, keep_editing_on_error=False): if self.edited_variable is not None: v = self.variables[self.edited_variable] text = v["ewidget"].get() if not cancel: try: new_value = [int(s) for s in text.split(",")] if len(new_value) != len(v["value"]): raise Exception() self.node.send_set_variables( {self.edited_variable: new_value}) except Exception as e: print(type(e), e) if keep_editing_on_error: return False v["ewidget"].destroy() v["ewidget"] = None self.edited_variable = None return True def remove_program_view(self): if self.text_program is not None: self.program_src = self.text_program.get("1.0", "end") self.text_program.destroy() self.text_program = None self.scrollbar.destroy() self.scrollbar = None def create_program_view(self): if self.frame is None: self.scrollbar = tk.Scrollbar(self.main_content, orient="vertical") self.scrollbar.pack(side=tk.RIGHT, fill="y") self.text_program = tk.Text(self.main_content, yscrollcommand=self.scrollbar.set) self.text_program.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.scrollbar.config(command=self.text_program.yview) if self.program_src.strip(): self.text_program.insert("1.0", self.program_src) self.text_program.focus_set() def connect(self): def on_nodes_changed(nodes): nodes = [ node for node in ClientAsync.filter_nodes( nodes, node_id=self.node_id, node_name=self.node_name) if node.status != self.client.NODE_STATUS_DISCONNECTED ] self.node = nodes[0] if len(nodes) > 0 else None if self.node is None: self.clear_variables() self.set_title() self.info_mode["text"] = "" else: self.info_mode["text"] = { self.client.NODE_STATUS_UNKNOWN: "No robot", self.client.NODE_STATUS_CONNECTED: "Robot connected", self.client.NODE_STATUS_AVAILABLE: "Observe", self.client.NODE_STATUS_BUSY: "Observe (robot busy)", self.client.NODE_STATUS_READY: "Control", self.client.NODE_STATUS_DISCONNECTED: "Robot disconnected", }[self.node.status] # new node, set it up by starting coroutine self.start_co = self.init_prog() # disable menu Control if busy if self.node.status in { self.client.NODE_STATUS_AVAILABLE, self.client.NODE_STATUS_READY }: self.robot_menu.entryconfig("Control", state="normal") else: self.robot_menu.entryconfig("Control", state="disabled") def on_variables_changed(node, variables): if self.edited_variable is None: for name in variables: if variables[name] is not None: self.add_variable(name, variables[name]) self.client = ClientAsync(zeroconf=self.zeroconf, tdm_addr=self.tdm_addr, tdm_port=self.tdm_port, password=self.password, debug=self.debug) self.client.on_nodes_changed = on_nodes_changed self.client.add_variables_changed_listener(on_variables_changed) # schedule communication self.after(100, self.run) def run(self): if self.start_co is not None: if not self.client.step_coroutine(self.start_co): # start_co is finished self.start_co = None else: self.client.process_waiting_messages() self.after(100, self.run)