def do_send_hex(data: str): """ Send raw hex to serial device, ignoring spaces \\x and 0x. :PARAM: data: Raw hex to send. """ assert ( ctserial.session ), "There is not an open session. Connect to one first." # ToDo assert session type raw_hex = data.lower().replace("0x", "").replace("\\x", "").replace(" ", "") assert re.match("^[0123456789abcdef]+$", raw_hex), "Only hex characters allowed" # assert len(raw_hex) % 2 != 0, "You must send an even number of hex characters" tx_bytes = bytes.fromhex(raw_hex) ctserial.views["transactions"].append([ "-->", common.bytes2hexstr(tx_bytes, group=8, sep=" ", line=35), common.bytes_decode(tx_bytes), ]) rx_bytes = common.send_instruction(ctserial.session, tx_bytes) if rx_bytes: ctserial.views["transactions"].append([ "<--", common.bytes2hexstr(rx_bytes, group=8, sep=" ", line=35), common.bytes_decode(rx_bytes), ]) else: message_dialog("ERROR", "No response received") return tabulate(ctserial.views["transactions"], tablefmt="plain", headers="firstrow")
def do_connect(self, args, output_text): """Generate a session with a single serial device to interact with it.""" parts = args.split() devices = [x.device for x in serial.tools.list_ports.comports()] if len(parts) > 0: device = parts[0] if len(parts) > 1: baudrate = parts[1] else: baudrate = 9600 if device in devices: self.session = serial.Serial(port=device, baudrate=baudrate, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS) # initiate a serial session and return success message self.session.isOpen() output_text += 'Connect session opened with {}\n'.format( device) return output_text # return list of devices if command incomplete or incorrect message = 'Valid devices:\n' + ', \n'.join(devices) + '\n' message_dialog(title='Invalid Device', text=message) return False
def do_connect(device: str = "/dev/null/", baudrate: int = 9600, parity: str = "none"): """ Connect to a serial device :PARAM: device: Serial device path or COM port :PARAM: baudrate: Defaults to 9600. Most standard rates from 75 to 4000000 supported. :PARAM: parity: Defaults to none. Can also be even, odd, mark, or space """ assert ( ctserial.session == None ), "Session already open. Close first." # ToDo assert session type valid_device = common.validate_serial_device(device) parities = { "none": PARITY_NONE, "even": PARITY_EVEN, "odd": PARITY_ODD, "mark": PARITY_MARK, "space": PARITY_SPACE, } assert (parity in parities.keys() ), "Parity must be none (default), even, odd, mark, or space" s = Serial( port=valid_device, baudrate=baudrate, parity=parities[parity], stopbits=STOPBITS_ONE, bytesize=EIGHTBITS, ) assert s.isOpen(), f"Could not connect to {valid_device}" ctserial.session = s message_dialog("SUCCESS", f"ASCII session OPENED with {valid_device}") return
def do_write_coils(self, args, output_text): """Write to registers in format: <address> <int>...""" desc = 'Modbus Function 3, Read Holding Registers' start, values = args.split(maxsplit=1) assert (start.isdigit()), 'start must be an integer' int_start = int(start) try: list_bool_values = [bool(int(x)) for x in values.replace(' ', '')] except: raise AssertionError('List of values to write must be decimals') if len(list_bool_values) == 1: self.session.write_coil(int_start, list_bool_values[0], unit=self.unit_id) desc = 'Modbus Function 5, Write Single Coil' else: self.session.write_coils(int_start, list_bool_values, unit=self.unit_id) desc = 'Modbus Function 15, Write Multiple Coils' message_dialog( title='Success', text=f'Wrote {list_bool_values} starting at {int_start}') results = {} stop = int_start + len(list_bool_values) for i in range(len(list_bool_values)): results[int_start + i] = int(list_bool_values[i]) output_text += self.log_and_output_bits(desc, int_start, stop, results) return output_text
def do_debug(cmd: str): """ Run a python command for debugging :PARAM: cmd: Python command to run in debug session """ message_dialog(title="Debug", text=str(eval(cmd)))
def summarize_word_responses(message, results): """ Summarize word reseponses in message dialog :PARAM: """ la, lr, fa = None, None, None # last_addres, last_result, first_address table = [["Addr", "Int", "HEX", "UTF-8"]] for address, result in results.items(): if address - 1 == la and result == lr: if fa == None: fa = la elif la != None: if fa == None: table.append([la, lr, "{:04x}".format(lr), str(chr(lr))]) fa = None else: s = "{0}-{1}".format(fa, la) table.append([s, lr, "{:04x}".format(lr), str(chr(lr))]) fa = None la, lr = address, result # Print final output from for loop if fa == None: table.append([la, lr, "{:04x}".format(lr), str(chr(lr))]) else: s = "{0}-{1}".format(fa, la) table.append([s, lr, "{:04x}".format(lr), str(chr(lr))]) message += tabulate(table, headers="firstrow", tablefmt="simple") message_dialog(title="Success", text=message)
def do_send_utf8(data: str): """ Send UTF-8 string to serial device. :PARAM: data: UTF-8 string to send. """ assert ( ctserial.session ), "There is not an open session. Connect to one first." # ToDo assert session type utf8_str = "".join( shlex.split(data)) # remove spaces not in quotes and format tx_bytes = bytes(utf8_str, encoding="utf-8") ctserial.views["transactions"].append([ "-->", common.bytes2hexstr(tx_bytes, group=8, sep=" ", line=35), common.bytes_decode(tx_bytes), ]) rx_bytes = common.send_instruction(ctserial.session, tx_bytes) if rx_bytes: ctserial.views["transactions"].append([ "<--", common.bytes2hexstr(rx_bytes, group=8, sep=" ", line=35), common.bytes_decode(rx_bytes), ]) else: message_dialog("ERROR", "No response received") return tabulate(ctserial.views["transactions"], tablefmt="plain", headers="firstrow")
def do_connect_udp(self, args, output_text): """Connect to a Modbus UDP device""" assert (self.session == None), 'Session already open. Close first.' # ToDo assert session type host, port = self.parse_ip_port(args) self.session = ModbusUdpClient(host, port) message_dialog( title = 'Success', text = 'Session opened with {}:{}'.format(host, port) )
def do_connect_rtu(self, args, output_text): """Connect to a Modbus RTU serial device""" assert (self.session == None), 'Session already open. Close first.' # ToDo assert session type device = self.validate_serial_device(args) self.session = ModbusSerialClient(method='rtu', port=device, timeout=1) message_dialog( title = 'Success', text = 'Session opened with {}'.format(device) )
def do_close(self, args, output_text): """Close a session.""" assert (self.session), 'There is not an open session. Connect to one first' # ToDo assert session type self.session.close() message_dialog( title = 'Success', text = 'Session closed with {}'.format(self.session) ) self.session = None return None
def do_project(): """Information about the current project""" message = f' Project Name: {ctui.project_name}\n' message += f' Project Path: {ctui._project_path}\n' message += f' File Size: {Path(ctui._project_path).stat().st_size} KB\n' message += f' History Count: {len(ctui.history)} records\n' message += f'Settings Count: {len(ctui.settings)} records\n' message += f' Storage Count: {len(ctui.storage)} records' message_dialog(title='Project Information', text=message)
def do_connect(): """Connect to modbus device/service or list suggestions""" output_text = "Connected Serial Devices\n" output_text += common.list_serial_devices() output_text += "\n\n\n" output_text += "Listening Services on Localhost\n" output_text += common.list_listening_ports() output_text += " \n" message_dialog(title="Suggestions", text=output_text)
def do_read_id(self, args, output_text): """Read device identification data.""" assert ( self.session ), 'There is not an open session. Connect to one first' # ToDo assert session type request = ReadDeviceInformationRequest(unit=1) response = self.session.execute(request) message_dialog(title="Response", text=str(reponse)) return None
def do_history(count: int = 0): """ Print history of commands entered :PARAM count: Optional number of last histories to print """ message = tabulate(ctui.history.all()[-count:], headers='keys', tablefmt='simple') message_dialog(title='History', text=message)
def do_project_list(): """List saved projects""" lines = [] for file in list(Path(ctui._project_folder).glob(f'*.{ctui.name}')): lines.append({ 'Project': file.stem, 'Size (KB)': file.stat().st_size }) message = tabulate(lines, headers='keys', tablefmt='simple') message_dialog(title='Saved Projects', text=message, scrollbar=True)
def do_history_search(query: str): """ Search history of commands ... :PARAM query: Regex string to search in history """ History = Query() search_results = ctui.history.search( History.Command.matches('.*' + query)) message = tabulate(search_results, headers='keys', tablefmt='simple') message_dialog(title='History Search Results', text=message)
def do_close(): """ Close the open session """ assert ( ctserial.session ), "There is not an open session. Connect to one first." # ToDo assert session type dev = ctserial.session.port ctserial.session.close() ctserial.session = None message_dialog("SUCCESS", f"Session with {dev} CLOSED") return
def send_instruction(session, tx_bytes): """Send data to serial device""" # clear out any leftover data try: rx_raw = bytes() if session.inWaiting() > 0: session.flushInput() session.write(tx_bytes) time.sleep(0.1) while session.inWaiting() > 0: rx_raw += session.read() except BaseException as e: message_dialog("ERROR", "\n\n{}".format(e)) time.sleep(0.1) return rx_raw
def do_project_load(name: str): """ Load saved project ... :PARAM name: Name of project to load """ project_to_load_path = f'{ctui._project_folder}{name}.{ctui.name}' assert (Path(project_to_load_path).is_file() ), f'"{name}" is not a valid project' ctui.db.close() ctui.project_name = name ctui._init_db() message_dialog(title='Success', text=f'{name} project loaded.')
def do_project_export(filename: str): """ Export the current project to ... :PARAM filename: Filename of file to export current project """ export_file = Path(f'{filename}.{ctui.name}').expanduser() def project_export(): export_file.write_bytes(Path(ctui._project_path).read_bytes()) if export_file.is_file(): yes_no_dialog(title='File Already Exists', text='Overwrite file?', yes_func=project_export) else: project_export() message_dialog(title='Success', text=f'Project exported as:\n"{export_file}"')
def do_project_saveas(name: str): """ Save current project as ... :PARAM name: New name of project you are saving """ project_to_save_path = f'{ctui._project_folder}{name}.{ctui.name}' def project_saveas(): ctui.db.close() old_project = Path(ctui._project_path) ctui.project_name = name Path(ctui._project_path).write_bytes(old_project.read_bytes()) ctui._init_db() if Path(project_to_save_path).is_file(): yes_no_dialog(title='Warning', text=f'Project "{name}" already exists! Overwrite?', yes_func=project_saveas) else: project_saveas() message_dialog(text=f'Project saved as "{name}".')
def do_write_coil(address: int, values: GreedyBin): """ Write single coil in format: <address> <0 or 1> :PARAM: address: Modbus address to start writes :PARAM: values: Space separated list of True or False to write """ assert ctmodbus.session, "There is not an open session. Connect to one first." if len(values) == 1: ctmodbus.session.write_coil(address, values[0], unit=unit_id) desc = "Modbus Function 5, Write Single Coil" else: # This is currently not working, as GreedyBin doesn't pass on a list ctmodbus.session.write_coils(address, values, unit=unit_id) desc = "Modbus Function 15, Write Multiple Coils" message_dialog(title="Success", text=f"Wrote {values} starting at {address}") results = {} stop = address + len(values) for i in range(len(values)): results[address + i] = int(values[i]) output_text = ctmodbus.output_text output_text += common.log_and_output_bits(desc, address, stop, results) return output_text
def do_write_register(address: int, values: GreedyInt): """ Write single register in format: <address> <int> :PARAM: address: Modbus address to start writes :PARAM: values: Space separated integers to write """ assert ctmodbus.session, "There is not an open session. Connect to one first." if len(values) == 1: ctmodbus.session.write_register(address, values[0], unit=unit_id) desc = "Modbus Function 6, Write Single Register" else: # This is currently not working, as GreedyInt doesn't pass on a list ctmodbus.session.write_registers(address, values, unit=unit_id) desc = "Modbus Function 16, Write Multiple Registers" message_dialog(title="Success", text=f"Wrote {values} starting at {address}") results = {} stop = address + len(values) for i in range(len(values)): results[address + i] = values[i] output_text = ctmodbus.output_text output_text += common.log_and_output_words(desc, address, stop, results) return output_text
def do_project_import(filename: str): """ Import exported project from ... :PARAM filename: Exported filename to import """ project_to_import_path = Path(filename).expanduser() if project_to_import_path.is_file() == False: project_to_import_path = Path( str(project_to_import_path) + f'.{ctui.name}') assert (project_to_import_path.is_file()), 'File does not exist' with open(project_to_import_path) as f: assert (f.read(14) == '{"_default": {' ), 'Invald or corrupted project file' # build new project name and path if project_to_import_path.suffix[1:] == ctui.name: ctui.project_name = project_to_import_path.stem else: ctui.project_name = project_to_import_path.name def project_import(): ctui.db.close() Path(ctui._project_path).write_bytes( project_to_import_path.read_bytes()) ctui._init_db() if Path(ctui._project_path).is_file(): yes_no_dialog(title='WARNING', text='Project already exists.\nOverwrite project?', yes_func=project_import) # TODO: use input_dialog to request new project name else: project_import() message_dialog(title='Success', text=f'Project imported as "{ctui.project_name}"')
def response_message_dialog(self, message, results): la, lr, fa = None, None, None #last_addres, last_result, first_address table = [['Addr', 'Int', 'HEX', 'ASCII']] for address, result in results.items(): if address - 1 == la and result == lr: if fa == None: fa = la elif la != None: if fa == None: table.append([la, lr, '{:04x}'.format(lr), str(chr(lr))]) fa = None else: s = '{0}-{1}'.format(fa, la) table.append([s, lr, '{:04x}'.format(lr), str(chr(lr))]) fa = None la, lr = address, result # Print final output from for loop if fa == None: table.append([la, lr, '{:04x}'.format(lr), str(chr(lr))]) else: s = '{0}-{1}'.format(fa, la) table.append([s, lr, '{:04x}'.format(lr), str(chr(lr))]) message += tabulate(table, headers='firstrow', tablefmt='simple') message_dialog(title='Success', text=message)
def do_debug(self, args, output_text): message_dialog(title='Debug', text=str(eval(args)))