DEV Community

Cover image for How to connect Zebra thermal printer to a custom iOS application
Ihor Feoktistov
Ihor Feoktistov

Posted on

How to connect Zebra thermal printer to a custom iOS application

I work at Relevant Software and we’ve been developing an app that allows event managers to track the attendance of their events and check-in/check-out visitors. One day our client came up with an idea to not just scan tickets with QR code, but also to print the badge with visitor info right after the scan. So we had to connect an app to the portable thermal printer.

We started looking for solutions and best practices but found little information on this topic. That’s why I decided to clarify the process of connecting an app to the printer with code examples to those who will face a similar challenge.

Why we used Zebra printer

Whether you’re making deliveries, manufacturing and distributing goods, or serving customers, thermal printers could help you. Printers manufactured by Zebra are easy to use, easy to deploy and easy to manage. Zebra provides a list of different models depending on your goals and expectations.
To connect a thermal printer to our iOS app we used Bluetooth low energy (BLE) as the best option for mobile printing and other devices that require short bursts of connectivity.

Bluetooth and BLE

There might be confusion about whether to choose Bluetooth or BLE, so let’s dive into the difference between these two.

BLE and Bluetooth both use the same 2.4 GHz ISM band. The main difference is that Bluetooth low energy stays in sleep mode until there is a connection request, which allows it to consume less power. BLE is a great solution when you have to exchange data quickly and intermittently, like a mobile printer.

Core Bluetooth framework and MFi

iOS and macOS SDKs support Bluetooth 4.0 LE devices with the Core Bluetooth framework and other Bluetooth versions devices with the External Accessory framework. The External Accessory framework requires that the manufacturer of the Bluetooth thermal printer is registered in the Mfi Program. Only the major manufacturers like Zebra, Epson, Star Micronics, Bixolon are registered with Mfi Program.

Advantages of MFi certified devices:

  • Could be easily used with native Bluetooth connection on iPhones and iPads
  • If you want to develop own approach it’s easy to integrate printing feature to the app with SDK provided by Zebra (MfiBtPrinterConnection) But, this class will only work with Zebra printers which have the Made For iPod/iPhone certification.

Disadvantages:

  • Supports not all models. Only a separate series of Zebra printers are MFi certified and compatible with the iOS Bluetooth Standards. There is a special procedure of approving Apps for MFi which takes additional time and steps. Each submitting or updating the app should be approved by Apple and Zebra company in tandem.

Alternative approach is to just use Core Bluetooth Framework

Pros:

  • You could use any BLE printer, not MFi only. The list of devices you can use is much larger.
  • There are no additional steps during moderation on AppStore.

Cons:

  • Development should be done with the native SDK only. Some specific moments exist in this process. We will share them below.

Main steps to make your app able to print via BLE

Setup the object of CBCentralManager to scan the nearest BLE devices. You can filter these devices by specification, to find printers only.


let ZPRINTER_SERV_ID_FOR_CONNECTION = "FE79"

centralManager = CBCentralManager(delegate: self, queue: nil)

let services:[CBUUID] = [CBUUID(string: ZPRINTER_SERV_ID_FOR_CONNECTION)]

self.centralManager?.scanForPeripherals(withServices: services, options: [CBCentralManagerScanOptionAllowDuplicatesKey : true])

When peripheral device is discovered the delegate method will be called. It’s better to filter them by signal length too.

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
let RSSImin = -15
let RSSImax = -80

    //Reject any where the value is above reasonable range

 if RSSI.intValue > RSSImin {
                return
      }
// Reject if the signal strength is too low to be close enough

         if RSSI.intValue < RSSImax {
                return
            }
//Keep devices in list
}

Connect to selected CBPeripheral device.


    self.centralManager?.connect(device, options: nil)

When a connection is established we should check the services provided by device to find out the required ones for receiving info about printers and sending commands to print.

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
//constants provided  in documentation by Zebra
let ZPRINTER_SERVICE_UUID = "38EB4A80-C570-11E3-9507-0002A5D5C51B"
let ZPRINTER_DIS_SERVICE = "180A"
        peripheral.delegate = self
        peripheral.discoverServices([CBUUID(string: ZPRINTER_SERVICE_UUID),CBUUID(string: ZPRINTER_DIS_SERVICE)])
    }

Delegate method will be called if asked services are discovered. The next step is to call characteristic discovering for the discovered service.

