Zebra

Zebra Bluetooth Printer and Xamarin

Hi everyone, recently I was assigned to make an app on Xamarin that can communicate with a Zebra Printer via Bluetooth, It wasn’t exactly the most straight forward experience, since the documentation is lacking of structure let say, but have to mention that the response time of the Zebra team on Xamarin repository on GitHub was insanely fast (1/2 hour).

So let’s begin:

Requirements:

  1. Visual Studio for Mac / Windows
  2. Zebra LinkOS SDK (version 1.1.75 at the time of this post)

What we’ll do:

  1. Create a Xamarin.Forms PCL Project
  2. Include Zebra LinkOS SDK
  3. Scan for bluetooth devices (printers) and list it on a ListView
  4. Select printer Bluetooth Mac Address
  5. Print sample Test from Android

Devices used to test:

Troubleshoot & Things to have in mind

  • I would recommend you download on iOS and Android Zebra official app (just so you know, test connection with their own app before blame Xamarin, Android or Apple) it’s called Zebra Utilities with this app you can test if your device can communicate with your printer and even print some samples.
  • On iOS you have to connect the printer from Settings > Bluetooth first. This is how things works on Apple.
  • Zebra team also have an app for Android and Windows called Zebra Setup Utilities, with this app you can set up friendly name of the printer, see printer status and other info.
  • While I was testing on android i got this error Could not connect to device: read failed, socket might closed or timeout, read ret: -1, and honestly didn’t know why this happen, and try several things and was very frustrating, I saw the error even from Zebra Utilities app and the Zebra Setup Utilities didn’t connect either, but the Printer appear on the available list so… didn’t know what to do… as last option I do what support techs always do… (Turn off and on the Android device and voila! Was able to connect )
  • Lastly here’s LinkOS SDK, it contains a Xamarin.Forms project where most parts of this where taken from.

