mirror of
https://github.com/naruxde/revpimodio2.git
synced 2026-03-31 15:08:09 +02:00
Created topic-based documentation: - basics.rst: core concepts and fundamental usage - cyclic_programming.rst: PLC-style programming with Cycletools - event_programming.rst: event-driven patterns and callbacks - advanced.rst: gateway IOs, replace_io_file, watchdog management - installation.rst and quickstart.rst: getting started guides Added complete API reference in docs/api/: - All device classes including ModularBaseConnect_4_5 and GatewayMixin - I/O, helper, and main class documentation Enhanced Sphinx configuration with RTD theme and improved autodoc settings. Removed auto-generated modules.rst and revpimodio2.rst.
605 lines
15 KiB
ReStructuredText
605 lines
15 KiB
ReStructuredText
==================
|
|
Cyclic Programming
|
|
==================
|
|
|
|
Cyclic programming executes a function at regular intervals, similar to PLC programming.
|
|
|
|
.. contents:: Contents
|
|
:local:
|
|
:depth: 2
|
|
|
|
When to Use Cyclic Programming
|
|
===============================
|
|
|
|
**Cyclic programming is ideal for:**
|
|
|
|
* Deterministic timing requirements
|
|
* Traditional PLC-style logic
|
|
* State machines
|
|
* Time-critical control
|
|
* Continuous monitoring and control
|
|
|
|
**Advantages:**
|
|
|
|
* Predictable timing
|
|
* Simple mental model
|
|
* Easy to reason about program flow
|
|
* Natural for control systems
|
|
|
|
**Considerations:**
|
|
|
|
* Consumes CPU even when idle
|
|
* Cycle time affects responsiveness
|
|
* Must keep cycle logic fast
|
|
|
|
Basic Structure
|
|
===============
|
|
|
|
Simple Cycle Loop
|
|
-----------------
|
|
|
|
.. code-block:: python
|
|
|
|
import revpimodio2
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
|
|
def main_cycle(ct: revpimodio2.Cycletools):
|
|
"""Execute each cycle."""
|
|
if ct.io.start_button.value:
|
|
ct.io.motor.value = True
|
|
if ct.io.stop_button.value:
|
|
ct.io.motor.value = False
|
|
|
|
rpi.cycleloop(main_cycle)
|
|
|
|
The ``main_cycle`` function is called repeatedly at the configured cycle time (typically 20-50ms).
|
|
|
|
Understanding Cycle Time
|
|
-------------------------
|
|
|
|
The cycle time determines execution frequency:
|
|
|
|
* **Core 1**: 40ms (25 Hz)
|
|
* **Core3/Connect**: 20ms (50 Hz)
|
|
* **NetIO**: 50ms (20 Hz)
|
|
|
|
Adjust cycle time to match your needs:
|
|
|
|
.. code-block:: python
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycletime = 100 # 100ms = 10 Hz
|
|
|
|
**Important:** Faster cycle times consume more CPU. Choose the slowest cycle time that meets your requirements.
|
|
|
|
Cycletools Object
|
|
=================
|
|
|
|
The ``Cycletools`` object is passed to your cycle function, providing access to:
|
|
|
|
* ``ct.io`` - All IOs
|
|
* ``ct.core`` - System control
|
|
* ``ct.device`` - Device access
|
|
* ``ct.var`` - Persistent variables
|
|
* Lifecycle flags (``first``, ``last``)
|
|
* Timing flags (``flag5c``, ``flank10c``, etc.)
|
|
* Timer functions (``set_tonc``, ``get_tofc``, etc.)
|
|
* Change detection (``changed``)
|
|
|
|
Initialization and Cleanup
|
|
===========================
|
|
|
|
Use ``ct.first`` and ``ct.last`` for setup and teardown:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct: revpimodio2.Cycletools):
|
|
if ct.first:
|
|
# Initialize on first cycle
|
|
ct.var.counter = 0
|
|
ct.var.state = "IDLE"
|
|
print("System started")
|
|
|
|
# Main logic runs every cycle
|
|
ct.var.counter += 1
|
|
|
|
if ct.last:
|
|
# Cleanup before exit
|
|
ct.io.motor.value = False
|
|
print(f"Total cycles: {ct.var.counter}")
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(main_cycle)
|
|
|
|
Persistent Variables
|
|
====================
|
|
|
|
Use ``ct.var`` to store variables that persist between cycles:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
if ct.first:
|
|
ct.var.counter = 0
|
|
ct.var.state = "IDLE"
|
|
ct.var.accumulator = 0.0
|
|
|
|
# Variables persist between cycles
|
|
ct.var.counter += 1
|
|
ct.var.accumulator += ct.io.sensor.value
|
|
|
|
# Access variables later
|
|
average = ct.var.accumulator / ct.var.counter
|
|
|
|
Variables defined in ``ct.var`` maintain their values across all cycle executions.
|
|
|
|
Change Detection
|
|
================
|
|
|
|
Detect input changes efficiently without storing previous values:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct: revpimodio2.Cycletools):
|
|
# Detect any change
|
|
if ct.changed(ct.io.sensor):
|
|
print(f"Sensor changed to: {ct.io.sensor.value}")
|
|
|
|
# Detect rising edge (button press)
|
|
if ct.changed(ct.io.button, edge=revpimodio2.RISING):
|
|
print("Button pressed!")
|
|
|
|
# Detect falling edge (button release)
|
|
if ct.changed(ct.io.button, edge=revpimodio2.FALLING):
|
|
print("Button released!")
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(main_cycle)
|
|
|
|
Edge types:
|
|
|
|
* ``revpimodio2.RISING`` - False to True transition
|
|
* ``revpimodio2.FALLING`` - True to False transition
|
|
* ``revpimodio2.BOTH`` - Any change (default)
|
|
|
|
Timing Flags
|
|
============
|
|
|
|
Built-in timing flags provide periodic execution without manual counting.
|
|
|
|
Toggle Flags
|
|
------------
|
|
|
|
Toggle flags alternate between True/False at regular intervals:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
# Blink LED - flag5c alternates every 5 cycles
|
|
ct.io.blink_led.value = ct.flag5c
|
|
|
|
# Different blink rates
|
|
ct.io.fast_blink.value = ct.flag2c # Every 2 cycles
|
|
ct.io.slow_blink.value = ct.flag20c # Every 20 cycles
|
|
|
|
**Available toggle flags:**
|
|
|
|
* ``ct.flag1c`` - Every cycle
|
|
* ``ct.flag2c`` - Every 2 cycles
|
|
* ``ct.flag5c`` - Every 5 cycles
|
|
* ``ct.flag10c`` - Every 10 cycles
|
|
* ``ct.flag20c`` - Every 20 cycles
|
|
|
|
Flank Flags
|
|
-----------
|
|
|
|
Flank flags are True for exactly one cycle at regular intervals:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
# Execute task every 10 cycles
|
|
if ct.flank10c:
|
|
print(f"Runtime: {ct.runtime:.3f}s")
|
|
|
|
# Execute task every 20 cycles
|
|
if ct.flank20c:
|
|
temp = ct.io.temperature.value
|
|
print(f"Temperature: {temp}°C")
|
|
|
|
**Available flank flags:**
|
|
|
|
* ``ct.flank5c`` - True every 5 cycles
|
|
* ``ct.flank10c`` - True every 10 cycles
|
|
* ``ct.flank15c`` - True every 15 cycles
|
|
* ``ct.flank20c`` - True every 20 cycles
|
|
|
|
Timers
|
|
======
|
|
|
|
RevPiModIO provides three timer types based on PLC standards. All timers are specified in cycle counts.
|
|
|
|
On-Delay Timer (TON/TONC)
|
|
--------------------------
|
|
|
|
Output becomes True only after input is continuously True for specified cycles:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
# Input: sensor value
|
|
ct.set_tonc("delay", 10)
|
|
|
|
# Output goes high after input is high for 10 cycles
|
|
if ct.get_tonc("delay"):
|
|
ct.io.output.value = True
|
|
else:
|
|
ct.io.output.value = False
|
|
|
|
**How it works:**
|
|
|
|
1. Input goes True
|
|
2. Timer starts counting
|
|
3. If input stays True for 10 cycles, output goes True
|
|
4. If input goes False before 10 cycles, timer resets
|
|
|
|
**Use cases:**
|
|
|
|
* Button debouncing
|
|
* Startup delays
|
|
* Confirming sustained conditions
|
|
|
|
Off-Delay Timer (TOF/TOFC)
|
|
---------------------------
|
|
|
|
Output stays True for specified cycles after input goes False:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
# Input: button value
|
|
ct.set_tofc("motor_coast", 20)
|
|
|
|
# Motor continues for 20 cycles after button release
|
|
ct.io.motor.value = ct.get_tofc("motor_coast")
|
|
|
|
**How it works:**
|
|
|
|
1. Input is True, output is True
|
|
2. Input goes False
|
|
3. Output stays True for 20 more cycles
|
|
4. After 20 cycles, output goes False
|
|
|
|
**Use cases:**
|
|
|
|
* Motor coast-down
|
|
* Relay hold-in
|
|
* Graceful shutdowns
|
|
|
|
Pulse Timer (TP/TPC)
|
|
--------------------
|
|
|
|
Generates a one-shot pulse of specified duration:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
# Trigger pulse on button press
|
|
if ct.changed(ct.io.trigger, edge=revpimodio2.RISING):
|
|
ct.set_tpc("pulse", 5)
|
|
|
|
# Output is True for 5 cycles
|
|
ct.io.pulse_output.value = ct.get_tpc("pulse")
|
|
|
|
**How it works:**
|
|
|
|
1. Call ``set_tpc`` to trigger pulse
|
|
2. Output is True for 5 cycles
|
|
3. After 5 cycles, output goes False
|
|
4. Additional triggers during pulse are ignored
|
|
|
|
**Use cases:**
|
|
|
|
* One-shot operations
|
|
* Acknowledgment pulses
|
|
* Retriggerable delays
|
|
|
|
Converting Time to Cycles
|
|
--------------------------
|
|
|
|
Calculate cycles from desired time:
|
|
|
|
.. code-block:: python
|
|
|
|
# At 20ms cycle time:
|
|
# 1 second = 50 cycles
|
|
# 100ms = 5 cycles
|
|
# 2 seconds = 100 cycles
|
|
|
|
def main_cycle(ct):
|
|
cycle_time_ms = rpi.cycletime
|
|
desired_time_ms = 1500 # 1.5 seconds
|
|
|
|
cycles_needed = int(desired_time_ms / cycle_time_ms)
|
|
ct.set_tonc("my_delay", cycles_needed)
|
|
|
|
State Machines
|
|
==============
|
|
|
|
State machines implement complex control logic with distinct operational modes.
|
|
|
|
Simple State Machine
|
|
---------------------
|
|
|
|
.. code-block:: python
|
|
|
|
def traffic_light(ct: revpimodio2.Cycletools):
|
|
"""Traffic light controller."""
|
|
|
|
if ct.first:
|
|
ct.var.state = "GREEN"
|
|
|
|
if ct.var.state == "GREEN":
|
|
ct.io.green_led.value = True
|
|
ct.io.yellow_led.value = False
|
|
ct.io.red_led.value = False
|
|
|
|
# After 100 cycles (2s @ 20ms), go to yellow
|
|
ct.set_tonc("green_time", 100)
|
|
if ct.get_tonc("green_time"):
|
|
ct.var.state = "YELLOW"
|
|
|
|
elif ct.var.state == "YELLOW":
|
|
ct.io.green_led.value = False
|
|
ct.io.yellow_led.value = True
|
|
|
|
ct.set_tonc("yellow_time", 25) # 500ms
|
|
if ct.get_tonc("yellow_time"):
|
|
ct.var.state = "RED"
|
|
|
|
elif ct.var.state == "RED":
|
|
ct.io.yellow_led.value = False
|
|
ct.io.red_led.value = True
|
|
|
|
ct.set_tonc("red_time", 150) # 3s
|
|
if ct.get_tonc("red_time"):
|
|
ct.var.state = "GREEN"
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(traffic_light)
|
|
|
|
Complex State Machine
|
|
----------------------
|
|
|
|
.. code-block:: python
|
|
|
|
def machine_controller(ct: revpimodio2.Cycletools):
|
|
"""Multi-state machine controller."""
|
|
|
|
if ct.first:
|
|
ct.var.state = "IDLE"
|
|
ct.var.production_count = 0
|
|
|
|
# State: IDLE - Ready to start
|
|
if ct.var.state == "IDLE":
|
|
ct.io.motor.value = False
|
|
ct.io.green_led.value = True
|
|
ct.io.red_led.value = False
|
|
|
|
if ct.changed(ct.io.start_button, edge=revpimodio2.RISING):
|
|
ct.var.state = "STARTING"
|
|
print("Starting...")
|
|
|
|
# State: STARTING - Startup sequence
|
|
elif ct.var.state == "STARTING":
|
|
ct.io.yellow_led.value = True
|
|
|
|
# 2-second startup delay
|
|
ct.set_tonc("startup", 100)
|
|
if ct.get_tonc("startup"):
|
|
ct.var.state = "RUNNING"
|
|
print("Running")
|
|
|
|
# State: RUNNING - Normal operation
|
|
elif ct.var.state == "RUNNING":
|
|
ct.io.motor.value = True
|
|
ct.io.yellow_led.value = False
|
|
ct.io.green_led.value = ct.flag5c # Blink
|
|
|
|
# Count production
|
|
if ct.changed(ct.io.sensor, edge=revpimodio2.RISING):
|
|
ct.var.production_count += 1
|
|
|
|
# Check for stop
|
|
if ct.io.stop_button.value:
|
|
ct.var.state = "STOPPING"
|
|
|
|
# Check for error
|
|
if ct.io.error_sensor.value:
|
|
ct.var.state = "ERROR"
|
|
|
|
# State: STOPPING - Controlled shutdown
|
|
elif ct.var.state == "STOPPING":
|
|
# Coast motor for 1 second
|
|
ct.set_tofc("coast", 50)
|
|
ct.io.motor.value = ct.get_tofc("coast")
|
|
|
|
if not ct.io.motor.value:
|
|
ct.var.state = "IDLE"
|
|
print("Stopped")
|
|
|
|
# State: ERROR - Fault condition
|
|
elif ct.var.state == "ERROR":
|
|
ct.io.motor.value = False
|
|
ct.io.red_led.value = ct.flag2c # Blink red
|
|
|
|
if ct.changed(ct.io.ack_button, edge=revpimodio2.RISING):
|
|
if not ct.io.error_sensor.value:
|
|
ct.var.state = "IDLE"
|
|
print("Error cleared")
|
|
|
|
if ct.last:
|
|
print(f"Total production: {ct.var.production_count}")
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(machine_controller)
|
|
|
|
Practical Examples
|
|
==================
|
|
|
|
Temperature Control
|
|
-------------------
|
|
|
|
Temperature monitoring with hysteresis control:
|
|
|
|
.. code-block:: python
|
|
|
|
def temperature_monitor(ct: revpimodio2.Cycletools):
|
|
"""Monitor temperature and control cooling."""
|
|
|
|
if ct.first:
|
|
ct.var.cooling_active = False
|
|
|
|
temp = ct.io.temperature.value
|
|
|
|
# Hysteresis: ON at 75°C, OFF at 65°C
|
|
if temp > 75 and not ct.var.cooling_active:
|
|
ct.io.cooling_fan.value = True
|
|
ct.var.cooling_active = True
|
|
print(f"Cooling ON: {temp}°C")
|
|
|
|
elif temp < 65 and ct.var.cooling_active:
|
|
ct.io.cooling_fan.value = False
|
|
ct.var.cooling_active = False
|
|
print(f"Cooling OFF: {temp}°C")
|
|
|
|
# Warning if too hot
|
|
if temp > 85:
|
|
ct.io.warning_led.value = ct.flag2c # Blink
|
|
|
|
# Emergency shutdown
|
|
if temp > 95:
|
|
ct.io.emergency_shutdown.value = True
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(temperature_monitor)
|
|
|
|
Production Counter
|
|
------------------
|
|
|
|
Count production items with start/stop control:
|
|
|
|
.. code-block:: python
|
|
|
|
def production_counter(ct: revpimodio2.Cycletools):
|
|
"""Track production count."""
|
|
|
|
if ct.first:
|
|
ct.var.total_count = 0
|
|
ct.var.running = False
|
|
|
|
# Start/stop control
|
|
if ct.changed(ct.io.start_button, edge=revpimodio2.RISING):
|
|
ct.var.running = True
|
|
|
|
if ct.changed(ct.io.stop_button, edge=revpimodio2.RISING):
|
|
ct.var.running = False
|
|
|
|
# Count items
|
|
if ct.var.running:
|
|
if ct.changed(ct.io.item_sensor, edge=revpimodio2.RISING):
|
|
ct.var.total_count += 1
|
|
ct.set_tpc("count_pulse", 5) # Pulse LED
|
|
print(f"Item #{ct.var.total_count}")
|
|
|
|
ct.io.count_led.value = ct.get_tpc("count_pulse")
|
|
|
|
# Reset counter
|
|
if ct.changed(ct.io.reset_button, edge=revpimodio2.RISING):
|
|
print(f"Final count: {ct.var.total_count}")
|
|
ct.var.total_count = 0
|
|
|
|
rpi = revpimodio2.RevPiModIO(autorefresh=True)
|
|
rpi.cycleloop(production_counter)
|
|
|
|
Best Practices
|
|
==============
|
|
|
|
Keep Cycle Logic Fast
|
|
----------------------
|
|
|
|
Minimize processing time in each cycle:
|
|
|
|
.. code-block:: python
|
|
|
|
def optimized_cycle(ct):
|
|
# Heavy work only when needed
|
|
if ct.flank100c:
|
|
heavy_calculation()
|
|
|
|
# Keep cycle logic minimal
|
|
ct.io.output.value = ct.io.input.value
|
|
|
|
**Guidelines:**
|
|
|
|
* Avoid blocking operations (network, file I/O)
|
|
* Use flank flags for expensive operations
|
|
* Keep cycle time ≥20ms for stability
|
|
|
|
Use Appropriate Cycle Time
|
|
---------------------------
|
|
|
|
Match cycle time to application requirements:
|
|
|
|
.. code-block:: python
|
|
|
|
# Fast control (motion, high-speed counting)
|
|
rpi.cycletime = 20 # 50 Hz
|
|
|
|
# Normal control (most applications)
|
|
rpi.cycletime = 50 # 20 Hz
|
|
|
|
# Slow monitoring (temperature, status)
|
|
rpi.cycletime = 100 # 10 Hz
|
|
|
|
Handle Errors Safely
|
|
--------------------
|
|
|
|
Always implement safe failure modes:
|
|
|
|
.. code-block:: python
|
|
|
|
def safe_cycle(ct):
|
|
try:
|
|
value = ct.io.sensor.value
|
|
ct.io.output.value = process(value)
|
|
except Exception as e:
|
|
print(f"Error: {e}")
|
|
ct.io.output.value = False # Safe state
|
|
|
|
Initialize Properly
|
|
-------------------
|
|
|
|
Use ``ct.first`` for all initialization:
|
|
|
|
.. code-block:: python
|
|
|
|
def main_cycle(ct):
|
|
if ct.first:
|
|
# Initialize all variables
|
|
ct.var.counter = 0
|
|
ct.var.state = "IDLE"
|
|
ct.var.last_value = 0
|
|
|
|
# Set initial outputs
|
|
ct.io.motor.value = False
|
|
|
|
See Also
|
|
========
|
|
|
|
* :doc:`basics` - Core concepts and configuration
|
|
* :doc:`event_programming` - Event-driven programming
|
|
* :doc:`advanced` - Advanced topics and examples
|
|
* :doc:`api/helper` - Cycletools API reference
|