A verdict on a screen disappears at the end of the shift; a stored image does not. This flow gives you one database record per inspected part: the JPEG the camera took, the result string it returned, the station name and a timestamp, inserted automatically as each part passes the camera. It is written for integrators who already run Node-RED on a vision controller beside a smart camera that accepts TCP commands and pushes images over FTP. You will configure the camera's command channel and FTP push, wire five community nodes into a working flow, and size the archive before go-live: the worked example budgets a 400 KB JPEG per part at one part per second, which is 30.7 GB per day and 921 GB for a 30-day retention window on a 1 TB drive.
How does an inspection image travel from camera to database?
The camera pushes each inspection JPEG to an FTP server that runs inside Node-RED, the flow converts that image into a base64 text string, and one database insert per part stores the string together with the result string, a station name and a timestamp. The three roles stay separate, and that separation is what makes the flow testable: the camera inspects and pushes, Node-RED converts and forwards, the database keeps. You can prove the image leg with a preview node before any database exists, and prove the insert leg with a hand-built test document before the camera is connected.
Base64 exists because the insert is a JSON document, and raw binary has no place in one. The encoding maps every 3 bytes of JPEG to 4 text characters, so it is lossless with a predictable size: a 400 KB JPEG becomes a 533 KB string. That 4/3 factor is the number every storage figure below is built from.
Two existing guides cover parts of this path and stop short of storage: pushing NG images from a vision system to an HMI over FTP shows the image without keeping it, and capturing and saving TCP/IP camera images in Node-RED archives snapshots from cameras you pull from rather than cameras that push. This flow joins the trigger leg, the image leg and the archive into one cycle.
What do you need before you build the flow?
You need five community nodes, a smart camera whose communication settings include a TCP command channel and an FTP image push, and a vision controller with enough disk to hold every image for the retention window you pick. Install the packages from the Node-RED palette manager:
-
node-red-contrib-buffer-parser— frames the TCP command bytes and splits the reply into fields -
node-red-contrib-ftp-server— hosts the FTP server inside the flow, so uploaded images arrive as messages -
node-red-node-base64— converts between Buffer and base64 string in both directions -
node-red-contrib-image-output— shows the arriving JPEG inside the editor, which is how you watch the image leg work -
node-red-contrib-mongodb4— inserts the finished document into MongoDB, the document database used in the example
Network basics: put the camera and the controller on the same subnet with static addresses — 192.168.1.10 for the camera and 192.168.1.20 for the controller in the examples below. Create a dedicated username and password on the controller for the camera's FTP push; do not reuse an administrator account, and restrict the FTP port to the camera's address.
Any smart camera that can be commanded over TCP and push files over FTP fits this flow. A 6 MP mono model with a GigE interface, rolling shutter and M12 lens mount is the class of part the example budgets assume:
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
The controller hosts Node-RED, the FTP server and the database, so its disk sets your retention window. The vision controllers below both run that stack; the first pairs an N97 processor with a 1 TB HDD for the archive, the second adds PoE camera ports, a 12th-generation Intel Core i5 and 32 GB of memory for multi-camera cells:
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- LAN ports
- 5+ LAN ports
- USB 3 ports
- 3-4 USB 3 ports
If you would rather shortlist first, the camera finder filters by interface, resolution and shutter type.
How do you configure the camera's TCP command channel and FTP push?
In the camera's configuration mode, add a TCP server as the command interface on a fixed port, point a result string output at that same socket, aim the FTP image push at the controller's IP address, then return the camera to run mode; the example uses port 12121. Work through the communication settings in this order:
- Give the camera its static IP address (192.168.1.10 in the example) and confirm its web interface opens from a browser on the same subnet.
- Switch the camera into configuration mode; jobs and communication cannot be edited while it runs.
- Add a TCP server as a communication interface and set its command port — 12121 in the example; any free port works as long as the flow uses the same one.
- Point the command channel at that TCP server, so bytes arriving on the port drive the inspection job.
- Format the result string your line expects, for example
1,OK,SN00412for job number, verdict and serial, and select the TCP server as its output interface. - Add the image push: FTP as the transfer, the controller's address (192.168.1.20), the port the flow will listen on (2121 in the example), and the dedicated FTP username and password you created.
- Save, return the camera to run mode, and trigger it once by hand to confirm that both the string and the image arrive.
If the camera offers JPEG quality settings for the FTP push, set them now: quality decides the kilobytes per frame, and the kilobytes decide the archive.
How do you trigger the camera from Node-RED?
Open one TCP connection to the camera's command port and keep it up, build the trigger command as a byte buffer so its framing matches the camera exactly, and write it once per part — a few command bytes go upstream and a short result string comes back. The steps:
- Add a TCP out node configured to connect once to 192.168.1.10 on port 12121 and stay connected for the shift.
- Feed it from a buffer-parser node that assembles the trigger command as bytes. The command itself is the camera's documented trigger string, typically short ASCII with a terminator; do not send it as a plain typed string, because framing is what the camera actually checks.
- Write once per part: an inject node for bench tests, or the part-present message from your line PLC in production.
- The camera answers on the same socket with the result string; a buffer-parser output splits it into fields — verdict, serial — that the database document will carry.
Command framing, timeouts and reconnects are the same problem set as in Trigger a smart camera over TCP with ASCII commands and read results, and the result hand-off to the PLC is covered in Smart camera to PLC over TCP: trigger and pass/fail. Keep the two paths independent: the PLC must never wait for the database, and the database must never wait for the PLC handshake.
How does the FTP upload become a database document?
The FTP server node hands each JPEG to the flow as a Buffer in msg.payload, the base64 node turns that Buffer into a text string exactly 4/3 as long, and a function node wraps the string into the document the database node inserts. The flow order is fixed: FTP output, a one-line function that stores the JPEG's length in msg.rawLength (a number), the base64 node in encode mode, the upload formatter, the insert. Remembering the length first is what makes the round-trip test in the next section possible.
For the example: 400 KB × 4/3 = 533 KB per document; 57,600 parts × 533 KB = 30.7 GB per 16-hour day; 30.7 GB × 30 days = 921 GB.
The formatter is one function node. The field names are this example's own illustrative choices — change them to whatever your queries will filter on:
// upload formatter: one document per inspected part
const fields = msg.fields; // object, set by buffer-parser from the result string
const doc = {
partId: fields.serial, // string, e.g. "SN00412"
verdict: fields.verdict, // string, "OK" or "NG"
station: "ST03", // string, fixed per camera
ts: new Date().toISOString(), // string, insert time on the controller clock
imageBase64: msg.payload // string, base64 from the base64 node
};
msg.payload = doc;
return msg;
Configure node-red-contrib-mongodb4 for an insert into your collection; it takes the finished document from msg.payload. Document stores cap a single document — MongoDB's limit is 16 MB — so the ceiling for this design is a JPEG near 12 MB, thirty times the example frame. Capturing several frames per trigger changes the shape, not the design: the set from multi-image capture per trigger becomes an array of base64 strings in one document.
How do you verify the insert worked?
Send one part through, query the collection by its part ID, and decode imageBase64 back to a Buffer — the decoded length must equal the original JPEG length exactly, 400,000 bytes on both sides in the example, and the decoded image must open and look like the part. In order:
- Watch the image leg alone: trigger the camera and confirm the image-output node shows the part. No database needed yet.
- Insert exactly one part, then query the collection by that part's ID and confirm one document comes back with all five fields filled.
- Run the round-trip decode below; it must pass without printing an error.
- At production rate, compare documents per minute against parts per minute for ten minutes; any gap means the FTP leg dropped frames, not the database.
// round-trip test: prove the stored string decodes to the exact original bytes
const decoded = Buffer.from(msg.payload.imageBase64, "base64");
const originalLength = msg.rawLength; // number, saved from the JPEG buffer before encoding
if (decoded.length !== originalLength) {
node.error("round-trip failed: " + decoded.length + " vs " + originalLength);
}
For the example frame the numbers close exactly: the FTP leg delivers 400,000 bytes, the base64 string holds 533,336 characters, and decoding returns 400,000 bytes — no error, image opens, part visible. Image intact, record queryable, one insert per part: the flow is proven end to end.
What breaks this flow most often?
A redeploy restarts the FTP server inside Node-RED, the camera's FTP client then stops delivering until it reconnects, and the flow looks dead even though only the image leg broke — the four quieter failures below are just as final and harder to see.
- Deploy without reconnect. The fix is the camera's FTP retry from the camera section, plus a dashboard timestamp for the last image received, so a silent gap is visible within a minute.
- Command sent as a plain string. The camera ignores bytes whose framing it did not expect. Build the command in buffer-parser exactly as the camera's communication manual defines it, terminator included.
- Buffer inserted instead of base64. The insert fails or stores a corrupt document. The base64 node must sit between the FTP output and the formatter, in encode mode.
- No index on the query fields. Thirty days of the example is 1,728,000 documents, and queries without an index on partId and ts slow down noticeably past a few hundred thousand. Create the index when you create the collection.
- Clock confusion. The ts field is stamped by the controller clock, not the camera's, so camera-side event logs and database records can disagree by seconds. Put controller and camera on one NTP source, or accept and document the offset.
When should images go to disk instead of the database?
Keep one insert per part while sustained traffic stays near the example's 0.53 MB/s of base64 at one part per second, and split storage once a line passes roughly 2 MB/s, because document count, index size and backup windows all grow with every stored byte. The table sizes it:
| Line rate and frame | Base64 write | Database growth, 16 h day | 30-day total | 1 TB drive? |
|---|---|---|---|---|
| 1 part/s, 400 KB JPEG (example) | 0.53 MB/s | 30.7 GB | 921 GB | Yes — 92% full |
| 5 parts/s, 400 KB JPEG | 2.7 MB/s | 154 GB | 4.6 TB | No — NG-only or files |
| 5 parts/s, 128 KB JPEG (1.6 MP) | 0.85 MB/s | 49 GB | 1.47 TB | No — 20-day full window |
The middle row is the boundary case: the insert rate is still comfortable for the database, but 4.6 TB is not comfortable for a controller. Past roughly 2 MB/s of sustained base64 we write JPEGs to the controller's disk — numbered filenames keep them sortable, and saving multiple Node-RED images with unique names covers the naming pattern in detail — while the database keeps only a metadata document of a few hundred bytes per part: partId, verdict, ts, file path. Backups shrink by the same ratio, and cloud archiving of barcode inspection images shows the off-site pattern when retention rules demand it.
A faster line does not have to leave the database, because frame size moves the same boundary: a 1.6 MP frame is about a quarter of a 6 MP frame's area, so its JPEG lands near 128 KB and five parts per second writes about 0.85 MB/s — under the 2 MB/s line, even though the row above shows the trade is a 20-day full window rather than 30. The compact option for such lines is a 1.6 MP smart camera rated at 60 fps with its 16 mm lens:
- Interface
- GigE
- Resolution
- Up to 3 MP
The decision, per case: one part per second at 6 MP — keep the database as built. Five parts per second at 6 MP — NG verdicts to the database, every image to disk. Five parts per second at 1.6 MP — the database still holds everything, for about 20 days. If you are sizing the whole station rather than the archive, the system configurator steps through camera, controller and storage, or ask us via the contact page before you order.
Questions
Does base64 encoding change the image or slow the flow down?
No. Base64 is lossless: decoding the 533,336-character string returns the original 400,000 JPEG bytes exactly, so an auditor sees the frame the camera took. The costs are size and CPU only — the string is one third longer than the binary, and encoding a 400 KB buffer takes a few milliseconds on a controller-class processor, negligible next to one insert per second.
How many images fit in one database document?
One image per document is the simple case, but a document can hold an array of images from a single trigger. Four 400 KB JPEGs become about 2.1 MB of base64 in one document, far inside the 16 MB cap; you approach that cap only near 12 MB of raw image data, thirty times the example frame.
Can the PLC get the pass/fail while the database stores the image?
Yes, and keep them separate. The result string reaches the PLC over its own link in milliseconds, while the FTP push, base64 encode and insert run in parallel and may land a second later. Never let an ejector wait on a database write: a busy or restarted archive must slow reporting, never the cycle itself.
How do I stop the image archive from filling the drive?
Three levers, in order of effect: store only NG verdicts, which at a 2 percent rate turns 30.7 GB per day into about 0.6 GB; shorten retention with a TTL index that deletes documents past your window; or move JPEGs to disk files and insert a few-hundred-byte metadata record per part. Decide before go-live, from the sizing table in the guide.
Can Node-RED collect images this way from IP cameras too?
Yes, with a different front end: an IP camera is pulled, not pushed, so the flow requests a snapshot over HTTP or RTSP and then encodes and inserts it exactly as in this flow. Trigger, base64, formatter and insert are unchanged, and smart-camera and IP-camera records can share one collection with the same fields.



