Good morning,
I'm using the provided TICSPro_TCP.py class and attributes to write a python script that remotely controls the TICSPro GUI to automate some testing for our LMX2615 eval board.
Is the 9001 port number provided in the __init__ attribute (line 81) supposed to be the default / intended port for connecting to the GUI? 9001 has not worked for me, I've been able to use a different port to establish connection after listening to all ports when starting the GUI. This port number has occasionally changed and I've had to re-listen to the ports and find the new one.
Thank you for your help,
Nolan
""" * Copyright (C) {2021-2022} Texas Instruments Incorporated - https://www.ti.com/ * ALL RIGHTS RESERVED """ import socket from socket import AF_INET, SOCK_STREAM, SHUT_RDWR import os import subprocess from subprocess import CREATE_NEW_CONSOLE, CREATE_NO_WINDOW import time import re import configparser class TICSProTCPClient: """ A communication object which exposes the APIs available through the TCP server. The current revision of TICS Pro (1.7.4) supported by this communication object represents a be ta version of the server - some features may not be supported, and there may be bugs in others. Communication with the TCP server takes the form of commands and responses. Commands are generated by the client, while responses are generated by TICS Pro. Commands use the string "<SOC>" to identify the start of a command, and "<EOC>" to identify the end of a command. Each command starts with a command string, and may potentially be followed by one to four parameters separated by ascii character 0x19 (). A few examples: Write all registers: '<SOC>writeallregisters<EOC>' Set POWERDOWN = 1: '<SOC>setfieldvaluePOWERDOWN1<EOC>' Readback POWERDOWN: '<SOC>readparameterPOWERDOWN<EOC>' Responses use the string "<SOR>" to identify the start of a response, and "<EOR>" to identify the end of a response. Each response consists of the command name which triggered the response, the status of the response (for now True on success and False on failure, loosely), and occasionally a return value from the triggering command, separated by ascii character 0x19 (). A few examples, representing responses to the examples above: Write all registers: '<SOR>writeallregistersTrue<EOR>' Set POWERDOWN = 1: '<SOR>setfieldvalueTrue<EOR>' Readback POWERDOWN: '<SOR>readparameterTrue1<EOR>' The server can be initialized in TICS Pro using the TICS Pro.ini file, located in the configuration directory (default: C:\\ProgramData\\Texas Instruments\\TICS Pro\\Configurations) by controlling the value of the keys in the API section. TCPCLIENT: If 'true', start with the TCP server enabled. If false, start with the TCP server disabled and ignore other keys. TCPLOCAL: If 'true', use localhost as the address. If false, use the currently active network adapter as the address. Strongly recommended not to set false while TCP server is enabled, since this behavior is still being defined. TCPPORT: A value between 0 and 65535, representing the port on which the TCP server will listen. DISABLECLOSEBUTTON: Set to 'false' for now. This behavior is not fully defined. The server can also be initialized from command line with the port number as an argument to the executable. The server will attempt to start listening on localhost:portNum: > "C:\\Program Files (x86)\\Texas Instruments\\TICS Pro\\TICS Pro.exe" 11000 The example invocation would attempt to start TICS Pro with the TCP server enabled and listening on port 11000. Typical usage of the TICSProTCPClient object looks like: tp = TICSProTCPClient(port=11000) tp.SetFieldValue("POWERDOWN",1) """ _SOC = "<SOC>" # Start of command (python -> TICS Pro) _EOC = "<EOC>" # End of command (python -> TICS Pro) _SOR = "<SOR>" # Start of response (TICS Pro -> python) _EOR = "<EOR>" # End of response (TICS Pro -> python) _SEP = chr(25) # Separates arguments in the command/response def __init__(self, ip_address="127.0.0.1", port=9001, check_alive=False, connection_retry_count=30): """ Create a connection to a TICS Pro TCP server. Assumes TICS Pro has already been started. Each connection attempt has a timeout period of three seconds by default, but some device profiles may take more than 3s to load; multiple attempts may be necessary to connect to TICS Pro. ip_address: String representing IPv4 address port: Integer port number check_alive: Raises ConnectionError the first time a connection attempt fails, regardless of retry count connection_retry_count: If check_alive==False, number of times to retry making the connection to TICS Pro. If check_alive==True, this argument is ignored. """ self.connected = False self.connect(ip_address, port, check_alive, connection_retry_count=connection_retry_count) def __del__(self): self.disconnect() def _prepare_command(self, *args): """ Formats commands for transmission. Adds separators, <SOC>/<EOC> headers and footers, and formats as UTF-8. """ command = self._SOC + self._SEP.join([str(x) for x in args]) + self._EOC return command.encode('utf-8') def _prepare_response(self, buf): """ Decodes response. Converts bytes to UTF-8, removes <SOR>/<EOR>, splits by separators, and returns response as a list. """ response = buf.decode('utf-8').lstrip(self._SOR).rstrip(self._EOR) return response.split(self._SEP) def _SEND(self, *args): """ Wraps command + args in required protocol header/footer/separator, sends it to TICS Pro, and returns the response as a string. """ if not self.connected: raise ConnectionError("Client is not connected to TICS Pro") # Send the data. In theory over localhost this should never split up # any API commands we could realistically send, but it's a good idea # to make sure we sent everything under normal circumstances. raw_cmd = self._prepare_command(*args) bytes_sent = 0 bytes_to_be_sent = len(raw_cmd) while bytes_sent < bytes_to_be_sent: bytes_sent_this_time = self._sock.send(raw_cmd[bytes_sent:]) if bytes_sent_this_time == 0: raise ConnectionError("Connection lost during send") bytes_sent += bytes_sent_this_time # Receive the response. We don't know the length, but in practice no # response should ever take more than 4kB. This may be updated in # future versions of TICS Pro. raw_resp = self._sock.recv(4096) response = self._prepare_response(raw_resp) return response def connect(self, ip_address, port, check_alive=False, connection_retry_count=30): """ Create a connection to a TICS Pro TCP server. Assumes TICS Pro has already been started. Each connection attempt has a timeout period of three seconds by default, but some device profiles may take more than 3s to load; multiple attempts may be necessary to connect to TICS Pro. ip_address: String representing IPv4 address port: Integer port number check_alive: Raises ConnectionError the first time a connection attempt fails, regardless of retry count connection_retry_count: If check_alive==False, number of times to retry making the connection to TICS Pro. If check_alive==True, this argument is ignored. """ if self.connected: raise ConnectionError("Already connected to TICSPro {0}:{1}".format(self.address)) self.address = (ip_address, port) self._sock = socket.socket(AF_INET, SOCK_STREAM, 0) self._sock.settimeout(10) # default communication timeout - change as needed connection_count = 0 while (not self.connected) and (connection_count < connection_retry_count): try: self._sock.connect(self.address) self.connected = True except: # could possibly be restricted to ConnectionRefusedError if check_alive: raise ConnectionError("Server at {0}:{1} is not running".format(self.address)) connection_count += 1 print("Could not connect on try {connection_count}, trying again until count reaches >= {connection_retry_count}".format(**locals())) time.sleep(3) def disconnect(self): """Terminates TICSPro connection. No effect if already disconnected.""" if self.connected: self._sock.shutdown(SHUT_RDWR) self._sock.close() self.connected = False # ===================== # === API FUNCTIONS === # ===================== def BurstDeleteAll(self): """ Deletes everything in the burst mode text box """ n = "burstdeleteall" r = self._SEND(n) def BurstRun(self, Run): """ Equivalent to pressing run button on burst mode. Run (bool): Set to True. """ n = "burstrun" r = self._SEND(n, Run) def BurstStop(self): """ Stops Burst mode """ n = "burststop" r = self._SEND(n) def CheckModeText(self, ModeName): """ Checks if the mode name is present in the default configuration menu. Returns True if it exists, or False if it does not. ModeName (str): exact name of mode in default configuration menu """ n = "checkmodetext" r = self._SEND(n, ModeName) return True if r[1].lower() == "true" else False def CloseTICSPro(self): """ Closes TICSPro. Subsequent communications will fail. """ n = "closeticspro" r = self._SEND(n) def ConnectToUSB2ANY(self, SerialNumber): """ Tries connecting to a USB2ANY with the listed serial number. Returns True on success or False on failure. SerialNumber (str): USB2ANY serial number. """ n = "connect" r = self._SEND(n, SerialNumber) if r[1].lower() == "true": return True else: return False def GetAllUSB2ANY(self): """ Returns a list of all connected USB2ANY serial numbers. """ n = "getallusb2any" r = self._SEND(n) if r[1].lower() == "true" and len(r) > 2: return r[2:] else: return [] def GetDevice(self): """ Retrieves the name and type of the currently loaded device. Returns deviceName, deviceType on success, or False, False on failure. """ n = "getdevice" r = self._SEND(n) if r[1].lower() == "true": return r[2], r[3] else: return False, False def GetFieldValue(self, ControlName): """ Returns integer representation of a control's value. For register- backed controls, this is the literal integer value of the field in the register map. Do not use with FlexControls (which are not register-backed). Updates controls with linked bits. ControlName (str): name of indexed control to read. """ n = "getfieldvalue" r = self._SEND(n, ControlName) if r[2] == "": # Control does not exist return -1 else: return int(r[2]) def GetIndex(self, ControlName): """ Returns integer index value of the GUI's currently selected item in the items collection. For register-backed controls, this is usually the literal integer value of the field in the register map, but can be the GUI index with sparse lists (where some values are missing). ControlName (str): name of indexed control to read. """ n = "getindex" r = self._SEND(n, ControlName) return int(r[2]) def GetPin(self, PinName): """ Returns 1 if the pin is set, 0 if it is not. Returns -1 if operation fails. PinName (str): name of the pin. """ n = "getpin" r = self._SEND(n, PinName) if r[1].lower() == "true": return int(r[2]) else: return -1 def GetRegister(self, RegisterName): """ <todo> """ pass def GetRegisterbyIndex(self, Index): """ <todo> """ pass def GetText(self, ControlName): """ Returns value in text field, or displayed text value of a combobox or listbox. ControlName (str): name of text field to read. """ n = "gettext" r = self._SEND(n, ControlName) return r[2] def Help(self): """ Prints a complete list of all the help options reported by the client. """ n = "help" r = self._SEND(n) print("|".join(r[2:])) def Initialize(self, AppDirectory): """ <todo> """ pass def PressButton(self, ControlName): """ Triggers a press event for a button. Returns False if press is not valid, and True if press is valid. ControlName (str): name of button to press. """ n = "pressbutton" r = self._SEND(n, ControlName) return True if r[1].lower() == "true" else False def PressSpinButton(self, ControlName, ByAmount): """ Triggers a press event for a spin button. Returns False if press is not valid, and True if press is valid. ControlName (str): name of button to press. ByAmount (int): If positive, increments by the specified amount. If negative, decrements by the specified amount. """ n = "pressspinbutton" r = self._SEND(n, ControlName, ByAmount) return True if r[1].lower() == "true" else False def ReadAllRegisters(self): """ Updates TICS Pro with the current value of all registers. """ n = "readallregisters" r = self._SEND(n) def ReadDirect_I2C(self, SlaveAddress, AddressBytes, DataBytes): """ <todo> """ pass def ReadDirect_SPI(self, AddressBytes, DataBytes): """ <todo> """ pass def ReadParameterAndUpdateUI(self, Parameter): """ Checks the register tied to a TICS Pro parameter, and reports the integer parameter value or -1 on failure. UI will be updated by readback event. Parameter (str): parameter name in TICSPro. """ n = "readparameter" r = self._SEND(n, Parameter) if r[1].lower() == "true": return int(r[2]) else: return -1 def ReadRegister(self, RegisterName): """ Attempts to read a register. Automation will hang if device is not configured for register readback and CheckReadback event occurs. Returns True on success or False on failure. RegisterName (str): Name of register e.g. "R0" """ n = "readregister" r = self._SEND(n, RegisterName) if r[1].lower() == "true": return int(r[2]) return -1 def ReadRegisterDataOnly(self, RegisterName): """ <todo> """ pass def ReadRegisterbyIndex(self, Index): """ <todo> """ pass def RestoreSetup(self, FileName, FilePath): """ Restores a .tcs file <FileName>.tcs from directory FilePath. FileName (str): a valid Windows filename. FilePath (str): a valid Windows path. Raises a FileNotFoundError if the .tcs file doesn't exist. """ n = "restoresetup" if not FileName.endswith(".tcs"): FileName += ".tcs" f = os.path.join(FilePath, FileName) if os.path.exists(f): r = self._SEND(n, f) else: raise FileNotFoundError(2, os.strerror(2), f) def SaveSetup(self, FileName, FilePath): """ Saves a .tcs file as <FileName>.tcs in the directory FilePath. FileName (str): a valid Windows filename. FilePath (str): a valid Windows path. """ n = "savesetup" if not FileName.endswith(".tcs"): FileName += ".tcs" f = os.path.join(FilePath, FileName) r = self._SEND(n, f) def SelectDevice(self, DeviceName): """ Loads built-in device into TICS Pro. DeviceName (str): Name of the device to be loaded. """ n = "selectdevice" r = self._SEND(n, DeviceName) def SelectInterface(self, Interface, Protocol, SerialNumber): """ Selects the communication interface. Interface (str): Can be DemoMode, USB2ANY, TIHera, or FTDI. Protocol (str): Can be SPI, SPI_CLKLOW, UWIRE, I2C. SerialNumber (str): The unique ID of the USB2ANY, FTDI, etc. """ n = "selectinterface" r = self._SEND(n, Interface, Protocol, SerialNumber) def SelectPage(self, PageName): """ Switches TICS Pro's selected page to PageName. PageName (str): Name of the page to select. """ n = "selectpage" r = self._SEND(n, PageName) def SelectUserDevice(self, DeviceName): """ Loads user device into TICS Pro. DeviceName (str): Name of the device to be loaded. """ n = "selectuserdevice" r = self._SEND(n, DeviceName) def SetAddress_I2C(self, SlaveAddress): """ Sets the I2C address to SlaveAddress. SlaveAddress (int): 0-10 bit integer address. """ n = "setaddress_i2c" r = self._SEND(n, SlaveAddress) def SetFieldValue(self, ControlName, Value): """ Sets the integer representation of a control's value. For register- backed controls, this is the literal integer value of the field in the register map. Do not use with FlexControls (which are not register-backed). ControlName (str): name of control to write. Value (int): integer value of control to write. """ n = "setfieldvalue" r = self._SEND(n, ControlName, Value) def SetIndex(self, ControlName, Value): """ Sets the integer index value of the GUI's currently selected item in the items collection. For register-backed controls, this is usually the literal integer value of the field in the register map, but can be the GUI index with sparse lists (where some values are missing). ControlName (str): name of control to write. Value (int): index of control to write. """ n = "setindex" r = self._SEND(n, ControlName, str(Value)) def SetMode(self, Index): """ Loads the mode at the index in the default configuration menu. Index (int): Index of mode. """ n = "setmode" r = self._SEND(n, str(Index)) def SetModeText(self, ModeName): """ Loads the mode with specified name in the default configuratin menu. ModeName (str): exact name of mode in default configuration menu. """ n = "setmodetext" r = self._SEND(n, ModeName) def SetPin(self, PinName, Value): """ Sets/Clears the pin. Returns -1 if operation fails. PinName (str): name of the pin. Value (bool): Pin value; True=set, False=clear """ n = "setpin" r = self._SEND(n, PinName, Value) def SetText(self, ControlName, Value): """ Sets a text field to Value, or sets a combobox or listbox to the index with text matching Value. ControlName (str): name of control to write. Value (str): value to write or match. """ n = "settext" r = self._SEND(n, ControlName, Value) def WriteAddressData(self, Address, Value): """ Writes the input value to the specified register address. Address (int): register address to send the write Value (int): Value to write to the register at Address """ n = "writeaddressdata" r = self._SEND(n, Address, Value) def WriteAllRegisters(self): """ Writes the current value of all registers to the device. """ n = "writeallregisters" r = self._SEND(n) def WriteDirect_I2C(self, SlaveAddress, AddressBytes, DataBytes): """ <todo> """ pass def WriteDirect_SPI(self, AddressBytes, DataBytes): """ <todo> """ pass def WriteRawData(self, RawRegData): """ Parses the input value into address and data, and writes the data of the register to the corresponding address of the device. Parser uses the position of the address bits on the register map to determine address and data positions and lengths. RawRegData (int): raw value of the register, including address and data. """ n = "writerawdata" r = self._SEND(n, RawRegData) def WriteRegister(self, RegisterName): """ Writes the current value of the register with the specified name. This does not write to register 64 if you specify R64, but if "PLL2_PRE" is a field in R64 and you specify "PLL2_PRE", this will write the current value of R64. """ n = "writeregister" r = self._SEND(n, RegisterName) def WriteRegisterbyIndex(self, Index): """ Writes the current value of the register located at the index in the register map. For example, when writing to register R64, Index=64. Index (int): index of register to write in register map """ n = "writeregisterbyindex" r = self._SEND(n, Index) def GetTICSProInstances(): ''' Returns a dictionary with keys equal to the string value of all PIDs for active TICS Pro instances, and values equal to either "" for instances without an active TCP server, or the local address as a string for instances with an active TCP server. An example return value might be: {'21804':'','20556':'127.0.0.1:11000'} '21804' and '20556' represent PIDs of TICS Pro instances. '21804' doesn't have a TCP server running, while '20556' has a TCP server running on port 11000. First, queries TASKLIST to get: - All process names and PIDs... - (/FO CSV) ...in CSV format From this table, only entries where the process is named "TICS Pro.exe" are retained, and their PIDs are recorded. Next, queries NETSTAT -AON -P TCP to get: - (-A) All active TCP/UDP connections - (-O) All PIDs associated with active TCP/UDP connections - (-N) All entries in numerical format (usually skips DNS resolution, which can save a lot of time) - (-P TCP) Filtered to only TCP connections From this table, scan the PIDs for entries matching known TICS Pro.exe PIDs, and save any associated TCP connections. This function may not succeed if TASKLIST or NETSTAT permissions are restricted for the user attempting to run the function. ''' TASKLIST_QUERY = subprocess.check_output(('TASKLIST','/FO','CSV'), creationflags=CREATE_NO_WINDOW) process_list = TASKLIST_QUERY.decode().replace('"','').split('\r\n') TICS_Pro_set = {} for row in process_list: if row.startswith("TICS Pro"): TICS_Pro_set[row.split(',')[1]] = None # column 1 is PID if TICS_Pro_set: NETSTAT_QUERY = subprocess.check_output(('NETSTAT','-AON'), creationflags=CREATE_NO_WINDOW) # Split up the NETSTAT response. Two or more whitespace characters # separate a table entry on every row, always active_connections = [x.lstrip() for x in NETSTAT_QUERY.decode().split('\r\n') if x] TCP_server_list = [re.split(r'\s\s+', x) for x in active_connections[2:] if x] for row in TCP_server_list: if row[-1] in TICS_Pro_set: # final entry is PID TICS_Pro_set[row[-1]] = row[1] # column 1 is local address return TICS_Pro_set def GetTICSProActivePorts(TICS_Pro_set=None): """ Runs GetTICSProInstances, then returns a set of all ports (as integers) being used by TICS Pro TCP servers. Specify TICS_Pro_set as the output of GetTICSProInstances to skip a function call. """ if TICS_Pro_set is None: TICS_Pro_set = GetTICSProInstances() active_ports = [] for v in TICS_Pro_set.values(): if v: i = v.rindex(":") active_ports.append(int(v[i+1:])) return set(active_ports) def update_ticspro_ini(enable=False, port=11000, root=r'C:\ProgramData\Texas Instruments\TICS Pro'): """ Configures the startup options for the TICS Pro TCP server. Options are stored in a static INI file located in the Configurations directory of the ProgramData, by default; other locations can be specified for the configuration directory location, but are usually not needed. Since options are stored in an INI file, they must be updated between starting new instances of TICS Pro. In cases where multiple instances of TICS Pro are running simultaneously, the port in the INI file must be changed to prevent TICS Pro from crashing on startup. This will be fixed in a subsequent release. enable (bool): Enables or disables TCP server on TICS Pro startup. port (int): Sets the port number on which to start the TCP server. This argument is ignored if enable = False. root (str): Path to Configurations directory. Default argument is the default installation path. """ cp = configparser.ConfigParser() cp.optionxform = str tp_path = os.path.join(root, "Configurations", "TICS Pro.ini") cp.read(tp_path, encoding="utf16") API_section_present = cp.has_section("API") change_port = enable or not API_section_present if not API_section_present: cp.add_section("API") cp.set("API", "TCPCLIENT", str(enable).lower()) cp.set("API", "TCPLOCAL", "true") # Recommend keeping local at all times if change_port: cp.set("API", "TCPPORT", str(port)) cp.set("API", "DISABLECLOSEBUTTON", "false") with open(tp_path, "w", encoding="utf16") as configfile: cp.write(configfile) def start_ticspro(enable_server=False, port=11000, connect_if_already_alive=False, TICS_path=r"C:\Program Files (x86)\Texas Instruments\TICS Pro", config_path=r"C:\ProgramData\Texas Instruments\TICS Pro"): """ A function that demonstrates how to start up a TICS Pro instance with the TCP server enabled/disabled, at the desired port. enable_server (bool): enables or disables the server for the started TICS Pro instance. port (int): port of the TCP server, if enable_server = True. Must be between 0 and 65535. connect_if_already_alive (bool): If TICS Pro is already active at the specified port, return an object that can communicate with the active instance. TICS_path (str): string to TICS Pro path. Default is standard install dir. config_path (str): string to config path. Default is standard install dir. """ if not enable_server and not connect_if_already_alive: # Start a TICS Pro without the TCP server enabled. No TICS Pro # communication object will be available, since the TCP server is off. update_ticspro_ini(enable=False, root=config_path) p = subprocess.Popen(["explorer", os.path.join(TICS_path, "TICS Pro.exe")], creationflags=CREATE_NEW_CONSOLE) t = None else: if port & 0xffff != port: raise ValueError("Port must be between 0 and 65535") TICS_Pro_set = GetTICSProInstances() if port in GetTICSProActivePorts(TICS_Pro_set): if not connect_if_already_alive: raise ConnectionError("Port is already in use by another TICS Pro instance") else: # Connect to an existing TICS Pro with a TCP server at port. # No Popen output, but a TICS Pro object is available. s_port = str(port) for v in TICS_Pro_set.values(): if v.endswith(s_port): break address, s_port = v.split(':') p = None t = TICSProTCPClient(address, port) else: # Start a TICS Pro with a TCP server enabled at port. update_ticspro_ini(enable=True, port=port, root=config_path) p = subprocess.Popen(["explorer", os.path.join(TICS_path, "TICS Pro.exe")], creationflags=CREATE_NEW_CONSOLE) t = TICSProTCPClient(port=port) # The value of 'p' is irrelevant for TCP communication and can safely be # ignored. It may sometimes be of interest if TICS Pro must be killed # or if the PID information is required. This function returns the TICS # Pro communication object 't', or None if 't' cannot be created. return t