WAGO Python WAGO PLC Library

User guide

This guide provides you with general information and practical examples to work with the python-wagoplc programming library.

Integration with VS Code Extension WAGO CC100

The following examples show how to create PLC application scripts and configuration. In order to actually run them, you’ll need a CC100 of the first generation (before 751-9402). In theory, transferring your project folder to the CC100 and running the following commands would be enough:

python -m pip install python-wagoplc
cd plc-application/
python main.py

Unfortunately, not all CC100 firmware versions have Python on board. Hence, a Docker image containing all requirements and a Python runtime for WAGO PLCs has been created. It is highly recommended to make use of the VS Code Extension WAGO CC100, which installs that Docker image for you, transfers all files and manages the Docker container; you are then able to control your application by using the operating-mode switch (OMS) and watching the status lights, like with CoDeSys.

The concept of PLC programming

PLC programming is standardised in IEC 61131-3. This library loosely adheres to that standard. Most importantly, a PLC is a multi-tasking system: the kernel alternates between multiple tasks, which appears to the user as if they were concurrently executed. A task can also be triggered once by an event, or periodically (which is currently not supported). A single task execution is called a cycle. It has an input image (read from the controller inputs) and an internal state and produces an output image (written to the controller outputs).

Each task has a specific cycle time that defines the interval between executions as well as a priority. Moreover, you can define a watchdog, which is the maximum time a task may take to execute before it is interrupted. A more advanced setting is the watchdog sensitivity. Each step adds a five percent tolerance to the watchdog time. See the docstring of the Task class for the allowed value ranges of these settings.

In this library, tasks are represented through Python functions: the function parameters are the input image, the return value (a dictionary) the output image. To keep track of the internal state, variables need to be defined outside of the function, passed into it and also returned from it. Example:

def task_function(di1, di2, state):
    # Do something with the variables
    state = 0
    do1 = True
    if di1:
        do1 = False
        state = 1

    # Return output and state variable
    # Key is variable name
    return dict(do1=do1, state=state)

For how to define the variables, see the examples below.

One-script PLC application: bottle filling plant

At the bottle filling plant, the filled bottles are transferred to waiting crates using conveyor belts. The full crates are afterwards loaded onto freight trains. To minimize the risk of delays or complications in this vital area, the plant contains a bottle buffer with a fixed capacity. Light barriers at the entrance and exit count the incoming and outgoing bottles. If the number of bottles exceeds the threshold, the motor is turned off.

The following steps show how to create the program for this plant using the WAGO PLC 751-9301. You can find the whole source code in this script.

main.py

This script is the centerpiece of the PLC application, written by the programmer. It is directly invoked by the runtime. Thus, it must contain a call to the library’s main function. Initially, you’ll want to define a few variables for later use. To access controller I/O, you can use wrapper classes. Example using an object of the wagoplc.tasks.Tasks class for collection of variables and tasks:

from wagoplc import main, Tasks, DI, AO
from wagoplc.fb import CTUD

tasks = Tasks()

@tasks.setup
def setup():
    # Bind variable light_barrier_in to digital output 1
    light_barrier_in = DI(1)
    light_barrier_out = DI(2)
    motor = AO(1)
    # up-counter function block as per IEC 61131-3
    bottle_counter = CTUD(pv=150)

    return locals()

The return locals() statement creates a dictionary of variable names and values and returns it. It is collected by the setup decorator and saved in the Tasks object.

Now, you write the actual program as a Python function, which represents a PLC task. Task-related settings like the name and cycle time can be defined via another decorator:

@tasks.register(
        name = "bottle buffer",
        # Cycle time of 5ms
        cycle_ms = 5
)
def bottle_buffer(light_barrier_in, light_barrier_out, bottle_counter: CTUD):
    # count up/down if a rising edge of the input variable is registered
    bottle_counter(cu=light_barrier_in, cd=light_barrier_out)
    # set to 5 V (5000 mV)
    motor = 5000
    # Counter has reached the threshold
    if bottle_counter.qu:
        # Turn motor off
        motor = 0

    # Return the output image
    return dict(motor=motor, bottle_counter=bottle_counter)

In the output image, the motor variable is mapped to an analog output and its value is written after every cycle, while the bottle_counter is considered a state variable. It is passed into the task function unchanged before the next cycle and therefore needs to be defined as a parameter.