//constants provided  in documentation by Zebra
let ZPRINTER_SERVICE_UUID = "38EB4A80-C570-11E3-9507-0002A5D5C51B"
let WRITE_TO_ZPRINTER_CHARACTERISTIC_UUID = "38EB4A82-C570-11E3-9507-0002A5D5C51B"
let READ_FROM_ZPRINTER_CHARACTERISTIC_UUID = "38EB4A81-C570-11E3-9507-0002A5D5C51B"

let ZPRINTER_DIS_SERVICE                    = "180A"
let ZPRINTER_DIS_CHARAC_MODEL_NAME          = "2A24"
let ZPRINTER_DIS_CHARAC_SERIAL_NUMBER       = "2A25"
let ZPRINTER_DIS_CHARAC_FIRMWARE_REVISION   = "2A26"
let ZPRINTER_DIS_CHARAC_HARDWARE_REVISION   = "2A27"
let ZPRINTER_DIS_CHARAC_SOFTWARE_REVISION   = "2A28"
let ZPRINTER_DIS_CHARAC_MANUFACTURER_NAME   = "2A29"

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
 // Discover the characteristics of Write-To-Printer and Read-From-Printer.
 // Loop through the newly filled peripheral.services array, just in case there's more than one service.
  if let services = peripheral.services {
     for service in services{
       // Discover the characteristics of read from and write to printer
      if (service.uuid == CBUUID.init(string: ZPRINTER_SERVICE_UUID)) {
      peripheral.discoverCharacteristics(
          [CBUUID.init(string: WRITE_TO_ZPRINTER_CHARACTERISTIC_UUID),
           CBUUID.init(string: READ_FROM_ZPRINTER_CHARACTERISTIC_UUID)], for: service)
      } else if (service.uuid == CBUUID.init(string: ZPRINTER_DIS_SERVICE)) {
                    // Discover the characteristics of Device Information Service (DIS)
         peripheral.discoverCharacteristics(
         [CBUUID.init(string: ZPRINTER_DIS_CHARAC_MODEL_NAME),
          CBUUID.init(string: ZPRINTER_DIS_CHARAC_SERIAL_NUMBER),
          CBUUID.init(string: ZPRINTER_DIS_CHARAC_FIRMWARE_REVISION),
          CBUUID.init(string: ZPRINTER_DIS_CHARAC_HARDWARE_REVISION),
          CBUUID.init(string: ZPRINTER_DIS_CHARAC_SOFTWARE_REVISION),
          CBUUID.init(string: ZPRINTER_DIS_CHARAC_MANUFACTURER_NAME)],for: service)
          }
     }
  }

}

When required characteristics are discovered the delegate method will be called. Our goal here is to get data about the BLE device and keep writeCharacteristic to use it for further data sending.


 func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    for characteristic in service.characteristics! {
           if (characteristic.uuid == CBUUID.init(string: WRITE_TO_ZPRINTER_CHARACTERISTIC_UUID)) {
           self.writeCharacteristic = characteristic                
              print("CONNECTED and ready to print")
            } else if (characteristic.uuid == CBUUID.init(string: READ_FROM_ZPRINTER_CHARACTERISTIC_UUID)) {
                // Set up notification for value update on "From Printer Data" characteristic,
                //i.e. READ_FROM_ZPRINTER_CHARACTERISTIC_UUID.
                peripheral.setNotifyValue(true, for: characteristic)
            } else if (characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_MODEL_NAME) ||
                characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_SERIAL_NUMBER) ||
                characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_FIRMWARE_REVISION) ||
                characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_HARDWARE_REVISION) ||
                characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_SOFTWARE_REVISION) ||
                characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_MANUFACTURER_NAME)) {
               // These characteristics are read-only characteristics.
               // Read value for these DIS characteristics
        peripheral.readValue(for: characteristic)
            }
        }
        // Once this is complete, we just need to wait for the data to come in or to send ZPL to printer.
    }

Now we are ready to print. And also when we read values from characteristics that provide info about the device, one more delegate method will be called.


 func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        if (characteristic.uuid == CBUUID.init(string: READ_FROM_ZPRINTER_CHARACTERISTIC_UUID)) {
            if let d = characteristic.value {
                self.data.append(d)
            }
            let text = String.init(data: self.data, encoding: .utf8) ?? ""
            print ("READ data from printer \(text)")

        } else if (characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_MODEL_NAME) ||
            characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_SERIAL_NUMBER) ||
            characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_FIRMWARE_REVISION) ||
            characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_HARDWARE_REVISION) ||
            characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_SOFTWARE_REVISION) ||
            characteristic.uuid == CBUUID.init(string: ZPRINTER_DIS_CHARAC_MANUFACTURER_NAME)) {
             if let d = characteristic.value {
                let text = String.init(data: d, encoding: .utf8) ?? ""
                print ("READ INFO from device \(text)")
             } else {
                print ("READ INFO from device is wrong")
            }
        }
    }

