class LogixDriverCOMServer: _reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER _public_methods_ = ['open', 'close', 'read_tag', 'write', ] #'get_plc_info', 'get_plc_name', 'get_tag_list'] _readonlu_attrs_ = [] _public_attrs_ = [] # _readonly_attrs_ = ['tags', 'info', 'name'] # _public_attrs_ = ['path', 'large_packets', 'init_tags', 'init_program_tags' ] + _readonly_attrs_ _reg_clsid_ = CLSID _reg_desc_ = 'Pycomm3 - Python Ethernet/IP ControlLogix Library COM Server' _reg_progid_ = 'Pycomm3.COMServer' def __init__(self): self.plc: LogixDriver = None def open(self, path, init_tags=True, init_program_tags=False, init_info=True): self.plc = LogixDriver(path, init_tags=init_tags, init_program_tags=init_program_tags, init_info=init_info) self.plc.open() def close(self): self.plc.close() def read(self, tag): result = self.plc.read(tag) return result.value if result else None def write(self, *tag_values): return self.plc.write(*tag_values)
def discoverPLCs(): # Function to discover any PLC on the network ips, slots, progName = [], [], [] try: discovery = CIPDriver.discover() # Return list of all CIP devices on the network for device in discovery: # Go through discovery list and append any PLC#'s to a list if device['product_type'] == "Programmable Logic Controller": ips.append(device['ip_address']) if len(ips) > 0: # Print the discovered PLC's, if there are any ips.sort() # Sort the IP address in ascending order table = Table(box=box.ROUNDED) # Create table table.add_column('#', justify='center') # Add column table.add_column('Device Type', justify='center') # Add column table.add_column('IP Address', justify='center') # Add column table.add_column('Slot #', justify='center') # Add column table.add_column('Program Name', justify='center') # Add column for i, ip in enumerate(ips): # Add row for each PLC discovered slots.append('Unknown') progName.append('Unknown') for slot in range(1, 18): try: plc = LogixDriver(f'{plc}/{str(slot)}', init_tags=False) if plc.open(): slots[i] = slot progName[i] = plc.get_plc_name() plc.close() break except: continue table.add_row(str(i+1), 'Programmable Logic Controller', ip, str(slots[i]), progName[i]) print(table) else: print("No PLC's discovered on the network") except Exception: traceback.print_exc()
def init(): # Funtion to initialize a connection to a PLC and retrive data from it seqs, tags = {}, {} ipOK, slotOK = False, False ipRegex = re.compile(r''' (\d{1,3}\.){3}\d{1,3}$ # IPv4 | ([0-9abcdef]{4}:){7}[0-9abcdef]{4}$ # IPv6 | ^cancel$ # Cancel the operation ''', re.VERBOSE|re.I) slotRegex = re.compile(r''' \d+$ # Any digit | ^cancel$ # Cancel the operation ''', re.VERBOSE|re.I) while ipOK == False: ip = re.match(ipRegex, input("PLC IP Address: ").strip()) # Request the PLC IP address and check it is the correct format if ip: # IP address format OK if ip.group().lower() == 'cancel': print('Operation cancelled\n') return None, seqs, tags else: ipOK = True # IP Address OK, allow user to enter slot else: # IP address format not OK print('Format of IP address was incorrect') while slotOK == False: slot = re.match(slotRegex, input("Rack slot: ").strip()) # Request the rack slot number if slot: # Slot format OK if slot.group().lower() == 'cancel': print('Operation cancelled\n') return None, seqs, tags else: slotOK = True else: # Slot format not OK print('Format of slot was incorrect') try: print(f"Initializing connection to {ip.group()}/{str(slot.group())}") plc = LogixDriver(f"{ip.group()}/{str(slot.group())}", init_tags=True, init_program_tags=True) # Set up the LogixDriver for the stated PLC. This is returned to be used within other functions if plc.open(): # Open the connection to the PLC and read data from it print("Connection to PLC established") plcInfo = plc.info # Write the PLC info to a variable del plcInfo['tasks'] # Remove tasks from plcInfo as it is not required del plcInfo['modules'] # Remove modules from plcInfo as it is not required programs = plcInfo.pop('programs') # Pop the 'programs' dictionary item into a variable for prog in programs.keys(): # Go through the programs and search for a sequence regex (Sxx where x are digits) mo1 = re.search(r'^S\d\d', prog) if mo1 != None: mo2 = re.search(r'\d\d', mo1.group()) # Search for the digits in the matched object and use them as keys in the sequences dictionary seqs.setdefault(mo2.group(), prog) # Assign the key (sequence number) and value (program name) to the sequences dictionary seqs = keySortDict(seqs) # Sort the sequence dictionary based on sequence number (keys) plcInfo.update({'revision':f"Major : {str(plcInfo['revision']['major'])} / Minor : {str(plcInfo['revision']['minor'])}"}) table = Table(box=box.ROUNDED) # Create a table table.add_column('PLC Information', justify='left') # Add column table.add_column('Sequences', justify='left') # Add column textInfo = '\n'.join(f"{k.capitalize().replace('_', ' ').ljust(15)} | {v}" for k, v in plcInfo.items()) # Combine the PLC information into one string textSeq = '\n'.join(f"{k} - {v}" for k, v in seqs.items()) # Combine the PLC sequences into one string table.add_row(textInfo, textSeq) # Add row print(table) for k, v in seqs.items(): # Loop for each sequence selected by the user maxStep = plc.read(f"zzSeq[{k}].MaxStepNo").value tags.setdefault(k, [f"zzSeq[{k}].MaxStepNo", f"Program:{v}.zzSteptimeLast[1]{{{maxStep}}}", f"Program:{v}.zzSteptimeLong[1]{{{maxStep}}}", f"Program:{v}.zzSteptimeShort[1]{{{maxStep}}}", f"Program:{v}.zzStepRefTime[xxTypexx,1]{{{maxStep}}}"]) # Add the sequence tags to the dictionary of tags print(f"Initialized step tags for sequence {k}: {v}, max step = {str(maxStep)}") plc.close() else: print('Failed to establish connection to PLC') except Exception: plc.close() traceback.print_exc() return plc, seqs, tags # Return the LogixDriver and a key sorted dictionary of sequence programs in the connected PLC
class allenbradley_logix(driver): ''' This driver supports services specific to ControlLogix, CompactLogix, and Micro800 PLCs. The driver is based on the library pycomm3: https://github.com/ottowayi/pycomm3. Parameters: ip : String : ip address of the PLC ''' def __init__(self, name: str, pipe: Optional[Pipe] = None): """ :param name: (optional) Name for the driver :param pipe: (optional) Pipe used to communicate with the driver thread. See gateway.py """ # Inherit driver.__init__(self, name, pipe) self.ip = '192.168.0.1' def connect(self) -> bool: """ Connect driver. : returns: True if connection stablished False if not """ # Create connection try: self._connection = LogixDriver(self.ip) self._connection.open() except Exception as e: self.sendDebugInfo(f"Connection with {self.ip} cannot be stablished.") return False # Check connection status. if self._connection.connected: return True else: self.sendDebugInfo(f"Driver not connected.") return False def disconnect(self): """ Disconnect driver. """ if self._connection: self._connection.close() def addVariables(self, variables: dict): """ Add variables to the driver. Correctly added variables will be added to internal dictionary 'variables'. Any error adding a variable should be communicated to the server using sendDebugInfo() method. : param variables: Variables to add in a dict following the setup format. (See documentation) """ for var_id in list(variables.keys()): var_data = dict(variables[var_id]) try: var_data['value'] = None self.variables[var_id] = var_data except Exception as e: self.sendDebugInfo(f'SETUP: {e} \"{var_id}\"') def readVariables(self, variables: list) -> list: """ Read given variable values. In case that the read is not possible or generates an error BAD quality should be returned. : param variables: List of variable ids to be read. : returns: list of tupples including (var_id, var_value, VariableQuality) """ res = [] try: values = self._connection.read(*variables) if not isinstance(values, list): values = [values] for (tag, value, _, error) in values: if error: res.append((tag, None, VariableQuality.BAD)) else: res.append((tag, value, VariableQuality.GOOD)) except Exception as e: self.sendDebugInfo(f'exception reading variable values: {e}') return res def writeVariables(self, variables: list) -> list: """ Write given variable values. In case that the write is not possible or generates an error BAD quality should be returned. : param variables: List of tupples with variable ids and the values to be written (var_id, var_value). : returns: list of tupples including (var_id, var_value, VariableQuality) """ res = [] try: values = self._connection.write(*variables) if not isinstance(values, list): values = [values] for (tag, value, _, error) in values: if error: res.append((tag, value, VariableQuality.BAD)) else: res.append((tag, value, VariableQuality.GOOD)) except Exception as e: self.sendDebugInfo(f'exception writing variable values: {e}') return res