System designHow-to

Trigger, capture and save TCP/IP camera images in Node-RED

Send a TCP/IP camera a capture command from Node-RED, collect the image buffer, check the byte count and save the file: the acquisition path with timing math.

LumaSpec Vision12 min read Share on LinkedIn

On this page 10 sections
  1. 01Can Node-RED talk to an industrial camera directly?
  2. 02What do you need before you build the flow?
  3. 03How do you send the capture command over TCP?
  4. 04How do you know the whole image has arrived?
  5. 05How do you save the image to disk?
  6. 06How long does one capture-and-save cycle take?
  7. 07Why does capture work once and then stop?
  8. 08When should you skip raw TCP and use the SDK instead?
  9. 09Which checks do you run before going live?
  10. 10Questions

A camera on the network, an SDK page that documents its TCP command port, and a Node-RED flow that needs images: this how-to wires the three together. You will send a capture command from an inject node, accumulate the reply, decide from a byte count when the frame is whole, and write the file to disk. The same arithmetic sets the reply timeout and tells you how many shots per second raw TCP can deliver. It is written for controls and test engineers who are adding cameras to Node-RED test stations and line controllers. The transport is plain TCP; only the command bytes and the port number come from your own SDK.

Can Node-RED talk to an industrial camera directly?

Node-RED ships no GigE Vision or GenICam stack, so it cannot stream a camera by itself; what it does hold is a plain TCP conversation, and any camera whose SDK exposes a command service on the network can be driven that way: capture, retrieve, save.

That splits the job in two. The transport half is generic: a TCP client node sends bytes out and collects the reply. The protocol half is specific: the command bytes, the port and the reply framing come from the camera SDK's TCP interface page, never from guesswork.

Three routes exist, ranked by effort. Snapshot-style IP cameras answer an HTTP GET on their web port (80 by default), so a plain request node is enough. Cameras whose SDK exposes a raw TCP command port take the flow in this guide. Everything else — continuous streaming, hardware-triggered bursts — belongs in an SDK application that Node-RED supervises over HTTP; that split is the subject of the second-to-last section.

What do you need before you build the flow?

Four things: the SDK page that documents the command service (address, port, command bytes, reply framing), a Node-RED host on the camera's subnet with a fixed address such as 192.168.1.10/24, a folder to write into, and a Catch node to trap timeouts and half-closed connections.

Network notes: give the camera NIC its own subnet and leave its gateway empty, so image bytes never try to leave for the office LAN. When the host still cannot see the camera, the GigE camera red X error fix plan works the usual checks: link lights, fixed addresses, firewall rules, duplicate IPs. Running Node-RED on an embedded controller instead of a desktop changes nothing in the flow; the USB barcode scanner on a Linux controller guide shows that environment set up.

The camera finder narrows the choices by interface, resolution and shutter. The 12 MP example below pairs a 12 MP rolling-shutter sensor with a GigE link at 9 fps full resolution:

The LS-CMR-0053 pairs a 12 MP rolling-shutter sensor with a GigE link and reads 9 fps at full resolution:

Area scan cameras12 MP GigE area scan camera, IMX226, 1/1.7" rolling shutter, 9 fps (LS-CMR-0053)LS-CMR-0053
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
12 MP
$560.00View partBuild a system with it

Either resolution slots into the same flow; only the frame-byte count and the transfer time change. For the host, a vision controller keeps capture traffic and plant traffic apart, so filter by LAN port count — this model carries five or more:

The MV-VC3301H-128G66 gives the flow a host with 5+ LAN ports, an Intel Core i3 and 8 GB of memory:

Vision controllersHikrobot MV-VC3301H-128G66 I3-8100, 8GB+128G SSD, No PoE, PCIe expansion supported, Incl. VM6200Pro licenseMV-VC3301H-128G66
LAN ports
5+ LAN ports
USB 3 ports
3-4 USB 3 ports
Light outputs
4 built-in channels
Memory
8 GB
$2,419.00View partBuild a system with it

One port faces the camera subnet, another faces the plant network, so 18 MB replies and dashboards never share a cable.

How do you send the capture command over TCP?

For a one-shot command the community tcp request node fits: it opens an outbound connection to the camera, sends the bytes you inject, and hands back what the camera returns.

When the connection should stay open between shots, node-red-contrib-tcp-client2 does the same job with explicit connect, send and close messages; its package page recommends a Catch node to manage exceptions, and the same applies here.

Build the send path in four steps:

  1. Address the node: server is the camera's IP, port is the command port the SDK documents. Keep both in the node config so a second camera gets a copied flow.
  2. Build the command Buffer. An inject node with a comma-separated byte list is the bench-test shortcut; a function node builds the real command once you add part IDs or exposure values. The bytes themselves come from the SDK page.
  3. Set the reply rule so the node waits for the whole reply — by timeout in milliseconds or by a match on the received data — not for the first packet that arrives.
  4. Log both directions. A debug node on the send path prints the command byte count; one on the reply path prints arrived bytes. Those two numbers decide everything later.

