Repository navigation
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:
GPIOrequires eitherRPi.GPIO(Raspberry Pi) orOPi.GPIO(Orange Pi) to be installed. If neither is available, the datatype is registered but itstype_validatorwill raise aTypeErroron any attempt to use it.
| 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.
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| 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". |
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.
All methods are accessed via actions.gpio.
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")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 offFor 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 cycleReads 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 / FalseThis bypasses the tag registry and reads the hardware directly. For most use cases,
start_pollingandon_changeare preferred.
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)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)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)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])}")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# 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)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.