Printing to a Label Printer via Device Hub

About 18 months ago, I wrote a customization to generate raw label files using both EPL (Eltron Printer Language) and ZPL (Zebra Printer Language). Having finally grasped most of the basics of Visual C#, I had a lot of fun translating the reference manuals into classes and methods that would simplify the creation of each type of data that I wanted to write to a label. One class would contain methods for EPL, and another would handle ZPL. The nature of C# would make it really easy to remember the parameters needed for each data element. Using these classes and methods, I could write label formats easily for each type of label that I wanted to generate. The Acumatica help even explained how to send that file to the browser and enable automatic printing of the downloaded file, all with a simple batch file to send the file to the local printer. At the time, printing via the batch file solution would have to suffice, but what I really wanted was a way to print the labels to remote printers connected via Device Hub so that tablets could be used to print the labels.

In the past month, I finally learned to leverage Device Hub for my custom labels. Here is some sample code to give you an idea of the solution I wrote, but the most important part is at the end where we ultimately upload the label file and send it to the printer via Device Hub.

First, let’s create the language classes to generate the label code for each data type that we will want to use. Here is a basic sample of a Text field builder in EPL and ZPL.

using PX.Data;
using PX.SM;
using System;
using System.Collections.Generic;
using System.Text;

namespace BLOG.LB
{
    public static class Epl
    {
        public static List<String> Text(
            List<String> label,
            int? hpos,
            int? vpos,
            int? rotation,
            int? font,
            int? hmult,
            int? vmult,
            bool reverse,
            string text)
        {
            label.Add(string.Format(
                "{0}{1},{2},{3},{4},{5},{6},{7},\"{8}\"",
                "A",
                hpos,
                vpos,
                rotation,
                font,
                hmult,
                vmult,
                reverse ? "R" : "N",
                (text == null) ? "" :
                text.Replace("\"", "\\\"")
                ));
            return label;
        }
    }


    public static class Zpl
    {
        public static List<String> Text(
            List<String> label, 
            int? xPos,
            int? yPos,
            string fontName,
            string fontOrientation,
            int? fontHeight,
            int? fontWidth,
            string text,
            bool reverse = false)
        {
            label.Add(string.Format(
                "^FO{0},{1}^A{2}{3},{4},{5}^FD{6}^FS",
                xPos,
                yPos,
                fontName,
                fontOrientation,
                fontHeight,
                0,
                text));
            return label;
        }
    }
}

Next, we will want to create a label.

namespace BLOG.LB
{
    public class LabelGenerator
    {
        public List<string> MyLabel()
        {
            List<string> label = new List<string>();
            label = Epl.Text(label, 10, 50, 0, 1, 1, 1, false,
                    "My Text");
            label = Epl.Text(label, 10, 80, 0, 1, 1, 1, false,
                    "More Text");
            return label;
        }
    }
}

Finally, we want to add a method into the LabelGenerator class to print the label.

namespace BLOG.LB
{
    public class LabelGenerator
    {
        public void Print(List<string> label, Guid? SMPrinterID)
        {
            
            StringBuilder labelSB = new StringBuilder();
            foreach(string data in label)
            {
                labelSB.AppendLine(data);
            }

            byte[] labelBytes = Encoding.ASCII.GetBytes(
                labelSB.ToString()
                );
            string filename = "label-" +
                Guid.NewGuid().ToString() +
                ".epl";
            FileInfo labelFileInfo = new FileInfo(
                filename,
                null,
                labelBytes);

            UploadFileMaintenance upload = 
                PXGraph.CreateInstance<UploadFileMaintenance>();

            if (upload.SaveFile(labelFileInfo))
            {
                Dictionary<string, string> parameters = 
                    new Dictionary<string, string> {
                    { "FILEID", labelFileInfo.UID.ToString() }
                    };
                string description = "Label";
                PrintSettings printSettings = new PrintSettings() {
                    PrinterID = SMPrinterID,
                    NumberOfCopies = 1,
                    PrintWithDeviceHub = true,
                    DefinePrinterManually = false
                };

                PXGraph.CreateInstance<SMPrintJobMaint>()
                    .CreatePrintJob(
                    printSettings,
                    (string)null,
                    parameters,
                    description);
            }
        }
    }
}

The printing uses the following steps:

  • Create a StringBuilder with each label instruction in a new line using AppendLine.
  • Convert the StringBuilder to a byte array.
  • Define a unique filename to upload into Acumatica to contain the label instructions.
  • Create an SM.FileInfo to contain the byte array into the file to upload.
  • Create an instance of UploadFileMaintenance.
  • Use the instance of UploadFileMaintenance to save the file using the UploadFileMaintenance.SaveFile method with the parameter of the FileInfo object containing the label instructions.
  • Test to ensure the label file uploaded properly.
  • Define a parameter to specify the FileInfo for printing.
  • Define a string to hold a description for the print job.
  • Create a PrintSettings object specifying the PrinterID (in SMPrinter) that identifies the label printer as connected via Device Hub (and set for raw mode), the number of copies, a boolean True to instruct use of Device Hub, and a boolean false for defining the printer manually.
  • Call the static method CreatePrintJob in SMPrintJobMaint passing all the necessary parameters to finally send the printer instruction file to the remote label printer attached via Device Hub.

The 4 statements in the block “if (upload.SaveFile(labelFileInfo)) { }” were the missing puzzle piece for me. Recently, I was able to explore the code behind SMPrintMaint to understand how to send my label file to Device Hub. A little trial and error, and then a label printed.

As programmers, we often struggle to figure out how to do something. That’s what makes the career interesting for many of us as we find new discoveries frequently. As with most challenges, retrospect show us how easy it really was to overcome our barriers. For me, this particular solution evaded me for 18 months. Not because I spent that long pursuing it, but because I had hit dead end after dead end in my search until I was able explore a portion of the source code to finally understand proper use.

While much of this solution is a wireframe to guide you, the solution I needed was the last part… simply 4 lines of code of which I had partially written in previous attempts. Now it is yours!

Happy coding!

4 thoughts on “Printing to a Label Printer via Device Hub”

  1. Hi Brian-
    Thanks for this write up, it was really helpful for me to understand some of the basics behind sending print jobs to printers with DeviceHub.

    On the topic of label printing specifically, I notice that you don’t use SMPrintJobMaint.CreatePrintJobForRawFile() to send your job to the printer. I don’t really blame you… once again Acumatica has created a totally undocumented API and refers to it like it’s fully documented, apropos this conversation: https://stackoverflow.com/questions/55340895/how-can-i-print-custom-zpl-labels-from-a-mobile-device-to-a-printer-connected-to

    Anyway, just curious why you chose to use CreatePrintJob instead of CreatePrintJobForRawFile….?

    Thanks,

    Jonathan

      1. Yup, that’s why I posted my follow on comment. Once I got down the rabbit hole on this one, I realized that CreatePrintJobForRawFile just wraps the CreatePrintJob method anyway (and not in a good way…it puts annoying restrictions on it based on the PXAdapter that don’t seem to be required).

  2. Hmmm….it appears that “CreatePrintJobForRawFile” is a rather thin wrapper around CreatePrintJob, and that it does annoying things like forcing you to have an adapter that has the “MassProcess” or “QuickProcessFlow” properties. So now I see why you just went around it. It basically just gets in your way, and you can just replicate its printSettings properties via the regular CreatePrintJob method.

Leave a Reply