Why does printing a label through DeviceHub require uploading a file to the database?
I have been going through the code required to print a label to a Zebra label printer in Acumatica. For those who are interested, below is the most minimal version of printing a label from DeviceHub using Acumatica Framework/C#. You can run this code in a PXAction, for example, in order to click a button and print a label. I have obfuscated the Guids, so that’s why they might look a little weird.
The FILEID GUID referenced in the CreatePrintJob call can reference any file that has been uploaded to Acumatica. You just need to figure out its GUID, which appears in the URL when you open the file (and there are a bunch of other ways to figure out the GUID as well, including looking inside the database). The printer GUID can also be grabbed from the database, or by making the appropriate method calls in C# (I don’t know how to get it out of the GUI at the moment).
My only problem with this whole paradigm is that it REQUIRES uploading a file to Acumatica. Some of the built in code that wraps these functions in Acumatica will also delete the file once its printed. So basically, we upload a zpl file to Acumatica, then call this method with a reference to the fileid, and then we destroy the zpl file.
Of course, wouldn’t it make more sense to be able to build a string on the fly, and send it to the printer? All the uploading and downloading of files creates a lot of delay in sending the file to the printer. The standard objection here might be...well what happens if DeviceHub is offline? Don’t you want your print job to execute when it comes back online? And my answer to this, with a focus on label printing in particular, is absolutely not. For some warehouses, they may want this. For us, if the label doesn’t arrive in 30 seconds, it may as well not arrive at all. We certainly don’t want it arriving, in bulk, 30 minutes later if DeviceHub has been offline for some reason (and even worse…. 2 days later in extreme cases...say a weekend outage). All this will do is cause the printer to kick out 100 untimely, useless labels that will have to be thrown away.
I don’t currently see a way to generate a string in code and send it to DeviceHub for printing, without an intervening file upload. Does anyone know of a way to do it?
For those who are interested to know, I am using PrintNode to replace the Device Hub in our FBA Asgard Labels printing solution (https://www.acumatica.com/acumatica-marketplace/asgard-alliance-software-asgard-labels/). It is much easier to setup (just a login really), faster then the DH and more reliable. It also works regardless of the Acumatica version, so no need to change anything when upgrading.
Currently by default the labels are generated(or retrieved from a third party service) when the shipment(and packages) are confirmed. If you are dynamically generating labels on a per print job basis what you are saying does make sense but most customers aren’t doing that.
I think there’s a fail safe that’s built into the current design/setup and thus becomes a trade-off between speed and reliability.
If a DeviceHub goes down (or the connection to it fails), the print jobs can continue to be queued and Acm can continue to process transactions. A new DH could be spun up and you can start printing again.
If the printing process pushed the job to the printer and the printer isn’t available - what do you do? When do you determine if the printer is off-line vs a slow connection vs it’s busy with a job?
Somewhere the jobs need to be queued to take into account processing bandwidth and availability.
Having an option as you’ve described would be nice and maybe that’s something that could be explored in one of the Developer Hackathons.
The jobs aren’t sent to the device hub, they are queued and then the device hub reaches out to your Acumatica instance to retrieve them(and as such must be persisted). I don’t believe there is any way to push a job out to the device hub from Acumatica.
I’m glad that blog post helped. When starting this project, I struggled to get ZPL to go to device hub and actually sent directly to the printer attached virtually to my server on the same network in a dev environment. This allowed me to build a library for EPL and ZPL, but it wasn’t ideal for production.
The problem with this approach was that I needed to print to a printer in another state with only internet access between the printer and my server. This meant Device Hub was a requirement. To understand why the file upload is required, you have to consider how Device Hub works. As I understand it, it is a remote polling application that logs into the Acumatica server, polls for print jobs, and then processes the jobs. This is not a push from Acumatica. That means the print job needs to exist on the server somewhere to await pickup by the PC running device hub on the next poll.
Since Device Hub can access Acumatica in the same fashion as the user on a browser, many legacy limitations simply vanish for communicating with remote devices through heavily guarded networks. On our legacy system, I used to run a telnet session with pass through printing to enable connecting a printer in a similar fashion. This was far from ideal. In the years since that, I switched to something where the file is pushed to the web browser and then processed by a batch file.
This batch file approach is documented in the help, but it requires connectivity to the printer from the machine where the browser downloads the file. This is fine on a PC, but then you lose use of a tablet to receive a PO and print labels using a tablet. Hence, Device Hub, complete with the file upload needed to support polling from an external PC.
@markusray17 As I have gone down the rabbit hole further, while the SignalR message telling DeviceHub what to print is on demand, it appears that the message that is sent only has a pointer to the correct file, and as you said, DeviceHub must then download the file.
I also have taken a look at the “External File Storage” option of Acumatica, and it seems we have the option to store files on the file system. However, it doesn’t seem that files with extension .zpl are copied to external storage.
As it stands right now, the chain of events to print a label look like this:
Query required label data from the database (i.e. inventory info, shipment info, whatever)
Write a file back to the database with label data + label template info to zpl file
Send SignalR message to DeviceHub that we want to print a label
Device Hub inititates a request to Acumatica for the required file
Acumatica queries the database for the file content
Database returns file content to Acumatica
Acumatica returns file information to DeviceHub
When they could look like this:
Query required label data from the database
Combine data + label template in code
Send SignalR message to DeviceHub with content of label to print
This system can get a LOT more efficient I think….
@markusray17 I can’t speak to what most customers do. But I do think that this is a pretty critical piece of good warehouse management, and the solution should be flexible enough to accomodate multiple use cases. I also think when it comes time to print (whenever that is), it should be as fast as possible. So persisting the label jobs somewhere, if they have to printed in response to some action, is fine. But the current setup sort of guarantees that it will be slow to retrieve given the back and forth required to DeviceHub.
I wouldn’t say that it’s guaranteed to be slow unless you have some unusual latency between your device hub and your Acumatica instance. I would say an extra 100-300ms isn’t likely to be noticeable in most use cases when it comes to printing labels.
@markusray17 I am testing it now. it is highly variable. If you print labels back to back, it seems there may be some kind of caching that makes it faster, sometimes as quick as 3-5 seconds. However, if a label hasn’t been sent to DeviceHub in a few minutes, it can be a lag of up to 30 seconds. Keep in mind this is with SignalR active and polling deactivated, so the initial print request message is not queued/polled, it is sent immediately (presumably...I haven’t definitevely proved this with Wireshark but that is how it appers anecdotally). I am also sending a file that is already uploaded to the database, so there is no overhead of having to generate and upload the initial zpl file to the database.
I have been using Acumatica for a while now. The platform can go from being fast to slow for non-obvious reasons, probably as a result of a lot of the caching and other things going on in the platform in the background. My main criticism of this printing system is that you are putting printing at the mercy of what’s happening on the platform at the moment, when we could dramatically reduce those problems by kicking the print job to the file system and then polling or pushing it from there to DeviceHub. In addition, we can leverge SignalR to push messages down immediately, and we can also verify that they have been queued with a push back from DeviceHub.
Anyway, as I’ve mentioned, this is an important topic to me, so I am going to run DeviceHub side-by-side with this product: https://www.printnode.com/en
I will trigger a print job via DeviceHub and via PrintNode in the same code at the same time, and we can see how they stack up. I’ve never used PrintNode before, but it claims to be “blazingly fast”. So let’s see how they stack up against each other. I may make a video to show the results
I’m going to put together a formal benchmark so that things are objectively fair. My observations so far are still anecdotal. When running in SignalR push mode, DeviceHub actually does pretty well from a notification perspective for print jobs. It compares pretty favorable against PrintNode with regards to reaching the destination print server when the print job is sent in code. However, as anticipated, where DeviceHub falls down is in fetching the print job from the server.
Print Node has a consistent < 1 second round trip, from making the API request in code to printer output.
With DeviceHub, the SignalR message is almost as fast, but the downloading of the file by DeviceHub leads to a delay of about 3-5 seconds to print the label. Keep in mind this is with the label template file id hardcoded, so there is no writing of the label file to the database to contend with. Once I add the overhead of writing the label file to database and then sending the print command, I anticipate that it’s going to be a blowout performance wise (especially if a lot of labels are being printed simultaneously).
I would really encourage Acumatica devs to consider using SignalR to push the label message to DeviceHub on demand. If a templating solution is implemented, most label files are sub 10kb (the one I’m testing with right now is 1kb and prints a QR code and some text). If compression is implemented I think there is probably not a label that couldn’t be printed using this technique, and worst case there is always chunking across multiple messages.
With regards to discussion about labels being queued or whatever, I think that obfuscates the issue. Of course, users should have the option to queue labels if that is consistent with their use case. But that simply avoids the issue here, which is that the architecture of this solution is clunky and bottlenecked at the writing of files to the database and the multiple round trips requred when DeviceHub has to download the print files rather than have them pushed in real time. This system also makes it way harder to give real time feedback to the user on the status of the print job when there are problems (was the initial message not received, was there a problem with the file download, etc?).
Note: edited to make it clear I’m talking about DeviceHub in particular, not “Acumatica” generally
So when DeviceHub polls the server, all of its print jobs are currently stored as attachments in the database (label jobs or not)?
There’s actually quite a lot to unpack here.
Point 1: Device hub is polling our server. It is looking for print jobs. Print jobs are best stored as flat files, not in a database (too many reasons...character set issues, binary data, etc, etc). Acumatica devs should read this: https://www.brentozar.com/archive/2021/07/store-files-in-a-file-system-not-a-relational-database/ If you store them as flat files locally, you can poll the server with zero additional load on the database. As currenttly implemented, the more you poll the server, the more you hit the database. This is coupled in a way that will reduce database performance and you really arent’ getting anything in return. That’s bad. So if we’re going to do polling, poll the file system not the database. Acumatica devs should think this through.
Point 2:
Don’t do polling at all. Acumatica is implementing SignalR and other “real-time” protocols that don't require polling at all. The most expensive part of a connection is tear up and tear down. Stop polling if you don't have to. Keep it as an option for those who need it (firewall rules, blah, blah), but don’t make it the default.
Point 3: Right now the code does weird things, like consolidating zpl print jobs into a single file when they exist in multiple files. I’m not sure why this is being done, but it probably wouldn’t have to be if you just stored the print jobs as flat files in a folder.
Point 4: Not everyone runs in the cloud. Allow method of printing where you can hit the printer in real time.
There are probably others. Label printing is a potentially high volume activity, and performance matters.
Per the point about home rolled solutions, yes I could do this using Acumatica framework. We could write file straight to file system, kick it to the printer, etc. We may end up having to do that. But I would prefer to stay within the bounds of the product whenever possible. I may not always be around, and its a lot easier to manage a system for upgrades, etc when you work within the bounds of the system. I am pushing on some of these points also so the system can get better for everyone.
I believe there are records for the print jobs with the attachment of the actual report or raw file.
Right now, the CreatePrintJobForRawFile method uses the delegate pattern to search for appropriate printers tied to the current user/circumstance, and then returns the printer id. I would propose that Acumatica implement a similar pattern for the actual print job file pointer, where you can either return a file pointer to the database, to the local file system, or to an in-memory value read from somewhere else (think a database slot). That would make this a lot more flexible.
If we go down the rabbit hole, these methods eventually call DeviceHubService.SendPrintJobs(). So perhaps it would be possible to extend/overload that method to do something different with the file pointer. I haven’t looked at that code yet.
It also seems like it’s now possible to push print jobs into DeviceHub queues, instead of using polling… I thought that it was coincidental how fast that print job popped into the queue after triggering my method…
@markusray17 I can confirm that Acumatica already has the capability to push print jobs to DeviceHub. This is just another reason why we really shouldn’t be doing file uploads to attach label files, if that data already resides inside the system.