controller.yaml

This file holds the application’s configuration. It is required to contain the itemNumber field with the controller’s item number. Based on this information, the library selects the correct controller:

itemNumber: 751-9301

With that, your application would be complete. But wait, there’s more! You can also make use of the config file to define I/O mapping, state variables, and tasks, which keeps your main.py script slim:

io_mapping:
  # Module name; for the CC100 controllers, the item number
  751-9301:
    # input image
    pii:
      di1: light_barrier_in
      di2: light_barrier_out
    # output image
    piq:
      ao1: motor
# state variables
vars:
  - name: bottle_counter
    fb: CTUD
tasks:
  - name: bottle buffer
    # Script entry point
    entry: main.bottle_buffer
    # Cycle time
    cycle_ms: 10
    priority:
    sensitivity:
    watchdog_ms:

Using this config, your main.py script would look like this:

from wagoplc import main
from wagoplc.fb import CTUD

def bottle_buffer(light_barrier_in, light_barrier_out, bottle_counter: CTUD):
    bottle_counter(cu=light_barrier_in, cd=light_barrier_out, pv=150)
    motor = 5000
    if bottle_counter.qu:
        motor = 0

    return dict(motor=motor, bottle_counter=bottle_counter)

if __name__ == "__main__":
    main()

Using own function blocks: factory gate control

A function block (fb) is a kind of module that takes a fixed set of input variables to determine a fixed set of output variables. The programmer operates on instances of an fb in order to retain the internal state. In the example above, you already used an up-counter function block from the standard library. With python-wagoplc, you’re also able to define your own function blocks for more complex setups.

Imagine having a large assembly hall with multiple entrance gates you need to control. Each gate has an open and a close button as well as two limit switches. Rather than duplicating the necessary code for each gate, you could employ a function block.

As always, your first step is to think about and define the variables and tasks you’ll need, here in controller.yaml:

itemNumber: 751-9301
  751-9301:
    pii:
      di1: open_btn
      di2: close_btn
      di3: limit_switch_left
      di4: limit_switch_right
    piq:
      do1: motor_open
      do2: motor_close
vars:
  - name: gate_control_fb
    # Retrieve class 'Gate_Control' from module 'gate_control.py'
    fb: gate_control.Gate_Control
tasks:
  - name: gate control
    entry: main.porta_westfalica
    cycle_ms: 20
    priority:
    sensitivity:
    watchdog_ms:

Next thing is to write main.py. In the following example, the input variables are collected and passed into a function block called gate_control_fb:

from gate_control import Gate_Control

def porta_westfalica(
        gate_control_fb: Gate_Control, limit_switch_left,
        limit_switch_right, open_btn, close_btn):
    # Call to the function block
    gate_control_fb(open_btn, close_btn, limit_switch_left, limit_switch_right)
    print(gate_control_fb.open, gate_control_fb.closed)

    # Motors controlled through output variables
    return dict(gate_control_fb=gate_control_fb,
                motor_open=gate_control_fb.motor_open,
                motor_close=gate_control_fb.motor_close)

:::{note} When you define your variables in the script, you can bind an instance of your fb to the gate_control_fb variable directly (which is what the library does, internally). :::

Now to the function block, which according to the above code is a class Gate_Control defined in a module gate_control.py (in the same directory as main.py and controller.yaml). A function block in its current, simple form consists of a constructor setting any instance variables, and a __call__() method containing the actual functionality, which makes the instance callable:

# Function block superclass
from wagoplc.fb import FB

class Gate_Control(FB):

    def __init__(self):
        self.open = False
        self.closed = True
        self.motor_open = False
        self.motor_close = False
        self.state = 0
    
    def __call__(self, open_btn: bool, close_btn: bool, 
        limit_switch_left: bool, limit_switch_right: bool):
        match self.state:
            case 0: # Gate closed
                if open_btn:
                    self.motor_open = True
                    self.closed = False
                    self.state = 1
            case 1: # Gate opens
                if not limit_switch_right:
                    self.motor_open = False
                    self.open = True
                    self.state = 2
            case 2: # Gate is open
                if close_btn:
                    self.motor_close = True
                    self.open = False
                    self.state = 3
            case 3: # Gate closes
                if not limit_switch_left:
                    self.motor_close = False
                    self.closed = True
                    self.state = 0