├── 1_PLCExcel ├── ExcelToPLC_Logix.py ├── ExcelToPLC_OPC.py ├── PLCToExcel_Logix.py ├── PLCToExcel_OPC.py ├── README.md └── requirements.txt ├── 2_BasicExcelLogger ├── ExcelLogger_Logix.py ├── ExcelLogger_OPC.py ├── README.md └── requirements.txt ├── 3_LiveExcel ├── LiveExcelLogix.py ├── LiveExcelOPC.py ├── README.md └── requirements.txt ├── 4_L5X_Editor ├── L5X_Reader.py ├── L5X_Writer.py ├── README.md └── requirements.txt ├── 5_TkinterGUI ├── README.md ├── TkinterGUI.pyw └── requirements.txt ├── 6_OCR_GUI ├── PythonGUI_OCR.pyw ├── README.md └── requirements.txt ├── 7_WebGUI ├── README.md ├── WebGUI.py ├── requirements.txt ├── static │ ├── script.js │ └── style.css └── templates │ └── index.html ├── 8_WatchWindow ├── README.md ├── WatchWindow.pyw └── requirements.txt ├── LICENSE.txt └── README.md /1_PLCExcel/ExcelToPLC_Logix.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | from pylogix import PLC 3 | 4 | excelFileName = 'tagsLogix.xlsx' 5 | 6 | # Open the workbook 7 | wb = openpyxl.load_workbook(excelFileName) 8 | 9 | # Select the sheet 10 | sheet = wb['Sheet1'] 11 | 12 | # Extract the tags from the A column, removing the column header 13 | tag_names = [cell.value for cell in sheet['A'] if cell.value][1:] 14 | # Extract the values from the B column, removing the column header 15 | tag_values = [cell.value for cell in sheet['B'] if cell.value][1:] 16 | 17 | # Connect to the PLC 18 | plc = PLC() 19 | plc.IPAddress = '192.168.123.100' 20 | plc.ProcessorSlot = 2 21 | plc.SocketTimeout = 2.5 22 | 23 | # Combine Tag Name with Tag Values 24 | tagsWithValue = list(zip(tag_names,tag_values)) 25 | 26 | ###### Option 1: Individual Write 27 | values = [plc.Write(t,v) for t,v in tagsWithValue] 28 | 29 | ###### Option 2: Multi-Write 30 | values = plc.Write(tagsWithValue) 31 | 32 | # Disconnect from the PLC 33 | plc.Close() -------------------------------------------------------------------------------- /1_PLCExcel/ExcelToPLC_OPC.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | from asyncua.sync import Client,ua 3 | 4 | excelFileName = 'tagsOPC.xlsx' 5 | 6 | # Open the workbook 7 | wb = openpyxl.load_workbook(excelFileName) 8 | 9 | # Select the sheet 10 | sheet = wb['Sheet1'] 11 | 12 | # Select the column you want to read 13 | column = sheet['A'] 14 | 15 | # Extract the tags from the A column, removing the column header 16 | tag_names = [cell.value for cell in sheet['A'] if cell.value][1:] 17 | # Extract the values from the B column, removing the column header 18 | tag_values = [cell.value for cell in sheet['B'] if cell.value][1:] 19 | 20 | # Connect to the PLC 21 | plc = Client("opc.tcp://192.168.123.100:49320") 22 | plc.connect() 23 | 24 | # Combine Tag Name with Tag Values 25 | tagNodes = [plc.get_node(t) for t in tag_names] 26 | tagDataTypes = [t.get_data_type_as_variant_type() for t in tagNodes] 27 | # UA format values 28 | tagUA = [ua.DataValue(ua.Variant(v,dt)) for v,dt in list(zip(tag_values,tagDataTypes))] 29 | 30 | ###### Option 1: Individual Write 31 | # Combine Tags with UA format value 32 | tagsWithValue = list(zip(tagNodes,tagUA)) 33 | values = [t.write_value(v) for t,v in tagsWithValue] 34 | 35 | ###### Option 2: Multi-Write 36 | values = plc.write_values(tagNodes,tagUA) 37 | 38 | # Disconnect from the PLC 39 | plc.disconnect() -------------------------------------------------------------------------------- /1_PLCExcel/PLCToExcel_Logix.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | from pylogix import PLC 3 | 4 | excelFileName = 'tagsLogix.xlsx' 5 | 6 | # Open the workbook 7 | wb = openpyxl.load_workbook(excelFileName) 8 | 9 | # Select the sheet 10 | sheet = wb['Sheet1'] 11 | 12 | # Select the column you want to read 13 | column = sheet['A'] 14 | 15 | # Extract the tags from the A column, removing the column header 16 | tag_names = [cell.value for cell in sheet['A'] if cell.value][1:] 17 | 18 | # Connect to the PLC 19 | plc = PLC() 20 | plc.IPAddress = '192.168.123.100' 21 | plc.ProcessorSlot = 2 22 | 23 | ###### Option 1: Individual Read 24 | values = [plc.Read(tag_name).Value for tag_name in tag_names] 25 | 26 | ###### Option 2: Multi Read 27 | values = [t.Value for t in plc.Read(tag_names)] 28 | 29 | # Disconnect from the PLC 30 | plc.Close() 31 | 32 | # Write the values to the next column 33 | for i, value in enumerate(values): 34 | sheet.cell(row=i+2, column=2).value = value 35 | 36 | # Save the workbook 37 | wb.save(excelFileName) -------------------------------------------------------------------------------- /1_PLCExcel/PLCToExcel_OPC.py: -------------------------------------------------------------------------------- 1 | import openpyxl 2 | from asyncua.sync import Client,ua 3 | 4 | excelFileName = 'tagsOPC.xlsx' 5 | 6 | # Open the workbook 7 | wb = openpyxl.load_workbook(excelFileName) 8 | 9 | # Select the sheet 10 | sheet = wb['Sheet1'] 11 | 12 | # Select the column you want to read 13 | column = sheet['A'] 14 | 15 | # Extract the tags from the A column, removing the column header 16 | tag_names = [cell.value for cell in sheet['A'] if cell.value][1:] 17 | 18 | # Connect to the PLC 19 | plc = Client("opc.tcp://192.168.123.100:49320") 20 | plc.connect() 21 | 22 | # Get OPC tag names 23 | tag_names = [plc.get_node(t) for t in tag_names] 24 | 25 | ###### Option 1: Individual Read 26 | values = [t.read_value() for t in tag_names] 27 | 28 | ###### Option 2: Multi Read 29 | values = [v for v in plc.read_values(tag_names)] 30 | 31 | # Disconnect from the PLC 32 | plc.disconnect() 33 | 34 | # Write the values to the next column 35 | for i, value in enumerate(values): 36 | sheet.cell(row=i+2, column=2).value = value 37 | 38 | # Save the workbook 39 | wb.save(excelFileName) -------------------------------------------------------------------------------- /1_PLCExcel/README.md: -------------------------------------------------------------------------------- 1 | # 1_PLCExcel 2 | 3 | * ```ExcelToPLC_Logix.py``` -> Write from Excel to a Logix PLC via EtherNet/IP 4 | * ```ExcelToPLC_OPC.py``` -> Write from Excel to a PLC via OPC-UA 5 | * ```PLCToExcel_Logix.py``` -> Read from a Logix PLC to an Excel file via EtherNet/IP 6 | * ```PLCToExcel_OPC.py``` -> Read from a PLC to an Excel file via OPC-UA 7 | -------------------------------------------------------------------------------- /1_PLCExcel/requirements.txt: -------------------------------------------------------------------------------- 1 | openpyxl 2 | pylogix 3 | asyncua -------------------------------------------------------------------------------- /2_BasicExcelLogger/ExcelLogger_Logix.py: -------------------------------------------------------------------------------- 1 | from pylogix import PLC 2 | from datetime import datetime 3 | from openpyxl import load_workbook 4 | import threading 5 | import time 6 | 7 | class PeriodicInterval(threading.Thread): 8 | def __init__(self, task_function, period): 9 | super().__init__() 10 | self.daemon = True 11 | self.task_function = task_function 12 | self.period = period 13 | self.i = 0 14 | self.t0 = time.time() 15 | self.stopper = 0 16 | self.start() 17 | 18 | def sleep(self): 19 | self.i += 1 20 | delta = self.t0 + self.period * self.i - time.time() 21 | if delta > 0: 22 | time.sleep(delta) 23 | 24 | def run(self): 25 | while self.stopper == 0: 26 | self.task_function() 27 | self.sleep() 28 | 29 | def stop(self): 30 | self.stopper = 1 31 | 32 | # Define Defaults 33 | fileName = "LoggerLogix.xlsx" 34 | 35 | # Load tag names from Excel file 36 | wb = load_workbook(fileName) 37 | ws = wb.active 38 | # Remove timestamp from tagnames 39 | tag_names = [cell.value for cell in ws[1] if cell.value is not None][:-1] 40 | # Setup PLC comms 41 | plc = PLC() 42 | plc.IPAddress = "192.168.123.100" 43 | plc.ProcessorSlot = 2 44 | plc.SocketTimeout = 1 45 | 46 | def log_data(): 47 | # Read tag values 48 | tag_values = [tag.Value for tag in plc.Read(tag_names)] 49 | # Append timestamp 50 | tag_values.append(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 51 | # Write tag values and timestamp to Excel file 52 | ws.append(tag_values) 53 | 54 | # Create a new periodic thread 55 | tag_thread = PeriodicInterval(log_data, 0.1) 56 | 57 | input("Press Enter to exit...") 58 | tag_thread.stop() 59 | # Join the tag thread to wait for it to finish 60 | tag_thread.join(timeout=2) 61 | # CLose PLC Comm link 62 | plc.Close() 63 | # Save Excel File 64 | wb.save(fileName) 65 | -------------------------------------------------------------------------------- /2_BasicExcelLogger/ExcelLogger_OPC.py: -------------------------------------------------------------------------------- 1 | from asyncua.sync import Client 2 | from datetime import datetime 3 | from openpyxl import load_workbook 4 | import threading 5 | import time 6 | 7 | class PeriodicInterval(threading.Thread): 8 | def __init__(self, task_function, period): 9 | super().__init__() 10 | self.daemon = True 11 | self.task_function = task_function 12 | self.period = period 13 | self.i = 0 14 | self.t0 = time.time() 15 | self.stopper = 0 16 | self.start() 17 | 18 | def sleep(self): 19 | self.i += 1 20 | delta = self.t0 + self.period * self.i - time.time() 21 | if delta > 0: 22 | time.sleep(delta) 23 | 24 | def run(self): 25 | while self.stopper == 0: 26 | self.task_function() 27 | self.sleep() 28 | 29 | def stop(self): 30 | self.stopper = 1 31 | 32 | # Define Defaults 33 | fileName = "LoggerOPC.xlsx" 34 | 35 | # Load tag names from Excel file 36 | wb = load_workbook(fileName) 37 | ws = wb.active 38 | # Setup PLC comms 39 | plc = Client("opc.tcp://192.168.123.100:49320") 40 | plc.connect() 41 | # Remove timestamp from tagnames 42 | tag_names = [cell.value for cell in ws[1] if cell.value is not None][:-1] 43 | # Get tag names in OPCUA format 44 | tag_names = [plc.get_node(x) for x in tag_names] 45 | 46 | def log_data(): 47 | # Read tag values 48 | tag_values = [v for v in plc.read_values(tag_names)] 49 | # Append timestamp 50 | tag_values.append(datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 51 | # Write tag values and timestamp to Excel file 52 | ws.append(tag_values) 53 | 54 | # Create a new periodic thread 55 | tag_thread = PeriodicInterval(log_data, 0.1) 56 | 57 | input("Press Enter to exit...") 58 | tag_thread.stop() 59 | # Join the tag thread to wait for it to finish 60 | tag_thread.join(timeout=2) 61 | # CLose PLC Comm link 62 | plc.disconnect() 63 | # Save Excel File 64 | wb.save(fileName) 65 | -------------------------------------------------------------------------------- /2_BasicExcelLogger/README.md: -------------------------------------------------------------------------------- 1 | # 2_BasicExcelLogger 2 | 3 | * ```ExcelLogger_Logix.py``` -> Log Data from a Logix PLC to Excel via EtherNet/IP including a timestamp 4 | * ```ExcelLogger_OPC.py``` -> Log Data from a PLC to Excel via OPC-UA including a timestamp 5 | -------------------------------------------------------------------------------- /2_BasicExcelLogger/requirements.txt: -------------------------------------------------------------------------------- 1 | openpyxl 2 | pylogix 3 | asyncua -------------------------------------------------------------------------------- /3_LiveExcel/LiveExcelLogix.py: -------------------------------------------------------------------------------- 1 | import xlwings as xw 2 | import time 3 | from pycomm3 import LogixDriver 4 | 5 | # Define Defaults 6 | fileName = "LiveExcelLogix.xlsx" 7 | 8 | # Connect to PLC 9 | IPAddr = "192.168.123.100" 10 | Slot = "2" 11 | plc = LogixDriver(IPAddr + "/" + Slot) 12 | plc.open() 13 | 14 | # Open WorkBook 15 | wb = xw.Book(fileName) 16 | sht1 = wb.sheets["Sheet1"] 17 | 18 | # Read and Write Values 19 | for x in range(600): 20 | plc.write(sht1.range("A2").value, sht1.range("B2").value) 21 | plc.write(sht1.range("A3").value, sht1.range("B3").value) 22 | time.sleep(0.1) 23 | sht1.range("B4").value = plc.read(sht1.range("A4").value).value 24 | 25 | # Disconnect from PLC 26 | plc.close() 27 | -------------------------------------------------------------------------------- /3_LiveExcel/LiveExcelOPC.py: -------------------------------------------------------------------------------- 1 | import xlwings as xw 2 | import time 3 | from asyncua.sync import Client, ua 4 | 5 | # Define Defaults 6 | fileName = "LiveExcelOPC.xlsx" 7 | 8 | # Connect to PLC 9 | OPCAddr = "opc.tcp://192.168.123.100:49320" 10 | plc = Client(OPCAddr) 11 | plc.connect() 12 | 13 | # Open WorkBook 14 | wb = xw.Book(fileName) 15 | sht1 = wb.sheets["Sheet1"] 16 | 17 | # Read and Write Values 18 | for x in range(600): 19 | plc.get_node(sht1.range("A2").value).write_value(ua.DataValue(bool(sht1.range("B2").value))) 20 | plc.get_node(sht1.range("A3").value).write_value(ua.DataValue(bool(sht1.range("B3").value))) 21 | time.sleep(0.1) 22 | sht1.range("B4").value = plc.get_node(sht1.range("A4").value).read_value() 23 | 24 | # Disconnect from PLC 25 | plc.disconnect() 26 | -------------------------------------------------------------------------------- /3_LiveExcel/README.md: -------------------------------------------------------------------------------- 1 | # 3_LiveExcel 2 | 3 | 4 | * ```LiveExcelLogix.py``` -> Reads an open Excel file in realtime and Writes to a PLC via EtherNet/IP 5 | * ```LiveExcelOPC.py``` -> Reads an open Excel file in realtime and Writes to a PLC via OPC-UA 6 | -------------------------------------------------------------------------------- /3_LiveExcel/requirements.txt: -------------------------------------------------------------------------------- 1 | xlwings 2 | pycomm3 -------------------------------------------------------------------------------- /4_L5X_Editor/L5X_Reader.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import l5x 3 | 4 | # Setup File Names 5 | fileName = "PLC01.L5X" 6 | saveFileName = fileName.split(".")[0] + "_tags.xlsx" 7 | 8 | # Load the L5X file 9 | prj = l5x.Project(fileName) 10 | 11 | # Create a list of dictionaries for the tags 12 | fullTagDataList = [] 13 | 14 | # Iterate over the tags 15 | for tag in prj.controller.tags.names: 16 | # Get Tag Name and Data Type 17 | try: 18 | dt = prj.controller.tags[tag].data_type 19 | except: 20 | fullTagDataList.append({"Name": tag, "Data Type": "Unknown"}) 21 | else: 22 | fullTagDataList.append({"Name": tag, "Data Type": dt}) 23 | 24 | # Get Description 25 | try: 26 | desc = prj.controller.tags[tag].description 27 | except: 28 | fullTagDataList[-1]["Description"] = "Unknown" 29 | else: 30 | fullTagDataList[-1]["Description"] = desc 31 | 32 | # Convert the list of dictionaries to a DataFrame 33 | df = pd.DataFrame(fullTagDataList) 34 | 35 | # Save the DataFrame to an Excel file 36 | df.to_excel(saveFileName, index=False) 37 | -------------------------------------------------------------------------------- /4_L5X_Editor/L5X_Writer.py: -------------------------------------------------------------------------------- 1 | import l5x 2 | 3 | # Setup File Names 4 | fileName = "PLC01.L5X" 5 | saveFileName = fileName.split(".")[0] + "_Edited.L5X" 6 | 7 | # Load the L5X file 8 | prj = l5x.Project(fileName) 9 | 10 | # Create a list of dictionaries for the tags 11 | fullTagDataList = [] 12 | 13 | # Iterate over the tags 14 | for tag in prj.controller.tags.names: 15 | # Get Tag Description and update it 16 | try: 17 | desc = prj.controller.tags[tag].description 18 | except: 19 | pass 20 | else: 21 | oldText = "" if desc == None else desc 22 | prj.controller.tags[tag].description = "New Text Portion " + oldText 23 | 24 | # Save to a new L5X file 25 | prj.write(saveFileName) 26 | -------------------------------------------------------------------------------- /4_L5X_Editor/README.md: -------------------------------------------------------------------------------- 1 | # 4_L5X_Editor 2 | 3 | * ```L5X_Reader.py``` -> Reads a L5X PLC file and generates an Excel Tag List 4 | * ```L5X_Writer.py``` -> Reads a L5X PLC file, edits the Tag Descriptions and saves the edited file as an L5X 5 | -------------------------------------------------------------------------------- /4_L5X_Editor/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | l5x -------------------------------------------------------------------------------- /5_TkinterGUI/README.md: -------------------------------------------------------------------------------- 1 | # 5_TkinterGUI 2 | 3 | * ```TkinterGUI.pyw``` -> Tkinter GUI to read and write to a PLC tag via OPC-UA or EtherNet/IP 4 | -------------------------------------------------------------------------------- /5_TkinterGUI/TkinterGUI.pyw: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from pylogix import PLC 4 | from asyncua.sync import Client,ua 5 | from ttkthemes import ThemedTk 6 | 7 | defaultIPAddress="192.168.123.100" 8 | defaultSlot=2 9 | defaultLogixTagName="myReal" 10 | defaultOPCAddress="opc.tcp://192.168.123.100:49320" 11 | defaultOPCTagName="ns=2;s=Channel1.Device1.myReal" 12 | 13 | class GUI(object): 14 | def __init__(self): 15 | #Tkinter Setup 16 | self.root = ThemedTk(theme='yaru') 17 | self.root.title("TkinterGUI") 18 | self.root.state('zoomed') 19 | self.frameMain = ttk.Frame(self.root) 20 | self.frameMain.pack(expand=True, fill="both") 21 | #Add frames 22 | self.frameTags=ttk.LabelFrame(self.frameMain,padding=15,text="Tag Setup") 23 | self.frameTags.grid(row=0, column=0, padx=10,pady=10, sticky="NESW") 24 | self.frameComms=ttk.LabelFrame(self.frameMain,padding=15,text="Comms Setup") 25 | self.frameComms.grid(row=1, column=0, padx=10,pady=10, sticky="NESW") 26 | self.widgetsTagsFrame() 27 | self.widgetsCommsFrame() 28 | 29 | def widgetsTagsFrame(self): 30 | #Static Labels 31 | ttk.Label(self.frameTags, text="Tag Address").grid(row=0,column=0,padx=10,pady=10,sticky="W") 32 | ttk.Label(self.frameTags, text="Value").grid(row=0,column=1,padx=10,pady=10) 33 | ttk.Label(self.frameTags, text="Actual Value").grid(row=0,column=2,padx=10,pady=10) 34 | ttk.Label(self.frameTags, text="Status").grid(row=0,column=3,padx=10,pady=10) 35 | #Tag Name 36 | self.tagName=tk.StringVar(value=defaultLogixTagName) 37 | self.tagNameEntry = ttk.Entry(self.frameTags, width=40,textvariable=self.tagName) 38 | self.tagNameEntry.grid(row=1,column=0,padx=10,pady=10) 39 | #Value to Write 40 | self.writeValue=tk.DoubleVar(value=0.0) 41 | self.writeValueEntry = ttk.Entry(self.frameTags, width=10,textvariable=self.writeValue,justify='center') 42 | self.writeValueEntry.grid(row=1,column=1,padx=10,pady=10) 43 | #Actual Value and Status 44 | self.tagValue=tk.StringVar(value="") 45 | ttk.Label(self.frameTags, textvariable=self.tagValue).grid(row=1,column=2,padx=10,pady=10) 46 | self.tagStatus=tk.StringVar(value="") 47 | ttk.Label(self.frameTags, textvariable=self.tagStatus).grid(row=1,column=3,padx=10,pady=10) 48 | #Buttons Setup 49 | self.buttonWrite = ttk.Button(self.frameTags, text="Write to PLC", command=lambda :[self.writeData()]) 50 | self.buttonWrite.grid(row=3,column=1,padx=10,pady=10,sticky="NESW") 51 | self.buttonRead = ttk.Button(self.frameTags, text="Read from PLC", command=lambda :[self.readData()]) 52 | self.buttonRead.grid(row=3,column=2,padx=10,pady=10,sticky="NESW") 53 | self.buttonReset = ttk.Button(self.frameTags, text="Reset", command=lambda :[self.reset()]) 54 | self.buttonReset.grid(row=3,column=3,padx=10,pady=10,sticky="NESW") 55 | 56 | def widgetsCommsFrame(self): 57 | #Comms Setup 58 | ttk.Label(self.frameComms, text="PLC Address:").grid(row=0,column=0,padx=10,pady=10,sticky="E") 59 | self.plcAddress=tk.StringVar(value=defaultIPAddress) 60 | self.addressEntry = ttk.Entry(self.frameComms, width=40,textvariable=self.plcAddress) 61 | self.addressEntry.grid(row=0,column=1,padx=10,pady=10,sticky="W") 62 | #Slot 63 | ttk.Label(self.frameComms, text="Slot:").grid(row=1,column=0,padx=10,pady=10,sticky="E") 64 | self.plcSlot=tk.IntVar(value=defaultSlot) 65 | self.slotEntry = ttk.Entry(self.frameComms, width=5,textvariable=self.plcSlot) 66 | self.slotEntry.grid(row=1,column=1,padx=10,pady=10,sticky="W") 67 | #Error Note 68 | self.guiErrorNote=tk.StringVar(value="") 69 | ttk.Label(self.frameComms, textvariable=self.guiErrorNote).grid(row=2,column=0,columnspan=3,padx=10,pady=10,sticky="W") 70 | #Radio Buttons 71 | self.radioSelector=tk.StringVar(value="Logix") 72 | self.radiobuttonLogix=ttk.Radiobutton(self.frameComms, text = "Logix", variable=self.radioSelector, command=self.pickLogix, value = "Logix") 73 | self.radiobuttonLogix.grid(row=0,column=2, padx=10,pady=10) 74 | self.radiobuttonOPC=ttk.Radiobutton(self.frameComms, text = "OPC-UA", variable=self.radioSelector,command=self.pickOPC, value = "OPC") 75 | self.radiobuttonOPC.grid(row=0,column=3, padx=10,pady=10) 76 | 77 | def readData(self): 78 | try: 79 | if self.radioSelector.get()=="Logix": 80 | tag=self.tagName.get() 81 | self.comm=PLC() 82 | self.comm.IPAddress=self.plcAddress.get() 83 | self.comm.ProcessorSlot=self.plcSlot.get() 84 | ret=self.comm.Read(tag) 85 | self.tagValue.set(ret.Value if ret.Value==None else round(ret.Value,2)) 86 | self.tagStatus.set(ret.Status) 87 | self.comm.Close() 88 | 89 | else: 90 | tag=self.tagName.get() 91 | self.comm=Client(self.plcAddress.get()) 92 | self.comm.connect() 93 | ret=self.comm.get_node(tag).read_value() 94 | self.tagValue.set(ret if ret==None else round(ret,2)) 95 | self.tagStatus.set("Error" if ret==None else "Success") 96 | self.comm.disconnect() 97 | 98 | except Exception as e: 99 | self.guiErrorNote.set(str(e)) 100 | 101 | def writeData(self): 102 | try: 103 | if self.radioSelector.get()=="Logix": 104 | tag=self.tagName.get() 105 | val=self.writeValue.get() 106 | self.comm=PLC() 107 | self.comm.IPAddress=self.plcAddress.get() 108 | self.comm.ProcessorSlot=self.plcSlot.get() 109 | ret=self.comm.Write(tag,val) 110 | self.tagValue.set(ret.Value if ret.Value==None else round(ret.Value,2)) 111 | self.tagStatus.set(ret.Status) 112 | self.comm.Close() 113 | 114 | else: 115 | tag=self.tagName.get() 116 | val=self.writeValue.get() 117 | self.comm=Client(self.plcAddress.get()) 118 | self.comm.connect() 119 | self.comm.get_node(tag).write_value(ua.DataValue(float(val))) 120 | ret=self.comm.get_node(tag).read_value() 121 | self.tagValue.set(ret if ret==None else round(ret,2)) 122 | self.tagStatus.set("Error" if ret==None else "Success") 123 | self.comm.disconnect() 124 | 125 | except Exception as e: 126 | self.guiErrorNote.set(str(e)) 127 | 128 | def reset(self): 129 | self.tagValue.set("") 130 | self.tagStatus.set("") 131 | self.guiErrorNote.set("") 132 | self.writeValue.set(0.0) 133 | 134 | def pickLogix(self): 135 | self.plcAddress.set(defaultIPAddress) 136 | self.plcSlot.set(defaultSlot) 137 | self.tagName.set(defaultLogixTagName) 138 | 139 | def pickOPC(self): 140 | self.plcAddress.set(defaultOPCAddress) 141 | self.plcSlot.set(0) 142 | self.tagName.set(defaultOPCTagName) 143 | 144 | gui=GUI() 145 | gui.root.mainloop() -------------------------------------------------------------------------------- /5_TkinterGUI/requirements.txt: -------------------------------------------------------------------------------- 1 | pylogix 2 | asyncua 3 | ttkthemes -------------------------------------------------------------------------------- /6_OCR_GUI/PythonGUI_OCR.pyw: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | from tkinter import ttk 3 | from ttkthemes import ThemedTk 4 | from pylogix import PLC 5 | import time 6 | import threading 7 | from PIL import ImageGrab 8 | import cv2 9 | import pytesseract 10 | from pytesseract import Output 11 | 12 | class PeriodicInterval(threading.Thread): 13 | def __init__(self, task_function, period): 14 | super().__init__() 15 | self.daemon = True 16 | self.task_function = task_function 17 | self.period = period 18 | self.i = 0 19 | self.t0 = time.time() 20 | self.stopper = 0 21 | self.start() 22 | 23 | def sleep(self): 24 | self.i += 1 25 | delta = self.t0 + self.period * self.i - time.time() 26 | if delta > 0: 27 | time.sleep(delta) 28 | 29 | def run(self): 30 | while self.stopper == 0: 31 | self.task_function() 32 | self.sleep() 33 | 34 | def stop(self): 35 | self.stopper = 1 36 | 37 | # Setup PLC Comms 38 | comm = PLC() 39 | comm.IPAddress = "192.168.123.100" 40 | comm.ProcessorSlot = 2 41 | 42 | # Setup GUI 43 | root = ThemedTk(theme="yaru") 44 | root.title("Python GUI for ControlLogix") 45 | root.state("zoomed") 46 | frameMain = ttk.Frame(root) 47 | frameMain.pack(expand=True, fill="both") 48 | # Add Canvas 49 | canvasHome = tk.Canvas(frameMain) 50 | canvasHome.pack(side="left", fill="both", expand=True) 51 | frameHome = ttk.Frame(frameMain) 52 | frameHome.pack(side="right", fill="both", expand=True) 53 | # Add GUI variables 54 | runTimeText = tk.IntVar(value=0) 55 | convertedText = tk.StringVar(value="") 56 | # Add button 57 | button_clear = ttk.Button(frameHome, text="Clear", command=lambda: [clear()]) 58 | button_clear.grid(row=0, column=0, columnspan=1, padx=10, pady=3, sticky="NESW") 59 | # Add Labels 60 | ttk.Label(frameHome, text="").grid(row=1, column=0, padx=10, pady=3, sticky="W") 61 | ttk.Label(frameHome, text="").grid(row=2, column=0, padx=10, pady=3, sticky="W") 62 | ttk.Label(frameHome, text="Run Time: ").grid(row=3, column=0, padx=10, pady=3, sticky="E") 63 | ttk.Label(frameHome, textvariable=runTimeText).grid(row=3, column=1, padx=10, pady=3, sticky="W") 64 | ttk.Label(frameHome, text="Drawing to Text: ").grid(row=4, column=0, padx=10, pady=3, sticky="E") 65 | ttk.Label(frameHome, textvariable=convertedText).grid(row=4, column=1, padx=10, pady=3, sticky="W") 66 | 67 | def start(): 68 | # Write Start command to PLC and start loop to read RunTime 69 | global loop_record 70 | ret = comm.Write([("Start", 1), ("Stop", 0)]) 71 | loop_record = PeriodicInterval(getData, 0.5) 72 | 73 | def stop(): 74 | # Write Stop command to PLC and stop loop 75 | ret = comm.Write([("Start", 0), ("Stop", 1)]) 76 | loop_record.stop() 77 | 78 | def getData(): 79 | # Read current RunTime from PLC and update GUI 80 | liveData = comm.Read("RunTime") 81 | runTimeText.set(liveData.Value) 82 | 83 | def get_x_and_y(event): 84 | # Mouse Pointer Location 85 | global lastx, lasty 86 | lastx, lasty = event.x, event.y 87 | 88 | def draw_smth(event): 89 | # Draw line at Mouse Pointer Location 90 | global lastx, lasty 91 | canvasHome.create_line((lastx, lasty, event.x, event.y), fill="black", width=4) 92 | lastx, lasty = event.x, event.y 93 | 94 | def get_image(widget): 95 | # Take a snapshot of the canvas and save image 96 | x = root.winfo_rootx() + widget.winfo_x() 97 | y = root.winfo_rooty() + widget.winfo_y() 98 | x1 = x + widget.winfo_width() 99 | y1 = y + widget.winfo_height() 100 | ImageGrab.grab().crop((x, y, x1, y1)).save("canvasImage.jpg") 101 | 102 | def clear(): 103 | canvasHome.delete("all") 104 | convertedText.set("") 105 | 106 | def convert(event): 107 | # Read the saved image and covert the image to a string 108 | get_image(canvasHome) 109 | img = cv2.imread("canvasImage.jpg") 110 | pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" 111 | cmdText = pytesseract.image_to_string(img, lang="eng").strip() 112 | convertedText.set(cmdText) 113 | if "Start" in cmdText or "start" in cmdText: 114 | start() 115 | elif "Stop" in cmdText or "stop" in cmdText: 116 | stop() 117 | 118 | # Left-Click to start drawing 119 | canvasHome.bind("", get_x_and_y) 120 | # Left-Click held down and motion adds drawing to canvas 121 | canvasHome.bind("", draw_smth) 122 | # Right-Click or Middle-Click triggers convert function 123 | canvasHome.bind("", convert) 124 | canvasHome.bind("", convert) 125 | 126 | root.mainloop() 127 | -------------------------------------------------------------------------------- /6_OCR_GUI/README.md: -------------------------------------------------------------------------------- 1 | # 6_OCR_GUI 2 | 3 | * ```PythonGUI_OCR.pyw``` -> Tkinter GUI to read and write PLC tags via EtherNet/IP with Optical Character Recognition (OCR) 4 | 5 | --- 6 | 7 | Instructions: 8 | 1. Execute the .pyw file. 9 | 2. Begin drawing by clicking the left mouse button. 10 | 3. Convert the drawing to text by clicking the right mouse button. 11 | 12 | --- 13 | 14 | Note: 15 | 16 | - Requires Tesseract to be installed: https://github.com/UB-Mannheim/tesseract/wiki 17 | -------------------------------------------------------------------------------- /6_OCR_GUI/requirements.txt: -------------------------------------------------------------------------------- 1 | ttkthemes 2 | pylogix 3 | opencv-python 4 | pytesseract 5 | -------------------------------------------------------------------------------- /7_WebGUI/README.md: -------------------------------------------------------------------------------- 1 | # 7_WebGUI 2 | 3 | * ```WebGUI.py``` -> Web Server (Flask) to read and write PLC tags via EtherNet/IP 4 | -------------------------------------------------------------------------------- /7_WebGUI/WebGUI.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, render_template, request 2 | from pylogix import PLC 3 | 4 | app = Flask(__name__) 5 | 6 | # Setup PLC Comms 7 | comm = PLC() 8 | comm.IPAddress = "192.168.123.100" 9 | comm.ProcessorSlot = 2 10 | 11 | 12 | # Process data requests and commands 13 | @app.route("/data", methods=["GET", "POST"]) 14 | def data(): 15 | if request.method == "POST": 16 | jsonData = request.get_json() 17 | if jsonData["cmd"] == "Start": 18 | ret = comm.Write([("Start", 1), ("Stop", 0)]) 19 | return {"sts": jsonData["sts"] + " - " + ret[0].Status} 20 | else: 21 | ret = comm.Write([("Start", 0), ("Stop", 1)]) 22 | return {"sts": jsonData["sts"] + " - " + ret[1].Status} 23 | if request.method == "GET": 24 | liveData = comm.Read("RunTime") 25 | return {"sts": liveData.Value} 26 | 27 | 28 | # Create Index 29 | @app.route("/") 30 | def index(): 31 | return render_template("index.html") 32 | 33 | 34 | app.run() 35 | # app.run(host="192.168.1.15") 36 | -------------------------------------------------------------------------------- /7_WebGUI/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | pylogix -------------------------------------------------------------------------------- /7_WebGUI/static/script.js: -------------------------------------------------------------------------------- 1 | const startButton = document.getElementById('start'); 2 | const stopButton = document.getElementById('stop'); 3 | 4 | startButton.addEventListener('click', function () { 5 | fetch('/data', { 6 | headers: { 7 | 'Content-Type': 'application/json' 8 | }, 9 | method: 'POST', 10 | body: JSON.stringify({ 11 | 'cmd': 'Start', 12 | 'sts': 'Start Req.' 13 | }) 14 | }) 15 | .then(function (response) { 16 | if (response.ok) { 17 | response.json() 18 | .then(function (response) { 19 | console.log(response) 20 | document.getElementById("reqStatus").innerHTML = response["sts"]; 21 | }); 22 | } 23 | else { 24 | throw Error('Something went wrong'); 25 | } 26 | }) 27 | .catch(function (error) { 28 | console.log(error); 29 | }); 30 | }); 31 | 32 | stopButton.addEventListener('click', function () { 33 | fetch('/data', { 34 | headers: { 35 | 'Content-Type': 'application/json' 36 | }, 37 | method: 'POST', 38 | body: JSON.stringify({ 39 | 'cmd': 'Stop', 40 | 'sts': 'Stop Req.' 41 | }) 42 | }) 43 | .then(function (response) { 44 | 45 | if (response.ok) { 46 | response.json() 47 | .then(function (response) { 48 | console.log(response) 49 | document.getElementById("reqStatus").innerHTML = response["sts"]; 50 | }); 51 | } 52 | else { 53 | throw Error('Something went wrong'); 54 | } 55 | }) 56 | .catch(function (error) { 57 | console.log(error); 58 | }); 59 | }); 60 | 61 | var myVar = setInterval(function () { displayRunT() }, 500); 62 | function displayRunT() { 63 | fetch('/data') 64 | .then(function (response) { 65 | if (response.ok) { 66 | response.json() 67 | .then(function (response) { 68 | console.log(response) 69 | document.getElementById("runTime").innerHTML = "Run Time: " + response["sts"]; 70 | }); 71 | } 72 | else { 73 | throw Error('Something went wrong'); 74 | } 75 | }) 76 | .catch(function (error) { 77 | console.log(error); 78 | }); 79 | } -------------------------------------------------------------------------------- /7_WebGUI/static/style.css: -------------------------------------------------------------------------------- 1 | .button { 2 | width: 200px; 3 | height: 60px; 4 | display: inline-block; 5 | font-size: 18px; 6 | cursor: pointer; 7 | text-align: center; 8 | text-decoration: none; 9 | outline: none; 10 | color: #fff; 11 | background-color: #4CAF50; 12 | border: none; 13 | border-radius: 15px; 14 | box-shadow: 0 9px #999; 15 | } 16 | 17 | .button:hover { 18 | background-color: #3e8e41 19 | } 20 | 21 | .button:active { 22 | background-color: #3e8e41; 23 | box-shadow: 0 5px #666; 24 | transform: translateY(5px); 25 | } 26 | 27 | body { 28 | font-family: "Gill Sans", sans-serif; 29 | font-size: 25pt; 30 | margin: 2%; 31 | } 32 | 33 | b { 34 | vertical-align: bottom; 35 | } -------------------------------------------------------------------------------- /7_WebGUI/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Simple Web GUI for ControlLogix 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Run Time: 17 |

