================== 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