The copy instruction is not lying to you: once in a while the reader's input assembly genuinely holds zero bytes, and an every-scan copy faithfully moves them into your string. The fix is not a better copy — it is a handshake. Copy the barcode data only when the reader's update counter has changed, then write that same counter back to the acknowledge counter so the reader knows the string was taken. The pattern works for any fixed code reader that exchanges an input and output assembly with the PLC over EtherNet/IP, and it takes about ten minutes of rung editing. The steps below use the members such readers expose — Data, LEN or BarcodeLength, UpdateCounter, AckCounter — and end with a bench test that proves the zeros are gone.
Why an every-scan copy returns zero bytes
The reader writes its input assembly on its own schedule, at the requested packet interval (RPI), while the PLC scans on another clock. When your copy executes, the bytes can be in one of three states: the previous string, a fresh string, or a buffer the reader has cleared for the next read. Nothing inside the data distinguishes the three, so a copy riding the scan clock eventually samples the empty one.
The exposure is bigger than it feels. At a 10 ms scan and one code per 1,000 ms — 60 cartons a minute — the ungated copy runs 100 times per code and 6,000 times per minute. One landing in the cleared window every few thousand decodes is enough to salt a shift with strings of 0x00 bytes.
Three symptoms, one race. A string of zero bytes means the copy ran after the reader cleared the buffer. Repeats of the last code are the hundreds of between-decode copies, all moving the same stale bytes. A string half old and half new is a copy overtaken by a rewrite — that one argues for a buffered copy as well as the gate.
The reader side knows the bytes alone carry no 'new' information, which is why the assembly carries a counter too: the echo handshake that clears the data-ready status is covered in the FAQ below. If the mapping itself is the shaky part, start with how to map the implicit I/O assembly — the handshake is useless until the connection owns the right bytes.
The handshake: one counter in, one counter out
Two members make the contract. The input assembly carries the string bytes from Data[0] on, a length member — LEN on some families, BarcodeLength on others — and UpdateCounter, incremented once per accepted decode. The output assembly carries AckCounter. When UpdateCounter changes, a decode is complete and its bytes are stable; you copy them and write the same value to AckCounter; the reader clears its wait and re-arms for the next label. The names shift a little between reader families; the roles do not.
The fix in five steps
- Map both assemblies and confirm the four members: Data[0], the length member and UpdateCounter arriving; AckCounter going back; at an RPI your line rate can live with.
- Declare two tags of your own: LastCounter, a DINT starting at 0, and BarcodeString, an empty STRING.
- Write the gated rung: compare the counter, copy the bytes, move the length into the string, echo the acknowledge, remember the counter. The compare is the only unconditional instruction of the five.
- Copy the bytes with the buffered instruction — CPS in the rung below — not a plain COP. Adapter data changes asynchronously, and the buffered copy snapshots the source inside one scan, so a rewrite cannot land mid-copy. Random garbage between good strings, rather than clean zeros, is that torn copy's signature; a cyclic string-copy fix walkthrough covers its mechanics.
- Echo AckCounter every time you copy. This is the move integrators skip, and it is what re-arms the reader's per-read status.
LastCounter DINT:= 0 // your tag; the reader never sees it
BarcodeString STRING // your tag; starts empty
NEQ Reader:I.UpdateCounter LastCounter // new decode?
BST CPS Reader:I.Data[0] BarcodeString.Data[0] Reader:I.BarcodeLength
NXB MOV Reader:I.BarcodeLength BarcodeString.LEN
NXB MOV Reader:I.UpdateCounter Reader:O.AckCounter // echo: re-arms
NXB MOV Reader:I.UpdateCounter LastCounter
BND
Prove it with one label and a soak
Print a label whose payload you know: BOX0421, seven characters. One read later, BarcodeString.LEN should be 7 and Data[0] should be 66 — ASCII 'B' is 0x42, which is 66. A length of 7 with a zero first byte means the gate fired on something that is not the counter; go back to step 3.
Soak it: read the same label 100 times. UpdateCounter should advance exactly 100 times, LastCounter should equal UpdateCounter when the last copy lands, and a log filter for strings of 0x00 should return nothing. At 60 codes per minute, the soak costs 100 seconds.
Then break it on purpose. Comment out the AckCounter move and read once: the data-ready status now sits set, waiting for the acknowledge counter to match, exactly as the reader's interface document describes the hold. Restore the echo and the status clears — visible proof of which direction the handshake runs.
How fast is the chain? Worst case, the decode completes just after the reader's last packet, so the bytes wait one RPI, the PLC notices within one scan, and the echo rides the next packet back. Assume a 20 ms RPI and a 10 ms scan:
At 60 codes per minute the slot between codes is 1,000 ms, so the handshake takes at most 5% of it. On a serial reader the same decode-to-flag chain is measured and trimmed differently; the serial scan-to-flag latency walkthrough shows the stops along it.
When the reader has no update counter
Some assemblies expose only Data and LEN. The best available gate is then an edge on the length — copy when LEN rises above zero — and it works only while the reader actually zeroes LEN between decodes. A reader that keeps the last LEN high never gives you an edge, and you are back to sampling blind; a per-read counter, sequence number or status register is the trigger to insist on.
The other exit is the transport. Readers that send ASCII over serial, USB or raw Ethernet frame each decode separately, so the new-data information arrives with the frame and the PLC-side rules change with it. The input-methods comparison lays serial, USB and Ethernet against PLC ports; the raw-socket pattern fits readers with plain TCP output; a Modbus RTU reader exposes a per-read register to watch in place of a counter. Whichever you pick, frame the receive buffer properly — one clean frame per decode beats any amount of gating.
Other PLC platforms fight this same race on other wires, with the same cure: PROFIBUS scanner data-block fix shows the counter-and-copy pattern on a cyclic DB transfer.
The transport decides which reader hangs on the PLC, so choose the interface first. For the plain-TCP path:
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Sensor format
- 1/1.7"
- Shutter
- Global
The LS-CRD-0028 reads up to 60 codes/s over its Fast Ethernet interface. A line feeding one carton a second sits sixty times inside that budget.
For the serial-style path, one ASCII frame per decode:
- Resolution
- Up to 3 MP
- Shutter
- Global
- Resolution
- Up to 3 MP
- Shutter
- Global
The LS-CRD-0047 reads at a 60-120 mm reading distance over USB 2.0. The LS-CRD-0052 has a 12 mm focal length and dual aviation plugs.
High-rate logistics loads sit on the GigE family:
- Interface
- GigE
- Resolution
- 6-10 MP
The MV-IDS006M-12C-C is a 6 MP logistics code reader with a GigE interface. One cable then carries the trigger, the images and the decode results together.
Bench checklist
- Gate the copy on the UpdateCounter change — never on the scan clock, never on LEN alone.
- Copy the bytes with the buffered instruction and move the length member into the string in the same branch; if your platform stores string length in dedicated bytes, fill the two length bytes correctly.
- Echo AckCounter in the same branch as the copy, and test the hold once with the echo commented out.
- Advance LastCounter last, then leave it alone.
- Run a 100-read soak with a 0x00 filter and a duplicate filter on the log.
- If the consumer of the string is slower than the line, latch it on the copy pulse or queue it, so the next decode cannot overwrite a value nobody has read.
If the reader side of this link is still on the order list, start in the fixed code readers category and filter by the interface your PLC speaks, or walk the system configurator from the camera step. In-stock items ship within one business day; for a multi-reader line, request a quote and we will match readers, mounting and cabling to the scan budget.
Questions
Why does my barcode string arrive as all zero bytes?
The bytes were zero at the moment of the copy: the reader had cleared the input assembly for a new read, or had not yet written the decode into it. Your copy logic then worked exactly as written — it moved what was there. Gating the copy on the update counter and echoing the acknowledge counter removes that empty state from the paths your copy can take.
Should the scanner array be copied with COP or CPS?
For adapter data that changes asynchronously with the scan, the buffered copy — CPS in this platform's instruction set — is the safer of the two: it snapshots the source inside one scan, so the reader cannot rewrite bytes mid-copy. COP behind a counter gate is usually good enough in practice, but only the buffered copy removes torn strings where old and new bytes mix. The handshake gate matters more than the instruction choice.
What if my code reader exposes no update counter at all?
Gate on the length field if, and only if, the reader zeroes it between decodes — copy on the LEN rising edge. If LEN stays high there is no edge to catch, and the honest options are a per-read status or sequence register, or a different transport: serial, USB or raw-Ethernet ASCII frames give one frame per decode, and Modbus RTU readers expose a per-read register to watch.
Does echoing the acknowledge counter change the reader's behavior?
On readers that implement it, yes: the data-ready status stays set while the reader waits for the controller to update the acknowledge counter for the current barcode, and clears when the acknowledge counter matches the update counter. Without the echo you lose that per-read flag, and anything polling the status bit keeps seeing the same decode instead of a clean per-read cycle.
Can the every-scan copy stay, with a filter for empty strings?
It can, and it will look fixed until the same race delivers a half-old half-new string that sails past the filter. Filtering also leaves the reader waiting: without the acknowledge echo, the data-ready status stays set. The counter gate is one branch of rung logic; the filter is a permanent tax on every scan for a problem one rung removes.



