From b7cbc0b3451edf5a8838ece815629e57b345b93d Mon Sep 17 00:00:00 2001 From: Brian ONeill Date: Tue, 6 Apr 2021 19:26:45 -0700 Subject: [PATCH 1/5] Support for adding interfaces at runtime --- canopen_monitor/__main__.py | 2 +- canopen_monitor/app.py | 52 ++++++++++++++++--- canopen_monitor/can/magic_can_bus.py | 19 ++++++- canopen_monitor/ui/__init__.py | 5 +- canopen_monitor/ui/windows.py | 77 ++++++++++++++++++++++++++-- 5 files changed, 140 insertions(+), 15 deletions(-) diff --git a/canopen_monitor/__main__.py b/canopen_monitor/__main__.py index 03ccd6b..cd36bd2 100755 --- a/canopen_monitor/__main__.py +++ b/canopen_monitor/__main__.py @@ -70,7 +70,7 @@ def main(): # Start the can bus and the curses app with MagicCANBus(args.interfaces, no_block=args.no_block) as bus, \ - App(mt, eds_configs) as app: + App(mt, eds_configs, bus) as app: while True: # Bus updates for message in bus: diff --git a/canopen_monitor/app.py b/canopen_monitor/app.py index 1ecfa08..f04ce4b 100644 --- a/canopen_monitor/app.py +++ b/canopen_monitor/app.py @@ -1,13 +1,14 @@ from __future__ import annotations import curses +import curses.ascii import datetime as dt from easygui import fileopenbox from shutil import copy from enum import Enum from . import APP_NAME, APP_VERSION, APP_LICENSE, APP_AUTHOR, APP_DESCRIPTION, \ APP_URL, CACHE_DIR -from .can import MessageTable, MessageType -from .ui import MessagePane, PopupWindow +from .can import MessageTable, MessageType, MagicCANBus +from .ui import MessagePane, PopupWindow, InputForm from .parse import eds # Key Constants not defined in curses @@ -53,6 +54,8 @@ class KeyMap(Enum): F2 = {'name': 'F2', 'description': 'Toggle this menu', 'key': curses.KEY_F2} F3 = {'name': 'F3', 'description': 'Toggle eds file select', 'key': curses.KEY_F3} + F4 = {'name': 'F4', 'description': 'Toggle add interface', + 'key': curses.KEY_F4} UP_ARR = {'name': 'Up Arrow', 'description': 'Scroll pane up 1 row', 'key': curses.KEY_UP} DOWN_ARR = {'name': 'Down Arrow', 'description': 'Scroll pane down 1 row', @@ -90,7 +93,8 @@ class App: :type selected_pane: MessagePane """ - def __init__(self: App, message_table: MessageTable, eds_configs: dict): + def __init__(self: App, message_table: MessageTable, eds_configs: dict, + bus: MagicCANBus): """ App Initialization function :param message_table: Reference to shared message table object @@ -98,6 +102,7 @@ def __init__(self: App, message_table: MessageTable, eds_configs: dict): """ self.table = message_table self.eds_configs = eds_configs + self.bus = bus self.selected_pane_pos = 0 self.selected_pane = None self.key_dict = { @@ -114,7 +119,8 @@ def __init__(self: App, message_table: MessageTable, eds_configs: dict): KeyMap.RESIZE.value['key']: self.resize, KeyMap.F1.value['key']: self.f1, KeyMap.F2.value['key']: self.f2, - KeyMap.F3.value['key']: self.f3 + KeyMap.F3.value['key']: self.f3, + KeyMap.F4.value['key']: self.f4, } def __enter__(self: App) -> App: @@ -130,6 +136,7 @@ def __enter__(self: App) -> App: self.screen.scrollok(True) # Enable window scroll self.screen.keypad(True) # Enable special key input self.screen.nodelay(True) # Disable user-input blocking + curses.noecho() # disable user-input echo curses.curs_set(False) # Disable the cursor self.__init_color_pairs() # Enable colors and create pairs @@ -157,6 +164,10 @@ def __enter__(self: App) -> App: list(KeyMap))), footer='F2: exit window', style=curses.color_pair(1)) + self.interface_win = InputForm(self.screen, + header='Add Interface', + footer='ENTER: save, F4: exit window', + style=curses.color_pair(1)) self.hb_pane = MessagePane(cols={'Node ID': ('node_name', 0, hex), 'State': ('state', 0), 'Status': ('message', 0)}, @@ -200,7 +211,7 @@ def __exit__(self: App, type, value, traceback) -> None: """ # Monitor destruction, restore terminal state curses.nocbreak() # Re-enable line-buffering - curses.noecho() # Enable user-input echo + curses.echo() # Enable user-input echo curses.curs_set(True) # Enable the cursor curses.resetty() # Restore the terminal state curses.endwin() # Destroy the virtual screen @@ -306,6 +317,20 @@ def f3(self): copy(filepath, CACHE_DIR) self.eds_configs[file.node_id] = file + def f4(self): + """ + Toggles Add Interface + :return: None + """ + if self.info_win.enabled: + self.info_win.toggle() + self.info_win.clear() + + if self.hotkeys_win.enabled: + self.hotkeys_win.toggle() + self.hotkeys_win.clear() + + self.interface_win.toggle() def _handle_keyboard_input(self: App) -> None: """ @@ -314,6 +339,16 @@ def _handle_keyboard_input(self: App) -> None: keyboard_input = self.screen.getch() curses.flushinp() + if self.interface_win.enabled: + if keyboard_input == curses.KEY_ENTER or \ + keyboard_input == 10 or keyboard_input == 13: + value = self.interface_win.get_value() + if value != "": + self.bus.add_interface(value) + self.interface_win.toggle() + else: + self.interface_win.read_input(keyboard_input) + try: self.key_dict[keyboard_input]() except KeyError: @@ -372,7 +407,8 @@ def __draw__footer(self: App) -> None: :return: None """ height, width = self.screen.getmaxyx() - footer = ': Info, : Hotkeys, : Add OD File' + footer = ': Info, : Hotkeys, : Add OD File, ' \ + ': Add Interface' self.screen.addstr(height - 1, 1, footer) def draw(self: App, ifaces: [tuple]) -> None: @@ -381,7 +417,8 @@ def draw(self: App, ifaces: [tuple]) -> None: :param ifaces: CAN Bus Interfaces :return: None """ - window_active = self.info_win.enabled or self.hotkeys_win.enabled + window_active = self.info_win.enabled or self.hotkeys_win.enabled or \ + self.interface_win.enabled self.__draw_header(ifaces) # Draw header info # Draw panes @@ -392,6 +429,7 @@ def draw(self: App, ifaces: [tuple]) -> None: # Draw windows self.info_win.draw() self.hotkeys_win.draw() + self.interface_win.draw() self.__draw__footer() diff --git a/canopen_monitor/can/magic_can_bus.py b/canopen_monitor/can/magic_can_bus.py index c97a941..db30f18 100644 --- a/canopen_monitor/can/magic_can_bus.py +++ b/canopen_monitor/can/magic_can_bus.py @@ -19,7 +19,7 @@ def __init__(self: MagicCANBus, if_names: [str], no_block: bool = False): self.keep_alive = t.Event() self.keep_alive.set() self.message_queue = queue.SimpleQueue() - self.threads = None + self.threads = [] @property def statuses(self: MagicCANBus) -> [tuple]: @@ -32,6 +32,21 @@ def statuses(self: MagicCANBus) -> [tuple]: """ return list(map(lambda x: (x.name, x.is_up), self.interfaces)) + def add_interface(self: MagicCANBus, interface: str) -> None: + """This will add an interface at runtime + + :param interface: The name of the interface to add + :type interface: string""" + + # Check if interface is already existing + interface_names = map(lambda x: str(x), self.interfaces) + if interface in interface_names: + return + + new_interface = Interface(interface) + self.interfaces.append(new_interface) + self.threads.append(self.start_handler(new_interface)) + def start_handler(self: MagicCANBus, iface: Interface) -> t.Thread: """This is a wrapper for starting a single interface listener thread @@ -51,7 +66,7 @@ def start_handler(self: MagicCANBus, iface: Interface) -> t.Thread: :rtype: threading.Thread """ tr = t.Thread(target=self.handler, - name=f'canopem-monitor-{iface.name}', + name=f'canopen-monitor-{iface.name}', args=[iface], daemon=True) tr.start() diff --git a/canopen_monitor/ui/__init__.py b/canopen_monitor/ui/__init__.py index 266c6f5..a254770 100755 --- a/canopen_monitor/ui/__init__.py +++ b/canopen_monitor/ui/__init__.py @@ -2,11 +2,12 @@ of Curses UI and general user interaction with the app, """ from .pane import Pane -from .windows import PopupWindow +from .windows import PopupWindow, InputForm from .message_pane import MessagePane __all__ = [ "Pane", "MessagePane", - "PopupWindow" + "PopupWindow", + "InputForm", ] diff --git a/canopen_monitor/ui/windows.py b/canopen_monitor/ui/windows.py index 825871f..e92a7f0 100755 --- a/canopen_monitor/ui/windows.py +++ b/canopen_monitor/ui/windows.py @@ -1,5 +1,6 @@ from __future__ import annotations import curses +import curses.ascii from .pane import Pane @@ -59,10 +60,12 @@ def break_lines(self: PopupWindow, length = len(line) mid = int(length / 2) - self.determine_to_break_content(content, i, length, line, max_width, mid) + self.determine_to_break_content(content, i, length, line, max_width, + mid) return content - def determine_to_break_content(self, content, i, length, line, max_width, mid): + def determine_to_break_content(self, content, i, length, line, max_width, + mid): if (length >= max_width): # Break the line at the next available space for j, c in enumerate(line[mid - 1:]): @@ -99,7 +102,7 @@ def __draw_content(self): self.add_line(1 + i, 2, line) def draw(self: PopupWindow) -> None: - if(self.enabled): + if (self.enabled): super().resize(self.v_height, self.v_width) super().draw() self.__draw_header() @@ -109,3 +112,71 @@ def draw(self: PopupWindow) -> None: else: # super().clear() ... + + +class InputForm(PopupWindow): + """ + Input form creates a popup window for retrieving + text input from the user + + :param parent: parent ui element + :type: any + :param header: header text of popup window + :type: str + :param footer: footer text of popup window + :type: str + :param style: style of window + :type: any + :param input_len: Maximum length of input text + :type: int + """ + def __init__(self: InputForm, + parent: any, + header: str = 'Alert', + footer: str = 'ESC: close', + style: any = None, + input_len: int = 30, + ): + + self.input_len = input_len + self.content = [" " * self.input_len] + super().__init__(parent, header, self.content, footer, style) + self.cursor_loc = 0 + + def read_input(self, keyboard_input: int) -> None: + """ + Read process keyboard input (ascii or backspace) + + :param keyboard_input: curses input character value from curses.getch + :type: int + """ + if curses.ascii.isalnum(keyboard_input) and \ + self.cursor_loc < self.input_len: + temp = list(self.content[0]) + temp[self.cursor_loc] = chr(keyboard_input) + self.content[0] = "".join(temp) + self.cursor_loc += 1 + elif keyboard_input == curses.KEY_BACKSPACE and self.cursor_loc > 0: + self.cursor_loc -= 1 + temp = list(self.content[0]) + temp[self.cursor_loc] = " " + self.content[0] = "".join(temp) + + def toggle(self: InputForm) -> bool: + """ + Toggle window and clear inserted text + :return: value indicating whether the window is enabled + :type: bool + """ + self.content = [" " * self.input_len] + self.cursor_loc = 0 + return super().toggle() + + def get_value(self) -> str: + """ + Get the value of user input without trailing spaces + :return: user input value + :type: str + """ + return self.content[0].strip() + From c039978e57cd353fb222359b0e75907a8ea2d4b8 Mon Sep 17 00:00:00 2001 From: Brian ONeill Date: Wed, 7 Apr 2021 10:22:36 -0700 Subject: [PATCH 2/5] Support for removing interfaces at runtime --- canopen_monitor/__main__.py | 2 +- canopen_monitor/app.py | 86 +++++++++++++++++----------- canopen_monitor/can/interface.py | 2 +- canopen_monitor/can/magic_can_bus.py | 51 ++++++++++++++--- canopen_monitor/ui/__init__.py | 5 +- canopen_monitor/ui/windows.py | 82 ++++++++++++++++++++++++-- scripts/socketcan-dev.py | 0 7 files changed, 178 insertions(+), 50 deletions(-) mode change 100644 => 100755 scripts/socketcan-dev.py diff --git a/canopen_monitor/__main__.py b/canopen_monitor/__main__.py index cd36bd2..10f2c5c 100755 --- a/canopen_monitor/__main__.py +++ b/canopen_monitor/__main__.py @@ -78,7 +78,7 @@ def main(): mt += message # User Input updates - app._handle_keyboard_input() + app.handle_keyboard_input() # Draw update app.draw(bus.statuses) diff --git a/canopen_monitor/app.py b/canopen_monitor/app.py index f04ce4b..43daba9 100644 --- a/canopen_monitor/app.py +++ b/canopen_monitor/app.py @@ -8,7 +8,7 @@ from . import APP_NAME, APP_VERSION, APP_LICENSE, APP_AUTHOR, APP_DESCRIPTION, \ APP_URL, CACHE_DIR from .can import MessageTable, MessageType, MagicCANBus -from .ui import MessagePane, PopupWindow, InputForm +from .ui import MessagePane, PopupWindow, InputPopup, SelectionPopup from .parse import eds # Key Constants not defined in curses @@ -56,6 +56,8 @@ class KeyMap(Enum): 'key': curses.KEY_F3} F4 = {'name': 'F4', 'description': 'Toggle add interface', 'key': curses.KEY_F4} + F5 = {'name': 'F5', 'description': 'Toggle remove interface', + 'key': curses.KEY_F5} UP_ARR = {'name': 'Up Arrow', 'description': 'Scroll pane up 1 row', 'key': curses.KEY_UP} DOWN_ARR = {'name': 'Down Arrow', 'description': 'Scroll pane down 1 row', @@ -121,6 +123,7 @@ def __init__(self: App, message_table: MessageTable, eds_configs: dict, KeyMap.F2.value['key']: self.f2, KeyMap.F3.value['key']: self.f3, KeyMap.F4.value['key']: self.f4, + KeyMap.F5.value['key']: self.f5, } def __enter__(self: App) -> App: @@ -164,10 +167,14 @@ def __enter__(self: App) -> App: list(KeyMap))), footer='F2: exit window', style=curses.color_pair(1)) - self.interface_win = InputForm(self.screen, - header='Add Interface', - footer='ENTER: save, F4: exit window', - style=curses.color_pair(1)) + self.add_if_win = InputPopup(self.screen, + header='Add Interface', + footer='ENTER: save, F4: exit window', + style=curses.color_pair(1)) + self.remove_if_win = SelectionPopup(self.screen, + header='Remove Interface', + footer='ENTER: remove, F5: exit window', + style=curses.color_pair(1)) self.hb_pane = MessagePane(cols={'Node ID': ('node_name', 0, hex), 'State': ('state', 0), 'Status': ('message', 0)}, @@ -197,6 +204,8 @@ def __enter__(self: App) -> App: name='Miscellaneous', message_table=self.table) self.__select_pane(self.hb_pane, 0) + self.popups = [self.hotkeys_win, self.info_win, self.add_if_win, + self.remove_if_win] return self def __exit__(self: App, type, value, traceback) -> None: @@ -286,20 +295,14 @@ def f1(self): Toggle app info menu :return: None """ - if self.hotkeys_win.enabled: - self.hotkeys_win.toggle() - self.hotkeys_win.clear() - self.info_win.toggle() + self.toggle_popup(self.info_win) def f2(self): """ Toggles KeyMap :return: None """ - if self.info_win.enabled: - self.info_win.toggle() - self.info_win.clear() - self.hotkeys_win.toggle() + self.toggle_popup(self.hotkeys_win) def f3(self): """ @@ -317,37 +320,55 @@ def f3(self): copy(filepath, CACHE_DIR) self.eds_configs[file.node_id] = file - def f4(self): + def f4(self) -> None: + """ + Toggles Add Interface Popup + :return: None + """ + self.toggle_popup(self.add_if_win) + + def f5(self) -> None: """ - Toggles Add Interface + Toggles Remove Interface Popup :return: None """ - if self.info_win.enabled: - self.info_win.toggle() - self.info_win.clear() + self.remove_if_win.content = self.bus.interface_list + self.toggle_popup(self.remove_if_win) - if self.hotkeys_win.enabled: - self.hotkeys_win.toggle() - self.hotkeys_win.clear() + def toggle_popup(self, selected_popup) -> None: + for popup in self.popups: + if popup != selected_popup and popup.enabled: + popup.toggle() + popup.clear() - self.interface_win.toggle() + selected_popup.toggle() - def _handle_keyboard_input(self: App) -> None: + def handle_keyboard_input(self: App) -> None: """ Retrieves keyboard input and calls the associated key function """ keyboard_input = self.screen.getch() curses.flushinp() - if self.interface_win.enabled: + if self.add_if_win.enabled: if keyboard_input == curses.KEY_ENTER or \ keyboard_input == 10 or keyboard_input == 13: - value = self.interface_win.get_value() + value = self.add_if_win.get_value() if value != "": self.bus.add_interface(value) - self.interface_win.toggle() + self.add_if_win.toggle() else: - self.interface_win.read_input(keyboard_input) + self.add_if_win.read_input(keyboard_input) + + elif self.remove_if_win.enabled: + if keyboard_input == curses.KEY_ENTER or \ + keyboard_input == 10 or keyboard_input == 13: + value = self.remove_if_win.get_value() + if value != "": + self.bus.remove_interface(value) + self.remove_if_win.toggle() + else: + self.remove_if_win.read_input(keyboard_input) try: self.key_dict[keyboard_input]() @@ -408,7 +429,7 @@ def __draw__footer(self: App) -> None: """ height, width = self.screen.getmaxyx() footer = ': Info, : Hotkeys, : Add OD File, ' \ - ': Add Interface' + ': Add Interface, Remove Interface' self.screen.addstr(height - 1, 1, footer) def draw(self: App, ifaces: [tuple]) -> None: @@ -417,8 +438,8 @@ def draw(self: App, ifaces: [tuple]) -> None: :param ifaces: CAN Bus Interfaces :return: None """ - window_active = self.info_win.enabled or self.hotkeys_win.enabled or \ - self.interface_win.enabled + window_active = any(popup.enabled for popup in self.popups) + self.screen.refresh() self.__draw_header(ifaces) # Draw header info # Draw panes @@ -427,9 +448,8 @@ def draw(self: App, ifaces: [tuple]) -> None: self.misc_pane.draw() # Draw windows - self.info_win.draw() - self.hotkeys_win.draw() - self.interface_win.draw() + for popup in self.popups: + popup.draw() self.__draw__footer() diff --git a/canopen_monitor/can/interface.py b/canopen_monitor/can/interface.py index ca5b9ae..127fa6d 100644 --- a/canopen_monitor/can/interface.py +++ b/canopen_monitor/can/interface.py @@ -101,7 +101,7 @@ def restart(self: Interface) -> None: >>> iface.start() """ self.stop() - self.start() + self.start(False) def recv(self: Interface) -> Message: """A wrapper for `pyvit.hw.SocketCanDev.recv()` diff --git a/canopen_monitor/can/magic_can_bus.py b/canopen_monitor/can/magic_can_bus.py index db30f18..e12772d 100644 --- a/canopen_monitor/can/magic_can_bus.py +++ b/canopen_monitor/can/magic_can_bus.py @@ -18,6 +18,7 @@ def __init__(self: MagicCANBus, if_names: [str], no_block: bool = False): self.no_block = no_block self.keep_alive = t.Event() self.keep_alive.set() + self.keep_alive_list = dict() self.message_queue = queue.SimpleQueue() self.threads = [] @@ -39,14 +40,47 @@ def add_interface(self: MagicCANBus, interface: str) -> None: :type interface: string""" # Check if interface is already existing - interface_names = map(lambda x: str(x), self.interfaces) + interface_names = self.interface_list if interface in interface_names: return + self.keep_alive_list[interface] = t.Event() + self.keep_alive_list[interface].set() + new_interface = Interface(interface) self.interfaces.append(new_interface) self.threads.append(self.start_handler(new_interface)) + def remove_interface(self: MagicCANBus, interface: str) -> None: + """This will add an interface at runtime + + :param interface: The name of the interface to add + :type interface: string""" + count = len(self.interfaces) + # Check if interface exists + interface_names = self.interface_list + if interface not in interface_names: + return + + self.keep_alive_list[interface].clear() + for thread in self.threads: + if thread.name == f'canopen-monitor-{interface}': + thread.join() + self.threads.remove(thread) + + for existing_interface in self.interfaces: + if str(existing_interface) == interface: + self.interfaces.remove(existing_interface) + assert(len(self.interfaces) == count - 1) + + @property + def interface_list(self: MagicCANBus) -> [str]: + """A list of strings representing all interfaces + :return: a list of strings indicating the name of each interface + :rtype: [str] + """ + return list(map(lambda x: str(x), self.interfaces)) + def start_handler(self: MagicCANBus, iface: Interface) -> t.Thread: """This is a wrapper for starting a single interface listener thread @@ -82,25 +116,26 @@ def handler(self: MagicCANBus, iface: Interface) -> None: :param iface: The interface to bind to when listening for messages :type iface: Interface """ - iface.start() # The outer loop exists to enable interface recovery, if the interface # is either deleted or goes down, the handler will try to start it # again and read messages as soon as possible - while(self.keep_alive.is_set()): + while (self.keep_alive.is_set() and + self.keep_alive_list[str(iface)].is_set()): try: # The inner loop is the constant reading of the bus and loading # of frames into a thread-safe queue. It is necessary to # check `iface.is_up` in the inner loop as well, so that the # handler will not block on bus reading if the MCB is trying # to close all threads and destruct itself - while(self.keep_alive.is_set() and iface.is_up): + while (self.keep_alive.is_set() and iface.is_up and iface.running + and self.keep_alive_list[str(iface)].is_set()): frame = iface.recv() - if(frame is not None): + if (frame is not None): self.message_queue.put(frame, block=True) iface.restart() except OSError: - iface.restart() + pass iface.stop() def __enter__(self: MagicCANBus) -> MagicCANBus: @@ -113,7 +148,7 @@ def __exit__(self: MagicCANBus, evalue: str, traceback: any) -> None: self.keep_alive.clear() - if(self.no_block): + if (self.no_block): print('WARNING: Skipping wait-time for threads to close' ' gracefully.') else: @@ -127,7 +162,7 @@ def __iter__(self: MagicCANBus) -> MagicCANBus: return self def __next__(self: MagicCANBus) -> Message: - if(self.message_queue.empty()): + if (self.message_queue.empty()): raise StopIteration return self.message_queue.get(block=True) diff --git a/canopen_monitor/ui/__init__.py b/canopen_monitor/ui/__init__.py index a254770..e82beb4 100755 --- a/canopen_monitor/ui/__init__.py +++ b/canopen_monitor/ui/__init__.py @@ -2,12 +2,13 @@ of Curses UI and general user interaction with the app, """ from .pane import Pane -from .windows import PopupWindow, InputForm +from .windows import PopupWindow, InputPopup, SelectionPopup from .message_pane import MessagePane __all__ = [ "Pane", "MessagePane", "PopupWindow", - "InputForm", + "InputPopup", + "SelectionPopup", ] diff --git a/canopen_monitor/ui/windows.py b/canopen_monitor/ui/windows.py index e92a7f0..dadc6c4 100755 --- a/canopen_monitor/ui/windows.py +++ b/canopen_monitor/ui/windows.py @@ -1,6 +1,7 @@ from __future__ import annotations import curses import curses.ascii + from .pane import Pane @@ -114,7 +115,7 @@ def draw(self: PopupWindow) -> None: ... -class InputForm(PopupWindow): +class InputPopup(PopupWindow): """ Input form creates a popup window for retrieving text input from the user @@ -130,7 +131,8 @@ class InputForm(PopupWindow): :param input_len: Maximum length of input text :type: int """ - def __init__(self: InputForm, + + def __init__(self: InputPopup, parent: any, header: str = 'Alert', footer: str = 'ESC: close', @@ -139,8 +141,8 @@ def __init__(self: InputForm, ): self.input_len = input_len - self.content = [" " * self.input_len] - super().__init__(parent, header, self.content, footer, style) + content = [" " * self.input_len] + super().__init__(parent, header, content, footer, style) self.cursor_loc = 0 def read_input(self, keyboard_input: int) -> None: @@ -162,7 +164,7 @@ def read_input(self, keyboard_input: int) -> None: temp[self.cursor_loc] = " " self.content[0] = "".join(temp) - def toggle(self: InputForm) -> bool: + def toggle(self: InputPopup) -> bool: """ Toggle window and clear inserted text :return: value indicating whether the window is enabled @@ -180,3 +182,73 @@ def get_value(self) -> str: """ return self.content[0].strip() + +class SelectionPopup(PopupWindow): + def __init__(self: InputPopup, + parent: any, + header: str = 'Alert', + footer: str = 'ESC: close', + style: any = None, + ): + content = [" " * 40] + super().__init__(parent, header, content, footer, style) + self.cursor_loc = 0 # TODO: Use scroll instead? + + def read_input(self, keyboard_input: int) -> None: + """ + Read process keyboard input (ascii or backspace) + + :param keyboard_input: curses input character value from curses.getch + :type: int + """ + if keyboard_input == curses.KEY_UP and self.cursor_loc > 0: + self.cursor_loc -= 1 + elif keyboard_input == curses.KEY_DOWN and self.cursor_loc < len( + self.content): + self.cursor_loc += 1 + + def __draw_header(self: SelectionPopup) -> None: + """Add the header line to the window""" + self.add_line(0, 1, self.header, underline=True) + + def __draw__footer(self: SelectionPopup) -> None: + """Add the footer to the window""" + f_width = len(self.footer) + 2 + self.add_line(self.v_height - 1, + self.v_width - f_width, + self.footer, + underline=True) + + def __draw_content(self: SelectionPopup) -> None: + """Read each line of the content and add to the window""" + for i, line in enumerate(self.content): + self.add_line(1 + i, 2, line, highlight=i == self.cursor_loc) + + def draw(self: SelectionPopup) -> None: + if (self.enabled): + super().resize(len(self.content)+2, self.v_width) + if self.needs_refresh: + self.refresh() + + self.parent.attron(self._style) + self._pad.attron(self._style) + + if (self.border): + self._pad.box() + self.__draw_header() + self.__draw_content() + self.__draw__footer() + super().refresh() + + def toggle(self: InputPopup) -> bool: + """ + Toggle window and reset selected item + :return: value indicating whether the window is enabled + :type: bool + """ + self.cursor_loc = 0 + self.clear() + return super().toggle() + + def get_value(self): + return self.content[self.cursor_loc] diff --git a/scripts/socketcan-dev.py b/scripts/socketcan-dev.py old mode 100644 new mode 100755 From 69a84e6007c547d34e097fc7d95285a3666e4c97 Mon Sep 17 00:00:00 2001 From: Brian ONeill Date: Fri, 9 Apr 2021 20:23:22 -0700 Subject: [PATCH 3/5] Minor cleanup changes for runtime interface changes --- canopen_monitor/app.py | 1 - canopen_monitor/can/magic_can_bus.py | 49 ++++++++++++++-------------- tests/spec_magic_can_bus.py | 2 +- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/canopen_monitor/app.py b/canopen_monitor/app.py index 43daba9..cd07631 100644 --- a/canopen_monitor/app.py +++ b/canopen_monitor/app.py @@ -439,7 +439,6 @@ def draw(self: App, ifaces: [tuple]) -> None: :return: None """ window_active = any(popup.enabled for popup in self.popups) - self.screen.refresh() self.__draw_header(ifaces) # Draw header info # Draw panes diff --git a/canopen_monitor/can/magic_can_bus.py b/canopen_monitor/can/magic_can_bus.py index e12772d..6e952a8 100644 --- a/canopen_monitor/can/magic_can_bus.py +++ b/canopen_monitor/can/magic_can_bus.py @@ -16,8 +16,6 @@ class MagicCANBus: def __init__(self: MagicCANBus, if_names: [str], no_block: bool = False): self.interfaces = list(map(lambda x: Interface(x), if_names)) self.no_block = no_block - self.keep_alive = t.Event() - self.keep_alive.set() self.keep_alive_list = dict() self.message_queue = queue.SimpleQueue() self.threads = [] @@ -33,6 +31,14 @@ def statuses(self: MagicCANBus) -> [tuple]: """ return list(map(lambda x: (x.name, x.is_up), self.interfaces)) + @property + def interface_list(self: MagicCANBus) -> [str]: + """A list of strings representing all interfaces + :return: a list of strings indicating the name of each interface + :rtype: [str] + """ + return list(map(lambda x: str(x), self.interfaces)) + def add_interface(self: MagicCANBus, interface: str) -> None: """This will add an interface at runtime @@ -44,20 +50,17 @@ def add_interface(self: MagicCANBus, interface: str) -> None: if interface in interface_names: return - self.keep_alive_list[interface] = t.Event() - self.keep_alive_list[interface].set() - new_interface = Interface(interface) self.interfaces.append(new_interface) self.threads.append(self.start_handler(new_interface)) def remove_interface(self: MagicCANBus, interface: str) -> None: - """This will add an interface at runtime + """This will remove an interface at runtime - :param interface: The name of the interface to add + :param interface: The name of the interface to remove :type interface: string""" - count = len(self.interfaces) - # Check if interface exists + + # Check if interface is already existing interface_names = self.interface_list if interface not in interface_names: return @@ -67,22 +70,16 @@ def remove_interface(self: MagicCANBus, interface: str) -> None: if thread.name == f'canopen-monitor-{interface}': thread.join() self.threads.remove(thread) + del self.keep_alive_list[interface] for existing_interface in self.interfaces: if str(existing_interface) == interface: self.interfaces.remove(existing_interface) - assert(len(self.interfaces) == count - 1) - - @property - def interface_list(self: MagicCANBus) -> [str]: - """A list of strings representing all interfaces - :return: a list of strings indicating the name of each interface - :rtype: [str] - """ - return list(map(lambda x: str(x), self.interfaces)) def start_handler(self: MagicCANBus, iface: Interface) -> t.Thread: """This is a wrapper for starting a single interface listener thread + This wrapper also creates a keep alive event for each thread which + can be used to kill the thread. .. warning:: @@ -99,6 +96,9 @@ def start_handler(self: MagicCANBus, iface: Interface) -> t.Thread: :return: The new listener thread spawned :rtype: threading.Thread """ + self.keep_alive_list[iface.name] = t.Event() + self.keep_alive_list[iface.name].set() + tr = t.Thread(target=self.handler, name=f'canopen-monitor-{iface.name}', args=[iface], @@ -120,16 +120,15 @@ def handler(self: MagicCANBus, iface: Interface) -> None: # The outer loop exists to enable interface recovery, if the interface # is either deleted or goes down, the handler will try to start it # again and read messages as soon as possible - while (self.keep_alive.is_set() and - self.keep_alive_list[str(iface)].is_set()): + while (self.keep_alive_list[iface.name].is_set()): try: # The inner loop is the constant reading of the bus and loading # of frames into a thread-safe queue. It is necessary to # check `iface.is_up` in the inner loop as well, so that the # handler will not block on bus reading if the MCB is trying # to close all threads and destruct itself - while (self.keep_alive.is_set() and iface.is_up and iface.running - and self.keep_alive_list[str(iface)].is_set()): + while (iface.is_up and iface.running and + self.keep_alive_list[iface.name].is_set()): frame = iface.recv() if (frame is not None): self.message_queue.put(frame, block=True) @@ -147,7 +146,8 @@ def __exit__(self: MagicCANBus, etype: str, evalue: str, traceback: any) -> None: - self.keep_alive.clear() + for keep_alive in self.keep_alive_list.values(): + keep_alive.clear() if (self.no_block): print('WARNING: Skipping wait-time for threads to close' ' gracefully.') @@ -172,5 +172,4 @@ def __str__(self: MagicCANBus) -> str: if_list = ', '.join(list(map(lambda x: str(x), self.interfaces))) return f"Magic Can Bus: {if_list}," \ f" pending messages: {self.message_queue.qsize()}" \ - f" threads: {alive_threads}," \ - f" keep-alive: {self.keep_alive.is_set()}" + f" threads: {alive_threads}" diff --git a/tests/spec_magic_can_bus.py b/tests/spec_magic_can_bus.py index a828487..56c3196 100644 --- a/tests/spec_magic_can_bus.py +++ b/tests/spec_magic_can_bus.py @@ -57,6 +57,6 @@ def test_str(self): of the bus """ expected = 'Magic Can Bus: vcan0, vcan1, pending messages:' \ - ' 0 threads: 0, keep-alive: True' + ' 0 threads: 0' actual = str(self.bus) self.assertEqual(expected, actual) From f22f24e3d88fb894bfd1c9ce6ca235f3b93f45d0 Mon Sep 17 00:00:00 2001 From: Brian ONeill Date: Mon, 19 Apr 2021 11:21:15 -0700 Subject: [PATCH 4/5] version bump 3.3.0 --- canopen_monitor/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/canopen_monitor/__init__.py b/canopen_monitor/__init__.py index 2a5e141..6a4401d 100755 --- a/canopen_monitor/__init__.py +++ b/canopen_monitor/__init__.py @@ -1,8 +1,8 @@ import os MAJOR = 3 -MINOR = 2 -PATCH = 3 +MINOR = 3 +PATCH = 0 APP_NAME = 'canopen-monitor' APP_DESCRIPTION = 'An NCurses-based TUI application for tracking activity' \ From 27351659c0f48a10ede5fe5eec1d1723429caa19 Mon Sep 17 00:00:00 2001 From: Brian ONeill Date: Mon, 19 Apr 2021 12:08:18 -0700 Subject: [PATCH 5/5] bugfix: allow selecting blank on remove interface --- canopen_monitor/ui/windows.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/canopen_monitor/ui/windows.py b/canopen_monitor/ui/windows.py index dadc6c4..00fa039 100755 --- a/canopen_monitor/ui/windows.py +++ b/canopen_monitor/ui/windows.py @@ -184,7 +184,20 @@ def get_value(self) -> str: class SelectionPopup(PopupWindow): - def __init__(self: InputPopup, + """ + Input form creates a popup window for selecting + from a list of options + + :param parent: parent ui element + :type: any + :param header: header text of popup window + :type: str + :param footer: footer text of popup window + :type: str + :param style: style of window + :type: any + """ + def __init__(self: SelectionPopup, parent: any, header: str = 'Alert', footer: str = 'ESC: close', @@ -192,9 +205,9 @@ def __init__(self: InputPopup, ): content = [" " * 40] super().__init__(parent, header, content, footer, style) - self.cursor_loc = 0 # TODO: Use scroll instead? + self.cursor_loc = 0 - def read_input(self, keyboard_input: int) -> None: + def read_input(self: SelectionPopup, keyboard_input: int) -> None: """ Read process keyboard input (ascii or backspace) @@ -204,7 +217,7 @@ def read_input(self, keyboard_input: int) -> None: if keyboard_input == curses.KEY_UP and self.cursor_loc > 0: self.cursor_loc -= 1 elif keyboard_input == curses.KEY_DOWN and self.cursor_loc < len( - self.content): + self.content) - 1: self.cursor_loc += 1 def __draw_header(self: SelectionPopup) -> None: @@ -251,4 +264,7 @@ def toggle(self: InputPopup) -> bool: return super().toggle() def get_value(self): + if self.cursor_loc >= len(self.content): + return "" + return self.content[self.cursor_loc]