Making the project

  1. Open Visual Studio For Mac
    1. New Project or (File > New Solution)
    2. From the Multiplatform options on the left click Blank Forms App
    3. Name your project and remember to select PCL project
      Here’s is my initial solution setup
    4. Let’s create some structure:
      1. On the PCL project (ZebraBluetoothSample)
      2. Let’s create a folder Called Dependencies and other Called Pages (we can pretend we are not savages, even is this just a sample 😉 )
      3. On the Dependencies folder add a new Interface called IPrinterDiscovery
      4. For simplicity every action would be on the main ContentPage, let’s move this to the Pages folder
      5. On each platform project let’s create the Dependencies folder and the PrinterDiscovery class.
    5. Include on each project the nuget package for LinkOS (On solution explorer right click every project and click on Add > Add NuGet Packages search for LinkOS and Add Packages
    6. On the PCL project, the file IPrinterDiscovery.cs add the following:
      using System;
      using LinkOS.Plugin.Abstractions;
      
      namespace ZebraBluetoothSample.Dependencies
      {
          public interface IPrinterDiscovery
          {
              void FindBluetoothPrinters(IDiscoveryHandler handler);
              void CancelDiscovery();
          }
      }
      

      We have a definition to find the Bluetooth Printers and another for Cancel the search. Let’s continue with the PCL code and then we implement the DepencyInjection on each Platform.

    7. Now, on Pages > ZebraBluetoothSamplePage.xaml
      1. We would use 2 buttons and a list view, inside a StackLayout
        1. First Button: Scan for Printers
        2. Second Button: Print
        3. The ListView will be used for display discovered printers
        4. The stack layout would contain every control on the screenWe should have a page like this in XAML
          <?xml version="1.0" encoding="utf-8"?>
          <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:forms" x:Class="forms.formsPage">
          
              <StackLayout
                  HorizontalOptions="FillAndExpand"
                  VerticalOptions="FillAndExpand"
                  Margin ="50">
                  <Button x:Name="btnScan" Text="Scan"/>
                  <Button x:Name="btnPrint" Text="Print"/>
                  <ListView x:Name="lstDevices" >
          
                  </ListView>
              </StackLayout>
          </ContentPage>
          

           

      2. Now, in the code behind ZebraBluetoothSamplePage.xaml.cs I included a few commentaries on each method used so it could be very intuitive and also, passed the methods from the sample provided by Zebra team:
        using System;
        using System.Collections.ObjectModel;
        using System.Diagnostics;
        using System.Text;
        using System.Threading.Tasks;
        using LinkOS.Plugin;
        using LinkOS.Plugin.Abstractions;
        using Xamarin.Forms;
        using ZebraBluetoothSample.Dependencies;
        
        namespace ZebraBluetoothSample
        {
            public partial class ZebraBluetoothSamplePage : ContentPage
            {
                #region Properties
                public delegate void PrinterSelectedHandler(IDiscoveredPrinter printer);
                public static event PrinterSelectedHandler OnPrinterSelected;
                ObservableCollection<IDiscoveredPrinter> printers = new ObservableCollection<IDiscoveredPrinter>();
                protected IDiscoveredPrinter ChoosenPrinter;
                #endregion
        
        
                public ZebraBluetoothSamplePage()
                {
                    InitializeComponent();
        
                    lstDevices.ItemsSource = printers;
                    lstDevices.ItemSelected += LstDevices_ItemSelected; ;
                    btnScan.Clicked += (sender, e) =>
                    {
                        IsBusy = true;
                        loading.IsRunning = true;
                        Task.Run(()=>{
                            StartBluetoothDiscovery();
                        });
                    };
        
                    btnPrint.Clicked += BtnPrint_Clicked; ;
                }
        
                void LstDevices_ItemSelected(object sender, SelectedItemChangedEventArgs e)
                {
                    //Stop searching for bluetooth devices/printers
                    DependencyService.Get<IPrinterDiscovery>().CancelDiscovery();
        
                    //Object type for printers returned are DiscoveredPrinters, theres an additional type that says USB but is not the target of this project
                    //We assign now the printer selected from the list.
                    ChoosenPrinter = e.SelectedItem as IDiscoveredPrinter;
                }
        
                void BtnPrint_Clicked(object sender, System.EventArgs e)
                {
                    IConnection connection = null;
                    try
                    {
                        connection = ChoosenPrinter.Connection;
                        connection.Open();
                        IZebraPrinter printer = ZebraPrinterFactory.Current.GetInstance(connection);
                        if ((!CheckPrinterLanguage(connection)) || (!PreCheckPrinterStatus(printer)))
                        {
                         
                            return;
                        }
                        sendZplReceipt(connection);
                    }
                    catch (Exception ex)
                    {
                        // Connection Exceptions and issues are caught here
                        Debug.WriteLine(ex.Message);
                    }
                    finally
                    {
                        connection.Open();
                        if ((connection != null) && (connection.IsConnected))
                            connection.Close();
                       
                    }
                }
        
        
                #region Zebra methods/functions
        
                //Start searching for printers
                private void StartBluetoothDiscovery()
                {
                    Debug.WriteLine("Discovering Bluetooth Printers");
                    IDiscoveryEventHandler bthandler = DiscoveryHandlerFactory.Current.GetInstance();
                    bthandler.OnDiscoveryError += DiscoveryHandler_OnDiscoveryError;
                    bthandler.OnDiscoveryFinished += DiscoveryHandler_OnDiscoveryFinished;
                    bthandler.OnFoundPrinter += DiscoveryHandler_OnFoundPrinter;
                   
                    System.Diagnostics.Debug.WriteLine("Starting Bluetooth Discovery");
                    DependencyService.Get<IPrinterDiscovery>().FindBluetoothPrinters(bthandler);
                }
        
        
                private void DiscoveryHandler_OnFoundPrinter(object sender, IDiscoveredPrinter discoveredPrinter)
                {
        
                    Debug.WriteLine("Found Printer:" + discoveredPrinter.ToString());
                    Device.BeginInvokeOnMainThread(() => {
                        lstDevices.BatchBegin();
        
                        if (!printers.Contains(discoveredPrinter))
                        {
                            printers.Add(discoveredPrinter);
                        }
                        lstDevices.BatchCommit();
                    });
                }
        
                private void DiscoveryHandler_OnDiscoveryFinished(object sender)
                {
                    Debug.WriteLine("Discovery Finished");
                    Device.BeginInvokeOnMainThread(()=>{
                        loading.IsRunning = false;
                        IsBusy = false;
                    });
                }
        
                private void DiscoveryHandler_OnDiscoveryError(object sender, string message)
                {
                    Debug.WriteLine("On Discovery Error" );
                    Debug.WriteLine(message);
                }
        
                //Connect and send to print
                private void PrintLineMode()
                {
                    IConnection connection = null;
                    try
                    {
        
                        connection = ChoosenPrinter.Connection;
                        connection.Open();
                        IZebraPrinter printer = ZebraPrinterFactory.Current.GetInstance(connection);
                        if ((!CheckPrinterLanguage(connection)) || (!PreCheckPrinterStatus(printer)))
                        {
                            
                            return;
                        }
                        sendZplReceipt(connection);
                        if (PostPrintCheckStatus(printer)) {
                            Debug.WriteLine("Printing process is done");
                        }
                    }
                    catch (Exception ex)
                    {
                        // Connection Exceptions and issues are caught here
                        Debug.WriteLine(ex.Message);
                    }
                    finally
                    {
                        connection.Open();
                        if ((connection != null) && (connection.IsConnected))
                            connection.Close();
                        
                    }
                }
        
               
                //Format and construct the body of the printer string
                private void sendZplReceipt(IConnection printerConnection)
                {
                    /*
                     This routine is provided to you as an example of how to create a variable length label with user specified data.
                     The basic flow of the example is as follows
        
                        Header of the label with some variable data
                        REMOVED TO TAKE THE EXAMPLE AS SIMPLE AS POSSIBLE Body of the label
                        REMOVED TO TAKE THE EXAMPLE AS SIMPLE AS POSSIBLE     Loops thru user content and creates small line items of printed material
                        REMOVED TO TAKE THE EXAMPLE AS SIMPLE AS POSSIBLE Footer of the label
        
                     As you can see, there are some variables that the user provides in the header, body and footer, and this routine uses that to build up a proper ZPL string for printing.
                     Using this same concept, you can create one label for your receipt header, one for the body and one for the footer. The body receipt will be duplicated as many items as there are in your variable data
        
                     */
        
                    String tmpHeader =
                            /*
                             Some basics of ZPL. Find more information here : http://www.zebra.com
        
                             ^XA indicates the beginning of a label
                             ^PW sets the width of the label (in dots)
                             ^MNN sets the printer in continuous mode (variable length receipts only make sense with variably sized labels)
                             ^LL sets the length of the label (we calculate this value at the end of the routine)
                             ^LH sets the reference axis for printing. 
                                You will notice we change this positioning of the 'Y' axis (length) as we build up the label. Once the positioning is changed, all new fields drawn on the label are rendered as if '0' is the new home position
                             ^FO sets the origin of the field relative to Label Home ^LH
                             ^A sets font information 
                             ^FD is a field description
                             ^GB is graphic boxes (or lines)
                             ^B sets barcode information
                             ^XZ indicates the end of a label
                             */
        
                            "^XA" +
        
                            "^POI^PW400^MNN^LL325^LH0,0" + "\r\n" +
        
                            "^FO50,50" + "\r\n" + "^A0,N,70,70" + "\r\n" + "^FD Shipping^FS" + "\r\n" +
        
                            "^FO50,130" + "\r\n" + "^A0,N,35,35" + "\r\n" + "^FDPurchase Confirmation^FS" + "\r\n" +
        
                            "^FO50,180" + "\r\n" + "^A0,N,25,25" + "\r\n" + "^FDCustomer:^FS" + "\r\n" +
        
                            "^FO225,180" + "\r\n" + "^A0,N,25,25" + "\r\n" + "^FDValego Consulting^FS" + "\r\n" +
        
                            "^FO50,220" + "\r\n" + "^A0,N,25,25" + "\r\n" + "^FDDelivery Date:^FS" + "\r\n" +
        
                            "^FO225,220" + "\r\n" + "^A0,N,25,25" + "\r\n" + "^FD{0}^FS" + "\r\n" +
        
                            //"^FO50,273" + "\r\n" + "^A0,N,30,30" + "\r\n" + "^FDItem^FS" + "\r\n" +
        
                            //"^FO280,273" + "\r\n" + "^A0,N,25,25" + "\r\n" + "^FDPrice^FS" + "\r\n\n\n\n" +
        
                        "^FO50,300" + "\r\n\n\n\n\n\n\n\n\n" + "^GB350,5,5,B,0^FS" + "^XZ";
        
                  //  int headerHeight = 325;
        
                    DateTime date = DateTime.Now;
                    string dateString = date.ToString("MMM dd, yyyy");
        
                    string header = string.Format(tmpHeader, dateString);
                    var t = new UTF8Encoding().GetBytes(header);
                    printerConnection.Write(t);
        
        
                }
        
                //Check if the printer is not null
                //If it is null means we should select one first
                protected bool CheckPrinter()
                {
                    if (ChoosenPrinter == null)
                    {
                        Debug.WriteLine("Please Select a printer");
                        //SelectPrinter();
                        return false;
                    }
                    return true;
                }
        
        
                //More info https://www.zebra.com/content/dam/zebra/manuals/en-us/software/zpl-zbi2-pm-en.pdf
                protected bool CheckPrinterLanguage(IConnection connection)
                {
                    if (!connection.IsConnected)
                        connection.Open();
                    //  Check the current printer language
                    byte[] response = connection.SendAndWaitForResponse(new UTF8Encoding().GetBytes("! U1 getvar \"device.languages\"\r\n"), 500, 100);
                    string language = Encoding.UTF8.GetString(response, 0, response.Length);
                    if (language.Contains("line_print"))
                    {
                        Debug.WriteLine("Switching printer to ZPL Control Language.", "Notification");
                    }
                    // printer is already in zpl mode
                    else if (language.Contains("zpl"))
                    {
                        return true;
                    }
        
                    //  Set the printer command languege
                    connection.Write(new UTF8Encoding().GetBytes("! U1 setvar \"device.languages\" \"zpl\"\r\n"));
                    response = connection.SendAndWaitForResponse(new UTF8Encoding().GetBytes("! U1 getvar \"device.languages\"\r\n"), 500, 100);
                    language = Encoding.UTF8.GetString(response, 0, response.Length);
                    if (!language.Contains("zpl"))
                    {
                        Debug.WriteLine("Printer language not set. Not a ZPL printer.");
                        return false;
                    }
                    return true;
                }
        
        
                //Before printing, check current printer status
                protected bool PreCheckPrinterStatus(IZebraPrinter printer)
                {
                    // Check the printer status
                    IPrinterStatus status = printer.CurrentStatus;
                    if (!status.IsReadyToPrint)
                    {
                        Debug.WriteLine("Unable to print. Printer is " + status.Status);
                        return false;
                    }
                    return true;
                }
        
        
                //Check what happens to the printer after print command was sent
                protected bool PostPrintCheckStatus(IZebraPrinter printer)
                {
                    // Check the status again to verify print happened successfully
                    IPrinterStatus status = printer.CurrentStatus;
                    // Wait while the printer is printing
                    while ((status.NumberOfFormatsInReceiveBuffer > 0) && (status.IsReadyToPrint))
                    {
                        status = printer.CurrentStatus;
                    }
                    // verify the print didn't have errors like running out of paper
                    if (!status.IsReadyToPrint)
                    {
                        Debug.WriteLine("Error durring print. Printer is " + status.Status);
                        return false;
                    }
                    return true;
                }
        
                #endregion
            }
        }
        

        Now we’re done in the PCL

Xamarin.Android

  1. Double click on the droid Project to open settings and go to Android Build and check Enable Multi-dex
  2. Go to Android Application and set enable these permissions
    1. AccessCoarseLocation
    2. AccessFineLocation
    3. Bluetooth
    4. BluetoothAdmin
    5. BluetoothPrivileged
  3. Let’s make de Dependency Injection class
    using System;
    using Android;
    using Android.Bluetooth;
    using Android.Content.PM;
    using Android.Support.V4.App;
    using Android.Support.V4.Content;
    using LinkOS.Plugin;
    using LinkOS.Plugin.Abstractions;
    using ZebraBluetoothSample.Dependencies;
    using ZebraBluetoothSample.Droid;
    
    [assembly: Xamarin.Forms.Dependency(typeof(PrinterDiscovery))]
    namespace ZebraBluetoothSample.Droid
    {
        public class PrinterDiscovery : IPrinterDiscovery
        {
            public PrinterDiscovery() { }
    
            public void CancelDiscovery()
            {
                if (BluetoothAdapter.DefaultAdapter.IsDiscovering)
                {
                    BluetoothAdapter.DefaultAdapter.CancelDiscovery();
                    System.Diagnostics.Debug.WriteLine("Cancelling Discovery");
                }
            }
    
            public void FindBluetoothPrinters(IDiscoveryHandler handler)
            {
                const string permission = Manifest.Permission.AccessCoarseLocation;
                if (ContextCompat.CheckSelfPermission(Android.App.Application.Context, permission) == (int)Permission.Granted)
                {
                    BluetoothDiscoverer.Current.FindPrinters(Android.App.Application.Context, handler);
                    return;
                }
                TempHandler = handler;
                //Finally request permissions with the list of permissions and Id
                ActivityCompat.RequestPermissions(MainActivity.GetActivity(), PermissionsLocation, RequestLocationId);
            }
            public static IDiscoveryHandler TempHandler { get; set; }
    
            public readonly string[] PermissionsLocation =
            {
              Manifest.Permission.AccessCoarseLocation
            };
            public const int RequestLocationId = 0;
    
    
    
            public void FindUSBPrinters(IDiscoveryHandler handler)
            {
                UsbDiscoverer.Current.FindPrinters(Android.App.Application.Context, handler);
            }
    
            public void RequestUSBPermission(IDiscoveredPrinterUsb printer)
            {
                if (!printer.HasPermissionToCommunicate)
                {
                    printer.RequestPermission(Android.App.Application.Context);
                }
            }
        }
    }

     

  4. And finally Let’s modify the MainActivity class to make an static reference of the activity to be used in the DependencyInjection context
    using System;
    
    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.Runtime;
    using Android.Views;
    using Android.Widget;
    using Android.OS;
    
    namespace ZebraBluetoothSample.Droid
    {
        [Activity(Label = "ZebraBluetoothSample.Droid", Icon = "@drawable/icon", Theme = "@style/MyTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
        public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
        {
            public static MainActivity CurrentActivity;
            protected override void OnCreate(Bundle bundle)
            {
                TabLayoutResource = Resource.Layout.Tabbar;
                ToolbarResource = Resource.Layout.Toolbar;
    
                base.OnCreate(bundle);
    
                global::Xamarin.Forms.Forms.Init(this, bundle);
    
                LoadApplication(new App());
                CurrentActivity = this;
            }
    
            public static Activity GetActivity()
            {
                return MainActivity.CurrentActivity;
            }
        }
    }
    

     

  5. Now we’re ready, deploy to device, test and… PRINT!

Xamarin.iOS

Here we got an issue regarding the debugging mode, it doesn’t compile. Once I get the workaround or the fix from Zebra team, will let you know.

 

Wrapping things up

I hope anyone find this of help , I think it should work very similar on others printing devices from Zebra. Here’s is the project on GitHub for you to explore.

https://github.com/starl1n/ZebraBluetoothSample

Remember, things can be better implemented, more backgrounding, ViewModels, etc, but I just want to let things as simple as possible.