Resolving node-red-contrib-opcua Bitbake Compile Hang on IOT2040

David Krause12 min read
OPC / OPC UASiemensTroubleshooting
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

Resolving node-red-contrib-opcua Bitbake Compile Hang on Siemens IOT2040

The Siemens SIMATIC IOT2040 is a rugged ARM-based industrial gateway that runs a Yocto/OpenEmbedded (OE) image with Node-RED pre-installed via the Siemens meta-iot2000 layer. Extending that image with additional Node-RED nodes from the public registry is normally done with a custom bitbake recipe. Several node packages, however, do not survive the build: the symptom reported by integrators is that node-red-contrib-opcua enters do_compile and never returns, even after more than 100,000 seconds of wall-clock time. The build produces no error, no warning, and no log line, which is what makes the failure particularly painful to diagnose.

This article documents the root cause of the hang, the diagnostic steps that actually surface a log, two robust remediation paths (runtime install and image replication), and the commissioning checks required before a mass roll-out.

Field-proven outcome: After independent attempts to make the package build cleanly in a Yocto/OE environment failed, the most reliable path is to install the node at runtime on the target instead of trying to bake it. That is the approach used in production for SIMATIC IOT2040 datalogger fleets.

1. Problem Summary

The build environment is a standard Poky/Yocto setup with the Siemens meta-iot2000 layer on top. The user has added a recipe in a custom meta-layer for the Node-RED node node-red-contrib-opcua, alongside a number of other community nodes. The recipe follows the canonical npm bbclass pattern:

inherit npm

SUMMARY = "Node-RED OPC UA client/server nodes"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://LICENSE;md5=..."

SRC_URI = "npm://registry.npmjs.org/;name=${PN};version=${PV} \
           npm://registry.npmjs.org/;name=node-opcua;version=0.10.10"

S = "${WORKDIR}/npm-${PN}-${PV}"

