From c26841cf9476cf9eb8082a2e38dca5bae8f1a6ef Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 10:16:40 +0100 Subject: [PATCH 01/18] docs: translate German comments and docstrings to English Translate all inline comments, docstrings, and documentation from German to English across the codebase to improve accessibility for international developers. Fixes #27 Signed-off-by: Nicolai Buchwitz --- src/revpimodio2/__init__.py | 16 +- src/revpimodio2/_internal.py | 8 +- src/revpimodio2/app.py | 8 +- src/revpimodio2/device.py | 551 ++++++++++++------------ src/revpimodio2/helper.py | 206 +++++---- src/revpimodio2/io.py | 529 +++++++++++------------ src/revpimodio2/modio.py | 451 ++++++++++--------- src/revpimodio2/netio.py | 327 +++++++------- src/revpimodio2/summary.py | 6 +- tests/common/test_devices.py | 2 +- tests/common/test_init_modio.py | 10 +- tests/common/test_modio_class_basics.py | 4 +- tests/compact/test_compact.py | 6 +- tests/flat/test_flat.py | 6 +- tests/io_tests/test_ios.py | 2 +- tests/revpi3/test_connect.py | 2 +- tests/revpi3/test_core.py | 2 +- 17 files changed, 1050 insertions(+), 1086 deletions(-) diff --git a/src/revpimodio2/__init__.py b/src/revpimodio2/__init__.py index 3dcbbd1..656c4a4 100644 --- a/src/revpimodio2/__init__.py +++ b/src/revpimodio2/__init__.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- """ -Stellt alle Klassen fuer den RevolutionPi zur Verfuegung. +Provides all classes for the RevolutionPi. Webpage: https://revpimodio.org/ -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. +Provides classes for easy use of the Revolution Pi from +KUNBUS GmbH (https://revolutionpi.com/) . All I/Os are +read from the piCtory configuration and made directly accessible by their names. +For gateways, custom IOs can be configured across multiple bytes. +With the defined names, the desired data is accessed directly. +The user can register functions as events for all IOs. The module +executes these when data changes. """ __all__ = [ "IOEvent", diff --git a/src/revpimodio2/_internal.py b/src/revpimodio2/_internal.py index e28752f..afb2b82 100644 --- a/src/revpimodio2/_internal.py +++ b/src/revpimodio2/_internal.py @@ -45,12 +45,12 @@ def acheck(check_type, **kwargs) -> None: def consttostr(value) -> str: """ - Gibt fuer Konstanten zurueck. + Returns for constants. - Diese Funktion ist erforderlich, da enum in Python 3.2 nicht existiert. + This function is required because enum does not exist in Python 3.2. - :param value: Konstantenwert - :return: Name der Konstanten + :param value: Constant value + :return: Name of the constant """ if value == 0: return "OFF" diff --git a/src/revpimodio2/app.py b/src/revpimodio2/app.py index 25cf844..54db85b 100644 --- a/src/revpimodio2/app.py +++ b/src/revpimodio2/app.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Bildet die App Sektion von piCtory ab.""" +"""Maps the App section from piCtory.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -8,13 +8,13 @@ from time import gmtime, strptime class App: - """Bildet die App Sektion der config.rsc ab.""" + """Maps the App section of config.rsc.""" __slots__ = "name", "version", "language", "layout", "savets" def __init__(self, app: dict): """ - Instantiiert die App-Klasse. + Instantiates the App class. :param app: piCtory Appinformationen """ @@ -36,5 +36,5 @@ class App: except Exception: self.savets = gmtime(0) - # TODO: Layout untersuchen und anders abbilden + # TODO: Examine layout and map differently self.layout = app.get("layout", {}) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index 92ce3b6..0400650 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Modul fuer die Verwaltung der Devices.""" +"""Module for managing devices.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -15,7 +15,7 @@ from .pictory import ProductType class DeviceList(object): - """Basisklasse fuer direkten Zugriff auf Device Objekte.""" + """Base class for direct access to device objects.""" def __init__(self): """Init DeviceList class.""" @@ -23,10 +23,10 @@ class DeviceList(object): def __contains__(self, key): """ - Prueft ob Device existiert. + Checks if device exists. - :param key: DeviceName / Positionsnummer - :return: True, wenn Device vorhanden + :param key: DeviceName / Position number + :return: True if device exists """ if type(key) == int: return key in self.__dict_position @@ -37,25 +37,25 @@ class DeviceList(object): def __delattr__(self, key, delcomplete=True): """ - Entfernt angegebenes Device. + Removes specified device. - :param key: Device zum entfernen - :param delcomplete: Wenn True wird Device komplett entfernt + :param key: Device to remove + :param delcomplete: If True, device will be removed completely """ if delcomplete: - # Device finden + # Find device if type(key) == int: dev_del = self.__dict_position[key] key = dev_del._name else: dev_del = getattr(self, key) - # Reinigungsjobs + # Cleanup jobs dev_del.autorefresh(False) for io in dev_del: delattr(dev_del._modio.io, io._name) - # Device aus dict löschen + # Delete device from dict del self.__dict_position[dev_del._position] if hasattr(self, key): @@ -63,9 +63,9 @@ class DeviceList(object): def __delitem__(self, key): """ - Entfernt Device an angegebener Position. + Removes device at specified position. - :param key: Deviceposition zum entfernen + :param key: Device position to remove """ if isinstance(key, Device): key = key._position @@ -73,10 +73,10 @@ class DeviceList(object): def __getitem__(self, key): """ - Gibt angegebenes Device zurueck. + Returns specified device. - :param key: DeviceName / Positionsnummer - :return: Gefundenes -Objekt + :param key: DeviceName / Position number + :return: Found object """ if type(key) == int: if key not in self.__dict_position: @@ -87,29 +87,29 @@ class DeviceList(object): def __iter__(self): """ - Gibt Iterator aller Devices zurueck. + Returns iterator of all devices. - Die Reihenfolge ist nach Position im Prozessabbild sortiert und nicht - nach Positionsnummer (Dies entspricht der Positionierung aus piCtory)! + The order is sorted by position in the process image and not + by position number (this corresponds to the positioning from piCtory)! - :return: aller Devices + :return: of all devices """ for dev in sorted(self.__dict_position, key=lambda key: self.__dict_position[key]._offset): yield self.__dict_position[dev] def __len__(self): """ - Gibt Anzahl der Devices zurueck. + Returns number of devices. - :return: Anzahl der Devices""" + :return: Number of devices""" return len(self.__dict_position) def __setattr__(self, key, value): """ - Setzt Attribute nur wenn Device. + Sets attributes only if device. - :param key: Attributname - :param value: Attributobjekt + :param key: Attribute name + :param value: Attribute object """ if isinstance(value, Device): object.__setattr__(self, key, value) @@ -120,11 +120,11 @@ class DeviceList(object): class Device(object): """ - Basisklasse fuer alle Device-Objekte. + Base class for all device objects. - Die Basisfunktionalitaet generiert bei Instantiierung alle IOs und - erweitert den Prozessabbildpuffer um die benoetigten Bytes. Sie verwaltet - ihren Prozessabbildpuffer und sorgt fuer die Aktualisierung der IO-Werte. + The base functionality generates all IOs upon instantiation and + extends the process image buffer by the required bytes. It manages + its process image buffer and ensures the updating of IO values. """ __slots__ = ( @@ -161,11 +161,11 @@ class Device(object): def __init__(self, parentmodio, dict_device, simulator=False): """ - Instantiierung der Device-Klasse. + Instantiation of the Device class. :param parentmodio: RevpiModIO parent object - :param dict_device: fuer dieses Device aus piCotry - :param simulator: Laedt das Modul als Simulator und vertauscht IOs + :param dict_device: for this device from piCtory + :param simulator: Loads the module as simulator and swaps IOs """ self._modio = parentmodio @@ -178,7 +178,7 @@ class Device(object): self._shared_procimg = False self._shared_write = set() - # Wertzuweisung aus dict_device + # Value assignment from dict_device self._name = dict_device.get("name") self._offset = int(dict_device.get("offset")) self._position = int(dict_device.get("position")) @@ -193,7 +193,7 @@ class Device(object): "".format(self._name, parentmodio.length, self._offset), Warning, ) - # IOM-Objekte erstellen und Adressen in SLCs speichern + # Create IOM objects and store addresses in SLCs if simulator: self._slc_inp = self._buildio(dict_device.get("out"), INP) self._slc_out = self._buildio(dict_device.get("inp"), OUT) @@ -202,7 +202,7 @@ class Device(object): self._slc_out = self._buildio(dict_device.get("out"), OUT) self._slc_mem = self._buildio(dict_device.get("mem"), MEM) - # SLCs mit offset berechnen + # Calculate SLCs with offset self._slc_devoff = slice(self._offset, self._offset + self.length) self._slc_inpoff = slice( self._slc_inp.start + self._offset, @@ -217,7 +217,7 @@ class Device(object): self._slc_mem.stop + self._offset, ) - # Alle restlichen attribute an Klasse anhängen + # Attach all remaining attributes to class self.bmk = dict_device.get("bmk", "") self.catalognr = dict_device.get("catalogNr", "") self.comment = dict_device.get("comment", "") @@ -228,29 +228,29 @@ class Device(object): self.outvariant = dict_device.get("outVariant", 0) self.type = dict_device.get("type", "") - # Spezielle Konfiguration von abgeleiteten Klassen durchführen + # Perform special configuration from derived classes self._devconfigure() - # IO Liste aktualisieren für schnellen Indexzugriff + # Update IO list for fast index access self._update_my_io_list() def __bytes__(self): """ - Gibt alle Daten des Devices als zurueck. + Returns all device data as . - :return: Devicedaten als + :return: Device data as """ return bytes(self._ba_devdata) def __contains__(self, key): """ - Prueft ob IO auf diesem Device liegt. + Checks if IO is on this device. :param key: IO-Name / IO-Bytenummer - :return: True, wenn IO auf Device vorhanden + :return: True if IO is present on device """ if isinstance(key, IOBase): - # Umwandlung für key + # Conversion for key key = key._name if type(key) == int: @@ -264,32 +264,32 @@ class Device(object): def __getitem__(self, key): """ - Gibt IO an angegebener Stelle zurueck. + Returns IO at specified position. - :param key: Index des IOs auf dem device als - :return: Gefundenes IO-Objekt + :param key: Index of the IO on the device as + :return: Found IO object """ return self.__my_io_list[key] def __int__(self): """ - Gibt die Positon im RevPi Bus zurueck. + Returns the position on the RevPi bus. - :return: Positionsnummer + :return: Position number """ return self._position def __iter__(self): """ - Gibt Iterator aller IOs zurueck. + Returns iterator of all IOs. - :return: aller IOs + :return: of all IOs """ return self.__getioiter(self._slc_devoff, None) def __len__(self): """ - Gibt Anzahl der Bytes zurueck, die dieses Device belegt. + Returns number of bytes occupied by this device. :return: """ @@ -297,19 +297,19 @@ class Device(object): def __str__(self): """ - Gibt den Namen des Devices zurueck. + Returns the name of the device. - :return: Devicename + :return: Device name """ return self._name def __getioiter(self, ioslc: slice, export): """ - Gibt mit allen IOs zurueck. + Returns with all IOs. - :param ioslc: IO Abschnitt - :param export: Filter fuer 'Export' Flag in piCtory - :return: IOs als Iterator + :param ioslc: IO section + :param export: Filter for 'Export' flag in piCtory + :return: IOs as Iterator """ for lst_io in self._modio.io[ioslc]: for io in lst_io: @@ -318,23 +318,23 @@ class Device(object): def _buildio(self, dict_io: dict, iotype: int) -> slice: """ - Erstellt aus der piCtory-Liste die IOs fuer dieses Device. + Creates the IOs for this device from the piCtory list. - :param dict_io: -Objekt aus piCtory Konfiguration - :param iotype: Wert - :return: mit Start und Stop Position dieser IOs + :param dict_io: object from piCtory configuration + :param iotype: value + :return: with start and stop position of these IOs """ if len(dict_io) <= 0: return slice(0, 0) int_min, int_max = PROCESS_IMAGE_SIZE, 0 for key in sorted(dict_io, key=lambda x: int(x)): - # Neuen IO anlegen + # Create new IO if iotype == MEM: # Memory setting io_new = MemIO(self, dict_io[key], iotype, "little", False) elif isinstance(self, RoModule) and dict_io[key][3] == "1": - # Relais of RO are on device address "1" and has a cycle counter + # Relays of RO are on device address "1" and has a cycle counter if dict_io[key][7]: # Each relais output has a single bit io_new = RelaisOutput(self, dict_io[key], iotype, "little", False) @@ -343,10 +343,10 @@ class Device(object): io_new = IntRelaisOutput(self, dict_io[key], iotype, "little", False) elif bool(dict_io[key][7]): - # Bei Bitwerten IOBase verwenden + # Use IOBase for bit values io_new = IOBase(self, dict_io[key], iotype, "little", False) elif isinstance(self, DioModule) and dict_io[key][3] in self._lst_counter: - # Counter IO auf einem DI oder DIO + # Counter IO on a DI or DIO io_new = IntIOCounter( self._lst_counter.index(dict_io[key][3]), self, @@ -356,7 +356,7 @@ class Device(object): False, ) elif isinstance(self, Gateway): - # Ersetzbare IOs erzeugen + # Create replaceable IOs io_new = IntIOReplaceable(self, dict_io[key], iotype, "little", False) else: io_new = IntIO( @@ -364,7 +364,7 @@ class Device(object): dict_io[key], iotype, "little", - # Bei AIO (103) signed auf True setzen + # Set signed to True for AIO (103) self._producttype == ProductType.AIO, ) @@ -374,10 +374,10 @@ class Device(object): Warning, ) else: - # IO registrieren + # Register IO self._modio.io._private_register_new_io_object(io_new) - # Kleinste und größte Speicheradresse ermitteln + # Determine smallest and largest memory address if io_new._slc_address.start < int_min: int_min = io_new._slc_address.start if io_new._slc_address.stop > int_max: @@ -387,143 +387,143 @@ class Device(object): return slice(int_min, int_max) def _devconfigure(self): - """Funktion zum ueberschreiben von abgeleiteten Klassen.""" + """Function to override in derived classes.""" pass def _get_offset(self) -> int: """ - Gibt den Deviceoffset im Prozessabbild zurueck. + Returns the device offset in the process image. - :return: Deviceoffset + :return: Device offset """ return self._offset def _get_producttype(self) -> int: """ - Gibt den Produkttypen des device zurueck. + Returns the product type of the device. - :return: Deviceprodukttyp + :return: Device product type """ return self._producttype def _update_my_io_list(self) -> None: - """Erzeugt eine neue IO Liste fuer schnellen Zugriff.""" + """Creates a new IO list for fast access.""" self.__my_io_list = list(self.__iter__()) def autorefresh(self, activate=True) -> None: """ - Registriert dieses Device fuer die automatische Synchronisierung. + Registers this device for automatic synchronization. - :param activate: Default True fuegt Device zur Synchronisierung hinzu + :param activate: Default True adds device to synchronization """ if activate and self not in self._modio._lst_refresh: - # Daten bei Aufnahme direkt einlesen! + # Read data directly when adding! self._modio.readprocimg(self) - # Datenkopie anlegen + # Create data copy with self._filelock: self._ba_datacp = self._ba_devdata[:] self._selfupdate = True - # Sicher in Liste einfügen + # Safely insert into list with self._modio._imgwriter.lck_refresh: self._modio._lst_refresh.append(self) - # Thread starten, wenn er noch nicht läuft + # Start thread if it is not yet running if not self._modio._imgwriter.is_alive(): - # Alte Einstellungen speichern + # Save old settings imgrefresh = self._modio._imgwriter.refresh - # ImgWriter mit alten Einstellungen erstellen + # Create ImgWriter with old settings self._modio._imgwriter = ProcimgWriter(self._modio) self._modio._imgwriter.refresh = imgrefresh self._modio._imgwriter.start() elif not activate and self in self._modio._lst_refresh: - # Sicher aus Liste entfernen + # Safely remove from list with self._modio._imgwriter.lck_refresh: self._modio._lst_refresh.remove(self) self._selfupdate = False - # Beenden, wenn keien Devices mehr in Liste sind + # Terminate if no more devices are in the list if len(self._modio._lst_refresh) == 0: self._modio._imgwriter.stop() - # Daten beim Entfernen noch einmal schreiben + # Write data once more when removing if not self._modio._monitoring: self._modio.writeprocimg(self) def get_allios(self, export=None) -> list: """ - Gibt eine Liste aller Inputs und Outputs zurueck, keine MEMs. + Returns a list of all inputs and outputs, no MEMs. - Bleibt Parameter 'export' auf None werden alle Inputs und Outputs - zurueckgegeben. Wird 'export' auf True/False gesetzt, werden nur Inputs - und Outputs zurueckgegeben, bei denen der Wert 'Export' in piCtory - uebereinstimmt. + If parameter 'export' remains None, all inputs and outputs will be + returned. If 'export' is set to True/False, only inputs + and outputs will be returned for which the 'Export' value in piCtory + matches. - :param export: Nur In-/Outputs mit angegebenen 'Export' Wert in piCtory - :return: Input und Output, keine MEMs + :param export: Only in-/outputs with specified 'Export' value in piCtory + :return: Input and Output, no MEMs """ return list(self.__getioiter(slice(self._slc_inpoff.start, self._slc_outoff.stop), export)) def get_inputs(self, export=None) -> list: """ - Gibt eine Liste aller Inputs zurueck. + Returns a list of all inputs. - Bleibt Parameter 'export' auf None werden alle Inputs zurueckgegeben. - Wird 'export' auf True/False gesetzt, werden nur Inputs zurueckgegeben, - bei denen der Wert 'Export' in piCtory uebereinstimmt. + If parameter 'export' remains None, all inputs will be returned. + If 'export' is set to True/False, only inputs will be returned + for which the 'Export' value in piCtory matches. - :param export: Nur Inputs mit angegebenen 'Export' Wert in piCtory + :param export: Only inputs with specified 'Export' value in piCtory :return: Inputs """ return list(self.__getioiter(self._slc_inpoff, export)) def get_outputs(self, export=None) -> list: """ - Gibt eine Liste aller Outputs zurueck. + Returns a list of all outputs. - Bleibt Parameter 'export' auf None werden alle Outputs zurueckgegeben. - Wird 'export' auf True/False gesetzt, werden nur Outputs - zurueckgegeben, bei denen der Wert 'Export' in piCtory uebereinstimmt. + If parameter 'export' remains None, all outputs will be returned. + If 'export' is set to True/False, only outputs + returned, for which the value 'Export' in piCtory matches. - :param export: Nur Outputs mit angegebenen 'Export' Wert in piCtory + :param export: Only outputs with specified 'Export' value in piCtory :return: Outputs """ return list(self.__getioiter(self._slc_outoff, export)) def get_memories(self, export=None) -> list: """ - Gibt eine Liste aller Memoryobjekte zurueck. + Returns a list of all memory objects. - Bleibt Parameter 'export' auf None werden alle Mems zurueckgegeben. - Wird 'export' auf True/False gesetzt, werden nur Mems zurueckgegeben, - bei denen der Wert 'Export' in piCtory uebereinstimmt. + If parameter 'export' remains None, all mems will be returned. + If 'export' is set to True/False, only mems will be returned + for which the 'Export' value in piCtory matches. - :param export: Nur Mems mit angegebenen 'Export' Wert in piCtory + :param export: Only mems with specified 'Export' value in piCtory :return: Mems """ return list(self.__getioiter(self._slc_memoff, export)) def readprocimg(self) -> bool: """ - Alle Inputs fuer dieses Device vom Prozessabbild einlesen. + Read all inputs for this device from process image. Same see - :return: True, wenn erfolgreich ausgefuehrt + :return: True if successfully executed :ref: :func:`revpimodio2.modio.RevPiModIO.readprocimg()` """ return self._modio.readprocimg(self) def setdefaultvalues(self) -> None: """ - Alle Outputbuffer fuer dieses Device auf default Werte setzen. + Set all output buffers for this device to default values. - :return: True, wenn erfolgreich ausgefuehrt + :return: True if successfully executed :ref: :func:`revpimodio2.modio.RevPiModIO.setdefaultvalues()` """ self._modio.setdefaultvalues(self) @@ -540,18 +540,18 @@ class Device(object): def syncoutputs(self) -> bool: """ - Lesen aller Outputs im Prozessabbild fuer dieses Device. + Read all outputs in process image for this device. - :return: True, wenn erfolgreich ausgefuehrt + :return: True if successfully executed :ref: :func:`revpimodio2.modio.RevPiModIO.syncoutputs()` """ return self._modio.syncoutputs(self) def writeprocimg(self) -> bool: """ - Schreiben aller Outputs dieses Devices ins Prozessabbild. + Write all outputs of this device to process image. - :return: True, wenn erfolgreich ausgefuehrt + :return: True if successfully executed :ref: :func:`revpimodio2.modio.RevPiModIO.writeprocimg()` """ return self._modio.writeprocimg(self) @@ -564,7 +564,7 @@ class Device(object): class Base(Device): - """Klasse fuer alle Base-Devices wie Core / Connect usw.""" + """Class for all base devices like Core / Connect etc.""" __slots__ = () @@ -575,27 +575,27 @@ class GatewayMixin: @property def leftgate(self) -> bool: """ - Statusbit links vom RevPi ist ein piGate Modul angeschlossen. + Statusbit links vom RevPi is a piGate Modul angeschlossen. - :return: True, wenn piGate links existiert + :return: True if piGate left exists """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 16) @property def rightgate(self) -> bool: """ - Statusbit rechts vom RevPi ist ein piGate Modul angeschlossen. + Statusbit rechts vom RevPi is a piGate Modul angeschlossen. - :return: True, wenn piGate rechts existiert + :return: True if piGate right exists """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 32) class ModularBase(Base): """ - Klasse fuer alle modularen Base-Devices wie Core / Connect usw.. + Class for all modular base devices like Core / Connect etc.. - Stellt Funktionen fuer den Status zur Verfuegung. + Provides functions for the status. """ __slots__ = ( @@ -611,10 +611,10 @@ class ModularBase(Base): def __errorlimit(self, slc_io: slice, errorlimit: int) -> None: """ - Verwaltet das Schreiben der ErrorLimits. + Manages writing the error limits. :param slc_io: Byte Slice vom ErrorLimit - :return: Aktuellen ErrorLimit oder None wenn nicht verfuegbar + :return: Current ErrorLimit or None if not available """ if 0 <= errorlimit <= 65535: self._ba_devdata[slc_io] = errorlimit.to_bytes(2, byteorder="little") @@ -623,54 +623,54 @@ class ModularBase(Base): def _get_status(self) -> int: """ - Gibt den RevPi Core Status zurueck. + Returns the RevPi Core status. - :return: Status als + :return: Status as """ return int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") @property def picontrolrunning(self) -> bool: """ - Statusbit fuer piControl-Treiber laeuft. + Statusbit for piControl-Treiber laeuft. - :return: True, wenn Treiber laeuft + :return: True, if Treiber laeuft """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 1) @property def unconfdevice(self) -> bool: """ - Statusbit fuer ein IO-Modul nicht mit PiCtory konfiguriert. + Status bit for an IO module not configured with piCtory. - :return: True, wenn IO Modul nicht konfiguriert + :return: True if IO module is not configured """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 2) @property def missingdeviceorgate(self) -> bool: """ - Statusbit fuer ein IO-Modul fehlt oder piGate konfiguriert. + Status bit for an IO module missing or piGate configured. - :return: True, wenn IO-Modul fehlt oder piGate konfiguriert + :return: True if IO module is missing or piGate is configured """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 4) @property def overunderflow(self) -> bool: """ - Statusbit Modul belegt mehr oder weniger Speicher als konfiguriert. + Status bit: Module occupies more or less memory than configured. - :return: True, wenn falscher Speicher belegt ist + :return: True if wrong memory is occupied """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 8) @property def iocycle(self) -> int: """ - Gibt Zykluszeit der Prozessabbildsynchronisierung zurueck. + Returns cycle time of process image synchronization. - :return: Zykluszeit in ms ( -1 wenn nicht verfuegbar) + :return: Cycle time in ms (-1 if not available) """ return ( -1 @@ -681,9 +681,9 @@ class ModularBase(Base): @property def temperature(self) -> int: """ - Gibt CPU-Temperatur zurueck. + Returns CPU temperature. - :return: CPU-Temperatur in Celsius (-273 wenn nich verfuegbar) + :return: CPU temperature in Celsius (-273 if not available) """ return ( -273 @@ -694,9 +694,9 @@ class ModularBase(Base): @property def frequency(self) -> int: """ - Gibt CPU Taktfrequenz zurueck. + Returns CPU clock frequency. - :return: CPU Taktfrequenz in MHz (-1 wenn nicht verfuegbar) + :return: CPU clock frequency in MHz (-1 if not available) """ return ( -1 @@ -707,9 +707,9 @@ class ModularBase(Base): @property def ioerrorcount(self) -> int: """ - Gibt Fehleranzahl auf RS485 piBridge Bus zurueck. + Returns error count on RS485 piBridge bus. - :return: Fehleranzahl der piBridge (-1 wenn nicht verfuegbar) + :return: Number of errors of the piBridge (-1 if not available) """ return ( -1 @@ -720,9 +720,9 @@ class ModularBase(Base): @property def errorlimit1(self) -> int: """ - Gibt RS485 ErrorLimit1 Wert zurueck. + Returns RS485 ErrorLimit1 value. - :return: Aktueller Wert fuer ErrorLimit1 (-1 wenn nicht verfuegbar) + :return: Current value for ErrorLimit1 (-1 if not available) """ return ( -1 @@ -733,9 +733,9 @@ class ModularBase(Base): @errorlimit1.setter def errorlimit1(self, value: int) -> None: """ - Setzt RS485 ErrorLimit1 auf neuen Wert. + Sets RS485 ErrorLimit1 to new value. - :param value: Neuer ErrorLimit1 Wert + :param value: Neuer ErrorLimit1 value """ if self._slc_errorlimit1 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit1") @@ -745,9 +745,9 @@ class ModularBase(Base): @property def errorlimit2(self) -> int: """ - Gibt RS485 ErrorLimit2 Wert zurueck. + Returns RS485 ErrorLimit2 value. - :return: Aktueller Wert fuer ErrorLimit2 (-1 wenn nicht verfuegbar) + :return: Current value for ErrorLimit2 (-1 if not available) """ return ( -1 @@ -758,9 +758,9 @@ class ModularBase(Base): @errorlimit2.setter def errorlimit2(self, value: int) -> None: """ - Setzt RS485 ErrorLimit2 auf neuen Wert. + Sets RS485 ErrorLimit2 to new value. - :param value: Neuer ErrorLimit2 Wert + :param value: Neuer ErrorLimit2 value """ if self._slc_errorlimit2 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit2") @@ -772,25 +772,25 @@ class ModularBase(Base): class Core(ModularBase, GatewayMixin): """ - Klasse fuer den RevPi Core. + Class for the RevPi Core. - Stellt Funktionen fuer die LEDs und den Status zur Verfuegung. + Provides functions for the LEDs and the status. """ __slots__ = "a1green", "a1red", "a2green", "a2red", "wd" def __setattr__(self, key, value): - """Verhindert Ueberschreibung der LEDs.""" + """Prevents overwriting the LEDs.""" if hasattr(self, key) and key in ("a1green", "a1red", "a2green", "a2red", "wd"): raise AttributeError("direct assignment is not supported - use .value Attribute") else: object.__setattr__(self, key, value) def _devconfigure(self) -> None: - """Core-Klasse vorbereiten.""" + """Prepare Core class.""" super()._devconfigure() - # Statische IO Verknüpfungen je nach Core-Variante + # Static IO links depending on Core variant # 2 Byte = Core1.0 self._slc_statusbyte = slice(0, 1) self._slc_led = slice(1, 2) @@ -818,7 +818,7 @@ class Core(ModularBase, GatewayMixin): self._slc_errorlimit1 = slice(7, 9) self._slc_errorlimit2 = slice(9, 11) - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_led = self._modio.io[self._slc_devoff][self._slc_led.start] if len(lst_led) == 8: exp_a1green = lst_led[0].export @@ -831,7 +831,7 @@ class Core(ModularBase, GatewayMixin): exp_a2green = exp_a1green exp_a2red = exp_a1green - # Echte IOs erzeugen + # Create actual IOs self.a1green = IOBase( self, ["core.a1green", 0, 1, self._slc_led.start, exp_a1green, None, "LED_A1_GREEN", "0"], @@ -872,27 +872,27 @@ class Core(ModularBase, GatewayMixin): def _get_leda1(self) -> int: """ - Gibt den Zustand der LED A1 vom Core zurueck. + Returns the state of LED A1 from the Core. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=from, 1=gruen, 2=rot """ # 0b00000011 = 3 return self._ba_devdata[self._slc_led.start] & 3 def _get_leda2(self) -> int: """ - Gibt den Zustand der LED A2 vom Core zurueck. + Returns the state of LED A2 from the Core. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=from, 1=gruen, 2=rot """ # 0b00001100 = 12 return (self._ba_devdata[self._slc_led.start] & 12) >> 2 def _set_leda1(self, value: int) -> None: """ - Setzt den Zustand der LED A1 vom Core. + Sets the state of LED A1 from the Core. - :param value: 0=aus, 1=gruen, 2=rot + :param value: 0=from, 1=gruen, 2=rot """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -902,9 +902,9 @@ class Core(ModularBase, GatewayMixin): def _set_leda2(self, value: int) -> None: """ - Setzt den Zustand der LED A2 vom Core. + Sets the state of LED A2 from the Core. - :param value: 0=aus, 1=gruen, 2=rot + :param value: 0=from, 1=gruen, 2=rot """ if 0 <= value <= 3: self.a2green(bool(value & 1)) @@ -921,32 +921,32 @@ class Core(ModularBase, GatewayMixin): class Connect(Core): - """Klasse fuer den RevPi Connect. + """Class for the RevPi Connect. - Stellt Funktionen fuer die LEDs, Watchdog und den Status zur Verfuegung. + Provides functions for the LEDs, watchdog and the status. """ __slots__ = "__evt_wdtoggle", "__th_wdtoggle", "a3green", "a3red", "x2in", "x2out" def __setattr__(self, key, value): - """Verhindert Ueberschreibung der speziellen IOs.""" + """Prevents overwriting the special IOs.""" if hasattr(self, key) and key in ("a3green", "a3red", "x2in", "x2out"): raise AttributeError("direct assignment is not supported - use .value Attribute") super(Connect, self).__setattr__(key, value) def __wdtoggle(self) -> None: - """WD Ausgang alle 10 Sekunden automatisch toggeln.""" + """WD Ausgang all 10 Sekunden automatisch toggeln.""" while not self.__evt_wdtoggle.wait(10): self.wd.value = not self.wd.value def _devconfigure(self) -> None: - """Connect-Klasse vorbereiten.""" + """Prepare Connect class.""" super()._devconfigure() self.__evt_wdtoggle = Event() self.__th_wdtoggle = None - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_myios = self._modio.io[self._slc_devoff] lst_led = lst_myios[self._slc_led.start] if len(lst_led) == 8: @@ -965,7 +965,7 @@ class Connect(Core): else: exp_x2in = lst_status[0].export - # Echte IOs erzeugen + # Create actual IOs self.a3green = IOBase( self, ["core.a3green", 0, 1, self._slc_led.start, exp_a3green, None, "LED_A3_GREEN", "4"], @@ -981,7 +981,7 @@ class Connect(Core): False, ) - # IO Objekte für WD und X2 in/out erzeugen + # Create IO objects for WD and X2 in/out self.x2in = IOBase( self, ["core.x2in", 0, 1, self._slc_statusbyte.start, exp_x2in, None, "Connect_X2_IN", "6"], @@ -1002,26 +1002,26 @@ class Connect(Core): def _get_leda3(self) -> int: """ - Gibt den Zustand der LED A3 vom Connect zurueck. + Returns the Zustand the LED A3 vom Connect. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=from, 1=gruen, 2=rot """ # 0b00110000 = 48 return (self._ba_devdata[self._slc_led.start] & 48) >> 4 def _get_wdtoggle(self) -> bool: """ - Ruft den Wert fuer Autowatchdog ab. + Retrieves the value for Autowatchdog. - :return: True, wenn Autowatchdog aktiv ist + :return: True if autowatchdog is active """ return self.__th_wdtoggle is not None and self.__th_wdtoggle.is_alive() def _set_leda3(self, value: int) -> None: """ - Setzt den Zustand der LED A3 vom Connect. + Sets the state of LED A3 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot + :param: value 0=from, 1=gruen, 2=rot """ if 0 <= value <= 3: self.a3green(bool(value & 1)) @@ -1031,18 +1031,14 @@ class Connect(Core): def _set_wdtoggle(self, value: bool) -> None: """ - Setzt den Wert fuer Autowatchdog. + Sets the value for autowatchdog. - Wird dieser Wert auf True gesetzt, wechselt im Hintergrund das noetige - Bit zum toggeln des Watchdogs alle 10 Sekunden zwichen True und False. - Dieses Bit wird bei autorefresh=True natuerlich automatisch in das - Prozessabbild geschrieben. + If this value is set to True, the necessary bit to toggle the watchdog is switched between True and False every 10 seconds in the background. + This bit is automatically written to the process image with autorefresh=True. - WICHTIG: Sollte autorefresh=False sein, muss zyklisch - .writeprocimg() aufgerufen werden, um den Wert in das - Prozessabbild zu schreiben!!! + IMPORTANT: If autorefresh=False, .writeprocimg() must be called cyclically to write the value to the process image!!! - :param value: True zum aktivieren, False zum beenden + :param value: True to activate, False to terminate """ if self._modio._monitoring: raise RuntimeError("can not toggle watchdog, while system is in monitoring mode") @@ -1053,7 +1049,7 @@ class Connect(Core): self.__evt_wdtoggle.set() elif not self._get_wdtoggle(): - # Watchdogtoggler erstellen + # Create watchdog toggler self.__evt_wdtoggle.clear() self.__th_wdtoggle = Thread(target=self.__wdtoggle, daemon=True) self.__th_wdtoggle.start() @@ -1085,7 +1081,7 @@ class ModularBaseConnect_4_5(ModularBase): ) def __setattr__(self, key, value): - """Verhindert Ueberschreibung der speziellen IOs.""" + """Prevents overwriting the special IOs.""" if hasattr(self, key) and key in ( "a1red", "a1green", @@ -1120,7 +1116,7 @@ class ModularBaseConnect_4_5(ModularBase): return led_calculated def _devconfigure(self) -> None: - """Connect 4/5-Klasse vorbereiten.""" + """Prepare Connect 4/5 class.""" super()._devconfigure() self._slc_statusbyte = slice(0, 1) @@ -1133,7 +1129,7 @@ class ModularBaseConnect_4_5(ModularBase): self._slc_errorlimit2 = slice(9, 11) self._slc_led = slice(11, 13) - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_myios = self._modio.io[self._slc_devoff] lst_led = lst_myios[self._slc_led.start] lst_output = lst_myios[self._slc_output.start] @@ -1171,7 +1167,7 @@ class ModularBaseConnect_4_5(ModularBase): exp_a5green = exp_a1red exp_a5blue = exp_a1red - # Echte IOs erzeugen + # Create actual IOs self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "0"], @@ -1284,50 +1280,50 @@ class ModularBaseConnect_4_5(ModularBase): def _get_leda1(self) -> int: """ - Gibt den Zustand der LED A1 vom Connect zurueck. + Returns the Zustand the LED A1 vom Connect. - :return: 0=aus, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors """ return self.__led_calculator(self._ba_devdata[self._slc_led.start] & 0b00000111) def _get_leda2(self) -> int: """ - Gibt den Zustand der LED A2 vom Core zurueck. + Returns the state of LED A2 from the Core. - :return: 0=aus, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start] & 0b00111000) >> 3) def _get_leda3(self) -> int: """ - Gibt den Zustand der LED A3 vom Core zurueck. + Returns the Zustand the LED A3 vom Core. - :return: 0=aus, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors """ word_led = self._ba_devdata[self._slc_led] return self.__led_calculator((unpack("> 6) def _get_leda4(self) -> int: """ - Gibt den Zustand der LED A4 vom Core zurueck. + Returns the Zustand the LED A4 vom Core. - :return: 0=aus, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start + 1] & 0b00001110) >> 1) def _get_leda5(self) -> int: """ - Gibt den Zustand der LED A5 vom Core zurueck. + Returns the Zustand the LED A5 vom Core. - :return: 0=aus, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start + 1] & 0b01110000) >> 4) def _set_leda1(self, value: int) -> None: """ - Setzt den Zustand der LED A1 vom Connect. + Sets the state of LED A1 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a1red(bool(value & 2)) @@ -1338,9 +1334,9 @@ class ModularBaseConnect_4_5(ModularBase): def _set_leda2(self, value: int) -> None: """ - Setzt den Zustand der LED A2 vom Connect. + Sets the state of LED A2 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a2red(bool(value & 2)) @@ -1351,9 +1347,9 @@ class ModularBaseConnect_4_5(ModularBase): def _set_leda3(self, value: int) -> None: """ - Setzt den Zustand der LED A3 vom Connect. + Sets the state of LED A3 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a3red(bool(value & 2)) @@ -1364,9 +1360,9 @@ class ModularBaseConnect_4_5(ModularBase): def _set_leda4(self, value: int) -> None: """ - Setzt den Zustand der LED A4 vom Connect. + Sets the state of LED A4 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a4red(bool(value & 2)) @@ -1377,9 +1373,9 @@ class ModularBaseConnect_4_5(ModularBase): def _set_leda5(self, value: int) -> None: """ - Setzt den Zustand der LED A5 vom Connect. + Sets the state of LED A5 on the Connect. - :param: value 0=aus, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a5red(bool(value & 2)) @@ -1403,18 +1399,18 @@ class ModularBaseConnect_4_5(ModularBase): class Connect5(ModularBaseConnect_4_5, GatewayMixin): - """Klasse fuer den RevPi Connect 5. + """Class for the RevPi Connect 5. - Stellt Funktionen fuer die LEDs und den Status zur Verfuegung. + Provides functions for the LEDs and the status. """ pass class Connect4(ModularBaseConnect_4_5): - """Klasse fuer den RevPi Connect 4. + """Class for the RevPi Connect 4. - Stellt Funktionen fuer die LEDs und den Status zur Verfuegung. + Provides functions for the LEDs and the status. """ __slots__ = ( @@ -1423,7 +1419,7 @@ class Connect4(ModularBaseConnect_4_5): ) def __setattr__(self, key, value): - """Verhindert Ueberschreibung der speziellen IOs.""" + """Prevents overwriting the special IOs.""" if hasattr(self, key) and key in ( "x2in", "x2out", @@ -1432,10 +1428,10 @@ class Connect4(ModularBaseConnect_4_5): super().__setattr__(key, value) def _devconfigure(self) -> None: - """Connect4-Klasse vorbereiten.""" + """Prepare Connect4 class.""" super()._devconfigure() - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_myios = self._modio.io[self._slc_devoff] lst_output = lst_myios[self._slc_output.start] @@ -1451,7 +1447,7 @@ class Connect4(ModularBaseConnect_4_5): else: exp_x2in = lst_status[0].export - # IO Objekte für X2 in/out erzeugen + # Create IO objects for X2 in/out self.x2in = IOBase( self, ["core.x2in", 0, 1, self._slc_statusbyte.start, exp_x2in, None, "Connect_X2_IN", "6"], @@ -1470,10 +1466,10 @@ class Connect4(ModularBaseConnect_4_5): class Compact(Base): """ - Klasse fuer den RevPi Compact. + Class for the RevPi Compact. - Stellt Funktionen fuer die LEDs zur Verfuegung. Auf IOs wird ueber das .io - Objekt zugegriffen. + Provides functions for the LEDs. IOs are accessed via the .io + object zugegriffen. """ __slots__ = ( @@ -1488,22 +1484,22 @@ class Compact(Base): ) def __setattr__(self, key, value): - """Verhindert Ueberschreibung der LEDs.""" + """Prevents overwriting the LEDs.""" if hasattr(self, key) and key in ("a1green", "a1red", "a2green", "a2red", "wd"): raise AttributeError("direct assignment is not supported - use .value Attribute") else: object.__setattr__(self, key, value) def _devconfigure(self) -> None: - """Core-Klasse vorbereiten.""" + """Prepare Core class.""" super()._devconfigure() - # Statische IO Verknüpfungen des Compacts + # Statische IO Verknüpfungen of the Compacts self._slc_led = slice(23, 24) self._slc_temperature = slice(0, 1) self._slc_frequency = slice(1, 2) - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_led = self._modio.io[self._slc_devoff][self._slc_led.start] if len(lst_led) == 8: exp_a1green = lst_led[0].export @@ -1516,7 +1512,7 @@ class Compact(Base): exp_a2green = exp_a1green exp_a2red = exp_a1green - # Echte IOs erzeugen + # Create actual IOs self.a1green = IOBase( self, ["core.a1green", 0, 1, self._slc_led.start, exp_a1green, None, "LED_A1_GREEN", "0"], @@ -1557,27 +1553,27 @@ class Compact(Base): def _get_leda1(self) -> int: """ - Gibt den Zustand der LED A1 vom Compact zurueck. + Returns the Zustand the LED A1 vom Compact. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=from, 1=gruen, 2=rot """ # 0b00000011 = 3 return self._ba_devdata[self._slc_led.start] & 3 def _get_leda2(self) -> int: """ - Gibt den Zustand der LED A2 vom Compact zurueck. + Returns the Zustand the LED A2 vom Compact. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=from, 1=gruen, 2=rot """ # 0b00001100 = 12 return (self._ba_devdata[self._slc_led.start] & 12) >> 2 def _set_leda1(self, value: int) -> None: """ - Setzt den Zustand der LED A1 vom Compact. + Sets the state of LED A1 on the Compact. - :param value: 0=aus, 1=gruen, 2=rot + :param value: 0=from, 1=gruen, 2=rot """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -1587,9 +1583,9 @@ class Compact(Base): def _set_leda2(self, value: int) -> None: """ - Setzt den Zustand der LED A2 vom Compact. + Sets the state of LED A2 on the Compact. - :param value: 0=aus, 1=gruen, 2=rot + :param value: 0=from, 1=gruen, 2=rot """ if 0 <= value <= 3: self.a2green(bool(value & 1)) @@ -1607,9 +1603,9 @@ class Compact(Base): @property def temperature(self) -> int: """ - Gibt CPU-Temperatur zurueck. + Returns CPU temperature. - :return: CPU-Temperatur in Celsius (-273 wenn nich verfuegbar) + :return: CPU temperature in Celsius (-273 if not available) """ return ( -273 @@ -1620,9 +1616,9 @@ class Compact(Base): @property def frequency(self) -> int: """ - Gibt CPU Taktfrequenz zurueck. + Returns CPU clock frequency. - :return: CPU Taktfrequenz in MHz (-1 wenn nicht verfuegbar) + :return: CPU clock frequency in MHz (-1 if not available) """ return ( -1 @@ -1633,10 +1629,10 @@ class Compact(Base): class Flat(Base): """ - Klasse fuer den RevPi Flat. + Class for the RevPi Flat. - Stellt Funktionen fuer die LEDs zur Verfuegung. Auf IOs wird ueber das .io - Objekt zugegriffen. + Provides functions for the LEDs. IOs are accessed via the .io + object zugegriffen. """ __slots__ = ( @@ -1661,7 +1657,7 @@ class Flat(Base): ) def __setattr__(self, key, value): - """Verhindert Ueberschreibung der LEDs.""" + """Prevents overwriting the LEDs.""" if hasattr(self, key) and key in ( "a1green", "a1red", @@ -1682,17 +1678,17 @@ class Flat(Base): object.__setattr__(self, key, value) def _devconfigure(self) -> None: - """Core-Klasse vorbereiten.""" + """Prepare Core class.""" super()._devconfigure() - # Statische IO Verknüpfungen des Compacts + # Statische IO Verknüpfungen of the Compacts self._slc_led = slice(7, 9) self._slc_temperature = slice(4, 5) self._slc_frequency = slice(5, 6) self._slc_switch = slice(6, 7) self._slc_dout = slice(11, 12) - # Exportflags prüfen (Byte oder Bit) + # Check export flags (Byte or Bit) lst_led = self._modio.io[self._slc_devoff][self._slc_led.start] if len(lst_led) == 8: exp_a1green = lst_led[0].export @@ -1720,7 +1716,7 @@ class Flat(Base): exp_a5green = exp_a1green exp_a5red = exp_a1green - # Echte IOs erzeugen + # Create actual IOs self.a1green = IOBase( self, ["core.a1green", 0, 1, self._slc_led.start, exp_a1green, None, "LED_A1_GREEN", "0"], @@ -1936,9 +1932,9 @@ class Flat(Base): @property def temperature(self) -> int: """ - Gibt CPU-Temperatur zurueck. + Returns CPU temperature. - :return: CPU-Temperatur in Celsius (-273 wenn nich verfuegbar) + :return: CPU temperature in Celsius (-273 if not available) """ return ( -273 @@ -1949,9 +1945,9 @@ class Flat(Base): @property def frequency(self) -> int: """ - Gibt CPU Taktfrequenz zurueck. + Returns CPU clock frequency. - :return: CPU Taktfrequenz in MHz (-1 wenn nicht verfuegbar) + :return: CPU clock frequency in MHz (-1 if not available) """ return ( -1 @@ -1961,20 +1957,20 @@ class Flat(Base): class DioModule(Device): - """Stellt ein DIO / DI / DO Modul dar.""" + """Represents a DIO / DI / DO module.""" __slots__ = "_lst_counter" def __init__(self, parentmodio, dict_device, simulator=False): """ - Erweitert Device-Klasse zum Erkennen von IntIOCounter. + Extends Device class for detecting IntIOCounter. :rev: :func:`Device.__init__()` """ - # Stringliste der Byteadressen (alle Module sind gleich) + # Stringliste the Byteadressen (all Module are gleich) self._lst_counter = list(map(str, range(6, 70, 4))) - # Basisklasse laden + # Load base class super().__init__(parentmodio, dict_device, simulator=simulator) @@ -1992,13 +1988,12 @@ class RoModule(Device): class Gateway(Device): """ - Klasse fuer die RevPi Gateway-Devices. + Class for the RevPi Gateway-Devices. - Stellt neben den Funktionen von RevPiDevice weitere Funktionen fuer die - Gateways bereit. IOs auf diesem Device stellen die replace_io Funktion - zur verfuegung, ueber die eigene IOs definiert werden, die ein - RevPiStructIO-Objekt abbilden. - Dieser IO-Typ kann Werte ueber mehrere Bytes verarbeiten und zurueckgeben. + Provides additional functions for the RevPi Gateway devices besides the functions from RevPiDevice. + Gateways are ready. IOs on this device provide the replace_io function, + which allows defining custom IOs that map to a RevPiStructIO object. + This IO type can process and return values via multiple bytes. :ref: :func:`revpimodio2.io.IntIOReplaceable.replace_io()` """ @@ -2007,7 +2002,7 @@ class Gateway(Device): def __init__(self, parent, dict_device, simulator=False): """ - Erweitert Device-Klasse um get_rawbytes-Funktionen. + Extends Device class with get_rawbytes functions. :ref: :func:`Device.__init__()` """ @@ -2021,21 +2016,20 @@ class Gateway(Device): def get_rawbytes(self) -> bytes: """ - Gibt die Bytes aus, die dieses Device verwendet. + Returns the bytes used by this device. - :return: des Devices + :return: of the Devices """ return bytes(self._ba_devdata) class Virtual(Gateway): """ - Klasse fuer die RevPi Virtual-Devices. + Class for the RevPi Virtual-Devices. - Stellt die selben Funktionen wie Gateway 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. + Provides the same functions as Gateway. Custom IOs can be + defined via the replace_io functions that map to RevPiStructIO objects. + This IO type can process and return values via multiple bytes. :ref: :func:`Gateway` """ @@ -2044,15 +2038,12 @@ class Virtual(Gateway): def writeinputdefaults(self): """ - Schreibt fuer ein virtuelles Device piCtory Defaultinputwerte. + Writes piCtory default input values for a virtual device. - 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! + If default values for inputs of a virtual device are specified in piCtory, these are only set at system startup or a piControl reset. If the process image is subsequently overwritten with NULL, these values will be lost. + This function can only be applied to virtual devices! - :return: True, wenn Arbeiten am virtuellen Device erfolgreich waren + :return: True if operations on the virtual device were successful """ if self._modio._monitoring: raise RuntimeError("can not write process image, while system is in monitoring mode") @@ -2063,7 +2054,7 @@ class Virtual(Gateway): for io in self.get_inputs(): self._ba_devdata[io._slc_address] = io._defaultvalue - # Inputs auf Bus schreiben + # Inputs to Bus schreiben self._modio._myfh_lck.acquire() try: self._modio._myfh.seek(self._slc_inpoff.start) diff --git a/src/revpimodio2/helper.py b/src/revpimodio2/helper.py index 0938cef..676b9ad 100644 --- a/src/revpimodio2/helper.py +++ b/src/revpimodio2/helper.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""RevPiModIO Helperklassen und Tools.""" +"""RevPiModIO helper classes and tools.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -16,22 +16,14 @@ from .io import IOBase class EventCallback(Thread): - """Thread fuer das interne Aufrufen von Event-Funktionen. + """Thread for internal calling of event functions. - 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. + The event function that this thread calls will receive the thread itself as a parameter. This must be considered when defining the function, e.g., "def event(th):". For extensive functions, this can be evaluated to prevent duplicate starts. + The name of the IO object can be retrieved via EventCallback.ioname, + which triggered the event. EventCallback.iovalue returns the value of the IO object at the time of triggering. + The thread provides the EventCallback.exit event as an abort condition for the called function. + By calling the EventCallback.stop() function, the exit event is set and can be used to abort loops. + A wait function can also be implemented with the .exit() event: "th.exit.wait(0.5)" - waits 500ms or aborts immediately if .stop() is called on the thread. while not th.exit.is_set(): # IO-Arbeiten @@ -44,9 +36,9 @@ class EventCallback(Thread): """ Init EventCallback class. - :param func: Funktion die beim Start aufgerufen werden soll + :param func: Function that should be called at startup :param name: IO-Name - :param value: IO-Value zum Zeitpunkt des Events + :param value: IO value at the time of the event """ super().__init__() self.daemon = True @@ -56,35 +48,33 @@ class EventCallback(Thread): self.iovalue = value def run(self): - """Ruft die registrierte Funktion auf.""" + """Calls the registered function.""" self.func(self) def stop(self): - """Setzt das exit-Event mit dem die Funktion beendet werden kann.""" + """Sets the exit event that can be used to terminate the function.""" self.exit.set() class Cycletools: """ - Werkzeugkasten fuer Cycleloop-Funktion. + Toolbox for cycle loop function. - 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. + This class contains tools for cycle functions, such as clock flags and edge flags. + Note that all edge flags have the value True on the first cycle! The Cycletools.first + flag can be used to determine if it is the first cycle. - 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. + Clock flags flag1c, flag5c, flag10c, etc. have the numerically specified + value for the specified number of cycles, alternating between False and 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. + Example: flag5c has the value False for 5 cycles and True for the next 5 cycles. - Diese Merker koennen z.B. verwendet werden um, an Outputs angeschlossene, - Lampen synchron blinken zu lassen. + Edge flags flank5c, flank10c, etc. always have the value True for one cycle + at the numerically specified cycle, otherwise False. + + Example: flank5c always has the value True every 5 cycles. + + These flags can be used, for example, to make lamps connected to outputs blink synchronously. """ __slots__ = ( @@ -129,7 +119,7 @@ class Cycletools: self.device = revpi_object.device self.io = revpi_object.io - # Taktmerker + # Clock flags self.first = True self.flag1c = False self.flag5c = False @@ -138,28 +128,28 @@ class Cycletools: self.flag20c = False self.last = False - # Flankenmerker + # Edge flags self.flank5c = True self.flank10c = True self.flank15c = True self.flank20c = True - # Benutzerdaten + # User data class Var: - """Hier remanente Variablen anfuegen.""" + """Add remanent variables here.""" pass self.var = Var() def _docycle(self) -> None: - """Zyklusarbeiten.""" - # Einschaltverzoegerung + """Cycle operations.""" + # Turn-off delay for tof in self.__dict_tof: if self.__dict_tof[tof] > 0: self.__dict_tof[tof] -= 1 - # Ausschaltverzoegerung + # Turn-on delay for ton in self.__dict_ton: if self.__dict_ton[ton][1]: if self.__dict_ton[ton][0] > 0: @@ -168,7 +158,7 @@ class Cycletools: else: self.__dict_ton[ton][0] = -1 - # Impuls + # Pulse for tp in self.__dict_tp: if self.__dict_tp[tp][1]: if self.__dict_tp[tp][0] > 0: @@ -178,7 +168,7 @@ class Cycletools: else: self.__dict_tp[tp][0] = -1 - # Flankenmerker + # Edge flags self.flank5c = False self.flank10c = False self.flank15c = False @@ -188,7 +178,7 @@ class Cycletools: self.first = False self.flag1c = not self.flag1c - # Berechnete Flags + # Calculated flags self.__cycle += 1 if self.__cycle == 5: self.__ucycle += 1 @@ -239,64 +229,64 @@ class Cycletools: def get_tof(self, name: str) -> bool: """ - Wert der Ausschaltverzoegerung. + Value of the off-delay. - :param name: Eindeutiger Name des Timers - :return: Wert der Ausschaltverzoegerung + :param name: Unique name of the timer + :return: Value of the off-delay """ return self.__dict_tof.get(name, 0) > 0 def get_tofc(self, name: str) -> bool: """ - Wert der Ausschaltverzoegerung. + Value of the off-delay. - :param name: Eindeutiger Name des Timers - :return: Wert der Ausschaltverzoegerung + :param name: Unique name of the timer + :return: Value of the off-delay """ return self.__dict_tof.get(name, 0) > 0 def set_tof(self, name: str, milliseconds: int) -> None: """ - Startet bei Aufruf einen ausschaltverzoegerten Timer. + Starts an off-delay timer when called. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param milliseconds: Verzoegerung in Millisekunden + :param name: Unique name for accessing the timer + :param milliseconds: Delay in milliseconds """ self.__dict_tof[name] = ceil(milliseconds / self.__cycletime) def set_tofc(self, name: str, cycles: int) -> None: """ - Startet bei Aufruf einen ausschaltverzoegerten Timer. + Starts an off-delay timer when called. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param cycles: Zyklusanzahl, der Verzoegerung wenn nicht neu gestartet + :param name: Unique name for accessing the timer + :param cycles: Number of cycles for the delay if not restarted """ self.__dict_tof[name] = cycles def get_ton(self, name: str) -> bool: """ - Einschaltverzoegerung. + On-delay. - :param name: Eindeutiger Name des Timers - :return: Wert der Einschaltverzoegerung + :param name: Unique name of the timer + :return: Value of the on-delay """ return self.__dict_ton.get(name, [-1])[0] == 0 def get_tonc(self, name: str) -> bool: """ - Einschaltverzoegerung. + On-delay. - :param name: Eindeutiger Name des Timers - :return: Wert der Einschaltverzoegerung + :param name: Unique name of the timer + :return: Value of the on-delay """ return self.__dict_ton.get(name, [-1])[0] == 0 def set_ton(self, name: str, milliseconds: int) -> None: """ - Startet einen einschaltverzoegerten Timer. + Starts an on-delay timer. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param milliseconds: Millisekunden, der Verzoegerung wenn neu gestartet + :param name: Unique name for accessing the timer + :param milliseconds: Milliseconds for the delay if restarted """ if self.__dict_ton.get(name, [-1])[0] == -1: self.__dict_ton[name] = [ceil(milliseconds / self.__cycletime), True] @@ -305,10 +295,10 @@ class Cycletools: def set_tonc(self, name: str, cycles: int) -> None: """ - Startet einen einschaltverzoegerten Timer. + Starts an on-delay timer. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param cycles: Zyklusanzahl, der Verzoegerung wenn neu gestartet + :param name: Unique name for accessing the timer + :param cycles: Number of cycles for the delay if restarted """ if self.__dict_ton.get(name, [-1])[0] == -1: self.__dict_ton[name] = [cycles, True] @@ -317,28 +307,28 @@ class Cycletools: def get_tp(self, name: str) -> bool: """ - Impulstimer. + Pulse timer. - :param name: Eindeutiger Name des Timers - :return: Wert des Impulses + :param name: Unique name of the timer + :return: Value of the pulse """ return self.__dict_tp.get(name, [-1])[0] > 0 def get_tpc(self, name: str) -> bool: """ - Impulstimer. + Pulse timer. - :param name: Eindeutiger Name des Timers - :return: Wert des Impulses + :param name: Unique name of the timer + :return: Value of the pulse """ return self.__dict_tp.get(name, [-1])[0] > 0 def set_tp(self, name: str, milliseconds: int) -> None: """ - Startet einen Impuls Timer. + Starts a pulse timer. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param milliseconds: Millisekunden, die der Impuls anstehen soll + :param name: Unique name for accessing the timer + :param milliseconds: Milliseconds the pulse should be active """ if self.__dict_tp.get(name, [-1])[0] == -1: self.__dict_tp[name] = [ceil(milliseconds / self.__cycletime), True] @@ -347,10 +337,10 @@ class Cycletools: def set_tpc(self, name: str, cycles: int) -> None: """ - Startet einen Impuls Timer. + Starts a pulse timer. - :param name: Eindeutiger Name fuer Zugriff auf Timer - :param cycles: Zyklusanzahl, die der Impuls anstehen soll + :param name: Unique name for accessing the timer + :param cycles: Number of cycles the pulse should be active """ if self.__dict_tp.get(name, [-1])[0] == -1: self.__dict_tp[name] = [cycles, True] @@ -371,11 +361,9 @@ class Cycletools: class ProcimgWriter(Thread): """ - Klasse fuer Synchroniseriungs-Thread. + Class for synchronization thread. - Diese Klasse wird als Thread gestartet, wenn das Prozessabbild zyklisch - synchronisiert werden soll. Diese Funktion wird hauptsaechlich fuer das - Event-Handling verwendet. + This class is started as a thread if the process image should be synchronized cyclically. This function is mainly used for event handling. """ __slots__ = ( @@ -409,7 +397,7 @@ class ProcimgWriter(Thread): self.newdata = Event() def __check_change(self, dev) -> None: - """Findet Aenderungen fuer die Eventueberwachung.""" + """Finds changes for event monitoring.""" for io_event in dev._dict_events: if dev._ba_datacp[io_event._slc_address] == dev._ba_devdata[io_event._slc_address]: continue @@ -435,7 +423,7 @@ class ProcimgWriter(Thread): else: self._eventq.put((regfunc, io_event._name, io_event.value), False) else: - # Verzögertes Event in dict einfügen + # Insert delayed event into dict tup_fire = ( regfunc, io_event._name, @@ -454,7 +442,7 @@ class ProcimgWriter(Thread): else: self._eventq.put((regfunc, io_event._name, io_event.value), False) else: - # Verzögertes Event in dict einfügen + # Insert delayed event into dict tup_fire = ( regfunc, io_event._name, @@ -464,11 +452,11 @@ class ProcimgWriter(Thread): if regfunc.overwrite or tup_fire not in self.__dict_delay: self.__dict_delay[tup_fire] = ceil(regfunc.delay / 1000 / self._refresh) - # Nach Verarbeitung aller IOs die Bytes kopieren (Lock ist noch drauf) + # Copy the bytes after processing all IOs (lock is still active) dev._ba_datacp = dev._ba_devdata[:] def __exec_th(self) -> None: - """Laeuft als Thread, der Events als Thread startet.""" + """Runs as thread that starts events as threads.""" while self.__eventwork: try: tup_fireth = self._eventqth.get(timeout=1) @@ -480,15 +468,15 @@ class ProcimgWriter(Thread): def _collect_events(self, value: bool) -> bool: """ - Aktiviert oder Deaktiviert die Eventueberwachung. + Enables or disables event monitoring. - :param value: True aktiviert / False deaktiviert - :return: True, wenn Anforderung erfolgreich war + :param value: True activates / False deactivates + :return: True, if request was successful """ if type(value) != bool: raise TypeError("value must be ") - # Nur starten, wenn System läuft + # Only start if system is running if not self.is_alive(): self.__eventwork = False return False @@ -497,12 +485,12 @@ class ProcimgWriter(Thread): with self.lck_refresh: self.__eventwork = value if not value: - # Nur leeren beim deaktivieren + # Only empty when deactivating self._eventqth = queue.Queue() self._eventq = queue.Queue() self.__dict_delay = {} - # Threadmanagement + # Thread management if value and not self.__eventth.is_alive(): self.__eventth = Thread(target=self.__exec_th) self.__eventth.daemon = True @@ -512,14 +500,14 @@ class ProcimgWriter(Thread): def get_refresh(self) -> int: """ - Gibt Zykluszeit zurueck. + Returns cycle time. - :return: Zykluszeit in Millisekunden + :return: cycle time in milliseconds """ return int(self._refresh * 1000) def run(self): - """Startet die automatische Prozessabbildsynchronisierung.""" + """Starts automatic process image synchronization.""" fh = self._modio._create_myfh() mrk_delay = self._refresh @@ -537,7 +525,7 @@ class ProcimgWriter(Thread): RuntimeWarning, ) mrk_delay = self._refresh - # Nur durch cycleloop erreichbar - keine verzögerten Events + # Only reachable through cycleloop - no delayed events continue try: @@ -558,7 +546,7 @@ class ProcimgWriter(Thread): bytesbuff[dev._slc_devoff] = fh.read(len(dev._ba_devdata)) if self._modio._monitoring or dev._shared_procimg: - # Inputs und Outputs in Puffer + # Inputs and outputs in buffer dev._ba_devdata[:] = bytesbuff[dev._slc_devoff] if ( self.__eventwork @@ -567,7 +555,7 @@ class ProcimgWriter(Thread): ): self.__check_change(dev) else: - # Inputs in Puffer, Outputs in Prozessabbild + # Inputs in buffer, outputs in process image dev._ba_devdata[dev._slc_inp] = bytesbuff[dev._slc_inpoff] if ( self.__eventwork @@ -601,12 +589,12 @@ class ProcimgWriter(Thread): ) mrk_warn = True - # Alle aufwecken + # Wake all self.lck_refresh.release() self.newdata.set() finally: - # Verzögerte Events prüfen + # Check delayed events if self.__eventwork: for tup_fire in tuple(self.__dict_delay.keys()): if tup_fire[0].overwrite and tup_fire[3].value != tup_fire[2]: @@ -614,7 +602,7 @@ class ProcimgWriter(Thread): else: self.__dict_delay[tup_fire] -= 1 if self.__dict_delay[tup_fire] <= 0: - # Verzögertes Event übernehmen und löschen + # Accept and delete delayed event if tup_fire[0].as_thread: self._eventqth.put(tup_fire, False) else: @@ -633,18 +621,18 @@ class ProcimgWriter(Thread): # Sleep and not .wait (.wait uses system clock) sleep(self._refresh - mrk_delay) - # Alle am Ende erneut aufwecken + # Wake all again at the end self._collect_events(False) self.newdata.set() fh.close() def stop(self): - """Beendet die automatische Prozessabbildsynchronisierung.""" + """Terminates automatic process image synchronization.""" self._work.set() def set_refresh(self, value): - """Setzt die Zykluszeit in Millisekunden. - @param value Millisekunden""" + """Sets the cycle time in milliseconds. + @param value Milliseconds""" if type(value) == int and 5 <= value <= 2000: self._refresh = value / 1000 else: diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index 5aa46b8..632271a 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""RevPiModIO Modul fuer die Verwaltung der IOs.""" +"""RevPiModIO module for managing IOs.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -12,14 +12,14 @@ from threading import Event from ._internal import consttostr, RISING, FALLING, BOTH, INP, OUT, MEM, PROCESS_IMAGE_SIZE try: - # Funktioniert nur auf Unix + # Only works on Unix from fcntl import ioctl except Exception: ioctl = None class IOEvent(object): - """Basisklasse fuer IO-Events.""" + """Base class for IO events.""" __slots__ = "as_thread", "delay", "edge", "func", "overwrite", "prefire" @@ -34,7 +34,7 @@ class IOEvent(object): class IOList(object): - """Basisklasse fuer direkten Zugriff auf IO Objekte.""" + """Base class for direct access to IO objects.""" def __init__(self, modio): """Init IOList class.""" @@ -44,10 +44,10 @@ class IOList(object): def __contains__(self, key): """ - Prueft ob IO existiert. + Checks if IO exists. - :param key: IO-Name oder Bytenummer - :return: True, wenn IO vorhanden / Byte belegt + :param key: IO name or byte number + :return: True if IO exists / byte is occupied """ if type(key) == int: return len(self.__dict_iobyte.get(key, [])) > 0 @@ -56,16 +56,16 @@ class IOList(object): def __delattr__(self, key): """ - Entfernt angegebenen IO. + Removes specified IO. - :param key: IO zum entfernen + :param key: IO to remove """ io_del = object.__getattribute__(self, key) - # Alte Events vom Device löschen + # Delete old events from device io_del.unreg_event() - # IO aus Byteliste und Attributen entfernen + # Remove IO from byte list and attributes if io_del._bitshift: self.__dict_iobyte[io_del.address][io_del._bitaddress] = None @@ -128,10 +128,10 @@ class IOList(object): def __getattr__(self, key): """ - Verwaltet geloeschte IOs (Attribute, die nicht existieren). + Manages deleted IOs (attributes that do not exist). - :param key: Name oder Byte eines alten IOs - :return: Alten IO, wenn in Ref-Listen + :param key: Name or byte of an old IO + :return: Old IO if in ref lists """ if key in self.__dict_iorefname: return self.__dict_iorefname[key] @@ -140,16 +140,15 @@ class IOList(object): def __getitem__(self, key): """ - Ruft angegebenen IO ab. + Retrieves specified IO. - Wenn der Key ist, wird ein einzelner IO geliefert. Wird - der Key als uebergeben, wird eine - geliefert mit 0, 1 oder 8 Eintraegen. - Wird als Key gegeben, werden die Listen in einer Liste - zurueckgegeben. + If the key is , a single IO is returned. If the key + is passed as , a is returned with 0, 1 + or 8 entries. If a is given as key, the lists are + returned in a list. - :param key: IO Name als oder Byte als . - :return: IO Objekt oder Liste der IOs + :param key: IO name as or byte as . + :return: IO object or list of IOs """ if type(key) == int: if key not in self.__dict_iobyte: @@ -165,9 +164,9 @@ class IOList(object): def __iter__(self): """ - Gibt Iterator aller IOs zurueck. + Returns iterator of all IOs. - :return: Iterator aller IOs + :return: Iterator of all IOs """ for int_io in sorted(self.__dict_iobyte): for io in self.__dict_iobyte[int_io]: @@ -176,9 +175,9 @@ class IOList(object): def __len__(self): """ - Gibt die Anzahl aller IOs zurueck. + Returns the number of all IOs. - :return: Anzahl aller IOs + :return: Number of all IOs """ int_ios = 0 for int_io in self.__dict_iobyte: @@ -188,7 +187,7 @@ class IOList(object): return int_ios def __setattr__(self, key, value): - """Verbietet aus Leistungsguenden das direkte Setzen von Attributen.""" + """Prohibits direct setting of attributes for performance reasons.""" if key in ( "_IOList__dict_iobyte", "_IOList__dict_iorefname", @@ -200,11 +199,11 @@ class IOList(object): def __private_replace_oldio_with_newio(self, io) -> None: """ - Ersetzt bestehende IOs durch den neu Registrierten. + Replaces existing IOs with the newly registered one. - :param io: Neuer IO der eingefuegt werden soll + :param io: New IO to be inserted """ - # Scanbereich festlegen + # Define scan range if io._bitshift: scan_start = io._parentio_address scan_stop = scan_start + io._parentio_length @@ -212,13 +211,13 @@ class IOList(object): scan_start = io.address scan_stop = scan_start + (1 if io._length == 0 else io._length) - # Defaultvalue über mehrere Bytes sammeln + # Collect default value over multiple bytes calc_defaultvalue = b"" for i in range(scan_start, scan_stop): for oldio in self.__dict_iobyte[i]: if type(oldio) == StructIO: - # Hier gibt es schon einen neuen IO + # There is already a new IO here if oldio._bitshift: if ( io._bitshift == oldio._bitshift @@ -230,28 +229,28 @@ class IOList(object): ) ) else: - # Bereits überschriebene bytes sind ungültig + # Already overwritten bytes are invalid raise MemoryError( "new io '{0}' overlaps memory of '{1}'".format(io._name, oldio._name) ) elif oldio is not None: - # IOs im Speicherbereich des neuen IO merken + # Remember IOs in the memory area of the new IO if io._bitshift: - # ios für ref bei bitaddress speichern + # Store IOs for ref at bitaddress self.__dict_iorefname[oldio._name] = DeadIO(oldio) else: - # Defaultwert berechnen + # Calculate default value oldio.byteorder = io._byteorder if io._byteorder == "little": calc_defaultvalue += oldio._defaultvalue else: calc_defaultvalue = oldio._defaultvalue + calc_defaultvalue - # ios aus listen entfernen + # Remove IOs from lists delattr(self, oldio._name) if io._defaultvalue is None: - # Nur bei StructIO und keiner gegebenen defaultvalue übernehmen + # Only take over for StructIO and no given defaultvalue if io._bitshift: io_byte_address = io._parentio_address - io.address io._defaultvalue = bool(io._parentio_defaultvalue[io_byte_address] & io._bitshift) @@ -260,9 +259,9 @@ class IOList(object): def _private_register_new_io_object(self, new_io) -> None: """ - Registriert neues IO Objekt unabhaenging von __setattr__. + Registers new IO object independently of __setattr__. - :param new_io: Neues IO Objekt + :param new_io: New IO object """ if isinstance(new_io, IOBase): if hasattr(self, new_io._name): @@ -274,10 +273,10 @@ class IOList(object): if do_replace: self.__private_replace_oldio_with_newio(new_io) - # Bytedict für Adresszugriff anpassen + # Adapt byte dict for address access if new_io._bitshift: if len(self.__dict_iobyte[new_io.address]) != 8: - # "schnell" 8 Einträge erstellen da es BIT IOs sind + # "Quickly" create 8 entries since these are BIT IOs self.__dict_iobyte[new_io.address] += [ None, None, @@ -343,21 +342,21 @@ class IOList(object): class DeadIO(object): - """Klasse, mit der ersetzte IOs verwaltet werden.""" + """Class for managing replaced IOs.""" __slots__ = "__deadio" def __init__(self, deadio): """ - Instantiierung der DeadIO-Klasse. + Instantiation of the DeadIO class. - :param deadio: IO, der ersetzt wurde + :param deadio: IO that was replaced """ self.__deadio = deadio def replace_io(self, name: str, frm: str, **kwargs) -> None: """ - Stellt Funktion fuer weiter Bit-Ersetzungen bereit. + Provides function for further bit replacements. :ref: :func:IntIOReplaceable.replace_io() """ @@ -368,16 +367,15 @@ class DeadIO(object): class IOBase(object): """ - Basisklasse fuer alle IO-Objekte. + Base class for all IO objects. - Die Basisfunktionalitaet ermoeglicht das Lesen und Schreiben der Werte - als oder . Dies entscheidet sich bei der - Instantiierung. - Wenn eine Bittadresse angegeben wird, werden -Werte erwartet - und zurueckgegeben, ansonsten . + The basic functionality enables reading and writing of values as + or . This is decided during instantiation. + If a bit address is specified, values are expected and + returned, otherwise . - Diese Klasse dient als Basis fuer andere IO-Klassen mit denen die Werte - auch als verwendet werden koennen. + This class serves as a basis for other IO classes with which the values + can also be used as . """ __slots__ = ( @@ -401,24 +399,24 @@ class IOBase(object): def __init__(self, parentdevice, valuelist: list, iotype: int, byteorder: str, signed: bool): """ - Instantiierung der IOBase-Klasse. + Instantiation of the IOBase class. - :param parentdevice: Parentdevice auf dem der IO liegt - :param valuelist: Datenliste fuer Instantiierung + :param parentdevice: Parent device on which the IO is located + :param valuelist: Data list for instantiation ["name","defval","bitlen","startaddrdev",exp,"idx","bmk","bitaddr"] - :param iotype: Wert - :param byteorder: Byteorder 'little'/'big' fuer Berechnung - :param signed: Intberechnung mit Vorzeichen durchfuehren + :param iotype: value + :param byteorder: Byteorder 'little'/'big' for calculation + :param signed: Perform int calculation with sign """ # ["name","defval","bitlen","startaddrdev",exp,"idx","bmk","bitaddr"] # [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ] self._parentdevice = parentdevice - # Bitadressen auf Bytes aufbrechen und umrechnen + # Break down bit addresses to bytes and convert self._bitaddress = -1 if valuelist[7] == "" else int(valuelist[7]) % 8 self._bitshift = None if self._bitaddress == -1 else 1 << self._bitaddress - # Längenberechnung + # Length calculation self._bitlength = int(valuelist[2]) self._length = 1 if self._bitaddress == 0 else int(self._bitlength / 8) @@ -434,11 +432,11 @@ class IOBase(object): int_startaddress = int(valuelist[3]) if self._bitshift: - # Höhere Bits als 7 auf nächste Bytes umbrechen + # Wrap bits higher than 7 to next bytes int_startaddress += int(int(valuelist[7]) / 8) self._slc_address = slice(int_startaddress, int_startaddress + 1) - # Defaultvalue ermitteln, sonst False + # Determine default value, otherwise False if valuelist[1] is None and type(self) == StructIO: self._defaultvalue = None else: @@ -447,21 +445,21 @@ class IOBase(object): except Exception: self._defaultvalue = False - # Ioctl für Bitsetzung setzen + # Set ioctl for bit setting self.__bit_ioctl_off = struct.pack("-Wert der Klasse. + value of the class. - :return: Nur False wenn False oder 0 sonst True + :return: Only False if False or 0, otherwise True """ if self._bitshift: return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift) @@ -508,17 +506,17 @@ class IOBase(object): def __len__(self): """ - Gibt die Bytelaenge des IO zurueck. + Returns the byte length of the IO. - :return: Bytelaenge des IO - 0 bei BITs + :return: Byte length of the IO - 0 for BITs """ return 0 if self._bitaddress > 0 else self._length def __str__(self): """ - -Wert der Klasse. + value of the class. - :return: Namen des IOs + :return: Name of the IO """ return self._name @@ -526,16 +524,16 @@ class IOBase(object): self, func, delay: int, edge: int, as_thread: bool, overwrite: bool, prefire: bool ) -> None: """ - Verwaltet reg_event und reg_timerevent. + Manages reg_event and reg_timerevent. - :param func: Funktion die bei Aenderung aufgerufen werden soll - :param delay: Verzoegerung in ms zum Ausloesen - auch bei Wertaenderung - :param edge: Ausfuehren bei RISING, FALLING or BOTH Wertaenderung - :param as_thread: Bei True, Funktion als EventCallback-Thread ausfuehren - :param overwrite: Wenn True, wird Event bei ueberschrieben - :param prefire: Ausloesen mit aktuellem Wert, wenn mainloop startet + :param func: Function to be called on change + :param delay: Delay in ms for triggering - also on value change + :param edge: Execute on RISING, FALLING or BOTH value change + :param as_thread: If True, execute function as EventCallback thread + :param overwrite: If True, event will be overwritten + :param prefire: Trigger with current value when mainloop starts """ - # Prüfen ob Funktion callable ist + # Check if function is callable if not callable(func): raise ValueError("registered function '{0}' is not callable".format(func)) if type(delay) != int or delay < 0: @@ -551,10 +549,10 @@ class IOBase(object): IOEvent(func, edge, as_thread, delay, overwrite, prefire) ] else: - # Prüfen ob Funktion schon registriert ist + # Check if function is already registered for regfunc in self._parentdevice._dict_events[self]: if regfunc.func != func: - # Nächsten Eintrag testen + # Test next entry continue if edge == BOTH or regfunc.edge == BOTH: @@ -576,7 +574,7 @@ class IOBase(object): "already in list".format(self._name, func, consttostr(edge)) ) - # Eventfunktion einfügen + # Insert event function with self._parentdevice._filelock: self._parentdevice._dict_events[self].append( IOEvent(func, edge, as_thread, delay, overwrite, prefire) @@ -584,15 +582,15 @@ class IOBase(object): def _get_address(self) -> int: """ - Gibt die absolute Byteadresse im Prozessabbild zurueck. + Returns the absolute byte address in the process image. - :return: Absolute Byteadresse + :return: Absolute byte address """ return self._parentdevice._offset + self._slc_address.start def _get_byteorder(self) -> str: """ - Gibt konfigurierte Byteorder zurueck. + Returns configured byteorder. :return: Byteorder """ @@ -604,7 +602,7 @@ class IOBase(object): def _get_iotype(self) -> int: """ - Gibt io type zurueck. + Returns io type. :return: io type """ @@ -631,10 +629,10 @@ class IOBase(object): # Write single bit to process image value = self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift if self._parentdevice._modio._run_on_pi: - # IOCTL auf dem RevPi + # IOCTL on the RevPi with self._parentdevice._modio._myfh_lck: try: - # Set value durchführen (Funktion K+16) + # Perform set value (function K+16) ioctl( self._parentdevice._modio._myfh, 19216, @@ -645,7 +643,7 @@ class IOBase(object): return False elif hasattr(self._parentdevice._modio._myfh, "ioctl"): - # IOCTL über Netzwerk + # IOCTL over network with self._parentdevice._modio._myfh_lck: try: self._parentdevice._modio._myfh.ioctl( @@ -657,9 +655,9 @@ class IOBase(object): return False else: - # IOCTL in Datei simulieren + # Simulate IOCTL in file try: - # Set value durchführen (Funktion K+16) + # Execute set value (function K+16) self._parentdevice._modio._simulate_ioctl( 19216, self.__bit_ioctl_on if value else self.__bit_ioctl_off, @@ -685,17 +683,17 @@ class IOBase(object): def get_defaultvalue(self): """ - Gibt die Defaultvalue von piCtory zurueck. + Returns the default value from piCtory. - :return: Defaultvalue als oder + :return: Default value as or """ return self._defaultvalue def get_value(self): """ - Gibt den Wert des IOs zurueck. + Returns the value of the IO. - :return: IO-Wert als oder + :return: IO value as or """ if self._bitshift: return bool(self._parentdevice._ba_devdata[self._slc_address.start] & self._bitshift) @@ -704,50 +702,49 @@ class IOBase(object): def reg_event(self, func, delay=0, edge=BOTH, as_thread=False, prefire=False): """ - Registriert fuer IO ein Event bei der Eventueberwachung. + Registers an event for the IO in the event monitoring. - Die uebergebene Funktion wird ausgefuehrt, wenn sich der IO Wert - aendert. Mit Angabe von optionalen Parametern kann das - Ausloeseverhalten gesteuert werden. + The passed function is executed when the IO value changes. With + specification of optional parameters, the trigger behavior can be + controlled. - HINWEIS: Die delay-Zeit muss in die .cycletime passen, ist dies nicht - der Fall, wird IMMER aufgerundet! + NOTE: The delay time must fit into .cycletime, if not, it will + ALWAYS be rounded up! - :param func: Funktion die bei Aenderung aufgerufen werden soll - :param delay: Verzoegerung in ms zum Ausloesen wenn Wert gleich bleibt - :param edge: Ausfuehren bei RISING, FALLING or BOTH Wertaenderung - :param as_thread: Bei True, Funktion als EventCallback-Thread ausfuehren - :param prefire: Ausloesen mit aktuellem Wert, wenn mainloop startet + :param func: Function to be called on change + :param delay: Delay in ms for triggering if value stays the same + :param edge: Execute on RISING, FALLING or BOTH value change + :param as_thread: If True, execute function as EventCallback thread + :param prefire: Trigger with current value when mainloop starts """ self.__reg_xevent(func, delay, edge, as_thread, True, prefire) def reg_timerevent(self, func, delay, edge=BOTH, as_thread=False, prefire=False): """ - Registriert fuer IO einen Timer, welcher nach delay func ausfuehrt. + Registers a timer for the IO which executes func after delay. - Der Timer wird gestartet, wenn sich der IO Wert aendert und fuehrt die - uebergebene Funktion aus - auch wenn sich der IO Wert in der - zwischenzeit geaendert hat. Sollte der Timer nicht abelaufen sein und - die Bedingugn erneut zutreffen, wird der Timer NICHT auf den delay Wert - zurueckgesetzt oder ein zweites Mal gestartet. Fuer dieses Verhalten - kann .reg_event(..., delay=wert) verwendet werden. + The timer is started when the IO value changes and executes the passed + function - even if the IO value has changed in the meantime. If the + timer has not expired and the condition is met again, the timer is NOT + reset to the delay value or started a second time. For this behavior, + .reg_event(..., delay=value) can be used. - HINWEIS: Die delay-Zeit muss in die .cycletime passen, ist dies nicht - der Fall, wird IMMER aufgerundet! + NOTE: The delay time must fit into .cycletime, if not, it will + ALWAYS be rounded up! - :param func: Funktion die bei Aenderung aufgerufen werden soll - :param delay: Verzoegerung in ms zum Ausloesen - auch bei Wertaenderung - :param edge: Ausfuehren bei RISING, FALLING or BOTH Wertaenderung - :param as_thread: Bei True, Funktion als EventCallback-Thread ausfuehren - :param prefire: Ausloesen mit aktuellem Wert, wenn mainloop startet + :param func: Function to be called on change + :param delay: Delay in ms for triggering - also on value change + :param edge: Execute on RISING, FALLING or BOTH value change + :param as_thread: If True, execute function as EventCallback thread + :param prefire: Trigger with current value when mainloop starts """ self.__reg_xevent(func, delay, edge, as_thread, False, prefire) def set_value(self, value) -> None: """ - Setzt den Wert des IOs. + Sets the value of the IO. - :param value: IO-Wert als oder + :param value: IO value as or """ if self._read_only_io: if self._iotype == INP: @@ -762,27 +759,27 @@ class IOBase(object): raise RuntimeError("the io object '{0}' is read only".format(self._name)) if self._bitshift: - # Versuchen egal welchen Typ in Bool zu konvertieren + # Try to convert any type to bool value = bool(value) - # Für Bitoperationen sperren + # Lock for bit operations self._parentdevice._filelock.acquire() if self._parentdevice._shared_procimg: # Mark this IO for write operations self._parentdevice._shared_write.add(self) - # Hier gibt es immer nur ein byte, als int holen + # There is always only one byte here, get as int int_byte = self._parentdevice._ba_devdata[self._slc_address.start] - # Aktuellen Wert vergleichen und ggf. setzen + # Compare current value and set if necessary if not bool(int_byte & self._bitshift) == value: if value: int_byte += self._bitshift else: int_byte -= self._bitshift - # Zurückschreiben wenn verändert + # Write back if changed self._parentdevice._ba_devdata[self._slc_address.start] = int_byte self._parentdevice._filelock.release() @@ -810,10 +807,10 @@ class IOBase(object): def unreg_event(self, func=None, edge=None) -> None: """ - Entfernt ein Event aus der Eventueberwachung. + Removes an event from event monitoring. - :param func: Nur Events mit angegebener Funktion - :param edge: Nur Events mit angegebener Funktion und angegebener Edge + :param func: Only events with specified function + :param edge: Only events with specified function and specified edge """ if self in self._parentdevice._dict_events: if func is None: @@ -825,7 +822,7 @@ class IOBase(object): if regfunc.func != func or edge is not None and regfunc.edge != edge: newlist.append(regfunc) - # Wenn Funktionen übrig bleiben, diese übernehmen + # If functions remain, take them over with self._parentdevice._filelock: if len(newlist) > 0: self._parentdevice._dict_events[self] = newlist @@ -834,48 +831,48 @@ class IOBase(object): def wait(self, edge=BOTH, exitevent=None, okvalue=None, timeout=0) -> int: """ - Wartet auf Wertaenderung eines IOs. + Waits for value change of an IO. - Die Wertaenderung wird immer uerberprueft, wenn fuer Devices - mit aktiviertem autorefresh neue Daten gelesen wurden. + The value change is always checked when new data has been read for devices + with autorefresh enabled. - Bei Wertaenderung, wird das Warten mit 0 als Rueckgabewert beendet. + On value change, waiting ends with 0 as return value. - HINWEIS: Wenn keine neuen Daten liefert, wird - bis in die Ewigkeit gewartet (nicht bei Angabe von "timeout"). + NOTE: If does not deliver new data, + it will wait forever (not when "timeout" is specified). - 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. + If edge is specified with RISING or FALLING, this edge must be + triggered. If the value is 1 when entering with edge + RISING, the wait will only end when changing from 0 to 1. - Als exitevent kann ein -Objekt uebergeben - werden, welches das Warten bei is_set() sofort mit 1 als Rueckgabewert - beendet. + A object can be passed as exitevent, + which ends the waiting immediately with 1 as return value + when is_set(). - Wenn der Wert okvalue an dem IO fuer das Warten anliegt, wird - das Warten sofort mit -1 als Rueckgabewert beendet. + If the value okvalue is present at the IO for waiting, the + waiting ends immediately with -1 as return value. - Der Timeoutwert bricht beim Erreichen das Warten sofort mit - Wert 2 Rueckgabewert ab. (Das Timeout wird ueber die Zykluszeit - der autorefresh Funktion berechnet, entspricht also nicht exakt den - angegeben Millisekunden! Es wird immer nach oben gerundet!) + The timeout value aborts the waiting immediately when reached with + value 2 as return value. (The timeout is calculated via the cycle time + of the autorefresh function, so it does not correspond exactly to the + specified milliseconds! It is always rounded up!) - :param edge: Flanke RISING, FALLING, BOTH die eintreten muss - :param exitevent: fuer vorzeitiges Beenden - :param okvalue: IO-Wert, bei dem das Warten sofort beendet wird - :param timeout: Zeit in ms nach der abgebrochen wird - :return: erfolgreich Werte <= 0 + :param edge: Edge RISING, FALLING, BOTH that must occur + :param exitevent: for early termination + :param okvalue: IO value at which waiting ends immediately + :param timeout: Time in ms after which to abort + :return: successful values <= 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 + - Successfully waited + - Value 0: IO has changed value + - Value -1: okvalue matched IO + - Erroneously waited + - Value 1: exitevent was set + - Value 2: timeout expired + - Value 100: Devicelist.exit() was called """ - # Prüfen ob Device in autorefresh ist + # Check if device is in autorefresh if not self._parentdevice._selfupdate: raise RuntimeError( "autorefresh is not activated for device '{0}|{1}' - there " @@ -895,7 +892,7 @@ class IOBase(object): if edge != BOTH and not self._bitshift: raise ValueError("parameter 'edge' can be used with bit Inputs only") - # Abbruchwert prüfen + # Check abort value if okvalue == self.value: return -1 @@ -933,15 +930,15 @@ class IOBase(object): elif bool_timecount: flt_timecount += 2.5 - # Abbruchevent wurde gesetzt + # Abort event was set if exitevent.is_set(): return 1 - # RevPiModIO mainloop wurde verlassen + # RevPiModIO mainloop was exited if self._parentdevice._modio._waitexit.is_set(): return 100 - # Timeout abgelaufen + # Timeout expired return 2 address = property(_get_address) @@ -956,12 +953,12 @@ class IOBase(object): class IntIO(IOBase): """ - Klasse fuer den Zugriff auf die Daten mit Konvertierung in int. + Class for accessing data with conversion to int. - Diese Klasse erweitert die Funktion von um Funktionen, - ueber die mit Werten gearbeitet werden kann. Fuer die - Umwandlung koennen 'Byteorder' (Default 'little') und 'signed' (Default - False) als Parameter gesetzt werden. + This class extends the functionality of with functions + for working with values. For the + conversion, 'byteorder' (default 'little') and 'signed' (default + False) can be set as parameters. :ref: :class:`IOBase` """ @@ -970,9 +967,9 @@ class IntIO(IOBase): def __int__(self): """ - Gibt IO-Wert zurueck mit Beachtung byteorder/signed. + Returns IO value considering byteorder/signed. - :return: IO-Wert als + :return: IO value as """ return int.from_bytes( self._parentdevice._ba_devdata[self._slc_address], @@ -1006,15 +1003,15 @@ class IntIO(IOBase): def _get_signed(self) -> bool: """ - Ruft ab, ob der Wert Vorzeichenbehaftet behandelt werden soll. + Retrieves whether the value should be treated as signed. - :return: True, wenn Vorzeichenbehaftet + :return: True if signed """ return self._signed def _set_byteorder(self, value: str) -> None: """ - Setzt Byteorder fuer Umwandlung. + Sets byteorder for conversion. :param value: 'little' or 'big' """ @@ -1026,9 +1023,9 @@ class IntIO(IOBase): def _set_signed(self, value: bool) -> None: """ - Left fest, ob der Wert Vorzeichenbehaftet behandelt werden soll. + Sets whether the value should be treated as signed. - :param value: True, wenn mit Vorzeichen behandel + :param value: True if to be treated as signed """ if type(value) != bool: raise TypeError("signed must be True or False") @@ -1036,17 +1033,17 @@ class IntIO(IOBase): def get_intdefaultvalue(self) -> int: """ - Gibt die Defaultvalue als zurueck. + Returns the default value as . - :return: Defaultvalue + :return: Default value """ return int.from_bytes(self._defaultvalue, byteorder=self._byteorder, signed=self._signed) def get_intvalue(self) -> int: """ - Gibt IO-Wert zurueck mit Beachtung byteorder/signed. + Returns IO value considering byteorder/signed. - :return: IO-Wert als + :return: IO value as """ return int.from_bytes( self._parentdevice._ba_devdata[self._slc_address], @@ -1056,9 +1053,9 @@ class IntIO(IOBase): def set_intvalue(self, value: int) -> None: """ - Setzt IO mit Beachtung byteorder/signed. + Sets IO considering byteorder/signed. - :param value: Wert + :param value: Value """ if type(value) == int: self.set_value( @@ -1081,15 +1078,15 @@ class IntIO(IOBase): class IntIOCounter(IntIO): - """Erweitert die IntIO-Klasse um die .reset() Funktion fuer Counter.""" + """Extends the IntIO class with the .reset() function for counters.""" __slots__ = ("__ioctl_arg",) def __init__(self, counter_id, parentdevice, valuelist, iotype, byteorder, signed): """ - Instantiierung der IntIOCounter-Klasse. + Instantiation of the IntIOCounter class. - :param counter_id: ID fuer den Counter, zu dem der IO gehoert (0-15) + :param counter_id: ID for the counter to which the IO belongs (0-15) :ref: :func:`IOBase.__init__(...)` """ if not isinstance(counter_id, int): @@ -1097,7 +1094,7 @@ class IntIOCounter(IntIO): if not 0 <= counter_id <= 15: raise ValueError("counter_id must be 0 - 15") - # Deviceposition + leer + Counter_ID + # Device position + empty + Counter_ID # ID-Bits: 7|6|5|4|3|2|1|0|15|14|13|12|11|10|9|8 self.__ioctl_arg = ( parentdevice._position.to_bytes(1, "little") @@ -1106,9 +1103,9 @@ class IntIOCounter(IntIO): ) """ - IOCTL fuellt dieses struct, welches durch padding im Speicher nach - uint8_t ein byte frei hat. Es muessen also 4 Byte uebergeben werden - wobei das Bitfield die Byteorder little hat!!! + IOCTL fills this struct, which has one byte free in memory after + uint8_t due to padding. Therefore 4 bytes must be passed + where the bitfield has little byteorder!!! typedef struct SDIOResetCounterStr { @@ -1117,27 +1114,27 @@ class IntIOCounter(IntIO): } SDIOResetCounter; """ - # Basisklasse laden + # Load base class super().__init__(parentdevice, valuelist, iotype, byteorder, signed) def reset(self) -> None: - """Setzt den Counter des Inputs zurueck.""" + """Resets the counter of the input.""" if self._parentdevice._modio._monitoring: raise RuntimeError("can not reset counter, while system is in monitoring mode") if self._parentdevice._modio._simulator: raise RuntimeError("can not reset counter, while system is in simulator mode") if self._parentdevice._modio._run_on_pi: - # IOCTL auf dem RevPi + # IOCTL on the RevPi with self._parentdevice._modio._myfh_lck: try: - # Counter reset durchführen (Funktion K+20) + # Execute counter reset (function K+20) ioctl(self._parentdevice._modio._myfh, 19220, self.__ioctl_arg) except Exception as e: self._parentdevice._modio._gotioerror("iorst", e) elif hasattr(self._parentdevice._modio._myfh, "ioctl"): - # IOCTL über Netzwerk + # IOCTL over network with self._parentdevice._modio._myfh_lck: try: self._parentdevice._modio._myfh.ioctl(19220, self.__ioctl_arg) @@ -1145,62 +1142,62 @@ class IntIOCounter(IntIO): self._parentdevice._modio._gotioerror("net_iorst", e) else: - # IOCTL in Datei simulieren + # Simulate IOCTL in file try: - # Set value durchführen (Funktion K+20) + # Execute set value (function K+20) self._parentdevice._modio._simulate_ioctl(19220, self.__ioctl_arg) except Exception as e: self._parentdevice._modio._gotioerror("file_iorst", e) class IntIOReplaceable(IntIO): - """Erweitert die IntIO-Klasse um die .replace_io Funktion.""" + """Extends the IntIO class with the .replace_io function.""" __slots__ = () def replace_io(self, name: str, frm: str, **kwargs) -> None: """ - Ersetzt bestehenden IO mit Neuem. + Replaces existing IO with new one. - Wenn die kwargs fuer byteorder und defaultvalue nicht angegeben werden, - uebernimmt das System die Daten aus dem ersetzten IO. + If the kwargs for byteorder and defaultvalue are not specified, + the system takes the data from the replaced IO. - Es darf nur ein einzelnes Formatzeichen 'frm' uebergeben werden. Daraus - wird dann die benoetigte Laenge an Bytes berechnet und der Datentyp - festgelegt. Moeglich sind: + Only a single format character 'frm' may be passed. From this, + the required length in bytes is calculated and the data type + is determined. Possible values are: - Bits / Bytes: ?, c, s - Integer : bB, hH, iI, lL, qQ - Float : e, f, d - Eine Ausnahme ist die Formatierung 's'. Hier koennen mehrere Bytes - zu einem langen IO zusammengefasst werden. Die Formatierung muss - '8s' fuer z.B. 8 Bytes sein - NICHT 'ssssssss'! + An exception is the 's' format. Here, multiple bytes + can be combined into one long IO. The formatting must be + '8s' for e.g. 8 bytes - NOT 'ssssssss'! - Wenn durch die Formatierung mehr Bytes benoetigt werden, als - der urspruenglige IO hat, werden die nachfolgenden IOs ebenfalls - verwendet und entfernt. + If more bytes are needed by the formatting than + the original IO has, the following IOs will also be + used and removed. - :param name: Name des neuen Inputs - :param frm: struct formatierung (1 Zeichen) oder 'ANZAHLs' z.B. '8s' - :param kwargs: Weitere Parameter + :param name: Name of the new input + :param frm: struct formatting (1 character) or 'NUMBERs' e.g. '8s' + :param kwargs: Additional parameters - - bmk: interne Bezeichnung fuer IO - - bit: Registriert IO als am angegebenen Bit im Byte - - byteorder: Byteorder fuer den IO, Standardwert=little - - wordorder: Wordorder wird vor byteorder angewendet - - defaultvalue: Standardwert fuer IO - - event: Funktion fuer Eventhandling registrieren - - delay: Verzoegerung in ms zum Ausloesen wenn Wert gleich bleibt - - edge: Event ausfuehren bei RISING, FALLING or BOTH Wertaenderung - - as_thread: Fuehrt die event-Funktion als RevPiCallback-Thread aus - - prefire: Ausloesen mit aktuellem Wert, wenn mainloop startet + - bmk: internal designation for IO + - bit: Registers IO as at the specified bit in the byte + - byteorder: Byteorder for the IO, default=little + - wordorder: Wordorder is applied before byteorder + - defaultvalue: Default value for IO + - event: Register function for event handling + - delay: Delay in ms for triggering when value remains the same + - edge: Execute event on RISING, FALLING or BOTH value change + - as_thread: Executes the event function as RevPiCallback thread + - prefire: Trigger with current value when mainloop starts ``_ """ - # StructIO erzeugen + # Create StructIO io_new = StructIO(self, name, frm, **kwargs) - # StructIO in IO-Liste einfügen + # Insert StructIO into IO list self._parentdevice._modio.io._private_register_new_io_object(io_new) # Optional Event eintragen @@ -1309,7 +1306,7 @@ class RelaisOutput(IOBase): return struct.unpack(self.__ioctl_arg_format, ioctl_return_value)[1:] else: # Return cycle value of just one relais as int, if this is a BOOL output - # Increase bit-address bei 1 to ignore fist element, which is the ioctl request value + # Increase bit-address by 1 to ignore first element, which is the ioctl request value return struct.unpack(self.__ioctl_arg_format, ioctl_return_value)[self._bitaddress + 1] switching_cycles = property(get_switching_cycles) @@ -1335,10 +1332,10 @@ class IntRelaisOutput(IntIO, RelaisOutput): class StructIO(IOBase): """ - Klasse fuer den Zugriff auf Daten ueber ein definierten struct. + Class for accessing data via a defined struct. - Sie stellt ueber struct die Werte in der gewuenschten Formatierung - bereit. Der struct-Formatwert wird bei der Instantiierung festgelegt. + It provides the values in the desired formatting via struct. + The struct format value is defined during instantiation. """ __slots__ = ( @@ -1352,30 +1349,30 @@ class StructIO(IOBase): def __init__(self, parentio, name: str, frm: str, **kwargs): """ - Erstellt einen IO mit struct-Formatierung. + Creates an IO with struct formatting. - :param parentio: ParentIO Objekt, welches ersetzt wird - :param name: Name des neuen IO - :param frm: struct formatierung (1 Zeichen) oder 'ANZAHLs' z.B. '8s' - :param kwargs: Weitere Parameter: - - bmk: Bezeichnung fuer IO - - bit: Registriert IO als am angegebenen Bit im Byte - - byteorder: Byteorder fuer IO, Standardwert vom ersetzten IO - - wordorder: Wordorder wird vor byteorder angewendet - - defaultvalue: Standardwert fuer IO, Standard vom ersetzten IO + :param parentio: ParentIO object that will be replaced + :param name: Name of the new IO + :param frm: struct formatting (1 character) or 'NUMBERs' e.g. '8s' + :param kwargs: Additional parameters: + - bmk: Description for IO + - bit: Registers IO as at specified bit in byte + - byteorder: Byteorder for IO, default from replaced IO + - wordorder: Wordorder is applied before byteorder + - defaultvalue: Default value for IO, default from replaced IO """ - # Structformatierung prüfen + # Check struct formatting regex = rematch("^([0-9]*s|[cbB?hHiIlLqQefd])$", frm) if regex is not None: - # Byteorder prüfen und übernehmen + # Check and take over byteorder byteorder = kwargs.get("byteorder", parentio._byteorder) if byteorder not in ("little", "big"): raise ValueError("byteorder must be 'little' or 'big'") bofrm = "<" if byteorder == "little" else ">" self._wordorder = kwargs.get("wordorder", None) - # Namen des parent fuer export merken + # Remember parent name for export self._parentio_name = parentio._name if frm == "?": @@ -1389,7 +1386,7 @@ class StructIO(IOBase): ) bitlength = 1 - # Bitweise Ersetzung erfordert diese Informationen zusätzlich + # Bitwise replacement requires this information additionally if parentio._byteorder == byteorder: self._parentio_defaultvalue = parentio._defaultvalue else: @@ -1414,7 +1411,7 @@ class StructIO(IOBase): # [name,default,anzbits,adressbyte,export,adressid,bmk,bitaddress] valuelist = [ name, - # Darf nur bei StructIO None sein, wird nur dann berechnet + # May only be None for StructIO, is only calculated then kwargs.get("defaultvalue", None), bitlength, parentio._slc_address.start, @@ -1429,7 +1426,7 @@ class StructIO(IOBase): "or 'COUNTs' e.g. '8s'" ) - # Basisklasse instantiieren + # Instantiate base class super().__init__( parentio._parentdevice, valuelist, parentio._iotype, byteorder, frm == frm.lower() ) @@ -1442,7 +1439,7 @@ class StructIO(IOBase): # export, so use parent settings for the new IO self._export = parentio._export - # Platz für neuen IO prüfen + # Check space for new IO if not ( self._slc_address.start >= parentio._parentdevice._dict_slc[parentio._iotype].start and self._slc_address.stop <= parentio._parentdevice._dict_slc[parentio._iotype].stop @@ -1471,15 +1468,15 @@ class StructIO(IOBase): def _get_frm(self) -> str: """ - Ruft die struct Formatierung ab. + Retrieves the struct formatting. - :return: struct Formatierung + :return: struct formatting """ return self.__frm[1:] def _get_signed(self) -> bool: """ - Ruft ab, ob der Wert Vorzeichenbehaftet behandelt werden soll. + Retrieves whether the value should be treated as signed. :return: True, wenn Vorzeichenbehaftet """ @@ -1504,9 +1501,9 @@ class StructIO(IOBase): def get_structdefaultvalue(self): """ - Gibt die Defaultvalue mit struct Formatierung zurueck. + Returns the default value with struct formatting. - :return: Defaultvalue vom Typ der struct-Formatierung + :return: Default value of struct formatting type """ if self._bitshift: return self._defaultvalue @@ -1519,7 +1516,7 @@ class StructIO(IOBase): def get_wordorder(self) -> str: """ - Gibt die wordorder für diesen IO zurück. + Returns the wordorder for this IO. :return: "little", "big" or "ignored" """ @@ -1527,9 +1524,9 @@ class StructIO(IOBase): def get_structvalue(self): """ - Gibt den Wert mit struct Formatierung zurueck. + Returns the value with struct formatting. - :return: Wert vom Typ der struct-Formatierung + :return: Value of struct formatting type """ if self._bitshift: return self.get_value() @@ -1542,9 +1539,9 @@ class StructIO(IOBase): def set_structvalue(self, value): """ - Setzt den Wert mit struct Formatierung. + Sets the value with struct formatting. - :param value: Wert vom Typ der struct-Formatierung + :param value: Value of struct formatting type """ if self._bitshift: self.set_value(value) @@ -1562,11 +1559,11 @@ class StructIO(IOBase): class MemIO(IOBase): """ - Erstellt einen IO für die Memory Werte in piCtory. + Creates an IO for the memory values in piCtory. - Dieser Typ ist nur für lesenden Zugriff vorgesehen und kann verschiedene - Datentypen über .value zurückgeben. Damit hat man nun auch Zugriff - auf Strings, welche in piCtory vergeben werden. + This type is only intended for read access and can return various + data types via .value. This also provides access to strings that + are assigned in piCtory. """ def get_variantvalue(self): diff --git a/src/revpimodio2/modio.py b/src/revpimodio2/modio.py index b6828de..a35a137 100644 --- a/src/revpimodio2/modio.py +++ b/src/revpimodio2/modio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""RevPiModIO Hauptklasse fuer piControl0 Zugriff.""" +"""RevPiModIO main class for piControl0 access.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -55,14 +55,14 @@ class DevSelect: class RevPiModIO(object): """ - Klasse fuer die Verwaltung der piCtory Konfiguration. + Class for managing the piCtory configuration. - Diese Klasse uebernimmt die gesamte Konfiguration aus piCtory und - laedt die Devices und IOs. 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. + This class takes over the entire configuration from piCtory and + loads the devices and IOs. It takes over exclusive management of the + process image and ensures that the data is synchronized. + If only individual devices are to be controlled, use + RevPiModIOSelected() and pass a list with + device positions or device names during instantiation. """ __slots__ = ( @@ -115,20 +115,20 @@ class RevPiModIO(object): shared_procimg=False, ): """ - Instantiiert die Grundfunktionen. + Instantiates the basic functions. - :param autorefresh: Wenn True, alle Devices zu autorefresh hinzufuegen - :param monitoring: In- und Outputs werden gelesen, niemals geschrieben - :param syncoutputs: Aktuell gesetzte Outputs vom Prozessabbild einlesen - :param procimg: Abweichender Pfad zum Prozessabbild - :param configrsc: Abweichender Pfad zur piCtory Konfigurationsdatei - :param simulator: Laedt das Modul als Simulator und vertauscht IOs - :param debug: Gibt alle Warnungen inkl. Zyklusprobleme aus - :param replace_io_file: Replace IO Konfiguration aus Datei laden + :param autorefresh: If True, add all devices to autorefresh + :param monitoring: Inputs and outputs are read, never written + :param syncoutputs: Read currently set outputs from process image + :param procimg: Alternative path to process image + :param configrsc: Alternative path to piCtory configuration file + :param simulator: Loads the module as simulator and swaps IOs + :param debug: Output all warnings including cycle problems + :param replace_io_file: Load replace IO configuration from file :param shared_procimg: Share process image with other processes, this could be insecure for automation """ - # Parameterprüfung + # Parameter validation acheck( bool, autorefresh=autorefresh, @@ -155,9 +155,9 @@ class RevPiModIO(object): self._init_shared_procimg = shared_procimg self._syncoutputs = syncoutputs - # TODO: bei simulator und procimg prüfen ob datei existiert / anlegen? + # TODO: check if file exists for simulator and procimg / create it? - # Private Variablen + # Private variables self.__cleanupfunc = None self._buffedwrite = False self._debug = 1 @@ -176,19 +176,19 @@ class RevPiModIO(object): self._th_mainloop = None self._waitexit = Event() - # Modulvariablen + # Module variables self.core = None - # piCtory Klassen + # piCtory classes self.app = None self.device = None self.io = None self.summary = None - # Event für Benutzeraktionen + # Event for user actions self.exitsignal = Event() - # Wert über setter setzen + # Set value via setter self.debug = debug try: @@ -196,12 +196,12 @@ class RevPiModIO(object): except Exception: self._run_on_pi = False - # Nur Konfigurieren, wenn nicht vererbt + # Only configure if not inherited if type(self) == RevPiModIO: self._configure(self.get_jconfigrsc()) def __del__(self): - """Zerstoert alle Klassen um aufzuraeumen.""" + """Destroys all classes to clean up.""" if hasattr(self, "_exit"): self.exit(full=True) if self._myfh is not None: @@ -239,10 +239,10 @@ class RevPiModIO(object): def __evt_exit(self, signum, sigframe) -> None: """ - Eventhandler fuer Programmende. + Event handler for program termination. - :param signum: Signalnummer - :param sigframe: Signalframe + :param signum: Signal number + :param sigframe: Signal frame """ signal(SIGINT, SIG_DFL) signal(SIGTERM, SIG_DFL) @@ -252,15 +252,15 @@ class RevPiModIO(object): def __exit_jobs(self): """Shutdown sub systems.""" if self._exit_level & 1: - # Nach Ausführung kann System weiter verwendet werden + # System can continue to be used after execution self._exit_level ^= 1 - # ProcimgWriter beenden und darauf warten + # Stop ProcimgWriter and wait for it if self._imgwriter is not None and self._imgwriter.is_alive(): self._imgwriter.stop() self._imgwriter.join(2.5) - # Alle Devices aus Autorefresh entfernen + # Remove all devices from autorefresh while len(self._lst_refresh) > 0: dev = self._lst_refresh.pop() dev._selfupdate = False @@ -285,15 +285,15 @@ class RevPiModIO(object): def _configure(self, jconfigrsc: dict) -> None: """ - Verarbeitet die piCtory Konfigurationsdatei. + Processes the piCtory configuration file. :param jconfigrsc: Data to build IOs as of JSON """ - # Filehandler konfigurieren, wenn er noch nicht existiert + # Configure file handler if it doesn't exist yet if self._myfh is None: self._myfh = self._create_myfh() - # App Klasse instantiieren + # Instantiate App class self.app = appmodule.App(jconfigrsc["App"]) # Apply device filter @@ -324,14 +324,14 @@ class RevPiModIO(object): lst_devices.append(dev) else: - # Devices aus JSON übernehmen + # Take devices from JSON lst_devices = jconfigrsc["Devices"] - # Device und IO Klassen anlegen + # Create Device and IO classes self.device = devicemodule.DeviceList() self.io = IOList(self) - # Devices initialisieren + # Initialize devices err_names_check = {} for device in sorted(lst_devices, key=lambda x: x["offset"]): # Pre-check of values @@ -347,14 +347,14 @@ class RevPiModIO(object): ) continue - # VDev alter piCtory Versionen auf KUNBUS-Standard ändern + # Change VDev of old piCtory versions to KUNBUS standard if device["position"] == "adap.": device["position"] = 64 while device["position"] in self.device: device["position"] += 1 if device["type"] == DeviceType.BASE: - # Basedevices + # Base devices pt = int(device["productType"]) if pt == ProductType.REVPI_CORE: # RevPi Core @@ -381,7 +381,7 @@ class RevPiModIO(object): dev_new = devicemodule.Flat(self, device, simulator=self._simulator) self.core = dev_new else: - # Base immer als Fallback verwenden + # Always use Base as fallback dev_new = devicemodule.Base(self, device, simulator=self._simulator) elif device["type"] == DeviceType.LEFT_RIGHT: # IOs @@ -393,7 +393,7 @@ class RevPiModIO(object): # RO dev_new = devicemodule.RoModule(self, device, simulator=self._simulator) else: - # Alle anderen IO-Devices + # All other IO devices dev_new = devicemodule.Device(self, device, simulator=self._simulator) elif device["type"] == DeviceType.VIRTUAL: # Virtuals @@ -405,7 +405,7 @@ class RevPiModIO(object): # Connectdevice dev_new = None else: - # Device-Type nicht gefunden + # Device type not found warnings.warn( "device type '{0}' on position {1} unknown" "".format(device["type"], device["position"]), @@ -414,7 +414,7 @@ class RevPiModIO(object): dev_new = None if dev_new is not None: - # Offset prüfen, muss mit Länge übereinstimmen + # Check offset, must match length if self._length < dev_new.offset: self._length = dev_new.offset @@ -428,7 +428,7 @@ class RevPiModIO(object): # Set shared_procimg mode, if requested on instantiation dev_new.shared_procimg(self._init_shared_procimg) - # DeviceList für direkten Zugriff aufbauen + # Build DeviceList for direct access setattr(self.device, dev_new.name, dev_new) # Check equal device names and destroy name attribute of device class @@ -443,18 +443,18 @@ class RevPiModIO(object): Warning, ) - # ImgWriter erstellen + # Create ImgWriter self._imgwriter = helpermodule.ProcimgWriter(self) if self._set_device_based_cycle_time: - # Refreshzeit CM1 25 Hz / CM3 50 Hz + # Refresh time CM1 25 Hz / CM3 50 Hz self._imgwriter.refresh = 20 if cpu_count() > 1 else 40 - # Aktuellen Outputstatus von procimg einlesen + # Read current output status from procimg if self._syncoutputs: self.syncoutputs() - # Für RS485 errors am core defaults laden sollte procimg NULL sein + # For RS485 errors at core, load defaults if procimg should be NULL if isinstance(self.core, devicemodule.Core) and not (self._monitoring or self._simulator): if self.core._slc_errorlimit1 is not None: io = self.io[self.core.offset + self.core._slc_errorlimit1.start][0] @@ -463,26 +463,25 @@ class RevPiModIO(object): io = self.io[self.core.offset + self.core._slc_errorlimit2.start][0] io.set_value(io._defaultvalue) - # RS485 errors schreiben + # Write RS485 errors self.writeprocimg(self.core) # Set replace IO before autostart to prevent cycle time exhausting self._configure_replace_io(self._get_cpreplaceio()) - # Optional ins autorefresh aufnehmen + # Optionally add to autorefresh if self._autorefresh: self.autorefresh_all() - # Summary Klasse instantiieren + # Instantiate Summary class self.summary = summarymodule.Summary(jconfigrsc["Summary"]) def _configure_replace_io(self, creplaceio: ConfigParser) -> None: """ - Importiert ersetzte IOs in diese Instanz. + Imports replaced IOs into this instance. - Importiert ersetzte IOs, welche vorher mit .export_replaced_ios(...) - in eine Datei exportiert worden sind. Diese IOs werden in dieser - Instanz wiederhergestellt. + Imports replaced IOs that were previously exported to a file using + .export_replaced_ios(...). These IOs are restored in this instance. :param creplaceio: Data to replace ios as """ @@ -490,10 +489,10 @@ class RevPiModIO(object): if io == "DEFAULT": continue - # IO prüfen + # Check IO parentio = creplaceio[io].get("replace", "") - # Funktionsaufruf vorbereiten + # Prepare function call dict_replace = { "frm": creplaceio[io].get("frm"), "byteorder": creplaceio[io].get("byteorder", "little"), @@ -558,11 +557,11 @@ class RevPiModIO(object): "".format(io, creplaceio[io]["defaultvalue"]) ) - # IO ersetzen + # Replace IO try: self.io[parentio].replace_io(name=io, **dict_replace) except Exception as e: - # NOTE: Bei Selected/Driver kann nicht geprüft werden + # NOTE: Cannot be checked for Selected/Driver if len(self._devselect.values) == 0: raise RuntimeError( "replace_io_file: can not replace '{0}' with '{1}' " @@ -571,7 +570,7 @@ class RevPiModIO(object): def _create_myfh(self): """ - Erstellt FileObject mit Pfad zum procimg. + Creates FileObject with path to procimg. :return: FileObject """ @@ -582,15 +581,15 @@ class RevPiModIO(object): """ Getter function. - :return: Pfad der verwendeten piCtory Konfiguration + :return: Path of the used piCtory configuration """ return self._configrsc def _get_cpreplaceio(self) -> ConfigParser: """ - Laedt die replace_io_file Konfiguration und verarbeitet sie. + Loads the replace_io_file configuration and processes it. - :return: der replace io daten + :return: of the replace io data """ cp = ConfigParser() @@ -608,17 +607,17 @@ class RevPiModIO(object): def _get_cycletime(self) -> int: """ - Gibt Aktualisierungsrate in ms der Prozessabbildsynchronisierung aus. + Returns the refresh rate in ms of the process image synchronization. - :return: Millisekunden + :return: Milliseconds """ return self._imgwriter.refresh def _get_debug(self) -> bool: """ - Gibt Status des Debugflags zurueck. + Returns the status of the debug flag. - :return: Status des Debugflags + :return: Status of the debug flag """ return self._debug == 1 @@ -626,7 +625,7 @@ class RevPiModIO(object): """ Getter function. - :return: Aktuelle Anzahl gezaehlter Fehler + :return: Current number of counted errors """ return self._ioerror @@ -634,7 +633,7 @@ class RevPiModIO(object): """ Getter function. - :return: Laenge in Bytes der Devices + :return: Length in bytes of the devices """ return self._length @@ -642,7 +641,7 @@ class RevPiModIO(object): """ Getter function. - :return: Anzahl erlaubte Fehler + :return: Number of allowed errors """ return self._maxioerrors @@ -650,7 +649,7 @@ class RevPiModIO(object): """ Getter function. - :return: True, wenn als Monitoring gestartet + :return: True if started as monitoring """ return self._monitoring @@ -658,15 +657,15 @@ class RevPiModIO(object): """ Getter function. - :return: Pfad des verwendeten Prozessabbilds + :return: Path of the used process image """ return self._procimg def _get_replace_io_file(self) -> str: """ - Gibt Pfad zur verwendeten replace IO Datei aus. + Returns the path to the used replace IO file. - :return: Pfad zur replace IO Datei + :return: Path to the replace IO file """ return self._replace_io_file @@ -674,17 +673,17 @@ class RevPiModIO(object): """ Getter function. - :return: True, wenn als Simulator gestartet + :return: True if started as simulator """ return self._simulator def _gotioerror(self, action: str, e=None, show_warn=True) -> None: """ - IOError Verwaltung fuer Prozessabbildzugriff. + IOError management for process image access. - :param action: Zusatzinformationen zum loggen + :param action: Additional information for logging :param e: Exception to log if debug is enabled - :param show_warn: Warnung anzeigen + :param show_warn: Show warning """ self._ioerror += 1 if self._maxioerrors != 0 and self._ioerror >= self._maxioerrors: @@ -706,9 +705,9 @@ class RevPiModIO(object): def _set_cycletime(self, milliseconds: int) -> None: """ - Setzt Aktualisierungsrate der Prozessabbild-Synchronisierung. + Sets the refresh rate of the process image synchronization. - :param milliseconds: in Millisekunden + :param milliseconds: in milliseconds """ if self._looprunning: raise RuntimeError("can not change cycletime when cycleloop or mainloop is running") @@ -717,14 +716,14 @@ class RevPiModIO(object): def _set_debug(self, value: bool) -> None: """ - Setzt debugging Status um mehr Meldungen zu erhalten oder nicht. + Sets debugging status to get more messages or not. - :param value: Wenn True, werden umfangreiche Medungen angezeigt + :param value: If True, extensive messages are displayed """ if type(value) == bool: value = int(value) if not type(value) == int: - # Wert -1 ist zum kompletten deaktivieren versteckt + # Value -1 is hidden for complete deactivation raise TypeError("value must be or ") if not -1 <= value <= 1: raise ValueError("value must be True/False or -1, 0, 1") @@ -740,9 +739,9 @@ class RevPiModIO(object): def _set_maxioerrors(self, value: int) -> None: """ - Setzt Anzahl der maximal erlaubten Fehler bei Prozessabbildzugriff. + Sets the number of maximum allowed errors for process image access. - :param value: Anzahl erlaubte Fehler + :param value: Number of allowed errors """ if type(value) == int and value >= 0: self._maxioerrors = value @@ -751,18 +750,18 @@ class RevPiModIO(object): def _simulate_ioctl(self, request: int, arg=b"") -> None: """ - Simuliert IOCTL Funktionen auf procimg Datei. + Simulates IOCTL functions on procimg file. :param request: IO Request :param arg: Request argument """ if request == 19216: - # Einzelnes Bit setzen + # Set single bit byte_address = int.from_bytes(arg[:2], byteorder="little") bit_address = arg[2] new_value = bool(0 if len(arg) <= 3 else arg[3]) - # Simulatonsmodus schreibt direkt in Datei + # Simulation mode writes directly to file with self._myfh_lck: self._myfh.seek(byte_address) int_byte = int.from_bytes(self._myfh.read(1), byteorder="little") @@ -780,7 +779,7 @@ class RevPiModIO(object): self._myfh.flush() elif request == 19220: - # Counterwert auf 0 setzen + # Set counter value to 0 dev_position = arg[0] bit_field = int.from_bytes(arg[2:], byteorder="little") io_byte = -1 @@ -802,64 +801,61 @@ class RevPiModIO(object): self._myfh.flush() def autorefresh_all(self) -> None: - """Setzt alle Devices in autorefresh Funktion.""" + """Sets all devices to autorefresh function.""" for dev in self.device: dev.autorefresh() def cleanup(self) -> None: - """Beendet autorefresh und alle Threads.""" + """Terminates autorefresh and all threads.""" self._exit_level |= 2 self.exit(full=True) def cycleloop(self, func, cycletime=50, blocking=True): """ - Startet den Cycleloop. + Starts the cycle loop. - Der aktuelle Programmthread wird hier bis Aufruf von - .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. + The current program thread is "trapped" here until .exit() is called. + After each update of the process image, it executes the passed + function "func" and processes it. During execution of the function, + the process image is not further updated. The inputs retain their + current value until the end. Set outputs are written to the process + image after the function run completes. - Verlassen wird der Cycleloop, wenn die aufgerufene Funktion einen - Rueckgabewert nicht gleich None liefert (z.B. return True), oder durch - Aufruf von .exit(). + The cycle loop is left when the called function returns a value + not equal to None (e.g. return True), or by calling .exit(). - HINWEIS: Die Aktualisierungszeit und die Laufzeit der Funktion duerfen - die eingestellte autorefresh Zeit, bzw. uebergebene cycletime nicht - ueberschreiten! + NOTE: The refresh time and the runtime of the function must not + exceed the set autorefresh time or passed cycletime! - Ueber den Parameter cycletime wird die gewuenschte Zukluszeit der - uebergebenen Funktion gesetzt. Der Standardwert betraegt - 50 Millisekunden, in denen das Prozessabild eingelesen, die uebergebene - Funktion ausgefuert und das Prozessabbild geschrieben wird. + The cycletime parameter sets the desired cycle time of the passed + function. The default value is 50 milliseconds, in which the process + image is read, the passed function is executed, and the process image + is written. - :param func: Funktion, die ausgefuehrt werden soll - :param cycletime: Zykluszeit in Millisekunden - Standardwert 50 ms - :param blocking: Wenn False, blockiert das Programm hier NICHT + :param func: Function to be executed + :param cycletime: Cycle time in milliseconds - default 50 ms + :param blocking: If False, the program does NOT block here :return: None or the return value of the cycle function """ # Check for context manager if self._context_manager: raise RuntimeError("Can not start cycleloop inside a context manager (with statement)") - # Prüfen ob ein Loop bereits läuft + # Check if a loop is already running if self._looprunning: raise RuntimeError("can not start multiple loops mainloop/cycleloop") - # Prüfen ob Devices in autorefresh sind + # Check if devices are in autorefresh if len(self._lst_refresh) == 0: raise RuntimeError( "no device with autorefresh activated - use autorefresh=True " "or call .autorefresh_all() before entering cycleloop" ) - # Prüfen ob Funktion callable ist + # Check if function is callable if not callable(func): raise RuntimeError("registered function '{0}' ist not callable".format(func)) - # Thread erstellen, wenn nicht blockieren soll + # Create thread if it should not block if not blocking: self._th_mainloop = Thread( target=self.cycleloop, @@ -869,7 +865,7 @@ class RevPiModIO(object): self._th_mainloop.start() return - # Zykluszeit übernehmen + # Take over cycle time old_cycletime = self._imgwriter.refresh if not cycletime == self._imgwriter.refresh: # Set new cycle time and wait one imgwriter cycle to sync fist cycle @@ -877,10 +873,10 @@ class RevPiModIO(object): self._imgwriter.newdata.clear() self._imgwriter.newdata.wait(self._imgwriter._refresh) - # Benutzerevent + # User event self.exitsignal.clear() - # Cycleloop starten + # Start cycle loop self._exit.clear() self._looprunning = True cycleinfo = helpermodule.Cycletools(self._imgwriter.refresh, self) @@ -889,7 +885,7 @@ class RevPiModIO(object): self._imgwriter.newdata.clear() try: while ec is None and not cycleinfo.last: - # Auf neue Daten warten und nur ausführen wenn set() + # Wait for new data and only execute if set() if not self._imgwriter.newdata.wait(2.5): if not self._imgwriter.is_alive(): self.exit(full=False) @@ -905,18 +901,18 @@ class RevPiModIO(object): self._imgwriter.newdata.clear() - # Vor Aufruf der Funktion autorefresh sperren + # Lock autorefresh before calling the function self._imgwriter.lck_refresh.acquire() - # Vorbereitung für cycleinfo + # Preparation for cycleinfo cycleinfo._start_timer = default_timer() cycleinfo.last = self._exit.is_set() - # Funktion aufrufen und auswerten + # Call and evaluate function ec = func(cycleinfo) cycleinfo._docycle() - # autorefresh freigeben + # Release autorefresh self._imgwriter.lck_refresh.release() except Exception as ex: if self._imgwriter.lck_refresh.locked(): @@ -925,17 +921,17 @@ class RevPiModIO(object): self.exit(full=False) e = ex finally: - # Cycleloop beenden + # End cycle loop self._looprunning = False self._th_mainloop = None - # Alte autorefresh Zeit setzen + # Set old autorefresh time self._imgwriter.refresh = old_cycletime - # Exitstrategie ausführen + # Execute exit strategy self.__exit_jobs() - # Auf Fehler prüfen die im loop geworfen wurden + # Check for errors that were thrown in the loop if e is not None: raise e @@ -943,23 +939,23 @@ class RevPiModIO(object): def exit(self, full=True) -> None: """ - Beendet mainloop() und optional autorefresh. + Terminates mainloop() and optionally autorefresh. - Wenn sich das Programm im mainloop() befindet, wird durch Aufruf - von exit() die Kontrolle wieder an das Hauptprogramm zurueckgegeben. + If the program is in mainloop(), calling exit() returns control + to the main program. - Der Parameter full ist mit True vorbelegt und entfernt alle Devices aus - dem autorefresh. Der Thread fuer die Prozessabbildsynchronisierung - wird dann gestoppt und das Programm kann sauber beendet werden. + The full parameter defaults to True and removes all devices from + autorefresh. The thread for process image synchronization is then + stopped and the program can be terminated cleanly. - :param full: Entfernt auch alle Devices aus autorefresh + :param full: Also removes all devices from autorefresh """ self._exit_level |= 1 if full else 0 - # Echten Loopwert vor Events speichern + # Save actual loop value before events full = full and not self._looprunning - # Benutzerevent + # User event self.exitsignal.set() self._exit.set() @@ -974,14 +970,13 @@ class RevPiModIO(object): def export_replaced_ios(self, filename="replace_ios.conf") -> None: """ - Exportiert ersetzte IOs dieser Instanz. + Exports replaced IOs of this instance. - Exportiert alle ersetzten IOs, welche mit .replace_io(...) angelegt - wurden. Die Datei kann z.B. fuer RevPiPyLoad verwendet werden um Daten - in den neuen Formaten per MQTT zu uebertragen oder mit RevPiPyControl - anzusehen. + Exports all replaced IOs that were created with .replace_io(...). + The file can be used, for example, for RevPiPyLoad to transfer data + in the new formats via MQTT or to view with RevPiPyControl. - @param filename Dateiname fuer Exportdatei + @param filename Filename for export file """ acheck(str, filename=filename) @@ -1019,18 +1014,18 @@ class RevPiModIO(object): def get_jconfigrsc(self) -> dict: """ - Laedt die piCtory Konfiguration und erstellt ein . + Loads the piCtory configuration and creates a . - :return: der piCtory Konfiguration + :return: of the piCtory configuration """ - # piCtory Konfiguration prüfen + # Check piCtory configuration if self._configrsc is not None: if not access(self._configrsc, F_OK | R_OK): raise RuntimeError( "can not access pictory configuration at {0}".format(self._configrsc) ) else: - # piCtory Konfiguration an bekannten Stellen prüfen + # Check piCtory configuration at known locations lst_rsc = ["/etc/revpi/config.rsc", "/opt/KUNBUS/config.rsc"] for rscfile in lst_rsc: if access(rscfile, F_OK | R_OK): @@ -1055,26 +1050,25 @@ class RevPiModIO(object): def handlesignalend(self, cleanupfunc=None) -> None: """ - Signalhandler fuer Programmende verwalten. + Manage signal handler for program termination. - 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. + When this function is called, RevPiModIO takes over the SignalHandler + for SIGINT and SIGTERM. These are received when the operating system + or the user wants to cleanly terminate the control program. - 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. + The optional function "cleanupfunc" is executed last after the last + reading of the inputs. Outputs set there are written one last time + after the function completes. + This is intended for cleanup work, such as switching off the + LEDs on the RevPi-Core. - Nach einmaligem Empfangen eines der Signale und dem Beenden der - RevPiModIO Thrads / Funktionen werden die SignalHandler wieder - freigegeben. + After receiving one of the signals once and terminating the + RevPiModIO threads / functions, the SignalHandler are released + again. - :param cleanupfunc: Funktion wird nach dem Beenden ausgefuehrt + :param cleanupfunc: Function to be executed after termination """ - # Prüfen ob Funktion callable ist + # Check if function is callable if not (cleanupfunc is None or callable(cleanupfunc)): raise RuntimeError("registered function '{0}' ist not callable".format(cleanupfunc)) self.__cleanupfunc = cleanupfunc @@ -1083,55 +1077,54 @@ class RevPiModIO(object): def mainloop(self, blocking=True) -> None: """ - Startet den Mainloop mit Eventueberwachung. + Starts the mainloop with event monitoring. - 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. + The current program thread is "trapped" here until + RevPiDevicelist.exit() is called (unless blocking=False). It + runs through the event monitoring and checks for changes of + registered IOs with an event. If a change is detected, + the program executes the associated functions in sequence. - 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. + If the parameter "blocking" is specified as False, this activates + event monitoring and does NOT block the program at the point of call. + Well suited for GUI programming when events from the RevPi are needed + but the program should continue to execute. - :param blocking: Wenn False, blockiert das Programm hier NICHT + :param blocking: If False, the program does NOT block here """ # Check for context manager if self._context_manager: raise RuntimeError("Can not start mainloop inside a context manager (with statement)") - # Prüfen ob ein Loop bereits läuft + # Check if a loop is already running if self._looprunning: raise RuntimeError("can not start multiple loops mainloop/cycleloop") - # Prüfen ob Devices in autorefresh sind + # Check if devices are in autorefresh if len(self._lst_refresh) == 0: raise RuntimeError( "no device with autorefresh activated - use autorefresh=True " "or call .autorefresh_all() before entering mainloop" ) - # Thread erstellen, wenn nicht blockieren soll + # Create thread if it should not block if not blocking: self._th_mainloop = Thread(target=self.mainloop, kwargs={"blocking": True}) self._th_mainloop.start() return - # Benutzerevent + # User event self.exitsignal.clear() - # Event säubern vor Eintritt in Mainloop + # Clean event before entering mainloop self._exit.clear() self._looprunning = True - # Beim Eintritt in mainloop Bytecopy erstellen und prefire anhängen + # Create byte copy and attach prefire when entering mainloop for dev in self._lst_refresh: with dev._filelock: dev._ba_datacp = dev._ba_devdata[:] - # Prefire Events vorbereiten + # Prepare prefire events for io in dev._dict_events: for regfunc in dev._dict_events[io]: if not regfunc.prefire: @@ -1149,28 +1142,28 @@ class RevPiModIO(object): else: self._imgwriter._eventq.put((regfunc, io._name, io.value), False) - # ImgWriter mit Eventüberwachung aktivieren + # Activate ImgWriter with event monitoring self._imgwriter._collect_events(True) e = None runtime = -1 if self._debug == -1 else 0 while not self._exit.is_set(): - # Laufzeit der Eventqueue auf 0 setzen + # Set runtime of event queue to 0 if self._imgwriter._eventq.qsize() == 0: runtime = -1 if self._debug == -1 else 0 try: tup_fire = self._imgwriter._eventq.get(timeout=1) - # Messung Laufzeit der Queue starten + # Start measuring runtime of the queue if runtime == 0: runtime = default_timer() - # Direct callen da Prüfung in io.IOBase.reg_event ist + # Call directly since check is in io.IOBase.reg_event tup_fire[0].func(tup_fire[1], tup_fire[2]) self._imgwriter._eventq.task_done() - # Laufzeitprüfung + # Runtime check if runtime != -1 and default_timer() - runtime > self._imgwriter._refresh: runtime = -1 warnings.warn( @@ -1186,28 +1179,28 @@ class RevPiModIO(object): e = ex break - # Mainloop verlassen + # Leave mainloop self._imgwriter._collect_events(False) self._looprunning = False self._th_mainloop = None - # Auf Fehler prüfen die im loop geworfen wurden + # Check for errors that were thrown in the loop if e is not None: self.exit(full=False) self.__exit_jobs() raise e - # Exitstrategie ausführen + # Execute exit strategy self.__exit_jobs() def readprocimg(self, device=None) -> bool: """ - Einlesen aller Inputs aller/eines Devices vom Prozessabbild. + Read all inputs of all/one device from the process image. - Devices mit aktiverem autorefresh werden ausgenommen! + Devices with active autorefresh are excluded! - :param device: nur auf einzelnes Device anwenden - :return: True, wenn Arbeiten an allen Devices erfolgreich waren + :param device: Only apply to single device + :return: True if work on all devices was successful """ if device is None: mylist = self.device @@ -1225,7 +1218,7 @@ class RevPiModIO(object): ) mylist = [dev] - # Daten komplett einlesen + # Read data completely self._myfh_lck.acquire() try: self._myfh.seek(0) @@ -1238,14 +1231,14 @@ class RevPiModIO(object): for dev in mylist: if not dev._selfupdate: - # FileHandler sperren + # Lock file handler dev._filelock.acquire() if self._monitoring or dev._shared_procimg: - # Alles vom Bus einlesen + # Read everything from the bus dev._ba_devdata[:] = bytesbuff[dev._slc_devoff] else: - # Inputs vom Bus einlesen + # Read inputs from the bus dev._ba_devdata[dev._slc_inp] = bytesbuff[dev._slc_inpoff] dev._filelock.release() @@ -1253,14 +1246,14 @@ class RevPiModIO(object): return True def resetioerrors(self) -> None: - """Setzt aktuellen IOError-Zaehler auf 0 zurueck.""" + """Resets current IOError counter to 0.""" self._ioerror = 0 def setdefaultvalues(self, device=None) -> None: """ - Alle Outputbuffer werden auf die piCtory default Werte gesetzt. + All output buffers are set to the piCtory default values. - :param device: nur auf einzelnes Device anwenden + :param device: Only apply to single device """ if self._monitoring: raise RuntimeError("can not set default values, while system is in monitoring mode") @@ -1281,12 +1274,12 @@ class RevPiModIO(object): def syncoutputs(self, device=None) -> bool: """ - Lesen aller aktuell gesetzten Outputs im Prozessabbild. + Read all currently set outputs in the process image. - Devices mit aktiverem autorefresh werden ausgenommen! + Devices with active autorefresh are excluded! - :param device: nur auf einzelnes Device anwenden - :return: True, wenn Arbeiten an allen Devices erfolgreich waren + :param device: Only apply to single device + :return: True if work on all devices was successful """ if device is None: mylist = self.device @@ -1324,12 +1317,12 @@ class RevPiModIO(object): def writeprocimg(self, device=None) -> bool: """ - Schreiben aller Outputs aller Devices ins Prozessabbild. + Write all outputs of all devices to the process image. - Devices mit aktiverem autorefresh werden ausgenommen! + Devices with active autorefresh are excluded! - :param device: nur auf einzelnes Device anwenden - :return: True, wenn Arbeiten an allen Devices erfolgreich waren + :param device: Only apply to single device + :return: True if work on all devices was successful """ if self._monitoring: raise RuntimeError("can not write process image, while system is in monitoring mode") @@ -1364,7 +1357,7 @@ class RevPiModIO(object): global_ex = IOError("error on shared procimg while write") dev._shared_write.clear() else: - # Outpus auf Bus schreiben + # Write outputs to bus self._myfh_lck.acquire() try: self._myfh.seek(dev._slc_outoff.start) @@ -1402,12 +1395,12 @@ class RevPiModIO(object): class RevPiModIOSelected(RevPiModIO): """ - Klasse fuer die Verwaltung einzelner Devices aus piCtory. + Class for managing individual devices from piCtory. - Diese Klasse uebernimmt nur angegebene Devices der piCtory Konfiguration - und bildet 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. + This class only takes over specified devices from the piCtory configuration + and maps them including IOs. It takes over exclusive management of the + address range in the process image where the specified devices are located + and ensures that the data is synchronized. """ __slots__ = () @@ -1426,13 +1419,12 @@ class RevPiModIOSelected(RevPiModIO): shared_procimg=False, ): """ - Instantiiert nur fuer angegebene Devices die Grundfunktionen. + Instantiates the basic functions only for specified devices. - Der Parameter deviceselection kann eine einzelne - Device Position / einzelner Device Name sein oder eine Liste mit - mehreren Positionen / Namen + The deviceselection parameter can be a single device position / + single device name or a list with multiple positions / names. - :param deviceselection: Positionsnummer oder Devicename + :param deviceselection: Position number or device name :ref: :func:`RevPiModIO.__init__(...)` """ super().__init__( @@ -1480,12 +1472,11 @@ class RevPiModIOSelected(RevPiModIO): class RevPiModIODriver(RevPiModIOSelected): """ - Klasse um eigene Treiber fuer die virtuellen Devices zu erstellen. + Class to create custom drivers for virtual devices. - 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. + With this class, only specified virtual devices are managed with RevPiModIO. + During instantiation, inputs and outputs are automatically swapped to allow + writing of inputs. The data can then be retrieved from the devices via logiCAD. """ __slots__ = () @@ -1502,15 +1493,15 @@ class RevPiModIODriver(RevPiModIOSelected): shared_procimg=False, ): """ - Instantiiert die Grundfunktionen. + Instantiates the basic functions. - Parameter 'monitoring' und 'simulator' stehen hier nicht zur - Verfuegung, da diese automatisch gesetzt werden. + Parameters 'monitoring' and 'simulator' are not available here + as they are set automatically. - :param virtdev: Virtuelles Device oder mehrere als + :param virtdev: Virtual device or multiple as :ref: :func:`RevPiModIO.__init__()` """ - # Parent mit monitoring=False und simulator=True laden + # Load parent with monitoring=False and simulator=True if type(virtdev) not in (list, tuple): virtdev = (virtdev,) dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev) diff --git a/src/revpimodio2/netio.py b/src/revpimodio2/netio.py index 69ccec1..2d2d9a5 100644 --- a/src/revpimodio2/netio.py +++ b/src/revpimodio2/netio.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""RevPiModIO Hauptklasse fuer Netzwerkzugriff.""" +"""RevPiModIO main class for network access.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" @@ -17,16 +17,16 @@ from .errors import DeviceNotFoundError from .modio import DevSelect, RevPiModIO as _RevPiModIO from .pictory import DeviceType -# Synchronisierungsbefehl +# Synchronization command _syssync = b"\x01\x06\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" # Disconnectbefehl _sysexit = b"\x01EX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" -# DirtyBytes von Server entfernen -_sysdeldirty = b"\x01EY\x00\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x17" -# piCtory Konfiguration laden +# Remove DirtyBytes from server +_sysdeldirty = b"\x01EY\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x17" +# Load piCtory configuration _syspictory = b"\x01PI\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" _syspictoryh = b"\x01PH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" -# ReplaceIO Konfiguration laden +# Load ReplaceIO configuration _sysreplaceio = b"\x01RP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" _sysreplaceioh = b"\x01RH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17" # Hashvalues @@ -37,24 +37,23 @@ HEADER_STOP = b"\x17" class AclException(Exception): - """Probleme mit Berechtigungen.""" + """Problems with permissions.""" pass class ConfigChanged(Exception): - """Aenderung der piCtory oder replace_ios Datei.""" + """Change to the piCtory or replace_ios file.""" pass class NetFH(Thread): """ - Netzwerk File Handler fuer das Prozessabbild. + Network file handler for the process image. - Dieses FileObject-like Object verwaltet das Lesen und Schriben des - Prozessabbilds ueber das Netzwerk. Ein entfernter Revolution Pi kann - so gesteuert werden. + This file-object-like object manages reading and writing of the + process image via the network. A remote Revolution Pi can be controlled this way. """ __slots__ = ( @@ -84,9 +83,9 @@ class NetFH(Thread): """ Init NetFH-class. - :param address: IP Adresse, Port des RevPi als - :param check_replace_ios: Prueft auf Veraenderungen der Datei - :param timeout: Timeout in Millisekunden der Verbindung + :param address: IP address, port of the RevPi as + :param check_replace_ios: Checks for changes to the file + :param timeout: Timeout in milliseconds for the connection """ super().__init__() self.daemon = True @@ -109,38 +108,38 @@ class NetFH(Thread): self._address = address self._serversock = None # type: socket.socket - # Parameterprüfung + # Parameter validation if not isinstance(address, tuple): raise TypeError("parameter address must be ('IP', PORT)") if not isinstance(timeout, int): raise TypeError("parameter timeout must be ") - # Verbindung herstellen + # Establish connection self.__set_systimeout(timeout) self._connect() if self._serversock is None: raise FileNotFoundError("can not connect to revpi server") - # NetFH konfigurieren + # Configure NetFH self.__position = 0 self.start() def __del__(self): - """NetworkFileHandler beenden.""" + """Terminate NetworkFileHandler.""" self.close() def __check_acl(self, bytecode: bytes) -> None: """ - Pueft ob ACL auf RevPi den Vorgang erlaubt. + Checks if ACL allows the operation on RevPi. - Ist der Vorgang nicht zulässig, wird der Socket sofort geschlossen - und eine Exception geworfen. + If the operation is not permitted, the socket is immediately closed + and an exception is thrown. - :param bytecode: Antwort, die geprueft werden solll + :param bytecode: Response to be checked """ if bytecode == b"\x18": - # Alles beenden, wenn nicht erlaubt + # Terminate everything if not permitted self.__sockend.set() self.__sockerr.set() self._serversock.close() @@ -152,39 +151,39 @@ class NetFH(Thread): def __set_systimeout(self, value: int) -> None: """ - Systemfunktion fuer Timeoutberechnung. + System function for timeout calculation. - :param value: Timeout in Millisekunden 100 - 60000 + :param value: Timeout in milliseconds 100 - 60000 """ if isinstance(value, int) and (100 <= value <= 60000): self.__timeout = value / 1000 - # Timeouts in Socket setzen + # Set timeouts in socket if self._serversock is not None: self._serversock.settimeout(self.__timeout) - # 45 Prozent vom Timeout für Synctimer verwenden + # Use 45 percent of timeout for sync timer self.__waitsync = self.__timeout / 100 * 45 else: raise ValueError("value must between 10 and 60000 milliseconds") def _connect(self) -> None: - """Stellt die Verbindung zu einem RevPiPlcServer her.""" - # Neuen Socket aufbauen + """Establishes the connection to a RevPiPlcServer.""" + # Build new socket so = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: so.connect(self._address) so.settimeout(self.__timeout) - # Hashwerte anfordern + # Request hash values recv_len = 16 so.sendall(_syspictoryh) if self.__check_replace_ios: so.sendall(_sysreplaceioh) recv_len += 16 - # Hashwerte empfangen mit eigenen Puffern, da nicht gelocked + # Receive hash values with own buffers, as not locked buff_recv = bytearray(recv_len) while recv_len > 0: block = so.recv(recv_len) @@ -193,7 +192,7 @@ class NetFH(Thread): buff_recv += block recv_len -= len(block) - # Änderung an piCtory prüfen + # Check for changes to piCtory if self.__pictory_h and buff_recv[:16] != self.__pictory_h: self.__config_changed = True self.close() @@ -201,7 +200,7 @@ class NetFH(Thread): else: self.__pictory_h = buff_recv[:16] - # Änderung an replace_ios prüfen + # Check for changes to replace_ios if ( self.__check_replace_ios and self.__replace_ios_h @@ -218,7 +217,7 @@ class NetFH(Thread): except Exception: so.close() else: - # Alten Socket trennen + # Disconnect old socket with self.__socklock: if self._serversock is not None: self._serversock.close() @@ -226,10 +225,10 @@ class NetFH(Thread): self._serversock = so self.__sockerr.clear() - # Timeout setzen + # Set timeout self.set_timeout(int(self.__timeout * 1000)) - # DirtyBytes übertragen + # Transfer dirty bytes for pos in self.__dictdirty: self.set_dirtybytes(pos, self.__dictdirty[pos]) @@ -285,19 +284,19 @@ class NetFH(Thread): def clear_dirtybytes(self, position=None) -> None: """ - Entfernt die konfigurierten Dirtybytes vom RevPi Server. + Removes the configured dirty bytes from the RevPi server. - Diese Funktion wirft keine Exception bei einem uebertragungsfehler, - veranlasst aber eine Neuverbindung. + This function does not throw an exception on transmission error, + but triggers a reconnection. - :param position: Startposition der Dirtybytes + :param position: Start position of the dirty bytes """ if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") if self.__sockend.is_set(): raise ValueError("I/O operation on closed file") - # Daten immer übernehmen + # Always accept data if position is None: self.__dictdirty.clear() elif position in self.__dictdirty: @@ -305,10 +304,10 @@ class NetFH(Thread): try: if position is None: - # Alle Dirtybytes löschen + # Clear all dirty bytes buff = self._direct_sr(_sysdeldirty, 1) else: - # Nur bestimmte Dirtybytes löschen + # Only clear specific dirty bytes # b CM ii xx c0000000 b = 16 buff = self._direct_sr( pack( @@ -322,7 +321,7 @@ class NetFH(Thread): 1, ) if buff != b"\x1e": - # ACL prüfen und ggf Fehler werfen + # Check ACL and throw error if necessary self.__check_acl(buff) raise IOError("clear dirtybytes error on network") @@ -333,14 +332,14 @@ class NetFH(Thread): self.__sockerr.set() def close(self) -> None: - """Verbindung trennen.""" + """Disconnect connection.""" if self.__sockend.is_set(): return self.__sockend.set() self.__sockerr.set() - # Vom Socket sauber trennen + # Cleanly disconnect from socket if self._serversock is not None: try: self.__socklock.acquire() @@ -354,7 +353,7 @@ class NetFH(Thread): self._serversock.close() def flush(self) -> None: - """Schreibpuffer senden.""" + """Send write buffer.""" if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") if self.__sockend.is_set(): @@ -380,12 +379,12 @@ class NetFH(Thread): except Exception: raise finally: - # Puffer immer leeren + # Always clear buffer self.__int_buff = 0 self.__by_buff.clear() if buff != b"\x1e": - # ACL prüfen und ggf Fehler werfen + # Check ACL and throw error if necessary self.__check_acl(buff) self.__sockerr.set() @@ -393,23 +392,23 @@ class NetFH(Thread): def get_closed(self) -> bool: """ - Pruefen ob Verbindung geschlossen ist. + Check if connection is closed. - :return: True, wenn Verbindung geschlossen ist + :return: True if connection is closed """ return self.__sockend.is_set() def get_config_changed(self) -> bool: """ - Pruefen ob RevPi Konfiguration geaendert wurde. + Check if RevPi configuration was changed. - :return: True, wenn RevPi Konfiguration geaendert ist + :return: True if RevPi configuration was changed """ return self.__config_changed def get_name(self) -> str: """ - Verbindugnsnamen zurueckgeben. + Return connection name. :return: IP:PORT """ @@ -417,23 +416,23 @@ class NetFH(Thread): def get_reconnecting(self) -> bool: """ - Interner reconnect aktiv wegen Netzwerkfehlern. + Internal reconnect active due to network errors. - :return: True, wenn reconnect aktiv + :return: True if reconnect is active """ return self.__sockerr.is_set() def get_timeout(self) -> int: """ - Gibt aktuellen Timeout zurueck. + Returns current timeout. - :return: in Millisekunden + :return: in milliseconds """ return int(self.__timeout * 1000) def ioctl(self, request: int, arg=b"") -> None: """ - IOCTL Befehle ueber das Netzwerk senden. + Send IOCTL commands via the network. :param request: Request as :param arg: Argument as @@ -451,7 +450,7 @@ class NetFH(Thread): pack("=c2s2xHI4xc", HEADER_START, b"IC", len(arg), request, HEADER_STOP) + arg, 1 ) if buff != b"\x1e": - # ACL prüfen und ggf Fehler werfen + # Check ACL and throw error if necessary self.__check_acl(buff) self.__sockerr.set() @@ -459,10 +458,10 @@ class NetFH(Thread): def read(self, length: int) -> bytes: """ - Daten ueber das Netzwerk lesen. + Read data via the network. - :param length: Anzahl der Bytes - :return: Gelesene + :param length: Number of bytes + :return: Read """ if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") @@ -501,9 +500,9 @@ class NetFH(Thread): def readpictory(self) -> bytes: """ - Ruft die piCtory Konfiguration ab. + Retrieves the piCtory configuration. - :return: piCtory Datei + :return: piCtory file """ if self.__sockend.is_set(): raise ValueError("read of closed file") @@ -517,7 +516,7 @@ class NetFH(Thread): def readreplaceio(self) -> bytes: """ - Ruft die replace_io Konfiguration ab. + Retrieves the replace_io configuration. :return: replace_io_file """ @@ -532,24 +531,24 @@ class NetFH(Thread): return self._direct_sr(b"", recv_length) def run(self) -> None: - """Handler fuer Synchronisierung.""" + """Handler for synchronization.""" state_reconnect = False while not self.__sockend.is_set(): - # Bei Fehlermeldung neu verbinden + # Reconnect on error message if self.__sockerr.is_set(): if not state_reconnect: state_reconnect = True warnings.warn("got a network error and try to reconnect", RuntimeWarning) self._connect() if self.__sockerr.is_set(): - # Verhindert beim Scheitern 100% CPU last + # Prevents 100% CPU load on failure self.__sockend.wait(self.__waitsync) continue else: state_reconnect = False warnings.warn("successfully reconnected after network error", RuntimeWarning) - # Kein Fehler aufgetreten, sync durchführen wenn socket frei + # No error occurred, perform sync if socket is free if self.__socklock.acquire(blocking=False): try: self._serversock.sendall(_syssync) @@ -573,12 +572,12 @@ class NetFH(Thread): finally: self.__socklock.release() - # Warten nach Sync damit Instantiierung funktioniert + # Wait after sync so instantiation works self.__sockerr.wait(self.__waitsync) def seek(self, position: int) -> None: - """Springt an angegebene Position. - @param position An diese Position springen""" + """Jump to specified position. + @param position Jump to this position""" if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") if self.__sockend.is_set(): @@ -587,20 +586,20 @@ class NetFH(Thread): def set_dirtybytes(self, position: int, dirtybytes: bytes) -> None: """ - Konfiguriert Dirtybytes fuer Prozessabbild bei Verbindungsfehler. + Configures dirty bytes for process image on connection error. - Diese Funktion wirft keine Exception bei einem uebertragungsfehler, - veranlasst aber eine Neuverbindung. + This function does not throw an exception on transmission error, + but triggers a reconnection. - :param position: Startposition zum Schreiben - :param dirtybytes: die geschrieben werden sollen + :param position: Start position for writing + :param dirtybytes: to be written """ if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") if self.__sockend.is_set(): raise ValueError("I/O operation on closed file") - # Daten immer übernehmen + # Always accept data self.__dictdirty[position] = dirtybytes try: @@ -612,7 +611,7 @@ class NetFH(Thread): ) if buff != b"\x1e": - # ACL prüfen und ggf Fehler werfen + # Check ACL and throw error if necessary self.__check_acl(buff) raise IOError("set dirtybytes error on network") @@ -625,14 +624,14 @@ class NetFH(Thread): def set_timeout(self, value: int) -> None: """ - Setzt Timeoutwert fuer Verbindung. + Sets timeout value for connection. - :param value: Timeout in Millisekunden + :param value: Timeout in milliseconds """ if self.__sockend.is_set(): raise ValueError("I/O operation on closed file") - # Timeoutwert verarbeiten (könnte Exception auslösen) + # Process timeout value (could throw exception) self.__set_systimeout(value) try: @@ -645,7 +644,7 @@ class NetFH(Thread): def tell(self) -> int: """ - Gibt aktuelle Position zurueck. + Returns aktuelle Position. :return: Aktuelle Position """ @@ -657,10 +656,10 @@ class NetFH(Thread): def write(self, bytebuff: bytes) -> int: """ - Daten ueber das Netzwerk schreiben. + Write data via the network. - :param bytebuff: Bytes zum schreiben - :return: Anzahl geschriebener bytes + :param bytebuff: Bytes to write + :return: Number of written bytes """ if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") @@ -672,14 +671,14 @@ class NetFH(Thread): with self.__socklock: self.__int_buff += 1 - # Datenblock mit Position und Länge in Puffer ablegen + # Store data block with position and length in buffer self.__by_buff += ( self.__position.to_bytes(length=2, byteorder="little") + len(bytebuff).to_bytes(length=2, byteorder="little") + bytebuff ) - # TODO: Bufferlänge und dann flushen? + # TODO: Bufferlänge and dann flushen? return len(bytebuff) @@ -692,14 +691,13 @@ class NetFH(Thread): class RevPiNetIO(_RevPiModIO): """ - Klasse fuer die Verwaltung der piCtory Konfiguration ueber das Netzwerk. + Class for managing the piCtory configuration via the network. - 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. + This class takes over the entire configuration from piCtory and maps + the devices and IOs. It takes over exclusive management of the + process image and ensures that the data is synchronized. + If only individual devices should be controlled, use + RevPiNetIOSelected() and pass a list with device positions or device names during instantiation. """ __slots__ = "_address" @@ -716,26 +714,26 @@ class RevPiNetIO(_RevPiModIO): shared_procimg=False, ): """ - Instantiiert die Grundfunktionen. + Instantiates the basic functions. - :param address: IP-Adresse / (IP, Port) - :param autorefresh: Wenn True, alle Devices zu autorefresh hinzufuegen - :param monitoring: In- und Outputs werden gelesen, niemals geschrieben - :param syncoutputs: Aktuell gesetzte Outputs vom Prozessabbild einlesen - :param simulator: Laedt das Modul als Simulator und vertauscht IOs - :param debug: Gibt bei allen Fehlern komplette Meldungen aus - :param replace_io_file: Replace IO Konfiguration aus Datei laden - :param shared_procimg: Share process image with other processes, this - could be insecure for automation + :param address: IP address / (IP, Port) + :param autorefresh: If True, add all devices to autorefresh + :param monitoring: Inputs and outputs are read, never written + :param syncoutputs: Read currently set outputs from process image + :param simulator: Loads the module as simulator and swaps IOs + :param debug: Output complete messages for all errors + :param replace_io_file: Load replace IO configuration from file + :param shared_procimg: Share process image with other processes, this + could be insecure for automation """ check_ip = compile(r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$") - # Adresse verarbeiten + # Process address if isinstance(address, str): self._address = (address, 55234) elif isinstance(address, tuple): if len(address) == 2 and isinstance(address[0], str) and isinstance(address[1], int): - # Werte prüfen + # Check values if not 0 < address[1] <= 65535: raise ValueError("port number out of range 1 - 65535") @@ -748,7 +746,7 @@ class RevPiNetIO(_RevPiModIO): "like (, )" ) - # IP-Adresse prüfen und ggf. auflösen + # Check IP address and resolve if necessary if check_ip.match(self._address[0]) is None: try: ipv4 = socket.gethostbyname(self._address[0]) @@ -772,16 +770,16 @@ class RevPiNetIO(_RevPiModIO): ) self._set_device_based_cycle_time = False - # Netzwerkfilehandler anlegen + # Create network file handler self._myfh = self._create_myfh() - # Nur Konfigurieren, wenn nicht vererbt + # Only configure if not inherited if type(self) == RevPiNetIO: self._configure(self.get_jconfigrsc()) def _create_myfh(self): """ - Erstellt NetworkFileObject. + Creates NetworkFileObject. :return: FileObject """ @@ -790,15 +788,15 @@ class RevPiNetIO(_RevPiModIO): def _get_cpreplaceio(self) -> ConfigParser: """ - Laed die replace_io Konfiguration ueber das Netzwerk. + Loads the replace_io configuration via the network. - :return: der replace io daten + :return: of the replace io data """ - # Normale Verwendung über Elternklasse erledigen + # Handle normal usage via parent class if self._replace_io_file != ":network:": return super()._get_cpreplaceio() - # Replace IO Daten über das Netzwerk beziehen + # Obtain replace IO data via the network byte_buff = self._myfh.readreplaceio() cp = ConfigParser() @@ -809,12 +807,12 @@ class RevPiNetIO(_RevPiModIO): return cp def disconnect(self) -> None: - """Trennt Verbindungen und beendet autorefresh inkl. alle Threads.""" + """Disconnects connections and terminates autorefresh including all threads.""" self.cleanup() def exit(self, full=True) -> None: """ - Beendet mainloop() und optional autorefresh. + Terminates mainloop() and optionally autorefresh. :ref: :func:`RevPiModIO.exit()` """ @@ -825,20 +823,20 @@ class RevPiNetIO(_RevPiModIO): def get_config_changed(self) -> bool: """ - Pruefen ob RevPi Konfiguration geaendert wurde. + Check if RevPi configuration was changed. - In diesem Fall ist die Verbindung geschlossen und RevPiNetIO muss - neu instanziert werden. + In this case, the connection is closed and RevPiNetIO must be + reinstantiated. - :return: True, wenn RevPi Konfiguration geaendert ist + :return: True if RevPi configuration was changed """ return self._myfh.config_changed def get_jconfigrsc(self) -> dict: """ - Laedt die piCotry Konfiguration und erstellt ein . + Loads the piCtory configuration and creates a . - :return: der piCtory Konfiguration + :return: of the piCtory configuration """ mynh = NetFH(self._address, False) byte_buff = mynh.readpictory() @@ -847,20 +845,20 @@ class RevPiNetIO(_RevPiModIO): def get_reconnecting(self) -> bool: """ - Interner reconnect aktiv wegen Netzwerkfehlern. + Internal reconnect active due to network errors. - Das Modul versucht intern die Verbindung neu herzustellen. Es ist - kein weiteres Zutun noetig. + The module tries internally to reestablish the connection. No + further action is needed. - :return: True, wenn reconnect aktiv + :return: True if reconnect is active """ return self._myfh.reconnecting def net_cleardefaultvalues(self, device=None) -> None: """ - Loescht Defaultwerte vom PLC Server. + Clears default values from the PLC server. - :param device: nur auf einzelnes Device anwenden, sonst auf Alle + :param device: Only apply to single device, otherwise to all """ if self.monitoring: raise RuntimeError("can not send default values, while system is in monitoring mode") @@ -876,12 +874,12 @@ class RevPiNetIO(_RevPiModIO): def net_setdefaultvalues(self, device=None) -> None: """ - Konfiguriert den PLC Server mit den piCtory Defaultwerten. + Configures the PLC server with the piCtory default values. - Diese Werte werden auf dem RevPi gesetzt, wenn die Verbindung - unerwartet (Netzwerkfehler) unterbrochen wird. + These values are set on the RevPi if the connection is + unexpectedly interrupted (network error). - :param device: nur auf einzelnes Device anwenden, sonst auf Alle + :param device: Only apply to single device, otherwise to all """ if self.monitoring: raise RuntimeError("can not send default values, while system is in monitoring mode") @@ -898,25 +896,25 @@ class RevPiNetIO(_RevPiModIO): listlen = len(lst_io) if listlen == 1: - # Byteorientierte Outputs direkt übernehmen + # Take byte-oriented outputs directly dirtybytes += lst_io[0]._defaultvalue elif listlen > 1: - # Bitorientierte Outputs in ein Byte zusammenfassen + # Combine bit-oriented outputs into one byte int_byte = 0 lstbyte = lst_io.copy() lstbyte.reverse() for bitio in lstbyte: - # Von hinten die bits nach vorne schieben + # Shift the bits from back to front int_byte <<= 1 if bitio is not None: int_byte += 1 if bitio._defaultvalue else 0 - # Errechneten Int-Wert in ein Byte umwandeln + # Convert calculated int value to a byte dirtybytes += int_byte.to_bytes(length=1, byteorder="little") - # Dirtybytes an PLC Server senden + # Send dirtybytes to PLC server self._myfh.set_dirtybytes(dev._offset + dev._slc_out.start, dirtybytes) config_changed = property(get_config_changed) @@ -925,12 +923,12 @@ class RevPiNetIO(_RevPiModIO): class RevPiNetIOSelected(RevPiNetIO): """ - Klasse fuer die Verwaltung einzelner Devices aus piCtory. + Class for managing individual devices from piCtory. - Diese Klasse uebernimmt nur angegebene Devices der piCtory Konfiguration - und bildet 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. + This class only takes over specified devices from the piCtory configuration + and maps them including IOs. It takes over exclusive management of the + address range in the process image where the specified devices are located + and ensures that the data is synchronized. """ __slots__ = () @@ -948,15 +946,15 @@ class RevPiNetIOSelected(RevPiNetIO): shared_procimg=False, ): """ - Instantiiert nur fuer angegebene Devices die Grundfunktionen. + Instantiates the basic functions only for specified devices. - Der Parameter deviceselection kann eine einzelne - Device Position / einzelner Device Name sein oder eine Liste mit - mehreren Positionen / Namen + The parameter deviceselection can be a single + device position / single device name or a list with + multiple positions / names - :param address: IP-Adresse / (IP, Port) - :param deviceselection: Positionsnummer oder Devicename - :ref: :func:`RevPiNetIO.__init__()` + :param address: IP address / (IP, Port) + :param deviceselection: Position number or device name + :ref: :func:`RevPiNetIO.__init__()` """ super().__init__( address, @@ -1002,12 +1000,11 @@ class RevPiNetIOSelected(RevPiNetIO): class RevPiNetIODriver(RevPiNetIOSelected): """ - Klasse um eigene Treiber fuer die virtuellen Devices zu erstellen. + Class to create custom drivers for virtual devices. - 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. + With this class, only specified virtual devices are managed with RevPiModIO. + During instantiation, inputs and outputs are automatically swapped to allow + writing of inputs. The data can then be retrieved from the devices via logiCAD. """ __slots__ = () @@ -1023,16 +1020,16 @@ class RevPiNetIODriver(RevPiNetIOSelected): shared_procimg=False, ): """ - Instantiiert die Grundfunktionen. + Instantiates the basic functions. - Parameter 'monitoring' und 'simulator' stehen hier nicht zur - Verfuegung, da diese automatisch gesetzt werden. + Parameters 'monitoring' and 'simulator' are not available here, + as these are set automatically. - :param address: IP-Adresse / (IP, Port) - :param virtdev: Virtuelles Device oder mehrere als - :ref: :func:`RevPiModIO.__init__()` + :param address: IP address / (IP, Port) + :param virtdev: Virtual device or multiple as + :ref: :func:`RevPiModIO.__init__()` """ - # Parent mit monitoring=False und simulator=True laden + # Load parent with monitoring=False and simulator=True if type(virtdev) not in (list, tuple): virtdev = (virtdev,) dev_select = DevSelect(DeviceType.VIRTUAL, "", virtdev) @@ -1062,7 +1059,7 @@ def run_net_plc(address, func, cycletime=50, replace_io_file=None, debug=True): rpi.handlesignalend() return rpi.cycleloop(func, cycletime) - :param address: IP-Adresse / (IP, Port) + :param address: IP address / (IP, Port) :param func: Function to run every set milliseconds :param cycletime: Cycle time in milliseconds :param replace_io_file: Load replace IO configuration from file diff --git a/src/revpimodio2/summary.py b/src/revpimodio2/summary.py index 7e7fafa..bf9361b 100644 --- a/src/revpimodio2/summary.py +++ b/src/revpimodio2/summary.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- -"""Bildet die Summary-Sektion von piCtory ab.""" +"""Maps the Summary section from piCtory.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" class Summary: - """Bildet die Summary-Sektion der config.rsc ab.""" + """Maps the Summary section of config.rsc.""" __slots__ = "inptotal", "outtotal" def __init__(self, summary: dict): """ - Instantiiert die RevPiSummary-Klasse. + Instantiates the RevPiSummary class. :param summary: piCtory Summaryinformationen """ diff --git a/tests/common/test_devices.py b/tests/common/test_devices.py index 166351a..be9a2fa 100644 --- a/tests/common/test_devices.py +++ b/tests/common/test_devices.py @@ -96,7 +96,7 @@ class TestDevicesModule(TestRevPiModIO): self.assertEqual(33 in rpi.device.virt01, False) self.assertEqual(552 in rpi.device.virt01, True) - # Löschen + # Delete del rpi.device.virt01 with self.assertRaises(AttributeError): rpi.device.virt01 diff --git a/tests/common/test_init_modio.py b/tests/common/test_init_modio.py index 0dffa5d..e5b7b19 100644 --- a/tests/common/test_init_modio.py +++ b/tests/common/test_init_modio.py @@ -93,10 +93,10 @@ class TestInitModio(TestRevPiModIO): self.assertEqual(2, len(rpi.device)) del rpi with self.assertRaises(revpimodio2.errors.DeviceNotFoundError): - # Liste mit einem ungültigen Device als + # List with an invalid device as rpi = revpimodio2.RevPiModIOSelected([32, 10], **defaultkwargs) with self.assertRaises(revpimodio2.errors.DeviceNotFoundError): - # Ungültiges Device als + # Invalid device as rpi = revpimodio2.RevPiModIOSelected(100, **defaultkwargs) with self.assertRaises(ValueError): # Ungültiger Devicetype @@ -116,10 +116,10 @@ class TestInitModio(TestRevPiModIO): # RevPiModIODriver with self.assertRaises(revpimodio2.errors.DeviceNotFoundError): - # Liste mit einem ungültigen Device als + # List with an invalid device as rpi = revpimodio2.RevPiModIODriver([64, 100], **defaultkwargs) with self.assertRaises(revpimodio2.errors.DeviceNotFoundError): - # Ungültiges Device als + # Invalid device as rpi = revpimodio2.RevPiModIODriver([100], **defaultkwargs) with self.assertRaises(ValueError): # Ungültiger Devicetype @@ -132,7 +132,7 @@ class TestInitModio(TestRevPiModIO): self.assertEqual(1, len(rpi.device)) del rpi - # Core ios als bits + # Core ios as bits rpi = self.modio(configrsc="config_core_bits.json") del rpi diff --git a/tests/common/test_modio_class_basics.py b/tests/common/test_modio_class_basics.py index 606d2fa..01189f9 100644 --- a/tests/common/test_modio_class_basics.py +++ b/tests/common/test_modio_class_basics.py @@ -27,7 +27,7 @@ class TestModioClassBasics(TestRevPiModIO): self.assertEqual(rpi.app.savets.tm_hour, 12) del rpi - # Alte config ohne saveTS + # Old config without saveTS with self.assertWarnsRegex(Warning, r"equal device name '.*' in pictory configuration."): rpi = self.modio(configrsc="config_old.rsc") self.assertIsNone(rpi.app.savets) @@ -79,7 +79,7 @@ class TestModioClassBasics(TestRevPiModIO): """Test interaction with process image.""" rpi = self.modio() - # Procimg IO alle + # Procimg IO all self.assertIsNone(rpi.setdefaultvalues()) self.assertEqual(rpi.writeprocimg(), True) self.assertEqual(rpi.syncoutputs(), True) diff --git a/tests/compact/test_compact.py b/tests/compact/test_compact.py index c2d71cf..a877557 100644 --- a/tests/compact/test_compact.py +++ b/tests/compact/test_compact.py @@ -19,7 +19,7 @@ class TestCompact(TestRevPiModIO): self.assertIsInstance(rpi.core, revpimodio2.device.Compact) - # COMPACT LEDs prüfen + # Check COMPACT LEDs self.assertEqual(rpi.io.RevPiLED.get_value(), b"\x00") rpi.core.A1 = revpimodio2.OFF self.assertEqual(rpi.core.A1, 0) @@ -44,12 +44,12 @@ class TestCompact(TestRevPiModIO): with self.assertRaises(ValueError): rpi.core.A2 = 5 - # Spezielle Werte aufrufen + # Call special values self.assertIsInstance(rpi.core.temperature, int) self.assertIsInstance(rpi.core.frequency, int) rpi.core.wd_toggle() - # Directzuweisung nicht erlaubt + # Direct assignment not allowed with self.assertRaisesRegex(AttributeError, r"direct assignment is not supported"): rpi.core.a1green = True diff --git a/tests/flat/test_flat.py b/tests/flat/test_flat.py index b8c938b..32cc926 100644 --- a/tests/flat/test_flat.py +++ b/tests/flat/test_flat.py @@ -20,7 +20,7 @@ class TestFlat(TestRevPiModIO): self.assertIsInstance(rpi.core, revpimodio2.device.Flat) - # FLAT LEDs prüfen + # Check FLAT LEDs rpi.core.A1 = revpimodio2.OFF self.assertEqual(rpi.io.RevPiLED.get_value(), b"\x00\x00") self.assertEqual(rpi.core.A1, 0) @@ -77,12 +77,12 @@ class TestFlat(TestRevPiModIO): with self.assertRaises(ValueError): rpi.core.A5 = 5 - # Spezielle Werte aufrufen + # Call special values self.assertIsInstance(rpi.core.temperature, int) self.assertIsInstance(rpi.core.frequency, int) rpi.core.wd_toggle() - # Directzuweisung nicht erlaubt + # Direct assignment not allowed with self.assertRaisesRegex(AttributeError, r"direct assignment is not supported"): rpi.core.a1green = True diff --git a/tests/io_tests/test_ios.py b/tests/io_tests/test_ios.py index e4a0666..c8488b1 100644 --- a/tests/io_tests/test_ios.py +++ b/tests/io_tests/test_ios.py @@ -84,7 +84,7 @@ class TestIos(TestRevPiModIO): """Testet mehrbittige IOs.""" rpi = self.modio(configrsc="config_supervirt.rsc") - # Adressen und Längen prüfen + # Check addresses and lengths self.assertEqual(rpi.device[65]._offset, 75) self.assertEqual(rpi.io.InBit_1.length, 1) diff --git a/tests/revpi3/test_connect.py b/tests/revpi3/test_connect.py index 23beeec..6c381f5 100644 --- a/tests/revpi3/test_connect.py +++ b/tests/revpi3/test_connect.py @@ -48,7 +48,7 @@ class TestRevPiConnect(TestRevPiModIO): with self.assertRaises(ValueError): rpi.core.A3 = BLUE - # Direkte Zuweisung darf nicht funktionieren + # Direct assignment must not work with self.assertRaises(AttributeError): rpi.core.a3green = True with self.assertRaises(AttributeError): diff --git a/tests/revpi3/test_core.py b/tests/revpi3/test_core.py index fbda827..22360e4 100644 --- a/tests/revpi3/test_core.py +++ b/tests/revpi3/test_core.py @@ -97,7 +97,7 @@ class TestRevPiCore(TestRevPiModIO): with self.assertWarnsRegex(Warning, r"equal device name '.*' in pictory configuration."): rpi = self.modio(configrsc="config_old.rsc") - # Errorlimits testen, die es nicht gibt (damals None, jetzt -1) + # Test error limits that don't exist (formerly None, now -1) self.assertEqual(rpi.core.errorlimit1, -1) self.assertEqual(rpi.core.errorlimit2, -1) From b35edac6941667b4ef7e4fdbaab229655a434975 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 10:21:01 +0100 Subject: [PATCH 02/18] fix: correct typos and complete German translations - Fix remaining untranslated German text in param descriptions - Fix typo: 'ist' -> 'is' in error messages - Fix typo: 'fist' -> 'first' in comment - Fix typo: 'configuraiton' -> 'configuration' - Fix typo: 'thrading' -> 'threading' in docstring - Translate remaining German TODO comment Signed-off-by: Nicolai Buchwitz --- src/revpimodio2/app.py | 4 ++-- src/revpimodio2/io.py | 2 +- src/revpimodio2/modio.py | 8 ++++---- src/revpimodio2/netio.py | 2 +- src/revpimodio2/summary.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/revpimodio2/app.py b/src/revpimodio2/app.py index 54db85b..446e9c7 100644 --- a/src/revpimodio2/app.py +++ b/src/revpimodio2/app.py @@ -16,7 +16,7 @@ class App: """ Instantiates the App class. - :param app: piCtory Appinformationen + :param app: piCtory app information """ self.name = app.get("name", "") """Name of creating app""" @@ -28,7 +28,7 @@ class App: """Language of creating app""" self.savets = app.get("saveTS", None) - """Timestamp of configuraiton""" + """Timestamp of configuration""" if self.savets is not None: try: diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index 632271a..5849d83 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -858,7 +858,7 @@ class IOBase(object): specified milliseconds! It is always rounded up!) :param edge: Edge RISING, FALLING, BOTH that must occur - :param exitevent: for early termination + :param exitevent: for early termination :param okvalue: IO value at which waiting ends immediately :param timeout: Time in ms after which to abort :return: successful values <= 0 diff --git a/src/revpimodio2/modio.py b/src/revpimodio2/modio.py index a35a137..1fd9c71 100644 --- a/src/revpimodio2/modio.py +++ b/src/revpimodio2/modio.py @@ -853,7 +853,7 @@ class RevPiModIO(object): # Check if function is callable if not callable(func): - raise RuntimeError("registered function '{0}' ist not callable".format(func)) + raise RuntimeError("registered function '{0}' is not callable".format(func)) # Create thread if it should not block if not blocking: @@ -868,7 +868,7 @@ class RevPiModIO(object): # Take over cycle time old_cycletime = self._imgwriter.refresh if not cycletime == self._imgwriter.refresh: - # Set new cycle time and wait one imgwriter cycle to sync fist cycle + # Set new cycle time and wait one imgwriter cycle to sync first cycle self._imgwriter.refresh = cycletime self._imgwriter.newdata.clear() self._imgwriter.newdata.wait(self._imgwriter._refresh) @@ -961,7 +961,7 @@ class RevPiModIO(object): self._exit.set() self._waitexit.set() - # Auf beenden von mainloop thread warten + # Wait for mainloop thread to finish if self._th_mainloop is not None and self._th_mainloop.is_alive(): self._th_mainloop.join(2.5) @@ -1070,7 +1070,7 @@ class RevPiModIO(object): """ # Check if function is callable if not (cleanupfunc is None or callable(cleanupfunc)): - raise RuntimeError("registered function '{0}' ist not callable".format(cleanupfunc)) + raise RuntimeError("registered function '{0}' is not callable".format(cleanupfunc)) self.__cleanupfunc = cleanupfunc signal(SIGINT, self.__evt_exit) signal(SIGTERM, self.__evt_exit) diff --git a/src/revpimodio2/netio.py b/src/revpimodio2/netio.py index 2d2d9a5..56253ee 100644 --- a/src/revpimodio2/netio.py +++ b/src/revpimodio2/netio.py @@ -678,7 +678,7 @@ class NetFH(Thread): + bytebuff ) - # TODO: Bufferlänge and dann flushen? + # TODO: Buffer length and then flush? return len(bytebuff) diff --git a/src/revpimodio2/summary.py b/src/revpimodio2/summary.py index bf9361b..51e34da 100644 --- a/src/revpimodio2/summary.py +++ b/src/revpimodio2/summary.py @@ -14,7 +14,7 @@ class Summary: """ Instantiates the RevPiSummary class. - :param summary: piCtory Summaryinformationen + :param summary: piCtory summary information """ self.inptotal = summary.get("inpTotal", -1) self.outtotal = summary.get("outTotal", -1) From 2eac69b7bd2d198eba5b41ed1d9120472fe2ad8c Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 14:59:39 +0100 Subject: [PATCH 03/18] docs: add comprehensive documentation structure and API reference Created topic-based documentation: - basics.rst: core concepts and fundamental usage - cyclic_programming.rst: PLC-style programming with Cycletools - event_programming.rst: event-driven patterns and callbacks - advanced.rst: gateway IOs, replace_io_file, watchdog management - installation.rst and quickstart.rst: getting started guides Added complete API reference in docs/api/: - All device classes including ModularBaseConnect_4_5 and GatewayMixin - I/O, helper, and main class documentation Enhanced Sphinx configuration with RTD theme and improved autodoc settings. Removed auto-generated modules.rst and revpimodio2.rst. --- docs/advanced.rst | 711 ++++++++++++++++++++++++++++++++++++ docs/api/device.rst | 163 +++++++++ docs/api/helper.rst | 194 ++++++++++ docs/api/index.rst | 148 ++++++++ docs/api/io.rst | 212 +++++++++++ docs/api/revpimodio.rst | 141 +++++++ docs/basics.rst | 331 +++++++++++++++++ docs/conf.py | 58 ++- docs/cyclic_programming.rst | 604 ++++++++++++++++++++++++++++++ docs/event_programming.rst | 584 +++++++++++++++++++++++++++++ docs/index.rst | 85 ++++- docs/installation.rst | 85 +++++ docs/modules.rst | 7 - docs/quickstart.rst | 207 +++++++++++ docs/revpimodio2.rst | 85 ----- 15 files changed, 3513 insertions(+), 102 deletions(-) create mode 100644 docs/advanced.rst create mode 100644 docs/api/device.rst create mode 100644 docs/api/helper.rst create mode 100644 docs/api/index.rst create mode 100644 docs/api/io.rst create mode 100644 docs/api/revpimodio.rst create mode 100644 docs/basics.rst create mode 100644 docs/cyclic_programming.rst create mode 100644 docs/event_programming.rst create mode 100644 docs/installation.rst delete mode 100644 docs/modules.rst create mode 100644 docs/quickstart.rst delete mode 100644 docs/revpimodio2.rst diff --git a/docs/advanced.rst b/docs/advanced.rst new file mode 100644 index 0000000..89695fe --- /dev/null +++ b/docs/advanced.rst @@ -0,0 +1,711 @@ +======== +Advanced +======== + +Advanced features, patterns, and best practices for RevPiModIO. + +.. contents:: Contents + :local: + :depth: 2 + +Custom IOs (Gateway Modules) +============================= + +Gateway modules (ModbusTCP, Profinet, etc.) allow defining custom IOs dynamically. + +Understanding Gateway IOs +-------------------------- + +Gateway modules provide raw memory regions that you can map to custom IOs with specific data types and addresses. + +Defining Custom IOs +------------------- + +Use the :py:meth:`~revpimodio2.io.MemIO.replace_io` method to define custom IOs on gateway modules. + +Gateway modules provide generic IOs (like ``Input_1``, ``Output_1``, etc.) that you can replace with custom definitions: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Replace a gateway IO with custom definition + # Gateway IOs have default names like Input_1, Output_1, etc. + rpi.io.Input_1.replace_io( + "temperature", # New IO name + "h", # struct format: signed short + defaultvalue=0 # Default value + ) + + # Use the custom IO by its new name + temp = rpi.io.temperature.value / 10.0 # Scale to degrees + print(f"Temperature: {temp}°C") + + rpi.exit() + +**Parameters:** + +* ``name`` - Name for the new IO (will be accessible via ``rpi.io.name``) +* ``frm`` - Struct format character (see `format codes `_ below) +* ``defaultvalue`` - Optional: Default value for the IO +* ``byteorder`` - Optional: Byte order (``'little'`` or ``'big'``), default is ``'little'`` +* ``bit`` - Optional: Bit position for boolean IOs (0-7) +* ``event`` - Optional: Register event callback on creation +* ``delay`` - Optional: Event debounce delay in milliseconds +* ``edge`` - Optional: Event edge trigger (RISING, FALLING, or BOTH) + +**Note:** The memory address is inherited from the IO being replaced (e.g., ``Input_1``). The new IO uses the same address in the process image. + +Struct Format Codes +------------------- + +Common format codes for ``replace_io`` (see `Python struct format characters `_ for complete reference): + +* ``'b'`` - signed byte (-128 to 127) +* ``'B'`` - unsigned byte (0 to 255) +* ``'h'`` - signed short (-32768 to 32767) +* ``'H'`` - unsigned short (0 to 65535) +* ``'i'`` - signed int (-2147483648 to 2147483647) +* ``'I'`` - unsigned int (0 to 4294967295) +* ``'f'`` - float (32-bit) + +Multiple Custom IOs +------------------- + +Define multiple custom IOs programmatically by replacing generic gateway IOs: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Replace multiple gateway IOs with custom definitions + # Assuming a gateway module with Input_1, Input_2, Output_1, Output_2 + rpi.io.Input_1.replace_io("temperature", "h", defaultvalue=0) + rpi.io.Input_2.replace_io("humidity", "h", defaultvalue=0) + rpi.io.Output_1.replace_io("setpoint", "h", defaultvalue=700) + rpi.io.Output_2.replace_io("control_word", "H", defaultvalue=0) + + # Use all custom IOs by their new names + temp = rpi.io.temperature.value / 10.0 + humidity = rpi.io.humidity.value / 10.0 + print(f"Temp: {temp}°C, Humidity: {humidity}%") + + # Write to output registers + rpi.io.setpoint.value = 750 # 75.0°C + rpi.io.control_word.value = 0x0001 # Enable bit + + rpi.exit() + +Using Configuration Files +-------------------------- + +For complex IO configurations, use the ``replace_io_file`` parameter to load custom IOs from a file: + +.. code-block:: python + + # Load custom IOs from configuration file + rpi = revpimodio2.RevPiModIO( + autorefresh=True, + replace_io_file="replace_ios.conf" + ) + + # Custom IOs are now available + temp = rpi.io.temperature.value / 10.0 + print(f"Temperature: {temp}°C") + + rpi.exit() + +**Configuration File Format:** + +Create an INI-style configuration file (``replace_ios.conf``): + +.. code-block:: ini + + [temperature] + replace = Input_1 + frm = h + defaultvalue = 0 + + [humidity] + replace = Input_2 + frm = h + defaultvalue = 0 + + [setpoint] + replace = Output_1 + frm = h + defaultvalue = 700 + + [control_word] + replace = Output_2 + frm = H + byteorder = big + +**Configuration Parameters:** + +* ``replace`` - Name of the gateway IO to replace (required) +* ``frm`` - Struct format character (required) +* ``bit`` - Bit position for boolean IOs (0-7) +* ``byteorder`` - Byte order: ``little`` or ``big`` (default: ``little``) +* ``wordorder`` - Word order for multi-word values +* ``defaultvalue`` - Default value for the IO +* ``bmk`` - Internal designation/bookmark +* ``export`` - Export flag for RevPiPyLoad/RevPiPyControl + +**Exporting Configuration:** + +Export your current custom IOs to a file: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Define custom IOs by replacing gateway IOs + rpi.io.Input_1.replace_io("temperature", "h", defaultvalue=0) + rpi.io.Input_2.replace_io("humidity", "h", defaultvalue=0) + + # Export to configuration file + rpi.export_replaced_ios("my_config.conf") + + rpi.exit() + +This is useful for: + +* Sharing IO configurations across multiple programs +* Integration with RevPiPyLoad and RevPiPyControl +* Version control of IO definitions +* Declarative IO configuration + +Watchdog Management +=================== + +The hardware watchdog monitors your program and resets the system if it stops responding. + +How the Watchdog Works +----------------------- + +The watchdog requires periodic toggling. If not toggled within the timeout period, the system resets. + +**Important:** Only enable the watchdog when your program logic is working correctly. + +Cyclic Watchdog Toggle +----------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def main_cycle(ct): + # Toggle every 10 cycles (200ms @ 20ms) + if ct.flank10c: + ct.core.wd_toggle() + + # Your control logic + ct.io.output.value = ct.io.input.value + + rpi.cycleloop(main_cycle) + +Event-Driven Watchdog Toggle +----------------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def toggle_wd(ioname, iovalue): + """Toggle watchdog every 500ms.""" + rpi.core.wd_toggle() + + # Register timer event for watchdog + rpi.core.wd.reg_timerevent(toggle_wd, 500, prefire=True) + + # Your event handlers + def on_button(ioname, iovalue): + rpi.io.led.value = iovalue + + rpi.io.button.reg_event(on_button) + + rpi.handlesignalend() + rpi.mainloop() + +Conditional Watchdog +-------------------- + +Enable watchdog only when system is operational: + +.. code-block:: python + + def machine_with_watchdog(ct): + if ct.first: + ct.var.state = "IDLE" + ct.var.watchdog_enabled = False + + # Enable watchdog only in RUNNING state + if ct.var.state == "RUNNING": + if not ct.var.watchdog_enabled: + ct.var.watchdog_enabled = True + print("Watchdog enabled") + + # Toggle watchdog + if ct.flank10c: + ct.core.wd_toggle() + + else: + ct.var.watchdog_enabled = False + + # State machine logic + if ct.var.state == "IDLE": + if ct.io.start_button.value: + ct.var.state = "RUNNING" + + elif ct.var.state == "RUNNING": + ct.io.motor.value = True + if ct.io.stop_button.value: + ct.var.state = "IDLE" + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(machine_with_watchdog) + +Combining Paradigms +=================== + +Combine cyclic and event-driven programming for optimal results. + +Cyclic Control with Event UI +----------------------------- + +Use cyclic for time-critical control, events for user interface: + +.. code-block:: python + + import revpimodio2 + import threading + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def cyclic_control(ct: revpimodio2.Cycletools): + """Fast control loop.""" + if ct.first: + ct.var.setpoint = 50.0 + ct.var.running = False + + if ct.var.running: + # Fast control logic + error = ct.var.setpoint - ct.io.sensor.value + if error > 5: + ct.io.actuator.value = True + elif error < -5: + ct.io.actuator.value = False + + def on_setpoint_change(ioname, iovalue): + """Event handler for user setpoint changes.""" + print(f"New setpoint: {iovalue}") + # Access ct.var from event requires thread-safe approach + # In practice, use shared data structure or message queue + + def on_start(ioname, iovalue): + print("System started") + + def on_stop(ioname, iovalue): + print("System stopped") + + # Register user events + rpi.io.start_button.reg_event(on_start, edge=revpimodio2.RISING) + rpi.io.stop_button.reg_event(on_stop, edge=revpimodio2.RISING) + rpi.io.setpoint_input.reg_event(on_setpoint_change, delay=100) + + # Run cyclic loop in background + threading.Thread( + target=lambda: rpi.cycleloop(cyclic_control), + daemon=True + ).start() + + # Run event loop in main thread + rpi.handlesignalend() + rpi.mainloop() + +Event Triggers with Cyclic Processing +-------------------------------------- + +Use events to trigger actions, cyclic for processing: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def cyclic_processor(ct): + """Process work queue.""" + if ct.first: + ct.var.work_queue = [] + + # Process queued work + if ct.var.work_queue: + item = ct.var.work_queue.pop(0) + process_item(item) + + def on_new_item(ioname, iovalue): + """Queue work from events.""" + # Note: Accessing ct.var from events requires synchronization + # This is a simplified example + print(f"New item queued from {ioname}") + + rpi.io.trigger1.reg_event(on_new_item, edge=revpimodio2.RISING) + rpi.io.trigger2.reg_event(on_new_item, edge=revpimodio2.RISING) + + rpi.cycleloop(cyclic_processor) + +Performance Optimization +======================== + +Keep Cycle Logic Fast +--------------------- + +Minimize processing time in each cycle: + +.. code-block:: python + + def optimized_cycle(ct): + # Good: Heavy work only when needed + if ct.flank100c: + expensive_calculation() + + # Good: Keep cycle logic minimal + ct.io.output.value = ct.io.input.value + + # Bad: Don't do this every cycle + # expensive_calculation() # 100ms processing! + +**Guidelines:** + +* Keep cycle time ≥20ms for stability +* Avoid blocking operations (network, file I/O) +* Use flank flags for expensive operations +* Profile your cycle function if experiencing timing issues + +Choose Appropriate Cycle Time +------------------------------ + +Match cycle time to application requirements: + +.. code-block:: python + + # Fast control (motion, high-speed counting) + rpi.cycletime = 20 # 50 Hz + + # Normal control (most applications) + rpi.cycletime = 50 # 20 Hz + + # Slow monitoring (temperature, status) + rpi.cycletime = 100 # 10 Hz + +**Trade-offs:** + +* Faster = Higher CPU usage, better responsiveness +* Slower = Lower CPU usage, adequate for most tasks + +Minimize Event Callbacks +------------------------- + +Keep event callbacks lightweight: + +.. code-block:: python + + # Good: Fast callback + def good_callback(ioname, iovalue): + rpi.io.output.value = iovalue + + # Poor: Slow callback blocks event loop + def poor_callback(ioname, iovalue): + time.sleep(1) # Blocks! + complex_calculation() # Slow! + rpi.io.output.value = iovalue + + # Better: Use threaded events for slow work + def threaded_callback(eventcallback): + complex_calculation() + rpi.io.output.value = result + + rpi.io.trigger.reg_event(threaded_callback, as_thread=True) + +Error Handling +============== + +Graceful Error Recovery +----------------------- + +Always implement safe failure modes: + +.. code-block:: python + + def safe_cycle(ct): + try: + value = ct.io.sensor.value + result = process(value) + ct.io.output.value = result + except ValueError as e: + print(f"Sensor error: {e}") + ct.io.output.value = 0 # Safe default + except Exception as e: + print(f"Unexpected error: {e}") + ct.io.output.value = False # Safe state + +Resource Cleanup +---------------- + +Always clean up resources: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + try: + # Your program logic + rpi.cycleloop(main_cycle) + except KeyboardInterrupt: + print("Interrupted by user") + except Exception as e: + print(f"Error: {e}") + finally: + # Always clean up + rpi.setdefaultvalues() # Reset outputs to defaults + rpi.exit() + +Monitor I/O Errors +------------------ + +Track and handle I/O errors: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.maxioerrors = 10 # Exception after 10 errors + + def main_cycle(ct): + # Check error count periodically + if ct.flank20c: + if rpi.ioerrors > 5: + print(f"Warning: {rpi.ioerrors} I/O errors detected") + ct.io.warning_led.value = True + + # Normal logic + ct.io.output.value = ct.io.input.value + + try: + rpi.cycleloop(main_cycle) + except RuntimeError as e: + print(f"I/O error threshold exceeded: {e}") + +Best Practices +============== + +Naming Conventions +------------------ + +Use descriptive IO names in piCtory: + +.. code-block:: python + + # Good - Clear intent + if rpi.io.emergency_stop.value: + rpi.io.motor.value = False + rpi.io.alarm.value = True + + # Poor - Generic names + if rpi.io.I_15.value: + rpi.io.O_3.value = False + rpi.io.O_7.value = True + +Code Organization +----------------- + +Structure your code for maintainability: + +.. code-block:: python + + import revpimodio2 + + # Constants + TEMP_HIGH_THRESHOLD = 75 + TEMP_LOW_THRESHOLD = 65 + + # Initialize + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def initialize(ct): + """Initialize system state.""" + ct.var.cooling_active = False + ct.var.alarm_active = False + ct.io.motor.value = False + + def monitor_temperature(ct): + """Temperature monitoring logic.""" + temp = ct.io.temperature.value + + if temp > TEMP_HIGH_THRESHOLD: + ct.io.cooling.value = True + ct.var.cooling_active = True + + if temp < TEMP_LOW_THRESHOLD: + ct.io.cooling.value = False + ct.var.cooling_active = False + + def main_cycle(ct): + """Main control loop.""" + if ct.first: + initialize(ct) + + monitor_temperature(ct) + + if ct.last: + ct.io.cooling.value = False + + # Run + try: + rpi.cycleloop(main_cycle) + finally: + rpi.exit() + +Documentation +------------- + +Document complex logic: + +.. code-block:: python + + def control_cycle(ct): + """Control cycle for temperature management. + + State machine: + - IDLE: Waiting for start + - HEATING: Active heating to setpoint + - COOLING: Active cooling from overshoot + - ERROR: Fault condition + + Hysteresis: ±5°C around setpoint + """ + if ct.first: + ct.var.state = "IDLE" + ct.var.setpoint = 70.0 + + # State machine implementation + # ... + +Testing +------- + +Test your code thoroughly: + +.. code-block:: python + + def test_temperature_control(ct): + """Test temperature control logic.""" + + if ct.first: + ct.var.cooling_active = False + ct.var.test_temp = 20.0 + + # Simulate temperature increase + if ct.var.test_temp < 80: + ct.var.test_temp += 0.5 + + # Test control logic + temp = ct.var.test_temp + + if temp > 75 and not ct.var.cooling_active: + assert ct.io.cooling.value == True + ct.var.cooling_active = True + + if temp < 65 and ct.var.cooling_active: + assert ct.io.cooling.value == False + ct.var.cooling_active = False + +Logging +------- + +Implement proper logging: + +.. code-block:: python + + import logging + from datetime import datetime + + # Configure logging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + def main_cycle(ct): + if ct.first: + logging.info("System started") + ct.var.error_count = 0 + + # Log errors + if ct.io.error_sensor.value: + ct.var.error_count += 1 + logging.error(f"Error detected: {ct.var.error_count}") + + # Log status periodically + if ct.flank100c: + logging.info(f"Temperature: {ct.io.temperature.value}°C") + + if ct.last: + logging.info("System stopped") + +Security Considerations +======================= + +Validate External Input +----------------------- + +Always validate external inputs: + +.. code-block:: python + + def on_setpoint_change(ioname, iovalue): + """Validate setpoint range.""" + if 0 <= iovalue <= 100: + rpi.io.setpoint.value = iovalue + else: + print(f"Invalid setpoint: {iovalue}") + rpi.io.error_led.value = True + +Fail-Safe Defaults +------------------ + +Use safe defaults for critical outputs: + +.. code-block:: python + + def main_cycle(ct): + if ct.first: + # Safe defaults + ct.io.motor.value = False + ct.io.heater.value = False + ct.io.valve.value = False + + try: + # Control logic + control_logic(ct) + except Exception as e: + # Revert to safe state on error + ct.io.motor.value = False + ct.io.heater.value = False + +See Also +======== + +* :doc:`basics` - Core concepts and configuration +* :doc:`cyclic_programming` - Cyclic programming patterns +* :doc:`event_programming` - Event-driven programming patterns +* :doc:`api/index` - API reference diff --git a/docs/api/device.rst b/docs/api/device.rst new file mode 100644 index 0000000..8d6e846 --- /dev/null +++ b/docs/api/device.rst @@ -0,0 +1,163 @@ +============== +Device Classes +============== + +Classes for managing Revolution Pi devices. + +.. currentmodule:: revpimodio2.device + +Device +====== + +.. autoclass:: Device + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Base class for all Revolution Pi devices. + +DeviceList +========== + +.. autoclass:: DeviceList + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__, __getitem__, __iter__ + + Container for accessing devices. + + **Example:** + + .. code-block:: python + + # Access device by name + dio_module = rpi.device.DIO_Module_1 + + # Access device by position + first_device = rpi.device[0] + + # Iterate all devices + for device in rpi.device: + print(f"Device: {device.name}") + +Base +==== + +.. autoclass:: Base + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Base class for Revolution Pi base modules. + +GatewayMixin +============ + +.. autoclass:: GatewayMixin + :members: + :undoc-members: + :show-inheritance: + + Mixin class providing gateway functionality for piGate modules. + +ModularBaseConnect_4_5 +====================== + +.. autoclass:: ModularBaseConnect_4_5 + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Base class for Connect 4 and Connect 5 modules. + +Core +==== + +.. autoclass:: Core + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Revolution Pi Core module. + +Connect +======= + +.. autoclass:: Connect + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Revolution Pi Connect module. + +Connect4 +======== + +.. autoclass:: Connect4 + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Revolution Pi Connect 4 module. + +Connect5 +======== + +.. autoclass:: Connect5 + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Revolution Pi Connect 5 module. + +DioModule +========= + +.. autoclass:: DioModule + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Digital I/O module. + +RoModule +======== + +.. autoclass:: RoModule + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Relay output module. + +Gateway +======= + +.. autoclass:: Gateway + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Gateway module (ModbusTCP, Profinet, etc.). + +Virtual +======= + +.. autoclass:: Virtual + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Virtual device for custom applications. diff --git a/docs/api/helper.rst b/docs/api/helper.rst new file mode 100644 index 0000000..51bf332 --- /dev/null +++ b/docs/api/helper.rst @@ -0,0 +1,194 @@ +============== +Helper Classes +============== + +Helper classes for cyclic and event-driven programming. + +.. currentmodule:: revpimodio2.helper + +Cycletools +========== + +.. autoclass:: Cycletools + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Toolkit provided to cyclic functions via ``.cycleloop()``. + + This class provides tools for cyclic functions including timing flags + and edge markers. Note that edge markers (flank flags) are all True + during the first cycle! + + **Attributes:** + + + + Reference to RevPiModIO core object + + + + Reference to RevPiModIO device object + + + + Reference to RevPiModIO io object + + + + True only on first cycle + + + + True when shutdown signal received + + + + Current function execution time in seconds + + + + Container for cycle-persistent variables + + **Toggle Flags** - Alternate between True/False: + + + + 1 cycle True, 1 cycle False + + + + 2 cycles True, 2 cycles False + + + + 5 cycles True, 5 cycles False + + + + 10 cycles True, 10 cycles False + + + + 20 cycles True, 20 cycles False + + **Flank Flags** - True every nth cycle: + + + + True every 5 cycles + + + + True every 10 cycles + + + + True every 15 cycles + + + + True every 20 cycles + + **Example:** + + .. code-block:: python + + def main(ct: revpimodio2.Cycletools): + if ct.first: + # Initialize + ct.var.counter = 0 + + # Main logic + if ct.changed(ct.io.sensor): + ct.var.counter += 1 + + # Blink LED using timing flag + ct.io.led.value = ct.flag5c + + if ct.last: + # Cleanup + print(f"Final: {ct.var.counter}") + +Change Detection +---------------- + + +Timer Functions +--------------- + +On-Delay Timers +~~~~~~~~~~~~~~~ + + +Off-Delay Timers +~~~~~~~~~~~~~~~~ + + +Pulse Timers +~~~~~~~~~~~~ + + +EventCallback +============= + +.. autoclass:: EventCallback + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Thread for internal event function calls. + + This class is passed to threaded event handlers registered with + ``as_thread=True``. The event function receives this thread object + as a parameter to access event information and control execution. + + **Attributes:** + + + + Name of IO that triggered the event + + + + Value of IO when event was triggered + + + + Threading event for abort conditions + + **Example:** + + .. code-block:: python + + def threaded_handler(eventcallback: revpimodio2.EventCallback): + print(f"{eventcallback.ioname} = {eventcallback.iovalue}") + + # Interruptible wait (3 seconds) + if eventcallback.exit.wait(3): + print("Wait interrupted!") + return + + # Check if stop was called + if eventcallback.exit.is_set(): + return + + # Register as threaded event + rpi.io.button.reg_event(threaded_handler, as_thread=True) + +Methods +------- + + +ProcimgWriter +============= + +.. autoclass:: ProcimgWriter + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Internal thread for process image writing and event management. diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..306999b --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,148 @@ +.. _api_reference: + +============= +API Reference +============= + +Complete API reference for RevPiModIO2 Python library. + +.. contents:: Table of Contents + :local: + :depth: 2 + +Overview +======== + +RevPiModIO provides several main classes for programming Revolution Pi hardware: + +* :class:`~revpimodio2.modio.RevPiModIO` - Main class for managing all devices and IOs +* :class:`~revpimodio2.modio.RevPiModIOSelected` - Manage specific devices only +* :class:`~revpimodio2.modio.RevPiModIODriver` - Write data to virtual device inputs +* :class:`~revpimodio2.io.IOList` - Container for accessing IOs +* :class:`~revpimodio2.io.IOBase` - Base class for all IO objects +* :class:`~revpimodio2.helper.Cycletools` - Toolkit for cyclic programming +* :class:`~revpimodio2.helper.EventCallback` - Event handler class + +Quick Examples +============== + +Basic Usage +----------- + +.. code-block:: python + + import revpimodio2 + + # Initialize + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Read input and control output + if rpi.io.button.value: + rpi.io.led.value = True + + # Cleanup + rpi.exit() + +Cyclic Programming +------------------ + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def main_cycle(ct: revpimodio2.Cycletools): + if ct.first: + ct.var.counter = 0 + + if ct.changed(ct.io.sensor): + ct.var.counter += 1 + + rpi.cycleloop(main_cycle) + +Event-Driven Programming +------------------------ + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def on_change(ioname, iovalue): + print(f"{ioname} = {iovalue}") + + rpi.io.button.reg_event(on_change) + rpi.handlesignalend() + rpi.mainloop() + +Constants +========= + +Edge Detection +-------------- + +.. py:data:: revpimodio2.RISING + :type: int + + Detect low-to-high transitions + +.. py:data:: revpimodio2.FALLING + :type: int + + Detect high-to-low transitions + +.. py:data:: revpimodio2.BOTH + :type: int + + Detect any transition + +LED Colors +---------- + +.. py:data:: revpimodio2.OFF + :type: int + + LED off + +.. py:data:: revpimodio2.GREEN + :type: int + + Green LED + +.. py:data:: revpimodio2.RED + :type: int + + Red LED + +IO Types +-------- + +.. py:data:: INP + :type: int + :value: 300 + + Input type + +.. py:data:: OUT + :type: int + :value: 301 + + Output type + +.. py:data:: MEM + :type: int + :value: 302 + + Memory type + +See Also +======== + +* :doc:`../installation` - Installation guide +* :doc:`../quickstart` - Quick start guide +* :doc:`../basics` - Core concepts +* :doc:`../cyclic_programming` - Cyclic programming patterns +* :doc:`../event_programming` - Event-driven programming patterns +* :doc:`../advanced` - Advanced topics diff --git a/docs/api/io.rst b/docs/api/io.rst new file mode 100644 index 0000000..721bf77 --- /dev/null +++ b/docs/api/io.rst @@ -0,0 +1,212 @@ +==================== +IO Classes and Types +==================== + +Classes for managing Revolution Pi inputs and outputs. + +.. currentmodule:: revpimodio2.io + +IOList +====== + +.. autoclass:: IOList + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__, __getitem__, __contains__, __iter__ + + Container for accessing all IO objects. + + The IOList provides multiple ways to access IOs: + + * **Direct attribute access**: ``rpi.io.button.value`` + * **String-based access**: ``rpi.io["button"].value`` + * **Iteration**: ``for io in rpi.io: ...`` + + **Example:** + + .. code-block:: python + + # Direct access + value = rpi.io.I_1.value + rpi.io.O_1.value = True + + # String-based access + value = rpi.io["I_1"].value + + # Check if IO exists + if "sensor" in rpi.io: + print(rpi.io.sensor.value) + + # Iterate all IOs + for io in rpi.io: + print(f"{io.name}: {io.value}") + +IOBase +====== + +.. autoclass:: IOBase + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Base class for all IO objects. + + **Properties:** + + + + IO name from piCtory configuration + + + + Current IO value (read/write) + + + + Byte address in process image + + + + Byte length (0 for single bits) + + + + IO type: 300=INPUT, 301=OUTPUT, 302=MEMORY + + + + Whether value is signed + + + + "little" or "big" endian + + + + Configured default value from piCtory + + + + Comment/description from piCtory + + + + Export flag status + +Event Registration Methods +--------------------------- + + +Value Manipulation Methods +--------------------------- + + +IntIO +===== + +.. autoclass:: IntIO + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + IO objects with integer value access. + + **Example:** + + .. code-block:: python + + # Get integer value + temp = rpi.io.temperature.get_intvalue() + + # Set integer value + rpi.io.setpoint.set_intvalue(1500) + +Integer Value Methods +--------------------- + + +IntIOCounter +============ + +.. autoclass:: IntIOCounter + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Counter input objects with reset capability. + + **Example:** + + .. code-block:: python + + # Read counter + count = rpi.io.counter.value + + # Reset counter + rpi.io.counter.reset() + +StructIO +======== + +.. autoclass:: StructIO + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Structured IO with format strings for complex data types. + + **Example:** + + .. code-block:: python + + # Get structured value + value = rpi.io.sensor_data.get_structvalue() + +Structured Value Methods +------------------------ + + +MemIO +===== + +.. autoclass:: MemIO + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Memory IO with variant value access (string or integer). + +RelaisOutput +============ + +.. autoclass:: RelaisOutput + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Relay output with switching cycle counter. + + **Example:** + + .. code-block:: python + + # Get number of switching cycles + cycles = rpi.io.relay.get_switching_cycles() + +IOEvent +======= + +.. autoclass:: IOEvent + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Internal class for IO event management. diff --git a/docs/api/revpimodio.rst b/docs/api/revpimodio.rst new file mode 100644 index 0000000..d1902e1 --- /dev/null +++ b/docs/api/revpimodio.rst @@ -0,0 +1,141 @@ +================== +RevPiModIO Classes +================== + +Main classes for managing Revolution Pi hardware. + +.. currentmodule:: revpimodio2.modio + +RevPiModIO +========== + +.. autoclass:: RevPiModIO + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Main class for managing all devices and IOs from the piCtory configuration. + + This class manages the complete piCtory configuration and loads all devices + and IOs. It handles exclusive management of the process image and ensures + data synchronization. + + **Constructor Parameters:** + + :param autorefresh: Automatically sync process image (recommended: True) + :type autorefresh: bool + :param monitoring: Read-only mode for supervision (no writes) + :type monitoring: bool + :param syncoutputs: Load current output values on initialization + :type syncoutputs: bool + :param debug: Enable detailed error messages and logging + :type debug: bool + + **Key Attributes:** + + + + Access to all configured inputs/outputs + + + + Access to RevPi Core values (LEDs, status) + + + + Access to specific devices by name + + + + Update frequency in milliseconds + + + + Threading event for clean shutdown + + + + Count of read/write failures + + + + Exception threshold (0 = disabled) + + **Example:** + + .. code-block:: python + + import revpimodio2 + + # Initialize with auto-refresh + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Access IOs + if rpi.io.button.value: + rpi.io.led.value = True + + # Clean shutdown + rpi.exit() + +Loop Execution Methods +---------------------- + + +Data Synchronization Methods +----------------------------- + + +Utility Methods +--------------- + + +RevPiModIOSelected +================== + +.. autoclass:: RevPiModIOSelected + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Manage only specific devices from the piCtory configuration. + + Use this class when you only need to control specific devices instead of + loading the entire configuration. + + **Example:** + + .. code-block:: python + + # Manage only specific devices + rpi = revpimodio2.RevPiModIOSelected("DIO_Module_1", "AIO_Module_1") + +RevPiModIODriver +================ + +.. autoclass:: RevPiModIODriver + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Write data to virtual device inputs for driver development. + + **Example:** + + .. code-block:: python + + # Create driver for virtual device + driver = revpimodio2.RevPiModIODriver("VirtualDevice") + +DevSelect +========= + +.. autoclass:: DevSelect + :members: + :undoc-members: + :show-inheritance: + :special-members: __init__ + + Customized search filter for RevPiModIOSelected. diff --git a/docs/basics.rst b/docs/basics.rst new file mode 100644 index 0000000..54a71ef --- /dev/null +++ b/docs/basics.rst @@ -0,0 +1,331 @@ +====== +Basics +====== + +Core concepts and fundamental usage of RevPiModIO. + +.. contents:: Contents + :local: + :depth: 2 + +Programming Paradigms +===================== + +RevPiModIO supports two complementary programming approaches: + +**Cyclic Programming** - Execute a function at regular intervals, similar to PLC programming. + +* Best for deterministic timing, state machines, and time-critical control +* Runs your function every cycle (typically 20-50ms) +* See :doc:`cyclic_programming` for details + +**Event-Driven Programming** - Register callbacks triggered by hardware state changes. + +* Best for user interactions, sporadic events, and system integration +* Consumes CPU only when events occur +* See :doc:`event_programming` for details + +Both approaches can be combined in a single application. See :doc:`advanced` for examples. + +Getting Started +=============== + +Basic Instantiation +------------------- + +Create a RevPiModIO instance to access your hardware: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Your code here + + rpi.exit() + +Configuration Parameters +------------------------ + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO( + autorefresh=True, # Auto-sync process image (recommended) + monitoring=False, # Read-only mode + syncoutputs=True, # Load output values on init + debug=False # Enable debug messages + ) + +**autorefresh** - Automatically reads inputs and writes outputs. Set to ``True`` for most applications. + +**monitoring** - Read-only mode. Use when monitoring without controlling hardware. + +**syncoutputs** - Load current output values on startup. Prevents outputs from resetting. + +**debug** - Enable debug logging for troubleshooting. + +Cycle Timing +------------ + +Default update rates depend on your hardware: + +* **Core 1**: 40ms (25Hz) +* **Core3/Connect**: 20ms (50Hz) +* **NetIO**: 50ms (20Hz) + +Adjust cycle time to match your needs: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycletime = 100 # Set to 100ms + +**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements. + +Error Handling +-------------- + +Configure I/O error threshold: + +.. code-block:: python + + rpi.maxioerrors = 10 # Raise exception after 10 errors + + # Check error count + if rpi.ioerrors > 5: + print("Warning: I/O errors detected") + +Core Objects +============ + +rpi.io - Input/Output Access +----------------------------- + +Access all configured IOs from piCtory: + +.. code-block:: python + + # Direct attribute access + value = rpi.io.button.value + rpi.io.led.value = True + + # String-based access + rpi.io["button"].value + + # Check existence + if "sensor" in rpi.io: + print(rpi.io.sensor.value) + + # Iterate all IOs + for io in rpi.io: + print(f"{io.name}: {io.value}") + +IO Properties +~~~~~~~~~~~~~ + +Each IO object has these properties: + +* ``.name`` - IO name from piCtory +* ``.value`` - Current value (read/write) +* ``.address`` - Byte address in process image +* ``.type`` - IO type (INPUT=300, OUTPUT=301, MEMORY=302) +* ``.defaultvalue`` - Default value from piCtory + +rpi.core - System Control +-------------------------- + +Access Revolution Pi system features: + +LED Control +~~~~~~~~~~~ + +.. code-block:: python + + # Using constants + rpi.core.A1 = revpimodio2.GREEN + rpi.core.A2 = revpimodio2.RED + rpi.core.A3 = revpimodio2.OFF + + # Individual colors + rpi.core.a1green.value = True + rpi.core.a1red.value = False + +System Status +~~~~~~~~~~~~~ + +.. code-block:: python + + # CPU information + temp = rpi.core.temperature.value + freq = rpi.core.frequency.value + + # piBridge status + cycle_time = rpi.core.iocycle.value + errors = rpi.core.ioerrorcount.value + +Watchdog +~~~~~~~~ + +.. code-block:: python + + # Toggle watchdog + rpi.core.wd_toggle() + + # Watchdog IO object + rpi.core.wd.value = True + +See :doc:`advanced` for complete watchdog management examples. + +rpi.device - Device Access +--------------------------- + +Access specific hardware devices: + +.. code-block:: python + + # By name + dio = rpi.device.DIO_Module_1 + + # By position + first = rpi.device[0] + + # Iterate + for device in rpi.device: + print(device.name) + +Signal Handling +=============== + +Graceful Shutdown +----------------- + +Handle SIGINT and SIGTERM for clean program termination: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Enable signal handling + rpi.handlesignalend() + + # Run main loop + rpi.mainloop() + +Custom Signal Handler +--------------------- + +Implement custom cleanup logic: + +.. code-block:: python + + def cleanup(signum, frame): + print("Shutting down...") + rpi.setdefaultvalues() + rpi.exit() + + rpi.handlesignalend(cleanup) + rpi.mainloop() + +Simple Examples +=============== + +Read Input, Control Output +--------------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Read input and control output + if rpi.io.button.value: + rpi.io.led.value = True + else: + rpi.io.led.value = False + + rpi.exit() + +LED Control +----------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Control status LEDs + rpi.core.A1 = revpimodio2.GREEN + rpi.core.A2 = revpimodio2.RED + + rpi.exit() + +Iterate All IOs +--------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Print all IOs and their values + for io in rpi.io: + print(f"{io.name}: {io.value}") + + rpi.exit() + +Best Practices +============== + +Use Descriptive IO Names +------------------------- + +Configure descriptive names in piCtory: + +.. code-block:: python + + # Good - Clear intent + if rpi.io.emergency_stop.value: + rpi.io.motor.value = False + + # Poor - Generic names + if rpi.io.I_15.value: + rpi.io.O_3.value = False + +Always Clean Up +--------------- + +Always call ``rpi.exit()`` to clean up resources: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + try: + # Your code here + pass + finally: + rpi.exit() + +Check IO Existence +------------------ + +Verify IOs exist before accessing: + +.. code-block:: python + + if "optional_sensor" in rpi.io: + value = rpi.io.optional_sensor.value + else: + print("Sensor not configured") + +See Also +======== + +* :doc:`cyclic_programming` - Cyclic programming patterns +* :doc:`event_programming` - Event-driven programming patterns +* :doc:`advanced` - Advanced topics and best practices +* :doc:`api/index` - API reference diff --git a/docs/conf.py b/docs/conf.py index 2a556bd..513a578 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,19 +15,73 @@ project = 'revpimodio2' copyright = '2023, Sven Sager' author = 'Sven Sager' version = __version__ +release = __version__ # -- General configuration --------------------------------------------------- extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.autosummary', 'sphinx.ext.todo', - 'sphinx.ext.viewcode' ] +# Autodoc configuration +autodoc_default_options = { + 'members': True, + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': True, + 'exclude-members': '__weakref__' +} + +# Napoleon settings (for NumPy and Google style docstrings) +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = True +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = True +napoleon_use_admonition_for_notes = True +napoleon_use_admonition_for_references = True +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_preprocess_types = True +napoleon_type_aliases = None +napoleon_attr_annotations = True + +# Autosummary settings +autosummary_generate = True + +# Intersphinx configuration +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] + +# Theme options +html_theme_options = { + 'navigation_depth': 4, + 'collapse_navigation': False, + 'sticky_navigation': True, + 'includehidden': True, + 'titles_only': False +} + +# -- Options for todo extension ---------------------------------------------- + +todo_include_todos = True diff --git a/docs/cyclic_programming.rst b/docs/cyclic_programming.rst new file mode 100644 index 0000000..eeeeb6e --- /dev/null +++ b/docs/cyclic_programming.rst @@ -0,0 +1,604 @@ +================== +Cyclic Programming +================== + +Cyclic programming executes a function at regular intervals, similar to PLC programming. + +.. contents:: Contents + :local: + :depth: 2 + +When to Use Cyclic Programming +=============================== + +**Cyclic programming is ideal for:** + +* Deterministic timing requirements +* Traditional PLC-style logic +* State machines +* Time-critical control +* Continuous monitoring and control + +**Advantages:** + +* Predictable timing +* Simple mental model +* Easy to reason about program flow +* Natural for control systems + +**Considerations:** + +* Consumes CPU even when idle +* Cycle time affects responsiveness +* Must keep cycle logic fast + +Basic Structure +=============== + +Simple Cycle Loop +----------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def main_cycle(ct: revpimodio2.Cycletools): + """Execute each cycle.""" + if ct.io.start_button.value: + ct.io.motor.value = True + if ct.io.stop_button.value: + ct.io.motor.value = False + + rpi.cycleloop(main_cycle) + +The ``main_cycle`` function is called repeatedly at the configured cycle time (typically 20-50ms). + +Understanding Cycle Time +------------------------- + +The cycle time determines execution frequency: + +* **Core 1**: 40ms (25 Hz) +* **Core3/Connect**: 20ms (50 Hz) +* **NetIO**: 50ms (20 Hz) + +Adjust cycle time to match your needs: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycletime = 100 # 100ms = 10 Hz + +**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements. + +Cycletools Object +================= + +The ``Cycletools`` object is passed to your cycle function, providing access to: + +* ``ct.io`` - All IOs +* ``ct.core`` - System control +* ``ct.device`` - Device access +* ``ct.var`` - Persistent variables +* Lifecycle flags (``first``, ``last``) +* Timing flags (``flag5c``, ``flank10c``, etc.) +* Timer functions (``set_tonc``, ``get_tofc``, etc.) +* Change detection (``changed``) + +Initialization and Cleanup +=========================== + +Use ``ct.first`` and ``ct.last`` for setup and teardown: + +.. code-block:: python + + def main_cycle(ct: revpimodio2.Cycletools): + if ct.first: + # Initialize on first cycle + ct.var.counter = 0 + ct.var.state = "IDLE" + print("System started") + + # Main logic runs every cycle + ct.var.counter += 1 + + if ct.last: + # Cleanup before exit + ct.io.motor.value = False + print(f"Total cycles: {ct.var.counter}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(main_cycle) + +Persistent Variables +==================== + +Use ``ct.var`` to store variables that persist between cycles: + +.. code-block:: python + + def main_cycle(ct): + if ct.first: + ct.var.counter = 0 + ct.var.state = "IDLE" + ct.var.accumulator = 0.0 + + # Variables persist between cycles + ct.var.counter += 1 + ct.var.accumulator += ct.io.sensor.value + + # Access variables later + average = ct.var.accumulator / ct.var.counter + +Variables defined in ``ct.var`` maintain their values across all cycle executions. + +Change Detection +================ + +Detect input changes efficiently without storing previous values: + +.. code-block:: python + + def main_cycle(ct: revpimodio2.Cycletools): + # Detect any change + if ct.changed(ct.io.sensor): + print(f"Sensor changed to: {ct.io.sensor.value}") + + # Detect rising edge (button press) + if ct.changed(ct.io.button, edge=revpimodio2.RISING): + print("Button pressed!") + + # Detect falling edge (button release) + if ct.changed(ct.io.button, edge=revpimodio2.FALLING): + print("Button released!") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(main_cycle) + +Edge types: + +* ``revpimodio2.RISING`` - False to True transition +* ``revpimodio2.FALLING`` - True to False transition +* ``revpimodio2.BOTH`` - Any change (default) + +Timing Flags +============ + +Built-in timing flags provide periodic execution without manual counting. + +Toggle Flags +------------ + +Toggle flags alternate between True/False at regular intervals: + +.. code-block:: python + + def main_cycle(ct): + # Blink LED - flag5c alternates every 5 cycles + ct.io.blink_led.value = ct.flag5c + + # Different blink rates + ct.io.fast_blink.value = ct.flag2c # Every 2 cycles + ct.io.slow_blink.value = ct.flag20c # Every 20 cycles + +**Available toggle flags:** + +* ``ct.flag1c`` - Every cycle +* ``ct.flag2c`` - Every 2 cycles +* ``ct.flag5c`` - Every 5 cycles +* ``ct.flag10c`` - Every 10 cycles +* ``ct.flag20c`` - Every 20 cycles + +Flank Flags +----------- + +Flank flags are True for exactly one cycle at regular intervals: + +.. code-block:: python + + def main_cycle(ct): + # Execute task every 10 cycles + if ct.flank10c: + print(f"Runtime: {ct.runtime:.3f}s") + + # Execute task every 20 cycles + if ct.flank20c: + temp = ct.io.temperature.value + print(f"Temperature: {temp}°C") + +**Available flank flags:** + +* ``ct.flank5c`` - True every 5 cycles +* ``ct.flank10c`` - True every 10 cycles +* ``ct.flank15c`` - True every 15 cycles +* ``ct.flank20c`` - True every 20 cycles + +Timers +====== + +RevPiModIO provides three timer types based on PLC standards. All timers are specified in cycle counts. + +On-Delay Timer (TON/TONC) +-------------------------- + +Output becomes True only after input is continuously True for specified cycles: + +.. code-block:: python + + def main_cycle(ct): + # Input: sensor value + ct.set_tonc("delay", 10) + + # Output goes high after input is high for 10 cycles + if ct.get_tonc("delay"): + ct.io.output.value = True + else: + ct.io.output.value = False + +**How it works:** + +1. Input goes True +2. Timer starts counting +3. If input stays True for 10 cycles, output goes True +4. If input goes False before 10 cycles, timer resets + +**Use cases:** + +* Button debouncing +* Startup delays +* Confirming sustained conditions + +Off-Delay Timer (TOF/TOFC) +--------------------------- + +Output stays True for specified cycles after input goes False: + +.. code-block:: python + + def main_cycle(ct): + # Input: button value + ct.set_tofc("motor_coast", 20) + + # Motor continues for 20 cycles after button release + ct.io.motor.value = ct.get_tofc("motor_coast") + +**How it works:** + +1. Input is True, output is True +2. Input goes False +3. Output stays True for 20 more cycles +4. After 20 cycles, output goes False + +**Use cases:** + +* Motor coast-down +* Relay hold-in +* Graceful shutdowns + +Pulse Timer (TP/TPC) +-------------------- + +Generates a one-shot pulse of specified duration: + +.. code-block:: python + + def main_cycle(ct): + # Trigger pulse on button press + if ct.changed(ct.io.trigger, edge=revpimodio2.RISING): + ct.set_tpc("pulse", 5) + + # Output is True for 5 cycles + ct.io.pulse_output.value = ct.get_tpc("pulse") + +**How it works:** + +1. Call ``set_tpc`` to trigger pulse +2. Output is True for 5 cycles +3. After 5 cycles, output goes False +4. Additional triggers during pulse are ignored + +**Use cases:** + +* One-shot operations +* Acknowledgment pulses +* Retriggerable delays + +Converting Time to Cycles +-------------------------- + +Calculate cycles from desired time: + +.. code-block:: python + + # At 20ms cycle time: + # 1 second = 50 cycles + # 100ms = 5 cycles + # 2 seconds = 100 cycles + + def main_cycle(ct): + cycle_time_ms = rpi.cycletime + desired_time_ms = 1500 # 1.5 seconds + + cycles_needed = int(desired_time_ms / cycle_time_ms) + ct.set_tonc("my_delay", cycles_needed) + +State Machines +============== + +State machines implement complex control logic with distinct operational modes. + +Simple State Machine +--------------------- + +.. code-block:: python + + def traffic_light(ct: revpimodio2.Cycletools): + """Traffic light controller.""" + + if ct.first: + ct.var.state = "GREEN" + + if ct.var.state == "GREEN": + ct.io.green_led.value = True + ct.io.yellow_led.value = False + ct.io.red_led.value = False + + # After 100 cycles (2s @ 20ms), go to yellow + ct.set_tonc("green_time", 100) + if ct.get_tonc("green_time"): + ct.var.state = "YELLOW" + + elif ct.var.state == "YELLOW": + ct.io.green_led.value = False + ct.io.yellow_led.value = True + + ct.set_tonc("yellow_time", 25) # 500ms + if ct.get_tonc("yellow_time"): + ct.var.state = "RED" + + elif ct.var.state == "RED": + ct.io.yellow_led.value = False + ct.io.red_led.value = True + + ct.set_tonc("red_time", 150) # 3s + if ct.get_tonc("red_time"): + ct.var.state = "GREEN" + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(traffic_light) + +Complex State Machine +---------------------- + +.. code-block:: python + + def machine_controller(ct: revpimodio2.Cycletools): + """Multi-state machine controller.""" + + if ct.first: + ct.var.state = "IDLE" + ct.var.production_count = 0 + + # State: IDLE - Ready to start + if ct.var.state == "IDLE": + ct.io.motor.value = False + ct.io.green_led.value = True + ct.io.red_led.value = False + + if ct.changed(ct.io.start_button, edge=revpimodio2.RISING): + ct.var.state = "STARTING" + print("Starting...") + + # State: STARTING - Startup sequence + elif ct.var.state == "STARTING": + ct.io.yellow_led.value = True + + # 2-second startup delay + ct.set_tonc("startup", 100) + if ct.get_tonc("startup"): + ct.var.state = "RUNNING" + print("Running") + + # State: RUNNING - Normal operation + elif ct.var.state == "RUNNING": + ct.io.motor.value = True + ct.io.yellow_led.value = False + ct.io.green_led.value = ct.flag5c # Blink + + # Count production + if ct.changed(ct.io.sensor, edge=revpimodio2.RISING): + ct.var.production_count += 1 + + # Check for stop + if ct.io.stop_button.value: + ct.var.state = "STOPPING" + + # Check for error + if ct.io.error_sensor.value: + ct.var.state = "ERROR" + + # State: STOPPING - Controlled shutdown + elif ct.var.state == "STOPPING": + # Coast motor for 1 second + ct.set_tofc("coast", 50) + ct.io.motor.value = ct.get_tofc("coast") + + if not ct.io.motor.value: + ct.var.state = "IDLE" + print("Stopped") + + # State: ERROR - Fault condition + elif ct.var.state == "ERROR": + ct.io.motor.value = False + ct.io.red_led.value = ct.flag2c # Blink red + + if ct.changed(ct.io.ack_button, edge=revpimodio2.RISING): + if not ct.io.error_sensor.value: + ct.var.state = "IDLE" + print("Error cleared") + + if ct.last: + print(f"Total production: {ct.var.production_count}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(machine_controller) + +Practical Examples +================== + +Temperature Control +------------------- + +Temperature monitoring with hysteresis control: + +.. code-block:: python + + def temperature_monitor(ct: revpimodio2.Cycletools): + """Monitor temperature and control cooling.""" + + if ct.first: + ct.var.cooling_active = False + + temp = ct.io.temperature.value + + # Hysteresis: ON at 75°C, OFF at 65°C + if temp > 75 and not ct.var.cooling_active: + ct.io.cooling_fan.value = True + ct.var.cooling_active = True + print(f"Cooling ON: {temp}°C") + + elif temp < 65 and ct.var.cooling_active: + ct.io.cooling_fan.value = False + ct.var.cooling_active = False + print(f"Cooling OFF: {temp}°C") + + # Warning if too hot + if temp > 85: + ct.io.warning_led.value = ct.flag2c # Blink + + # Emergency shutdown + if temp > 95: + ct.io.emergency_shutdown.value = True + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(temperature_monitor) + +Production Counter +------------------ + +Count production items with start/stop control: + +.. code-block:: python + + def production_counter(ct: revpimodio2.Cycletools): + """Track production count.""" + + if ct.first: + ct.var.total_count = 0 + ct.var.running = False + + # Start/stop control + if ct.changed(ct.io.start_button, edge=revpimodio2.RISING): + ct.var.running = True + + if ct.changed(ct.io.stop_button, edge=revpimodio2.RISING): + ct.var.running = False + + # Count items + if ct.var.running: + if ct.changed(ct.io.item_sensor, edge=revpimodio2.RISING): + ct.var.total_count += 1 + ct.set_tpc("count_pulse", 5) # Pulse LED + print(f"Item #{ct.var.total_count}") + + ct.io.count_led.value = ct.get_tpc("count_pulse") + + # Reset counter + if ct.changed(ct.io.reset_button, edge=revpimodio2.RISING): + print(f"Final count: {ct.var.total_count}") + ct.var.total_count = 0 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.cycleloop(production_counter) + +Best Practices +============== + +Keep Cycle Logic Fast +---------------------- + +Minimize processing time in each cycle: + +.. code-block:: python + + def optimized_cycle(ct): + # Heavy work only when needed + if ct.flank100c: + heavy_calculation() + + # Keep cycle logic minimal + ct.io.output.value = ct.io.input.value + +**Guidelines:** + +* Avoid blocking operations (network, file I/O) +* Use flank flags for expensive operations +* Keep cycle time ≥20ms for stability + +Use Appropriate Cycle Time +--------------------------- + +Match cycle time to application requirements: + +.. code-block:: python + + # Fast control (motion, high-speed counting) + rpi.cycletime = 20 # 50 Hz + + # Normal control (most applications) + rpi.cycletime = 50 # 20 Hz + + # Slow monitoring (temperature, status) + rpi.cycletime = 100 # 10 Hz + +Handle Errors Safely +-------------------- + +Always implement safe failure modes: + +.. code-block:: python + + def safe_cycle(ct): + try: + value = ct.io.sensor.value + ct.io.output.value = process(value) + except Exception as e: + print(f"Error: {e}") + ct.io.output.value = False # Safe state + +Initialize Properly +------------------- + +Use ``ct.first`` for all initialization: + +.. code-block:: python + + def main_cycle(ct): + if ct.first: + # Initialize all variables + ct.var.counter = 0 + ct.var.state = "IDLE" + ct.var.last_value = 0 + + # Set initial outputs + ct.io.motor.value = False + +See Also +======== + +* :doc:`basics` - Core concepts and configuration +* :doc:`event_programming` - Event-driven programming +* :doc:`advanced` - Advanced topics and examples +* :doc:`api/helper` - Cycletools API reference diff --git a/docs/event_programming.rst b/docs/event_programming.rst new file mode 100644 index 0000000..0b21dad --- /dev/null +++ b/docs/event_programming.rst @@ -0,0 +1,584 @@ +==================== +Event Programming +==================== + +Event-driven programming uses callbacks triggered by hardware state changes. + +.. contents:: Contents + :local: + :depth: 2 + +When to Use Event-Driven Programming +===================================== + +**Event-driven programming is ideal for:** + +* Handling user interactions +* Processing occasional events +* Background tasks +* System integration +* Low CPU usage requirements + +**Advantages:** + +* Consumes CPU only when events occur +* Natural for user interfaces +* Simple asynchronous operation +* Efficient for sporadic events + +**Considerations:** + +* Non-deterministic timing +* Must handle concurrent events carefully +* Less intuitive for continuous control + +Basic Structure +=============== + +Simple Event Handler +-------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def on_button_change(ioname, iovalue): + """Called when button changes.""" + print(f"{ioname} = {iovalue}") + rpi.io.led.value = iovalue + + # Register event + rpi.io.button.reg_event(on_button_change) + + # Run main loop + rpi.handlesignalend() + rpi.mainloop() + +The callback function receives: + +* ``ioname`` - Name of the IO that changed +* ``iovalue`` - New value of the IO + +Event Registration +================== + +Value Change Events +------------------- + +Register callbacks for IO value changes: + +.. code-block:: python + + def on_change(ioname, iovalue): + print(f"{ioname} changed to {iovalue}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Any change + rpi.io.sensor.reg_event(on_change) + + # Rising edge only + rpi.io.button.reg_event(on_change, edge=revpimodio2.RISING) + + # Falling edge only + rpi.io.button.reg_event(on_change, edge=revpimodio2.FALLING) + + rpi.handlesignalend() + rpi.mainloop() + +**Edge types:** + +* ``revpimodio2.RISING`` - False to True transition +* ``revpimodio2.FALLING`` - True to False transition +* ``revpimodio2.BOTH`` - Any change (default) + +Lambda Functions +---------------- + +Use lambda for simple callbacks: + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Simple lambda callback + rpi.io.button.reg_event( + lambda name, val: print(f"Button: {val}") + ) + + # Lambda with edge filter + rpi.io.start_button.reg_event( + lambda name, val: print("Started!"), + edge=revpimodio2.RISING + ) + + rpi.handlesignalend() + rpi.mainloop() + +Multiple Events +--------------- + +Register multiple callbacks on one IO: + +.. code-block:: python + + def on_press(ioname, iovalue): + print("Pressed!") + + def on_release(ioname, iovalue): + print("Released!") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Different callbacks for different edges + rpi.io.button.reg_event(on_press, edge=revpimodio2.RISING) + rpi.io.button.reg_event(on_release, edge=revpimodio2.FALLING) + + rpi.handlesignalend() + rpi.mainloop() + +Or register one callback on multiple IOs: + +.. code-block:: python + + def any_sensor_changed(ioname, iovalue): + print(f"{ioname} changed to {iovalue}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Same callback for multiple sensors + for sensor in ["sensor1", "sensor2", "sensor3"]: + if sensor in rpi.io: + rpi.io[sensor].reg_event(any_sensor_changed) + + rpi.handlesignalend() + rpi.mainloop() + +Debouncing +========== + +Add debounce delays to filter noise and false triggers: + +.. code-block:: python + + def on_stable_press(ioname, iovalue): + """Called only after button is stable for 50ms.""" + print(f"Confirmed: {iovalue}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # 50ms debounce delay + rpi.io.noisy_button.reg_event(on_stable_press, delay=50) + + rpi.handlesignalend() + rpi.mainloop() + +**How debouncing works:** + +1. IO value changes +2. RevPiModIO waits for ``delay`` milliseconds +3. If value is still changed, callback is triggered +4. If value changed back, callback is not triggered + +**Typical debounce times:** + +* Mechanical switches: 20-50ms +* Relays: 10-20ms +* Analog sensors: 100-500ms + +Debouncing with Edge Detection +------------------------------- + +.. code-block:: python + + def on_confirmed_press(ioname, iovalue): + """Called only for stable button press.""" + print("Confirmed press") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Rising edge with 30ms debounce + rpi.io.button.reg_event( + on_confirmed_press, + edge=revpimodio2.RISING, + delay=30 + ) + + rpi.handlesignalend() + rpi.mainloop() + +Timer Events +============ + +Execute callbacks at regular intervals independent of IO changes: + +.. code-block:: python + + def periodic_task(ioname, iovalue): + """Called every 500ms.""" + print(f"Periodic task: {iovalue}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Execute every 500ms + rpi.io.dummy.reg_timerevent(periodic_task, 500) + + rpi.handlesignalend() + rpi.mainloop() + +Timer Event Parameters +---------------------- + +.. code-block:: python + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def blink_led(ioname, iovalue): + """Toggle LED every 500ms.""" + rpi.io.blink_led.value = not rpi.io.blink_led.value + + def log_temperature(ioname, iovalue): + """Log temperature every 5 seconds.""" + temp = rpi.io.temperature.value + print(f"Temperature: {temp}°C") + + # Blink every 500ms, trigger immediately + rpi.io.blink_led.reg_timerevent(blink_led, 500, prefire=True) + + # Log every 5 seconds, don't trigger immediately + rpi.io.temperature.reg_timerevent(log_temperature, 5000, prefire=False) + + rpi.handlesignalend() + rpi.mainloop() + +**Parameters:** + +* ``interval`` - Milliseconds between calls +* ``prefire`` - If True, trigger immediately on registration + +Threaded Events +=============== + +Use threaded events for long-running operations that would block the main loop: + +.. code-block:: python + + def long_task(eventcallback: revpimodio2.EventCallback): + """Threaded handler for time-consuming tasks.""" + print(f"Starting task for {eventcallback.ioname}") + + for i in range(10): + # Check if stop requested + if eventcallback.exit.is_set(): + print("Task cancelled") + return + + # Interruptible wait (1 second) + eventcallback.exit.wait(1) + print(f"Progress: {(i+1)*10}%") + + print("Task complete") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Register as threaded event + rpi.io.trigger.reg_event( + long_task, + as_thread=True, + edge=revpimodio2.RISING + ) + + rpi.handlesignalend() + rpi.mainloop() + +EventCallback Object +-------------------- + +Threaded callbacks receive an ``EventCallback`` object with: + +* ``eventcallback.ioname`` - Name of the IO +* ``eventcallback.iovalue`` - Value that triggered event +* ``eventcallback.exit`` - ``threading.Event`` for cancellation + +**Important:** Always check ``eventcallback.exit.is_set()`` periodically to allow graceful shutdown. + +Interruptible Sleep +------------------- + +Use ``eventcallback.exit.wait()`` instead of ``time.sleep()`` for interruptible delays: + +.. code-block:: python + + def background_task(eventcallback: revpimodio2.EventCallback): + """Long task with interruptible waits.""" + + while not eventcallback.exit.is_set(): + # Do some work + process_data() + + # Wait 5 seconds or until exit requested + if eventcallback.exit.wait(5): + break # Exit was requested + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.io.trigger.reg_event(background_task, as_thread=True) + rpi.handlesignalend() + rpi.mainloop() + +Unregistering Events +==================== + +Remove event callbacks when no longer needed: + +.. code-block:: python + + def my_callback(ioname, iovalue): + print(f"{ioname} = {iovalue}") + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Register event + rpi.io.button.reg_event(my_callback) + + # Unregister specific callback + rpi.io.button.unreg_event(my_callback) + + # Unregister all events for this IO + rpi.io.button.unreg_event() + +Practical Examples +================== + +LED Toggle on Button Press +--------------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def toggle_led(ioname, iovalue): + """Toggle LED on button press.""" + rpi.io.led.value = not rpi.io.led.value + print(f"LED: {rpi.io.led.value}") + + rpi.io.button.reg_event(toggle_led, edge=revpimodio2.RISING) + rpi.handlesignalend() + rpi.mainloop() + +Multiple Button Handler +------------------------ + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def on_start(ioname, iovalue): + print("Starting motor...") + rpi.io.motor.value = True + rpi.core.A1 = revpimodio2.GREEN + + def on_stop(ioname, iovalue): + print("Stopping motor...") + rpi.io.motor.value = False + rpi.core.A1 = revpimodio2.RED + + def on_emergency(ioname, iovalue): + print("EMERGENCY STOP!") + rpi.io.motor.value = False + rpi.io.alarm.value = True + rpi.core.A1 = revpimodio2.RED + + # Register different buttons + rpi.io.start_button.reg_event(on_start, edge=revpimodio2.RISING) + rpi.io.stop_button.reg_event(on_stop, edge=revpimodio2.RISING) + rpi.io.emergency_stop.reg_event(on_emergency, edge=revpimodio2.RISING) + + rpi.handlesignalend() + rpi.mainloop() + +Sensor Logging +-------------- + +.. code-block:: python + + import revpimodio2 + from datetime import datetime + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def log_sensor_change(ioname, iovalue): + """Log sensor changes with timestamp.""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"{timestamp} - {ioname}: {iovalue}") + + # Log all sensor changes + for io_name in ["sensor1", "sensor2", "temperature"]: + if io_name in rpi.io: + rpi.io[io_name].reg_event(log_sensor_change) + + rpi.handlesignalend() + rpi.mainloop() + +Periodic Status Report +---------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def status_report(ioname, iovalue): + """Print system status every 10 seconds.""" + print("=== Status Report ===") + print(f"Temperature: {rpi.core.temperature.value}°C") + print(f"CPU Frequency: {rpi.core.frequency.value} MHz") + print(f"IO Errors: {rpi.core.ioerrorcount.value}") + print() + + # Status report every 10 seconds + rpi.io.dummy.reg_timerevent(status_report, 10000, prefire=True) + + rpi.handlesignalend() + rpi.mainloop() + +Threaded Data Processing +------------------------- + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def process_batch(eventcallback: revpimodio2.EventCallback): + """Process data batch in background thread.""" + print(f"Starting batch processing...") + + batch_size = 100 + for i in range(batch_size): + if eventcallback.exit.is_set(): + print("Processing cancelled") + return + + # Simulate processing + eventcallback.exit.wait(0.1) + + if i % 10 == 0: + print(f"Progress: {i}/{batch_size}") + + print("Batch processing complete") + rpi.io.done_led.value = True + + # Trigger on button press + rpi.io.start_batch.reg_event( + process_batch, + as_thread=True, + edge=revpimodio2.RISING + ) + + rpi.handlesignalend() + rpi.mainloop() + +Best Practices +============== + +Keep Callbacks Fast +------------------- + +Event callbacks should complete quickly: + +.. code-block:: python + + # Good - Fast callback + def good_callback(ioname, iovalue): + rpi.io.output.value = iovalue + + # Poor - Blocking callback + def poor_callback(ioname, iovalue): + time.sleep(5) # Blocks event loop! + rpi.io.output.value = iovalue + +For slow operations, use threaded events: + +.. code-block:: python + + def slow_task(eventcallback): + # Long operation in separate thread + process_data() + + rpi.io.trigger.reg_event(slow_task, as_thread=True) + +Use Debouncing +-------------- + +Always debounce mechanical inputs: + +.. code-block:: python + + # Good - Debounced button + rpi.io.button.reg_event(callback, delay=30) + + # Poor - No debounce (may trigger multiple times) + rpi.io.button.reg_event(callback) + +Handle Errors Gracefully +------------------------- + +Protect callbacks from exceptions: + +.. code-block:: python + + def safe_callback(ioname, iovalue): + try: + result = risky_operation(iovalue) + rpi.io.output.value = result + except Exception as e: + print(f"Error in callback: {e}") + rpi.io.output.value = False # Safe state + +Check IO Existence +------------------ + +Verify IOs exist before registering events: + +.. code-block:: python + + if "optional_button" in rpi.io: + rpi.io.optional_button.reg_event(callback) + else: + print("Optional button not configured") + +Clean Up Threads +---------------- + +Threaded events are automatically cleaned up on exit, but you can manually unregister: + +.. code-block:: python + + def long_task(eventcallback): + while not eventcallback.exit.is_set(): + work() + eventcallback.exit.wait(1) + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Register + rpi.io.trigger.reg_event(long_task, as_thread=True) + + # Later: unregister to stop thread + rpi.io.trigger.unreg_event(long_task) + +See Also +======== + +* :doc:`basics` - Core concepts and configuration +* :doc:`cyclic_programming` - Cyclic programming patterns +* :doc:`advanced` - Combining paradigms and advanced topics +* :doc:`api/helper` - EventCallback API reference diff --git a/docs/index.rst b/docs/index.rst index 8fc05d2..ce031ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,87 @@ -.. revpimodio2 documentation main file, created by - sphinx-quickstart on Sun Jan 22 17:49:41 2023. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +==================================== +Welcome to RevPiModIO Documentation! +==================================== -Welcome to revpimodio2's documentation! -======================================= +RevPiModIO is a Python3 module for programming Revolution Pi hardware from KUNBUS GmbH. +It provides easy access to all devices and IOs from the piCtory configuration, supporting +both cyclic (PLC-style) and event-driven programming paradigms. + +.. note:: + **New to RevPiModIO?** Start with :doc:`installation` and :doc:`quickstart`. + +Key Features +============ + +* **Dual Programming Models**: Cyclic (PLC-style) and event-driven approaches +* **Direct Hardware Access**: Simple Python interface to all I/O devices +* **Automatic Configuration**: Loads piCtory device configuration +* **Event System**: Callbacks for value changes and timer events +* **Open Source**: LGPLv2 license, no licensing fees + +Quick Example +============= + +**Cyclic Programming**:: + + import revpimodio2 + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def main(ct): + if ct.io.button.value: + ct.io.led.value = True + + rpi.cycleloop(main) + +**Event-Driven Programming**:: + + import revpimodio2 + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def on_change(ioname, iovalue): + print(f"{ioname} = {iovalue}") + + rpi.io.button.reg_event(on_change) + rpi.handlesignalend() + rpi.mainloop() + +Documentation +============= .. toctree:: :maxdepth: 2 - :caption: Contents: + :caption: Getting Started + installation + quickstart +.. toctree:: + :maxdepth: 2 + :caption: User Guide -Indices and tables + basics + cyclic_programming + event_programming + advanced + +.. toctree:: + :maxdepth: 3 + :caption: API Reference + + api/index + api/revpimodio + api/io + api/helper + api/device + +External Resources +================== + +* **Official Website**: `revpimodio.org `_ +* **GitHub Repository**: `github.com/naruxde/ `_ +* **Discussion Forum**: `revpimodio.org/diskussionsforum/ `_ +* **Revolution Pi Hardware**: `revolution.kunbus.com `_ + +Indices and Tables ================== * :ref:`genindex` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..856d032 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,85 @@ +============ +Installation +============ + +System Requirements +=================== + +* Python 3.7 or higher +* Revolution Pi hardware (Core, Core3, Connect, Compact, Flat) +* piCtory configuration tool + +Prerequisites +============= + +User Permissions +---------------- + +On Bookworm images, users must belong to the ``picontrol`` group:: + + sudo usermod -a -G picontrol username + +Log out and log back in for the group change to take effect. + +Installing RevPiModIO +===================== + +Using pip +--------- + +Install from PyPI:: + + pip install revpimodio2 + +From Source +----------- + +Clone the repository and install:: + + git clone https://github.com/naruxde/revpimodio2.git + cd revpimodio2 + pip install . + +Verify Installation +=================== + +Test the installation:: + + python3 -c "import revpimodio2; print(revpimodio2.__version__)" + +Optional Components +=================== + +RevPiPyLoad +----------- + +For advanced features like XML-RPC server and MQTT integration:: + + sudo apt-get update + sudo apt-get install revpipyload + +Configure XML-RPC Server +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Edit ``/etc/revpipyload/revpipyload.conf``:: + + [XMLRPC] + xmlrpc = 1 + +Configure access permissions in ``/etc/revpipyload/aclxmlrpc.conf``, then restart:: + + sudo service revpipyload restart + +RevPi Commander +--------------- + +RevPi Commander provides a GUI for testing I/O without programming: + +1. Download from `revpimodio.org `_ +2. Configure connection via File → Connections with your RevPi's IP address (port: 55123) +3. Use "PLC watch mode" to monitor sensors and control outputs + +Next Steps +========== + +After installation, proceed to :doc:`quickstart` to write your first program. diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index 76962cf..0000000 --- a/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -src -=== - -.. toctree:: - :maxdepth: 4 - - revpimodio2 diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..6a8520d --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,207 @@ +========== +Quick Start +========== + +This guide will help you write your first RevPiModIO program. + +Basic Concepts +============== + +RevPiModIO provides two main programming paradigms: + +* **Cyclic Programming** - Execute a function at regular intervals (PLC-style) +* **Event-Driven Programming** - Register callbacks triggered by hardware changes + +Both approaches use the same core objects: + +* ``rpi.io`` - Access inputs and outputs by name +* ``rpi.core`` - Control LEDs, watchdog, and system status +* ``rpi.device`` - Access specific hardware devices + +Hardware Configuration +====================== + +Before programming, configure your hardware using piCtory: + +1. Access piCtory web interface on your RevPi Core module +2. Add and configure your I/O modules +3. Assign symbolic names to inputs and outputs + + * Example: ``button``, ``led``, ``temperature`` + * Good names make your code readable + +4. Save configuration and activate + +Your First Program +================== + +Simple Input to Output +---------------------- + +The simplest program reads an input and controls an output: + +.. code-block:: python + + import revpimodio2 + + # Initialize with auto-refresh + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Read input and control output + if rpi.io.button.value: + rpi.io.led.value = True + else: + rpi.io.led.value = False + + # Clean up + rpi.exit() + +Cyclic Program +-------------- + +For continuous operation, use a cyclic loop: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def main_cycle(ct: revpimodio2.Cycletools): + """Called every cycle (default: 20-50ms).""" + + if ct.first: + # Initialize on first cycle + ct.var.counter = 0 + print("Program started") + + # Main logic + if ct.io.button.value: + ct.io.led.value = True + else: + ct.io.led.value = False + + # Count button presses + if ct.changed(ct.io.button, edge=revpimodio2.RISING): + ct.var.counter += 1 + print(f"Button pressed {ct.var.counter} times") + + if ct.last: + # Cleanup on exit + print("Program stopped") + + # Run cyclic loop + rpi.cycleloop(main_cycle) + +Event-Driven Program +-------------------- + +For event-based operation, use callbacks: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + def on_button_press(ioname, iovalue): + """Called when button changes.""" + print(f"Button is now: {iovalue}") + rpi.io.led.value = iovalue + + # Register event callback + rpi.io.button.reg_event(on_button_press) + + # Handle shutdown signals + rpi.handlesignalend() + + # Start event loop + rpi.mainloop() + +LED Control +=========== + +Control the RevPi status LEDs: + +.. code-block:: python + + import revpimodio2 + + rpi = revpimodio2.RevPiModIO(autorefresh=True) + + # Set LED colors using constants + rpi.core.A1 = revpimodio2.GREEN # Success + rpi.core.A2 = revpimodio2.RED # Error + rpi.core.A3 = revpimodio2.OFF # Off + + # Or control individual colors + rpi.core.a1green.value = True + rpi.core.a1red.value = False + + rpi.exit() + +Common Patterns +=============== + +Initialize and Cleanup +---------------------- + +Always initialize variables and clean up resources: + +.. code-block:: python + + def main_cycle(ct): + if ct.first: + # Initialize + ct.var.state = "IDLE" + ct.var.error_count = 0 + + # Main logic here... + + if ct.last: + # Cleanup + ct.io.motor.value = False + print(f"Errors: {ct.var.error_count}") + +Edge Detection +-------------- + +Detect rising or falling edges: + +.. code-block:: python + + def main_cycle(ct): + # Detect button press (rising edge) + if ct.changed(ct.io.button, edge=revpimodio2.RISING): + print("Button pressed!") + + # Detect button release (falling edge) + if ct.changed(ct.io.button, edge=revpimodio2.FALLING): + print("Button released!") + +Timers +------ + +Use built-in cycle-based timers: + +.. code-block:: python + + def main_cycle(ct): + # On-delay: Input must be True for 10 cycles + ct.set_tonc("startup", 10) + if ct.get_tonc("startup"): + ct.io.motor.value = True + + # Pulse: Generate 5-cycle pulse + if ct.io.trigger.value: + ct.set_tpc("pulse", 5) + ct.io.pulse_output.value = ct.get_tpc("pulse") + +Next Steps +========== + +* :doc:`basics` - Core concepts and configuration +* :doc:`cyclic_programming` - Cyclic programming patterns +* :doc:`event_programming` - Event-driven programming patterns +* :doc:`advanced` - Advanced topics and best practices +* :doc:`api/index` - API reference diff --git a/docs/revpimodio2.rst b/docs/revpimodio2.rst deleted file mode 100644 index 6980e58..0000000 --- a/docs/revpimodio2.rst +++ /dev/null @@ -1,85 +0,0 @@ -revpimodio2 package -=================== - -Submodules ----------- - -revpimodio2.app module ----------------------- - -.. automodule:: revpimodio2.app - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.device module -------------------------- - -.. automodule:: revpimodio2.device - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.errors module -------------------------- - -.. automodule:: revpimodio2.errors - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.helper module -------------------------- - -.. automodule:: revpimodio2.helper - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.io module ---------------------- - -.. automodule:: revpimodio2.io - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.modio module ------------------------- - -.. automodule:: revpimodio2.modio - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.netio module ------------------------- - -.. automodule:: revpimodio2.netio - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.pictory module --------------------------- - -.. automodule:: revpimodio2.pictory - :members: - :undoc-members: - :show-inheritance: - -revpimodio2.summary module --------------------------- - -.. automodule:: revpimodio2.summary - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: revpimodio2 - :members: - :undoc-members: - :show-inheritance: From bf631ea176b33e85e28bdb36af82048c1e557d73 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:13:07 +0100 Subject: [PATCH 04/18] docs: add missing docstrings to classes and methods Added docstrings to: - Error classes (RevPiModIOError, DeviceNotFoundError) - ProductType constants class - GatewayMixin class for piGate module detection - IO __call__ methods (IOBase, IntIO, StructIO) - MemIO.get_variantvalue method - RevPiModIO context manager methods (__enter__, __exit__) --- src/revpimodio2/device.py | 2 ++ src/revpimodio2/errors.py | 4 ++++ src/revpimodio2/io.py | 28 ++++++++++++++++++++++++++++ src/revpimodio2/modio.py | 11 +++++++++++ src/revpimodio2/pictory.py | 2 ++ 5 files changed, 47 insertions(+) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index 0400650..cad4284 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -572,6 +572,8 @@ class Base(Device): class GatewayMixin: + """Mixin class providing piGate module detection functionality.""" + @property def leftgate(self) -> bool: """ diff --git a/src/revpimodio2/errors.py b/src/revpimodio2/errors.py index cd456f9..121ab3e 100644 --- a/src/revpimodio2/errors.py +++ b/src/revpimodio2/errors.py @@ -6,8 +6,12 @@ __license__ = "LGPLv2" class RevPiModIOError(Exception): + """Base exception class for RevPiModIO errors.""" + pass class DeviceNotFoundError(RevPiModIOError): + """Raised when a requested device cannot be found in the configuration.""" + pass diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index 5849d83..c155eed 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -493,6 +493,12 @@ class IOBase(object): return any(self._parentdevice._ba_devdata[self._slc_address]) def __call__(self, value=None): + """ + Get or set the IO value using function call syntax. + + :param value: If None, returns current value; otherwise sets the value + :return: Current IO value when called without arguments + """ if value is None: # Inline get_value() if self._bitshift: @@ -978,6 +984,13 @@ class IntIO(IOBase): ) def __call__(self, value=None): + """ + Get or set the integer IO value using function call syntax. + + :param value: If None, returns current integer value; otherwise sets the integer value + :return: Current IO value as integer when called without arguments + :raises TypeError: If value is not an integer + """ if value is None: # Inline get_intvalue() return int.from_bytes( @@ -1447,6 +1460,14 @@ class StructIO(IOBase): raise BufferError("registered value does not fit process image scope") def __call__(self, value=None): + """ + Get or set the structured IO value using function call syntax. + + Handles byte and word order conversion based on configuration. + + :param value: If None, returns current value unpacked using struct format; otherwise packs and sets the value + :return: Current IO value unpacked according to struct format when called without arguments + """ if value is None: # Inline get_structdefaultvalue() if self._bitshift: @@ -1567,6 +1588,13 @@ class MemIO(IOBase): """ def get_variantvalue(self): + """ + Get the default value as either string or integer based on bit length. + + For values > 64 bits, returns as decoded string. Otherwise returns as integer. + + :return: Default value as string (if > 64 bits) or integer + """ val = bytes(self._defaultvalue) if self._bitlength > 64: diff --git a/src/revpimodio2/modio.py b/src/revpimodio2/modio.py index 1fd9c71..ff7fa47 100644 --- a/src/revpimodio2/modio.py +++ b/src/revpimodio2/modio.py @@ -208,6 +208,12 @@ class RevPiModIO(object): self._myfh.close() def __enter__(self): + """ + Context manager entry (deprecated). + + .. deprecated:: + Use ``with revpi.io:`` or ``with revpi.device.my_device:`` instead. + """ # todo: Remove this context manager in future warnings.warn( "This context manager is deprecated and will be removed!\n\n" @@ -231,6 +237,11 @@ class RevPiModIO(object): return self def __exit__(self, exc_type, exc_val, exc_tb): + """ + Context manager exit (deprecated). + + Writes process image and performs cleanup. + """ if not self._monitoring: self.writeprocimg() self.exit(full=True) diff --git a/src/revpimodio2/pictory.py b/src/revpimodio2/pictory.py index fd7c55d..b8d1839 100644 --- a/src/revpimodio2/pictory.py +++ b/src/revpimodio2/pictory.py @@ -15,6 +15,8 @@ __license__ = "LGPLv2" class ProductType: + """Product type constants for Revolution Pi devices and modules.""" + CON_BT = 111 CON_CAN = 109 CON_MBUS = 110 From 3c56eed9785c9998f4617becf1ed92468526d15b Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:16:05 +0100 Subject: [PATCH 05/18] docs: translate German text in docstrings to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced German terms with English equivalents: - Zustand → state - vom → of the - Ausgang → output - Sekunden → seconds - toggeln → toggle - from → off - gruen → green - rot/root → red - blau → blue --- src/revpimodio2/device.py | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index cad4284..673d9f1 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -577,7 +577,7 @@ class GatewayMixin: @property def leftgate(self) -> bool: """ - Statusbit links vom RevPi is a piGate Modul angeschlossen. + Status bit indicating a piGate module is connected on the left side. :return: True if piGate left exists """ @@ -586,7 +586,7 @@ class GatewayMixin: @property def rightgate(self) -> bool: """ - Statusbit rechts vom RevPi is a piGate Modul angeschlossen. + Status bit indicating a piGate module is connected on the right side. :return: True if piGate right exists """ @@ -615,7 +615,7 @@ class ModularBase(Base): """ Manages writing the error limits. - :param slc_io: Byte Slice vom ErrorLimit + :param slc_io: Byte Slice of the ErrorLimit :return: Current ErrorLimit or None if not available """ if 0 <= errorlimit <= 65535: @@ -876,7 +876,7 @@ class Core(ModularBase, GatewayMixin): """ Returns the state of LED A1 from the Core. - :return: 0=from, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 0b00000011 = 3 return self._ba_devdata[self._slc_led.start] & 3 @@ -885,7 +885,7 @@ class Core(ModularBase, GatewayMixin): """ Returns the state of LED A2 from the Core. - :return: 0=from, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 0b00001100 = 12 return (self._ba_devdata[self._slc_led.start] & 12) >> 2 @@ -894,7 +894,7 @@ class Core(ModularBase, GatewayMixin): """ Sets the state of LED A1 from the Core. - :param value: 0=from, 1=gruen, 2=rot + :param value: 0=off, 1=green, 2=red """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -906,7 +906,7 @@ class Core(ModularBase, GatewayMixin): """ Sets the state of LED A2 from the Core. - :param value: 0=from, 1=gruen, 2=rot + :param value: 0=off, 1=green, 2=red """ if 0 <= value <= 3: self.a2green(bool(value & 1)) @@ -937,7 +937,7 @@ class Connect(Core): super(Connect, self).__setattr__(key, value) def __wdtoggle(self) -> None: - """WD Ausgang all 10 Sekunden automatisch toggeln.""" + """Automatically toggle WD output every 10 seconds.""" while not self.__evt_wdtoggle.wait(10): self.wd.value = not self.wd.value @@ -1004,9 +1004,9 @@ class Connect(Core): def _get_leda3(self) -> int: """ - Returns the Zustand the LED A3 vom Connect. + Returns the state of LED A3 of the Connect. - :return: 0=from, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 0b00110000 = 48 return (self._ba_devdata[self._slc_led.start] & 48) >> 4 @@ -1023,7 +1023,7 @@ class Connect(Core): """ Sets the state of LED A3 on the Connect. - :param: value 0=from, 1=gruen, 2=rot + :param: value 0=off, 1=green, 2=red """ if 0 <= value <= 3: self.a3green(bool(value & 1)) @@ -1282,9 +1282,9 @@ class ModularBaseConnect_4_5(ModularBase): def _get_leda1(self) -> int: """ - Returns the Zustand the LED A1 vom Connect. + Returns the state of LED A1 of the Connect. - :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ return self.__led_calculator(self._ba_devdata[self._slc_led.start] & 0b00000111) @@ -1292,32 +1292,32 @@ class ModularBaseConnect_4_5(ModularBase): """ Returns the state of LED A2 from the Core. - :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start] & 0b00111000) >> 3) def _get_leda3(self) -> int: """ - Returns the Zustand the LED A3 vom Core. + Returns the state of LED A3 of the Core. - :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ word_led = self._ba_devdata[self._slc_led] return self.__led_calculator((unpack("> 6) def _get_leda4(self) -> int: """ - Returns the Zustand the LED A4 vom Core. + Returns the state of LED A4 of the Core. - :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start + 1] & 0b00001110) >> 1) def _get_leda5(self) -> int: """ - Returns the Zustand the LED A5 vom Core. + Returns the state of LED A5 of the Core. - :return: 0=from, 1=gruen, 2=root, 4=blau, mixed RGB colors + :return: 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ return self.__led_calculator((self._ba_devdata[self._slc_led.start + 1] & 0b01110000) >> 4) @@ -1325,7 +1325,7 @@ class ModularBaseConnect_4_5(ModularBase): """ Sets the state of LED A1 on the Connect. - :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a1red(bool(value & 2)) @@ -1338,7 +1338,7 @@ class ModularBaseConnect_4_5(ModularBase): """ Sets the state of LED A2 on the Connect. - :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a2red(bool(value & 2)) @@ -1351,7 +1351,7 @@ class ModularBaseConnect_4_5(ModularBase): """ Sets the state of LED A3 on the Connect. - :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a3red(bool(value & 2)) @@ -1364,7 +1364,7 @@ class ModularBaseConnect_4_5(ModularBase): """ Sets the state of LED A4 on the Connect. - :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a4red(bool(value & 2)) @@ -1377,7 +1377,7 @@ class ModularBaseConnect_4_5(ModularBase): """ Sets the state of LED A5 on the Connect. - :param: value 0=from, 1=gruen, 2=rot, 4=blue, mixed RGB colors + :param: value 0=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a5red(bool(value & 2)) @@ -1555,18 +1555,18 @@ class Compact(Base): def _get_leda1(self) -> int: """ - Returns the Zustand the LED A1 vom Compact. + Returns the state of LED A1 of the Compact. - :return: 0=from, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 0b00000011 = 3 return self._ba_devdata[self._slc_led.start] & 3 def _get_leda2(self) -> int: """ - Returns the Zustand the LED A2 vom Compact. + Returns the state of LED A2 of the Compact. - :return: 0=from, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 0b00001100 = 12 return (self._ba_devdata[self._slc_led.start] & 12) >> 2 @@ -1575,7 +1575,7 @@ class Compact(Base): """ Sets the state of LED A1 on the Compact. - :param value: 0=from, 1=gruen, 2=rot + :param value: 0=off, 1=green, 2=red """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -1587,7 +1587,7 @@ class Compact(Base): """ Sets the state of LED A2 on the Compact. - :param value: 0=from, 1=gruen, 2=rot + :param value: 0=off, 1=green, 2=red """ if 0 <= value <= 3: self.a2green(bool(value & 1)) From e7d5bd9fdf9493759e60de8389da519e2b44bb0a Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:17:29 +0100 Subject: [PATCH 06/18] docs: fix last German text in io.py docstring Changed 'wenn Vorzeichenbehaftet' to 'if signed' --- src/revpimodio2/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index c155eed..375c8f2 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -1499,7 +1499,7 @@ class StructIO(IOBase): """ Retrieves whether the value should be treated as signed. - :return: True, wenn Vorzeichenbehaftet + :return: True if signed """ return self._signed From 4892adaeff2e866429d9ce3e216f68a603f36407 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:18:47 +0100 Subject: [PATCH 07/18] docs: improve watchdog toggle terminology and formatting Changed 'Autowatchdog' to 'automatic watchdog toggle' for clarity. Fixed inconsistent indentation in _set_wdtoggle docstring. --- src/revpimodio2/device.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index 673d9f1..cf2cfc8 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -1013,9 +1013,9 @@ class Connect(Core): def _get_wdtoggle(self) -> bool: """ - Retrieves the value for Autowatchdog. + Retrieves the automatic watchdog toggle status. - :return: True if autowatchdog is active + :return: True if automatic watchdog toggle is active """ return self.__th_wdtoggle is not None and self.__th_wdtoggle.is_alive() @@ -1033,14 +1033,14 @@ class Connect(Core): def _set_wdtoggle(self, value: bool) -> None: """ - Sets the value for autowatchdog. + Sets the automatic watchdog toggle. - If this value is set to True, the necessary bit to toggle the watchdog is switched between True and False every 10 seconds in the background. + If this value is set to True, the necessary bit to toggle the watchdog is switched between True and False every 10 seconds in the background. This bit is automatically written to the process image with autorefresh=True. IMPORTANT: If autorefresh=False, .writeprocimg() must be called cyclically to write the value to the process image!!! - :param value: True to activate, False to terminate + :param value: True to activate, False to terminate """ if self._modio._monitoring: raise RuntimeError("can not toggle watchdog, while system is in monitoring mode") From d4817adff1bf44b7469a166dd76be818df2a270b Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:22:38 +0100 Subject: [PATCH 08/18] docs: enable inherited members display by default Added :inherited-members: to autodoc_default_options in conf.py so all classes automatically show inherited methods and attributes from their base classes. This makes Connect5 show members from GatewayMixin and ModularBaseConnect_4_5, for example. --- docs/api/device.rst | 9 +++++++++ docs/conf.py | 1 + 2 files changed, 10 insertions(+) diff --git a/docs/api/device.rst b/docs/api/device.rst index 8d6e846..ce42a48 100644 --- a/docs/api/device.rst +++ b/docs/api/device.rst @@ -70,6 +70,7 @@ ModularBaseConnect_4_5 :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Base class for Connect 4 and Connect 5 modules. @@ -81,6 +82,7 @@ Core :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Revolution Pi Core module. @@ -92,6 +94,7 @@ Connect :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Revolution Pi Connect module. @@ -103,6 +106,7 @@ Connect4 :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Revolution Pi Connect 4 module. @@ -114,6 +118,7 @@ Connect5 :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Revolution Pi Connect 5 module. @@ -125,6 +130,7 @@ DioModule :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Digital I/O module. @@ -136,6 +142,7 @@ RoModule :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Relay output module. @@ -147,6 +154,7 @@ Gateway :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Gateway module (ModbusTCP, Profinet, etc.). @@ -158,6 +166,7 @@ Virtual :members: :undoc-members: :show-inheritance: + :inherited-members: :special-members: __init__ Virtual device for custom applications. diff --git a/docs/conf.py b/docs/conf.py index 513a578..87460c0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ autodoc_default_options = { 'member-order': 'bysource', 'special-members': '__init__', 'undoc-members': True, + 'inherited-members': True, 'exclude-members': '__weakref__' } From 7136a2b23a36b0ea922e7e30356eb92798c5f055 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 16:26:50 +0100 Subject: [PATCH 09/18] docs: use inline attribute docstrings for Sphinx compatibility Added inline docstring documentation for Device class attributes: - bmk, catalognr, comment, extend, guid, id, inpvariant, outvariant, type Added inline docstrings for LED and IO attributes in device classes: - Core: a1green, a1red, a2green, a2red, wd - Connect: a3green, a3red, x2in, x2out - ModularBaseConnect_4_5: a1-a5 red/green/blue - Connect4: x2in, x2out - Compact: a1green, a1red, a2green, a2red, wd - Flat: a1-a5 green/red, switch, relais, wd Translated remaining German text: - 'Statusbit for piControl-Treiber laeuft' -> 'Status bit indicating piControl driver is running' - 'Treiber laeuft' -> 'driver is running' - 'Inputs to Bus schreiben' -> 'Write inputs to bus' --- src/revpimodio2/device.py | 60 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index cf2cfc8..d2b0697 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -219,14 +219,23 @@ class Device(object): # Attach all remaining attributes to class self.bmk = dict_device.get("bmk", "") + """Component designator from piCtory configuration.""" self.catalognr = dict_device.get("catalogNr", "") + """Catalog number of the device (deprecated).""" self.comment = dict_device.get("comment", "") + """Comment text from piCtory configuration.""" self.extend = dict_device.get("extend", {}) + """Extended configuration data from piCtory.""" self.guid = dict_device.get("GUID", "") + """Global unique identifier of the device.""" self.id = dict_device.get("id", "") + """Device identifier from piCtory configuration.""" self.inpvariant = dict_device.get("inpVariant", 0) + """Input variant number.""" self.outvariant = dict_device.get("outVariant", 0) + """Output variant number.""" self.type = dict_device.get("type", "") + """Device position type string.""" # Perform special configuration from derived classes self._devconfigure() @@ -634,9 +643,9 @@ class ModularBase(Base): @property def picontrolrunning(self) -> bool: """ - Statusbit for piControl-Treiber laeuft. + Status bit indicating piControl driver is running. - :return: True, if Treiber laeuft + :return: True if driver is running """ return bool(int.from_bytes(self._ba_devdata[self._slc_statusbyte], byteorder="little") & 1) @@ -841,6 +850,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """LED A1 green.""" self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "1"], @@ -848,6 +858,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """LED A1 red.""" self.a2green = IOBase( self, ["core.a2green", 0, 1, self._slc_led.start, exp_a2green, None, "LED_A2_GREEN", "2"], @@ -855,6 +866,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """LED A2 green.""" self.a2red = IOBase( self, ["core.a2red", 0, 1, self._slc_led.start, exp_a2red, None, "LED_A2_RED", "3"], @@ -862,6 +874,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """LED A2 red.""" # Watchdog einrichten (Core=soft / Connect=soft/hard) self.wd = IOBase( @@ -871,6 +884,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """Watchdog bit.""" def _get_leda1(self) -> int: """ @@ -975,6 +989,7 @@ class Connect(Core): "little", False, ) + """LED A3 green.""" self.a3red = IOBase( self, ["core.a3red", 0, 1, self._slc_led.start, exp_a3red, None, "LED_A3_RED", "5"], @@ -982,6 +997,7 @@ class Connect(Core): "little", False, ) + """LED A3 red.""" # Create IO objects for WD and X2 in/out self.x2in = IOBase( @@ -991,6 +1007,7 @@ class Connect(Core): "little", False, ) + """Digital input on X2 connector.""" self.x2out = IOBase( self, ["core.x2out", 0, 1, self._slc_led.start, exp_x2out, None, "Connect_X2_OUT", "6"], @@ -998,6 +1015,7 @@ class Connect(Core): "little", False, ) + """Digital output on X2 connector.""" # Export hardware watchdog to use it with other systems self.wd._export = int(exp_wd) # Do this without mrk for export! @@ -1170,6 +1188,7 @@ class ModularBaseConnect_4_5(ModularBase): exp_a5blue = exp_a1red # Create actual IOs + """LED A1 red.""" self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "0"], @@ -1177,6 +1196,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A1 red.""" self.a1green = IOBase( self, ["core.a1green", 0, 1, self._slc_led.start, exp_a1green, None, "LED_A1_GREEN", "1"], @@ -1184,6 +1204,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A1 green.""" self.a1blue = IOBase( self, ["core.a1blue", 0, 1, self._slc_led.start, exp_a1blue, None, "LED_A1_BLUE", "2"], @@ -1191,6 +1212,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A1 blue.""" self.a2red = IOBase( self, @@ -1199,6 +1221,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A2 red.""" self.a2green = IOBase( self, ["core.a2green", 0, 1, self._slc_led.start, exp_a2green, None, "LED_A2_GREEN", "4"], @@ -1206,6 +1229,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A2 green.""" self.a2blue = IOBase( self, ["core.a2blue", 0, 1, self._slc_led.start, exp_a2blue, None, "LED_A2_BLUE", "5"], @@ -1213,6 +1237,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A2 blue.""" self.a3red = IOBase( self, @@ -1221,6 +1246,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A3 red.""" self.a3green = IOBase( self, ["core.a3green", 0, 1, self._slc_led.start, exp_a3green, None, "LED_A3_GREEN", "7"], @@ -1228,6 +1254,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A3 green.""" self.a3blue = IOBase( self, ["core.a3blue", 0, 1, self._slc_led.start, exp_a3blue, None, "LED_A3_BLUE", "8"], @@ -1235,6 +1262,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A3 blue.""" self.a4red = IOBase( self, @@ -1243,6 +1271,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A4 red.""" self.a4green = IOBase( self, ["core.a4green", 0, 1, self._slc_led.start, exp_a4green, None, "LED_A4_GREEN", "10"], @@ -1250,6 +1279,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A4 green.""" self.a4blue = IOBase( self, ["core.a4blue", 0, 1, self._slc_led.start, exp_a4blue, None, "LED_A4_BLUE", "11"], @@ -1257,6 +1287,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A4 blue.""" self.a5red = IOBase( self, @@ -1265,6 +1296,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A5 red.""" self.a5green = IOBase( self, ["core.a5green", 0, 1, self._slc_led.start, exp_a5green, None, "LED_A5_GREEN", "13"], @@ -1272,6 +1304,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A5 green.""" self.a5blue = IOBase( self, ["core.a5blue", 0, 1, self._slc_led.start, exp_a5blue, None, "LED_A5_BLUE", "14"], @@ -1279,6 +1312,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A5 blue.""" def _get_leda1(self) -> int: """ @@ -1457,6 +1491,7 @@ class Connect4(ModularBaseConnect_4_5): "little", False, ) + """Digital input on X2 connector.""" self.x2out = IOBase( self, ["core.x2out", 0, 1, self._slc_output.start, exp_x2out, None, "Connect_X2_OUT", "0"], @@ -1464,6 +1499,7 @@ class Connect4(ModularBaseConnect_4_5): "little", False, ) + """Digital output on X2 connector.""" class Compact(Base): @@ -1522,6 +1558,7 @@ class Compact(Base): "little", False, ) + """LED A1 green.""" self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "1"], @@ -1529,6 +1566,7 @@ class Compact(Base): "little", False, ) + """LED A1 red.""" self.a2green = IOBase( self, ["core.a2green", 0, 1, self._slc_led.start, exp_a2green, None, "LED_A2_GREEN", "2"], @@ -1536,6 +1574,7 @@ class Compact(Base): "little", False, ) + """LED A2 green.""" self.a2red = IOBase( self, ["core.a2red", 0, 1, self._slc_led.start, exp_a2red, None, "LED_A2_RED", "3"], @@ -1543,6 +1582,7 @@ class Compact(Base): "little", False, ) + """LED A2 red.""" # Software watchdog einrichten self.wd = IOBase( @@ -1552,6 +1592,7 @@ class Compact(Base): "little", False, ) + """Watchdog bit.""" def _get_leda1(self) -> int: """ @@ -1726,6 +1767,7 @@ class Flat(Base): "little", False, ) + """LED A1 green.""" self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "1"], @@ -1733,6 +1775,7 @@ class Flat(Base): "little", False, ) + """LED A1 red.""" self.a2green = IOBase( self, ["core.a2green", 0, 1, self._slc_led.start, exp_a2green, None, "LED_A2_GREEN", "2"], @@ -1740,6 +1783,7 @@ class Flat(Base): "little", False, ) + """LED A2 green.""" self.a2red = IOBase( self, ["core.a2red", 0, 1, self._slc_led.start, exp_a2red, None, "LED_A2_RED", "3"], @@ -1747,6 +1791,7 @@ class Flat(Base): "little", False, ) + """LED A2 red.""" self.a3green = IOBase( self, ["core.a3green", 0, 1, self._slc_led.start, exp_a3green, None, "LED_A3_GREEN", "4"], @@ -1754,6 +1799,7 @@ class Flat(Base): "little", False, ) + """LED A3 green.""" self.a3red = IOBase( self, ["core.a3red", 0, 1, self._slc_led.start, exp_a3red, None, "LED_A3_RED", "5"], @@ -1761,6 +1807,7 @@ class Flat(Base): "little", False, ) + """LED A3 red.""" self.a4green = IOBase( self, ["core.a4green", 0, 1, self._slc_led.start, exp_a4green, None, "LED_A4_GREEN", "6"], @@ -1768,6 +1815,7 @@ class Flat(Base): "little", False, ) + """LED A4 green.""" self.a4red = IOBase( self, ["core.a4red", 0, 1, self._slc_led.start, exp_a4red, None, "LED_A4_RED", "7"], @@ -1775,6 +1823,7 @@ class Flat(Base): "little", False, ) + """LED A4 red.""" self.a5green = IOBase( self, ["core.a5green", 0, 1, self._slc_led.start, exp_a5green, None, "LED_A5_GREEN", "8"], @@ -1782,6 +1831,7 @@ class Flat(Base): "little", False, ) + """LED A5 green.""" self.a5red = IOBase( self, ["core.a5red", 0, 1, self._slc_led.start, exp_a5red, None, "LED_A5_RED", "9"], @@ -1789,6 +1839,7 @@ class Flat(Base): "little", False, ) + """LED A5 red.""" # Real IO for switch lst_io = self._modio.io[self._slc_devoff][self._slc_switch.start] @@ -1800,6 +1851,7 @@ class Flat(Base): "little", False, ) + """Switch input.""" # Real IO for relais lst_io = self._modio.io[self._slc_devoff][self._slc_dout.start] @@ -1811,6 +1863,7 @@ class Flat(Base): "little", False, ) + """Relais output.""" # Software watchdog einrichten self.wd = IOBase( @@ -1820,6 +1873,7 @@ class Flat(Base): "little", False, ) + """Watchdog bit.""" def _get_leda1(self) -> int: """ @@ -2056,7 +2110,7 @@ class Virtual(Gateway): for io in self.get_inputs(): self._ba_devdata[io._slc_address] = io._defaultvalue - # Inputs to Bus schreiben + # Write inputs to bus self._modio._myfh_lck.acquire() try: self._modio._myfh.seek(self._slc_inpoff.start) From e8af56ff709737b8ca16644c863e58a34a4bdec1 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Tue, 3 Feb 2026 15:01:37 +0100 Subject: [PATCH 10/18] ci: add Read the Docs configuration --- .readthedocs.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..289f0af --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + extra_requirements: + - docs From c3380698331d6162a871a8acd257e6bb9246e579 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Thu, 5 Feb 2026 08:57:00 +0100 Subject: [PATCH 11/18] docs: enforce 100 character line width in docstrings Reformatted docstrings across device.py, helper.py, and io.py to honor the 100 character line width limit, improving readability and consistency with project formatting standards. --- src/revpimodio2/device.py | 23 ++++++++++++++++------- src/revpimodio2/helper.py | 22 ++++++++++++++++------ src/revpimodio2/io.py | 6 ++++-- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index d2b0697..c374994 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -1053,10 +1053,14 @@ class Connect(Core): """ Sets the automatic watchdog toggle. - If this value is set to True, the necessary bit to toggle the watchdog is switched between True and False every 10 seconds in the background. - This bit is automatically written to the process image with autorefresh=True. + If this value is set to True, the necessary bit to toggle the + watchdog is switched between True and False every 10 seconds in + the background. + This bit is automatically written to the process image with + autorefresh=True. - IMPORTANT: If autorefresh=False, .writeprocimg() must be called cyclically to write the value to the process image!!! + IMPORTANT: If autorefresh=False, .writeprocimg() must be called + cyclically to write the value to the process image!!! :param value: True to activate, False to terminate """ @@ -2046,9 +2050,11 @@ class Gateway(Device): """ Class for the RevPi Gateway-Devices. - Provides additional functions for the RevPi Gateway devices besides the functions from RevPiDevice. - Gateways are ready. IOs on this device provide the replace_io function, - which allows defining custom IOs that map to a RevPiStructIO object. + Provides additional functions for the RevPi Gateway devices besides + the functions from RevPiDevice. + Gateways are ready. IOs on this device provide the replace_io + function, which allows defining custom IOs that map to a + RevPiStructIO object. This IO type can process and return values via multiple bytes. :ref: :func:`revpimodio2.io.IntIOReplaceable.replace_io()` @@ -2096,7 +2102,10 @@ class Virtual(Gateway): """ Writes piCtory default input values for a virtual device. - If default values for inputs of a virtual device are specified in piCtory, these are only set at system startup or a piControl reset. If the process image is subsequently overwritten with NULL, these values will be lost. + If default values for inputs of a virtual device are specified + in piCtory, these are only set at system startup or a piControl + reset. If the process image is subsequently overwritten with NULL, + these values will be lost. This function can only be applied to virtual devices! :return: True if operations on the virtual device were successful diff --git a/src/revpimodio2/helper.py b/src/revpimodio2/helper.py index 676b9ad..53157ca 100644 --- a/src/revpimodio2/helper.py +++ b/src/revpimodio2/helper.py @@ -18,12 +18,20 @@ from .io import IOBase class EventCallback(Thread): """Thread for internal calling of event functions. - The event function that this thread calls will receive the thread itself as a parameter. This must be considered when defining the function, e.g., "def event(th):". For extensive functions, this can be evaluated to prevent duplicate starts. + The event function that this thread calls will receive the thread itself + as a parameter. This must be considered when defining the function, e.g., + "def event(th):". For extensive functions, this can be evaluated to + prevent duplicate starts. The name of the IO object can be retrieved via EventCallback.ioname, - which triggered the event. EventCallback.iovalue returns the value of the IO object at the time of triggering. - The thread provides the EventCallback.exit event as an abort condition for the called function. - By calling the EventCallback.stop() function, the exit event is set and can be used to abort loops. - A wait function can also be implemented with the .exit() event: "th.exit.wait(0.5)" - waits 500ms or aborts immediately if .stop() is called on the thread. + which triggered the event. EventCallback.iovalue returns the value of + the IO object at the time of triggering. + The thread provides the EventCallback.exit event as an abort condition + for the called function. + By calling the EventCallback.stop() function, the exit event is set + and can be used to abort loops. + A wait function can also be implemented with the .exit() event: + "th.exit.wait(0.5)" - waits 500ms or aborts immediately if .stop() + is called on the thread. while not th.exit.is_set(): # IO-Arbeiten @@ -363,7 +371,9 @@ class ProcimgWriter(Thread): """ Class for synchronization thread. - This class is started as a thread if the process image should be synchronized cyclically. This function is mainly used for event handling. + This class is started as a thread if the process image should be + synchronized cyclically. This function is mainly used for event + handling. """ __slots__ = ( diff --git a/src/revpimodio2/io.py b/src/revpimodio2/io.py index 375c8f2..130e9f6 100644 --- a/src/revpimodio2/io.py +++ b/src/revpimodio2/io.py @@ -1465,8 +1465,10 @@ class StructIO(IOBase): Handles byte and word order conversion based on configuration. - :param value: If None, returns current value unpacked using struct format; otherwise packs and sets the value - :return: Current IO value unpacked according to struct format when called without arguments + :param value: If None, returns current value unpacked using struct + format; otherwise packs and sets the value + :return: Current IO value unpacked according to struct format when + called without arguments """ if value is None: # Inline get_structdefaultvalue() From cd64dbcd1b241348a1705f730f2db83955c4c18b Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Thu, 12 Feb 2026 13:57:21 +0100 Subject: [PATCH 12/18] style: Remove redundant whitespaces from docstrings and comments Cleaned up excessive indentation and unnecessary spacing in docstrings and comments across multiple files to improve code readability and maintain consistency. Signed-off-by: Sven Sager --- src/revpimodio2/device.py | 20 +++++------- src/revpimodio2/helper.py | 6 ++-- src/revpimodio2/netio.py | 69 ++++++++++++++++++++------------------- 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/revpimodio2/device.py b/src/revpimodio2/device.py index c374994..7f24cbc 100644 --- a/src/revpimodio2/device.py +++ b/src/revpimodio2/device.py @@ -520,9 +520,6 @@ class Device(object): """ Read all inputs for this device from process image. - - Same see - :return: True if successfully executed :ref: :func:`revpimodio2.modio.RevPiModIO.readprocimg()` """ @@ -652,7 +649,7 @@ class ModularBase(Base): @property def unconfdevice(self) -> bool: """ - Status bit for an IO module not configured with piCtory. + Status bit for an IO module not configured with piCtory. :return: True if IO module is not configured """ @@ -661,7 +658,7 @@ class ModularBase(Base): @property def missingdeviceorgate(self) -> bool: """ - Status bit for an IO module missing or piGate configured. + Status bit for an IO module missing or piGate configured. :return: True if IO module is missing or piGate is configured """ @@ -746,7 +743,7 @@ class ModularBase(Base): """ Sets RS485 ErrorLimit1 to new value. - :param value: Neuer ErrorLimit1 value + :param value: New ErrorLimit1 value """ if self._slc_errorlimit1 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit1") @@ -771,7 +768,7 @@ class ModularBase(Base): """ Sets RS485 ErrorLimit2 to new value. - :param value: Neuer ErrorLimit2 value + :param value: New ErrorLimit2 value """ if self._slc_errorlimit2 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit2") @@ -1192,7 +1189,6 @@ class ModularBaseConnect_4_5(ModularBase): exp_a5blue = exp_a1red # Create actual IOs - """LED A1 red.""" self.a1red = IOBase( self, ["core.a1red", 0, 1, self._slc_led.start, exp_a1red, None, "LED_A1_RED", "0"], @@ -1511,7 +1507,7 @@ class Compact(Base): Class for the RevPi Compact. Provides functions for the LEDs. IOs are accessed via the .io - object zugegriffen. + object. """ __slots__ = ( @@ -1536,7 +1532,7 @@ class Compact(Base): """Prepare Core class.""" super()._devconfigure() - # Statische IO Verknüpfungen of the Compacts + # Link static IOs of the Compact self._slc_led = slice(23, 24) self._slc_temperature = slice(0, 1) self._slc_frequency = slice(1, 2) @@ -2100,7 +2096,7 @@ class Virtual(Gateway): def writeinputdefaults(self): """ - Writes piCtory default input values for a virtual device. + Writes piCtory default input values for a virtual device. If default values for inputs of a virtual device are specified in piCtory, these are only set at system startup or a piControl @@ -2108,7 +2104,7 @@ class Virtual(Gateway): these values will be lost. This function can only be applied to virtual devices! - :return: True if operations on the virtual device were successful + :return: True if operations on the virtual device were successful """ if self._modio._monitoring: raise RuntimeError("can not write process image, while system is in monitoring mode") diff --git a/src/revpimodio2/helper.py b/src/revpimodio2/helper.py index 53157ca..ba415a5 100644 --- a/src/revpimodio2/helper.py +++ b/src/revpimodio2/helper.py @@ -34,7 +34,7 @@ class EventCallback(Thread): is called on the thread. while not th.exit.is_set(): - # IO-Arbeiten + # Work with IOs th.exit.wait(0.5) """ @@ -45,7 +45,7 @@ class EventCallback(Thread): Init EventCallback class. :param func: Function that should be called at startup - :param name: IO-Name + :param name: IO name :param value: IO value at the time of the event """ super().__init__() @@ -612,7 +612,7 @@ class ProcimgWriter(Thread): else: self.__dict_delay[tup_fire] -= 1 if self.__dict_delay[tup_fire] <= 0: - # Accept and delete delayed event + # Put event to queue and delete delayed event if tup_fire[0].as_thread: self._eventqth.put(tup_fire, False) else: diff --git a/src/revpimodio2/netio.py b/src/revpimodio2/netio.py index 56253ee..0d56db9 100644 --- a/src/revpimodio2/netio.py +++ b/src/revpimodio2/netio.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + """RevPiModIO main class for network access.""" __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2023 Sven Sager" @@ -50,7 +51,7 @@ class ConfigChanged(Exception): class NetFH(Thread): """ - Network file handler for the process image. + Network file handler for the process image. This file-object-like object manages reading and writing of the process image via the network. A remote Revolution Pi can be controlled this way. @@ -131,7 +132,7 @@ class NetFH(Thread): def __check_acl(self, bytecode: bytes) -> None: """ - Checks if ACL allows the operation on RevPi. + Checks if ACL allows the operation on RevPi. If the operation is not permitted, the socket is immediately closed and an exception is thrown. @@ -151,7 +152,7 @@ class NetFH(Thread): def __set_systimeout(self, value: int) -> None: """ - System function for timeout calculation. + System function for timeout calculation. :param value: Timeout in milliseconds 100 - 60000 """ @@ -284,7 +285,7 @@ class NetFH(Thread): def clear_dirtybytes(self, position=None) -> None: """ - Removes the configured dirty bytes from the RevPi server. + Removes the configured dirty bytes from the RevPi server. This function does not throw an exception on transmission error, but triggers a reconnection. @@ -392,7 +393,7 @@ class NetFH(Thread): def get_closed(self) -> bool: """ - Check if connection is closed. + Check if connection is closed. :return: True if connection is closed """ @@ -400,7 +401,7 @@ class NetFH(Thread): def get_config_changed(self) -> bool: """ - Check if RevPi configuration was changed. + Check if RevPi configuration was changed. :return: True if RevPi configuration was changed """ @@ -408,7 +409,7 @@ class NetFH(Thread): def get_name(self) -> str: """ - Return connection name. + Return connection name. :return: IP:PORT """ @@ -416,7 +417,7 @@ class NetFH(Thread): def get_reconnecting(self) -> bool: """ - Internal reconnect active due to network errors. + Internal reconnect active due to network errors. :return: True if reconnect is active """ @@ -424,7 +425,7 @@ class NetFH(Thread): def get_timeout(self) -> int: """ - Returns current timeout. + Returns current timeout. :return: in milliseconds """ @@ -458,7 +459,7 @@ class NetFH(Thread): def read(self, length: int) -> bytes: """ - Read data via the network. + Read data via the network. :param length: Number of bytes :return: Read @@ -586,12 +587,12 @@ class NetFH(Thread): def set_dirtybytes(self, position: int, dirtybytes: bytes) -> None: """ - Configures dirty bytes for process image on connection error. + Configures dirty bytes for process image on connection error. - This function does not throw an exception on transmission error, - but triggers a reconnection. + This function does not throw an exception on transmission error, + but triggers a reconnection. - :param position: Start position for writing + :param position: Start position for writing :param dirtybytes: to be written """ if self.__config_changed: @@ -624,7 +625,7 @@ class NetFH(Thread): def set_timeout(self, value: int) -> None: """ - Sets timeout value for connection. + Sets timeout value for connection. :param value: Timeout in milliseconds """ @@ -644,9 +645,9 @@ class NetFH(Thread): def tell(self) -> int: """ - Returns aktuelle Position. + Returns actual position in file. - :return: Aktuelle Position + :return: Actual position in file """ if self.__config_changed: raise ConfigChanged("configuration on revolution pi was changed") @@ -656,7 +657,7 @@ class NetFH(Thread): def write(self, bytebuff: bytes) -> int: """ - Write data via the network. + Write data via the network. :param bytebuff: Bytes to write :return: Number of written bytes @@ -691,7 +692,7 @@ class NetFH(Thread): class RevPiNetIO(_RevPiModIO): """ - Class for managing the piCtory configuration via the network. + Class for managing the piCtory configuration via the network. This class takes over the entire configuration from piCtory and maps the devices and IOs. It takes over exclusive management of the @@ -714,7 +715,7 @@ class RevPiNetIO(_RevPiModIO): shared_procimg=False, ): """ - Instantiates the basic functions. + Instantiates the basic functions. :param address: IP address / (IP, Port) :param autorefresh: If True, add all devices to autorefresh @@ -723,8 +724,8 @@ class RevPiNetIO(_RevPiModIO): :param simulator: Loads the module as simulator and swaps IOs :param debug: Output complete messages for all errors :param replace_io_file: Load replace IO configuration from file - :param shared_procimg: Share process image with other processes, this - could be insecure for automation + :param shared_procimg: Share process image with other processes, this + could be insecure for automation """ check_ip = compile(r"^(25[0-5]|(2[0-4]|[01]?\d|)\d)(\.(25[0-5]|(2[0-4]|[01]?\d|)\d)){3}$") @@ -823,7 +824,7 @@ class RevPiNetIO(_RevPiModIO): def get_config_changed(self) -> bool: """ - Check if RevPi configuration was changed. + Check if RevPi configuration was changed. In this case, the connection is closed and RevPiNetIO must be reinstantiated. @@ -834,7 +835,7 @@ class RevPiNetIO(_RevPiModIO): def get_jconfigrsc(self) -> dict: """ - Loads the piCtory configuration and creates a . + Loads the piCtory configuration and creates a . :return: of the piCtory configuration """ @@ -845,7 +846,7 @@ class RevPiNetIO(_RevPiModIO): def get_reconnecting(self) -> bool: """ - Internal reconnect active due to network errors. + Internal reconnect active due to network errors. The module tries internally to reestablish the connection. No further action is needed. @@ -874,12 +875,12 @@ class RevPiNetIO(_RevPiModIO): def net_setdefaultvalues(self, device=None) -> None: """ - Configures the PLC server with the piCtory default values. + Configures the PLC server with the piCtory default values. These values are set on the RevPi if the connection is unexpectedly interrupted (network error). - :param device: Only apply to single device, otherwise to all + :param device: Only apply to single device, otherwise to all """ if self.monitoring: raise RuntimeError("can not send default values, while system is in monitoring mode") @@ -923,7 +924,7 @@ class RevPiNetIO(_RevPiModIO): class RevPiNetIOSelected(RevPiNetIO): """ - Class for managing individual devices from piCtory. + Class for managing individual devices from piCtory. This class only takes over specified devices from the piCtory configuration and maps them including IOs. It takes over exclusive management of the @@ -946,15 +947,15 @@ class RevPiNetIOSelected(RevPiNetIO): shared_procimg=False, ): """ - Instantiates the basic functions only for specified devices. + Instantiates the basic functions only for specified devices. - The parameter deviceselection can be a single + The parameter deviceselection can be a single device position / single device name or a list with multiple positions / names :param address: IP address / (IP, Port) :param deviceselection: Position number or device name - :ref: :func:`RevPiNetIO.__init__()` + :ref: :func:`RevPiNetIO.__init__()` """ super().__init__( address, @@ -1020,14 +1021,14 @@ class RevPiNetIODriver(RevPiNetIOSelected): shared_procimg=False, ): """ - Instantiates the basic functions. + Instantiates the basic functions. - Parameters 'monitoring' and 'simulator' are not available here, + Parameters 'monitoring' and 'simulator' are not available here, as these are set automatically. :param address: IP address / (IP, Port) :param virtdev: Virtual device or multiple as - :ref: :func:`RevPiModIO.__init__()` + :ref: :func:`RevPiModIO.__init__()` """ # Load parent with monitoring=False and simulator=True if type(virtdev) not in (list, tuple): From bae85e1b09d19e12a0a38b8071cd80f057b22fe6 Mon Sep 17 00:00:00 2001 From: Nicolai Buchwitz Date: Thu, 12 Feb 2026 15:02:10 +0100 Subject: [PATCH 13/18] docs: add docs extra dependencies and missing _static directory The Read the Docs build fails because the "docs" extra is referenced in .readthedocs.yaml but not defined in setup.py, so sphinx_rtd_theme is never installed. Signed-off-by: Nicolai Buchwitz --- docs/_static/.gitkeep | 0 setup.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 docs/_static/.gitkeep diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 6f6f3c7..89291fe 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,12 @@ setup( python_requires=">= 3.2", install_requires=[], + extras_require={ + "docs": [ + "sphinx", + "sphinx_rtd_theme", + ], + }, entry_points={}, platforms=["all"], From 3294c5e980efb0445c66d55627b918d3b94c043f Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Tue, 17 Feb 2026 12:05:58 +0100 Subject: [PATCH 14/18] docs: Update documentation for improved clarity and consistency Revised various sections across multiple documentation files to reflect updated methods (`run_plc` replacing manual setup with `cycleloop`) and adjust for new default parameters (e.g., `autorefresh`). Enhanced descriptions for timers, Cycletools usage, and new method explanations. Removed outdated or redundant examples and updated system requirements. Signed-off-by: Sven Sager --- docs/advanced.rst | 186 +++--------------------------------- docs/basics.rst | 9 +- docs/cyclic_programming.rst | 96 ++++++++----------- docs/event_programming.rst | 82 ++-------------- docs/index.rst | 3 +- docs/installation.rst | 5 +- docs/quickstart.rst | 4 +- 7 files changed, 71 insertions(+), 314 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 89695fe..f2b9b15 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -36,7 +36,6 @@ Gateway modules provide generic IOs (like ``Input_1``, ``Output_1``, etc.) that rpi.io.Input_1.replace_io( "temperature", # New IO name "h", # struct format: signed short - defaultvalue=0 # Default value ) # Use the custom IO by its new name @@ -70,6 +69,7 @@ Common format codes for ``replace_io`` (see `Python struct format characters 5: - ct.io.actuator.value = True - elif error < -5: - ct.io.actuator.value = False - - def on_setpoint_change(ioname, iovalue): - """Event handler for user setpoint changes.""" - print(f"New setpoint: {iovalue}") - # Access ct.var from event requires thread-safe approach - # In practice, use shared data structure or message queue - - def on_start(ioname, iovalue): - print("System started") - - def on_stop(ioname, iovalue): - print("System stopped") - - # Register user events - rpi.io.start_button.reg_event(on_start, edge=revpimodio2.RISING) - rpi.io.stop_button.reg_event(on_stop, edge=revpimodio2.RISING) - rpi.io.setpoint_input.reg_event(on_setpoint_change, delay=100) - - # Run cyclic loop in background - threading.Thread( - target=lambda: rpi.cycleloop(cyclic_control), - daemon=True - ).start() - - # Run event loop in main thread - rpi.handlesignalend() - rpi.mainloop() - -Event Triggers with Cyclic Processing --------------------------------------- - -Use events to trigger actions, cyclic for processing: - -.. code-block:: python - - import revpimodio2 - - rpi = revpimodio2.RevPiModIO(autorefresh=True) - - def cyclic_processor(ct): - """Process work queue.""" - if ct.first: - ct.var.work_queue = [] - - # Process queued work - if ct.var.work_queue: - item = ct.var.work_queue.pop(0) - process_item(item) - - def on_new_item(ioname, iovalue): - """Queue work from events.""" - # Note: Accessing ct.var from events requires synchronization - # This is a simplified example - print(f"New item queued from {ioname}") - - rpi.io.trigger1.reg_event(on_new_item, edge=revpimodio2.RISING) - rpi.io.trigger2.reg_event(on_new_item, edge=revpimodio2.RISING) - - rpi.cycleloop(cyclic_processor) - Performance Optimization ======================== @@ -487,23 +357,19 @@ Track and handle I/O errors: .. code-block:: python - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.maxioerrors = 10 # Exception after 10 errors + maxioerrors = 10 # Exception after 10 errors def main_cycle(ct): # Check error count periodically if ct.flank20c: - if rpi.ioerrors > 5: - print(f"Warning: {rpi.ioerrors} I/O errors detected") + if rpi.core.ioerrorcount > maxioerrors: + print(f"Warning: {rpi.core.ioerrorcount} I/O errors detected") ct.io.warning_led.value = True # Normal logic ct.io.output.value = ct.io.input.value - try: - rpi.cycleloop(main_cycle) - except RuntimeError as e: - print(f"I/O error threshold exceeded: {e}") + revpimodio2.run_plc(main_cycle) Best Practices ============== @@ -600,35 +466,6 @@ Document complex logic: # State machine implementation # ... -Testing -------- - -Test your code thoroughly: - -.. code-block:: python - - def test_temperature_control(ct): - """Test temperature control logic.""" - - if ct.first: - ct.var.cooling_active = False - ct.var.test_temp = 20.0 - - # Simulate temperature increase - if ct.var.test_temp < 80: - ct.var.test_temp += 0.5 - - # Test control logic - temp = ct.var.test_temp - - if temp > 75 and not ct.var.cooling_active: - assert ct.io.cooling.value == True - ct.var.cooling_active = True - - if temp < 65 and ct.var.cooling_active: - assert ct.io.cooling.value == False - ct.var.cooling_active = False - Logging ------- @@ -676,6 +513,7 @@ Always validate external inputs: """Validate setpoint range.""" if 0 <= iovalue <= 100: rpi.io.setpoint.value = iovalue + rpi.io.error_led.value = False else: print(f"Invalid setpoint: {iovalue}") rpi.io.error_led.value = True diff --git a/docs/basics.rst b/docs/basics.rst index 54a71ef..7181855 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -78,10 +78,11 @@ Adjust cycle time to match your needs: .. code-block:: python - rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi = revpimodio2.RevPiModIO() rpi.cycletime = 100 # Set to 100ms + rpi.autorefresh_all() -**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements. +**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements. Default values will fit most needs. Error Handling -------------- @@ -90,10 +91,10 @@ Configure I/O error threshold: .. code-block:: python - rpi.maxioerrors = 10 # Raise exception after 10 errors + maxioerrors = 10 # Raise exception after 10 errors # Check error count - if rpi.ioerrors > 5: + if rpi.core.ioerrorcount > maxioerrors: print("Warning: I/O errors detected") Core Objects diff --git a/docs/cyclic_programming.rst b/docs/cyclic_programming.rst index eeeeb6e..c37164e 100644 --- a/docs/cyclic_programming.rst +++ b/docs/cyclic_programming.rst @@ -42,8 +42,6 @@ Simple Cycle Loop import revpimodio2 - rpi = revpimodio2.RevPiModIO(autorefresh=True) - def main_cycle(ct: revpimodio2.Cycletools): """Execute each cycle.""" if ct.io.start_button.value: @@ -51,10 +49,17 @@ Simple Cycle Loop if ct.io.stop_button.value: ct.io.motor.value = False - rpi.cycleloop(main_cycle) + revpimodio2.run_plc(main_cycle) + + # .run_plc is a shortcut for: + # rpi = revpimodio2.RevPiModIO(autorefresh=True) + # rpi.handlesignalend() + # rpi.cycleloop(main_cycle) The ``main_cycle`` function is called repeatedly at the configured cycle time (typically 20-50ms). +**Info:** ``rpi.handlesignalend()`` + Understanding Cycle Time ------------------------- @@ -68,10 +73,9 @@ Adjust cycle time to match your needs: .. code-block:: python - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.cycletime = 100 # 100ms = 10 Hz + revpimodio2.run_plc(main_cycle, cycletime=100) # 100ms = 10 Hz -**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements. +**Important:** Faster cycle times consume more CPU but will detect fast changes of input values. Cycletools Object ================= @@ -109,8 +113,7 @@ Use ``ct.first`` and ``ct.last`` for setup and teardown: ct.io.motor.value = False print(f"Total cycles: {ct.var.counter}") - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.cycleloop(main_cycle) + revpimodio2.run_plc(main_cycle) Persistent Variables ==================== @@ -155,6 +158,7 @@ Detect input changes efficiently without storing previous values: print("Button released!") rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.handlesignalend() rpi.cycleloop(main_cycle) Edge types: @@ -176,17 +180,16 @@ Toggle flags alternate between True/False at regular intervals: .. code-block:: python def main_cycle(ct): - # Blink LED - flag5c alternates every 5 cycles - ct.io.blink_led.value = ct.flag5c + # Blink LED - flag5c alternates every cycle + ct.io.blink_led.value = ct.flag1c # Different blink rates - ct.io.fast_blink.value = ct.flag2c # Every 2 cycles + ct.io.fast_blink.value = ct.flag5c # Every 5 cycles ct.io.slow_blink.value = ct.flag20c # Every 20 cycles **Available toggle flags:** * ``ct.flag1c`` - Every cycle -* ``ct.flag2c`` - Every 2 cycles * ``ct.flag5c`` - Every 5 cycles * ``ct.flag10c`` - Every 10 cycles * ``ct.flag20c`` - Every 20 cycles @@ -218,12 +221,12 @@ Flank flags are True for exactly one cycle at regular intervals: Timers ====== -RevPiModIO provides three timer types based on PLC standards. All timers are specified in cycle counts. +RevPiModIO provides three timer types based on PLC standards. All timers are specified in cycle counts or milliseconds. On-Delay Timer (TON/TONC) -------------------------- -Output becomes True only after input is continuously True for specified cycles: +Output becomes True only after input is continuously True for specified cycles (use ton with milliseconds value instead of cycles): .. code-block:: python @@ -253,7 +256,7 @@ Output becomes True only after input is continuously True for specified cycles: Off-Delay Timer (TOF/TOFC) --------------------------- -Output stays True for specified cycles after input goes False: +Output stays True for specified cycles or milliseconds after input goes False (use tof with milliseconds value instead of cycles): .. code-block:: python @@ -280,7 +283,7 @@ Output stays True for specified cycles after input goes False: Pulse Timer (TP/TPC) -------------------- -Generates a one-shot pulse of specified duration: +Generates a one-shot pulse of specified duration (use tp with milliseconds value instead of cycles): .. code-block:: python @@ -305,25 +308,6 @@ Generates a one-shot pulse of specified duration: * Acknowledgment pulses * Retriggerable delays -Converting Time to Cycles --------------------------- - -Calculate cycles from desired time: - -.. code-block:: python - - # At 20ms cycle time: - # 1 second = 50 cycles - # 100ms = 5 cycles - # 2 seconds = 100 cycles - - def main_cycle(ct): - cycle_time_ms = rpi.cycletime - desired_time_ms = 1500 # 1.5 seconds - - cycles_needed = int(desired_time_ms / cycle_time_ms) - ct.set_tonc("my_delay", cycles_needed) - State Machines ============== @@ -345,28 +329,29 @@ Simple State Machine ct.io.yellow_led.value = False ct.io.red_led.value = False - # After 100 cycles (2s @ 20ms), go to yellow - ct.set_tonc("green_time", 100) - if ct.get_tonc("green_time"): + # After 2 seconds, go to yellow + ct.set_ton("green_time", 2000) + if ct.get_ton("green_time"): ct.var.state = "YELLOW" elif ct.var.state == "YELLOW": ct.io.green_led.value = False ct.io.yellow_led.value = True - ct.set_tonc("yellow_time", 25) # 500ms - if ct.get_tonc("yellow_time"): + ct.set_ton("yellow_time", 500) + if ct.get_ton("yellow_time"): ct.var.state = "RED" elif ct.var.state == "RED": ct.io.yellow_led.value = False ct.io.red_led.value = True - ct.set_tonc("red_time", 150) # 3s - if ct.get_tonc("red_time"): + ct.set_ton("red_time", 3000) + if ct.get_ton("red_time"): ct.var.state = "GREEN" rpi = revpimodio2.RevPiModIO(autorefresh=True) + rpi.handlesignalend() rpi.cycleloop(traffic_light) Complex State Machine @@ -396,8 +381,8 @@ Complex State Machine ct.io.yellow_led.value = True # 2-second startup delay - ct.set_tonc("startup", 100) - if ct.get_tonc("startup"): + ct.set_ton("startup", 2000) + if ct.get_ton("startup"): ct.var.state = "RUNNING" print("Running") @@ -422,8 +407,8 @@ Complex State Machine # State: STOPPING - Controlled shutdown elif ct.var.state == "STOPPING": # Coast motor for 1 second - ct.set_tofc("coast", 50) - ct.io.motor.value = ct.get_tofc("coast") + ct.set_tof("coast", 1000) + ct.io.motor.value = ct.get_tof("coast") if not ct.io.motor.value: ct.var.state = "IDLE" @@ -432,7 +417,7 @@ Complex State Machine # State: ERROR - Fault condition elif ct.var.state == "ERROR": ct.io.motor.value = False - ct.io.red_led.value = ct.flag2c # Blink red + ct.io.red_led.value = ct.flag5c # Blink red if ct.changed(ct.io.ack_button, edge=revpimodio2.RISING): if not ct.io.error_sensor.value: @@ -442,8 +427,7 @@ Complex State Machine if ct.last: print(f"Total production: {ct.var.production_count}") - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.cycleloop(machine_controller) + revpimodio.run_plc(machine_controller) Practical Examples ================== @@ -476,14 +460,17 @@ Temperature monitoring with hysteresis control: # Warning if too hot if temp > 85: - ct.io.warning_led.value = ct.flag2c # Blink + ct.core.a1green.value = False + ct.core.a1red.value = ct.flag5c # Blink + else: + ct.core.a1green.value = ct.flag5c # Blink + ct.core.a1red.value = False # Emergency shutdown if temp > 95: ct.io.emergency_shutdown.value = True - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.cycleloop(temperature_monitor) + revpimodio2.run_plc(temperature_monitor) Production Counter ------------------ @@ -520,8 +507,7 @@ Count production items with start/stop control: print(f"Final count: {ct.var.total_count}") ct.var.total_count = 0 - rpi = revpimodio2.RevPiModIO(autorefresh=True) - rpi.cycleloop(production_counter) + revpimodio2.run_plc(production_counter) Best Practices ============== @@ -544,7 +530,7 @@ Minimize processing time in each cycle: **Guidelines:** * Avoid blocking operations (network, file I/O) -* Use flank flags for expensive operations +* Use flank flags for expensive operations or even Threads * Keep cycle time ≥20ms for stability Use Appropriate Cycle Time diff --git a/docs/event_programming.rst b/docs/event_programming.rst index 0b21dad..0042f78 100644 --- a/docs/event_programming.rst +++ b/docs/event_programming.rst @@ -94,29 +94,6 @@ Register callbacks for IO value changes: * ``revpimodio2.FALLING`` - True to False transition * ``revpimodio2.BOTH`` - Any change (default) -Lambda Functions ----------------- - -Use lambda for simple callbacks: - -.. code-block:: python - - rpi = revpimodio2.RevPiModIO(autorefresh=True) - - # Simple lambda callback - rpi.io.button.reg_event( - lambda name, val: print(f"Button: {val}") - ) - - # Lambda with edge filter - rpi.io.start_button.reg_event( - lambda name, val: print("Started!"), - edge=revpimodio2.RISING - ) - - rpi.handlesignalend() - rpi.mainloop() - Multiple Events --------------- @@ -212,12 +189,15 @@ Debouncing with Edge Detection Timer Events ============ -Execute callbacks at regular intervals independent of IO changes: +The timer is started when the IO value changes and executes the passed +function - even if the IO value has changed in the meantime. If the +timer has not expired and the condition is met again, the timer is NOT +reset to the delay value or started a second time. .. code-block:: python def periodic_task(ioname, iovalue): - """Called every 500ms.""" + """Called after 500ms.""" print(f"Periodic task: {iovalue}") rpi = revpimodio2.RevPiModIO(autorefresh=True) @@ -255,8 +235,8 @@ Timer Event Parameters **Parameters:** -* ``interval`` - Milliseconds between calls -* ``prefire`` - If True, trigger immediately on registration +* ``interval`` - Delay in milliseconds. +* ``prefire`` - If True, trigger immediately after starting the mainloop. Threaded Events =============== @@ -425,29 +405,6 @@ Sensor Logging rpi.handlesignalend() rpi.mainloop() -Periodic Status Report ----------------------- - -.. code-block:: python - - import revpimodio2 - - rpi = revpimodio2.RevPiModIO(autorefresh=True) - - def status_report(ioname, iovalue): - """Print system status every 10 seconds.""" - print("=== Status Report ===") - print(f"Temperature: {rpi.core.temperature.value}°C") - print(f"CPU Frequency: {rpi.core.frequency.value} MHz") - print(f"IO Errors: {rpi.core.ioerrorcount.value}") - print() - - # Status report every 10 seconds - rpi.io.dummy.reg_timerevent(status_report, 10000, prefire=True) - - rpi.handlesignalend() - rpi.mainloop() - Threaded Data Processing ------------------------- @@ -515,19 +472,6 @@ For slow operations, use threaded events: rpi.io.trigger.reg_event(slow_task, as_thread=True) -Use Debouncing --------------- - -Always debounce mechanical inputs: - -.. code-block:: python - - # Good - Debounced button - rpi.io.button.reg_event(callback, delay=30) - - # Poor - No debounce (may trigger multiple times) - rpi.io.button.reg_event(callback) - Handle Errors Gracefully ------------------------- @@ -543,18 +487,6 @@ Protect callbacks from exceptions: print(f"Error in callback: {e}") rpi.io.output.value = False # Safe state -Check IO Existence ------------------- - -Verify IOs exist before registering events: - -.. code-block:: python - - if "optional_button" in rpi.io: - rpi.io.optional_button.reg_event(callback) - else: - print("Optional button not configured") - Clean Up Threads ---------------- diff --git a/docs/index.rst b/docs/index.rst index ce031ef..cdb3dc6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,13 +24,12 @@ Quick Example **Cyclic Programming**:: import revpimodio2 - rpi = revpimodio2.RevPiModIO(autorefresh=True) def main(ct): if ct.io.button.value: ct.io.led.value = True - rpi.cycleloop(main) + revpimodio2.run_plc(main) **Event-Driven Programming**:: diff --git a/docs/installation.rst b/docs/installation.rst index 856d032..b46cab1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -5,7 +5,7 @@ Installation System Requirements =================== -* Python 3.7 or higher +* Python 3.2 or higher * Revolution Pi hardware (Core, Core3, Connect, Compact, Flat) * piCtory configuration tool @@ -24,6 +24,9 @@ Log out and log back in for the group change to take effect. Installing RevPiModIO ===================== +RevPiModIO is preinstalled on your Revolution Pi. It is distributed as debian package and will be +updated by `apt`. + Using pip --------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6a8520d..a996169 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -65,8 +65,6 @@ For continuous operation, use a cyclic loop: import revpimodio2 - rpi = revpimodio2.RevPiModIO(autorefresh=True) - def main_cycle(ct: revpimodio2.Cycletools): """Called every cycle (default: 20-50ms).""" @@ -91,7 +89,7 @@ For continuous operation, use a cyclic loop: print("Program stopped") # Run cyclic loop - rpi.cycleloop(main_cycle) + revpimodio2.run_plc(main_cycle) Event-Driven Program -------------------- From 5e0463b09bfe79546938909e13b73bbca74d3190 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Wed, 18 Feb 2026 10:04:09 +0100 Subject: [PATCH 15/18] chore: Update `setup.py` and README with project URLs Added `Documentation` and `Source` links to `setup.py` under `project_urls`. Updated README with a link to the official documentation. Signed-off-by: Sven Sager --- README.md | 6 ++++++ setup.py | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 72a08d3..59649a7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # RevPiModIO +### Documentation: + +For a complete reference of all classes, methods, and functions, please see the +official documentation: +[https://revpimodio2.readthedocs.io/](https://revpimodio2.readthedocs.io/) + ### Python3 programming for RevolutionPi of KUNBUS GmbH. The module provides all devices and IOs from the piCtory configuration in diff --git a/setup.py b/setup.py index 89291fe..b189bdb 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,9 @@ with open("README.md") as fh: setup( name="revpimodio2", version=__version__, - packages=find_namespace_packages("src"), - package_dir={'': 'src'}, + package_dir={"": "src"}, include_package_data=True, - python_requires=">= 3.2", install_requires=[], extras_require={ @@ -30,10 +28,12 @@ setup( ], }, entry_points={}, - platforms=["all"], - url="https://revpimodio.org/", + project_urls={ + "Documentation": "https://revpimodio2.readthedocs.io/", + "Source": "https://github.com/naruxde/revpimodio2", + }, license="LGPLv2", author="Sven Sager", author_email="akira@narux.de", @@ -47,12 +47,11 @@ setup( "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: " - "GNU Lesser General Public License v2 (LGPLv2)", + "License :: OSI Approved :: " "GNU Lesser General Public License v2 (LGPLv2)", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", ], ) From ef07b6ce4ffc1e4a28d9f867cc70b1c0489f639a Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Wed, 18 Feb 2026 10:07:56 +0100 Subject: [PATCH 16/18] chore: Bump version to 2.8.1 Signed-off-by: Sven Sager --- src/revpimodio2/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/revpimodio2/__about__.py b/src/revpimodio2/__about__.py index dc6a336..cc03ee0 100644 --- a/src/revpimodio2/__about__.py +++ b/src/revpimodio2/__about__.py @@ -3,4 +3,4 @@ __author__ = "Sven Sager " __copyright__ = "Copyright (C) 2023 Sven Sager" __license__ = "LGPLv2" -__version__ = "2.8.0" +__version__ = "2.8.1" From 2544dcf38b53931a145132ad620c4ad59937f014 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Wed, 18 Feb 2026 15:32:04 +0100 Subject: [PATCH 17/18] chore: Add `sphinx-rtd-theme` to `requirements.txt` for documentation Signed-off-by: Sven Sager --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 2b87f61..0f7025c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ pytest-cov setuptools sphinx +sphinx-rtd-theme wheel # Runtime dependencies, must match install_requires in setup.py From 8a19959582817710fa411f58591a1eb192fa0489 Mon Sep 17 00:00:00 2001 From: Sven Sager Date: Wed, 18 Feb 2026 15:40:20 +0100 Subject: [PATCH 18/18] build: Add `docs/_build` to `make clean` target in Makefile Signed-off-by: Sven Sager --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 07dca0b..951e5b9 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,8 @@ clean: rm -rf build dist src/*.egg-info # PyInstaller created files rm -rf *.spec + # Documentation builds + rm -rf docs/_build distclean: clean # Virtual environment