Test before any camera is involved: point the node at an echo server on a second PC and send a 4-byte test command such as 01 00 00 00. The echo returns the same 4 bytes, and the debug pane shows 4 sent, 4 received. That proves addressing, framing and the reply rule while the flow is still simple and a failure costs little.

How do you know the whole image has arrived?

TCP hands you a byte stream, not a file, so count bytes: a 12 MP frame packed at 12 bits is 12,000,000 pixels × 1.5 bytes = 18,000,000 bytes, and the reply is complete only when the accumulator reaches that number.

frame bytes = pixels × bytes per pixel 12-bit packed: 2 pixels travel in 3 bytes → 1.5 bytes per pixel 12,000,000 px × 1.5 B = 18,000,000 B 8-bit mono: 12,000,000 px × 1 B = 12,000,000 B

One function node does the counting and the concatenation. Declare the target once, in bytes:

// Reply accumulator. msg.payload: Buffer (one TCP chunk).
let expected = context.get('expected') || 18000000; // Number: bytes in one frame
let acc      = context.get('acc')      || 0;        // Number: bytes seen so far
let parts    = context.get('parts')    || [];       // Array of Buffer: chunks kept
acc += msg.payload.length;
parts.push(msg.payload);
if (acc >= expected) {
    msg.payload = Buffer.concat(parts);  // Buffer: the whole frame
    context.set('acc', 0);
    context.set('parts', []);
    return [msg, null];                  // output 1: complete
}
context.set('acc', acc);
context.set('parts', parts);
return [null, msg];                      // output 2: still collecting

Bench test: set expected to 12 and inject three 4-byte Buffers. Output 1 stays empty after the first two chunks and returns exactly one 12-byte Buffer after the third. That proves the completion logic before image data ever flows.

If the count stalls below expected, the timeout fires and the Catch node logs it — the same failure mode behind one-shot captures that miss frames. The recovery cases are gathered in the troubleshooting section below.

How do you save the image to disk?

Send the finished Buffer straight into the file node: a Buffer payload is written byte-for-byte, so the file on disk matches the reply exactly — convert to base64 only when the image travels as text, into a dashboard or an HTTP response.

  1. Name the file in a function node ahead of the write: msg.filename is the property the file node reads, and a timestamp keeps every shot separate.
  2. Write the Buffer as-is. When the payload stays a Buffer, the bytes land on disk unchanged; inserting a string conversion on this path corrupts every image silently.
  3. Choose the extension from the reply format. Encoded replies — capture libraries commonly deliver RAW, BMP, TIF or JPEG — open in any viewer once the extension matches; raw pixel replies keep the SDK's own format name and are decoded by its viewer.
// Filename builder. msg.payload: Buffer (complete frame).
const stamp = new Date().toISOString().replace(/[:.]/g, '-');  // String, filename-safe
msg.filename = '/home/ops/captures/shot-' + stamp + '.bin';    // String: your folder
return msg;

On-disk test: after one inject, the file size equals the accumulator's byte count — 18,000,000 bytes in the worked example. A file that is consistently short means the reply rule fired early, which the next section fixes. For counters instead of timestamps (shot-0001, shot-0002), the triggered image with a numbered filename walkthrough shows a reset-safe counter pattern.

The embedding path needs one formula and one line of code:

base64 characters = 4 × ceil(bytes / 3)
// Base64 copy. msg.payload: Buffer, msg.base64: String.
msg.base64 = msg.payload.toString('base64');
return msg;

Test: a 1,000,000-byte Buffer must produce a string of 4 × 333,334 = 1,333,336 characters. Assert on msg.base64.length before pointing the flow at real frames.

How long does one capture-and-save cycle take?

Budget about 0.2 s per shot for a 12 MP frame on one GigE link: 18,000,000 bytes at a planned 100 MB/s is 0.18 s, and exposure plus command round trip adds a few tens of milliseconds — roughly four to five shots per second.

transfer time = frame bytes / usable throughput 18,000,000 B / 100,000,000 B/s = 0.18 s cycle ≈ exposure + round trip + transfer = 0.02 + 0.01 + 0.18 = 0.21 s shots per second ≈ 1 / 0.21 ≈ 4.8

The basis: a 1 Gbit/s link is 1,000,000,000 bit/s ÷ 8 = 125,000,000 B/s at line rate; real TCP payload measures around 110–118 MB/s, so 100 MB/s is the planning figure that leaves margin for the disk write and the editor. The 20 ms exposure and 10 ms round trip are example inputs — substitute your own and the formula holds.

Halve the frame, halve the wait. A 6 MP reply is 6,000,000 × 1.5 = 9,000,000 bytes, which takes 0.09 s on the same link, so the cycle lands near 0.12 s — about eight shots per second. The sensor is never the pacing item: a 9 fps part delivers a fresh frame every 111 ms (1 ÷ 9), well inside the 0.2 s TCP cycle. When shots must land faster than the save absorbs, buffer between them; the burst capture and buffer walkthrough shows that pattern. Decision: raw TCP is comfortable to about five 12 MP shots per second; past that, take the SDK route described below.

Why does capture work once and then stop?

A one-shot-then-hang flow almost always has a framing or state bug: the reply rule fired on the first packet, the accumulator kept bytes from the previous shot, or the camera waits for a close command the flow never sends.

