From 8105323d18b347bb3e1105c5a6d9843e4c6c6a8a Mon Sep 17 00:00:00 2001 From: NaruX Date: Sun, 13 Aug 2017 12:05:23 +0200 Subject: [PATCH] new project started --- .hgignore | 19 + MANIFEST.in | 2 + revpimodio2.e4p | 336 +++++++++++ revpimodio2/__init__.py | 34 ++ revpimodio2/app.py | 22 + revpimodio2/device.py | 1170 +++++++++++++++++++++++++++++++++++++++ revpimodio2/helper.py | 355 ++++++++++++ revpimodio2/io.py | 631 +++++++++++++++++++++ revpimodio2/modio.py | 899 ++++++++++++++++++++++++++++++ revpimodio2/summary.py | 19 + setup.py | 41 ++ 11 files changed, 3528 insertions(+) create mode 100644 .hgignore create mode 100644 MANIFEST.in create mode 100644 revpimodio2.e4p create mode 100644 revpimodio2/__init__.py create mode 100644 revpimodio2/app.py create mode 100644 revpimodio2/device.py create mode 100644 revpimodio2/helper.py create mode 100644 revpimodio2/io.py create mode 100644 revpimodio2/modio.py create mode 100644 revpimodio2/summary.py create mode 100644 setup.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000..4bb729e --- /dev/null +++ b/.hgignore @@ -0,0 +1,19 @@ +glob:.eric6project +glob:_eric6project +glob:.eric5project +glob:_eric5project +glob:.eric4project +glob:_eric4project +glob:.ropeproject +glob:_ropeproject +glob:.directory +glob:**.pyc +glob:**.pyo +glob:**.orig +glob:**.bak +glob:**.rej +glob:**~ +glob:cur +glob:tmp +glob:__pycache__ +glob:**.DS_Store diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..cae98b7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +global-exclude test/* +global-exclude *.pyc diff --git a/revpimodio2.e4p b/revpimodio2.e4p new file mode 100644 index 0000000..a0b8b62 --- /dev/null +++ b/revpimodio2.e4p @@ -0,0 +1,336 @@ + + + + + + + en_US + 7ea159534ad3516e9069331120048abf9b00151e + Python3 + Console + + 2.0.0 + Sven Sager + akira@narux.de + + + setup.py + revpimodio2/modio.py + revpimodio2/summary.py + revpimodio2/app.py + revpimodio2/io.py + revpimodio2/__init__.py + revpimodio2/device.py + revpimodio2/helper.py + + + + + + + doc + .hgignore + + + Mercurial + + + + add + + + + + + + + checkout + + + + + + + + commit + + + + + + + + diff + + + + + + + + export + + + + + + + + global + + + + + + + + history + + + + + + + + log + + + + + + + + remove + + + + + + + + status + + + + + + + + tag + + + + + + + + update + + + + + + + + + + + + + + + + + + + + + + + + ERIC4API + + + + + ignoreDirectories + + + + deb + dist + doc + test + + + + ignoreFilePatterns + + + + setup.py + + + + languages + + + + Python3 + + + + outputFile + + + eric-revpimodio.api + + + useRecursion + + + True + + + + + ERIC4DOC + + + + + ignoreDirectories + + + + deb + dist + doc + test + + + + ignoreFilePatterns + + + + setup.py + + + + noindex + + + True + + + outputDirectory + + + doc + + + qtHelpEnabled + + + False + + + sourceExtensions + + + + + + + + useRecursion + + + True + + + + + + + + + + + Pep8Checker + + + + + DocstringType + + + pep257 + + + ExcludeFiles + + + + + + ExcludeMessages + + + E123,E226,E24 + + + FixCodes + + + + + + FixIssues + + + False + + + HangClosing + + + False + + + IncludeMessages + + + + + + MaxLineLength + + + 79 + + + NoFixCodes + + + E501 + + + RepeatMessages + + + True + + + ShowIgnored + + + False + + + + + + + diff --git a/revpimodio2/__init__.py b/revpimodio2/__init__.py new file mode 100644 index 0000000..5883a0a --- /dev/null +++ b/revpimodio2/__init__.py @@ -0,0 +1,34 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +"""Stellt alle Klassen fuer den RevolutionPi zur Verfuegung. + +Stellt Klassen fuer die einfache Verwendung des Revolution Pis der +Kunbus GmbH (https://revolution.kunbus.de/) zur Verfuegung. Alle I/Os werden +aus der piCtory Konfiguration eingelesen und mit deren Namen direkt zugreifbar +gemacht. Fuer Gateways sind eigene IOs ueber mehrere Bytes konfigurierbar +Mit den definierten Namen greift man direkt auf die gewuenschten Daten zu. +Auf alle IOs kann der Benutzer Funktionen als Events registrieren. Diese +fuehrt das Modul bei Datenaenderung aus. + +""" +import warnings +from .modio import * + +__all__ = ["RevPiModIO", "RevPiModIOSelected", "RevPiModIODriver"] +__author__ = "Sven Sager " +__version__ = "2.0.0" + +# Global package values +OFF = 0 +GREEN = 1 +RED = 2 +RISING = 31 +FALLING = 32 +BOTH = 33 + +warnings.simplefilter(action="always") diff --git a/revpimodio2/app.py b/revpimodio2/app.py new file mode 100644 index 0000000..601010a --- /dev/null +++ b/revpimodio2/app.py @@ -0,0 +1,22 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +"""Bildet die App Sektion von piCtory ab.""" + + +class App(object): + + """Bildet die App Sektion der config.rsc ab.""" + + def __init__(self, app): + """Instantiiert die App-Klasse. + @param app piCtory Appinformationen""" + self.name = app["name"] + self.version = app["version"] + self.language = app["language"] + # TODO: Layout untersuchen und anders abbilden + self.layout = app["layout"] diff --git a/revpimodio2/device.py b/revpimodio2/device.py new file mode 100644 index 0000000..4267111 --- /dev/null +++ b/revpimodio2/device.py @@ -0,0 +1,1170 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +import struct +from .helper import ProcimgWriter +from .io import IOBase, IOType, IntIO, StructIO +from .__init__ import BOTH +from threading import Lock + + +class DeviceList(object): + + """Basisklasse fuer direkten Zugriff auf Device Objekte.""" + + def __init__(self): + """Init DeviceList class.""" + self.__dict_position = {} + + def __contains__(self, key): + """Prueft ob Device existiert. + @param key DeviceName str() / Positionsnummer int() + @return True, wenn Device vorhanden""" + if type(key) == int: + return key in self.__dict_position + else: + return hasattr(self, key) + + def __getitem__(self, key): + """Gibt angegebenes Device zurueck. + @param key DeviceName str() / Positionsnummer int() + @return Gefundenes Device()-Objekt""" + if type(key) == int: + return self.__dict_position[key] + else: + return getattr(self, key) + + def __setattr__(self, key, value): + """Setzt Attribute nur wenn Device. + @param key Attributname + @param value Attributobjekt""" + if issubclass(type(value), Device): + object.__setattr__(self, key, value) + self.__dict_position[value.position] = value + elif key == "_DeviceList__dict_position": + object.__setattr__(self, key, value) + + +class Devicelist(): + + """Enthaelt alle Devices des RevolutionPi Buses.""" + + def __init__(self, parentmodio): + """Instantiiert die einzelnen Bus-Devices. + + @param procimg Dateiname des piControl Devices + + """ + self._modio = parentmodio + self.core = self._modio.core + + def __contains__(self, key): + """Prueft ob Device existiert. + @param key DeviceName str() / Positionsnummer int() + @return True, wenn device vorhanden""" + return key in self._modio.device + + def __getitem__(self, key): + """Gibt angegebenes Device zurueck. + @param key DeviceName str() / Positionsnummer int() + @return Gefundenes RevPiDevice()-Objekt""" + return self._modio.device[key] + + def __iter__(self): + """Gibt alle Devices zurueck. + @return Iterator alle Devices""" + return iter(self._modio._device) + + def __len__(self): + """Gibt Anzahl der Devices zurueck. + @return int() Anzahl der Devices""" + return len(self._modio._device) + + def auto_refresh(self, device, remove=False): + """Registriert ein Device fuer die automatische Synchronisierung. + @param device Device fuer Synchronisierung + @param remove bool() True entfernt Device aus Synchronisierung""" + + dev = device if issubclass(type(device), Device) \ + else self._modio.device[device] + + dev.auto_refresh(remove) + + def auto_refresh_maxioerrors(self, value=None): + """Maximale IO Fehler fuer auto_refresh. + @param value Setzt maximale Anzahl bis exception ausgeloest wird + @return Maximale Anzahl bis exception ausgeloest wird""" + return self._modio.auto_refresh_maxioerrors(value) + + def auto_refresh_resetioerrors(self): + """Setzt aktuellen IOError-Zaehler auf 0 zurueck.""" + self._modio.auto_refresh_resetioerrors() + + def cycleloop(self, func, cycletime=50): + """Startet den Cycleloop. + + Der aktuelle Programmthread wird hier bis Aufruf von + RevPiDevicelist.exit() "gefangen". Er fuehrt nach jeder Aktualisierung + des Prozessabbilds die uebergebene Funktion "func" aus und arbeitet sie + ab. Waehrend der Ausfuehrung der Funktion wird das Prozessabbild nicht + weiter aktualisiert. Die Inputs behalten bis zum Ende den aktuellen + Wert. Gesetzte Outputs werden nach Ende des Funktionsdurchlaufs in das + Prozessabbild geschrieben. + + Verlassen wird der Cycleloop, wenn die aufgerufene Funktion einen + Rueckgabewert nicht gleich None liefert, oder durch Aufruf von + revpimodio.exit(). + + HINWEIS: Die Aktualisierungszeit und die Laufzeit der Funktion duerfen + die eingestellte auto_refresh Zeit, bzw. uebergebene cycletime nicht + ueberschreiten! + + Ueber den Parameter cycletime kann die Aktualisierungsrate fuer das + Prozessabbild gesetzt werden (selbe Funktion wie + set_refreshtime(milliseconds)). + + @param func Funktion, die ausgefuehrt werden soll + @param cycletime auto_refresh Wert in Millisekunden + @return None + + """ + return self._modio.cycleloop(func, cycletime) + + def exit(self, full=True): + """Beendet mainloop() und optional auto_refresh. + + Wenn sich das Programm im mainloop() befindet, wird durch Aufruf + von exit() die Kontrolle wieder an das Hauptprogramm zurueckgegeben. + + Der Parameter full ist mit True vorbelegt und entfernt alle Devices aus + dem auto_refresh. Der Thread fuer die Prozessabbildsynchronisierung + wird dann gestoppt und das Programm kann sauber beendet werden. + + @param full Entfernt auch alle Devices aus auto_refresh""" + self._modio.exit(full) + + def get_devbyname(self, name): + """Gibt durch Namen angegebenes Device zurueck. + @param name Devicename aus piCtory + @return Gefundenes RevPiDevice()""" + return self._modio.device[name] + + def get_devbyposition(self, position): + """Gibt durch Position angegebenes Device zurueck. + @param position Deviceposition aus piCtory + @return Gefundenes RevPiDevice()""" + return self._modio.device[position] + + def get_refreshtime(self): + """Gibt Aktualisierungsrate in ms der Prozessabbildsynchronisierung aus. + @return Millisekunden""" + return self._modio._imgwriter.refresh + + def readprocimg(self, force=False, device=None): + """Einlesen aller Inputs aller Devices vom Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + return self._modio.readprocimg(force, device) + + def mainloop(self, freeze=False, blocking=True): + """Startet den Mainloop mit Eventueberwachung. + + Der aktuelle Programmthread wird hier bis Aufruf von + RevPiDevicelist.exit() "gefangen" (es sei denn blocking=False). Er + durchlaeuft die Eventueberwachung und prueft Aenderungen der, mit + einem Event registrierten, IOs. Wird eine Veraenderung erkannt, + fuert das Programm die dazugehoerigen Funktionen der Reihe nach aus. + + Wenn der Parameter "freeze" mit True angegeben ist, wird die + Prozessabbildsynchronisierung angehalten bis alle Eventfunktionen + ausgefuehrt wurden. Inputs behalten fuer die gesamte Dauer ihren + aktuellen Wert und Outputs werden erst nach Durchlauf aller Funktionen + in das Prozessabbild geschrieben. + + Wenn der Parameter "blocking" mit False angegeben wird, aktiviert + dies die Eventueberwachung und blockiert das Programm NICHT an der + Stelle des Aufrufs. Eignet sich gut fuer die GUI Programmierung, wenn + Events vom RevPi benoetigt werden, aber das Programm weiter ausgefuehrt + werden soll. + + @param freeze Wenn True, Prozessabbildsynchronisierung anhalten + @param blocking Wenn False, blockiert das Programm NICHT + @return None + + """ + return self._modio.mainloop(freeze, blocking) + + def set_refreshtime(self, milliseconds): + """Setzt Aktualisierungsrate der Prozessabbild-Synchronisierung. + @param milliseconds int() in Millisekunden""" + self._modio.set_refreshtime(milliseconds) + + def setdefaultvalues(self, force=False, device=None): + """Alle Outputbuffer werden auf die piCtory default Werte gesetzt. + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden""" + self._modio.setdefaultvalues(force, device) + + def syncoutputs(self, force=False, device=None): + """Lesen aller aktuell gesetzten Outputs im Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + return self._modio.syncoutputs(force, device) + + def updateprocimg(self, force=False, device=None): + """Schreiben/Lesen aller Outputs/Inputs aller Devices im Prozessab. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + return self.readprocimg(force=force, device=device) and \ + self.writeprocimg(force=force, device=device) + + def wait(self, device, io, **kwargs): + """Wartet auf Wertaenderung eines IOs. + + Die Wertaenderung wird immer uerberprueft, wenn fuer Devices + in RevPiDevicelist.auto_refresh() neue Daten gelesen wurden. + + Bei Wertaenderung, wird das Warten mit 0 als Rueckgabewert beendet. + + HINWEIS: Wenn RevPiProcimgWriter() keine neuen Daten liefert, wird + bis in die Ewigkeit gewartet (nicht bei Angabe von "timeout"). + + Wenn edge mit RISING oder FALLING angegeben wird muss diese Flanke + ausgeloest werden. Sollte der Wert 1 sein beim Eintritt mit Flanke + RISING, wird das Warten erst bei Aenderung von 0 auf 1 beendet. + + Als exitevent kann ein threading.Event()-Objekt uebergeben werden, + welches das Warten bei is_set() sofort mit 1 als Rueckgabewert + beendet. + + Wenn der Wert okvalue an dem IO fuer das Warten anliegt, wird + das Warten sofort mit -1 als Rueckgabewert beendet. + + Der Timeoutwert bricht beim Erreichen das Warten sofort mit + Wert 2 Rueckgabewert ab. (Das Timeout wird ueber die Zykluszeit + der auto_refresh Funktion berechnet, entspricht also nicht exact den + angegeben Millisekunden! Es wird immer nach oben gerundet!) + + @param device Device auf dem sich der IO befindet + @param io Name des IOs auf dessen Aenderung gewartet wird + @param kwargs Weitere Parameter: + - edge: Flanke RISING, FALLING, BOTH bei der mit True beendet wird + - exitevent: thrading.Event() fuer vorzeitiges Beenden mit False + - okvalue: IO-Wert, bei dem das Warten sofort mit True beendet wird + - timeout: Zeit in ms nach der mit False abgebrochen wird + @return int() erfolgreich Werte <= 0 + - Erfolgreich gewartet + Wert 0: IO hat den Wert gewechselt + Wert -1: okvalue stimmte mit IO ueberein + - Fehlerhaft gewartet + Wert 1: exitevent wurde gesetzt + Wert 2: timeout abgelaufen + Wert 100: RevPiDevicelist.exit() wurde aufgerufen + + """ + dev = device if issubclass(type(device), Device) \ + else self.__getitem__(device) + + io_watch = dev[io] + if type(io_watch) == list: + if len(io_watch) == 1: + io_watch = io_watch[0] + else: + raise KeyError( + "byte '{}' contains more than one bit-input".format(io) + ) + + # kwargs auswerten + edge = kwargs.get("edge", BOTH) + evt_exit = kwargs.get("exitevent", None) + val_ok = kwargs.get("okvalue", None) + flt_timeout = kwargs.get("timeout", 0) + + return io_watch.wait(edge, evt_exit, val_ok, flt_timeout) + + def writedefaultinputs(self, virtual_device): + """Schreibt fuer ein virtuelles Device piCtory Defaultinputwerte. + + Sollten in piCtory Defaultwerte fuer Inputs eines virtuellen Devices + angegeben sein, werden diese nur beim Systemstart oder einem piControl + Reset gesetzt. Sollte danach das Prozessabbild mit NULL ueberschrieben, + gehen diese Werte verloren. + Diese Funktion kann nur auf virtuelle Devices angewendet werden! + + @param virtual_device Virtuelles Device fuer Wiederherstellung + @return True, wenn Arbeiten am virtuellen Device erfolgreich waren + + """ + return self._modio.writedefaultinputs(virtual_device) + + def writeprocimg(self, force=False, device=None): + """Schreiben aller Outputs aller Devices ins Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + return self._modio.writeprocimg(force, device) + + +class Device(object): + + """Basisklasse fuer alle Device-Objekte der RevPiDevicelist()-Klasse. + + Die Basisfunktionalitaet generiert bei Instantiierung alle IOs und + erweitert den Prozessabbildpuffer um die benoetigten Bytes. Ueber diese + Klasse oder von dieser abgeleiteten Klassen, werden alle IOs angesprochen. + Sie verwaltet ihren Prozessabbildpuffer und sorgt fuer die Aktualisierung + der IO-Werte. + + """ + + def __init__(self, parentmodio, dict_device, **kwargs): + """Instantiierung der Device()-Klasse. + + @param parent RevpiModIO parent object + @param dict_device dict() fuer dieses Device aus piCotry Konfiguration + @param kwargs Weitere Parameter: + - autoupdate: Wenn True fuehrt dieses Device Arbeiten am + Prozessabbild bei Aufruf der RevPiDevicelist-Funktionen aus + - simulator: Laed das Modul als Simulator und vertauscht IOs + + """ + self._modio = parentmodio + + self._dict_events = {} + self._filelock = Lock() + self._length = 0 + self._lst_io = [] + self._selfupdate = False + + self.autoupdate = kwargs.get("autoupdate", True) + + # Wertzuweisung aus dict_device + self.name = dict_device.pop("name") + self.offset = int(dict_device.pop("offset")) + self.position = int(dict_device.pop("position")) + self.producttype = int(dict_device.pop("productType")) + + # Neues bytearray und Kopie für mainloop anlegen + self._ba_devdata = bytearray() + self._ba_datacp = bytearray() + + # Erst inp/out/mem poppen, dann in Klasse einfügen + if kwargs.get("simulator", False): + self.slc_inp = self._buildio(dict_device.pop("out"), IOType.INP) + self.slc_out = self._buildio(dict_device.pop("inp"), IOType.OUT) + else: + self.slc_inp = self._buildio(dict_device.pop("inp"), IOType.INP) + self.slc_out = self._buildio(dict_device.pop("out"), IOType.OUT) + self.slc_mem = self._buildio(dict_device.pop("mem"), IOType.MEM) + + # Alle IOs nach Adresse sortieren + self._lst_io.sort(key=lambda x: x.slc_address.start) + + # SLCs mit offset berechnen + self.slc_devoff = slice(self.offset, self.offset + self._length) + self.slc_inpoff = slice( + self.slc_inp.start + self.offset, self.slc_inp.stop + self.offset + ) + self.slc_outoff = slice( + self.slc_out.start + self.offset, self.slc_out.stop + self.offset + ) + self.slc_memoff = slice( + self.slc_mem.start + self.offset, self.slc_mem.stop + self.offset + ) + + # Alle restlichen attribute an Klasse anhängen + self.__dict__.update(dict_device) + + # Spezielle Konfiguration von abgeleiteten Klassen durchführen + self._devconfigure() + + def __bytes__(self): + """Gibt alle Daten des Devices als bytes() zurueck. + @return Devicedaten als bytes()""" + return bytes(self._ba_devdata) + + def __contains__(self, key): + """Prueft ob IO existiert. + @param key IO-Name str() / Positionsnummer int() + @return True, wenn device vorhanden""" + if type(key) == str: + return hasattr(self._modio.io, key) + if type(key) == int: + key += self.offset + return key in self._modio.io \ + and len(self._modio.io[key]) > 0 + else: + return key in self._lst_io + + def __getitem__(self, key): + """Gibt angegebenes IO-Objekt zurueck. + @param key Name order Byteadresse des IOs + @return IO-Objekt wenn Name, sonst list() mit IO-Objekt""" + if type(key) == int: + key += self.offset + if key in self._modio.io: + return self._modio.io[key] + else: + raise KeyError("byte '{}' does not exist".format(key)) + else: + if hasattr(self._modio.io, key): + return getattr(self._modio.io, key) + else: + raise KeyError("'{}' does not exist".format(key)) + + def __int__(self): + """Gibt die Positon im RevPi Bus zurueck. + @return Positionsnummer""" + return self.position + + def __iter__(self): + """Gibt Iterator aller IOs zurueck. + @return iter() aller IOs""" + return iter(self._lst_io) + + def __str__(self): + """Gibt den Namen des Devices zurueck. + @return Devicename""" + return self.name + + def __setitem__(self, key, value): + """Setzt den Wert des angegebenen Inputs. + @param key Name oder Byte des Inputs + @param value Wert der gesetzt werden soll""" + if type(key) == int: + key += self.offset + if key in self._modio.io: + if len(self._modio.io[key]) == 1: + self._modio.io[key][0].value = value + elif len(self._modio.io[key]) == 0: + raise KeyError("byte '{}' contains no input".format(key)) + else: + raise KeyError( + "byte '{}' contains more than one bit-input" + "".format(key) + ) + else: + raise KeyError("byte '{}' does not exist".format(key)) + else: + getattr(self._modio.io, key).value = value + + def _buildio(self, dict_io, iotype): + """Erstellt aus der piCtory-Liste die IOs fuer dieses Device. + + @param dict_io dict()-Objekt aus piCtory Konfiguration + @param iotype IOType() Wert + @return slice()-Objekt mit Start und Stop Position dieser IOs + + """ + if len(dict_io) > 0: + int_min, int_max = 4096, 0 + for key in sorted(dict_io, key=lambda x: int(x)): + + # Neuen IO anlegen + if bool(dict_io[key][7]) or self.producttype == 95: + # Bei Bitwerten oder Core RevPiIOBase verwenden + io_new = IOBase( + self, + dict_io[key], + iotype, + byteorder="little" + ) + else: + io_new = IntIO( + self, + dict_io[key], + iotype, + byteorder="little" + ) + + # IO registrieren + if hasattr(self._modio.io, io_new.name): + raise NameError( + "name '{}' already exists on device '{}'".format( + io_new._name, self.name + ) + ) + else: + # Namesregister aufbauen + setattr(self._modio.io, io_new._name, io_new) + + # Speicherbereich zuweisen + self._ba_devdata.extend(bytes(io_new._length)) + + # IO eintragen + self._lst_io.append(io_new) + self._length += io_new._length + + # Kleinste und größte Speicheradresse ermitteln + if io_new.slc_address.start < int_min: + int_min = io_new.slc_address.start + if io_new.slc_address.stop > int_max: + int_max = io_new.slc_address.stop + + return slice(int_min, int_max) + + else: + return slice(0, 0) + + def _devconfigure(self): + """Funktion zum ueberschreiben von abgeleiteten Klassen.""" + pass + + def auto_refresh(self, remove=False): + """Registriert ein Device fuer die automatische Synchronisierung. + @param remove bool() True entfernt Device aus Synchronisierung""" + if not remove and self not in self._modio._lst_refresh: + + # Daten bei Aufnahme direkt einlesen! + self._modio.readprocimg(True, self) + + # Datenkopie anlegen + self._filelock.acquire() + self._ba_datacp = self._ba_devdata[:] + self._filelock.release() + + self._selfupdate = True + self._modio._lst_refresh.append(self) + + # Thread starten, wenn er noch nicht läuft + if not self._modio._imgwriter.is_alive(): + + # Alte Einstellungen speichern + imgmaxioerrors = self._modio._imgwriter.maxioerrors + imgrefresh = self._modio._imgwriter.refresh + + # ImgWriter mit alten Einstellungen erstellen + self._modio._imgwriter = ProcimgWriter(self._modio) + self._modio._imgwriter.maxioerrors = imgmaxioerrors + self._modio._imgwriter.refresh = imgrefresh + self._modio._imgwriter.start() + + elif remove and self in self._modio._lst_refresh: + # Sicher aus Liste entfernen + with self._modio._imgwriter.lck_refresh: + self._modio._lst_refresh.remove(self) + self._selfupdate = False + + # Beenden, wenn keien Devices mehr in Liste sind + if len(self._modio._lst_refresh) == 0: + self._modio._imgwriter.stop() + + # Daten beim Entfernen noch einmal schreiben + if not self._modio._monitoring: + self._modio.writeprocimg(True, self) + + def get_inps(self): + """Gibt eine Liste aller Inputs zurueck. + @return list() Inputs""" + return [ + io for io in self._lst_io if io._iotype == IOType.INP + ] + + def get_outs(self): + """Gibt eine Liste aller Outputs zurueck. + @return list() Outputs""" + return [ + io for io in self._lst_io if io._iotype == IOType.OUT + ] + + def get_mems(self): + """Gibt eine Liste aller mems zurueck. + @return list() Mems""" + return [ + io for io in self._lst_io if io._iotype == IOType.MEM + ] + + def get_iobyabsaddress(self, address): + """Gibt das IO-Objekt an angegebenen Byte im Prozessabbild zurueck. + @param address Byteadresse im Prozessabbild + @return list() mit IO-Objekt/en""" + return self[address - self.offset] + + def get_iobyaddress(self, address): + """Gibt das IO-Objekt an angegebenen Byte des Devices zurueck. + @param address Byteadresse im Deviceabbild + @return list() mit IO-Objekt/en""" + return self[address] + + def get_iobyname(self, name): + """Gibt das IO-Objekt mit angegebenen Namen zurueck. + @param name Name des IO-Objekts + @return IO-Objekt""" + return getattr(self._modio.io, name) + + def reg_event(self, io_name, func, edge=BOTH, as_thread=False): + """Registriert ein Event bei der Eventueberwachung. + + @param io_name Name des Inputs oder Outputs der ueberwacht wird + @param func Funktion die bei Aenderung aufgerufen werden soll + @param edge Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @param as_thread Bei True, Funktion als RevPiCallback-Thread ausfuehren + + """ + io_event = self.__getitem__(io_name) + if type(io_event) == list: + if len(io_event) == 1: + io_event = io_event[0] + elif len(io_event) == 0: + raise KeyError( + "byte '{}' contains no io object".format(io_name)) + else: + raise KeyError( + "byte '{}' contains more than one bit io object".format( + io_name + ) + ) + + # NOTE: Abgelaufen + io_event.reg_event(func, edge, as_thread) + + def unreg_event(self, io_name, func=None, edge=None): + """Entfernt ein Event aus der Eventueberwachung. + + @param io_name Name des Inputs, dessen Events entfert werden sollen + @param func Nur Events mit angegebener Funktion + @param edge Nur Events mit angegebener Funktion und angegebener Edge + + """ + io_event = self.__getitem__(io_name) + if type(io_event) == list: + if len(io_event) == 1: + io_event = io_event[0] + elif len(io_event) == 0: + raise KeyError( + "byte '{}' contains no io object".format(io_name)) + else: + raise KeyError( + "byte '{}' contains more than one bit io object".format( + io_name + ) + ) + + # NOTE: Abgelaufen + io_event.unreg_event(func, edge) + + +class Core(Device): + + """Klasse fuer den RevPi Core. + + Stellt Funktionen fuer die LEDs und den Status zur Verfuegung. + + """ + + def _devconfigure(self): + """Core-Klasse vorbereiten.""" + self._iocycle = None + self._iotemperatur = None + self._iofrequency = None + self._ioerrorcnt = None + self._ioled = 1 + self._ioerrorlimit1 = None + self._ioerrorlimit2 = None + + int_lenio = len(self._lst_io) + if int_lenio == 6: + # Core 1.1 + self._iocycle = 1 + self._ioerrorcnt = 2 + self._ioled = 3 + self._ioerrorlimit1 = 4 + self._ioerrorlimit2 = 5 + elif int_lenio == 8: + # core 1.2 + self._iocycle = 1 + self._ioerrorcnt = 2 + self._iotemperatur = 3 + self._iofrequency = 4 + self._ioled = 5 + self._ioerrorlimit1 = 6 + self._ioerrorlimit2 = 7 + + def _errorlimit(self, io_id, errorlimit): + """Verwaltet das Lesen und Schreiben der ErrorLimits. + @param io_id Index des IOs fuer ErrorLimit + @return Aktuellen ErrorLimit oder None wenn nicht verfuegbar""" + if errorlimit is None: + return None if io_id is None else int.from_bytes( + self._lst_io[io_id].get_value(), + byteorder=self._lst_io[io_id]._byteorder + ) + else: + if 0 <= errorlimit <= 65535: + self._lst_io[io_id].set_value(errorlimit.to_bytes( + 2, byteorder=self._lst_io[io_id]._byteorder + )) + else: + raise ValueError( + "errorlimit value int() must be between 0 and 65535" + ) + + def get_status(self): + """Gibt den RevPi Core Status zurueck. + @return Status als int()""" + return int.from_bytes( + self._lst_io[0].get_value(), byteorder=self._lst_io[0]._byteorder + ) + + def get_leda1(self): + """Gibt den Zustand der LED A1 vom core zurueck. + @return 0=aus, 1=gruen, 2=rot""" + int_led = int.from_bytes( + self._lst_io[self._ioled].get_value(), + byteorder=self._lst_io[self._ioled]._byteorder + ) + led = int_led & 1 + led += int_led & 2 + return led + + def get_leda2(self): + """Gibt den Zustand der LED A2 vom core zurueck. + @return 0=aus, 1=gruen, 2=rot""" + int_led = int.from_bytes( + self._lst_io[self._ioled].get_value(), + byteorder=self._lst_io[self._ioled]._byteorder + ) + led = 1 if bool(int_led & 4) else 0 + led = led + 2 if bool(int_led & 8) else led + return led + + def set_leda1(self, value): + """Setzt den Zustand der LED A1 vom core. + @param value 0=aus, 1=gruen, 2=rot""" + if 0 <= value <= 3: + int_led = (self.get_leda2() << 2) + value + self._lst_io[self._ioled].set_value(int_led.to_bytes( + length=1, byteorder=self._lst_io[self._ioled]._byteorder + )) + else: + raise ValueError("led status int() must be between 0 and 3") + + def set_leda2(self, value): + """Setzt den Zustand der LED A2 vom core. + @param value 0=aus, 1=gruen, 2=rot""" + if 0 <= value <= 3: + int_led = (value << 2) + self.get_leda1() + self._lst_io[self._ioled].set_value(int_led.to_bytes( + length=1, byteorder=self._lst_io[self._ioled]._byteorder + )) + else: + raise ValueError("led status int() must be between 0 and 3") + + A1 = property(get_leda1, set_leda1) + A2 = property(get_leda2, set_leda2) + status = property(get_status) + + @property + def picontrolrunning(self): + """Statusbit fuer piControl-Treiber laeuft. + @return True, wenn Treiber laeuft""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 1) + + @property + def unconfdevice(self): + """Statusbit fuer ein IO-Modul nicht mit PiCtory konfiguriert. + @return True, wenn IO Modul nicht konfiguriert""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 2) + + @property + def missingdeviceorgate(self): + """Statusbit fuer ein IO-Modul fehlt oder piGate konfiguriert. + @return True, wenn IO-Modul fehlt oder piGate konfiguriert""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 4) + + @property + def overunderflow(self): + """Statusbit Modul belegt mehr oder weniger Speicher als konfiguriert. + @return True, wenn falscher Speicher belegt ist""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 8) + + @property + def leftgate(self): + """Statusbit links vom RevPi ist ein piGate Modul angeschlossen. + @return True, wenn piGate links existiert""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 16) + + @property + def rightgate(self): + """Statusbit rechts vom RevPi ist ein piGate Modul angeschlossen. + @return True, wenn piGate rechts existiert""" + return bool(int.from_bytes( + self._lst_io[0].get_value(), + byteorder=self._lst_io[0]._byteorder + ) & 32) + + @property + def iocycle(self): + """Gibt Zykluszeit der Prozessabbildsynchronisierung zurueck. + @return Zykluszeit in ms""" + return None if self._iocycle is None else int.from_bytes( + self._lst_io[self._iocycle].get_value(), + byteorder=self._lst_io[self._iocycle]._byteorder + ) + + @property + def temperatur(self): + """Gibt CPU-Temperatur zurueck. + @return CPU-Temperatur in Celsius""" + return None if self._iotemperatur is None else int.from_bytes( + self._lst_io[self._iotemperatur].get_value(), + byteorder=self._lst_io[self._iotemperatur]._byteorder + ) + + @property + def frequency(self): + """Gibt CPU Taktfrequenz zurueck. + @return CPU Taktfrequenz in MHz""" + return None if self._iofrequency is None else int.from_bytes( + self._lst_io[self._iofrequency].get_value(), + byteorder=self._lst_io[self._iofrequency]._byteorder + ) * 10 + + @property + def ioerrorcount(self): + """Gibt Fehleranzahl auf RS485 piBridge Bus zurueck. + @return Fehleranzahl der piBridge""" + return None if self._ioerrorcnt is None else int.from_bytes( + self._lst_io[self._ioerrorcnt].get_value(), + byteorder=self._lst_io[self._ioerrorcnt]._byteorder + ) + + @property + def errorlimit1(self): + """Gibt RS485 ErrorLimit1 Wert zurueck. + @return Aktueller Wert fuer ErrorLimit1""" + return self._errorlimit(self._ioerrorlimit1, None) + + @errorlimit1.setter + def errorlimit1(self, value): + """Setzt RS485 ErrorLimit1 auf neuen Wert. + @param value Neuer ErrorLimit1 Wert""" + self._errorlimit(self._ioerrorlimit1, value) + + @property + def errorlimit2(self): + """Gibt RS485 ErrorLimit2 Wert zurueck. + @return Aktueller Wert fuer ErrorLimit2""" + return self._errorlimit(self._ioerrorlimit2, None) + + @errorlimit2.setter + def errorlimit2(self, value): + """Setzt RS485 ErrorLimit2 auf neuen Wert. + @param value Neuer ErrorLimit2 Wert""" + self._errorlimit(self._ioerrorlimit2, value) + + +class Gateway(Device): + + """Klasse fuer die RevPi Gateway-Devices. + + Stellt neben den Funktionen von RevPiDevice weitere Funktionen fuer die + Gateways bereit. Es koennen ueber die reg_*-Funktionen eigene IOs definiert + werden, die ein RevPiStructIO-Objekt abbilden. + Dieser IO-Typ kann Werte ueber mehrere Bytes verarbeiten und zurueckgeben. + + """ + + def __init__(self, parent, dict_device, **kwargs): + """Erweitert RevPiDevice um reg_*-Funktionen. + @see #RevPiDevice.__init__ RevPiDevice.__init__(...)""" + super().__init__(parent, dict_device, **kwargs) + + # TODO: evtl. an modio.io anhängen + self._dict_iorefbyte = {} + self._dict_iorefname = {} + self._dict_slc = { + IOType.INP: self.slc_inp, + IOType.OUT: self.slc_out, + IOType.MEM: self.slc_mem + } + + def _create_io(self, name, startio, frm, io_type, **kwargs): + """Erstellt einen neuen IO und ersetzt den/die Bestehenden. + + @param name Name des neuen IO + @param startio IO ab dem eingefuegt wird + @param frm struct() formatierung (1 Zeichen) + @param io_type IOType() Wert + @param kwargs Weitere Parameter: + - bmk: Bezeichnung fuer IO + - bit: Registriert IO als bool() am angegebenen Bit im Byte + - byteorder: Byteorder fuer diesen IO, Standardwert=little + - defaultvalue: Standardwert fuer IO, Standard ist 0 + + """ + if len(frm) == 1: + + # Byteorder prüfen und übernehmen + byteorder = kwargs.get("byteorder", "little") + if not (byteorder == "little" or byteorder == "big"): + raise ValueError("byteorder must be 'little' or 'big'") + bofrm = "<" if byteorder == "little" else ">" + + bitaddress = "" if frm != "?" else str(kwargs.get("bit", 0)) + if bitaddress == "" or \ + (int(bitaddress) >= 0 and int(bitaddress) < 8): + + bitlength = "1" if bitaddress.isnumeric() else \ + struct.calcsize(bofrm + frm) * 8 + + if startio in self._dict_iorefname: + startaddress = self._dict_iorefname[startio] + else: + startaddress = self.__getitem__(startio).slc_address.start + + # [name,default,anzbits,adressbyte,export,adressid,bmk,bitaddress] + list_value = [ + name, + kwargs.get("defaultvalue", 0), + bitlength, + startaddress, + False, + str(startaddress).rjust(4, "0"), + kwargs.get("bmk", ""), + bitaddress + ] + + # Neuen IO instantiieren + io_new = StructIO( + self, + list_value, + io_type, + byteorder, + bofrm + frm + ) + io_new._byteorder = byteorder + + # Platz für neuen IO prüfen + if (io_new.slc_address.start >= + self._dict_slc[io_type].start and + io_new.slc_address.stop <= + self._dict_slc[io_type].stop): + + self._replace_io(io_new) + + else: + raise BufferError( + "registered value does not fit process image scope" + ) + else: + raise AttributeError( + "bitaddress must be a value between 0 and 7" + ) + else: + raise AttributeError("parameter frm has to be a single sign") + + def _getbytename(self, iobyte): + """Ermittelt den Namen eines IOs auf der Byteadresse. + @param iobyte Bytenummer + @return IO-Namen""" + + # Wenn IO schon ausgetauscht wurde + if iobyte in self._dict_iorefbyte: + return self._dict_iorefbyte[iobyte] + + # Wenn IO jetzt ausgetauscht wird + if iobyte in self._modio.io: + intlen = len(self._modio.io[iobyte]) + if intlen == 1: + return self._modio.io[iobyte][0].name + elif len == 0: + raise KeyError("byte '{}' contains no input".format(iobyte)) + else: + raise KeyError( + "byte '{}' contains more than one bit-input".format(iobyte) + ) + else: + raise KeyError("byte '{}' does not exist".format(iobyte)) + + def _replace_io(self, io): + """Ersetzt bestehende IOs durch den neu Registrierten. + @param io Neuer IO der eingefuegt werden soll""" + if hasattr(self._modio.io, io.name): + raise NameError( + "name '{}' already exists on device '{}'".format( + io._name, self.name + ) + ) + else: + dict_oldio = {} + for oldio in self._lst_io: + # Alle IOs Prüfen ob sie im neuen Speicherbereich sind + errstart = oldio.slc_address.start >= io.slc_address.start \ + and oldio.slc_address.start < io.slc_address.stop + errstop = oldio.slc_address.stop > io.slc_address.start \ + and oldio.slc_address.stop <= io.slc_address.stop + + if errstart or errstop: + if type(oldio) == StructIO: + # Hier gibt es schon einen neuen IO + if oldio._bitaddress >= 0: + if io._bitaddress == oldio._bitaddress: + raise MemoryError( + "bit {} already assigned to '{}'".format( + io._bitaddress, oldio._name + ) + ) + + else: + # Bereits überschriebene bytes() sind ungültig + raise MemoryError( + "new io '{}' overlaps memory of '{}'".format( + io._name, oldio._name + ) + ) + + else: + # IOs im Speicherbereich des neuen IO merken + dict_oldio[oldio.name] = oldio + + for oldio in dict_oldio.values(): + if io._bitaddress >= 0: + # ios für ref bei bitaddress speichern + self._dict_iorefbyte[oldio.slc_address.start] = oldio.name + self._dict_iorefname[oldio.name] = oldio.slc_address.start + + # ios aus listen entfernen + delattr(self._modio.io, oldio.name) + self._lst_io.remove(oldio) + + # Namensregister erweitern + setattr(self._modio.io, io.name, io) + + # io einfügen (auch wenn nicht richtige stelle wegen BitOffset) + self._lst_io.insert(io.slc_address.start, io) + + # Liste neu sortieren + self._lst_io.sort(key=lambda x: x.slc_address.start) + + def get_rawbytes(self): + """Gibt die Bytes aus, die dieses Device verwendet. + @return bytes() des Devices""" + return bytes(self._ba_devdata) + + def reg_inp(self, name, startinp, frm, **kwargs): + """Registriert einen neuen Input. + + @param name Name des neuen Inputs + @param startinp Inputname ab dem eingefuegt wird + @param frm struct() formatierung (1 Zeichen) + @param kwargs Weitere Parameter: + - bmk: Bezeichnung fuer Input + - bit: Registriert Input als bool() am angegebenen Bit im Byte + - byteorder: Byteorder fuer den Input, Standardwert=little + - defaultvalue: Standardwert fuer Input, Standard ist 0 + - event: Funktion fuer Eventhandling registrieren + - as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus + - edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @see Python3 struct() + + """ + if type(startinp) == int: + # Byte int() umwandeln in Namen + startinp = self._getbytename(startinp) + + if type(startinp) == str: + self._create_io(name, startinp, frm, IOType.INP, **kwargs) + else: + raise TypeError( + "start input must be str() or int() not {}".format( + type(startinp) + ) + ) + + # Optional Event eintragen + reg_event = kwargs.get("event", None) + if reg_event is not None: + as_thread = kwargs.get("as_thread", False) + edge = kwargs.get("edge", None) + self.reg_event(name, reg_event, as_thread=as_thread, edge=edge) + + def reg_out(self, name, startout, frm, **kwargs): + """Registriert einen neuen Output. + + @param name Name des neuen Outputs + @param startout Outputname ab dem eingefuegt wird + @param frm struct() formatierung (1 Zeichen) + @param kwargs Weitere Parameter: + - bmk: Bezeichnung fuer Output + - bit: Registriert Outputs als bool() am angegebenen Bit im Byte + - byteorder: Byteorder fuer den Output, Standardwert=little + - defaultvalue: Standardwert fuer Output, Standard ist 0 + - event: Funktion fuer Eventhandling registrieren + - as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus + - edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @see Python3 struct() + + """ + if type(startout) == int: + # Byte int() umwandeln in Namen + startout = self._getbytename(startout) + + if type(startout) == str: + self._create_io(name, startout, frm, IOType.OUT, **kwargs) + else: + raise TypeError( + "start output must be str() or int() not {}".format( + type(startout) + ) + ) + + # Optional Event eintragen + reg_event = kwargs.get("event", None) + if reg_event is not None: + as_thread = kwargs.get("as_thread", False) + edge = kwargs.get("edge", None) + self.reg_event(name, reg_event, as_thread=as_thread, edge=edge) + + +class Virtual(Gateway): + + """Klasse fuer die RevPi Virtual-Devices. + + Stellt die selben Funktionen wie RevPiGateway zur Verfuegung. Es koennen + ueber die reg_*-Funktionen eigene IOs definiert werden, die ein + RevPiStructIO-Objekt abbilden. + Dieser IO-Typ kann Werte ueber mehrere Bytes verarbeiten und zurueckgeben. + @see #RevPiGateway RevPiGateway + + """ + + pass diff --git a/revpimodio2/helper.py b/revpimodio2/helper.py new file mode 100644 index 0000000..48f6617 --- /dev/null +++ b/revpimodio2/helper.py @@ -0,0 +1,355 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +import warnings +from threading import Event, Lock, Thread +from timeit import default_timer + + +class EventCallback(Thread): + + """Thread fuer das interne Aufrufen von Event-Funktionen. + + Der Eventfunktion, welche dieser Thread aufruft, wird der Thread selber + als Parameter uebergeben. Darauf muss bei der definition der Funktion + geachtet werden z.B. "def event(th):". Bei umfangreichen Funktionen kann + dieser ausgewertet werden um z.B. doppeltes Starten zu verhindern. + Ueber EventCallback.ioname kann der Name des IO-Objekts abgerufen werden, + welches das Event ausgeloest hast. EventCallback.iovalue gibt den Wert des + IO-Objekts zum Ausloesezeitpunkt zurueck. + Der Thread stellt das EventCallback.exit Event als Abbruchbedingung fuer + die aufgerufene Funktion zur Verfuegung. + Durch Aufruf der Funktion EventCallback.stop() wird das exit-Event gesetzt + und kann bei Schleifen zum Abbrechen verwendet werden. + Mit dem .exit() Event auch eine Wartefunktion realisiert + werden: "th.exit.wait(0.5)" - Wartet 500ms oder bricht sofort ab, wenn + fuer den Thread .stop() aufgerufen wird. + + while not th.exit.is_set(): + # IO-Arbeiten + th.exit.wait(0.5) + + """ + + def __init__(self, func, name, value): + """Init EventCallback class. + + @param func Funktion die beim Start aufgerufen werden soll + @param name IO-Name + @param value IO-Value zum Zeitpunkt des Events + + """ + super().__init__() + self.daemon = True + self.exit = Event() + self.func = func + self.ioname = name + self.iovalue = value + + def run(self): + """Ruft die registrierte Funktion auf.""" + self.func(self) + + def stop(self): + """Setzt das exit-Event mit dem die Funktion beendet werden kann.""" + self.exit.set() + + +class Cycletools(): + + """Werkzeugkasten fuer Cycleloop-Funktion. + + Diese Klasse enthaelt Werkzeuge fuer Zyklusfunktionen, wie Taktmerker + und Flankenmerker. + Zu beachten ist, dass die Flankenmerker beim ersten Zyklus alle den Wert + True haben! Ueber den Merker Cycletools.first kann ermittelt werden, + ob es sich um den ersten Zyklus handelt. + + Taktmerker flag1c, flag5c, flag10c, usw. haben den als Zahl angegebenen + Wert an Zyklen jeweils False und True. + Beispiel: flag5c hat 5 Zyklen den Wert False und in den naechsten 5 Zyklen + den Wert True. + + Flankenmerker flank5c, flank10c, usw. haben immer im, als Zahl angebenen + Zyklus fuer einen Zyklusdurchlauf den Wert True, sonst False. + Beispiel: flank5c hat immer alle 5 Zyklen den Wert True. + + Diese Merker koennen z.B. verwendet werden um, an Outputs angeschlossene, + Lampen synchron blinken zu lassen. + + """ + + def __init__(self): + """Init Cycletools class.""" + self.__cycle = 0 + self.__ucycle = 0 + self.__dict_ton = {} + self.__dict_tof = {} + self.__dict_tp = {} + + # Taktmerker + self.first = True + self.flag1c = False + self.flag5c = False + self.flag10c = False + self.flag15c = False + self.flag20c = False + + # Flankenmerker + self.flank5c = True + self.flank10c = True + self.flank15c = True + self.flank20c = True + + def _docycle(self): + """Zyklusarbeiten.""" + # Einschaltverzoegerung + for tof in self.__dict_tof: + if self.__dict_tof[tof] > 0: + self.__dict_tof[tof] -= 1 + + # Ausschaltverzoegerung + for ton in self.__dict_ton: + if self.__dict_ton[ton][1]: + if self.__dict_ton[ton][0] > 0: + self.__dict_ton[ton][0] -= 1 + self.__dict_ton[ton][1] = False + else: + self.__dict_ton[ton][0] = -1 + + # Impuls + for tp in self.__dict_tp: + if self.__dict_tp[tp][1]: + if self.__dict_tp[tp][0] > 0: + self.__dict_tp[tp][0] -= 1 + else: + self.__dict_tp[tp][1] = False + else: + self.__dict_tp[tp][0] = -1 + + # Flankenmerker + self.flank5c = False + self.flank10c = False + self.flank15c = False + self.flank20c = False + + # Logische Flags + self.first = False + self.flag1c = not self.flag1c + + # Berechnete Flags + self.__cycle += 1 + if self.__cycle == 5: + self.__ucycle += 1 + if self.__ucycle == 3: + self.flank15c = True + self.flag15c = not self.flag15c + self.__ucycle = 0 + if self.flag5c: + if self.flag10c: + self.flank20c = True + self.flag20c = not self.flag20c + self.flank10c = True + self.flag10c = not self.flag10c + self.flank5c = True + self.flag5c = not self.flag5c + self.__cycle = 0 + + def get_tofc(self, name): + """Wert der Ausschaltverzoegerung. + @param name Eindeutiger Name des Timers + @return Wert der Ausschaltverzoegerung""" + return self.__dict_tof.get(name, 0) > 0 + + def set_tofc(self, name, cycles): + """Startet bei Aufruf einen ausschaltverzoegerten Timer. + + @param name Eindeutiger Name fuer Zugriff auf Timer + @param cycles Zyklusanzahl, der Verzoegerung wenn nicht neu gestartet + + """ + self.__dict_tof[name] = cycles + + def get_tonc(self, name): + """Einschaltverzoegerung. + @param name Eindeutiger Name des Timers + @return Wert der Einschaltverzoegerung""" + return self.__dict_ton.get(name, [-1])[0] == 0 + + def set_tonc(self, name, cycles): + """Startet einen einschaltverzoegerten Timer. + + @param name Eindeutiger Name fuer Zugriff auf Timer + @param cycles Zyklusanzahl, der Verzoegerung wenn neu gestartet + + """ + if self.__dict_ton.get(name, [-1])[0] == -1: + self.__dict_ton[name] = [cycles, True] + else: + self.__dict_ton[name][1] = True + + def get_tpc(self, name): + """Impulstimer. + @param name Eindeutiger Name des Timers + @return Wert der des Impulses""" + return self.__dict_tp.get(name, [-1])[0] > 0 + + def set_tpc(self, name, cycles): + """Startet einen Impuls Timer. + + @param name Eindeutiger Name fuer Zugriff auf Timer + @param cycles Zyklusanzahl, die der Impuls anstehen soll + + """ + if self.__dict_tp.get(name, [-1])[0] == -1: + self.__dict_tp[name] = [cycles, True] + else: + self.__dict_tp[name][1] = True + + +class ProcimgWriter(Thread): + + """Klasse fuer Synchroniseriungs-Thread. + + Diese Klasse wird als Thread gestartet, wenn das Prozessabbild zyklisch + synchronisiert werden soll. Diese Funktion wird hauptsaechlich fuer das + Event-Handling verwendet. + + """ + + def __init__(self, parentmodio): + """Init ProcimgWriter class. + @param parentmodio Parent Object""" + super().__init__() + self._adjwait = 0 + self._ioerror = 0 + self._modio = parentmodio + self._refresh = 0.05 + self._work = Event() + + self.daemon = True + self.lck_refresh = Lock() + self.maxioerrors = 0 + self.newdata = Event() + + def _gotioerror(self): + """IOError Verwaltung fuer auto_refresh.""" + self._ioerror += 1 + if self.maxioerrors != 0 and self._ioerror >= self.maxioerrors: + raise RuntimeError( + "reach max io error count {} on process image".format( + self.maxioerrors + ) + ) + warnings.warn( + "count {} io errors on process image".format(self._ioerror), + RuntimeWarning + ) + + def get_refresh(self): + """Gibt Zykluszeit zurueck. + @return int() Zykluszeit in Millisekunden""" + return int(self._refresh * 1000) + + def run(self): + """Startet die automatische Prozessabbildsynchronisierung.""" + fh = self._modio._create_myfh() + self._adjwait = self._refresh + while not self._work.is_set(): + ot = default_timer() + + # Lockobjekt holen und Fehler werfen, wenn nicht schnell genug + if not self.lck_refresh.acquire(timeout=self._adjwait): + warnings.warn( + "cycle time of {} ms exceeded on lock".format( + int(self._refresh * 1000) + ), + RuntimeWarning + ) + continue + + try: + fh.seek(0) + bytesbuff = bytearray(fh.read(self._modio._length)) + except IOError: + self._gotioerror() + self.lck_refresh.release() + self._work.wait(self._adjwait) + continue + + if self._modio._monitoring: + # Inputs und Outputs in Puffer + for dev in self._modio._lst_refresh: + dev._filelock.acquire() + dev._ba_devdata[:] = bytesbuff[dev.slc_devoff] + dev._filelock.release() + else: + # Inputs in Puffer, Outputs in Prozessabbild + ioerr = False + for dev in self._modio._lst_refresh: + dev._filelock.acquire() + dev._ba_devdata[dev.slc_inp] = bytesbuff[dev.slc_inpoff] + try: + fh.seek(dev.slc_outoff.start) + fh.write(dev._ba_devdata[dev.slc_out]) + except IOError: + ioerr = True + finally: + dev._filelock.release() + + if self._modio._buffedwrite: + try: + fh.flush() + except IOError: + ioerr = True + + if ioerr: + self._gotioerror() + self.lck_refresh.release() + self._work.wait(self._adjwait) + continue + + self.lck_refresh.release() + + # Alle aufwecken + self.newdata.set() + self._work.wait(self._adjwait) + + # Wartezeit anpassen um echte self._refresh zu erreichen + if default_timer() - ot >= self._refresh: + self._adjwait -= 0.001 + if self._adjwait < 0: + warnings.warn( + "cycle time of {} ms exceeded".format( + int(self._refresh * 1000) + ), + RuntimeWarning + ) + self._adjwait = 0 + else: + self._adjwait += 0.001 + + # Alle am Ende erneut aufwecken + self.newdata.set() + fh.close() + + def stop(self): + """Beendet die automatische Prozessabbildsynchronisierung.""" + self._work.set() + + def set_refresh(self, value): + """Setzt die Zykluszeit in Millisekunden. + @param value int() Millisekunden""" + if value >= 10 and value < 2000: + self._refresh = value / 1000 + self._adjwait = self._refresh + else: + raise ValueError( + "refresh time must be 10 to 2000 milliseconds" + ) + + refresh = property(get_refresh, set_refresh) diff --git a/revpimodio2/io.py b/revpimodio2/io.py new file mode 100644 index 0000000..649b908 --- /dev/null +++ b/revpimodio2/io.py @@ -0,0 +1,631 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +import struct +from .__init__ import RISING, FALLING, BOTH +from .device import Gateway +from threading import Event + + +class IOType(object): + + """IO Typen.""" + + INP = 300 + OUT = 301 + MEM = 302 + + +class IOList(object): + + """Basisklasse fuer direkten Zugriff auf IO Objekte.""" + + def __init__(self): + """Init IOList clacc.""" + self.__dict_iobyte = {k: [] for k in range(4096)} + + def __contains__(self, key): + """Prueft ob IO existiert. + @param key IO-Name str() + @return True, wenn IO vorhanden""" + if type(key) == int: + return key in self.__dict_iobyte \ + and len(self.__dict_iobyte[key]) > 0 + else: + return hasattr(self, key) + + def __delattr__(self, key): + """Entfernt angegebenen IO. + @param key IO zum entfernen""" + # TODO: Prüfen ob auch Bit sein kann + dev = getattr(self, key) + self.__dict_iobyte[dev.address].remove(dev) + object.__delattr__(self, key) + + def __getitem__(self, key): + """Ruft angegebenen IO ab. + @param key IO Name oder Byte + @return IO Object""" + if type(key) == int: + if key in self.__dict_iobyte: + return self.__dict_iobyte[key] + else: + raise KeyError("byte '{}' does not exist".format(key)) + else: + return getattr(self, key) + + def __setitem__(self, key, value): + """Setzt IO Wert. + @param key IO Name oder Byte + @param value Wert, auf den der IO gesetzt wird""" + if type(key) == int: + if key in self.__dict_iobyte: + if len(self.__dict_iobyte[key]) == 1: + self.__dict_iobyte[key][0].value = value + elif len(self.__dict_iobyte[key]) == 0: + raise KeyError("byte '{}' contains no input".format(key)) + else: + raise KeyError( + "byte '{}' contains more than one bit-input" + "".format(key) + ) + else: + raise KeyError("byte '{}' does not exist".format(key)) + else: + getattr(self, key).value = value + + def __setattr__(self, key, value): + """Setzt IO Wert. + @param key IO Name oder Byte + @param value Wert, auf den der IO gesetzt wird""" + if issubclass(type(value), IOBase): + if hasattr(self, key): + raise AttributeError( + "attribute {} already exists - can not set io".format(key) + ) + object.__setattr__(self, key, value) + + # Bytedict erstellen für Adresszugriff + if value._bitaddress < 0: + self.__dict_iobyte[value.address].append(value) + else: + if len(self.__dict_iobyte[value.address]) != 8: + # "schnell" 8 Einträge erstellen da es BIT IOs sind + self.__dict_iobyte[value.address] += [ + None, None, None, None, None, None, None, None + ] + self.__dict_iobyte[value.address][value._bitaddress] = value + + elif key == "_IOList__dict_iobyte": + object.__setattr__(self, key, value) + + else: + getattr(self, key).value = value + + def _testme(self): + # NOTE: Nur Debugging + for x in self.__dict_iobyte: + if len(self.__dict_iobyte[x]) > 0: + print(x, self.__dict_iobyte[x]) + + def reg_inp(self, name, frm, **kwargs): + """Registriert einen neuen Input an Adresse von Diesem. + + @param name Name des neuen Inputs + @param frm struct() formatierung (1 Zeichen) + @param kwargs Weitere Parameter: + - bmk: Bezeichnung fuer Input + - bit: Registriert Input als bool() am angegebenen Bit im Byte + - byteorder: Byteorder fuer den Input, Standardwert=little + - defaultvalue: Standardwert fuer Input, Standard ist 0 + - event: Funktion fuer Eventhandling registrieren + - as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus + - edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @see Python3 struct() + + """ + if not issubclass(self._parentdevice, Gateway): + raise RuntimeError( + "this function can be used on gatway or virtual devices only" + ) + + self._create_io(name, startinp, frm, IOType.INP, **kwargs) + + # Optional Event eintragen + reg_event = kwargs.get("event", None) + if reg_event is not None: + as_thread = kwargs.get("as_thread", False) + edge = kwargs.get("edge", None) + self.reg_event(name, reg_event, as_thread=as_thread, edge=edge) + + def reg_out(self, name, startout, frm, **kwargs): + """Registriert einen neuen Output. + + @param name Name des neuen Outputs + @param startout Outputname ab dem eingefuegt wird + @param frm struct() formatierung (1 Zeichen) + @param kwargs Weitere Parameter: + - bmk: Bezeichnung fuer Output + - bit: Registriert Outputs als bool() am angegebenen Bit im Byte + - byteorder: Byteorder fuer den Output, Standardwert=little + - defaultvalue: Standardwert fuer Output, Standard ist 0 + - event: Funktion fuer Eventhandling registrieren + - as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus + - edge: event-Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @see Python3 struct() + + """ + if not issubclass(self._parentdevice, Gateway): + raise RuntimeError( + "this function can be used on gatway or virtual devices only" + ) + + self._create_io(name, startout, frm, IOType.OUT, **kwargs) + + # Optional Event eintragen + reg_event = kwargs.get("event", None) + if reg_event is not None: + as_thread = kwargs.get("as_thread", False) + edge = kwargs.get("edge", None) + self.reg_event(name, reg_event, as_thread=as_thread, edge=edge) + + +class IOBase(object): + + """Basisklasse fuer alle IO-Objekte. + + Die Basisfunktionalitaet ermoeglicht das Lesen und Schreiben der Werte + als bytes() oder bool(). Dies entscheidet sich bei der Instantiierung. + Wenn eine Bittadresse angegeben wird, werden bool()-Werte erwartet + und zurueckgegeben, ansonsten bytes(). + + Diese Klasse dient als Basis fuer andere IO-Klassen mit denen die Werte + auch als int() verwendet werden koennen. + + """ + + def __init__(self, parentdevice, valuelist, iotype, byteorder): + """Instantiierung der IOBase()-Klasse. + + @param parentdevice Parentdevice auf dem der IO liegt + @param valuelist Datenliste fuer Instantiierung + @param iotype IOType() Wert + @param byteorder Byteorder 'little' / 'big' fuer int() Berechnung + + """ + self._parentdevice = parentdevice + + # Bitadressen auf Bytes aufbrechen und umrechnen + self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8 + + # Längenberechnung + self._bitlength = int(valuelist[2]) + self._length = 1 if self._bitaddress == 0 else int(self._bitlength / 8) + + self._byteorder = byteorder + self._iotype = iotype + self._name = valuelist[0] + self._signed = False + self.bmk = valuelist[6] + + int_startaddress = int(valuelist[3]) + if self._bitaddress == -1: + self.slc_address = slice( + int_startaddress, int_startaddress + self._length + ) + # Defaultvalue aus Zahl in Bytes umrechnen + if str(valuelist[1]).isnumeric(): + self.defaultvalue = int(valuelist[1]).to_bytes( + self._length, byteorder=self._byteorder + ) + else: + # Defaultvalue direkt von bytes übernehmen + if type(valuelist[1]) == bytes: + if len(valuelist[1]) != self._length: + raise ValueError( + "given bytes for default value must have a length " + "of {}".format(self._length) + ) + else: + self.defaultvalue = valuelist[1] + else: + self.defaultvalue = bytes(self._length) + + else: + # Höhere Bits als 7 auf nächste Bytes umbrechen + int_startaddress += int((int(valuelist[7]) % 16) / 8) + self.slc_address = slice( + int_startaddress, int_startaddress + 1 + ) + self.defaultvalue = bool(int(valuelist[1])) + + def __bool__(self): + """bool()-wert der Klasse. + @return IO-Wert als bool(). Nur False wenn False oder 0 sonst True""" + return bool(self.get_value()) + + def __bytes__(self): + """bytes()-wert der Klasse. + @return IO-Wert als bytes()""" + if self._bitaddress >= 0: + int_byte = int.from_bytes( + self._parentdevice._ba_devdata[self.slc_address], + byteorder=self._byteorder + ) + return b'\x01' if bool(int_byte & 1 << self._bitaddress) \ + else b'\x00' + else: + return bytes(self._parentdevice._ba_devdata[self.slc_address]) + + def __str__(self): + """str()-wert der Klasse. + @return Namen des IOs""" + return self._name + + def _get_byteorder(self): + """Gibt konfigurierte Byteorder zurueck. + @return str() Byteorder""" + return self._byteorder + + def get_address(self): + """Gibt die absolute Byteadresse im Prozessabbild zurueck. + @return Absolute Byteadresse""" + return self._parentdevice.offset + self.slc_address.start + + def get_length(self): + """Gibt die Bytelaenge des IO zurueck. + @return Bytelaenge des IO""" + return self._length + + def get_name(self): + """Gibt den Namen des IOs zurueck. + @return IO Name""" + return self._name + + def get_value(self): + """Gibt den Wert des IOs als bytes() oder bool() zurueck. + @return IO-Wert""" + if self._bitaddress >= 0: + int_byte = int.from_bytes( + self._parentdevice._ba_devdata[self.slc_address], + byteorder=self._byteorder + ) + return bool(int_byte & 1 << self._bitaddress) + + else: + return bytes(self._parentdevice._ba_devdata[self.slc_address]) + + def reg_event(self, func, edge=BOTH, as_thread=False): + """Registriert ein Event bei der Eventueberwachung. + + @param func Funktion die bei Aenderung aufgerufen werden soll + @param edge Ausfuehren bei RISING, FALLING or BOTH Wertaenderung + @param as_thread Bei True, Funktion als EventCallback-Thread ausfuehren + + """ + # Prüfen ob Funktion callable ist + if not callable(func): + raise RuntimeError( + "registered function '{}' ist not callable".format(func) + ) + + if edge != BOTH and self._bitaddress < 0: + raise AttributeError( + "parameter 'edge' can be used with bit io objects only" + ) + + if self not in self._parentdevice._dict_events: + self._parentdevice._dict_events[self] = [(func, edge, as_thread)] + else: + # Prüfen ob Funktion schon registriert ist + for regfunc in self._parentdevice._dict_events[self]: + + if regfunc[0] == func and edge == BOTH: + if self._bitaddress < 0: + raise AttributeError( + "io '{}' with function '{}' already in list." + "".format(self._name, func) + ) + else: + raise AttributeError( + "io '{}' with function '{}' already in list. " + "edge 'BOTH' not allowed anymore".format( + self._name, func + ) + ) + elif regfunc[0] == func and regfunc[1] == edge: + raise AttributeError( + "io '{}' with function '{}' for given edge " + "already in list".format(self._name, func) + ) + else: + self._parentdevice._dict_events[self].append( + (func, edge, as_thread) + ) + break + + def set_value(self, value): + """Setzt den Wert des IOs mit bytes() oder bool(). + @param value IO-Wert als bytes() oder bool()""" + if self._iotype == IOType.OUT: + if self._bitaddress >= 0: + # Versuchen egal welchen Typ in Bool zu konvertieren + value = bool(value) + + # ganzes Byte laden + byte_buff = self._parentdevice._ba_devdata[self.slc_address] + + # Bytes in integer umwandeln + int_len = len(byte_buff) + int_byte = int.from_bytes(byte_buff, byteorder=self._byteorder) + int_bit = 1 << self._bitaddress + + # Aktuellen Wert vergleichen und ggf. setzen + if not bool(int_byte & int_bit) == value: + if value: + int_byte += int_bit + else: + int_byte -= int_bit + + # Zurückschreiben wenn verändert + self._parentdevice._ba_devdata[self.slc_address] = \ + int_byte.to_bytes(int_len, byteorder=self._byteorder) + + else: + if type(value) == bytes: + if self._length == len(value): + self._parentdevice._ba_devdata[self.slc_address] = \ + value + else: + raise ValueError( + "requires a bytes() object of length {}, but" + " {} was given".format(self._length, len(value)) + ) + else: + raise ValueError( + "requires a bytes() object, not {}".format(type(value)) + ) + + elif self._iotype == IOType.INP: + raise AttributeError("can not write to input") + elif self._iotype == IOType.MEM: + raise AttributeError("can not write to memory") + + def unreg_event(self, func=None, edge=None): + """Entfernt ein Event aus der Eventueberwachung. + + @param func Nur Events mit angegebener Funktion + @param edge Nur Events mit angegebener Funktion und angegebener Edge + + """ + if self in self._parentdevice._dict_events: + if func is None: + del self._parentdevice._dict_events[self] + else: + newlist = [] + for regfunc in self._parentdevice._dict_events[self]: + if regfunc[0] != func or edge is not None \ + and regfunc[1] != edge: + + newlist.append(regfunc) + + # Wenn Funktionen übrig bleiben, diese übernehmen + if len(newlist) > 0: + self._parentdevice._dict_events[self] = newlist + else: + del self._parentdevice._dict_events[self] + + def wait(self, edge=BOTH, exitevent=None, okvalue=None, timeout=0): + """Wartet auf Wertaenderung eines IOs. + + Die Wertaenderung wird immer uerberprueft, wenn fuer Devices + in Devicelist.auto_refresh() neue Daten gelesen wurden. + + Bei Wertaenderung, wird das Warten mit 0 als Rueckgabewert beendet. + + HINWEIS: Wenn ProcimgWriter() keine neuen Daten liefert, wird + bis in die Ewigkeit gewartet (nicht bei Angabe von "timeout"). + + Wenn edge mit RISING oder FALLING angegeben wird muss diese Flanke + ausgeloest werden. Sollte der Wert 1 sein beim Eintritt mit Flanke + RISING, wird das Warten erst bei Aenderung von 0 auf 1 beendet. + + Als exitevent kann ein threading.Event()-Objekt uebergeben werden, + welches das Warten bei is_set() sofort mit 1 als Rueckgabewert + beendet. + + Wenn der Wert okvalue an dem IO fuer das Warten anliegt, wird + das Warten sofort mit -1 als Rueckgabewert beendet. + + Der Timeoutwert bricht beim Erreichen das Warten sofort mit + Wert 2 Rueckgabewert ab. (Das Timeout wird ueber die Zykluszeit + der auto_refresh Funktion berechnet, entspricht also nicht exact den + angegeben Millisekunden! Es wird immer nach oben gerundet!) + + @param edge Flanke RISING, FALLING, BOTH bei der mit True beendet wird + @param exitevent thrading.Event() fuer vorzeitiges Beenden mit False + @param okvalue IO-Wert, bei dem das Warten sofort mit True beendet wird + @param timeout Zeit in ms nach der mit False abgebrochen wird + @return int() erfolgreich Werte <= 0 + - Erfolgreich gewartet + Wert 0: IO hat den Wert gewechselt + Wert -1: okvalue stimmte mit IO ueberein + - Fehlerhaft gewartet + Wert 1: exitevent wurde gesetzt + Wert 2: timeout abgelaufen + Wert 100: Devicelist.exit() wurde aufgerufen + + """ + # Prüfen ob Device in auto_refresh ist + if not self._parentdevice._selfupdate: + raise RuntimeError( + "auto_refresh is not activated for device '{}|{}' - there " + "will never be new data".format( + self._parentdevice.position, self._parentdevice.name + ) + ) + + if edge != BOTH and self._bitaddress < 0: + raise AttributeError( + "parameter 'edge' can be used with bit Inputs only" + ) + + # Abbruchwert prüfen + if okvalue == self.value: + return -1 + + # WaitExit Event säubern + self._parentdevice._parent._waitexit.clear() + + val_start = self.value + timeout = timeout / 1000 + bool_timecount = timeout > 0 + if exitevent is None: + exitevent = Event() + + flt_timecount = 0 if bool_timecount else -1 + while not self._parentdevice._parent._waitexit.is_set() \ + and not exitevent.is_set() \ + and flt_timecount < timeout: + + if self._parentdevice._parent.imgwriter.newdata.wait(2.5): + self._parentdevice._parent.imgwriter.newdata.clear() + + if val_start != self.value: + if edge == BOTH \ + or edge == RISING and not val_start \ + or edge == FALLING and val_start: + return 0 + else: + val_start = not val_start + if bool_timecount: + flt_timecount += \ + self._parentdevice._parent.imgwriter._refresh + elif bool_timecount: + # TODO: Prüfen + flt_timecount += 1 + + # Abbruchevent wurde gesetzt + if exitevent.is_set(): + return 1 + + # RevPiModIO mainloop wurde verlassen + if self._parentdevice._parent._waitexit.is_set(): + return 100 + + # Timeout abgelaufen + return 2 + + address = property(get_address) + length = property(get_length) + name = property(get_name) + value = property(get_value, set_value) + + +class IntIO(IOBase): + + """Klasse fuer den Zugriff auf die Daten mit Konvertierung in int(). + + Diese Klasse erweitert die Funktion von IOBase() um Funktionen, + ueber die mit int() Werten gearbeitet werden kann. Fuer die Umwandlung + koennen 'Byteorder' (Default 'little') und 'signed' (Default False) als + Parameter gesetzt werden. + @see #IOBase IOBase + + """ + + def __int__(self): + """Gibt IO als int() Wert zurueck mit Beachtung byteorder/signed. + @return int() ohne Vorzeichen""" + return self.get_int() + + def _get_signed(self): + """Ruft ab, ob der Wert Vorzeichenbehaftet behandelt werden soll. + @return True, wenn Vorzeichenbehaftet""" + return self._signed + + def _set_byteorder(self, value): + """Setzt Byteorder fuer int() Umwandlung. + @param value str() 'little' or 'big'""" + if not (value == "little" or value == "big"): + raise ValueError("byteorder must be 'little' or 'big'") + self._byteorder = value + + def _set_signed(self, value): + """Left fest, ob der Wert Vorzeichenbehaftet behandelt werden soll. + @param value True, wenn mit Vorzeichen behandel""" + if type(value) != bool: + raise ValueError("signed must be bool() True or False") + self._signed = value + + def get_int(self): + """Gibt IO als int() Wert zurueck mit Beachtung byteorder/signed. + @return int() Wert""" + return int.from_bytes( + self._parentdevice._ba_devdata[self.slc_address], + byteorder=self._byteorder, + signed=self._signed + ) + + def set_int(self, value): + """Setzt IO mit Beachtung byteorder/signed. + @param value int()""" + if type(value) == int: + self.set_value(value.to_bytes( + self._length, + byteorder=self._byteorder, + signed=self._signed + )) + else: + raise ValueError( + "need an int() value, but {} was given".format(type(value)) + ) + + byteorder = property(IOBase._get_byteorder, _set_byteorder) + signed = property(_get_signed, _set_signed) + value = property(get_int, set_int) + + +class StructIO(IOBase): + + """Klasse fuer den Zugriff auf Daten ueber ein definierten struct(). + + Diese Klasse ueberschreibt get_value() und set_value() der IOBase() + Klasse. Sie stellt ueber struct die Werte in der gewuenschten Formatierung + bereit. Der struct-Formatwert wird bei der Instantiierung festgelegt. + @see #IOBase IOBase + + """ + + def __init__(self, parentdevice, valuelist, iotype, byteorder, frm): + """Erweitert IOBase um struct-Formatierung. + @see #IOBase.__init__ IOBase.__init__(...)""" + super().__init__(parentdevice, valuelist, iotype, byteorder) + self.frm = frm + + def get_structvalue(self): + """Gibt den Wert mit struct Formatierung zurueck. + @return Wert vom Typ der struct-Formatierung""" + if self._bitaddress >= 0: + return self.get_value() + else: + return struct.unpack(self.frm, self.get_value())[0] + + def set_structvalue(self, value): + """Setzt den Wert mit struct Formatierung. + @param value Wert vom Typ der struct-Formatierung""" + if self._bitaddress >= 0: + self.set_value(value) + else: + self.set_value(struct.pack(self.frm, value)) + + byteorder = property(IOBase._get_byteorder) + value = property(get_structvalue, set_structvalue) diff --git a/revpimodio2/modio.py b/revpimodio2/modio.py new file mode 100644 index 0000000..795fd4b --- /dev/null +++ b/revpimodio2/modio.py @@ -0,0 +1,899 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +import warnings + +from . import app as appmodule +from . import device as devicemodule +from . import helper as helpermodule +from . import io as iomodule +from . import summary as summarymodule + +from .__init__ import RISING, FALLING, BOTH + +from json import load as jload +from os import access, F_OK, R_OK +from signal import signal, SIG_DFL, SIGINT, SIGTERM +from threading import Thread, Event + + +class RevPiModIO(object): + + """Klasse fuer die Verwaltung aller piCtory Informationen. + + Diese Klasse uebernimmt die gesamte Konfiguration aus piCtory und bilded + die Devices und IOs ab. Sie uebernimmt die exklusive Verwaltung des + Prozessabbilds und stellt sicher, dass die Daten synchron sind. + Sollten nur einzelne Devices gesteuert werden, verwendet man + RevPiModIOSelected() und uebergibt bei Instantiierung eine Liste mit + Device Positionen oder Device Namen. + + """ + + def __init__(self, **kwargs): + """Instantiiert die Grundfunktionen. + + @param kwargs Weitere Parameter: + - auto_refresh: Wenn True, alle Devices zu auto_refresh hinzufuegen + - configrsc: Pfad zur piCtory Konfigurationsdatei + - procimg: Pfad zum Prozessabbild + - monitoring: In- und Outputs werden gelesen, niemals geschrieben + - simulator: Laed das Modul als Simulator und vertauscht IOs + - syncoutputs: Aktuell gesetzte Outputs vom Prozessabbild einlesen + + """ + self._auto_refresh = kwargs.get("auto_refresh", False) + self._configrsc = kwargs.get("configrsc", None) + self._monitoring = kwargs.get("monitoring", False) + self._procimg = kwargs.get("procimg", "/dev/piControl0") + self._simulator = kwargs.get("simulator", False) + self._syncoutputs = kwargs.get("syncoutputs", True) + + # TODO: bei simulator und procimg prüfen ob datei existiert / anlegen? + + # Private Variablen + self.__cleanupfunc = None + self._buffedwrite = False + self._device = [] + self._exit = Event() + self._imgwriter = None + self._length = 0 + self._looprunning = False + self._lst_devselect = [] + self._lst_refresh = [] + self._myfh = self._create_myfh() + self._th_mainloop = None + self._waitexit = Event() + + # Modulvariablen + self.core = None + + # piCtory Klassen + self.app = None + self.device = None + self.devices = None + self.io = None + self.summary = None + + # Nur Konfigurieren, wenn nicht vererbt + if type(self) == RevPiModIO: + self._configure() + + def __del__(self): + """Zerstoert alle Klassen um aufzuraeumen.""" + self.exit(full=True) + self._myfh.close() + + def __evt_exit(self, signum, sigframe): + """Eventhandler fuer Programmende. + @param signum Signalnummer + @param sigframe Signalframe""" + signal(SIGINT, SIG_DFL) + signal(SIGTERM, SIG_DFL) + self.exit(full=True) + if self.__cleanupfunc is not None: + self.readprocimg() + self.__cleanupfunc() + self.writeprocimg() + + def _configure(self): + """Verarbeitet die piCtory Konfigurationsdatei.""" + jconfigrsc = self.get_jconfigrsc() + + # App Klasse instantiieren + self.app = appmodule.App(jconfigrsc["App"]) + + # Devicefilter anwenden + if len(self._lst_devselect) > 0: + lst_found = [] + + if type(self) == RevPiModIODriver: + _searchtype = "VIRTUAL" + else: + _searchtype = None + + # Angegebene Devices suchen + for dev in jconfigrsc["Devices"]: + if _searchtype is None or dev["type"] == _searchtype: + if dev["name"] in self._lst_devselect: + lst_found.append(dev) + elif dev["position"].isnumeric() \ + and int(dev["position"]) in self._lst_devselect: + lst_found.append(dev) + + # Devices aus JSON oder Filter übernehmen + lst_devices = jconfigrsc["Devices"] if len(self._lst_devselect) == 0 \ + else lst_found + + # Device und IO Klassen anlegen + self.device = devicemodule.DeviceList() + self.io = iomodule.IOList() + + # Devices initialisieren + err_names = [] + for device in sorted(lst_devices, key=lambda x: x["position"]): + + # Bei VDev in alter piCtory Version, Position eindeutig machen + if device["position"] == "adap.": + device["position"] = -1 + # NOTE: Testen mit alter piCtory Version + while device["position"] in self.device: + device["position"] -= 1 + + if device["type"] == "BASE": + # Core + dev_new = devicemodule.Core( + self, device, simulator=self._simulator + ) + self.core = dev_new + + # Für RS485 errors defaults laden und schreiben + # NOTE: Soll das wirklich gemacht werden? + for io in dev_new.get_outs(): + io.set_value(io.defaultvalue) + if not self._monitoring: + self.writeprocimg(True, dev_new) + + elif device["type"] == "LEFT_RIGHT": + # IOs + dev_new = devicemodule.Device( + self, device, simulator=self._simulator + ) + elif device["type"] == "VIRTUAL": + # Virtuals + dev_new = devicemodule.Virtual( + self, device, simulator=self._simulator + ) + elif device["type"] == "EDGE": + # Gateways + dev_new = devicemodule.Gateway( + self, device, simulator=self._simulator + ) + else: + # Device-Type nicht gefunden + warnings.warn( + "device type {} unknown", + Warning + ) + dev_new = None + + if dev_new is not None: + self._device.append(dev_new) + + # Offset prüfen, muss mit Länge übereinstimmen + if self._length < dev_new.offset: + self._length = dev_new.offset + + self._length += dev_new._length + + # Auf doppelte Namen prüfen, da piCtory dies zulässt + if hasattr(self.device, dev_new.name): + err_names.append(dev_new.name) + + # DeviceList für direkten Zugriff aufbauen + setattr(self.device, dev_new.name, dev_new) + + # dict_devname zerstören, wenn doppelte Namen vorhanden sind + for errdev in err_names: + delattr(self.device, errdev) + warnings.warn( + "equal device name in pictory configuration. can not " + "build device to acces by name. you can access all devices " + "by position number pos_XX only!", + Warning + ) + + # ImgWriter erstellen + self._imgwriter = helpermodule.ProcimgWriter(self) + + # Aktuellen Outputstatus von procimg einlesen + if self._syncoutputs: + self.syncoutputs(force=True) + + # NOTE: Nur noch bis Final für kompatibilität + # Devices Klasse instantiieren + self.devices = devicemodule.Devicelist(self) + + # Optional ins auto_refresh aufnehmen + if self._auto_refresh: + for dev in self._device: + dev.auto_refresh() + + # Summary Klasse instantiieren + self.summary = summarymodule.Summary(jconfigrsc["Summary"]) + + def _create_myfh(self): + """Erstellt FileObject mit Pfad zum procimg. + return FileObject""" + self._buffedwrite = False + return open(self._procimg, "r+b", 0) + + def _get_configrsc(self): + """Getter function. + @return Pfad der verwendeten piCtory Konfiguration""" + return self._configrsc + + def _get_cycletime(self): + """Gibt Aktualisierungsrate in ms der Prozessabbildsynchronisierung aus. + @return Millisekunden""" + return self._imgwriter.refresh + + def _get_length(self): + """Getter function. + @return Laenge in Bytes der Devices""" + return self._length + + def _get_monitoring(self): + """Getter function. + @return True, wenn als Monitoring gestartet""" + return self._monitoring + + def _get_procimg(self): + """Getter function. + @return Pfad des verwendeten Prozessabbilds""" + return self._procimg + + def _get_simulator(self): + """Getter function. + @return True, wenn als Simulator gestartet""" + return self._simulator + + def _set_cycletime(self, milliseconds): + """Setzt Aktualisierungsrate der Prozessabbild-Synchronisierung. + @param milliseconds int() in Millisekunden""" + self._imgwriter.refresh = milliseconds + + def auto_refresh_maxioerrors(self, value=None): + """Maximale IO Fehler fuer auto_refresh. + @param value Setzt maximale Anzahl bis exception ausgeloest wird + @return Maximale Anzahl bis exception ausgeloest wird""" + if value is None: + return self._imgwriter.maxioerrors + elif type(value) == int and value >= 0: + self._imgwriter.maxioerrors = value + + def auto_refresh_resetioerrors(self): + """Setzt aktuellen IOError-Zaehler auf 0 zurueck.""" + self._imgwriter.maxioerrors = 0 + + def cleanup(self): + """Beendet auto_refresh und alle Threads.""" + # TODO: wirklich alles löschen + self.exit(full=True) + self._myfh.close() + self.app = None + self.device = None + self.devices = None + self.io = None + self.summary = None + + def cycleloop(self, func, cycletime=50): + """Startet den Cycleloop. + + Der aktuelle Programmthread wird hier bis Aufruf von + RevPiDevicelist.exit() "gefangen". Er fuehrt nach jeder Aktualisierung + des Prozessabbilds die uebergebene Funktion "func" aus und arbeitet sie + ab. Waehrend der Ausfuehrung der Funktion wird das Prozessabbild nicht + weiter aktualisiert. Die Inputs behalten bis zum Ende den aktuellen + Wert. Gesetzte Outputs werden nach Ende des Funktionsdurchlaufs in das + Prozessabbild geschrieben. + + Verlassen wird der Cycleloop, wenn die aufgerufene Funktion einen + Rueckgabewert nicht gleich None liefert, oder durch Aufruf von + revpimodio.exit(). + + HINWEIS: Die Aktualisierungszeit und die Laufzeit der Funktion duerfen + die eingestellte auto_refresh Zeit, bzw. uebergebene cycletime nicht + ueberschreiten! + + Ueber den Parameter cycletime kann die Aktualisierungsrate fuer das + Prozessabbild gesetzt werden (selbe Funktion wie + set_refreshtime(milliseconds)). + + @param func Funktion, die ausgefuehrt werden soll + @param cycletime auto_refresh Wert in Millisekunden + @return None + + """ + # Prüfen ob ein Loop bereits läuft + if self._looprunning: + raise RuntimeError( + "can not start multiple loops mainloop/cycleloop" + ) + + # Prüfen ob Devices in auto_refresh sind + if len(self._lst_refresh) == 0: + raise RuntimeError("no device with auto_refresh activated") + + # Prüfen ob Funktion callable ist + if not callable(func): + raise RuntimeError( + "registered function '{}' ist not callable".format(func) + ) + + # Zykluszeit übernehmen + if cycletime != self._imgwriter.refresh: + self._imgwriter.refresh = cycletime + + # Cycleloop starten + self._looprunning = True + cycleinfo = helpermodule.Cycletools() + ec = None + while ec is None and not self._exit.is_set(): + # Auf neue Daten warten und nur ausführen wenn set() + if not self._imgwriter.newdata.wait(2.5): + if not self._exit.is_set() and not self._imgwriter.is_alive(): + raise RuntimeError("auto_refresh thread not running") + continue + self._imgwriter.newdata.clear() + + # Vor Aufruf der Funktion auto_refresh sperren + self._imgwriter.lck_refresh.acquire() + + # Funktion aufrufen und auswerten + ec = func(cycleinfo) + cycleinfo._docycle() + + # auto_refresh freigeben + self._imgwriter.lck_refresh.release() + + # Cycleloop beenden + self._looprunning = False + + return ec + + def exit(self, full=True): + """Beendet mainloop() und optional auto_refresh. + + Wenn sich das Programm im mainloop() befindet, wird durch Aufruf + von exit() die Kontrolle wieder an das Hauptprogramm zurueckgegeben. + + Der Parameter full ist mit True vorbelegt und entfernt alle Devices aus + dem auto_refresh. Der Thread fuer die Prozessabbildsynchronisierung + wird dann gestoppt und das Programm kann sauber beendet werden. + + @param full Entfernt auch alle Devices aus auto_refresh""" + self._exit.set() + self._waitexit.set() + if full: + if self._imgwriter.is_alive(): + self._imgwriter.stop() + self._imgwriter.join(self._imgwriter._refresh) + while len(self._lst_refresh) > 0: + dev = self._lst_refresh.pop() + dev._selfupdate = False + if not self._monitoring: + self.writeprocimg(True, dev) + + def get_jconfigrsc(self): + """Laed die piCotry Konfiguration und erstellt ein dict(). + @return dict() der piCtory Konfiguration""" + # piCtory Konfiguration prüfen + if self._configrsc is not None: + if not access(self._configrsc, F_OK | R_OK): + raise RuntimeError( + "can not access pictory configuration at {}".format( + self._configrsc)) + else: + # piCtory Konfiguration an bekannten Stellen prüfen + lst_rsc = ["/etc/revpi/config.rsc", "/opt/KUNBUS/config.rsc"] + for rscfile in lst_rsc: + if access(rscfile, F_OK | R_OK): + self._configrsc = rscfile + break + if self._configrsc is None: + raise RuntimeError( + "can not access known pictory configurations at {} - " + "use 'configrsc' parameter so specify location" + "".format(", ".join(lst_rsc)) + ) + + with open(self._configrsc, "r") as fhconfigrsc: + return jload(fhconfigrsc) + + def handlesignalend(self, cleanupfunc=None): + """Signalhandler fuer Programmende verwalten. + + Wird diese Funktion aufgerufen, uebernimmt RevPiModIO die SignalHandler + fuer SIGINT und SIGTERM. Diese werden Empfangen, wenn das + Betriebssystem oder der Benutzer das Steuerungsprogramm sauber beenden + will. + + Die optionale Funktion "cleanupfunc" wird als letztes nach dem letzten + Einlesen der Inputs ausgefuehrt. Dort gesetzte Outputs werden nach + Ablauf der Funktion ein letztes Mal geschrieben. + Gedacht ist dies fuer Aufraeumarbeiten, wie z.B. das abschalten der + LEDs am RevPi-Core. + + Nach einmaligem Empfangen eines der Signale und dem Beenden der + RevPiModIO Thrads / Funktionen werden die SignalHandler wieder + freigegeben. + + @param cleanupfunc Funktion wird nach dem letzten Lesen der Inputs + ausgefuehrt, gefolgt vom letzten Schreiben der Outputs + + """ + # Prüfen ob Funktion callable ist + if not callable(cleanupfunc): + raise RuntimeError( + "registered function '{}' ist not callable".format(cleanupfunc) + ) + self.__cleanupfunc = cleanupfunc + signal(SIGINT, self.__evt_exit) + signal(SIGTERM, self.__evt_exit) + + def mainloop(self, freeze=False, blocking=True): + """Startet den Mainloop mit Eventueberwachung. + + Der aktuelle Programmthread wird hier bis Aufruf von + RevPiDevicelist.exit() "gefangen" (es sei denn blocking=False). Er + durchlaeuft die Eventueberwachung und prueft Aenderungen der, mit + einem Event registrierten, IOs. Wird eine Veraenderung erkannt, + fuert das Programm die dazugehoerigen Funktionen der Reihe nach aus. + + Wenn der Parameter "freeze" mit True angegeben ist, wird die + Prozessabbildsynchronisierung angehalten bis alle Eventfunktionen + ausgefuehrt wurden. Inputs behalten fuer die gesamte Dauer ihren + aktuellen Wert und Outputs werden erst nach Durchlauf aller Funktionen + in das Prozessabbild geschrieben. + + Wenn der Parameter "blocking" mit False angegeben wird, aktiviert + dies die Eventueberwachung und blockiert das Programm NICHT an der + Stelle des Aufrufs. Eignet sich gut fuer die GUI Programmierung, wenn + Events vom RevPi benoetigt werden, aber das Programm weiter ausgefuehrt + werden soll. + + @param freeze Wenn True, Prozessabbildsynchronisierung anhalten + @param blocking Wenn False, blockiert das Programm NICHT + @return None + + """ + # Prüfen ob ein Loop bereits läuft + if self._looprunning: + raise RuntimeError( + "can not start multiple loops mainloop/cycleloop" + ) + + # Prüfen ob Devices in auto_refresh sind + if len(self._lst_refresh) == 0: + raise RuntimeError("no device with auto_refresh activated") + + # Thread erstellen, wenn nicht blockieren soll + if not blocking: + self._th_mainloop = Thread( + target=self.mainloop, + kwargs={"freeze": freeze, "blocking": True} + ) + self._th_mainloop.start() + return + + # Event säubern vor Eintritt in Mainloop + self._exit.clear() + self._looprunning = True + + # Beim Eintritt in mainloop Bytecopy erstellen + for dev in self._lst_refresh: + dev._filelock.acquire() + dev._ba_datacp = dev._ba_devdata[:] + dev._filelock.release() + + lst_fire = [] + while not self._exit.is_set(): + + # Auf neue Daten warten und nur ausführen wenn set() + if not self._imgwriter.newdata.wait(2.5): + if not self._exit.is_set() and not self._imgwriter.is_alive(): + raise RuntimeError("auto_refresh thread not running") + continue + + self._imgwriter.newdata.clear() + + # Während Auswertung refresh sperren + self._imgwriter.lck_refresh.acquire() + + for dev in self._lst_refresh: + + if len(dev._dict_events) == 0 \ + or dev._ba_datacp == dev._ba_devdata: + continue + + for io_event in dev._dict_events: + + if dev._ba_datacp[io_event.slc_address] == \ + dev._ba_devdata[io_event.slc_address]: + continue + + if io_event._bitaddress >= 0: + boolcp = bool(int.from_bytes( + dev._ba_datacp[io_event.slc_address], + byteorder=io_event._byteorder + ) & 1 << io_event._bitaddress) + boolor = bool(int.from_bytes( + dev._ba_devdata[io_event.slc_address], + byteorder=io_event._byteorder + ) & 1 << io_event._bitaddress) + + if boolor == boolcp: + continue + + for regfunc in dev._dict_events[io_event]: + if regfunc[1] == BOTH \ + or regfunc[1] == RISING and boolor \ + or regfunc[1] == FALLING and not boolor: + lst_fire.append( + (regfunc, io_event.name, io_event.value) + ) + + else: + for regfunc in dev._dict_events[io_event]: + lst_fire.append( + (regfunc, io_event.name, io_event.value) + ) + + # Nach Verarbeitung aller IOs die Bytes kopieren + dev._filelock.acquire() + dev._ba_datacp = dev._ba_devdata[:] + dev._filelock.release() + + # Refreshsperre aufheben wenn nicht freeze + if not freeze: + self._imgwriter.lck_refresh.release() + + # Erst nach Datenübernahme alle Events feuern + while len(lst_fire) > 0: + tup_fire = lst_fire.pop() + event_func = tup_fire[0][0] + passname = tup_fire[1] + passvalue = tup_fire[2] + if tup_fire[0][2]: + th = helpermodule.EventCallback( + event_func, passname, passvalue + ) + th.start() + else: + # Direct callen da Prüfung in RevPiDevice.reg_event ist + event_func(passname, passvalue) + + # Refreshsperre aufheben wenn freeze + if freeze: + self._imgwriter.lck_refresh.release() + + # Mainloop verlassen + self._looprunning = False + + def readprocimg(self, force=False, device=None): + """Einlesen aller Inputs aller/eines Devices vom Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + if device is None: + mylist = self._device + else: + # TODO: Devicesuchen ändern + dev = device if issubclass(type(device), devicemodule.Device) \ + else self.device.__getitem__(device) + + if dev._selfupdate: + raise RuntimeError( + "can not read process image, while device '{}|{}'" + "is in auto_refresh mode".format(dev.position, dev.name) + ) + mylist = [dev] + + # Daten komplett einlesen + try: + self._myfh.seek(0) + bytesbuff = self._myfh.read(self._length) + except IOError: + warnings.warn( + "read error on process image '{}'".format(self.myfh.name), + RuntimeWarning + ) + return False + + for dev in mylist: + if (force or dev.autoupdate) and not dev._selfupdate: + + # FileHandler sperren + dev._filelock.acquire() + + if self._monitoring: + # Alles vom Bus einlesen + dev._ba_devdata[:] = bytesbuff[dev.slc_devoff] + else: + # Inputs vom Bus einlesen + dev._ba_devdata[dev.slc_inp] = bytesbuff[dev.slc_inpoff] + + # Mems vom Bus lesen + dev._ba_devdata[dev.slc_mem] = bytesbuff[dev.slc_memoff] + + dev._filelock.release() + + return True + + def setdefaultvalues(self, force=False, device=None): + """Alle Outputbuffer werden auf die piCtory default Werte gesetzt. + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden""" + if self._monitoring: + raise RuntimeError( + "can not set default values, while system is in monitoring " + "mode" + ) + + if device is None: + mylist = self._device + else: + dev = device if issubclass(type(device), devicemodule.Device) \ + else self.__getitem__(device) + mylist = [dev] + + for dev in mylist: + if (force or dev.autoupdate): + for io in dev.get_outs(): + io.set_value(io.defaultvalue) + + def syncoutputs(self, force=False, device=None): + """Lesen aller aktuell gesetzten Outputs im Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + if device is None: + mylist = self._device + else: + dev = device if issubclass(type(device), devicemodule.Device) \ + else self.__getitem__(device) + + if dev._selfupdate: + raise RuntimeError( + "can not sync process image, while device '{}|{}'" + "is in auto_refresh mode".format(dev.position, dev.name) + ) + mylist = [dev] + + try: + self._myfh.seek(0) + bytesbuff = self._myfh.read(self._length) + except IOError: + warnings.warn( + "read error on process image '{}'".format(self._myfh.name), + RuntimeWarning + ) + return False + + for dev in mylist: + if (force or dev.autoupdate) and not dev._selfupdate: + dev._filelock.acquire() + # Outputs vom Bus einlesen + dev._ba_devdata[dev.slc_out] = bytesbuff[dev.slc_outoff] + dev._filelock.release() + return True + + def writedefaultinputs(self, virtual_device): + """Schreibt fuer ein virtuelles Device piCtory Defaultinputwerte. + + Sollten in piCtory Defaultwerte fuer Inputs eines virtuellen Devices + angegeben sein, werden diese nur beim Systemstart oder einem piControl + Reset gesetzt. Sollte danach das Prozessabbild mit NULL ueberschrieben, + gehen diese Werte verloren. + Diese Funktion kann nur auf virtuelle Devices angewendet werden! + + @param virtual_device Virtuelles Device fuer Wiederherstellung + @return True, wenn Arbeiten am virtuellen Device erfolgreich waren + + """ + if self._monitoring: + raise RuntimeError( + "can not write process image, while system is in monitoring " + "mode" + ) + + # Device suchen + dev = virtual_device if issubclass(type(virtual_device), devicemodule.Device) \ + else self.__getitem__(virtual_device) + + # Prüfen ob es ein virtuelles Device ist + if not issubclass(type(dev), devicemodule.Virtual): + raise RuntimeError( + "this function can be used for virtual devices only" + ) + + workokay = True + dev._filelock.acquire() + + for io in dev.get_inps(): + dev._ba_devdata[io.slc_address] = io.defaultvalue + + # Outpus auf Bus schreiben + try: + self._myfh.seek(dev.slc_inpoff.start) + self._myfh.write(dev._ba_devdata[dev.slc_inp]) + if self._buffedwrite: + self._myfh.flush() + except IOError: + warnings.warn( + "write error on process image '{}'" + "".format(self._myfh.name), + RuntimeWarning + ) + workokay = False + + dev._filelock.release() + return workokay + + def writeprocimg(self, force=False, device=None): + """Schreiben aller Outputs aller Devices ins Prozessabbild. + + @param force auch Devices mit autoupdate=False + @param device nur auf einzelnes Device anwenden + @return True, wenn Arbeiten an allen Devices erfolgreich waren + + """ + if self._monitoring: + raise RuntimeError( + "can not write process image, while system is in monitoring " + "mode" + ) + + if device is None: + mylist = self._device + else: + dev = device if issubclass(type(device), devicemodule.Device) \ + else self.__getitem__(device) + + if dev._selfupdate: + raise RuntimeError( + "can not write process image, while device '{}|{}'" + "is in auto_refresh mode".format(dev.position, dev.name) + ) + mylist = [dev] + + workokay = True + for dev in mylist: + if (force or dev.autoupdate) and not dev._selfupdate: + dev._filelock.acquire() + + # Outpus auf Bus schreiben + try: + self._myfh.seek(dev.slc_outoff.start) + self._myfh.write(dev._ba_devdata[dev.slc_out]) + except IOError: + workokay = False + + dev._filelock.release() + + if self._buffedwrite: + try: + self._myfh.flush() + except IOError: + workokay = False + + if not workokay: + warnings.warn( + "write error on process image '{}'" + "".format(self._myfh.name), + RuntimeWarning + ) + + return workokay + + configrsc = property(_get_configrsc) + cycletime = property(_get_cycletime, _set_cycletime) + length = property(_get_length) + monitoring = property(_get_monitoring) + procimg = property(_get_procimg) + simulator = property(_get_simulator) + + +class RevPiModIOSelected(RevPiModIO): + + """Klasse fuer die Verwaltung einzelner Devices aus piCtory. + + Diese Klasse uebernimmt nur angegebene Devices der piCtory Konfiguration + und bilded sie inkl. IOs ab. Sie uebernimmt die exklusive Verwaltung des + Adressbereichs im Prozessabbild an dem sich die angegebenen Devices + befinden und stellt sicher, dass die Daten synchron sind. + + """ + + def __init__(self, deviceselection, **kwargs): + """Instantiiert nur fuer angegebene Devices die Grundfunktionen. + + Der Parameter deviceselection kann eine einzelne + Device Position / einzelner Device Name sein oder eine Liste mit + mehreren Positionen / Namen + + @param deviceselection Positionsnummer oder Devicename + @param kwargs Weitere Parameter + @see #RevPiModIO.__init__ RevPiModIO.__init__(...) + + """ + super().__init__(**kwargs) + + # Device liste erstellen + if type(deviceselection) == list: + for dev in deviceselection: + self._lst_devselect.append(dev) + else: + self._lst_devselect.append(deviceselection) + + for vdev in self._lst_devselect: + if type(vdev) != int and type(vdev) != str: + raise ValueError( + "need device position as int() or device name as str()" + ) + + self._configure() + + if len(self._device) == 0: + if type(self) == RevPiModIODriver: + raise RuntimeError( + "could not find any given VIRTUAL devices in config" + ) + else: + raise RuntimeError( + "could not find any given devices in config" + ) + elif len(self._device) != len(self._lst_devselect): + if type(self) == RevPiModIODriver: + raise RuntimeError( + "could not find all given VIRTUAL devices in config" + ) + else: + raise RuntimeError( + "could not find all given devices in config" + ) + + +class RevPiModIODriver(RevPiModIOSelected): + + """Klasse um eigene Treiber fuer die virtuellen Devices zu erstellen. + + Mit dieser Klasse werden nur angegebene Virtuelle Devices mit RevPiModIO + verwaltet. Bei Instantiierung werden automatisch die Inputs und Outputs + verdreht, um das Schreiben der Inputs zu ermoeglichen. Die Daten koennen + dann ueber logiCAD an den Devices abgerufen werden. + + """ + + def __init__(self, vdev, **kwargs): + """Instantiiert die Grundfunktionen. + + @param vdev Virtuelles Device fuer die Verwendung / oder list() + @param kwargs Weitere Parameter (nicht monitoring und simulator) + @see #RevPiModIO.__init__ RevPiModIO.__init__(...) + + """ + kwargs["monitoring"] = False + kwargs["simulator"] = True + super().__init__(vdev, **kwargs) diff --git a/revpimodio2/summary.py b/revpimodio2/summary.py new file mode 100644 index 0000000..79bac74 --- /dev/null +++ b/revpimodio2/summary.py @@ -0,0 +1,19 @@ +# +# python3-RevPiModIO +# +# Webpage: https://revpimodio.org/ +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +"""Bildet die Summary-Sektion von piCtory ab.""" + + +class Summary(object): + + """Bildet die Summary-Sektion der config.rsc ab.""" + + def __init__(self, summary): + """Instantiiert die RevPiSummary-Klasse. + @param summary piCtory Summaryinformationen""" + self.inptotal = summary["inpTotal"] + self.outtotal = summary["outTotal"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2bc61cc --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python3 +# +# (c) Sven Sager, License: LGPLv3 +# +# -*- coding: utf-8 -*- +"""Setupscript fuer python3-revpimodio.""" +from distutils.core import setup + +setup( + author="Sven Sager", + author_email="akira@narux.de", + url="https://revpimodio.org", + maintainer="Sven Sager", + maintainer_email="akira@revpimodio.org", + + license="LGPLv3", + name="revpimodio2", + version="2.0.0", + + py_modules=["revpimodio2"], + + description="Python3 Programmierung für Kunbus RevolutionPi", + long_description="" + "Das Modul stellt alle Devices und IOs aus der piCtory Konfiguration \n" + "in Python3 zur Verfügung. Es ermöglicht den direkten Zugriff auf die \n" + "Werte über deren vergebenen Namen. Lese- und Schreibaktionen mit dem \n" + "Prozessabbild werden von dem Modul selbst verwaltet, ohne dass sich \n" + "der Programmierer um Offsets und Adressen kümmern muss. Für die \n" + "Gatewaymodule wie ModbusTCP oder Profinet sind eigene 'Inputs' und \n" + "'Outputs' über einen bestimmten Adressbereich definierbar. Auf \n" + "diese IOs kann mit Python3 über den Namen direkt auf die Werte \n" + "zugegriffen werden.", + + classifiers=[ + "License :: OSI Approved :: " + "GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules" + ], +)