System designHow-to

Stop duplicate barcode scans in a batch table: event over polling

Swap the 500 ms timer for a confirmed-scan event and a last-value guard, so each barcode scan inserts exactly one batch table row and duplicate scans stop.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01Why the 500 ms timer writes duplicates
  2. 02What you need
  3. 03Wire the confirmation into one event
  4. 04Move the write into the event
  5. 05Worked example: a 20-part batch against the 500 ms timer
  6. 06Make the database the backstop
  7. 07Verify it worked
  8. 08Common mistakes
  9. 09Questions

A timer that samples a scan field every 500 ms keeps firing while the field still holds the last code, so one scanned part can become two, three or four rows in a batch table. This guide is for anyone whose handheld reader feeds an HMI or SCADA client that writes scans into a database. You will move the write into an event that runs once per confirmed scan, add a compare against the last accepted value to absorb trigger bounce, and back the table with a unique constraint on batch and serial. A 20-part batch then produces exactly 20 rows, no matter how fast the operator scans.

Why the 500 ms timer writes duplicates

The timer reads whatever sits in the field when it ticks; the scan itself never tells it anything. Two failure modes follow, and both corrupt the batch.

Repeats. The field keeps each code until the next scan replaces it. If the operator averages one part per second, the 500 ms timer samples every code twice — 1.0 s ÷ 0.5 s = 2 samples — and runs the insert twice per part. Slow the pace to one part every 2 s and each code sits through 4 ticks, so it is written 4 times. The length check does not help: a repeated code is a perfectly valid code.

Losses. Two scans that arrive within one 500 ms window leave only the second code in the field at tick time. The first part never reaches the table, and nothing on the bench tells you.

Both modes have the same root cause: the write is scheduled by the timer, not by the scan. Fire the insert when the scan is confirmed and both disappear.

What you need

Three things, most of which you already have:

  1. A reader that ends every scan with a terminator. The Enter suffix is what tells the client a scan is complete. The add an Enter key with programming barcodes guide shows how to set the suffix, and the CR LF suffix guide shows how Enter commits the value on the HMI side.
  2. An input that commits on that terminator: a single text field with deferred updates, or a tag the reader writes over Ethernet.
  3. A batch table with a unique key on batch and serial, added in the SQL step below.

A wired USB reader drives the text-field route directly. An Ethernet reader writes to a tag or a PLC instead, and the tag change becomes the trigger:

Handheld code readersHikrobot MV-IDH2000P/13NR/05RN/U Industial Wired Handheld Code ReaderMV-IDH2000P/13NR/05RN/U
Interface
USB
Connection
Wired
$179.00View part
Handheld code readersHikrobot MV-IDH3000/13NR/05RN/LE Industrial Wired Handheld Code Reader (IP LAN)MV-IDH3000/13NR/05RN/LE
Interface
Fast Ethernet
Connection
Wired
$379.00View part

Wire the confirmation into one event

Two routes, depending on where the scan lands.

Field route. Put the scan into one text field and enable its commit-on-Enter option (Defer Updates on the input field). With it on, a propertyChanged event script watching the text property runs only when Enter confirms the value — once per scan, never on partial input. That single change removes the timer and its failure modes together.

Tag route. Point the reader at a tag and run the logic in a Tag Change script on that tag. The tag only changes when a scan arrives, so the script inherits the once-per-scan behavior without any timing of its own.

Move the write into the event

  1. In the event handler, read the newly confirmed value into a string variable, scan_value, which starts empty at the beginning of each batch.
  2. Apply the sanity filter you already had: reject values of four characters or fewer. A scan arrives complete in one burst, so partial values are rare, but the check still catches an empty field or a stray keystroke.
  3. Compare scan_value with the last accepted value for this batch and skip the insert if they are identical. This is the trigger-bounce filter.
  4. Insert the part into the batch table. On a duplicate-key error, skip or update the row's quantity, as the SQL step below shows.
  5. Update the last accepted value only after a successful write, so a failed insert can be retried on the next scan.
  6. Show accepted and skipped counts at the bench; a climbing skip counter makes double-pulls visible immediately.

In pseudocode, with illustrative variable names:

scan_value = ""    # string, the confirmed scan text
last_value = ""    # string, last value written for this batch

on scan confirmed:                     # commit on Enter, or tag change
    if length(scan_value) > 4 and scan_value != last_value:
        insert scan_value into batch_parts
        last_value = scan_value

Worked example: a 20-part batch against the 500 ms timer

Inputs. Tick interval 500 ms, from the timer in the original set-up. Batch size 20 parts, the top of the 5-to-20 range the operator handles. Pace one part per second, an illustrative bench pace — measure your operator's real pace with a stopwatch. Trigger bounce: the reader re-sends the same code once on 3 of the 20 scans, an illustrative condition you can read from your own event log.

Rule. A polling writer runs once per tick while a valid value sits in the field, so rows = parts × samples per part, where samples per part = pace interval ÷ tick interval. An event writer runs once per confirmed send, minus sends identical to the last accepted value.

samples per part = pace interval / tick interval timer rows = parts × samples per part event rows = confirmed sends − repeats equal to the last accepted value

