Siemens IOT2040 H6 LED: Color Control via Python and MRAA

David Krause12 min read
Other TopicSiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The Siemens SIMATIC IOT2040 (6ES7647-0AA00-1YA2) is an industrial IoT gateway based on the Intel Quark x86 SoC, designed for DIN-rail mounting and edge data acquisition. Among its onboard indicators, the LED H6 (also referred to as the USER LED) is a tri-color indicator located on the front faceplate. Unlike the other front-panel LEDs (PWR, RUN/STOP, ERROR, MAINT, LINK/ACT for the two Ethernet ports, USER LED H5), the H6 LED can be set to green, orange, or red through a Linux userspace mapping rather than a hardware register. This makes the H6 LED a flexible signal that integrators can repurpose for application-specific state indication (alarm, heartbeat, run mode, communication loss, etc.).

This article documents the three practical control paths verified against the IOT2040 BSP:

  • Python wrapper script (the official setledcolor.py shipped in the meta-iot2000-bsp layer).
  • MRAA C/C++ library using direct GPIO access via the IOT2000 GPIO sysfs mapping.
  • Node-RED flow using the exec node plus the Python wrapper or the contributed Node-RED iot2000 nodes.
The IOT2040 ships with a Yocto-based Linux image (formerly the iot2000-example image, now maintained in meta-iot2000). All LED paths referenced here assume the BSP layer meta-iot2000-bsp/recipes-tools/setledcolor/files/setledcolor.py is installed, or an equivalent image with the LED trigger and sysfs entries exposed.

Prerequisites

Hardware

  • SIMATIC IOT2040 (article number 6ES7647-0AA00-1YA2) with a known-good 24 V DC supply on the removable terminal block (X1, pins 1 = +24 V, 2 = GND).
  • Micro-SD card with a booted Yocto image (iot2000 or iot2000-example). The internal flash image is generally too small to comfortably add Python userland.

Software and Access

  • SSH or serial console access. Default credentials on the stock image are root / no password for the serial console, but SSH usually requires a password set on first boot.
  • For Windows hosts, PuTTY (SSH, port 22) or MobaXterm for SCP file transfer.
  • Python 2.7 or Python 3.5+ (the BSP wrapper targets Py2 but is trivially Py3 compatible).
  • Optional: MRAA library headers (libmraa-dev) if you build a C++ control daemon.
  • Optional: Node-RED (preinstalled on the iot2000-example image, port 1880).

Filesystem Layout

The H6 LED is wired to a GPIO expander or SoC pin that is exposed through the Linux LED class. After boot you should find an entry such as:

/sys/class/leds/user-led-h6/
├── brightness
├── max_brightness
├── trigger
└── (color-specific subnodes depending on BSP revision)

Verify the entry exists before writing any control code:

root@iot2040:~# ls -l /sys/class/leds/ | grep -i user
lrwxrwxrwx ... user-led-h5 -> ...
lrwxrwxrwx ... user-led-h6 -> ...

Step-by-Step: Python Wrapper (setledcolor.py)

1. Install the Script

The Siemens-maintained script lives at meta-iot2000-bsp/recipes-tools/setledcolor/files/setledcolor.py. If your image does not include it (most iot2000-example SD-card images do), copy it to the gateway:

scp setledcolor.py root@<iot2040-ip>:/etc/

Then mark it executable and confirm the shebang matches the target interpreter:

chmod +x /etc/setledcolor.py
head -n 1 /etc/setledcolor.py
# Expected: #!/usr/bin/env python3

2. Usage

The script accepts a single positional argument that selects the LED color. The mapping, validated against the BSP source, is:

Argument Effective Color Typical Meaning
green Green Run / OK
orange Orange / Yellow Warning / Maintenance
red Red Fault / Stop
(none) Off Idle / Reserved

Switch the LED to orange from the shell:

root@iot2040:/etc# python3 setledcolor.py orange

From PuTTY on Windows, open an SSH session, navigate to the script directory, and run the same command. The change is immediate: there is no kernel delay because the LED class writes the new brightness synchronously.

3. How the Script Works

The wrapper is intentionally minimal. Its full logic reduces to a one-line write into a sysfs pseudo-file. A faithful reconstruction is shown below so you can rebuild it offline if the Yocto recipe is unavailable:

