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 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 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/docs/_static/.gitkeep b/docs/_static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/advanced.rst b/docs/advanced.rst new file mode 100644 index 0000000..f2b9b15 --- /dev/null +++ b/docs/advanced.rst @@ -0,0 +1,549 @@ +======== +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 + ) + + # 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) +* ``'d'`` - float (64-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") + rpi.io.Input_2.replace_io("humidity", "h") + 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 + + [humidity] + replace = Input_2 + frm = h + + [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. If you +use the software watchdog will will only works if you use RevPiPyControl as runtime for your python +program. + +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 + + 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 + + revpimodio2.run_plc(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() + +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 + + maxioerrors = 10 # Exception after 10 errors + + def main_cycle(ct): + # Check error count periodically + if ct.flank20c: + 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 + + revpimodio2.run_plc(main_cycle) + +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 + # ... + +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 + rpi.io.error_led.value = False + 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..ce42a48 --- /dev/null +++ b/docs/api/device.rst @@ -0,0 +1,172 @@ +============== +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: + :inherited-members: + :special-members: __init__ + + Base class for Connect 4 and Connect 5 modules. + +Core +==== + +.. autoclass:: Core + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Revolution Pi Core module. + +Connect +======= + +.. autoclass:: Connect + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Revolution Pi Connect module. + +Connect4 +======== + +.. autoclass:: Connect4 + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Revolution Pi Connect 4 module. + +Connect5 +======== + +.. autoclass:: Connect5 + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Revolution Pi Connect 5 module. + +DioModule +========= + +.. autoclass:: DioModule + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Digital I/O module. + +RoModule +======== + +.. autoclass:: RoModule + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Relay output module. + +Gateway +======= + +.. autoclass:: Gateway + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :special-members: __init__ + + Gateway module (ModbusTCP, Profinet, etc.). + +Virtual +======= + +.. autoclass:: Virtual + :members: + :undoc-members: + :show-inheritance: + :inherited-members: + :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..7181855 --- /dev/null +++ b/docs/basics.rst @@ -0,0 +1,332 @@ +====== +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() + 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. Default values will fit most needs. + +Error Handling +-------------- + +Configure I/O error threshold: + +.. code-block:: python + + maxioerrors = 10 # Raise exception after 10 errors + + # Check error count + if rpi.core.ioerrorcount > maxioerrors: + 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..87460c0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,19 +15,74 @@ 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, + 'inherited-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..c37164e --- /dev/null +++ b/docs/cyclic_programming.rst @@ -0,0 +1,590 @@ +================== +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 + + 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 + + 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 +------------------------- + +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 + + revpimodio2.run_plc(main_cycle, cycletime=100) # 100ms = 10 Hz + +**Important:** Faster cycle times consume more CPU but will detect fast changes of input values. + +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}") + + revpimodio2.run_plc(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.handlesignalend() + 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 cycle + ct.io.blink_led.value = ct.flag1c + + # Different blink rates + 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.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 or milliseconds. + +On-Delay Timer (TON/TONC) +-------------------------- + +Output becomes True only after input is continuously True for specified cycles (use ton with milliseconds value instead of 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 or milliseconds after input goes False (use tof with milliseconds value instead of cycles): + +.. 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 (use tp with milliseconds value instead of cycles): + +.. 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 + +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 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_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_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 +---------------------- + +.. 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_ton("startup", 2000) + if ct.get_ton("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_tof("coast", 1000) + ct.io.motor.value = ct.get_tof("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.flag5c # 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}") + + revpimodio.run_plc(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.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 + + revpimodio2.run_plc(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 + + revpimodio2.run_plc(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 or even Threads +* 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..0042f78 --- /dev/null +++ b/docs/event_programming.rst @@ -0,0 +1,516 @@ +==================== +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) + +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 +============ + +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 after 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`` - Delay in milliseconds. +* ``prefire`` - If True, trigger immediately after starting the mainloop. + +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() + +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) + +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 + +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..cdb3dc6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,86 @@ -.. 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 + + def main(ct): + if ct.io.button.value: + ct.io.led.value = True + + revpimodio2.run_plc(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..b46cab1 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,88 @@ +============ +Installation +============ + +System Requirements +=================== + +* Python 3.2 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 +===================== + +RevPiModIO is preinstalled on your Revolution Pi. It is distributed as debian package and will be +updated by `apt`. + +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..a996169 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,205 @@ +========== +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 + + 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 + revpimodio2.run_plc(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: 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 diff --git a/setup.py b/setup.py index 6f6f3c7..b189bdb 100644 --- a/setup.py +++ b/setup.py @@ -16,18 +16,24 @@ 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={ + "docs": [ + "sphinx", + "sphinx_rtd_theme", + ], + }, 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", @@ -41,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", ], ) 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" 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..446e9c7 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,15 +8,15 @@ 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 + :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: @@ -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..7f24cbc 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,40 +217,49 @@ 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", "") + """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.""" - # 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 +273,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 +306,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 +327,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 +352,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 +365,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 +373,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 +383,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 +396,140 @@ 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 +546,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 +570,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__ = () @@ -572,30 +578,32 @@ class Base(Device): class GatewayMixin: + """Mixin class providing piGate module detection functionality.""" + @property def leftgate(self) -> bool: """ - Statusbit links vom RevPi ist ein piGate Modul angeschlossen. + Status bit indicating a piGate module is connected on the left side. - :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. + Status bit indicating a piGate module is connected on the right side. - :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 +619,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 + :param slc_io: Byte Slice of the ErrorLimit + :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 +631,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. + Status bit indicating piControl driver is running. - :return: True, wenn Treiber laeuft + :return: True if driver is running """ 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 +689,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 +702,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 +715,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 +728,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 +741,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: New ErrorLimit1 value """ if self._slc_errorlimit1 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit1") @@ -745,9 +753,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 +766,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: New ErrorLimit2 value """ if self._slc_errorlimit2 is None: raise RuntimeError("selected core item in piCtory does not support errorlimit2") @@ -772,25 +780,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 +826,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 +839,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"], @@ -839,6 +847,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"], @@ -846,6 +855,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"], @@ -853,6 +863,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"], @@ -860,6 +871,7 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """LED A2 red.""" # Watchdog einrichten (Core=soft / Connect=soft/hard) self.wd = IOBase( @@ -869,30 +881,31 @@ class Core(ModularBase, GatewayMixin): "little", False, ) + """Watchdog bit.""" 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=off, 1=green, 2=red """ # 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=off, 1=green, 2=red """ # 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=off, 1=green, 2=red """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -902,9 +915,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=off, 1=green, 2=red """ if 0 <= value <= 3: self.a2green(bool(value & 1)) @@ -921,32 +934,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.""" + """Automatically toggle WD output every 10 seconds.""" 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 +978,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"], @@ -973,6 +986,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"], @@ -980,8 +994,9 @@ class Connect(Core): "little", False, ) + """LED A3 red.""" - # 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"], @@ -989,6 +1004,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"], @@ -996,32 +1012,33 @@ 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! def _get_leda3(self) -> int: """ - Gibt den Zustand der LED A3 vom Connect zurueck. + Returns the state of LED A3 of the Connect. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 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 automatic watchdog toggle status. - :return: True, wenn Autowatchdog aktiv ist + :return: True if automatic watchdog toggle 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=off, 1=green, 2=red """ if 0 <= value <= 3: self.a3green(bool(value & 1)) @@ -1031,18 +1048,18 @@ class Connect(Core): def _set_wdtoggle(self, value: bool) -> None: """ - Setzt den Wert fuer Autowatchdog. + Sets the automatic watchdog toggle. - 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 +1070,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 +1102,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 +1137,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 +1150,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 +1188,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"], @@ -1179,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"], @@ -1186,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"], @@ -1193,6 +1212,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A1 blue.""" self.a2red = IOBase( self, @@ -1201,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"], @@ -1208,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"], @@ -1215,6 +1237,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A2 blue.""" self.a3red = IOBase( self, @@ -1223,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"], @@ -1230,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"], @@ -1237,6 +1262,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A3 blue.""" self.a4red = IOBase( self, @@ -1245,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"], @@ -1252,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"], @@ -1259,6 +1287,7 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A4 blue.""" self.a5red = IOBase( self, @@ -1267,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"], @@ -1274,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"], @@ -1281,53 +1312,54 @@ class ModularBaseConnect_4_5(ModularBase): "little", False, ) + """LED A5 blue.""" def _get_leda1(self) -> int: """ - Gibt den Zustand der LED A1 vom Connect zurueck. + Returns the state of LED A1 of the Connect. - :return: 0=aus, 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) 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=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: """ - Gibt den Zustand der LED A3 vom Core zurueck. + Returns the state of LED A3 of the Core. - :return: 0=aus, 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: """ - Gibt den Zustand der LED A4 vom Core zurueck. + Returns the state of LED A4 of the Core. - :return: 0=aus, 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: """ - Gibt den Zustand der LED A5 vom Core zurueck. + Returns the state of LED A5 of the Core. - :return: 0=aus, 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) 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=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a1red(bool(value & 2)) @@ -1338,9 +1370,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=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a2red(bool(value & 2)) @@ -1351,9 +1383,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=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a3red(bool(value & 2)) @@ -1364,9 +1396,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=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a4red(bool(value & 2)) @@ -1377,9 +1409,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=off, 1=green, 2=red, 4=blue, mixed RGB colors """ if 0 <= value <= 7: self.a5red(bool(value & 2)) @@ -1403,18 +1435,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 +1455,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 +1464,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 +1483,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"], @@ -1459,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"], @@ -1466,14 +1499,15 @@ class Connect4(ModularBaseConnect_4_5): "little", False, ) + """Digital output on X2 connector.""" 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. """ __slots__ = ( @@ -1488,22 +1522,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 + # Link static IOs of the Compact 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 +1550,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"], @@ -1524,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"], @@ -1531,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"], @@ -1538,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"], @@ -1545,6 +1582,7 @@ class Compact(Base): "little", False, ) + """LED A2 red.""" # Software watchdog einrichten self.wd = IOBase( @@ -1554,30 +1592,31 @@ class Compact(Base): "little", False, ) + """Watchdog bit.""" def _get_leda1(self) -> int: """ - Gibt den Zustand der LED A1 vom Compact zurueck. + Returns the state of LED A1 of the Compact. - :return: 0=aus, 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: """ - Gibt den Zustand der LED A2 vom Compact zurueck. + Returns the state of LED A2 of the Compact. - :return: 0=aus, 1=gruen, 2=rot + :return: 0=off, 1=green, 2=red """ # 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=off, 1=green, 2=red """ if 0 <= value <= 3: self.a1green(bool(value & 1)) @@ -1587,9 +1626,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=off, 1=green, 2=red """ if 0 <= value <= 3: self.a2green(bool(value & 1)) @@ -1607,9 +1646,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 +1659,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 +1672,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 +1700,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 +1721,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 +1759,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"], @@ -1728,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"], @@ -1735,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"], @@ -1742,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"], @@ -1749,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"], @@ -1756,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"], @@ -1763,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"], @@ -1770,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"], @@ -1777,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"], @@ -1784,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"], @@ -1791,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] @@ -1802,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] @@ -1813,6 +1863,7 @@ class Flat(Base): "little", False, ) + """Relais output.""" # Software watchdog einrichten self.wd = IOBase( @@ -1822,6 +1873,7 @@ class Flat(Base): "little", False, ) + """Watchdog bit.""" def _get_leda1(self) -> int: """ @@ -1936,9 +1988,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 +2001,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 +2013,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 +2044,14 @@ 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 +2060,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 +2074,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 +2096,15 @@ 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 +2115,7 @@ class Virtual(Gateway): for io in self.get_inputs(): self._ba_devdata[io._slc_address] = io._defaultvalue - # Inputs auf Bus schreiben + # Write inputs to bus self._modio._myfh_lck.acquire() try: self._modio._myfh.seek(self._slc_inpoff.start) 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/helper.py b/src/revpimodio2/helper.py index 0938cef..ba415a5 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,25 +16,25 @@ 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 + # Work with IOs th.exit.wait(0.5) """ @@ -44,9 +44,9 @@ class EventCallback(Thread): """ Init EventCallback class. - :param func: Funktion die beim Start aufgerufen werden soll - :param name: IO-Name - :param value: IO-Value zum Zeitpunkt des Events + :param func: Function that should be called at startup + :param name: IO name + :param value: IO value at the time of the event """ super().__init__() self.daemon = True @@ -56,35 +56,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 +127,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 +136,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 +166,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 +176,7 @@ class Cycletools: else: self.__dict_tp[tp][0] = -1 - # Flankenmerker + # Edge flags self.flank5c = False self.flank10c = False self.flank15c = False @@ -188,7 +186,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 +237,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 +303,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 +315,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 +345,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 +369,11 @@ 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 +407,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 +433,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 +452,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 +462,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 +478,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 +495,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 +510,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 +535,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 +556,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 +565,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 +599,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 +612,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 + # Put event to queue and delete delayed event if tup_fire[0].as_thread: self._eventqth.put(tup_fire, False) else: @@ -633,18 +631,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..130e9f6 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) @@ -495,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: @@ -508,17 +512,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 +530,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 +555,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 +580,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 +588,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 +608,7 @@ class IOBase(object): def _get_iotype(self) -> int: """ - Gibt io type zurueck. + Returns io type. :return: io type """ @@ -631,10 +635,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 +649,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 +661,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 +689,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 +708,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 +765,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 +813,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 +828,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 +837,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 +898,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 +936,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 +959,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 +973,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], @@ -981,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( @@ -1006,15 +1016,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 +1036,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 +1046,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 +1066,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 +1091,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 +1107,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 +1116,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 +1127,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 +1155,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 +1319,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 +1345,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 +1362,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 +1399,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 +1424,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 +1439,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 +1452,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 @@ -1450,6 +1460,16 @@ 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: @@ -1471,17 +1491,17 @@ 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 + :return: True if signed """ return self._signed @@ -1504,9 +1524,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 +1539,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 +1547,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 +1562,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,14 +1582,21 @@ 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): + """ + 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 b6828de..ff7fa47 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,18 +196,24 @@ 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: 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) @@ -239,10 +250,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 +263,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 +296,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 +335,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 +358,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 +392,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 +404,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 +416,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 +425,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 +439,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 +454,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 +474,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 +500,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 +568,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 +581,7 @@ class RevPiModIO(object): def _create_myfh(self): """ - Erstellt FileObject mit Pfad zum procimg. + Creates FileObject with path to procimg. :return: FileObject """ @@ -582,15 +592,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 +618,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 +636,7 @@ class RevPiModIO(object): """ Getter function. - :return: Aktuelle Anzahl gezaehlter Fehler + :return: Current number of counted errors """ return self._ioerror @@ -634,7 +644,7 @@ class RevPiModIO(object): """ Getter function. - :return: Laenge in Bytes der Devices + :return: Length in bytes of the devices """ return self._length @@ -642,7 +652,7 @@ class RevPiModIO(object): """ Getter function. - :return: Anzahl erlaubte Fehler + :return: Number of allowed errors """ return self._maxioerrors @@ -650,7 +660,7 @@ class RevPiModIO(object): """ Getter function. - :return: True, wenn als Monitoring gestartet + :return: True if started as monitoring """ return self._monitoring @@ -658,15 +668,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 +684,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 +716,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 +727,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 +750,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 +761,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 +790,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 +812,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)) + raise RuntimeError("registered function '{0}' is 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,18 +876,18 @@ 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 + # 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) - # 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 +896,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 +912,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 +932,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,29 +950,29 @@ 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() 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) @@ -974,14 +981,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 +1025,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,83 +1061,81 @@ 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)) + raise RuntimeError("registered function '{0}' is not callable".format(cleanupfunc)) self.__cleanupfunc = cleanupfunc signal(SIGINT, self.__evt_exit) signal(SIGTERM, self.__evt_exit) 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 +1153,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 +1190,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 +1229,7 @@ class RevPiModIO(object): ) mylist = [dev] - # Daten komplett einlesen + # Read data completely self._myfh_lck.acquire() try: self._myfh.seek(0) @@ -1238,14 +1242,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 +1257,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 +1285,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 +1328,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 +1368,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 +1406,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 +1430,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 +1483,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 +1504,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..0d56db9 100644 --- a/src/revpimodio2/netio.py +++ b/src/revpimodio2/netio.py @@ -1,5 +1,6 @@ # -*- 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 +18,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 +38,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 +84,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 +109,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 +152,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 +193,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 +201,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 +218,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 +226,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 +285,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 +305,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 +322,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 +333,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 +354,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 +380,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 +393,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 +417,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 +451,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 +459,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 +501,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 +517,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 +532,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 +573,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 +587,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 +612,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 +625,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,9 +645,9 @@ class NetFH(Thread): def tell(self) -> int: """ - Gibt aktuelle Position zurueck. + 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") @@ -657,10 +657,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 +672,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: Buffer length and then flush? return len(bytebuff) @@ -692,14 +692,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 +715,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 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 + 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 +747,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 +771,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 +789,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 +808,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 +824,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 +846,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 +875,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 +897,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 +924,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,14 +947,14 @@ 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 + :param address: IP address / (IP, Port) + :param deviceselection: Position number or device name :ref: :func:`RevPiNetIO.__init__()` """ super().__init__( @@ -1002,12 +1001,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 +1021,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 + :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 +1060,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/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 diff --git a/src/revpimodio2/summary.py b/src/revpimodio2/summary.py index 7e7fafa..51e34da 100644 --- a/src/revpimodio2/summary.py +++ b/src/revpimodio2/summary.py @@ -1,20 +1,20 @@ # -*- 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 + :param summary: piCtory summary information """ self.inptotal = summary.get("inpTotal", -1) self.outtotal = summary.get("outTotal", -1) 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)