Arithmetic. Timer route: samples per part = 1.0 s ÷ 0.5 s = 2, so rows = 20 × 2 = 40, of which 20 are duplicates. At a 2 s pace the same batch writes 20 × (2.0 s ÷ 0.5 s) = 80 rows. Event route: confirmed sends = 20 + 3 bounces = 23, so rows = 23 − 3 = 20.

Decision. Any pace slower than one scan per tick makes the timer write parts more than once — at these numbers, every part exactly twice, 20 bad rows per batch. Any pace faster than one scan per tick starts losing parts instead, and no SQL filter recovers a scan that never landed. Move the write to the event: exactly 20 rows, with bounce absorbed by the last-value compare.

Make the database the backstop

The client-side filter handles normal operation. The constraint handles everything else: a second client station on the same batch, a retry after a network blip, a script edit deployed mid-shift. Give the table a unique key and let a repeat insert fail loudly:

CREATE TABLE batch_parts (
  batch_id  INTEGER      NOT NULL,
  part_id   VARCHAR(32)  NOT NULL,
  serial_no VARCHAR(64)  NOT NULL,
  qty       INTEGER      NOT NULL DEFAULT 1,
  UNIQUE (batch_id, serial_no)
);

The columns are illustrative — rename them to your own. Write the insert so a duplicate-key error runs an update that adds one to qty for that batch and serial, or skips outright if repeats in one batch are never legitimate. The qty column costs nothing today and saves a table rework the day a part genuinely appears twice.

If this batch table later feeds a pass/fail comparison, the same unique-key table carries it: see barcode match verification: build a pass/fail check station.

Verify it worked

Four bench tests, in order:

  1. Scan five different codes in quick succession. Expect exactly five rows, one per code, with no timing pattern tied to the old 500 ms tick.
  2. Scan the same code twice on purpose. Expect the second send to be skipped — five rows still — or qty 2 on one row, whichever policy you chose.
  3. Scan two different codes less than 500 ms apart. Both rows must appear; this is the case the timer version silently dropped.
  4. Restart the client mid-batch and scan a code already in the table. The insert must fail on the constraint rather than add a row, which proves the database backstop works without the client guard.

If the skip counter climbs during normal scanning, enable the reader's own repeat suppression as well; filtering at both ends costs nothing.

Common mistakes

  • The timer still runs. Beside the event it re-writes every value the event already accepted. Delete the timer; lengthening its interval only makes duplicates rarer without removing them.
  • The field appends instead of replaces. New scans glue onto the old value, every code looks longer and new, and the guard passes them all. The mechanism is in barcode scanner appends to the previous value: fix it with selectAll.
  • The suffix never arrives. Without Enter the commit event never fires and rows stop appearing altogether. Type a value by hand and press Enter: if that writes a row, the event path is fine and the reader suffix is the problem.
  • Guard in the client only. A second station or a replayed insert writes duplicates the client never sees. The unique constraint is what catches those.
  • Writes slower than scans. Twenty codes in under ten seconds can outrun the database write; queue confirmed scans and write them in order instead of inserting directly in the handler. The queue pattern is in the PLC to HMI barcode data handshake guide.

The same pattern covers line-side stations: a fixed reader writing one confirmed value per trigger needs the identical guard and constraint. A compact fixed reader for that job:

Fixed code readersCompact fixed industrial barcode reader, 1.2 MP, USB 2.0, 60-120 mm reading distance (LS-CRD-0047)LS-CRD-0047
Resolution
Up to 3 MP
Shutter
Global
Interface
USB
$559.00View part

To pick handhelds for the bench route, start with the handheld code readers, or narrow by interface in the code reader finder. For help matching the write pattern to your client and database, contact us. More system-side scan patterns are collected on the systems topic page.

Questions

Can I just add a unique constraint and keep the 500 ms timer?

The constraint stops duplicate rows, but it does not fix the timer's second failure: two scans that land inside one 500 ms tick leave a single value in the field, so one part goes missing with no error anywhere. The confirmed-scan event fires once per scan, so nothing is lost, and the constraint stays as the backstop for retries and restarts.

How do I stop a scanner that sends the same code twice when the operator holds the trigger?

Layer two filters. Most handheld readers offer a read mode that suppresses an identical code until a different one is scanned or a set time passes — set it in the reader's own configuration. The compare against the last accepted value in your event catches anything that still gets through, so the table sees one row per part.

The same serial can legitimately appear twice in one batch — should the guard skip it?

No. Add a qty column that defaults to 1 and turn the duplicate-key case into an update that adds one to qty. The unique key on batch and serial still guarantees one row per serial, and the quantity now records the physical count. Skipping would silently under-count real parts.

Does the pattern change if the scanner is wireless?

No. The confirmation still arrives as the Enter suffix through the base station, and the event fires when the value lands, so radio delay only moves the row's timestamp later — it cannot create a second row. If scans queue while the link recovers, process them in arrival order and the batch stays correct.

Where should the dedupe live: the client event or the database?

Both, for different jobs. The event guard keeps the table clean during normal operation and gives instant feedback at the bench; the unique constraint is what stands when a second client, a retry or a restart replays the same insert. The constraint costs one index on batch id and serial.