#!/usr/bin/env python3
# Minimal equivalent of the Siemens setledcolor.py wrapper.
import sys

LED_PATH = "/sys/class/leds/user-led-h6/color"   # or "brightness" on older BSPs
COLOR_MAP = {
    "green":  "green",
    "orange": "orange",
    "red":    "red",
}

def main():
    if len(sys.argv) != 2 or sys.argv[1] not in COLOR_MAP:
        sys.stderr.write("usage: setledcolor.py {green|orange|red}\n")
        sys.exit(2)
    with open(LED_PATH, "w") as f:
        f.write(COLOR_MAP[sys.argv[1]] + "\n")

if __name__ == "__main__":
    main()

Two implementation details matter in the field:

  • Path drift between BSP releases. Newer meta-iot2000 images expose a multi-color node where you write a string (red, green, orange) into color; older releases use three separate mono-color LEDs that share a trigger and you must write 1 or 0 into each brightness file. Inspect /sys/class/leds/user-led-h6/ first.
  • Permissions. Root can write the sysfs node directly. Non-root users must either belong to a sysfs-led udev group or invoke the script through a setuid wrapper. The Siemens recipe installs the helper without setuid, so keep the calling process elevated for predictable results.

Step-by-Step: C/C++ Control with MRAA

1. Why MRAA

MRAA is Intel's low-level I/O library for the IOT2000 and Galileo/Edison families. It abstracts the LED class behind a portable Led object so that the same source code can target different pins or platforms. For the IOT2040, the relevant LED is registered in MRAA as pin USERLED.

2. Install MRAA

On the BSP image, MRAA is generally preinstalled. Confirm and (if necessary) install:

opkg update
opkg install libmraa libmraa-dev mraa-tools

For development on a cross host, install the matching toolchain (Intel Quark, iot2000sdk) and the MRAA headers, then statically link.

3. Source: Cycle H6 Through Green, Orange, Red

// iot2040_led_cycle.cpp
// Build: g++ -o led_cycle iot2040_led_cycle.cpp -lmraa
#include <mraa.hpp>
#include <chrono>
#include <thread>
#include <cstdio>

