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.pyshipped in themeta-iot2000-bsplayer). - MRAA C/C++ library using direct GPIO access via the IOT2000 GPIO sysfs mapping.
-
Node-RED flow using the
execnode plus the Python wrapper or the contributed Node-RED iot2000 nodes.
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 (
iot2000oriot2000-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-exampleimage, 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) intocolor; older releases use three separate mono-color LEDs that share a trigger and you must write1or0into eachbrightnessfile. 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-ledudev 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.
Verification
After installing and exercising the LED through any of the three paths above, perform these checks:
- Visual. Confirm the LED on the front faceplate transitions between green, orange, and red without flicker (a single <20 ms transition is expected).
-
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) -
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. -
Survive reboot. To persist a chosen color, add the wrapper call to
/etc/rc.localor 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.targetsystemctl daemon-reload systemctl enable --now iot2040-h6.service - 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-h6to theheartbeatornetdevtrigger. As long as a trigger is active, manual writes are silently overridden. Disable the trigger withecho none > /sys/class/leds/user-led-h6/triggerbefore 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.