diff --git a/revpipycontrol/revpicheckclient.py b/revpipycontrol/revpicheckclient.py index e6443ee..284ec88 100644 --- a/revpipycontrol/revpicheckclient.py +++ b/revpipycontrol/revpicheckclient.py @@ -10,7 +10,7 @@ import pickle import tkinter from threading import Lock -from xmlrpc.client import ServerProxy +from xmlrpc.client import ServerProxy, MultiCall class RevPiCheckClient(tkinter.Frame): @@ -18,17 +18,20 @@ class RevPiCheckClient(tkinter.Frame): def __init__(self, master, xmlcli, xmlmode=0): """Instantiiert MyApp-Klasse.""" super().__init__(master) - self.pack(fill="both", expand=True) # XML-Daten abrufen self.xmlmode = xmlmode self.cli = xmlcli + + # FIXME: Fehlerabfang self.cli.psstart() + self.lst_devices = self.cli.ps_devices() self.dict_inps = pickle.loads(self.cli.ps_inps().data) self.dict_outs = pickle.loads(self.cli.ps_outs().data) self.lk = Lock() + self.lst_wins = [] self.autorw = tkinter.BooleanVar() self.dowrite = tkinter.BooleanVar() @@ -37,10 +40,25 @@ class RevPiCheckClient(tkinter.Frame): self._createwidgets() # Aktuelle Werte einlesen - self.readvalues() + self.refreshvalues() - def onfrmconf(self, canvas): - canvas.configure(scrollregion=canvas.bbox("all")) + def __chval(self, device, io): + if self.dowrite.get(): + with self.lk: + self.cli.ps_setvalue(device, io[0], io[5].get()) + + # Alles neu einlesen wenn nicht AutoRW aktiv ist + if not self.autorw.get(): + self.refreshvalues() + + def __hidewin(self, win, event=None): + win.withdraw() + + def __showwin(self, win): + if win.winfo_viewable(): + win.withdraw() + else: + win.deiconify() def _createiogroup(self, device, frame, iotype): """Erstellt IO-Gruppen.""" @@ -55,7 +73,7 @@ class RevPiCheckClient(tkinter.Frame): canvas.create_window((4, 4), window=s_frame, anchor="nw") s_frame.bind( - "", lambda event, canvas=canvas: self.onfrmconf(canvas) + "", lambda event, canvas=canvas: self._onfrmconf(canvas) ) rowcount = 0 @@ -76,7 +94,7 @@ class RevPiCheckClient(tkinter.Frame): var = tkinter.BooleanVar() check = tkinter.Checkbutton(s_frame) check["command"] = \ - lambda device=device, io=io: self.chval(device, io) + lambda device=device, io=io: self.__chval(device, io) check["state"] = "disabled" if iotype == "inp" else "normal" check["text"] = "" check["variable"] = var @@ -86,7 +104,7 @@ class RevPiCheckClient(tkinter.Frame): # FIXME: Mehrere Bytes möglich txt = tkinter.Spinbox(s_frame, to=255 * io[1]) txt["command"] = \ - lambda device=device, io=io: self.chval(device, io) + lambda device=device, io=io: self.__chval(device, io) txt["state"] = "disabled" if iotype == "inp" else "normal" txt["width"] = 4 txt["textvariable"] = var @@ -97,19 +115,8 @@ class RevPiCheckClient(tkinter.Frame): rowcount += 1 - def __hidewin(self, win, event=None): - win.withdraw() - - def __showwin(self, win): - if win.winfo_viewable(): - win.withdraw() - else: - win.deiconify() - def _createwidgets(self): """Erstellt den Fensterinhalt.""" - # Hauptfenster - self.master.wm_title("RevPi Onlineview") devgrp = tkinter.LabelFrame(self) devgrp["text"] = "Devices of RevPi" @@ -123,6 +130,7 @@ class RevPiCheckClient(tkinter.Frame): lambda win=win: self.__hidewin(win) ) win.withdraw() + self.lst_wins.append(win) # Devicegruppe erstellen group = tkinter.LabelFrame(win) @@ -145,44 +153,56 @@ class RevPiCheckClient(tkinter.Frame): cntgrp["text"] = "Control" cntgrp.pack(fill="y", side="right") + self.btn_refresh = tkinter.Button(cntgrp) + self.btn_refresh["text"] = "Alle IOs lesen" + self.btn_refresh["command"] = self.refreshvalues + self.btn_refresh.pack(fill="x") + self.btn_read = tkinter.Button(cntgrp) - self.btn_read["text"] = "LESEN" + self.btn_read["text"] = "Inputs einlesen" self.btn_read["command"] = self.readvalues self.btn_read.pack(fill="x") + self.btn_write = tkinter.Button(cntgrp) + self.btn_write["text"] = "Outputs schreiben" + self.btn_write["command"] = self.writevalues + self.btn_write.pack(fill="x") + check = tkinter.Checkbutton(cntgrp) check["command"] = self.toggleauto - check["text"] = "autorefresh processimage" + check["text"] = "Autorefresh values" check["variable"] = self.autorw check.pack(anchor="w") check = tkinter.Checkbutton(cntgrp) check["state"] = "disabled" if self.xmlmode < 3 else "normal" - check["text"] = "write values to processimage" + check["text"] = "Write values to RevPi" check["variable"] = self.dowrite check.pack(anchor="w") - def chval(self, device, io): - if self.dowrite.get(): - with self.lk: - self.cli.ps_setvalue(device, io[0], io[5].get()) + def _onfrmconf(self, canvas): + canvas.configure(scrollregion=canvas.bbox("all")) - # Alles neu einlesen wenn nicht AutoRW aktiv ist - if not self.autorw.get(): - self.readvalues() - - def _readvalues(self): + def _workvalues(self, io_dicts=None, writeout=False): """Alle Werte der Inputs und Outputs abrufen.""" + # Abfragelisten vorbereiten + if io_dicts is None: + io_dicts = [self.dict_inps, self.dict_outs] + # Werte abrufen with self.lk: ba_values = bytearray(self.cli.ps_values().data) + # Multicall zum Schreiben vorbereiten + if writeout: + xmlmc = MultiCall(self.cli) + for dev in self.lst_devices: # io = [name,bytelen,byteaddr,bmk,bitaddress,(tkinter_var)] # IO Typ verarbeiten - for iotype in [self.dict_inps, self.dict_outs]: + for iotype in io_dicts: # ios verarbeiten for io in iotype[dev[0]]: @@ -192,22 +212,50 @@ class RevPiCheckClient(tkinter.Frame): ) if io[4] >= 0: # Bit-IO - io[5].set(bool(int_byte & 1 << io[4])) + new_val = bool(int_byte & 1 << io[4]) + if writeout and new_val != io[5].get(): + xmlmc.ps_setvalue(dev[0], io[0], io[5].get()) + else: + io[5].set(new_val) else: # Byte-IO - io[5].set(int_byte) + if writeout and int_byte != io[5].get(): + xmlmc.ps_setvalue(dev[0], io[0], io[5].get()) + else: + io[5].set(int_byte) + + # Werte per Multicall schreiben + if writeout: + with self.lk: + xmlmc() if self.autorw.get(): - self.master.after(200, self._readvalues) + self.master.after(200, self._workvalues) + + def hideallwindows(self): + for win in self.lst_wins: + win.withdraw() def readvalues(self): if not self.autorw.get(): - self._readvalues() + self._workvalues([self.dict_inps]) + + def refreshvalues(self): + if not self.autorw.get(): + self._workvalues() def toggleauto(self): - self.btn_read["state"] = "disabled" if self.autorw.get() else "normal" + stateval = "disabled" if self.autorw.get() else "normal" + self.btn_refresh["state"] = stateval + self.btn_read["state"] = stateval + self.btn_write["state"] = stateval + if self.autorw.get(): - self._readvalues() + self._workvalues() + + def writevalues(self): + if not self.autorw.get(): + self._workvalues([self.dict_outs], True) # Testdrive diff --git a/revpipycontrol/revpiprogram.py b/revpipycontrol/revpiprogram.py index 06dff81..55f41df 100644 --- a/revpipycontrol/revpiprogram.py +++ b/revpipycontrol/revpiprogram.py @@ -32,6 +32,7 @@ savefile = os.path.join(homedir, ".revpipyplc", "programpath.dat") class RevPiProgram(tkinter.Frame): def __init__(self, master, xmlcli, xmlmode, revpi): + u"""Init RevPiProgram-Class.""" if xmlmode < 2: return None @@ -223,7 +224,7 @@ class RevPiProgram(tkinter.Frame): return {} def _savedefaults(self): - """Schreibt fuer den Pi die letzen Pfade.""" + u"""Schreibt fuer den Pi die letzen Pfade.""" try: makedirs(os.path.dirname(savefile), exist_ok=True) dict_all = self._loaddefault(full=True) @@ -236,7 +237,7 @@ class RevPiProgram(tkinter.Frame): return True def create_filelist(self, rootdir): - """Erstellt eine Dateiliste von einem Verzeichnis. + u"""Erstellt eine Dateiliste von einem Verzeichnis. @param rootdir: Verzeichnis fuer das eine Liste erstellt werden soll @returns: Dateiliste""" filelist = [] @@ -249,7 +250,7 @@ class RevPiProgram(tkinter.Frame): """Gibt das rootdir von einem entpackten Verzeichnis zurueck. Dabei wird geprueft, ob es sich um einen einzelnen Ordner handelt - und ob es eine piCtory Konfiguraiton im rootdir gibt. + und ob es eine piCtory Konfiguration im rootdir gibt. @param rootdir: Verzeichnis fuer Pruefung @returns: Abgeaendertes rootdir @@ -270,6 +271,7 @@ class RevPiProgram(tkinter.Frame): return (rootdir, None) def getpictoryrsc(self): + u"""Läd die piCtory Konfiguration herunter.""" fh = tkfd.asksaveasfile( mode="wb", parent=self.master, confirmoverwrite=True, @@ -300,6 +302,7 @@ class RevPiProgram(tkinter.Frame): fh.close() def getprocimg(self): + u"""Läd das aktuelle Prozessabbild herunter.""" fh = tkfd.asksaveasfile( mode="wb", parent=self.master, confirmoverwrite=True, @@ -330,6 +333,7 @@ class RevPiProgram(tkinter.Frame): fh.close() def setpictoryrsc(self, filename=None): + u"""Überträgt die angegebene piCtory-Konfiguration.""" if filename is None: fh = tkfd.askopenfile( mode="rb", parent=self.master, @@ -381,6 +385,7 @@ class RevPiProgram(tkinter.Frame): fh.close() def picontrolreset(self): + u"""Fürt ein Reset der piBridge durch.""" ask = tkmsg.askyesno( parent=self.master, title="Frage...", message="Soll piControlReset wirklich durchgeführt werden? \n" @@ -401,6 +406,7 @@ class RevPiProgram(tkinter.Frame): ) def plcdownload(self): + u"""Läd das aktuelle Projekt herunter.""" tdown = self.lst_typedown.index(self.var_typedown.get()) fh = None dirselect = "" @@ -491,6 +497,7 @@ class RevPiProgram(tkinter.Frame): fh.close() def plcupload(self): + u"""Lädt das angegebene Projekt auf den RevPi.""" tup = self.lst_typeup.index(self.var_typeup.get()) dirselect = "" dirtmp = None diff --git a/revpipycontrol/revpipycontrol.py b/revpipycontrol/revpipycontrol.py index c107e62..de8cfb5 100755 --- a/revpipycontrol/revpipycontrol.py +++ b/revpipycontrol/revpipycontrol.py @@ -41,6 +41,8 @@ def addroot(filename): class RevPiPyControl(tkinter.Frame): def __init__(self, master=None): + u"""Init RevPiPyControl-Class. + @param master: tkinter master""" super().__init__(master) self.pack(fill="both", expand=True) @@ -48,8 +50,12 @@ class RevPiPyControl(tkinter.Frame): self.dict_conn = revpiplclist.get_connections() self.errcount = 0 self.revpiname = None + self.xmlfuncs = [] self.xmlmode = 0 + # Debugger vorbereiten + self.debugframe = None + # Globale Fenster self.tkcheckclient = None self.tklogs = None @@ -63,14 +69,30 @@ class RevPiPyControl(tkinter.Frame): self.tmr_plcrunning() def _btnstate(self): + u"""Setzt den state der Buttons.""" stateval = "disabled" if self.cli is None else "normal" self.btn_plclogs["state"] = stateval self.btn_plcstart["state"] = stateval self.btn_plcstop["state"] = stateval self.btn_plcrestart["state"] = stateval + self.btn_debug["state"] = stateval + + def _closeall(self): + u"""Schließt alle Fenster.""" + if self.tkcheckclient is not None: + self.tkcheckclient.destroy() + if self.tklogs is not None: + self.tklogs.master.destroy() + if self.tkoptions is not None: + self.tkoptions.destroy() + if self.tkprogram is not None: + self.tkprogram.destroy() + if self.debugframe is not None: + self.debugframe.destroy() + self.debugframe = None def _createwidgets(self): - """Erstellt den Fensterinhalt.""" + u"""Erstellt den Fensterinhalt.""" # Hauptfenster self.master.wm_title("RevPi Python PLC Loader") self.master.wm_iconphoto( @@ -122,26 +144,39 @@ class RevPiPyControl(tkinter.Frame): self.txt_status["textvariable"] = self.var_status self.txt_status.pack(fill="x") - def _fillmbar(self): - # PLC Menü - self.mplc = tkinter.Menu(self.mbar, tearoff=False) - self.mplc.add_command(label="PLC log...", command=self.plclogs) - self.mplc.add_command(label="PLC monitor...", command=self.plcmonitor) - self.mplc.add_command(label="PLC options...", command=self.plcoptions) - self.mplc.add_command(label="PLC program...", command=self.plcprogram) - self.mbar.add_cascade(label="PLC", menu=self.mplc, state="disabled") - - # Connection Menü - self.mconn = tkinter.Menu(self.mbar, tearoff=False) - self.mbar.add_cascade(label="Connect", menu=self.mconn) + self.btn_debug = tkinter.Button(self) + self.btn_debug["text"] = "PLC Debugmodus" + self.btn_debug["command"] = self.plcdebug + self.btn_debug.pack(fill="x") def _fillconnbar(self): + u"""Generiert Menüeinträge für Verbindungen.""" self.mconn.delete(0, "end") for con in sorted(self.dict_conn.keys(), key=lambda x: x.lower()): self.mconn.add_command( label=con, command=partial(self._opt_conn, con) ) + def _fillmbar(self): + u"""Generiert Menüeinträge.""" + # PLC Menü + self.mplc = tkinter.Menu(self.mbar, tearoff=False) + self.mplc.add_command( + label="PLC log...", command=self.plclogs) + self.mplc.add_command( + label="PLC options...", command=self.plcoptions) + self.mplc.add_command( + label="PLC program...", command=self.plcprogram) + self.mplc.add_separator() + + self.mplc.add_command( + label="Disconnect", command=self.serverdisconnect) + self.mbar.add_cascade(label="PLC", menu=self.mplc, state="disabled") + + # Connection Menü + self.mconn = tkinter.Menu(self.mbar, tearoff=False) + self.mbar.add_cascade(label="Connect", menu=self.mconn) + def _opt_conn(self, text): socket.setdefaulttimeout(2) sp = ServerProxy( @@ -151,6 +186,7 @@ class RevPiPyControl(tkinter.Frame): ) # Server prüfen try: + self.xmlfuncs = sp.system.listMethods() self.xmlmode = sp.xmlmodus() except: self.servererror() @@ -168,17 +204,30 @@ class RevPiPyControl(tkinter.Frame): )) self.mbar.entryconfig("PLC", state="normal") - def _closeall(self): - if self.tkcheckclient is not None: - self.tkcheckclient.destroy() - if self.tklogs is not None: - self.tklogs.master.destroy() - if self.tkoptions is not None: - self.tkoptions.destroy() - if self.tkprogram is not None: - self.tkprogram.destroy() + def plcdebug(self): + u"""Baut den Debugframe und packt ihn.""" + self.btn_debug["state"] = "disabled" + + # Debugfenster laden + if self.debugframe is None: + self.debugframe = revpicheckclient.RevPiCheckClient( + self, self.cli, self.xmlmode + ) + + # Show/Hide wechseln + if self.debugframe.winfo_viewable(): + self.debugframe.hideallwindows() + self.debugframe.autorw.set(False) + self.debugframe.toggleauto() + self.debugframe.dowrite.set(False) + self.debugframe.pack_forget() + else: + self.debugframe.pack(fill="y") + + self.btn_debug["state"] = "normal" def plclist(self): + u"""Öffnet das Fenster für die Verbindungen.""" win = tkinter.Toplevel(self) revpiplclist.RevPiPlcList(win) win.focus_set() @@ -188,24 +237,15 @@ class RevPiPyControl(tkinter.Frame): self._fillconnbar() def plclogs(self): + u"""Öffnet das Fenster für Logdateien.""" if self.tklogs is None or len(self.tklogs.children) == 0: win = tkinter.Toplevel(self) self.tklogs = revpilogfile.RevPiLogfile(win, self.cli) else: self.tklogs.focus_set() - def plcmonitor(self): - """Startet das Monitorfenster.""" - if self.tkcheckclient is None or len(self.tkcheckclient.children) == 0: - win = tkinter.Toplevel(self) - self.tkcheckclient = revpicheckclient.RevPiCheckClient( - win, self.cli, self.xmlmode - ) - else: - self.tkcheckclient.focus_set() - def plcoptions(self): - """Startet das Optionsfenster.""" + u"""Startet das Optionsfenster.""" if self.xmlmode < 2: tkmsg.showwarning( parent=self.master, title="Warnung", @@ -222,6 +262,7 @@ class RevPiPyControl(tkinter.Frame): self.xmlmode = self.tkoptions.xmlmode def plcprogram(self): + u"""Startet das Programmfenster.""" if self.xmlmode < 2: tkmsg.showwarning( parent=self.master, title="Warnung", @@ -237,23 +278,30 @@ class RevPiPyControl(tkinter.Frame): self.wait_window(win) def plcstart(self): + u"""Startet das PLC Programm.""" self.cli.plcstart() def plcstop(self): + u"""Beendet das PLC Programm.""" self.cli.plcstop() def plcrestart(self): + u"""Startet das PLC Programm neu.""" self.cli.plcstop() self.cli.plcstart() - def servererror(self): - """Setzt alles auf NULL.""" + def serverdisconnect(self): + u"""Trennt eine bestehende Verbindung.""" socket.setdefaulttimeout(2) self.cli = None self._btnstate() self.mbar.entryconfig("PLC", state="disabled") self.var_conn.set("") self._closeall() + + def servererror(self): + u"""Setzt alles zurück für neue Verbindungen.""" + self.serverdisconnect() tkmsg.showerror("Fehler", "Server ist nicht erreichbar!") def tmr_plcrunning(self):