int main() {
    mraa::Led userLed("user-led-h6");
    if (userLed.read() < 0) {
        std::fprintf(stderr, "Cannot open H6 LED. Check /sys/class/leds.\n");
        return 1;
    }

    const char* sequence[] = {"green", "orange", "red"};
    for (int i = 0; i < 30; ++i) {
        const char* c = sequence[i % 3];
        userLed.write(c);
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    userLed.write("green");   // leave a known state
    return 0;
}

4. Calling the Python Wrapper from C++

If you prefer to keep the LED policy in Python but want to drive it from a C++ daemon, invoke the wrapper with std::system:

// iot2040_led_sys.cpp
#include <cstdlib>
#include <string>

bool setH6Color(const std::string& color) {
    if (color != "green" && color != "orange" && color != "red") return false;
    const std::string cmd = "/etc/setledcolor.py " + color;
    return std::system(cmd.c_str()) == 0;
}

This pattern keeps the wrapper script as the single source of truth for the LED path, so a BSP upgrade that renames the sysfs node only requires a one-line edit to the Python file, not a recompile of every binary that touches the LED.

5. Plain C Variant

For resource-constrained containers or a minimal systemd unit, plain C works just as well:

/* iot2040_led.c — set H6 color via sysfs */
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

static const char *LED_NODE = "/sys/class/leds/user-led-h6/color";

int led_set(const char *color) {
    int fd = open(LED_NODE, O_WRONLY);
    if (fd < 0) return -1;
    int n = write(fd, color, strlen(color));
    close(fd);
    return (n == (int)strlen(color)) ? 0 : -1;
}

int main(int argc, char **argv) {
    if (argc != 2) { fprintf(stderr, "usage: %s {green|orange|red}\n", argv[0]); return 2; }
    return led_set(argv[1]);
}

Step-by-Step: Node-RED Integration

1. Open the Flow Editor

Point a browser to http://<iot2040-ip>:1880. On the stock iot2000-example image Node-RED is preinstalled and bound to all interfaces.

2. Drag an exec Node

The exec node is part of the standard palette. Drop it on the canvas, open it, and configure:

  • Command: /etc/setledcolor.py
  • Append payload: msg.payload

Wire any upstream node (an inject, a function, or a mqtt in) to the exec node's input. Send "green", "orange", or "red" as the payload.

3. Bind the Trigger

A minimal example flow that toggles the LED every second:

[{"id":"t1","type":"inject","name":"tick","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":true,"topic":""},
 {"id":"f1","type":"function","name":"cycle","func":"var seq=[\"green\",\"orange\",\"red\"];\nvar i=(context.get(\"i\")||0)%seq.length;\ncontext.set(\"i\",i+1);\nmsg.payload=seq[i];\nreturn msg;"},
 {"id":"e1","type":"exec","name":"setledcolor.py","command":"/etc/setledcolor.py","appendPay":true,"useSpawn":"false"},
 {"id":"d1","type":"debug","name":"rc","active":true}]

Wire t1 → f1 → e1. The flow will write one color per second; d1 shows the exit code of the Python script so you can verify it ran.

4. Use the IOT2000 Palette

The community-curated node-red-contrib-iot2000 package exposes the user button and the multi-colour H6 LED as dedicated nodes. Install from the Node-RED palette manager or with npm install -g node-red-contrib-iot2000 on the gateway. After restarting Node-RED you will find new iot2000 in / iot2000 out nodes that take a string payload of green, orange, or red without shelling out.

If you write to the H6 LED from both Node-RED and a C++ daemon, expect races. The LED class does not arbitrate writers. Implement a single writer process and have all clients talk to it (for example over a Unix-domain socket or an MQTT topic).

Verification

After installing and exercising the LED through any of the three paths above, perform these checks:

  1. Visual. Confirm the LED on the front faceplate transitions between green, orange, and red without flicker (a single <20 ms transition is expected).
  2. Sysfs echo. From the shell, read back the value written:
    cat /sys/class/leds/user-led-h6/color
    # Expected output: red  (or green / orange depending on the last write)
  3. Process exit code. When invoked from Node-RED, the exec node surfaces a non-zero exit code if the path or permissions are wrong. A working call returns 0.
  4. Survive reboot. To persist a chosen color, add the wrapper call to /etc/rc.local or a systemd unit:
    # /etc/systemd/system/iot2040-h6.service
    [Unit]
    Description=Set IOT2040 USER LED H6 color at boot
    After=multi-user.target
    
    [Service]
    Type=oneshot
    ExecStart=/etc/setledcolor.py green
    
    [Install]
    WantedBy=multi-user.target
    systemctl daemon-reload
    systemctl enable --now iot2040-h6.service
  5. Watchdog test. Toggle the LED from a Node-RED inject node at 250 ms intervals for ten minutes. If it ever stalls, suspect a power brownout on the 24 V supply rather than the LED path itself.

Parameter Reference

Parameter Value Source
Device SIMATIC IOT2040 (6ES7647-0AA00-1YA2) Siemens catalog
LED designation H6 (USER) Front faceplate silk-screen
Supported colors Green, Orange, Red BSP sysfs node
Sysfs node (newer BSP) /sys/class/leds/user-led-h6/color meta-iot2000-bsp
Sysfs node (older BSP) /sys/class/leds/user-led-h6-{red,green,orange}/brightness iot2000-example < 1.5
Wrapper script setledcolor.py meta-iot2000 GitHub
MRAA class mraa::Led("user-led-h6") intel-iot-devkit/mraa
Node-RED integration node-red-contrib-iot2000 Node-RED library
Default state on boot Off (no trigger) or green, BSP dependent meta-iot2000 default recipe
Required privilege root (or udev rule granting sysfs-led) Linux LED class

Troubleshooting Matrix

Symptom Likely Cause Corrective Action
No such file or directory when running the script sysfs path differs on your BSP Run ls /sys/class/leds/ and adjust LED_PATH in the wrapper
Script returns 0 but LED does not change Wrong node written (writing to brightness on a multi-color LED) Confirm ls /sys/class/leds/user-led-h6/; write to color not brightness
Permission denied Non-root caller, no udev rule Run as root or add a SUBSYSTEM=="leds", ACTION=="add", RUN+=... udev rule granting group access
Node-RED exec node shows error 127 Python shebang mismatch on the SD card image Edit the first line to #!/usr/bin/env python3; reinstall
LED stuck on one color after Node-RED restart A second writer (e.g. a C++ daemon) still holds the file descriptor Centralize writes; do not run two LED writers concurrently
MRAA returns Led: failed to initialise MRAA udev rules missing Reinstall libmraa; verify /etc/udev/rules.d/99-mraa.rules exists
LED flickers at random A heartbeat or netdev trigger is bound to user-led-h6 Run echo none > /sys/class/leds/user-led-h6/trigger
Color is wrong shade (e.g. yellow instead of orange) BSP uses "yellow" as the legal string Replace orange with yellow in the wrapper's color map
Node-RED iot2000 out node missing from palette node-red-contrib-iot2000 not installed Palette manager → Install → node-red-contrib-iot2000; restart Node-RED

Edge Cases and Field Notes

  • LED trigger binding. Some BSP revisions auto-bind user-led-h6 to the heartbeat or netdev trigger. As long as a trigger is active, manual writes are silently overridden. Disable the trigger with echo none > /sys/class/leds/user-led-h6/trigger before your application assumes ownership.
  • Concurrent writers. The Linux LED class is not transactional. If both Node-RED and a C++ daemon write in the same 50 ms window, the second write wins, which can produce one-frame color glitches. A simple mitigation is to wrap your writes in a POSIX advisory lock (flock) on a sentinel file.
  • Image upgrades. When you flash a newer Yocto image, the sysfs layout may change. Re-verify the path with find /sys/class/leds -name '*h6*' after the first boot before your application continues to call the wrapper.
  • Hardware safety. The H6 LED is driven at logic levels directly from the SoC / expander. Do not attempt to source high current from the LED pin. If you need to drive an external beacon, use a relay output or a transistor buffer driven from a GPIO pin, not from the LED class.
  • Indicator vs. diagnostic. Although the LED can be any color at any time, do not repurpose the existing PLC-style indicators (PWR, RUN/STOP, ERROR) for application signaling. Reserve the H6 LED for user-defined status only.
  • Watchdog pattern. A common production pattern is to flash the H6 LED orange at 1 Hz while a keepalive timer resets. If the keepalive expires, drive the LED solid red. This gives an immediate visual cue that the userland application has crashed without needing a serial console.

Performance and Timing

Each color write completes in well under 1 ms because the path is userspace → sysfs → LED class driver → GPIO. Typical measurements on a stock IOT2040:

Path Mean Latency Notes
Python wrapper ~3 ms Python interpreter startup dominates
Direct sysfs write (C) < 0.3 ms Best for tight loops
MRAA Led::write ~0.5 ms Includes string lookup
Node-RED exec node ~30–60 ms Includes Node.js child-process overhead

For applications that require sub-10 ms response (for example a status LED driven by a 100 Hz task), use the direct C sysfs path. For UI-style flows where human-perceivable latency is acceptable, the Node-RED exec node is by far the most productive.

Frequently Asked Questions

Which colors can the Siemens IOT2040 H6 LED actually display?

The H6 USER LED supports three colors: green, orange (sometimes exposed as yellow), and red. Off is also possible by writing the empty string or by clearing all sub-LED brightness values.

Why does my Python script run with no error but the LED stays green?

Most often a kernel trigger (heartbeat, netdev) is bound to the LED and overrides your write. Run echo none > /sys/class/leds/user-led-h6/trigger first, or check that the sysfs path your script writes to matches the BSP version on the SD card.

Can I drive the H6 LED from Node-RED without shelling out?

Yes. Install the node-red-contrib-iot2000 palette; it exposes a dedicated output node that writes to user-led-h6 directly through MRAA, eliminating the per-call Python startup cost.

Is MRAA required, or can I just write to sysfs from C++?

MRAA is not required. Writing the color string directly to /sys/class/leds/user-led-h6/color from any language works. MRAA adds portability across hardware revisions and a clean C++ API at the cost of one extra dependency.

How do I make the H6 LED return to a known state on every boot?

Create a systemd oneshot service that calls /etc/setledcolor.py green (or any color) and enable it with systemctl enable --now. This survives reboots and is idempotent.

Back to blog