Ready...

18 | 19 | 20 | -------------------------------------------------------------------------------- /8_WatchWindow/README.md: -------------------------------------------------------------------------------- 1 | # 8_WatchWindow 2 | 3 | * ```WatchWindow.pyw``` -> Watch Window for Logix PLCs to read tags via EtherNet/IP 4 | -------------------------------------------------------------------------------- /8_WatchWindow/WatchWindow.pyw: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | import winreg 4 | import ttkbootstrap as ttk 5 | from ttkbootstrap.scrolled import ScrolledFrame 6 | from pycomm3 import LogixDriver 7 | 8 | 9 | class AutocompleteCombobox(ttk.Combobox): 10 | def __init__(self, parent, values, **kwargs): 11 | super().__init__(parent, **kwargs) 12 | self.values = values 13 | self.configure(values=self.values) 14 | self.bind("", self.autocomplete) 15 | 16 | def autocomplete(self, event): 17 | """ 18 | Filter tag values 19 | """ 20 | current_text = self.get() 21 | matching_values = [value for value in self.values if current_text.lower() in value.lower()] 22 | self.configure(values=matching_values) 23 | 24 | 25 | class PeriodicInterval(threading.Thread): 26 | def __init__(self, task_function, period): 27 | super().__init__() 28 | self.daemon = True 29 | self.task_function = task_function 30 | self.period = period 31 | self.i = 0 32 | self.t0 = time.time() 33 | self.stopper = threading.Event() 34 | self.start() 35 | 36 | def sleep(self): 37 | self.i += 1 38 | delta = self.t0 + self.period * self.i - time.time() 39 | if delta > 0: 40 | self.stopper.wait(delta) 41 | 42 | def run(self): 43 | while not self.stopper.is_set(): 44 | self.task_function() 45 | self.sleep() 46 | 47 | def stop(self): 48 | self.stopper.set() 49 | 50 | def starter(self): 51 | self.stopper.clear() 52 | self.i = 0 53 | self.t0 = time.time() 54 | 55 | 56 | class GUIApp(object): 57 | def __init__(self): 58 | # Setup defaults 59 | self.rows = [] 60 | self.comm = None 61 | self.cyclicThread = None 62 | # Setup Window 63 | self.root = ttk.Window() 64 | self.root.state("zoomed") 65 | self.root.title("WatchWindow") 66 | try: 67 | style = ttk.Style() 68 | key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") 69 | subkeyLightMode = winreg.QueryValueEx(key, "AppsUseLightTheme")[0] 70 | if subkeyLightMode: 71 | style.theme_use("yeti") 72 | else: 73 | style.theme_use("vapor") 74 | except: 75 | pass 76 | 77 | # Create the two frames 78 | self.frameControl = ttk.LabelFrame(self.root, text=" Control ", padding=10, bootstyle="primary") 79 | self.frameControl.pack(side="left", fill="both", expand=False, padx=10, pady=10) 80 | self.frameWatch = ttk.LabelFrame(self.root, text=" Watch Window ", padding=10, bootstyle="primary") 81 | self.frameWatch.pack(side="right", fill="both", expand=True, padx=10, pady=10) 82 | self.watchPane = ScrolledFrame(self.frameWatch, autohide=False) 83 | self.watchPane.pack(fill="both", expand=True) 84 | self.frameTable = ttk.Frame(self.watchPane) 85 | self.frameTable.pack(fill="x", padx=(0, 10)) 86 | self.frameAddRemove = ttk.Frame(self.watchPane) 87 | self.frameAddRemove.pack(fill="x") 88 | 89 | ## Control Frame 90 | # Open File Button Placement 91 | self.button_connect = ttk.Button(self.frameControl, text="Connect", command=lambda: [self.connect_to_PLC()]) 92 | self.button_connect.grid(row=0, column=0, columnspan=1, padx=10, pady=10, sticky="NESW") 93 | # Check Config Button Placement 94 | self.button_start = ttk.Button(self.frameControl, text="Start", bootstyle="success", command=lambda: [self.start()]) 95 | self.button_start.grid(row=1, column=0, columnspan=1, padx=10, pady=10, sticky="NESW") 96 | # Read Button Placement 97 | self.button_stop = ttk.Button(self.frameControl, bootstyle="warning", text="Stop", command=lambda: [self.stop()]) 98 | self.button_stop.grid(row=2, column=0, columnspan=1, padx=10, pady=10, sticky="NESW") 99 | # Write Button Placement 100 | self.button_reset = ttk.Button(self.frameControl, bootstyle="danger", text="Reset", command=lambda: [self.reset()]) 101 | self.button_reset.grid(row=3, column=0, columnspan=1, padx=10, pady=10, sticky="NESW") 102 | 103 | # Scale 104 | ttk.Label(self.frameControl, text="Fast Refresh Rate").grid(row=4, column=0, padx=10, pady=10, sticky="NESW") 105 | self.refreshRate = ttk.DoubleVar(value=1.0) 106 | self.refreshRateSlider = ttk.Scale(self.frameControl, bootstyle="primary", orient="vertical", variable=self.refreshRate, from_=0.001, to=2.0) 107 | self.refreshRateSlider.grid(row=5, column=0, rowspan=5, padx=10, pady=10, sticky="NESW") 108 | ttk.Label(self.frameControl, text="Slow Refresh Rate").grid(row=10, column=0, padx=10, pady=10, sticky="NESW") 109 | 110 | # Separator 111 | ttk.Separator(self.frameControl, bootstyle="secondary", orient="horizontal").grid(row=11, column=0, columnspan=2, padx=10, pady=10, sticky="ew") 112 | 113 | # Setup GUI tags 114 | self.ipAddress = ttk.StringVar() 115 | self.plcSlot = ttk.StringVar() 116 | self.status = ttk.StringVar() 117 | 118 | # PLC Comms 119 | ttk.Label(self.frameControl, text="IP Address:").grid(row=12, column=0, padx=10, pady=10, sticky="NESW") 120 | self.ipEntryBox = ttk.Entry(self.frameControl, textvariable=self.ipAddress, bootstyle="primary", width=15) 121 | self.ipEntryBox.grid(row=12, column=1, padx=4, pady=3, sticky="W") 122 | ttk.Label(self.frameControl, text="Slot:").grid(row=13, column=0, padx=10, pady=10, sticky="NESW") 123 | self.slotEntryBox = ttk.Entry(self.frameControl, textvariable=self.plcSlot, bootstyle="primary", width=3) 124 | self.slotEntryBox.grid(row=13, column=1, padx=4, pady=3, sticky="W") 125 | # Status 126 | ttk.Label(self.frameControl, text="Status:").grid(row=14, column=0, padx=10, pady=10, columnspan=6, sticky="W") 127 | ttk.Label(self.frameControl, textvariable=self.status, bootstyle="danger", wraplength=100, width=20).grid(row=14, column=1, padx=10, pady=10, columnspan=1, sticky="W") 128 | 129 | # Create a button to add another row 130 | self.button_add = ttk.Button(self.frameAddRemove, text="Add New Row", command=self.add_row) 131 | self.button_add.pack(side="left", padx=5, pady=5) 132 | 133 | # Create a button to remove the last row 134 | self.button_remove = ttk.Button(self.frameAddRemove, text="Remove Last Row", command=self.remove_row) 135 | self.button_remove.pack(side="left", padx=5, pady=5) 136 | self.button_remove.configure(state="disabled") 137 | # Reset GUI at startup 138 | self.reset() 139 | 140 | def connect_to_PLC(self): 141 | """ 142 | Connect to PLC and get the tag list 143 | """ 144 | self.status.set("") 145 | self.button_connect.configure(state="disabled") 146 | try: 147 | self.comm = LogixDriver(self.ipAddress.get() + "/" + self.plcSlot.get()) 148 | self.comm.open() 149 | self.fullBaseTagList = get_all_tags(self.comm.tags) 150 | for row in self.rows: 151 | row[4].configure(values=self.fullBaseTagList) 152 | row[4].values = self.fullBaseTagList 153 | 154 | except Exception as e: 155 | self.status.set(str(e)) 156 | self.button_connect.configure(state="normal") 157 | else: 158 | self.button_start.configure(state="normal") 159 | self.button_reset.configure(state="normal") 160 | self.ipEntryBox.configure(state="disabled") 161 | self.slotEntryBox.configure(state="disabled") 162 | 163 | def start(self): 164 | """ 165 | Setup the timeout, read the tags once and get the data type 166 | """ 167 | self.status.set("") 168 | self.button_start.configure(state="disabled") 169 | try: 170 | # Generate a tagList 171 | self.tagList = [] 172 | for row in self.rows: 173 | if row[0].get(): 174 | self.tagList.append(row[0].get()) 175 | # If the taglist is empty raise exception otherwise remove empty rows 176 | if len(self.tagList) == 0: 177 | raise Exception("Empty Tag List") 178 | else: 179 | for i in range(len(self.rows) - 1, -1, -1): 180 | if not self.rows[i][0].get(): 181 | self.rows[i][3].destroy() 182 | self.rows.pop(i) 183 | else: 184 | self.rows[i][4].configure(state="disabled") 185 | # Set the timeout 186 | self.comm._sock.sock.settimeout(self.refreshRate.get() + 0.5) 187 | retData = self.comm.read(*self.tagList) 188 | ret = retData if isinstance(retData, list) else [retData] 189 | for i, row in enumerate(self.rows): 190 | row[1].set(ret[i].error if ret[i].value == None else ret[i].value) 191 | row[2].set(ret[i].type) 192 | except Exception as e: 193 | self.status.set(str(e)) 194 | self.button_start.configure(state="normal") 195 | self.refreshRateSlider.configure(state="normal") 196 | for row in self.rows: 197 | row[4].configure(state="normal") 198 | else: 199 | self.button_stop.configure(state="normal") 200 | self.button_add.configure(state="disabled") 201 | self.button_remove.configure(state="disabled") 202 | self.refreshRateSlider.configure(state="disabled") 203 | self.cyclicThread = PeriodicInterval(self.loop_read, self.refreshRate.get()) 204 | 205 | def loop_read(self): 206 | """ 207 | Read the taglist from the PLC 208 | """ 209 | try: 210 | retData = self.comm.read(*self.tagList) 211 | ret = retData if isinstance(retData, list) else [retData] 212 | for i, row in enumerate(self.rows): 213 | row[1].set(ret[i].error if ret[i].value == None else ret[i].value) 214 | except Exception as e: 215 | self.status.set(time.strftime("%H:%M:%S -> ") + str(e)) 216 | 217 | def stop(self): 218 | """ 219 | Stop the periodic thread and re-enable entry boxes 220 | """ 221 | try: 222 | self.cyclicThread.stop() 223 | self.refreshRateSlider.configure(state="normal") 224 | for row in self.rows: 225 | row[4].configure(state="normal") 226 | self.button_add.configure(state="normal") 227 | if len(self.rows) > 1: 228 | self.button_remove.configure(state="normal") 229 | except Exception as e: 230 | self.status.set(str(e)) 231 | finally: 232 | self.button_start.configure(state="normal") 233 | self.button_stop.configure(state="disabled") 234 | self.cyclicThread = None 235 | 236 | def reset(self): 237 | """ 238 | Reset everything to start-up state 239 | """ 240 | self.status.set("") 241 | self.ipAddress.set("192.168.123.100") 242 | self.plcSlot.set("2") 243 | self.button_connect.configure(state="normal") 244 | self.button_start.configure(state="disabled") 245 | self.button_stop.configure(state="disabled") 246 | self.button_reset.configure(state="disabled") 247 | self.refreshRateSlider.configure(state="normal") 248 | self.ipEntryBox.configure(state="normal") 249 | self.slotEntryBox.configure(state="normal") 250 | for widget in self.frameTable.winfo_children(): 251 | widget.destroy() 252 | if self.comm: 253 | try: 254 | self.comm.close() 255 | except: 256 | pass 257 | if self.cyclicThread: 258 | self.cyclicThread.stop() 259 | self.comm = None 260 | self.cyclicThread = None 261 | self.rows = [] 262 | self.fullBaseTagList = [] 263 | self.add_row() 264 | self.button_add.configure(state="normal") 265 | self.button_remove.configure(state="disabled") 266 | 267 | def add_row(self): 268 | self.button_remove.configure(state="normal") 269 | # Create a new row with three entry boxes 270 | row_frame = ttk.Frame(self.frameTable) 271 | row_frame.pack(side="top", fill="x", padx=5, pady=5) 272 | 273 | tag_entry_var = ttk.StringVar() 274 | tag_entry = AutocompleteCombobox(row_frame, textvariable=tag_entry_var, values=self.fullBaseTagList) 275 | tag_entry.pack(side="left", padx=(0, 5), expand=True, fill="x") 276 | 277 | value_entry_var = ttk.StringVar() 278 | value_entry = ttk.Entry(row_frame, textvariable=value_entry_var, state="disabled") 279 | value_entry.pack(side="left", padx=(0, 5)) 280 | 281 | datatype_entry_var = ttk.StringVar() 282 | datatype_entry = ttk.Entry(row_frame, textvariable=datatype_entry_var, state="disabled") 283 | datatype_entry.pack(side="left") 284 | 285 | # Add the new entry variables and row frame to the row list 286 | self.rows.append((tag_entry_var, value_entry_var, datatype_entry_var, row_frame, tag_entry)) 287 | 288 | def remove_row(self): 289 | if len(self.rows) > 1: 290 | self.rows[-1][3].destroy() 291 | self.rows.pop() 292 | if len(self.rows) < 2: 293 | self.button_remove.configure(state="disabled") 294 | 295 | 296 | def explode_struct(struct): 297 | """ 298 | Breaks down a structure 299 | """ 300 | exploded_tags = [] 301 | for attr in struct["attributes"]: 302 | exploded_tags.append(attr) 303 | if struct["internal_tags"][attr]["tag_type"] == "struct": 304 | exploded_tags.extend(f"{attr}.{x}" for x in explode_struct(struct["internal_tags"][attr]["data_type"])) 305 | return exploded_tags 306 | 307 | 308 | def get_all_tags(tags): 309 | """ 310 | Takes in a list of Tags in dictionary format and returns a list of all tags 311 | """ 312 | full_list_of_tags = [] 313 | for tag, tag_data in tags.items(): 314 | full_list_of_tags.append(tag) 315 | if tag_data["tag_type"] == "struct": 316 | full_list_of_tags.extend(f"{tag}.{attr}" for attr in explode_struct(tag_data["data_type"])) 317 | return full_list_of_tags 318 | 319 | 320 | if __name__ == "__main__": 321 | pyQuickWatch = GUIApp() 322 | pyQuickWatch.root.mainloop() 323 | -------------------------------------------------------------------------------- /8_WatchWindow/requirements.txt: -------------------------------------------------------------------------------- 1 | ttkbootstrap 2 | pycomm3 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Destination2Unknown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Quick Start for PLCs 2 | 3 | ### 0_PythonSetup 4 | 5 | * Download and Install Python: https://www.python.org/downloads/ 6 | 7 | * Download and Install an IDE: https://code.visualstudio.com/ 8 | 9 | * Install Python Extension (in VSCode) 10 | 11 | * Use PIP to install required packages via cmd line: 12 | * `pip install pandas` 13 | 14 | ### 1_PLCExcel 15 | 16 | Excel <-> PLC via Ethernet/IP or OPC-UA. 17 | 18 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/b6756f91-d2f1-474f-9483-1fabc43ff7b0 19 | 20 | 21 | --- 22 | ### 2_BasicExcelLogger 23 | 24 | Excel Logger via Ethernet/IP or OPC-UA. 25 | 26 | ![image](https://user-images.githubusercontent.com/92536730/228231219-e147a149-d3e6-4d83-83e4-ec723c7cba15.png) 27 | 28 | 29 | --- 30 | ### 3_LiveExcel 31 | Real-time communication between Excel and a PLC using Ethernet/IP. 32 | 33 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/c84617fa-bdb7-45c7-91c0-1d812ea55b76 34 | 35 | 36 | --- 37 | ### 4_L5X_Reader 38 | A .L5X editor: 39 | * Generate an Excel Tag List 40 | * Edit Tag Descriptions etc.. 41 | --- 42 | ### 5_TkinterGUI 43 | A Tkinter GUI to reada/write PLC tags. 44 | 45 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/49e4d2cd-1273-4fb9-b94c-36515f9af232 46 | 47 | 48 | --- 49 | ### 6_OCR_GUI 50 | A Tkinter GUI to read and write PLC tags via EtherNet/IP using Optical Character Recognition (OCR) 51 | 52 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/e6149ff3-d508-4404-a05d-de67dfa22375 53 | 54 | 55 | --- 56 | ### 7_WebGUI 57 | A Flask Web Server to read and write PLC tags via EtherNet/IP. 58 | 59 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/ea3a1a5a-13e2-4c0b-9e59-3f1d43ea2d4e 60 | 61 | 62 | --- 63 | ### 8_WatchWindow 64 | A Watch Window to read Logix PLC tags via EtherNet/IP. 65 | 66 | https://github.com/Destination2Unknown/pyQuickStart/assets/92536730/e00205d3-6382-4be5-8176-b90460e5e250 67 | 68 | --------------------------------------------------------------------------------