Remy Virin

iOS dude

Building an iOS hardware app

As a software engineer, it is really exciting to work at the crossing of software and hardware. It is also a trend on the rise — with the spread of smart things (IoT) and increased use of AI as a differentiator for hardware (think Google’s Pixel Buds), understanding the connection between a hardware device and an iOS app can be a nice to have skill. It’s not something really common as an iOS developer, so I was glad to have this opportunity at Prynt.
Prynt builds a portable, easy-to-use printer for iPhone, you plug it in your iPhone and you can instantly print your pictures and see them video-animated in the Prynt app (think turning your smartphone into a Polaroid camera mixed with Harry Potter-style features).
So how do we connect to a hardware device?
First, you need to have a prototype board with the required hardware (at least a lightning connector). Then the goal is just to do a simple project, that can light a LED for instance, or compute an addition.

Let’s code!

Apple provides a great sample project in Objective-C, don’t forget to turn on Wireless Accessory Configuration in the capabilities of your target. This project demonstrates the basic use of
ExternalAccessory.framework
. The protocol used to communicate with your accessory is iAP2 (iPod Accessory Protocol 2). It’s the same protocol on Bluetooth.
You have 3 main classes:
  • EAAccessoryManager
    : responsible to give you the list of accessories (plugged on the lightning port, via bluetooth etc..) your app can connect to.
  • EAAccessory
    : an accessory connected to your iPhone ( not necessarily connected to your app). It has property like name, manufacturer, serial number etc..
  • EASession
    : Responsible for the connection and exchanging data with the accessory
Here is how to create a session and set up its output and input stream:
let accessory = EAAccessoryManager.shared().connectedAccessories.first!
let session = EASession(accessory: accessory, forProtocol: protocolString)

session.outputStream?.delegate = self
session.outputStream?.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
session.outputStream?.open()

session.inputStream?.delegate = self
session.inputStream?.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode)
session.inputStream?.open()
The
outputStream
and
inputStream
inherit from
[NS]Stream
. Basically you will write data on the
outputStream
which your hardware device can read and it can send you data on the
inputStream
 , which will be read by your app.
Note: The output & input streams are scheduled on the main thread. We tried to scheduled them on a background queue, it may work for a simple project, but in the end we didn’t received / send all the data. I recommend to schedule them on the main thread and dispatch the parsing and serialization on background queues.
Data exchange between your app and your device
This is how you write and read bytes:
var session: EASession

// Write bytes on the output buffer
var bytesToWrite : [UInt8] = [0x00, 0x01, 0xFA]
session.outputStream?.write(&bytesToWrite, maxLength: bytesToWrite.count)

// ----------------------------------
// Read bytes on the input buffer
let bufferSize = 16
var buffer = [UInt8](repeating: 0, count: bufferSize)
if session.inputStream!.hasBytesAvailable {
   session.inputStream!.read(&buffer, maxLength: bufferSize)
   // buffer contains maximum 16 bytes read on the buffer
}
At this point it may not work on the first time, you are sending data, don’t really know if your hardware received them. Your app is supposed to get some data, but nothing on the input stream…
You need to investigate, to understand why your data are not sent or retrieved.
Depending on your project you may need a Beagle and an ATS to analyse the bytes transmitted between the iPhone and your hardware device in real time. Basically you will have access to all the bytes that are going from and to the lighting port in real time. I found Hex Fiend really useful to read, parse (with regex) and understand all the data sent and received.
If your cables between the ATS, the beagle, your mac, iPhone and the hardware board are too long or of poor quality, it may not work (seriously, we had this problem with too long cables).

About Code Quality…

Sending and receiving bytes can become a nightmare if you don’t have a good specification and you stick to it. Organising bytes as frames is really helpful, here is how we organised our data exchange at Prynt:
  • Header: A fixed size amount of bytes. It’s main goal is to transmit a command.
  • Payload: (Optional) Data you want to send, it can be a firmware update, a photo to print, the battery level, status of the hardware etc…
  • Frame: Header + Payload
  • Sync Bytes: We use the first two bytes to verify we’re reading the beginning of the header. In our case we arbitrary choose 0x0F and 0xF0 for the sync bytes, but you can choose any value
  • Packet Number: Just a counter, you increment each time you send a new command, it helps to check if a frame is missing
  • Command: The information you want to ask / send (Print a picture, get the status, get the battery level etc…)
  • CRC (Cyclic Redundancy Check): Useful if you want to be sure your payload or header as not been altered.
Having bytes organised as a frame is easier to parse, one thing that is helpful for us having our model made of UInt8 (= a byte), like for instance, the
BatteryLevel
 :
enum BatteryLevel: UInt8 {
    case low = 0x01
    case normal = 0x02
    case high = 0x03
    case notInitialized = 0xFF
}

let data: Data // data read from inputStream
// Here is how we parse it if the battery information is on the 3rd byte of data.
let battery = BatteryLevel(rawValue: (data[3])) ?? .notInitialized
And of course, the parsing and serialization of those data should be well tested.

Conclusion

Playing with hardware is really fun for several reasons, your code has a material impact on something else, something physical, I remembered the feeling the first picture was printed with our Prynt Pocket, it was breathtaking. One thing I really enjoyed is dealing with data from a lower level perspective (rather than a REST API). It was fun to learn! But the MFi process might be complicated for a side project, you need to have a company and the tools can be costly.
For a side project, a great way to play with hardware is via Bluetooth and with a Raspberry Pi.
If you like this article or have any questions, I’ll be happy to read them in the comments!

Tags

More by Remy Virin

Topics of interest