do_install_append() {
    mkdir -p ${D}/usr/lib/node_modules/${PN}
    cp -r ${WORKDIR}/node_modules/* ${D}/usr/lib/node_modules/${PN}/
}

FILES_${PN} += "/usr/lib/node_modules/${PN}"
RDEPENDS_${PN} += "nodejs node-red"

Bitbake reaches the do_compile task. The task starts, CPU activity on the build host is low, and the wall-clock time keeps growing. There is no exit, no error, and no apparent progress. The build is stuck.

Field Observed value
Yocto layer meta-iot2000 on Poky 2.x (krogoth/jethro lineage)
Target hardware SIMATIC IOT2040 (Intel Quark x86, 1 GB RAM, Yocto Linux)
Node version Node.js bundled with the IOT2040 image
Failing task do_compile of node-red-contrib-opcua
Wall-clock time at hang > 100,000 s, then manually killed
Error output None
Other nodes in same layer Build cleanly

2. Root Cause Analysis

The hang is not a single bug; it is the interaction of three structural problems in how node-red-contrib-opcua resolves its dependency graph inside a hermetic, rootless Yocto build sandbox.

2.1 node-opcua native compile loop

node-red-contrib-opcua depends on the C++ client node-opcua, which historically pulled in node-gyp-driven native modules. In a Poky build sandbox the npm dependency walker will recursively fetch node-opcua, attempt to compile its C++ bindings, and either:

  • retry the network fetch for a sub-dependency that resolves to a git URL which the sandbox cannot reach, or
  • spin in a node-gyp configure loop because the cross-compile environment variables are not propagated through npm install.

In both cases the symptom is identical: a long, silent do_compile that produces no log line because npm writes its progress to a tty-detecting stream that the bitbake log capture treats as empty.

2.2 npm registry resolution inside a network-isolated sandbox

The Poky build sandbox runs in a pseudo-tty with restricted network egress. If a sub-dependency of node-opcua resolves to a package that is no longer on the npm registry or has been moved to a git+ssh URL, the npm pack / npm install steps block on socket reads. The IOT2040 image line uses a specific node version (e.g. Node 6.x for jethro, Node 8.x for sumo) and node-opcua 0.10.x branch has known registry resolution issues for that range.

2.3 npm and node-gyp log redirection

When npm detects that its output is not a tty, it switches to non-progress mode and writes nothing during the dependency walk. Bitbake's log.do_compile.<task> therefore contains only the command echo and an empty stdout. The build host is doing real work (npm is hashing tarballs), but the log shows nothing. From the outside, it looks like a hang.

Why the build never errors out: npm does not treat unreachable git sub-dependencies as fatal in all --ignore-scripts modes, and node-gyp's prebuild check loop is bounded only by its internal retry counter. Combined, this can hold the task open for an unbounded wall-clock time.

3. Diagnostic Procedure

Before changing the recipe, surface the actual log. The following sequence forces npm to write to a file that bitbake captures.

3.1 Reproduce the hang under npm directly

  1. Download the same tarball the recipe is using:
    npm pack node-red-contrib-opcua@<version> --verbose 2>&1 | tee /tmp/npm-pack.log
  2. Extract the tarball and run install with maximum verbosity:
    mkdir work && cd work
    npm init -y
    npm install ../node-red-contrib-opcua-<version>.tgz --loglevel=silly --foreground-scripts 2>&1 | tee /tmp/npm-install.log
  3. Inspect /tmp/npm-install.log for the dependency that takes the longest, then the error chain that immediately precedes it. In every reported case the bottleneck is the node-opcua chain, not node-red-contrib-opcua itself.

3.2 Force npm progress into the bitbake log

Patch the recipe to disable npm's stdout detection and to ignore scripts (which stops node-gyp from looping):

do_compile() {
    cd ${S}
    export NPM_CONFIG_LOGLEVEL=silly
    export NPM_CONFIG_PROGRESS=false
    export NPM_CONFIG_FOREGROUND_SCRIPTS=true
    export NPM_CONFIG_REGISTRY="https://registry.npmjs.org/"
    npm install --production --ignore-scripts --no-audit --no-fund 2>&1 | tee ${B}/npm.log
}

Re-run bitbake node-red-contrib-opcua. The log now contains a full npm trace; the hang is replaced by a real error message you can act on.

3.3 Capture native build output

If the recipe does install (with --ignore-scripts) but you still need the native bindings at runtime, set:

export npm_config_build_from_source=true
npm_config_node_gyp=$(which node-gyp) npm rebuild --verbose 2>&1 | tee ${B}/rebuild.log

On x86_64 Poky hosts, node-red-contrib-opcua's bundled node-opcua typically uses a prebuilt binary for Node 6/8, so the rebuild step often finishes in under a minute. The hang only appears when the prebuilt lookup fails and the source build path is taken.

3.4 Confirm the cause is the package, not the layer

Build a known-good node next to the failing one:

bitbake node-red-contrib-ui-table node-red-contrib-opcua

If node-red-contrib-ui-table finishes in seconds and node-red-contrib-opcua hangs, the failure is package-specific, not a layer problem.

4. Recommended Solution: Install at Runtime

Packaging JavaScript-only nodes into a Yocto image is convenient but rarely worth the maintenance cost when the node pulls in a non-trivial native chain. For production IOT2040 datalogger fleets the engineering answer is to install node-red-contrib-opcua at runtime on the target.

4.1 Single-device manual install

  1. Log in to the IOT2040 over SSH as the user that runs Node-RED (typically node-red or root).
    ssh root@<iot2040-ip>
  2. Stop the running Node-RED service:
    systemctl stop node-red
  3. Install the node into the Node-RED user directory:
    cd ~/.node-red
    npm install node-red-contrib-opcua@<tested-version> --save \
      --no-audit --no-fund --omit=optional
  4. Restart and verify the OPC UA nodes appear in the palette:
    systemctl start node-red
    curl -s http://localhost:1880/nodes | jq '.[] | select(.name | test("opcua";"i"))'

4.2 First-boot provisioning script

Wrap the install in a systemd one-shot unit that runs once on first boot. Save /usr/lib/node-red/iot2000setup-nodered-opcua.sh:

#!/bin/sh
# Install the tested node-red-contrib-opcua version on first boot
set -e
MARKER="/var/lib/node-red/.node-red-contrib-opcua.installed"
NPM_PKG="[email protected]"
[ -f "$MARKER" ] && exit 0

systemctl stop node-red
mkdir -p /var/lib/node-red/.node-red
cd /var/lib/node-red/.node-red
npm install "$NPM_PKG" --no-audit --no-fund --omit=optional
chown -R node-red:node-red /var/lib/node-red/.node-red
systemctl start node-red
touch "$MARKER"

And a matching unit file /etc/systemd/system/iot2000setup-nodered-opcua.service:

[Unit]
Description=IOT2000 first-boot install: node-red-contrib-opcua
After=network-online.target node-red.service
Wants=network-online.target
Before=node-red.service

[Service]
Type=oneshot
ExecStart=/usr/lib/node-red/iot2000setup-nodered-opcua.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Enable it from inside the image recipe (e.g. a bbappend for node-red):

SYSTEMD_SERVICE_${PN} += "iot2000setup-nodered-opcua.service"
INITSCRIPT_PARAMS = "defaults 99"
Pin the version. The discussion thread explicitly calls this out: make sure to specify the version you tested so that they are consistent across all devices. Always pass an exact @x.y.z range, never a ^ or ~ range, when scripting the install.

5. Mass Roll-Out: Image Replication

For a datalogger fleet of 10, 100, or 1000 units, installing at runtime on every device is wasteful. The standard IOT2000 pattern is to install once, capture the SD card, and replicate.

  1. On a reference IOT2040, complete the runtime install from §4.1, including the flow, certificates, and palette configuration.
  2. Power the unit down and pull the microSD card.
  3. On the build host, image the card:
    sudo dd if=/dev/sdX of=iot2040-datalogger-<rev>.img bs=4M status=progress conv=fsync
    sudo sync
  4. Pre-expand the root filesystem on the gold image so each clone is ready to use (see the Siemens meta-iot2000 expandfs.sh reference for the production pattern). The script lives in meta-iot2000-example/recipes-example/iot2000setup/files/expandfs.sh.
  5. Flash the gold image onto every datalogger SD card. The first boot on the target runs the expandfs.sh and (optionally) the iot2000setup-nodered-opcua.service one-shot from §4.2.
Approach Build time Reproducibility Per-device flash time Best for
Bitbake the node into the image Hours to days, frequently hangs High in theory, fragile in practice Same as base image Air-gapped, locked-fleet use cases
Runtime install script None at build time High if version is pinned +20-40 s per device Lab / small fleet
Image replication None at build time Highest, byte-for-byte SD card write time only Mass roll-out (>5 units)

6. Alternative: node-red-contrib-opcua-suite

If your fleet requires a buildable OPC UA package, switch from the legacy node-red-contrib-opcua to the actively maintained node-red-contrib-opcua-suite. Key differences that affect the Yocto build:

  • Shared connections. All nodes referencing the same endpoint share a single TCP connection (ref-counted), which means fewer sockets and fewer native bindings to compile.
  • Batch read/write. Internal batching reduces the number of async calls, which means the npm dependency graph is shallower.
  • Pure-JS OPC UA stack. No node-gyp build loop in the install path; the package is a thin Node-RED wrapper around an existing OPC UA client.

The recipe pattern is identical, but the build time drops from "hangs forever" to "finishes in under 60 seconds" on a typical Poky build host.

7. Endpoint Configuration and Certificate Permissions

Once the node is installed (by any path), the most common runtime error on the IOT2040 is the OPC UA client failing with an "invalid endpoint" message. The fix is filesystem permission, not configuration.

7.1 Diagnose the certificate store

The OPC UA client creates a per-user certificate store under the home directory of the user that runs Node-RED. If that directory is not writable, the client throws Invalid endpoint at first connect.

ls -ld /home/node-red/.node-red/opcua
# expected: drwxr-xr-x node-red node-red

7.2 Repair permissions

systemctl stop node-red
rm -rf /home/node-red/.node-red/opcua
mkdir -p /home/node-red/.node-red/opcua
chown -R node-red:node-red /home/node-red/.node-red
systemctl start node-red

Re-test the endpoint. The client will regenerate its certificate store and the connection will succeed.

8. Tuning keepSessionAlive

On long-lived datalogger flows the default keepSessionAlive = true can keep a half-closed socket alive for hours after a network blip, surfacing as "subscription lost" in the Node-RED log. If your flow handles reconnection in the application layer, set the property to false in the OPC UA Client node configuration:

// In a function node that dynamically configures the client
msg.endpoint = {
    endpoint: "opc.tcp://192.168.200.20:4840",
    keepSessionAlive: false,
    securityMode: "None",
    securityPolicy: "None",
    name: "plc-line-1"
};
return msg;

The change is exposed through the client's options object; no restart of Node-RED is required, only a redeploy of the flow.

9. Verification Matrix

Check Command Pass criterion
OPC UA nodes present in palette curl -s http://localhost:1880/nodes | jq '.[].name' | grep -i opcua At least 4 nodes (Client, Server, Endpoint, Browser)
Endpoint reachable opcua-client browse opc.tcp://<plc>:4840 Root objects tree returned
Subscribe to a tag Deploy a Subscribe node, monitor msg.payload Values update at the configured sampling interval
Survive PLC reboot Power-cycle the PLC, wait 30 s Client auto-reconnects without manual flow deploy
Mass-rollout fingerprint sha256sum /var/lib/node-red/.node-red/package.json Identical hash on every datalogger

10. Hardening Notes for the Recipe Maintainer

  • Pin every npm version. Use SRC_URI = "npm://registry.npmjs.org/;name=...;version=..." for every dependency you can. Tarball reproducibility is the only way to keep a Yocto build deterministic across rebuilds.
  • Prefer --omit=optional in the runtime install script. Optional dependencies of node-opcua include native bindings that you do not need on a 1 GB RAM IOT2040.
  • Disable node-gyp download of headers. Set npm_config_node_gyp= in the runtime script to avoid the network fetch that bitbake sandboxes cannot complete.
  • Capture the gold image SHA-256 in the deployment record. The Siemens IOT2000 success story pattern is image-fingerprinted roll-out, not per-device install.

11. Frequently Asked Questions

Why does node-red-contrib-opcua hang at do_compile when other nodes build fine?

The node pulls in node-opcua, which has a long native-binding dependency chain. In a hermetic Yocto sandbox the npm install step blocks on a network fetch or on a node-gyp compile loop that npm does not report as an error. The build is doing real work, but bitbake sees no log output. Adding NPM_CONFIG_LOGLEVEL=silly and --ignore-scripts to the recipe's do_compile forces a real error message that you can act on.

Should I package node-red-contrib-opcua into the Yocto image or install it at runtime?

For a production datalogger fleet on the IOT2040, install at runtime and replicate the SD card image. Bitbake packaging of this node is fragile because of its native dependency chain, and runtime install lets you pin a tested version, run it once on a gold unit, and capture the image. The Siemens meta-iot2000 example layer's expandfs.sh is the reference pattern for first-boot provisioning.

What version of node-red-contrib-opcua should I pin?

Pin the exact version that you have tested end-to-end on a real IOT2040, including the OPC UA server it talks to. For datalogger fleets this is typically the latest 0.2.x release at the time of project freeze. Always pass @x.y.z in the runtime install script, never a range operator, so every device in the fleet ends up with the same package.json.

Why does the OPC UA client throw "invalid endpoint" right after install?

The client cannot create its certificate store. The Node-RED process user (typically node-red or root) does not have write permission to the .node-red/opcua directory. Stop Node-RED, delete the directory, recreate it owned by the Node-RED user, and restart. The client will regenerate its certificate and the endpoint will resolve.

Can I use the node-red-contrib-opcua-suite instead, and will it bitbake?

Yes, and in most cases it will. The suite wraps a pure-JS OPC UA client, so the npm install path is shallow and free of node-gyp. Use it as a drop-in replacement in your recipe; the rest of this article's runtime install, certificate permission fix, and image replication patterns still apply.

How do I auto-reconnect after a PLC reboot?

Leave the OPC UA Client node's auto-reconnect at the default (true) and keep the sampling interval short (e.g. 1000 ms). On a PLC power-cycle the client will reopen the session within a few seconds. If you also want to drop half-open sockets promptly, set keepSessionAlive = false in the endpoint options and handle the reconnection explicitly in your flow.

Back to blog