diff --git a/project/__init__.py b/project/__init__.py index cdb1313..4fd5954 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -1,10 +1,12 @@ +from project.controller.maincontroller import MainController from .view import MainUI -from .controller import SerialController +from .model import MainModel class Project: def main(self): - serialPort = SerialController() - mainWindow = MainUI('Porty', serialPort) + mainModel = MainModel() + mainController = MainController(mainModel) + mainWindow = MainUI('Porty', mainController) mainWindow.launch() \ No newline at end of file diff --git a/project/controller/__init__.py b/project/controller/__init__.py index 0c41a8d..67dba8f 100644 --- a/project/controller/__init__.py +++ b/project/controller/__init__.py @@ -1 +1,2 @@ -from .serialcontroller import SerialController \ No newline at end of file +from .serialcontroller import SerialController +from .maincontroller import MainController \ No newline at end of file diff --git a/project/controller/maincontroller.py b/project/controller/maincontroller.py new file mode 100644 index 0000000..722cd88 --- /dev/null +++ b/project/controller/maincontroller.py @@ -0,0 +1,43 @@ + +from project.controller.serialcontroller import SerialController +from project.model.main import MainModel + + +class MainController(): + def __init__(self, model:MainModel) -> None: + self._mainModel = model + self._serialController = SerialController(self._mainModel) + pass + + def set_device_port(self, comPort:str): + self._serialController.set_comport(comPortName = comPort, conf=self._mainModel.get_all_port_settings()) + self._mainModel.update_port_name(comPort) + def connect_to_device(self): + self._serialController.connect() + def disconnect_from_device(self): + self._serialController.disconnect() + def send_packet(self, msg): + self._serialController.send_packet(msg) + def update_port_baudrate(self, baudrate): + self._mainModel.update_setting_baudrate(baudrate) + self._serialController.set_comport(comPortName = self._mainModel.get_port_name(), conf=self._mainModel.get_all_port_settings()) + def update_serial_cb(self, handlePacketCb, connectionCb, disconnectionCb): + self._serialController.handle_packet = handlePacketCb + self._serialController.connection_callback = connectionCb + self._serialController.disconnection_callback = disconnectionCb + + + def save_project_settings(self, filename:str): + self._mainModel.save_project_file(filename) + def open_project_settings(self, filename:str): + self._mainModel.load_project_file(filename) + + def update_send_sequence(self, sequence:list): + self._mainModel.update_send_sequence(sequence) + def get_send_sequences(self)->list: + return self._mainModel.get_sequences() + def get_saved_port_name(self)->str: + return self._mainModel.get_port_name() + + def list_serial_ports(self) -> list: + return self._serialController.list_serial_ports() \ No newline at end of file diff --git a/project/controller/serialcontroller.py b/project/controller/serialcontroller.py index 2a8988c..d57daaf 100644 --- a/project/controller/serialcontroller.py +++ b/project/controller/serialcontroller.py @@ -2,40 +2,54 @@ from serial import Serial, SerialException from serial.threaded import Protocol, ReaderThread from serial.tools import list_ports + +from project.model.main import MainModel class FixedLengthPacketHandler(Protocol): """\ Modified Protocol as used by the ReaderThread. """ PACKET_LENGTH = 256 + isConnected:bool def __init__(self, controller): super().__init__() self.buffer = bytearray() self.controller = controller self.transport = None + self.isConnected = False def connection_made(self, transport): """Called when reader thread is started""" super(FixedLengthPacketHandler, self).connection_made(transport) - print(f'Serial connection made for {transport.serial.name}') self.transport = transport self.buffer = bytearray() + self.isConnected = True if self.controller and self.controller.connection_callback: self.controller.connection_callback() + def chunk_and_handle_packets(self, big_buff:bytearray)->bytearray: + small_buff = big_buff[:self.PACKET_LENGTH] + big_buff = big_buff[self.PACKET_LENGTH:] + if self.controller and self.controller.handle_packet: + convhex = " ".join(["{:02x}".format(bytes) for bytes in small_buff]) + self.controller.handle_packet('[RX]: ' + convhex.upper()) + while len(big_buff)>=len(small_buff): + small_buff = big_buff[:self.PACKET_LENGTH] + big_buff = big_buff[self.PACKET_LENGTH:] + if self.controller and self.controller.handle_packet: + convhex = " ".join(["{:02x}".format(bytes) for bytes in small_buff]) + self.controller.handle_packet('[RX]: ' + convhex.upper()) + return big_buff + def data_received(self, data): """\ Called when data is received from the serial port Append bytes till buffer is full and handle the packet. """ self.buffer.extend(data) + out = " ".join(["{:02x}".format(bytes) for bytes in data]) if len(self.buffer) >= self.PACKET_LENGTH: - temp_buff = self.buffer[:self.PACKET_LENGTH] - self.buffer = self.buffer[self.PACKET_LENGTH:] - # TODO: Handle this - if self.controller and self.controller.handle_packet: - convhex = " ".join(["{:02x}".format(bytes) for bytes in temp_buff]) - self.controller.handle_packet('[RX]: ' + convhex.upper()) + self.buffer=self.chunk_and_handle_packets(self.buffer) def send_data(self, data): @@ -50,9 +64,9 @@ def connection_lost(self, exc): Called when the serial port is closed or the reader loop terminated otherwise. """ - print(f'Connection Lost') self.transport = None self.buffer = bytearray() + self.isConnected = False if self.controller and self.controller.disconnection_callback: self.controller.disconnection_callback() if isinstance(exc, Exception): @@ -73,34 +87,37 @@ class SerialController: _rt:ReaderThread _proto:FixedLengthPacketHandler - def __init__(self, comPortName:str = None, conf:dict = None) -> None: + def __init__(self, model:MainModel) -> None: self._sp = None + self._proto = None + self._model= model + conf = self._model.get_all_port_settings() + print(conf) if conf: for setting in conf: if setting in self._conf: self._conf[setting]=conf[setting] else: - raise ValueError(f'Unsupported or invalid setting {setting}') - - if comPortName: - self.set_comport(comPortName) - + raise ValueError(f'Unsupported or invalid setting {setting}') self.connection_callback = lambda : print('connection_callback') self.disconnection_callback = lambda : print('disconnection_callback') self.handle_packet = lambda : print('handle_packet') - def set_comport(self, comPortName): + def set_comport(self, comPortName:str, conf:dict = None): '''\ Set comport ''' availablePorts = self.list_serial_ports() if comPortName in availablePorts: if self._sp: - self.disconnect() + if self._sp.is_open: + self.disconnect() self._sp = None self._selectedPortName = comPortName try: + if conf: + self._conf = conf self._sp = Serial(self._selectedPortName) self._sp.baudrate = self._conf['baudrate'] self._sp.bytesize=self._conf['bytesize'] @@ -113,7 +130,7 @@ def set_comport(self, comPortName): except SerialException as e: print(f'Cannot open COM Port:{self._selectedPortName}, {e}') else: - raise ValueError('Port {comPortName} doesn\'t exist') + raise ValueError(f'Port {comPortName} doesn\'t exist') def connect(self): '''\ @@ -128,7 +145,7 @@ def disconnect(self): '''\ Attempts to disconnect to the specified serial port ''' - if self._rt is not None: + if self._proto is not None: try: self._rt.close() except Exception as e: @@ -151,6 +168,7 @@ def send_packet(self, msg): def serial_packet_handler(self): self._proto = FixedLengthPacketHandler(self) return self._proto + @staticmethod def list_serial_ports() -> list: @@ -166,7 +184,8 @@ def list_serial_ports() -> list: TEST_PORT = 'COM9' ports = SerialController.list_serial_ports() portSetting = {'baudrate':115200} - sPort = SerialController(TEST_PORT, portSetting) + sPort = SerialController() + sPort.set_comport(TEST_PORT) with sPort as device: sleep(1) print('End') diff --git a/project/model/__init__.py b/project/model/__init__.py index e69de29..1b7fb60 100644 --- a/project/model/__init__.py +++ b/project/model/__init__.py @@ -0,0 +1 @@ +from .main import MainModel \ No newline at end of file diff --git a/project/model/main.py b/project/model/main.py new file mode 100644 index 0000000..f8ee322 --- /dev/null +++ b/project/model/main.py @@ -0,0 +1,63 @@ +import json +from project.util import valuemodifier as UVM +class MainModel(): + def __init__(self) -> None: + self._portName='' + self._portSettings = { + 'baudrate':115200, + 'bytesize':8, + 'parity':'N', + 'stopbits':1, + 'timeout':1, + 'xonxoff':False, + 'rtscts':False + } + self._sendSequence=[] + + def get_all_port_settings(self)->dict: + return self._portSettings + + def update_port_name(self, portName:str): + self._portName = portName + def get_port_name(self)->str: + return self._portName + def update_setting_baudrate(self, baudrate:int): + self._portSettings['baudrate']=UVM.return_as_int(baudrate) + def update_setting_bytesize(self, bytesize:int): + self._portSettings['bytesize'] = UVM.return_as_int(bytesize) + def update_setting_parity(self, parity): + self._portSettings['parity'] = parity + def update_setting_stopbits(self, stopbits:int): + self._portSettings['stopbits'] = UVM.return_as_int(stopbits) + def update_setting_timeout(self, timeout:int): + self._portSettings['timeout'] = UVM.return_as_int(timeout) + def update_setting_xonxoff(self, xonxoff:bool): + self._portSettings['xonxoff'] = xonxoff + def update_setting_rtscts(self, rtscts:bool): + self._portSettings['rtscts'] = rtscts + + def update_send_sequence(self, sequence:list)->None: + self._sendSequence = sequence + def get_sequences(self)->list: + return self._sendSequence + + def save_project_file(self, filepath:str): + outData = { + 'port':self._portName, + 'portSetting':self._portSettings, + 'sequences':self._sendSequence + } + with open(filepath, "w+") as outfile: + json.dump(outData, outfile) + + def load_project_file(self, filepath:str)->bool: + projectSetting={} + with open(filepath, 'r') as projectFile: + projectSetting = json.load(projectFile) + if 'sequences' in projectSetting: + self._sendSequence = projectSetting['sequences'] + if 'port' in projectSetting: + self._portName = projectSetting['port'] + if 'portSetting' in projectSetting: + self._portSettings = projectSetting['portSetting'] + return True \ No newline at end of file diff --git a/project/util/valuemodifier.py b/project/util/valuemodifier.py new file mode 100644 index 0000000..d8fc999 --- /dev/null +++ b/project/util/valuemodifier.py @@ -0,0 +1,8 @@ + +def return_as_int(value)->int: + if type(value)==str: + return int(value) + elif type(value)==int: + return value + else: + raise TypeError(f'Incorrect Baudrate type {type(value)}') \ No newline at end of file diff --git a/project/view/main.py b/project/view/main.py index 093df9a..4f9f2bd 100644 --- a/project/view/main.py +++ b/project/view/main.py @@ -1,7 +1,6 @@ -from faulthandler import disable -from operator import contains import PySimpleGUI as gui from project.controller.serialcontroller import SerialController +from project.controller.maincontroller import MainController class MainUI: KEY_MSG_LIST = "MSG_LIST" KEY_PORT_LIST = "PORT_LIST" @@ -24,16 +23,22 @@ class MainUI: EV_MENU_QUIT = 'Quit' EV_MENU_ABOUT = '&About' EV_MENU_BAUDRATE_9600 = '9600' + EV_MENU_BAUDRATE_19200 = '19200' + EV_MENU_BAUDRATE_38400 = '38400' + EV_MENU_BAUDRATE_57600 = '57600' EV_MENU_BAUDRATE_115200 = '115200' + BAUD_RATES = [EV_MENU_BAUDRATE_9600, EV_MENU_BAUDRATE_19200, EV_MENU_BAUDRATE_38400, + EV_MENU_BAUDRATE_57600, EV_MENU_BAUDRATE_115200 ] MENU_DEF = [['&File', [EV_MENU_OPEN_PROJECT, EV_MENU_SAVE_PROJECT, EV_MENU_SAVE_AS_PROJECT, EV_MENU_QUIT ]], - ['&Edit', ['&Baudrate', [EV_MENU_BAUDRATE_9600, EV_MENU_BAUDRATE_115200, ], '&Encoding',['bytes','string']], ], + ['&Settings', ['&Baudrate', BAUD_RATES, + '&Encoding',['bytes','string']], ], ['&Help', EV_MENU_ABOUT], ] APPEND_TX_MSG = "[TX]: " LEFT_COLUMN_WIDTH = 40 RIGHT_COLUMN_WIDTH = 80 - _controller: SerialController + _controller: MainController def __init__(self, title:str, controller) -> None: self._controller = controller self._msgList = [] @@ -67,7 +72,7 @@ def __init__(self, title:str, controller) -> None: ] # ----- Full layout ----- self._layout = [ - [gui.Menu(self.MENU_DEF)], + [gui.Menu(self.MENU_DEF, key='menu')], [gui.Column(conf_column, expand_x=True, expand_y=True), gui.VSeperator(), gui.Column(console_column, expand_x=True, expand_y=True),] @@ -75,34 +80,37 @@ def __init__(self, title:str, controller) -> None: self._ui = gui.Window(title, self._layout, resizable=True) gui.cprint_set_output_destination(self._ui, self.KEY_CONSOLE) self._isConnected = False - + + def set_port(self, portName:str): + self._controller.set_device_port(portName) + self._ui[self.KEY_PORT_LIST].update(value=portName) + def refresh_port_list(self): portList=self._controller.list_serial_ports() self._ui[self.KEY_PORT_LIST].update(values=portList, set_to_index=0) if len(portList) != 0: - self._controller.set_comport(self._ui[self.KEY_PORT_LIST].get()) + self.set_port(self._ui[self.KEY_PORT_LIST].get()) def open_port_connection(self): self._ui[self.KEY_PORT_LIST].update(disabled=True) self._ui[self.KEY_OPEN_PORT].update(text='Opening') self._ui.read(timeout=1) - self._controller.connect() + self._controller.connect_to_device() def port_connected_cb(self): self._ui[self.KEY_SEND_MSG_BTN].update(disabled=False) self._isConnected = True + self.enable_menu_item('&Settings') def close_port_connection(self): self._ui[self.KEY_OPEN_PORT].update(text='Closing') self._ui.read(timeout=1) - self._controller.disconnect() + self._controller.disconnect_from_device() def port_disconnected_cb(self): - # self._ui[self.KEY_PORT_LIST].update(disabled=False) - # self._ui[self.KEY_OPEN_PORT].update(text='Open') - # self._ui[self.KEY_SEND_MSG_BTN].update(disabled=True) self._isConnected = False self.refresh_port_list() + self.enable_menu_item('!&Settings', False) def update_msg_list(self, msg, append:bool): if append: @@ -112,6 +120,7 @@ def update_msg_list(self, msg, append:bool): if msg in self._msgList: self._msgList.remove(msg) self._ui[self.KEY_MSG_LIST].update(values=self._msgList) + self._controller.update_send_sequence(self._msgList) def update_send_msg_input(self, msg): if msg is not None: @@ -134,67 +143,90 @@ def update_console(self, msg): def about_popup(self): gui.popup('Serial Port Gui Tool', 'Sahil Khanna', 'https://github.com/sahilkhanna/sp-ui-tool', grab_anywhere=True) + def popup_error(self, msg:str): + gui.popup_error(msg, title="Uh Oh!") + + def enable_menu_item(self, menuElement, enable=True): + for idx, el in enumerate(self.MENU_DEF): + if menuElement in el: + if enable: + self.MENU_DEF[idx][0] = '!' + self.MENU_DEF[idx][0] + else: + self.MENU_DEF[idx][0]=self.MENU_DEF[idx][0].replace('!','') + self._ui['menu'].update(self.MENU_DEF) + break + + # print(self.MENU_DEF[idx], idx, menuElement) + # # self.MENU_DEF[idx]='!'+self.MENU_DEF[idx] + # break def open_project_file(self): filename = gui.popup_get_file('file to open', file_types=(( 'Porty Project (.prtyprj)','.prtyprj'),), no_window=True) - print(filename) + self._controller.open_project_settings(filename) + self._msgList = self._controller.get_send_sequences() + self._ui[self.KEY_MSG_LIST].update(values=self._msgList) + self.set_port(self._controller.get_saved_port_name()) def saveas_project_file(self): filename = gui.popup_get_file('file to Save', file_types=(( 'Porty Project (.prtyprj)','.prtyprj'),), save_as=True,no_window=True) - print(filename) + self._controller.save_project_settings(filename) def _debug_print_var(self, var): print(f'{var}') def launch(self): # Setup Callbacks for connections - self._controller.handle_packet = self.update_console - self._controller.connection_callback = self.port_connected_cb - self._controller.disconnection_callback = self.port_disconnected_cb + self._controller.update_serial_cb(self.update_console, self.port_connected_cb, self.port_disconnected_cb) event, values = self._ui.read(timeout=10) self.refresh_port_list() while True: - event, values = self._ui.read(timeout=10) - if event in [gui.WIN_CLOSED, self.EV_MENU_QUIT]: - break - elif event == self.KEY_REFRESH_PORT_LIST: - self.refresh_port_list() - elif event == self.KEY_PORT_LIST: - self._controller.set_comport(values[self.KEY_PORT_LIST]) - elif event == self.KEY_OPEN_PORT: + try: + event, values = self._ui.read(timeout=10) + if event in [gui.WIN_CLOSED, self.EV_MENU_QUIT]: + break + elif event == self.KEY_REFRESH_PORT_LIST: + self.refresh_port_list() + elif event == self.KEY_PORT_LIST: + self.set_port(values[self.KEY_PORT_LIST]) + elif event == self.KEY_OPEN_PORT: + if self._isConnected: + self.close_port_connection() + else: + self.open_port_connection() + elif event == self.KEY_CLEAR_TERMINAL: + self._ui[self.KEY_CONSOLE].update(value='') + elif event == self.KEY_ADD_MSG_BTN: + self.update_msg_list(values[self.KEY_SEND_MSG_INPUT], True) + elif event == self.KEY_REMOVE_MSG_BTN: + self.update_msg_list(values[self.KEY_SEND_MSG_INPUT], False) + elif event == self.KEY_SEND_MSG_BTN: + self.send_msg(values[self.KEY_SEND_MSG_INPUT]) + elif event == self.KEY_MSG_LIST: + try: + self.update_send_msg_input(values[self.KEY_MSG_LIST][0]) + except IndexError: + pass + elif event == self.EV_MENU_OPEN_PROJECT: + self.open_project_file() + elif event == self.EV_MENU_SAVE_AS_PROJECT: + self.saveas_project_file() + elif event == self.EV_MENU_ABOUT: + self.about_popup() + elif event in self.BAUD_RATES: + self._controller.update_port_baudrate(event) + # self._debug_print_var((event, values)) if self._isConnected: - self.close_port_connection() + self._ui[self.KEY_PORT_LIST].update(disabled=True) + self._ui[self.KEY_OPEN_PORT].update(text='Close') + self._ui[self.KEY_SEND_MSG_BTN].update(disabled=False) else: - self.open_port_connection() - elif event == self.KEY_CLEAR_TERMINAL: - self._ui[self.KEY_CONSOLE].update(value='') - elif event == self.KEY_ADD_MSG_BTN: - self.update_msg_list(values[self.KEY_SEND_MSG_INPUT], True) - elif event == self.KEY_REMOVE_MSG_BTN: - self.update_msg_list(values[self.KEY_SEND_MSG_INPUT], False) - elif event == self.KEY_SEND_MSG_BTN: - self.send_msg(values[self.KEY_SEND_MSG_INPUT]) - elif event == self.KEY_MSG_LIST: - try: - self.update_send_msg_input(values[self.KEY_MSG_LIST][0]) - except IndexError: - pass - elif event == self.EV_MENU_OPEN_PROJECT: - self.open_project_file() - elif event == self.EV_MENU_SAVE_AS_PROJECT: - self.save_project_file() - elif event == self.EV_MENU_ABOUT: - self.about_popup() - elif event == self.EV_MENU_BAUDRATE_9600: - self._debug_print_var((event, values)) - if self._isConnected: - self._ui[self.KEY_PORT_LIST].update(disabled=True) - self._ui[self.KEY_OPEN_PORT].update(text='Close') - self._ui[self.KEY_SEND_MSG_BTN].update(disabled=False) - else: - self._ui[self.KEY_PORT_LIST].update(disabled=False) - self._ui[self.KEY_OPEN_PORT].update(text='Open') - self._ui[self.KEY_SEND_MSG_BTN].update(disabled=True) + self._ui[self.KEY_PORT_LIST].update(disabled=False) + self._ui[self.KEY_OPEN_PORT].update(text='Open') + self._ui[self.KEY_SEND_MSG_BTN].update(disabled=True) + except Exception as err: + print(err) + self.popup_error(err) + # Finish up by removing from the screen self._ui.close() \ No newline at end of file