Skip to content
This repository has been archived by the owner on Apr 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #56 from oresat/change_interfaces
Browse files Browse the repository at this point in the history
Change interfaces at runtime #42
  • Loading branch information
Boneill3 authored Apr 19, 2021
2 parents d7b5f3d + 2735165 commit 691fe2e
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 43 deletions.
4 changes: 2 additions & 2 deletions canopen_monitor/__init__.py
Original file line number Diff line number Diff line change
@@ -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' \
Expand Down
4 changes: 2 additions & 2 deletions canopen_monitor/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ 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:
if message is not None:
mt += message

# User Input updates
app._handle_keyboard_input()
app.handle_keyboard_input()

# Draw update
app.draw(bus.statuses)
Expand Down
93 changes: 75 additions & 18 deletions canopen_monitor/app.py
Original file line number Diff line number Diff line change
@@ -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, InputPopup, SelectionPopup
from .parse import eds

# Key Constants not defined in curses
Expand Down Expand Up @@ -53,6 +54,10 @@ 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}
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',
Expand Down Expand Up @@ -90,14 +95,16 @@ 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
:type MessageTable
"""
self.table = message_table
self.eds_configs = eds_configs
self.bus = bus
self.selected_pane_pos = 0
self.selected_pane = None
self.key_dict = {
Expand All @@ -114,7 +121,9 @@ 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,
KeyMap.F5.value['key']: self.f5,
}

def __enter__(self: App) -> App:
Expand All @@ -130,6 +139,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

Expand Down Expand Up @@ -157,6 +167,14 @@ def __enter__(self: App) -> App:
list(KeyMap))),
footer='F2: 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)},
Expand Down Expand Up @@ -186,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:
Expand All @@ -200,7 +220,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
Expand Down Expand Up @@ -275,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):
"""
Expand All @@ -306,14 +320,56 @@ def f3(self):
copy(filepath, CACHE_DIR)
self.eds_configs[file.node_id] = file

def f4(self) -> None:
"""
Toggles Add Interface Popup
:return: None
"""
self.toggle_popup(self.add_if_win)

def f5(self) -> None:
"""
Toggles Remove Interface Popup
:return: None
"""
self.remove_if_win.content = self.bus.interface_list
self.toggle_popup(self.remove_if_win)

def toggle_popup(self, selected_popup) -> None:
for popup in self.popups:
if popup != selected_popup and popup.enabled:
popup.toggle()
popup.clear()

def _handle_keyboard_input(self: App) -> None:
selected_popup.toggle()

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.add_if_win.enabled:
if keyboard_input == curses.KEY_ENTER or \
keyboard_input == 10 or keyboard_input == 13:
value = self.add_if_win.get_value()
if value != "":
self.bus.add_interface(value)
self.add_if_win.toggle()
else:
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]()
except KeyError:
Expand Down Expand Up @@ -372,7 +428,8 @@ def __draw__footer(self: App) -> None:
:return: None
"""
height, width = self.screen.getmaxyx()
footer = '<F1>: Info, <F2>: Hotkeys, <F3>: Add OD File'
footer = '<F1>: Info, <F2>: Hotkeys, <F3>: Add OD File, ' \
'<F4>: Add Interface, <F5> Remove Interface'
self.screen.addstr(height - 1, 1, footer)

def draw(self: App, ifaces: [tuple]) -> None:
Expand All @@ -381,7 +438,7 @@ 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 = any(popup.enabled for popup in self.popups)
self.__draw_header(ifaces) # Draw header info

# Draw panes
Expand All @@ -390,8 +447,8 @@ def draw(self: App, ifaces: [tuple]) -> None:
self.misc_pane.draw()

# Draw windows
self.info_win.draw()
self.hotkeys_win.draw()
for popup in self.popups:
popup.draw()

self.__draw__footer()

Expand Down
2 changes: 1 addition & 1 deletion canopen_monitor/can/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down
77 changes: 63 additions & 14 deletions canopen_monitor/can/magic_can_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ 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 = None
self.threads = []

@property
def statuses(self: MagicCANBus) -> [tuple]:
Expand All @@ -32,8 +31,55 @@ 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
:param interface: The name of the interface to add
:type interface: string"""

# Check if interface is already existing
interface_names = self.interface_list
if interface in interface_names:
return

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 remove an interface at runtime
:param interface: The name of the interface to remove
:type interface: string"""

# Check if interface is already existing
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)
del self.keep_alive_list[interface]

for existing_interface in self.interfaces:
if str(existing_interface) == interface:
self.interfaces.remove(existing_interface)

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::
Expand All @@ -50,8 +96,11 @@ 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'canopem-monitor-{iface.name}',
name=f'canopen-monitor-{iface.name}',
args=[iface],
daemon=True)
tr.start()
Expand All @@ -67,25 +116,25 @@ 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_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):
while (iface.is_up and iface.running and
self.keep_alive_list[iface.name].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:
Expand All @@ -97,8 +146,9 @@ def __exit__(self: MagicCANBus,
etype: str,
evalue: str,
traceback: any) -> None:
self.keep_alive.clear()
if(self.no_block):
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.')
else:
Expand All @@ -112,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)

Expand All @@ -122,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}"
Loading

0 comments on commit 691fe2e

Please sign in to comment.