Work down this list; each line names the symptom, the test and the fix.

  1. Reply fires early. Short buffer, well before the timeout. Test: the arrived-byte count stops near a multiple of the packet size. Fix: set the wait rule to the full reply — timeout at twice the expected transfer (0.4 s for the 18 MB frame) or a match only the trailer satisfies.
  2. Stale accumulator. Every second image is short by a constant byte count. Test: reset the flow, send one shot — it completes; the second fails. Fix: clear expected, acc and parts at the top of every cycle and again on the Catch path. Starting from a known state is the general cure; the leftover-settings walkthrough argues the same case from the camera-settings side.
  3. Half-open connection. The second inject returns nothing at all. Test: the camera's status page or its packet counter shows the session still open. Fix: with node-red-contrib-tcp-client2, send the close message after each save; with one connection per command, the close happens by itself.
  4. Missing session step. The camera answers the first command with an error code or silence. Test: log the first reply bytes and compare them with the SDK's error list. Fix: send the SDK's sequence — login, parameters, capture — in order, one node chain each.
  5. Single-shot mode on the camera side. The flow is perfect and the camera still stops after frame one. Test: the same symptom appears in the SDK's own tool. Fix: set the acquisition mode for continuous or re-arm per shot; the hardware trigger stops after one frame checklist names the settings behind it.

One case sits above the flow: the SDK's own viewer captures while Node-RED receives nothing, so the fault sits between transport and application. A no-image troubleshooting walkthrough for a desktop capture library works that same divide step by step, and the method transfers directly.

When should you skip raw TCP and use the SDK instead?

Stay on raw TCP for on-demand shots up to about five per second at 12 MP; move to an SDK application — or a GenICam consumer — for streaming, hardware-triggered bursts, or any rate the 0.2 s link budget cannot cover.

The architecture that scales: a small capture service built on the SDK owns the camera, and Node-RED supervises it. An http-in endpoint exposes capture and latest-image, an http request node calls it, and the flow keeps the filing, logging and dashboard work.

Two live walkthroughs cover the halves this flow leaves out: the GigE Vision capture-and-save guide works the SDK side of commanding and saving frames, and runtime register access from your control code is the middle path when you need to set exposure or ROI but not fetch pixels. When the image must continue to a machine controller, the byte-count discipline travels with it — one worked robot-to-Python transfer over TCP/IP shows the same framing care on the machine side.

Which checks do you run before going live?

Run six checks before production: echo test passed, byte count equals frame size (18,000,000 for the 12 MP example), saved file size equals the byte count, Catch path logs errors, camera subnet isolated from the office LAN, and a two-hour soak leaves no gaps in the timestamped files.

  • Echo test passed — proves addressing and reply framing while the flow is still simple.
  • Byte count equals frame size — proves the accumulator target matches the sensor geometry actually configured, not the one assumed.
  • File size equals byte count — proves the write path is binary end to end; a short file is an early fire, a corrupt one is a string conversion.
  • Catch path logs — proves the timeout has a controlled recovery, not a silent stall.
  • Camera subnet isolated — keeps 18 MB replies off the office LAN and plant chatter out of the capture path.
  • Two-hour soak, no gaps — proves the close-and-reset handling survives real cycling, the exact bug the troubleshooting section describes.

To size the hardware around this flow, the camera finder filters by interface, resolution and shutter, and the vision controller category filters by LAN port count and memory — or start at the camera configurator and work forward. If the station mixes several cameras, a PLC and a dashboard, tell us the shot rate and frame sizes through the contact page and we will spec the path. More integration guides sit on the systems topic page.

Questions

Which port does the camera's command service use?

There is no standard number: the SDK's TCP interface page names the port, and that same number goes into the tcp request node's address field as host:port. Keep the port inside the camera subnet and blocked from the office LAN, so only the Node-RED host can send commands. Confirm reachability with a quick echo test before building the rest of the flow.

Can a PLC signal start the capture instead of an inject node?

Yes. Poll the PLC bit or register over Modbus TCP or OPC UA, detect the rising edge, and feed the same command Buffer into the TCP node. The image path is unchanged, and stamping the trigger time in the same flow gives you a shot log you can match against the saved filenames.

How much larger does an image get as base64 text?

Exactly one third, plus rounding: 4 characters per 3 bytes. An 18,000,000-byte frame becomes 24,000,000 characters, and a dashboard then needs the data URI prefix (data:image/jpeg;base64, for a JPEG) in front of it. Keep the raw Buffer for files and use the string only on the dashboard or HTTP path.

Does the flow work over Wi-Fi?

It connects, but expect partial images: an 18 MB reply needs about 0.18 s of sustained throughput on wired GigE, and Wi-Fi retries push the transfer past the reply timeout, so the accumulator fires late or never. Wire the camera-to-controller link; leave Wi-Fi to the dashboard side.

Can the image reach a dashboard without a file write?

Yes. Skip the file node, convert the finished Buffer with toString('base64'), and hand the data URI to an image widget, or return it from an http response node with the image's content type. Nothing else changes: the accumulator and its byte count still decide when the frame is whole.