How to connect Zebra thermal printer to a custom iOS application
Tags: ios, tutorial, swift
Published: 2019-11-08 15:21:38

 I work at [Relevant Software](http://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](https://techdocs.zebra.com/link-os/2-12/ios/content/interface_mfi_bt_printer_connection.html).


Disadvantages: 
- Supports not all models. Only a separate series of Zebra printers are MFi certified and [compatible](https://www.zebra.com/us/en/support-downloads/knowledge-articles/ios-and-bluetooth-connectivity-for-zebra-printer.html) 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](https://www.zebra.com/content/dam/zebra/product-information/en-us/brochures-datasheets/software/ios-app-whitelisting-faq-en.pdf) 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.**

```Swift
 
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.**

```Swift
    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.**

```Swift
 
	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.**

```Swift
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.**

```Swift
//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.**

```Swift

 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.**

```Swift

 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.

```Swift

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:**

```Swift
 
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:** 
- [https://en.wikipedia.org/wiki/Zebra_(programming_language)](https://en.wikipedia.org/wiki/Zebra_(programming_language)) 
- [http://labelary.com/viewer.html](http://labelary.com/viewer.html)

Here is an example of a simple command: 
```Swift
^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.