Our factory is looking to move to using barcode scanners to be able to pre-fill tracking/part number data onto machines as we move pallets around. After a bit of research and thought, I came up with the following possible setup to allow us to use barcodes/scanners everywhere without requiring a vision client instance nearby to capture the data string.
I'd appreciate feedback on the setup as a whole or individual parts, like if I missed any possible pitfalls, 'gotcha's, completely forgot something or possibly better variable/member names/process flow.
Expected Process Flow
Operator scans a machine identifying QR/barcode at the load point of the machine (If a machine has multiple load points then each load point is a unique code)
Operator then scans the QR/Barcode on the pallet or travelers provided with the pallet of parts
Ignition fetches the data associated with the pallet and copies required information into the data entry section of the machine (mainly SN's or lot numbers)
Extra Goal:
For scanners that are wirelessly connected to a base, be able to take a scanner from one area to another, connect it to a new base, and be able to execute the above process flow with no additional configuration (some zebra scanners can apparently connect to the new base by scanning the barcode on the base)
System Hardware Setup
One Ignition gateway.
Barcode Scanners, either:
Barcode scanner 'Base Station' (I've seen some that claim to be able to connect up 10 bluetooth/wireless scanners to a single base)
Connection from these bases to ignition will be made via a serial to ethernet gateway.
Bases will be placed strategically around the floor to allow maximum coverage
Wifi connected barcode scanner
Machine Configuration Changes
Unique ID's on load and unload points, either in QR or barcode format
Scanner Configuration
For the wifi connected scanners, no additional configuration is needed other than an DHCP IP reservation
For the multiple scanners connected to the base station:
Scanners are configured to send their serial number either before or after the scanned data, with a separating delimiter
Ignition Setup
UDT containing the following members:
Name: String - Human readable name of the device or base station, used to help identify what the device is
Location: String - Human readable name of the expected location of the device (used more for the scanner bases)
Delimiters: StringArray - allows user to configure what delimiter should be expected at beginning/end of barcode scanner data as well as the delimiter that separates data from the barcode identifier(SN), in the order of 'StartingDelimeter', 'IdDelimiter', 'EndingDelimiter'
RawScannerString: String - raw data from the scanner/base station
PairedScanners: Dataset - a list of scanner id's that ignition has seen send data through this base, with a column for ID and last seen time
ScannersData: Dataset - a dataset with a column for scanner ID and 3 to 5 columns of the data from the last x number of scans. 1 row per scanner ID
Each connected device (wifi connected scanner or a base) gets 1 UDT instance created for it. The raw data from the connection is pushed to 'RawScannerString'
RawScannerString has an 'onValueChanged' script that performs the following:
Parses the raw string sent from the scanner and extracts the ID and the actual data
Updates PairedScanners with the latest timestamp for the associated ID (or creates a new row if the ID is new)
Updates ScannersData, placing the latest data in the leftmost data column associated with the scanner ID and shifting the existing data to the right (dropping the rightmost column's existing data) or creates a new row if the ID is new.
Sends a gateway message to alert a processing message handler that scanner ID 'x' has passed new data in. Message payload contains the source device UDT name/path and the scanner ID that caused the value change.
The gateway has a message handler with a name along the lines of 'ScannerHasNewData' or such, with a process flow of the following:
Grab the row of data associated with the provided scanner ID from the UDT that sent the message
Parse the most recent data the scanner sent to determine if the data is a machine load/unload point ID or a traveler ID
Based on the determination of what the data is the system does 1 of the following:
If the data is a machine load point identifier or blank, do nothing
If the data is something the system doesn't recognize, log a warning
If the data is a traveler ID, check the previous scanner data to see if there was a machine identifier present
If a machine identifier is present, grab the associated traveler data from database and transfer the needed traveler data to the machine.
Other Thoughts
Machine load point IDs and traveler IDs need to have 2 distinct string patterns (Our factory already does this via a prefix)
Setup requires a standard DB format for storing the traveler data for multiple types of parts
Some sort of mapping data is required to store what machines need what data and in what order, and what tags they go to.
Depending on what data the user is trying to move between DB and machine, either the main processing message handler gets very large to handle all possible processes or multiple smaller message handlers need to be created/called based on what the expected data/process is.
Machines will still be configured to allow manual entry in the case the system is not working
Thanks for taking the time to read this, I look forward to your feedback.
This is an interesting topic and I I have a similar requirement.
I was thinking about the same approach of assigning a QR/Barcode for each machine load point.
I need guidance on the barcode Scanner selection.
Could you please suggest Barcode Scanner vendor or model which can connect more than one barcode scanners to a single base.
And the other option you suggested is having multiple bases, are you suggesting a multiple bases with single barcode scanner or a base per each scanner? If we connect multiple bases with multiple scanners, will it be treated as multiple inputs to the server or single input. My worry is, if multiple scanners are triggered at the same time, will we see the data as merged in Ignition?
Interesting application. (I somehow missed this six months ago.)
I'd use a long-lived gateway thread to read from the ethernet-to-serial channels, with scan results posted to long-lived java LinkedBlockingQueue or similar.
I'd use a quick timer event to drain events from the queue in batches for processing, maintaining state in a dataset or document memory tag (write-only while running, read on gateway/project startup to re-initialize).
Consider not using multiple UDTs. Keeping all state together will simplify the movement of a scanner from one base station to another.
Under no circumstances should this complex logic be called from a tag's valueChange event (or any other tag event). Not appropriate, and can disrupt all the tags in your gateway.
The Zebra STB3678 cordless cradle can run in multi point to point mode where you can connect up to 7 scanners to it. The associated cordless scanner models are the Zebra DS3678 series. This model is the one I'm basing most of my design around since we use a lot of zebra scanners already (not these models yet, but I'm hoping to get some)
This would depend entirely of what your chosen scanner bases are capable of. In some cases it may make more sense to have a single base with a single scanner paired for use on a specific machine, vs maybe one base with multiple scanners paired for use in a holding area or warehouse.
To my knowledge, at least with Zebra scanners, a scanner can only be paired to a single base at a time. So if I had a scanner paired to receiving dock A base, then went to receiving dock B base and paired it, receiving dock A base would no longer receive data from that scanner.
For this case, would one column of the dataset be the paired base ID for the scanner? Or would you omit the scanner base ID entirely and just manage records and pair incoming data with existing data based on the scanner serial number?
Does this include Gateway Tag Change event scripts?
Yes. But include the base station in the report tuples that get queued--that is important for your process, I presume. Or convert them to locations.
The dataset or document just holds the unfinished parts of a multiple-scan operation. Once recorded in your actual DB, that scanner's row drops out until it starts another operation.
No. Those are in a project and have their own threading. They do not impact the tag subsystem (other than overall CPU load).
We use CipherLab RS36s, and the Perspective app/Barcode Scanner Input component with good results so far. We have about 25 guns in use with plans to expand.
CipherLab provides free EMM software called ReMoCloud. It's a little rough around the edges, but it's free and makes device management much easier. You could use another EMM if you want.
The devices also have built-in NFC which we use with our employee badges for user login. The scanning engines are solid, and I've been pleased with the performance over the last year of operation.
We have different profiles setup in ReMoCloud, so all we have to do to get a gun production ready is boot the gun up and scan an enrollment code. Within 2-3 minutes the gun is provisioned and ready to go.
I'm guessing you are running a Perspective project on each one of those? Is the project bare bones to just have the scanner input component or can the operators double check the scanned data/discard the scan/other record handling tasks?
I'll have to look further into the software and if Zebra has an equivalent other than their standalone software for setting up the scanners. The built in badge reading is kind of cool but we don't utilize badge logins at this time.
We looked into the equivalent Zebra scanner to the RS36 and I did not like how their EMM stuff was setup. I don't remember specifics, but it seemed to have newer functionality glued to an older framework and wasn't seamless as it should be, IMO.
The scanner project is pretty barebones, a couple of dropdowns, scanner inputs, and a scan history table, with manual entry capability as well. I use these same guns for "on the floor" management projects without issue.
The only trouble I've had with these is trying to run a carousel component. It can't handle that, but I think that is partly due to the carousel being heavier than it should be (embedded views).
Just to clarify, is this instead of making a TCP device connection in Ignition for each ethernet-serial gateway device? Would the gateway thread make socket connections to each ethernet-serial gateway instead?
I was able to get our ethernet-serial gateways to talk to ignition by adding them as a TCP device. Is it better to use them as a TCP device or rely on the gateway thread socket connections?
My experience with Zebra's software so far has been 'meh'.
Struggled a bit with the setup barcodes it generates, was able to consistently get scanners into a stuck state where they would not communicate scanned data, claiming to be in setup mode, but then also would not accept setup barcode data. Would normally have to wait a while for the scanner to time out and then try again.
I was thinking of using multiple scanners connected to the user pc on USB ports with Ignition client receiving the data on a Text box having focus. So, the data is processed directly on the Ignition client.
This is to avoid additional TCP level data handling.
Looks like, if we want to connect multiple bases (with a scanner paired to each base) to a single server/PC, we have to go with the Serial to Ethernet converter and process the scanned data in the backend.
Only Ignition's Vision Client can do this, using its ability to run client-side serial ports (including USB-emulated serial ports). But if you don't open the Vision Client, or you accidentally open two, you will have problems.
Using serial-to-ethernet converters communicating with the gateway instead of the client is a robust solution, and is agnostic as to which kind of client is used.
You are right. I will have to see if the gateway communication implementation using Ethernet converter can be tested quickly. Thanks a lot for your valuable inputs.
My brain seems to not be working well today and this is way outside of what I normally work on, but I wanted to confirm the general logic flow of the long lived thread for listening for the TCP messages from the ethernet-serial gateways:
Thread startup, check for previous instance and interrupt if it exists. Something along the lines of
bvm = system.util.globalVarMap("PersistentTCPScannerListener")
def TCPClientListener():
me = Thread.CurrentThread()
priorThread = bvm.put('runningClientListener', me)
if priorThread:
priorThread.interrupt()
priorThread.join()
Create a socket for each of the defined scanner base stations / wifi scanners.
I'm not sure how to handle sockets that failed to create, maybe keep a list of the ID's of the base stations that were not able to have a socket created along with a timestamp of last creation attempt? And then check on every loop to see if the timestamp is older than the retry period and try to create the socket again if it is?
While the thread is not interrupted, check each of our created sockets for any incoming data
If we have incoming data on a socket, read all the incoming data and then stick it into a LinkedBlockingQueue
After each socket data read, re-check for interrupt and exit if present
On thread interrupt, loop through all our created sockets and close them.
My concerns are in regard to the sockets, since I'm not sure of a surefire way to keep track of all of them to be able to close them if an exception happens that prevents the socket closing part of the thread to run.
Can sockets be put into a concurrentHashMap without issues/memory leaks? Should I even do that? Or should I just rely on socket timeouts and creation retry?
For some wifi scanners I know they behave as a client not a server, so I'd have to make a different thread to create a server socket to collect their messages.
Consider making a thread per socket. Much simpler code (Thread interrupt will interrupt any wait on a java socket). Have all of the threads deliver to the same concurrent queue for singulation in your processing event/thread.
Each socket thread would have an outer function that opens and connects the socket. If that fails, retry after a long-ish interval. This outer function would run in an infinite loop, stopping only on interrupt. When it succeeds, it runs an inner loop that reads data until any error, with a finally clause ensuring the socket closes.
I tend to subclass Java's Thread for these kinds of things, specifically to mark them as daemon threads--they won't prevent gateway restart itself.
Startup would be running through a list of thread names that need to exist, one per base station, looking in the persistent dictionary for prior instances to interrupt and join before starting the new one.
The processing task would be in the same project library script, but be called just by a reasonably fast timer event.
Right, a long-lived thread to .accept() connections, which spawns time-limited threads to receive the messages.
from java.lang import Throwable, Thread
from java.net import Socket
from java.io import BufferedReader, InputStreamReader, IOException
from java.util.concurrent import ConcurrentLinkedQueue
bvm = system.util.globalVarMap("PersistentTCPScannerListener")
tcpDeviceConfigurations = [
{"baseID": "DESK_STAND_SERIAL1", "hostname": "hostname1", "port": 1002},
{"baseID": "DESK_STAND_SERIAL2", "hostname": "hostname2", "port": 1002}
]
SOCKET_RETRY_PERIOD = 30000
SCANNER_CONCURRENT_QUEUE = bvm.setdefault("tcpScannerQueue", ConcurrentLinkedQueue())
class TCPClientListener(Thread):
connectionRetryInterval = SOCKET_RETRY_PERIOD
def __init__(self, deviceConfig, concurrentQueue):
self.setDaemon(1)
self.hostName = deviceConfig['hostname']
self.port = deviceConfig['port']
self.cq = concurrentQueue
def readTCPData(self):
try:
reader = BufferedReader(InputStreamReader(self.socket.inputStream))
while reader:
currentLine = reader.readLine()
if currentLine:
self.cq.offer(currentLine)
except Exception as e:
logger.warn("Jython exception while reading TCP socket data", shared.later.PythonAsJavaException(e))
except Throwable as t:
logger.warn("Java exception while reading TCP socket data", t)
finally:
self.socket.close()
return
def run(self):
while not self.interrupted():
try:
self.socket = Socket(self.hostName, self.port)
self.readTCPData()
except IOException as ioe:
logger.warnf("Unable to connect to endpoint, retrying in %d seconds", self.connectionRetryInterval/1000, ioe)
self.sleep(self.connectionRetryInterval)
except Exception as e:
logger.warn("Jython exception while attempting to monitor TCP socket", shared.later.PythonAsJavaException(e))
except Throwable as t:
logger.warn("Java exception while attempting to monitor TCP socket", t)
def stop(self):
self.interrupt()
def startTCPListeners():
for device in tcpDeviceConfigurations:
threadID = device['baseID']
newThread = system.util.invokeAsynchronous(TCPClientListener, kwargs={"deviceConfig": device, "concurrentQueue": SCANNER_CONCURRENT_QUEUE})
priorThread = bvm.put(threadID, newThread)
if priorThread:
priorThread.interrupt()
priorThread.join()
startTCPListeners()
One other thing I thought of, if I end up changing the name of a thread for some reason (device removed or maybe device moved to different location and needs to be renamed accordingly), that would mean I would have a having thread leftover on script restart, correct? Since the new version of the code would have a different key for the thread, the old one would never be stopped.
Looks pretty like what I'd do, with one flaw: You don't use system.util.invokeAsynchronous() with Thread subclasses. You just .start() them. Obtain, interrupt, and join the prior thread before you start the new one.
You can stick a set() of the thread names you start in the bvm, so you can identify any leftovers after startup. Consider using socket addresses as keys (IP:port), as those would be the definitive non-duplicating targets for your sockets.