#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2020 Rolf Dahl-Skog # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # #________________________________________________________________ config = {} def configuracion(): """ Lee configuracion desde archivo "config.txt" """ archivo = "config.txt" ruta = os.getcwd() + os.path.sep global config # valores por defecto, si no estan en el archivo. config["mb_ip"] = '192.168.0.100' # ip del PLC config["mb_port"] = 502 config["mb_timeout"] = 0.25 # segundos config["DI_ini"] = 0 # direccion de primer discreto leido config["DI_cant"] = 128 # cuantos discretos leer config["AI_ini"] = 0 # direccion de primer analogo leido config["AI_cant"] = 24 # cuantos analogos leer config["DO_ini"] = 500 # direccion primer discreto para escribir config["DO_cant"] = 8 # cuantos discretos para escribir config["AO_ini"] = 100 # direccion primer analogo para escribir config["AO_cant"] = 4 # cuantos analogos para escribir if (os.path.exists(ruta + archivo) != True): tc = "Error: No se encuentra el archivo '"+ archivo +"'\nen la ruta actual\n"+ ruta tc =tc + "\nSe usan valores por defecto." else: # si existe archivo lo lee y analiza conf = configparser.ConfigParser() conf.read(ruta + archivo) print(ruta + archivo) try: config["mb_ip"] = conf.get('Modbus', 'ip') print(" get ip",config["mb_ip"]) except: print(" default ip",config["mb_ip"]) try: config["mb_port"] = conf.getint('Modbus', 'port') print(" get port",config["mb_port"]) except: print(" default port",config["mb_port"]) try: config["mb_timeout"] = conf.getfloat('Modbus', 'timeout') print(" get timeout",config["mb_timeout"]) except: print(" default timeout",config["mb_timeout"]) try: config["DI_ini"] = conf.getint('Digital Read Only', 'init') print(" get DI_ini",config["DI_ini"]) except: print(" default DI_ini",config["DI_ini"]) try: config["DI_cant"] = conf.getint('Digital Read Only', 'count') print(" get DI_cant",config["DI_cant"]) except: print(" default DI_cant",config["DI_cant"]) try: config["AI_ini"] = conf.getint('Analog Read Only', 'init') print(" get AI_ini",config["AI_ini"]) except: print(" default AI_ini",config["AI_ini"]) try: config["AI_cant"] = conf.getint('Analog Read Only', 'count') print(" get AI_cant",config["AI_cant"]) except: print(" default AI_cant",config["AI_cant"]) try: config["DO_ini"] = conf.getint('Digital Read And Write', 'init') print(" get DO_ini",config["DO_ini"]) except: print(" default DO_ini",config["DO_ini"]) try: config["DO_cant"] = conf.getint('Digital Read And Write', 'count') print(" get DO_cant",config["DO_cant"]) except: print(" default DO_cant",config["DO_cant"]) try: config["AO_ini"] = conf.getint('Analog Read And Write', 'init') print(" get AO_ini",config["AO_ini"]) except: print(" default AO_ini",config["AO_ini"]) try: config["AO_cant"] = conf.getint('Analog Read And Write', 'count') print(" get AO_cant",config["AO_cant"]) except: print(" default AO_cant",config["AO_cant"]) tc = "La configuracion se leyo desde '"+ archivo +"'\nen la ruta actual\n"+ ruta return tc #___________________________________________________________ try: import serial except: print("Error: falta libreria pyserial") import socket try: from tkinter import * from tkinter import messagebox except: print("Error: falta libreria tkinter") import configparser import os try: import pymodbus from pymodbus.client.sync import ModbusTcpClient except: messagebox.showerror('Faltan dependencias',"""Error: Faltan dependencias, no se encuentra pymodbus https://github.com/riptideio/pymodbus/ https://pypi.org/project/pymodbus/ En un terminal ejecuta pip install pymodbus""", icon='error') #___________________________________________________________ class dialogAbout(): def __init__(self, parent, texto): ventAbout = self.ventAbout = Toplevel(parent) self.parent = parent ventAbout.title("About") ventAbout.resizable(width=False, height=False) ventAbout.config(bg="#E5E5E5") ventAbout.transient(parent) # esta ventana por sobre ventMain ventAbout.takefocus = True ventAbout.focus_set() LabTit = Label(ventAbout, text="Modbus test") LabTit.config(font=('Verdana', 10, 'bold'), fg="black") LabTit.config(justify=LEFT, anchor='w', bg="#E5E5E5") LabTit.grid(row=0, column=0, sticky="ew", padx=10, pady=2) t = """Esta herramienta es un simple cliente Modbus/Tcp puede leer y escribir, desde/hacia un esclavo (servidor). Es software libre, puedes copiarlo y usarlo, bajo los terminos de la licencia GNU General Public License, version 3. Rolf Dahl-skog Stade. (Abril 2020)""" LabAbout = Label(ventAbout, text= t) LabAbout.config(font=('Verdana', 10, 'normal'), fg="black") LabAbout.config(justify=LEFT, anchor='w', bg="#E5E5E5") LabAbout.grid(row=1, column=0, sticky="ew", padx=10, pady=2) LabConf = Label(ventAbout, text= texto) if "Error" in texto: LabConf.config(font=('Verdana', 10, 'normal'), fg="red") else: LabConf.config(font=('Verdana', 10, 'normal'), fg="black") LabConf.config(justify=LEFT, anchor='w', bg="#E5E5E5") LabConf.grid(row=2, column=0, sticky="ew", padx=10, pady=2) botCancel = Button(ventAbout, text= 'Ok', command= self.BotonCerrar) botCancel.config(borderwidth=3, relief="raised", padx=5, pady=5) botCancel.config(font=('Verdana', 10, 'normal'), anchor='center') botCancel.config(fg="black", bg="#E5E5E5") botCancel.grid(row=10, column=0, sticky="ew", padx=10, pady=2) self.ventAbout.after(180000, self.BotonCerrar) def BotonCerrar(self): self.ventAbout.destroy() def show(self): self.parent.takefocus = True self.parent.focus_set() self.ventAbout.takefocus = True self.ventAbout.focus_set() self.ventAbout.wait_window() #___________________________________________________________ class Aplicacion(): def __init__(self, ventMain): self.ventMain = ventMain self.ventMain.title("Modbus Test") #self.ventMain.geometry('700x360') self.ventMain.minsize(550,100) self.ventMain.maxsize(1300,900) self.ventMain.resizable(width=True,height=True) self.colorFrente = "black" self.colorFondo = "#E5E5E5" self.fuente = ('Verdana', 9, 'normal') self.ventMain.config(bg=self.colorFondo) self.ventMain.grid_rowconfigure(2,weight=1) tc = configuracion() crear_basedatos() # si discretos < 230 usar 10 columnas, sino usar 20 columnas Dcolum = 10 if config["DI_cant"] < 230 else 20 # si analogas < 49 usar 2 columnas, sino usar 4 columnas Acolum = 2 if config["AI_cant"] < 49 else 4 self.LabMsg = Label(self.ventMain, anchor='w') self.LabMsg.config(fg=self.colorFrente, bg=self.colorFondo) self.LabMsg.config(font=self.fuente, justify=CENTER) self.LabMsg.config(borderwidth=0, relief="solid",padx=4, pady=1) self.LabMsg.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=1, pady=1) self.msg= " Leer M"+str(config["DI_ini"]) \ +" a "+str(config["DI_ini"]+config["DI_cant"]-1) \ +" y MW"+str(config["AI_ini"])+" a "+str(config["AI_ini"]+config["AI_cant"]-1) \ +" desde Modbus/TCP "+config["mb_ip"]+" " self.LabMsg.config(text= self.msg) self.LabCom = Label(self.ventMain, anchor='center') self.LabCom.config(fg="red", bg=self.colorFondo) self.LabCom.config(font=('Verdana', 10, 'bold'), justify=CENTER) self.LabCom.config(borderwidth=0, relief="solid",padx=4, pady=1) self.LabCom.grid(row=0, column=2, sticky="nsew", padx=1, pady=1) self.LabCom.config(text= "Sin com.") # arreglo de botones para discretas que se pueden escribir self.cajaDO = Frame(self.ventMain) self.cajaDO.config( borderwidth=0, relief="solid", bg=self.colorFondo) self.cajaDO.grid(row=1, column=0, sticky="w", padx=3, pady=2) self.labDO = [0 for i in range(config["DO_cant"])] for i in range(len(self.labDO)): self.labDO[i] = Label(self.cajaDO) self.labDO[i].config(font=self.fuente, padx=1, pady=0) self.labDO[i].config(text= str(i + config["DO_ini"])) self.labDO[i].config(foreground="black", bg="#E5E5E5") self.labDO[i].config(borderwidth=3, relief="raised", anchor='center') self.labDO[i].bind("", lambda event, num=i: self.DOclick(event,num) ) f = int( i / Dcolum) # en ... columnas c = i % Dcolum self.labDO[i].grid(row=f, column=c, sticky="nsew", padx=2, pady=2) self.cajaDO.grid_rowconfigure(f,weight=0) self.cajaDO.grid_columnconfigure(c,weight=1) # arreglo de luces para discretas que solo se pueden leer self.cajaDI = Frame(self.ventMain) self.cajaDI.config(borderwidth=0, relief="solid", bg=self.colorFondo) self.cajaDI.grid(row=2, column=0, sticky="nsew", padx=3, pady=1) self.labDI = [0 for i in range(config["DI_cant"])] for i in range(len(self.labDI)): self.labDI[i] = Label(self.cajaDI) self.labDI[i].config(font=self.fuente, padx=2, pady=0) self.labDI[i].config(text= str(i + config["DI_ini"])) self.labDI[i].config(foreground="black", bg="#E5E5E5") self.labDI[i].config(borderwidth=1, relief="solid", anchor='center') f = int( i / Dcolum) # en ... columnas c = i % Dcolum self.labDI[i].grid(row=f, column=c, sticky="nsew", padx=1, pady=2) self.cajaDI.grid_rowconfigure(f,weight=0) self.cajaDI.grid_columnconfigure(c,weight=1) self.cajaA = Frame(self.ventMain) self.cajaA.config(borderwidth=0, relief="solid", bg=self.colorFondo) self.cajaA.grid(row=1, column=1, rowspan=2, columnspan=2, sticky="nsew", padx=3, pady=3) # arreglo valores analogas que se pueden escribir self.labAO = [0 for a in range(len(AO))] for i in range(len(self.labAO)): self.labAO[i] = Label(self.cajaA) self.labAO[i].config(font=self.fuente, padx=4, pady=2 ) self.labAO[i].config(text= str(i + config["AO_ini"])+"= "+ str(AO[i]).zfill(5) ) self.labAO[i].config(borderwidth=3, relief="raised", anchor='center') self.labAO[i].config(foreground="black", bg="#E5E5E5") self.labAO[i].bind("", lambda event, num=i: self.AOclick(event,num) ) f1 = int( i / Acolum) # en ... columnas c = i % Acolum self.labAO[i].grid(row=f1, column=c, sticky="nsew", padx=2, pady=2) self.cajaA.grid_rowconfigure(f,weight=0) self.cajaA.grid_columnconfigure(c,weight=1) # arreglo valores analogas que solo se pueden leer self.labAI = [0 for a in range(len(AI))] for i in range(len(self.labAI)): self.labAI[i] = Label(self.cajaA) self.labAI[i].config(font=self.fuente, padx=5, pady=1 ) self.labAI[i].config(text= str(i + config["AI_ini"])+"= "+ str(AI[i]).zfill(5) ) self.labAI[i].config(borderwidth=0, relief="solid", anchor='center') self.labAI[i].config(foreground="black", bg="#E5E5E5") f2 = int( i / Acolum) # en ... columnas c = i % Acolum self.labAI[i].grid(row=(f1+f2+1), column=c, sticky="nsew", padx=4, pady=1) self.cajaA.grid_rowconfigure((f1+f2+1),weight=0) self.cajaA.grid_columnconfigure(c,weight=1) d = dialogAbout(self.ventMain, tc).show() ventMain.after(100, self.Actualizar) #___________________________________________ def DOclick(self,event, num): boton = num direccion = num + config["DO_ini"] donde, resp = dialogDI(self.ventMain, direcc=direccion).show() if resp=='1': r = Escribir_Modbus_coil(donde, True) if not (r is None): print(r) if resp=='0': r = Escribir_Modbus_coil(donde, False) if not (r is None): print(r) #___________________________________________ def AOclick(self,event, num): boton = num direccion = num + config["AO_ini"] if num < len(AO): val = AO[num] else: val = 5000 donde, resp = dialogAI(self.ventMain, direc=direccion, valor=val).show() if resp != None: if resp < 0: num = 32766 - resp else: num = resp r = Escribir_Modbus_Reg(donde, num) if not (r is None): print(r) #___________________________________________ def Actualizar(self): global DI, AI, DO, AO # pedir nuevos datos comOk = Leer_Modbus() if comOk: # si hay comunicacion self.LabCom.config(text= "Com Ok", fg="green") # mostrar estados discretas que se pueden escribir for i in range(len(self.labDO)): if i < len(DO): if DO[i] == "1": self.labDO[i].config(bg="#99FE99") # verde else: self.labDO[i].config(bg="#FFD5C0") # rojo # mostrar estados discretas que solo se pueden leer for i in range(len(self.labDI)): if i < len(DI): if DI[i] == "1": self.labDI[i].config(bg="#99FE99") # verde else: self.labDI[i].config(bg="#FFD5C0") # rojo # mostrar valores analogas que se pueden escribir for i in range(len(self.labAO)): if i < len(AO): self.labAO[i].config(text= str(i + config["AO_ini"]) \ +"= "+ str(AO[i]).zfill(5) ) # mostrar valores analogas que solo se pueden leer for i in range(len(self.labAI)): if i < len(AI): self.labAI[i].config(text= str(i + config["AI_ini"]) \ +"= "+ str(AI[i]).zfill(5) ) else: # si NO hay comunicacion self.LabCom.config(text= "Sin Com.", fg="red") # repetir self.ventMain.after( 250, self.Actualizar) #___________________________________________________________ class dialogDI(): # cambiar una discreta def __init__(self, parent, direcc): dialog = self.dialog = Toplevel(parent) dialog.title('%M'+ str(direcc) +' ?') dialog.minsize(250, 150) dialog.resizable(width=False, height=False) dialog.config(bg="#E5E5E5") dialog.grid_rowconfigure([0,1,2],weight=1) dialog.grid_columnconfigure([0,1],weight=1) dialog.transient(parent) # por sobre... dialog.takefocus = True dialog.focus_set() self.direcc = direcc self.resp = "" self.labTexto = Label(dialog, text='Escribir en %M'+ str(direcc) +' ?') self.labTexto.config( wraplength=190, font=('Verdana', 11, 'normal') ) self.labTexto.config( foreground='black', bg='#E5E5E5') self.labTexto.config( justify=LEFT, anchor='w', padx=5, pady=0) self.labTexto.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=8, pady=10) self.bot1 = Button(dialog, text="1", command= self.Bot_1 ) self.bot1.config(borderwidth=3, relief="raised", padx=1, pady=5) self.bot1.config( font=('Verdana', 11, 'normal'), anchor='center') self.bot1.config(fg="black", bg="#E5E5E5") self.bot1.grid(row=1, column=0, sticky="nsew", padx=8, pady=10) self.bot0 = Button(dialog, text="0", command= self.Bot_0 ) self.bot0.config(borderwidth=3, relief="raised", padx=1, pady=5) self.bot0.config( font=('Verdana', 11, 'normal'), anchor='center') self.bot0.config(fg="black", bg="#E5E5E5") self.bot0.grid(row=1, column=1, sticky="nsew", padx=8, pady=10) self.botCancelar = Button(dialog, text="Cancelar", command= self.Bot_Cancelar ) self.botCancelar.config(borderwidth=3, relief="raised", padx=1, pady=5) self.botCancelar.config( font=('Verdana', 10, 'normal'), anchor='center') self.botCancelar.config(fg="black", bg="#E5E5E5") self.botCancelar.grid(row=2, column=0, columnspan=2, sticky="nsew", padx=8, pady=10) self.botCancelar.focus() dialog.after(30000, self.Bot_Cancelar) def show(self): self.dialog.wait_window() return self.direcc, self.resp def Bot_1(self): self.resp = "1" self.dialog.destroy() def Bot_0(self): self.resp = "0" self.dialog.destroy() def Bot_Cancelar(self): self.resp = "" self.dialog.destroy() #___________________________________________________________ class dialogAI(): # cambiar una analoga def __init__(self, parent, direc, valor): dialog = self.dialog = Toplevel(parent) dialog.title('%MW'+ str(direc) +' ?') dialog.minsize(300, 100) dialog.resizable(width=False, height=False) dialog.config(bg="#E5E5E5") dialog.grid_rowconfigure([0,1,2],weight=1) dialog.grid_columnconfigure([0,1],weight=1) dialog.transient(parent) # por sobre... dialog.takefocus = True dialog.focus_set() self.direc = direc self.resp = valor self.labTexto = Label(dialog, text='Valor para escribir en %MW'+ str(direc) +' ?') self.labTexto.config( wraplength=290, font=('Verdana', 11, 'normal') ) self.labTexto.config( foreground='black', bg='#E5E5E5') self.labTexto.config( justify=LEFT, anchor='w', padx=5, pady=0) self.labTexto.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=8, pady=10) self.entrada1 = Entry(dialog, width=8, justify="left") self.entrada1.insert(0,str(self.resp)) self.entrada1.config(font=('Verdana', 12, 'normal')) self.entrada1.grid(row=1, column=0, padx=20, pady=10) self.lab1 = Label(dialog, justify=LEFT, anchor='w') self.lab1.config(font=('Verdana', 9, 'normal'),foreground='black', bg='#E5E5E5') self.lab1.config(text='Entero 16 bits con signo\nRango valido:\ndesde -32768 hasta +32767') self.lab1.grid(row=1, column=1, padx=2, pady=3) self.botGo = Button(dialog, text="enviar", command= self.Bot_Go ) self.botGo.config(borderwidth=3, relief="raised", padx=1, pady=5) self.botGo.config( font=('Verdana', 11, 'normal'), anchor='center') self.botGo.config(fg="black", bg="#E5E5E5") self.botGo.grid(row=2, column=0, sticky="nsew", padx=8, pady=10) self.botCancelar = Button(dialog, text="Cancelar", command= self.Bot_Cancelar ) self.botCancelar.config(borderwidth=3, relief="raised", padx=1, pady=5) self.botCancelar.config( font=('Verdana', 10, 'normal'), anchor='center') self.botCancelar.config(fg="black", bg="#E5E5E5") self.botCancelar.grid(row=2, column=1, columnspan=2, sticky="nsew", padx=8, pady=10) self.botCancelar.focus() dialog.after(60000, self.Bot_Cancelar) def show(self): self.dialog.wait_window() return self.direc, self.resp def Bot_Go(self): txt = self.entrada1.get().strip() if txt[0] == '-': neg = True txt = txt[1:] else: neg = False if txt.isdigit(): num = int(txt) if neg: num = 65536 - num if num < 65536: self.resp = num self.dialog.destroy() def Bot_Cancelar(self): self.resp = None self.dialog.destroy() #___________________________________________________________ def Leer_Modbus(): global DI, AI, DO, AO # leer coils y registros desde PLC ip = config["mb_ip"] port = config["mb_port"] timeout = config["mb_timeout"] bitsIn = [] regsIn = [] bitsOut = [] regsOut = [] PLC = ModbusTcpClient(host=ip, port=port, timeout=timeout, retries=1) if PLC.connect(): try: if config["DI_cant"] > 0: resp = PLC.read_coils(address=config['DI_ini'], \ count=config["DI_cant"]) bitsIn = resp.bits[0:] if config["AI_cant"] > 0: resp = PLC.read_holding_registers(address=config["AI_ini"], \ count=config["AI_cant"]) regsIn = resp.registers[0:] if config["DO_cant"] > 0: resp = PLC.read_coils(address=config['DO_ini'], \ count=config["DO_cant"]) bitsOut = resp.bits[0:] if config["AO_cant"] > 0: resp = PLC.read_holding_registers(address=config["AO_ini"], \ count=config["AO_cant"]) regsOut = resp.registers[0:] PLC.close() except: pass if len(bitsIn) > 0: t = "" for i in range(len(bitsIn)): if bitsIn[i]: t = t + '1' else: t = t + '0' DI = t if len(regsIn) > 0: analogos = [] for i in range(len(regsIn)): num = regsIn[i] if num > 32767: num = num - 65536 analogos.append( num ) AI = analogos if len(bitsOut) > 0: t = "" for i in range(len(bitsOut)): if bitsOut[i]: t = t + '1' else: t = t + '0' DO = t if len(regsOut) > 0: analogos = [] for i in range(len(regsOut)): num = regsOut[i] if num > 32767: num = num - 65536 analogos.append( num ) AO = analogos if (len(bitsIn) > 0) or (len(regsIn) > 0): return True else: return False #___________________________________________________________ def Escribir_Modbus_coil(direccion, valor): datos = [valor] r = None if direccion >= 0 and direccion < 10000: ip = config["mb_ip"] port = config["mb_port"] timeout = config["mb_timeout"] PLC = ModbusTcpClient(host=ip, port=port, timeout=timeout, retries=1) if PLC.connect(): try: resp = PLC.write_coils(direccion, datos) PLC.close() r = ' send '+ str(valor) +' to coil '+ str(direccion) except: pass return r #___________________________________________________________ def Escribir_Modbus_Reg(direccion, valores): # valores es una lista de valores, o un unico valor. r = None if direccion >= 0 and direccion < 10000: ip = config["mb_ip"] port = config["mb_port"] timeout = config["mb_timeout"] PLC = ModbusTcpClient(host=ip, port=port, timeout=timeout, retries=1) if PLC.connect(): try: resp = PLC.write_registers(direccion, valores ) PLC.close() r = ' send '+ str(valores) +' to reg '+ str(direccion) except: pass return r #___________________________________________________________ DI = [] AI = [] DO = [] AO = [] def crear_basedatos(): """crear arreglos para guardar los datos""" global DI, AI, DO, AO for i in range(config["DI_cant"]): DI.append("?") for i in range(config["AI_cant"]): AI.append(0) for i in range(config["DO_cant"]): DO.append("?") for i in range(config["AO_cant"]): AO.append(0) #___________________________________________________________ root = Tk() def main(): app = Aplicacion(root) root.mainloop() if __name__ == '__main__': main() #____________________________________________________________