The next step is just to send a print command to the device with the help of writeCharacteristic detected before. But here we face one more tricky moment.

There is a limited size of data that can be sent at a time to the BLE device. It’s called MTU - Maximum Transmission Unit. It should be small enough to make a BLE connection more effective. We should divide our data into small chunks in order to just send a command to print, which consists of several parts. For modern iOS devices, it is less than 100 bytes.


var chunksToBeSent = 0 
var chunksIsAlreadySent = 0
func sendZPLToPrinter(string: String) {
        if let payload = string.data(using: .utf8) {
            if let writeCharacteristic = self.writeCharacteristic {
                let chunks =  createChunks(data: payload)
                chunksToBeSent = chunks.count
                chunksIsAlreadySent = 0
                for chunk in chunks {
                    self.selectedDevice?.writeValue(chunk, for: writeCharacteristic, type: .withResponse)
                }
           }
        }
    }

    func createChunks(data: Data) -> ([Data]) {
      var chunks : [Data] = []
      let length = data.count
        let chunkSize = 80 
//really small value of bytes to be less than MTU size
        var offset = 0
        repeat {
          // get the length of the chunk
          let thisChunkSize = ((length - offset) > chunkSize) ? chunkSize : (length - offset);
          // get the chunk
          let chunk = data.subdata(in: offset..<offset + thisChunkSize )
          chunks.append(chunk)
          // update the offset
          offset += thisChunkSize;
        } while (offset < length);
        return chunks
    }

Each time when the chunk of data is received by characteristic it triggers method:


func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        chunksIsAlreadySent += 1
        let status = "\(chunksIsAlreadySent)/\(chunksToBeSent)"
        print("status \(status)")
        if (chunksToBeSent == chunksIsAlreadySent) {
//notify user that printing is done
        }
        if let error = error {
            print("ERROR: \(error.localizedDescription)")
            return
        }
    }

One more thing is that printers could receive commands sent only in special formats. One of the most popular of them is ZPL. You can learn more about it here:

Here is an example of a simple command:

^XA
^LH30,30
^FO20,10
^ADN,90,50
^FDHello^FS
^XZ

^XA - start the content, ^XZ - finish the command and triggers the printing itself.

Conclusion

As conclusion we can admit that printing data with the help of a BLE connection is not a complicated task when you are aware of all the steps.

I would like to highlight 3 main points:

  1. Decide which specification you’ll use - MFi Bluetooth or Core Bluetooth for BLE connection.
  2. Handle small MTU size.
  3. Prepare proper content to print with ZPL or another format.

If you have any questions left, feel free to ask me in the comments.

Top comments (5)

Collapse
 
kevinvandrielatharbor profile image
KevinVandrielAtHarbor • Edited

I'm most of the way through with incorporating what you have here. I have a "Connected and ready to print" status and have preserved the writeCharacteristic by declaring it as a variable of type CBCharacteristic. The next piece of code, "sendZPLToPrinter", contains a reference to self.selectedDevice? and this is throwing me for a loop. It'll be interesting to get through this. However, I'm blown away how far I've gotten so far. Thank you.

Collapse
 
kevinvandrielatharbor profile image
KevinVandrielAtHarbor • Edited

I figured it out. The self.selectedDevice? is actually the discovered peripheral. In my case I set the discovered peripheral to a declared variable earlier in the class, so this line: "var myPerif: CBPeripheral?;", so upon discovery I held on to the peripheral as such: "self.myPerif = peripheral; self.myPerif?.delegate = self" and so later, when printing, it looks like this: "for chunk in chunks {self.myPerif!.writeValue(chunk, for: writeCharacteristic, type: .withResponse)}"

Collapse
 
ihor_feoktistov profile image
Ihor Feoktistov

Did you have any experience developing an app for printing?

Collapse
 
renanzdm profile image
renanzdm

I would appreciate your help as I couldn't find much material about it, there is the possibility of printing on other types of thermal printers other than Zebra. Would there be some material on this subject where I can learn a little more

Collapse
 
renanzdm profile image
renanzdm

hi, i have been working with flutter lately i am having a challenge to create a plugin that makes ios and android imprint, but i have no experience with ios it has been a huge challenge