Skip to content

GPIO Datatype

Stephen Chryn edited this page Aug 3, 2026 · 1 revision

GPIO Datatype

The GPIO datatype maps EtherNet/IP tags to physical hardware pins on a Raspberry Pi or Orange Pi. It supports digital input, digital output, and PWM output modes.

Hardware requirement: GPIO requires either RPi.GPIO (Raspberry Pi) or OPi.GPIO (Orange Pi) to be installed. If neither is available, the datatype is registered but its type_validator will raise a TypeError on any attempt to use it.

Platform Support

Library Platform Pin naming
RPi.GPIO Raspberry Pi Integer BCM pin number (e.g. 17)
OPi.GPIO Orange Pi SUNXI string name (e.g. "PA1")

The correct library is detected automatically at import time. The pin mode is set to BCM on RPi and SUNXI on OPi.

Tag Definition

GPIO tags are registered like any other tag. The default value is a 3-tuple: (pin, default, mode).

("O_LED",    actions.type.GPIO((17,    False, "out"))),  # RPi digital output, BCM pin 17
("I_BUTTON", actions.type.GPIO((27,    False, "in"))),   # RPi digital input,  BCM pin 27
("O_FAN",    actions.type.GPIO((18,    0.0,   "pwm"))),  # RPi PWM output,      BCM pin 18
("O_RELAY",  actions.type.GPIO(("PA1", False, "out"))),  # OPi digital output,  SUNXI PA1

Default tuple fields

Field Type (RPi) Type (OPi) Description
pin int (≥ 0) str (SUNXI name) The physical pin to bind.
default bool bool Initial value for in/out modes.
default float float Initial duty cycle (0–100) for pwm mode.
mode str str One of "in", "out", or "pwm".

Expanded Subtags

Each GPIO tag expands into four primitive subtags:

Subtag Type Description
<name>.PIN DINT (RPi) / STRING (OPi) The pin identifier.
<name>.VALUE DINT (in/out) / REAL (pwm) The current value or duty cycle.
<name>.MODE STRING The pin mode: "in", "out", or "pwm".
<name>.SETUP BOOL False until the pin has been configured; then True.

Writing to <name>.VALUE is the primary way to drive output pins — the on_set_hook translates the write into a GPIO.output() or PWM.ChangeDutyCycle() call automatically.

Runtime API

All methods are accessed via actions.gpio.

setup(name_prefix)

Configures the physical pin if it has not already been set up (i.e. <name>.SETUP is False). This is called automatically by on_set_hook before every write, so manual calls are rarely needed.

actions.gpio.setup("O_LED")

write_pin(name_prefix, value, key?)

Writes a boolean value to <name>.VALUE, which triggers on_set_hook and drives the physical pin.

actions.gpio.write_pin("O_LED", True)   # turn on
actions.gpio.write_pin("O_LED", False)  # turn off

For PWM tags, write a duty cycle as a float via set_val on the value subtag directly:

actions.real.set_val("O_FAN.VALUE", 75.0)   # 75% duty cycle

read_pin(tag_name)

Reads the current physical state of the pin directly from the GPIO hardware. Returns False if the pin has not been set up.

state = actions.gpio.read_pin("I_BUTTON")   # → True / False

This bypasses the tag registry and reads the hardware directly. For most use cases, start_polling and on_change are preferred.

start_polling(name_prefix, *, period?, key?)

Starts a background worker thread that polls the physical pin at a fixed interval and writes any state changes to <name>.VALUE. This is the recommended way to react to digital input pins.

Parameter Type Default Description
name_prefix str — The GPIO tag name.
period float 0.01 Poll interval in seconds.
key slice slice(0, 1) Index to write on change.

If called before the server with block is entered, the worker is queued and started automatically when the server starts.

actions.gpio.start_polling("I_BUTTON", period=0.01)

on_change(name_prefix, callback?, *, key?)

Registers a listener that fires when <name>.VALUE is written — either by the polling worker or by a direct write_pin call.

@actions.gpio.on_change("I_BUTTON")
def handle_button(attr, key, value) -> None:
    if value[0]:
        actions.gpio.write_pin("O_LED", True)
    else:
        actions.gpio.write_pin("O_LED", False)

Modes

"out" — Digital Output

The pin is configured as GPIO.OUT. Writing True or 1 to <name>.VALUE drives the pin high; False or 0 drives it low.

("O_LED", actions.type.GPIO((17, False, "out")))

actions.gpio.write_pin("O_LED", True)

"in" — Digital Input

The pin is configured as GPIO.IN. Use start_polling to detect changes and on_change to react to them. The tag value is updated only when the physical pin state changes.

("I_BUTTON", actions.type.GPIO((27, False, "in")))

actions.gpio.start_polling("I_BUTTON", period=0.01)

@actions.gpio.on_change("I_BUTTON")
def handle_button(attr, key, value) -> None:
    print(f"Button state: {bool(value[0])}")

"pwm" — PWM Output

The pin is configured as GPIO.OUT with a PWM instance running at 1000 Hz. The duty cycle (0.0–100.0) is stored in <name>.VALUE as a REAL. Writing a new value clamps it to the valid range automatically.

("O_FAN", actions.type.GPIO((18, 0.0, "pwm")))

actions.real.set_val("O_FAN.VALUE", 50.0)   # 50% speed

Full Example

# tags.py
from ethernetip_emulator.server.tag_specs import tag_registry
from ethernetip_emulator.server.device import actions


@tag_registry.register
def _gpioPins():
    return [
        ("I_StartButton", actions.type.GPIO((17, False, "in"))),
        ("O_StatusLED",   actions.type.GPIO((27, False, "out"))),
        ("O_FanSpeed",    actions.type.GPIO((18, 0.0,   "pwm"))),
    ]


# actions.py
from ethernetip_emulator.server.device import actions


actions.gpio.start_polling("I_StartButton", period=0.005)


@actions.gpio.on_change("I_StartButton")
def handle_start(attr, key, value) -> None:
    if value[0]:
        actions.gpio.write_pin("O_StatusLED", True)
        actions.real.set_val("O_FanSpeed.VALUE", 80.0)
    else:
        actions.gpio.write_pin("O_StatusLED", False)
        actions.real.set_val("O_FanSpeed.VALUE", 0.0)

Cleanup

When the server exits the with AttributeDevice._actions block, Gpio.__exit__ is called automatically. It stops all PWM instances, calls GPIO.cleanup(), and clears the internal pin registry. No manual cleanup is required.

Clone this wiki locally