System designHow-to

Save an IP camera snapshot JPEG to a PLC file over HTTP GET

Save an IP camera snapshot JPEG to PLC storage with an HTTP GET: peer port, credentials in the request string, and a byte count matched to Content-Length.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01The decision up front
  2. 02What you need
  3. 03The request the camera answers
  4. 04Save the JPEG: step by step
  5. 05Worked example: one snapshot per minute over an 8-hour shift
  6. 06How to verify the file is good
  7. 07Mistakes that corrupt cam1.jpg
  8. 08When the PLC is the wrong client
  9. 09Checklist before you commit the design
  10. 10Questions

Yes, a PLC can pull a still image from an IP camera and keep it on its own storage, with no PC in the loop. The PLC is the HTTP client: it opens a TCP connection to the camera, sends one HTTP GET to snapshot.cgi with the user and password in the request string, checks that HTTPStatus comes back 200, and writes the body — exactly Content-Length bytes — to a file such as cam1.jpg. From a PC the same job is a single call, and grab images from an IP camera over an HTTP snapshot URL covers that side of it. This guide walks the PLC side: the function block settings, the header/body split, the byte count that decides when the file closes, and the storage math for archiving.

The decision up front

Feasible, and the right tool when the picture belongs to the station: one frame per event, stored next to the data that caused it. The decision is about rate and storage, not difficulty:

  • Overwrite one file every cycle — always PLC work. Storage stays at one frame's size forever.
  • Archive at one frame per minute — still PLC work with timestamped names: about 22 MB per 8-hour shift per camera, worked out below.
  • One frame per second or faster — hand the saving to a PC or SCADA client. That pace is roughly 1.3 GB per shift and eats storage a PLC needs for logs and recipes.

Nothing here is limited by the network: a 45 kB frame crosses Fast Ethernet in under 4 ms. The ceiling is the file system and your scan cycle, so one frame per second is the comfortable PLC pace.

What you need

  • A PLC with an HTTP client function block and a writable file system. The library call is the HTTPClient FB; vendors that ship one also ship a to-file example, and ST_HTTPClient_ToFile is the pattern name to look for. Storage must be writable at runtime — SD card or user flash.
  • The camera's three facts: address, HTTP port, credentials. Common values are port 81 with page snapshot.cgi; plenty of models answer on 80 instead.
  • One Ethernet cable. The LS-CBL-0117 is a camera data cable, RJ45 with a locking stud, 15 m, which holds the plug in a vibrating cabinet. For a camera that moves on a drag chain, the LS-CBL-0199 is a 3 m high-flex camera cable with screw locks. Copper Ethernet runs 100 m per channel under IEEE 802.3, so one cable covers the whole machine.

Two choices from the cables range cover most installs:

  • Somewhere repeatable to put the camera. The LS-MNT-0040 is a machine vision camera stand, professional slide-table type, height 700 mm with a 400 x 450 mm base; browse the camera stands range or the accessories finder for other reaches.
Camera standsMachine vision camera stand, professional slide-table type, height 700 mm, 400 x 450 mm base (LS-MNT-0040)LS-MNT-0040
Type
Camera stands
$629.00View partBuild a system with it

The request the camera answers

On the wire you send:

GET /snapshot.cgi?user=usr1&pwd=pw1 HTTP/1.1
Host: 192.168.0.50

The camera answers with a status line (HTTP/1.1 200 OK), a header block that includes Content-Type: image/jpeg and Content-Length: N, one empty line, then N bytes of JPEG. The JPEG itself starts with hex FF D8 and ends FF D9 — that pair is how you confirm the file later.

In structured text the settings are the ones the to-file example prints:

TCPClient.PeerAdd  := ADR('192.168.0.50');   // the camera's address
TCPClient.PeerPort := 81;                    // this camera's HTTP port
HTTPRq.RMethod     := HTTP_REQUEST#HTTP_GET;
HTTPRq.Page        := ADR('snapshot.cgi');
HTTPRq.Request     := ADR('user=usr1&pwd=pw1');

usr1 and pw1 stand for the camera account you created. Three details bite here:

  • Credentials travel in clear text in that request line and can land in logs on any hop.

