Bluetooth Logo

Writing an SDK With Core Bluetooth – 08 – The Object Model

This entry is part 11 of 24 in the series Writing an SDK With Core Bluetooth

MODELING THE SYSTEM

When I write SDKs, they tend to take the form of “object models.” That is, they provide a set of instances to the user, simulating/emulating the lower-level system (in the past, these have been class instances, which are objects, but these days, Swift can provide a number of choices. I’ll still call them “objects,” but they could be structs or enums).

The idea is to take a system that is usually accessed using lower-level types of functionality (like basic functions and property settings), and then convert it to a “smarter,” simpler and more intuitive model.

This is not a new idea at all. C++ Streams are a classic example of the concept. Most languages have something like that in their standard library. Streams model low-level interfaces, like the simple C I/O library, in an object package.

WHAT TYPES OF ENTITY SHOULD THESE BE?

In Swift, we don’t just have to have classes. We can also have value types, such as structs, and enums.

In fact, in Swift the only referenced type is a class. Enums are powerful as all git-go in Swift, but aren’t always the best choice for main data/functionality modeling. We will definitely be using enums in our SDK, but probably not as a main data structure.

So that means that when we present things like SDK instances, and devices, to the user, they will either be a class or a struct, under the hood.

CORE BLUETOOTH IS ATOMIC AND STATEFUL

One thing that we should keep in mind, and that will become obvious, as we go along, is that Core Bluetooth is loaded with state, and handles things internally as referenced instances. It doesn’t make copies of instances, and caches a lot of instance state.

Classes are generally better for modeling a stateful system, as they won’t cause problems like copies presenting a different state from their progenitors.

Nevertheless, if we can get away with presenting a data element as a struct or an enum, we should try to do so. Swift rewards value semantics. Whenever we are determining a data type, we should see if we can make it a value type.

COMMON MODELS

Let’s begin with models that can apply to both Central and Peripheral roles, or that apply to a level above them:

  • Error Types
    • Common Errors
  • Core Bluetooth Error Status
  • Core Bluetooth Powered On State
  • Basic Device
    • Name Property (Read-Only)
    • Device Error Status Property (Read-Only)
    • Question Property (Read-Only)
  • SDK Observers (Base Definition)
    • Error Reporting Method

CENTRAL MODELS

  • Error Types (Extension from Common)
    • Central-Specific Errors
  • Basic Device (Extension from Common)
    • Device Observers (Extension from Common)
      • Question Answered Method
    • Answer Property (Read-Only)
    • Send Question Method
    • Device Observers
  • Central Observers (Extension from Common)
    • Device Added Method
  • Discovered Device List
  • Core Bluetooth Scanning State

PERIPHERAL MODELS

  • Error Types (Extension from Common)
    • Peripheral-Specific Errors
  • Peripheral Observers (Extension from Common)
    • Question Asked Method
  • Core Bluetooth Advertising State

WE ARE RUNNING MODELS, NOT INSTANCES

Note that, instead of having the Question and Answer properties read/write, they are read-only, and we have “Send Question/Answer” methods.

That’s because we are dealing with device control, and setting a variable indicates immediate state change.

We don’t do “immediate” in device control. We have to basically ask the device “If it isn’t too much trouble, would you mind terribly, asking your user this question?”.

We then have to wait for the device to say “I asked this question.”, at which time, we can change the state of the property to the question that was sent in the confirmation (not the one we asked).

We could have a “Question Being Asked” state for the device, in which we prevent a new question being asked until we get the answer from the previous one. I try to avoid that kind of thing, if possible, and leave it up to the device to respond with “I’m busy, leave me alone.”, as opposed to tracking it in the local reference. Maybe the device can handle multiple questions, or a new version comes out, that can. You then suddenly have new functionality.

As a compromise, I did add a semaphore (I dislike semaphores) to the Peripheral Mode app. It can probably be removed in a later version of the project.

This is how we need to work, if we are managing local models of remote instances. Never change local state without feedback or prompting from the real instance that we are modeling.

PROTOCOLS

Regardless of how we structure our models under the hood, I want to expose them in the API as Swift Protocols. This allows the user to have as much flexibility as possible, and also allows us to encapsulate a great deal of structure and functionality inside the SDK.

OBSERVERS

As we are developing a model for a device control system, we need to have a system of callbacks (closures). These are function references that the user supplies to the SDK, and that are called when certain events occur, or states change.

The model that I am choosing is a modification of the OBSERVER pattern. In this pattern, the user “registers” observer callbacks with the system, attaching them to events or status changes, and then responds to these callbacks when they are called.

Apple has a specialization of OBSERVER, called “Delegation.” Apple uses Delegation in almost all of their APIs. It is a safe, simple methodology. that works extremely well. It has been around long enough to develop a “tribal knowledge” base, and have the cobwebs blown out. It is virtually “baked into” the language.

The main issue that I have with Delegation, is that you can only have one (1) delegate object, and that object always has to be a class.

OBSERVER is more basic, but not as simple. It is more flexible than Delegation. You can have more than one observer, and the observer does not need to be a class. It could be as simple as a function reference. Apple also uses OBSERVER in their KVO implementation.

The main issue with KVO, is that you are only providing a single, fairly blind function reference, and binding it to a single event/status change.

Also, you usually need to be very careful to “unsubscribe” observers when they go away, or you can get crashes, and you don’t have the safeguard provided by a “weak” reference, as you do with Delegation. The onus is on the user to manage their registration status with the SDK.

I like to have a middle ground. Instead of a simple function reference, I like the user to be able to provide a protocol that defines a property interface, as well as a functional interface. I like being able to “register” for more than one observer, and I like the idea of registering a full interface; not just a single function, and not binding it to just an event. Delegates allow you to associate an interface with an instance, and that instance can choose to exercise whatever parts of that interface it likes.

I also don’t like restricting the interface to a class only. I’d like the observer to be applicable to structs and enums (if possible), and not just classes.

So we should be able to define OBSERVER protocols for observers, and register zero or more instances with SDK objects.

We are now finally ready to start creating some code.