The sequence is older than PLCs: microcontroller sketches do http.get(kHostname, 81, kPath), then responseStatusCode(), skipResponseHeaders(), contentLength(), then SD.open("cam1.jpg", FILE_WRITE) and file.seek(0). Same five steps, smaller chip.

Save the JPEG: step by step

  1. Bring the path up. Camera and PLC on one subnet or VLAN, cable seated, camera answering in its own web page on the port you will use.
  2. Set the peer. TCPClient.PeerAdd to the camera's address, TCPClient.PeerPort to 81 (or 80 on models that answer there).
  3. Set the request. HTTPRq.RMethod := HTTP_REQUEST#HTTP_GET, HTTPRq.Page := ADR('snapshot.cgi'), HTTPRq.Request := ADR('user=usr1&pwd=pw1').
  4. Trigger and read HTTPStatus. 200 means the camera accepted the GET; 401 is credentials, 404 is the page name, and a connection that never opens is the path.
  5. Skip the headers. Consume the response up to the empty line — the skipResponseHeaders step — so the first byte you keep is FF D8, not the H of HTTP/1.1.
  6. Read Content-Length before the first body byte and store it as the target count.
  7. Open, write, count. Open cam1.jpg with FILE_WRITE, seek(0), and append every chunk that arrives:
// counter names are mine; FB names come from the library
bodyLen   : DINT := 0;   // Content-Length from the response
jpegCount : DINT := 0;   // bytes written this cycle
chunkLen  : DINT := 0;   // bytes in the chunk just received

WHILE jpegCount < bodyLen DO
    chunkLen  := readChunk();   // however your FB hands over arrived bytes
    writeChunk(chunkLen);       // append to the open file
    jpegCount := jpegCount + chunkLen;
END_WHILE

IF jpegCount = bodyLen THEN
    keepFile();                 // complete
ELSE
    discardFile();              // short read: retry next cycle
END_IF
  1. Close on the count, not on the timer. The file is finished when jpegCount = bodyLen; a timeout before that means discard, and the next cycle tries again.

The test that proves it: file size equals Content-Length exactly, and the first and last bytes are FF D8 and FF D9.

Worked example: one snapshot per minute over an 8-hour shift

Inputs, each with its source:

  • Polling interval 60 s — your timer, chosen as the archive cadence.
  • Shift 8 h = 28,800 s — the station's working day.
  • Frame size 45 kB — measured once by saving the image from the camera's own page in a browser; scene detail and the quality setting move it, so measure yours.
  • Link 100 Mbit/s Fast Ethernet.
  • Storage 2 GB — the card in the PLC.
frames = shift_seconds / interval_seconds archive_MB = frames * jpeg_kB / 1000 wire_ms = jpeg_bytes * 8 / link_bit_per_s * 1000
  • frames = 28,800 s / 60 s = 480 per shift per camera
  • archive = 480 x 45 kB = 21,600 kB = 21.6 MB per shift; on a 2 GB card that is 2,000 / 21.6 = 92 shifts
  • overwrite instead of archiving: storage stays at 45 kB, one file
  • wire time = 45,000 bytes x 8 / 100,000,000 bit/s = 0.0036 s = 3.6 ms — never the bottleneck
  • at one frame per second instead: 45 kB x 3,600 = 162 MB per hour, about 1.3 GB per shift

Decision: overwrite one cam1.jpg when only the newest frame matters; archive at one per minute with timestamped names and the PLC holds months; at one per second or above, move the saving to a PC.

How to verify the file is good

  • File size equals Content-Length — the same number the header printed.
  • First two bytes FF D8, last two FF D9.
  • The image opens in a viewer, or feeds show an IP camera HTTP snapshot on a remote dashboard once you copy it off the PLC.
  • Every cycle logs HTTPStatus 200; anything else goes to an alarm bit, not to the file.

Mistakes that corrupt cam1.jpg

  1. Headers written into the body. The file starts with the text HTTP/1.1 200 OK — the skip step never ran, and the viewer refuses it.
  2. Closing on the first short chunk. TCP delivers in pieces; if the loop exits when a chunk is small instead of when the count is reached, the JPEG truncates and decodes as image-then-gray.
  3. No Content-Length at all. The camera is chunk-encoding the body. Request HTTP/1.0 — chunked transfer is a 1.1 feature, so a 1.0 request gets a plain byte stream you read until the camera closes.
  4. Overwriting a longer file with a shorter one. Plain overwrite leaves the old tail past the new FF D9. Delete the file each cycle, or the new frame carries garbage after its end marker.
  5. Wrong port. 81 is a common camera port, but many models answer on 80; a refused connection is usually the port.
  6. Polling faster than the camera encodes. Each GET asks for a fresh encode; past a point you receive the same JPEG twice — IP camera snapshot update period: why polling looks sluggish measures where that point sits.

When the PLC is the wrong client

Client Use it when Practical pace Where the JPEG lands
PLC HTTP client FB one frame tied to station events or alarms about one per second PLC file system / SD card
PC or SCADA HTTP client continuous viewing, decoding, long history tens per second PC disk, historian
Microcontroller + serial JPEG camera capture with no network at all a frame every second or two SD card on the module
Smart camera or vision sensor you need a decision, not a picture the camera's own frame rate pass/fail data over Ethernet

On the PC row, downloading an IP camera snapshot through a SCADA HTTP client shows the identical GET with software handling the retries and the history. The microcontroller row is the sketch pattern this task grew out of; JPEG capture from a serial camera module covers its wiring, and serial JPEG camera commands: snapshot vs get picture timing the command order. If what you actually want is inspection verdicts rather than pictures, smart cameras do the checking on the device, and vision camera to PLC over Ethernet: ASCII strings, no serial port shows that data path.

Code readers & vision sensorsSmart cameras & vision sensors

Inspection on the device, no PC

350 models · from $169.00

Checklist before you commit the design

  • Camera address, port, page name and a viewer-only account recorded.
  • HTTPStatus 200 observed from the PLC, not just from a browser.
  • Content-Length present in the response; if not, HTTP/1.0 requested.
  • Write loop closes the file on the byte count, and a short read discards it.
  • First and last bytes FF D8 / FF D9 checked on a real capture.
  • Overwrite handled by delete or seek-to-zero, so no stale tail survives.
  • Camera and PLC on an isolated VLAN; password percent-encoded if it holds &, = or space.
  • Storage sized with the formula above, with a rotation or offload plan for archived frames.

Questions

Does the PLC need to be on the same subnet as the IP camera?

It needs a clear TCP path to the camera's address and port, which one subnet or VLAN gives you for free. Across a router, the port must be forwarded and the camera must answer requests coming from the PLC's address range. If the camera's own web page loads from a laptop but the PLC connection never opens, the path is the suspect, not the camera.

The file is written but no viewer opens it. What do I check first?

Open it in a hex view. A healthy JPEG starts FF D8 and ends FF D9; text reading HTTP/1.1 at the start means the header block was written too, so the skip step never ran. A file that starts correctly but stops short means the write loop quit before Content-Length bytes arrived, and a zero-length file means the trigger fired but no body came. Delete and let the next cycle retry.

Can I archive one JPEG per second on the PLC?

Not sensibly. At 45 kB a frame that is 162 MB an hour, about 1.3 GB per 8-hour shift, and PLC storage serves logs and recipes better. One frame per minute is the comfortable archive pace, roughly 22 MB per shift with timestamped names, and above that a PC or SCADA client should do the saving, with the PLC keeping only the newest frame.

The camera answers 401 although the password is correct. Why?

Some snapshot endpoints ignore credentials in the query string and expect an Authorization header instead, so the GET arrives but never authenticates. Check the camera's CGI reference for the accepted form, send user and password in the order it specifies, and percent-encode any &, = or space inside the password so the request string does not split early.

Should I reuse the TCP connection between snapshots?

Open a fresh connection for each snapshot. One GET is one frame, so reconnecting costs only the handshake and it recovers cleanly from cameras that drop idle links. Keep-alive saves a few milliseconds but can leave a half-open socket that makes the next request hang until its timeout; on an archive cadence of seconds, the saving buys you nothing.