NordicSemiconductor / IOS-nRF-Connect-Device-Manager

A mobile management library for devices supporting nRF Connect Device Manager.
https://www.nordicsemi.com/Software-and-tools/Software/nRF-Connect-SDK
Apache License 2.0
86 stars 37 forks source link

[!NOTE]
This repository is a fork of the McuManager iOS Library, which is no longer being supported by its original maintainer. As of 2021, we have taken ownership of the library, so all new features and bug fixes will be added here. Please, migrate your projects to point to this Git repsository in order to get future updates. See migration guide.

nRF Connect Device Manager

Swift Platforms License Release Swift Package Manager CocoaPods

nRF Connect Device Manager library is compatible with McuManager (or McuMgr for short) and [SUIT (shorthand for Software Update for the Internet of Things)](). McuManager is a management subsystem supported by nRF Connect SDK, Zephyr and Apache Mynewt. McuManager relies on its own McuBoot bootloader for secure bootstrapping after a firmware update and, uses the Simple Management Protocol, or SMP, for communication over Bluetooth LE. The SMP Transport definition for Bluetooth Low Energy, which this library implements, can be found here.

SUIT and McuManager are related, but not interchangeable. SUIT relies on its own bootloader, but communicates over the SMP Service. Additionally, SUIT supports some functionalities from McuManager, but is not guaranteed to do so. It's best to always check if a McuManager feature is supported by sending the request, rather than assuming it is.

The library provides a transport agnostic implementation of the McuManager protocol. It contains a default implementation for BLE transport.

Minimum required iOS version is 9.0, originally released in Fall of 2015.

[!Warning]
This library, the default & main API for Device Firmware Update by Nordic Semiconductor, should not be confused with the previous protocol, NordicDFU, serviced by the Old DFU Library.

Compatible Devices

nRF52 Series nRF53 Series nRF54 Series nRF91 Series

This library is designed to work with the SMP Transport over BLE. It is implemented and maintained by Nordic Semiconductor, but it should work any devices communicating via SMP Protocol. If you encounter an issue communicating with a device using any chip, not just Nordic, please file an Issue.

Library Adoption into an Existing Project (Install)

SPM or Swift Package Manager (Recommended)

In Xcode, open your root Project file. Then, switch to the Package Dependencies Tab, and hit the + button underneath your list of added Packages. A new modal window will pop-up. On the upper-right corner of this new window, there's a search box. Paste the URL for this GitHub project https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager and the Add Package button should enable.

After Xcode fetches your new project dependency, you should now be able to add import iOSMcuManagerLibrary to the Swift files from where you'd like to call upon this library. And you're good to go.

CocoaPods

pod 'iOSMcuManagerLibrary'

Building the Example Project (Requires Xcode & CocoaPods)

"Cocoapods?"

Not to worry, we have you covered. Just follow the instructions here.

Instructions

First, clone the project:

git clone https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager.git

Then, open the project's directory, navigate to the Example folder, and run pod install:

cd IOS-nRF-Connect-Device-Manager/
cd Example/
pod install

The output should look similar to this:

Analyzing dependencies
Downloading dependencies
Installing SwiftCBOR (0.4.4)
Installing ZIPFoundation (0.9.11)
Installing iOSMcuManagerLibrary (1.3.1)
Generating Pods project
Integrating client project
Pod installation complete! There are 2 dependencies from the Podfile and 3 total pods installed.

You should now be able to open, build & run the Example project by opening the nRF Connect Device Manager.xcworkspace file:

open nRF\ Connect\ Device\ Manager.xcworkspace

Introduction

McuManager is an application layer protocol used to manage and monitor microcontrollers running Apache Mynewt and Zephyr. More specifically, McuManagr implements over-the-air (OTA) firmware upgrades, log and stat collection, and file-system and configuration management.

Command Groups

McuManager are organized by functionality into command groups. In mcumgr-ios, command groups are called managers and extend the McuManager class. The managers (groups) implemented in mcumgr-ios are:

Firmware Upgrade

Firmware upgrade is generally a four step process performed using commands from the image and default commands groups: upload, test, reset, and confirm.

This library provides FirmwareUpgradeManager as a convenience for upgrading the image running on a device.

FirmwareUpgradeManager

A FirmwareUpgradeManager provides an easy way to perform firmware upgrades on a device. A FirmwareUpgradeManager must be initialized with an McuMgrTransport which defines the transport scheme and device. Once initialized, a FirmwareUpgradeManager can perform one firmware upgrade at a time. Firmware upgrades are started using the start(hash: Data, data: Data) method and can be paused, resumed, and canceled using pause(), resume(), and cancel() respectively.

[!CAUTION] Always make your start/pause/cancel DFU API calls from the Main Thread.

Legacy / App Core-Only Upgrade Example

import iOSMcuManagerLibrary

do {
    // Initialize the BLE transport using a scanned peripheral
    let bleTransport = McuMgrBleTransport(cbPeripheral)

    // Initialize the FirmwareUpgradeManager using the transport and a delegate
    let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)

    let imageData = /* Read Image Data */
    let imageHash = try McuMgrImage(data: imageData).hash

    // Start the firmware upgrade with the image data
    dfuManager.start(hash: imageHash, data: imageData)
} catch {
    // Reading File / Image, Hash, etc. errors here.
}

Why Hash, now?

Hash was added as a parameter as part of our support for SUIT and DirectXIP (for more information, see below). It is now possible, with SUIT, to selectively pick from within a single 'image' different hashes to Upload for the same 'slot'. Therefore, a way to determine which specific bag of bytes of the same Image the user wants to upload is required. There were two ways of solving this - trying to make the library 'smart' and remove work, or, provide the library user (developer) the freedom to decide. It is a pain to have to add a new parameter, but we have alternative APIs to try to help with this process.

Multi-Image DFU Example

public class ImageManager: McuManager {

    public struct Image {
        public let image: Int
        public let slot: Int
        public let hash: Data
        public let data: Data

        /* ... */
    }
}

The above is the input type for Multi-Image DFU call, where a value of 0 for the image parameter means App Core, and an input of 1 means Net Core. These representations are of course subject to change as we expand the capabilities of our products. For the slot parameter, you will typically want to set it to 1, which is the alternate slot that is currently not in use for that specific core. Then, after upload, the firmware device will reset to swap over its slots, making the contents previously uploaded to slot 1 (now in slot 0 after the swap) as active, and vice-versa.

With the Image struct at hand, it's straightforward to make a call to start DFU for either or both cores:

import iOSMcuManagerLibrary

try {
    // Initialize the BLE transport using a scanned peripheral
    let bleTransport = McuMgrBleTransport(cbPeripheral)

    // Initialize the FirmwareUpgradeManager using the transport and a delegate
    let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)

    // Build Multi-Image DFU parameters
    let appCoreData = try Data(contentsOf: appCoreFileURL)
    let appCoreDataHash = try McuMgrImage(data: appCoreData).hash
    let netCoreData = try Data(contentsOf: netCoreFileURL)
    let netCoreDataHash = try McuMgrImage(data: netCoreData).hash

    let images: [ImageManager.Image] = [
        (image: 0, slot: 1, hash: appCoreDataHash, data: appCoreData),
        (image: 1, slot: 1, hash: netCoreDataHash, data: netCoreData)
    ]

    // Start Multi-Image DFU firmware upgrade
    dfuManager.start(images: images)
} catch {
    // Errors here.
}

Whereas non-DirectXIP packages target the secondary / non-active slot, also known as slot 1, for each ImageManager.Image, special attention must be given to DirectXIP packages. Since they provide multiple hashes of the same ImageManager.Image, one for each available slot. This is because firmware supporting DirectXIP can boot from either slot, not requiring a swap. So, for DirectXIP the [ImageManager.Image] array might closer to:

import iOSMcuManagerLibrary

try {
    /*
    Initialise transport & manager as above.
    */

    // Build DirectXIP parameters
    let appCoreSlotZeroData = try Data(contentsOf: appCoreSlotZeroURL)
    let appCoreSlotZeroHash = try McuMgrImage(data: appCoreSlotZeroData).hash
    let appCoreSlotOneData = try Data(contentsOf: appCoreSlotOneURL)
    let appCoreSlotOneHash = try McuMgrImage(data: appCoreSlotOneData).hash

    let directXIP: [ImageManager.Image] = [
        (image: 0, slot: 0, hash: appCoreSlotZeroHash, data: appCoreSlotZeroData),
        (image: 0, slot: 1, hash: appCoreSlotOneHash, data: appCoreSlotOneData)
    ]

    // Start DirectXIP Firmware Upgrade
    dfuManager.start(images: directXIP)
} catch {
    // Errors here.
}

Multi-Image DFU Format

Usually, when performing Multi-Image DFU, the delivery format of the attached images for each core will be in a .zip file. This is because the .zip file allows us to bundle the necessary information, including the images for each core and which image should be uploaded to each core. This association between the image files, usually in .bin format, and which core they should be uploaded to, is written in a mandatory JSON format called the Manifest. This manifest.json is generated by our nRF Connect SDK as part of our Zephyr build system, as documented here. You can look at the McuMgrManifest struct definition within the library for an insight into the information contained within the manifest.

Now, the issue is that there's a gap between the aforementioned API, and the output from our Zephyr build system, which is a .zip file. To bridge this gap, we wrote McuMgrPackage, which takes a URL in its init() function. So, given the URL to the .zip file, it is possible to kickstart Multi-Image DFU in this manner:

import iOSMcuManagerLibrary

do {
    // Initialize the BLE transport using a scanned peripheral
    let bleTransport = McuMgrBleTransport(cbPeripheral)

    // Initialize the FirmwareUpgradeManager using the transport and a delegate
    let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)

    // Read Multi-Image DFU package
    let dfuPackage = try McuMgrPackage(from: dfuPackageUrl)

    // Start Multi-Image DFU firmware upgrade
    dfuManager.start(images: dfuPackage.images)
} catch {
    // try McuMgrPackage(from:) will throw McuMgrPackage.Error(s) here.
}

Have a look at FirmwareUpgradeViewController.swift from the Example project for a more detailed usage sample.

Because of the JSON Manifest Parsing nature of the McuMgrPackage method, you might encounter corner cases / crashes. If you find these, please report them back to us. But regardless, the McuMgrPackage shortcut is a wrapper that initialises the aforementioned [ImageManager.Image] array API. So you can always fallback to that.

SUIT Example

SUIT, unlike McuManager, places a lot of the logic (read: blame) for firmware update onto the target device rather than the sender (aka 'you', the API user). This simplifies the internal process, but also makes parsing the raw Data and its contents much more complicated. For example, we can't ascertain the proper Hash signature of every component (file) sent to the firmware because rather than a fixed binary for each Slot or Core, SUIT is designed to represent a sequence of instructions for the bootloader to execute. This means the hashes for the final binaries to be flashed change on-the-fly during the firmware update on the target device's end.

From the sender's perspective, we only need to send "the Data" in full, and allow the target to figure things out. This pack of bytes represents what we call the SUIT Envelope, which is the sequence of instructions for the firmware to run, akin to the code we write before feeding it into a compiler. These instructions might require other files outside the Envelope itself, known as resources, which will be requested via API Callback. There resources are usually part of a .zip package that includes the SUIT Envelope and a Manifest file similar to McuManager's.

[!NOTE]
Resources don't need to have a valid Hash attached to them since, as explained above, only the target device knows the proper Hash. But the Envelope's Hash is required, and it supports different Modes, also known as Types or Algorithms. The list of SUIT Algorithms includes SHA256, SHAKE128, SHA384, SHA512 and SHAKE256. Of these, the only currently supported mode is SHA256.

import iOSMcuManagerLibrary

do {
    // Initialize the BLE transport using a scanned peripheral
    let bleTransport = McuMgrBleTransport(cbPeripheral)

    // Initialize the FirmwareUpgradeManager using the transport and a delegate
    let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)

    // Parse McuMgrSuitEnvelope from File URL
    let envelope = try McuMgrSuitEnvelope(from: dfuSuitEnvelopeUrl)

    // Look for valid Algorithm Hash 
    guard let sha256Hash = envelope.digest.hash(for: .sha256) else {
        throw McuMgrSuitParseError.supportedAlgorithmNotFound
    }

    // Set Configuration for SUIT
    var configuration = FirmwareUpgradeConfiguration()
    configuration.suitMode = true
    // Other Upgrade Modes can be set, but upgrade will fail since
    // SUIT doesn't support TEST and CONFIRM commands for example. 
    configuration.upgradeMode = .uploadOnly
    try dfuManager.start(hash: sha256Hash, data: envelope.data, using: configuration)
} catch {
    // Handle errors from McuMgrSuitEnvelope init, start() API call, etc.
}

Firmware Upgrade Mode

McuManager firmware upgrades can be performed following slightly different procedures. These different upgrade modes determine the commands sent after the upload step. The FirmwareUpgradeManager can be configured to perform these upgrade variations by setting the upgradeMode in FirmwareUpgradeManager's configuration property, explained below. (NOTE: this was previously set with mode property of FirmwareUpgradeManager, now removed) The different firmware upgrade modes are as follows:

Firmware Upgrade State

FirmwareUpgradeManager acts as a simple, mostly linear state machine which is determined by the mode. As the manager moves through the firmware upgrade process, state changes are provided through the FirmwareUpgradeDelegate's upgradeStateDidChange method.

The FirmwareUpgradeManager contains an additional state, validate, which precedes the upload. The validate state checks the current image state of the device in an attempt to bypass certain states of the firmware upgrade. For example, if the image to upgrade to already exists in slot 1 on the device, the FirmwareUpgradeManager will skip upload and move directly to test (or confirm if .confirmOnly mode has been set) from validate. If the uploaded image is already active, and confirmed in slot 0, the upgrade will succeed immediately. In short, the validate state makes it easy to reattempt an upgrade without needing to re-upload the image or manually determine where to start.

Firmware Upgrade Configuration

In version 1.2, new features were introduced to speed-up the Upload speeds, mirroring the work first done on the Android side, and they're all available through the new FirmwareUpgradeConfiguration struct.

Configuration Example

This is the way to start DFU with your own custom FirmwareUpgradeConfiguration:

import iOSMcuManagerLibrary

// Setup
let bleTransport = McuMgrBleTransport(cbPeripheral)
let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)

// Non-Pipelined Example
let nonPipelinedConfiguration = FirmwareUpgradeConfiguration(
    estimatedSwapTime: 10.0, eraseAppSettings: false, pipelineDepth: 2,
)

// Legacy / App-Core Only DFU Example
dfuManager.start(data: imageData, using: nonPipelinedConfiguration)

// Pipelined Example
let pipelinedConfiguration = FirmwareUpgradeConfiguration(
    estimatedSwapTime: 10.0, eraseAppSettings: true, pipelineDepth: 4,
    byteAlignment: .fourByte
)

// Multi-Image DFU Example
dfuManager.start(images: images, using: pipelinedConfiguration)

Note: You can of course mix-and-match configurations and the input parameter type of the images to upload.

Logging

Setting logDelegate property in a manager gives access to low level logs, that can help debugging both the app and your device. Messages are logged on 6 log levels, from .debug to .error, and additionally contain a McuMgrLogCategory, which identifies the originating component. Additionally, the logDelegate property of McuMgrBleTransport provides access to the BLE Transport logs.

Example

import iOSMcuManagerLibrary

// Initialize the BLE transport using a scanned peripheral
let bleTransport = McuMgrBleTransport(cbPeripheral)
bleTransport.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate

// Initialize the DeviceManager using the transport and a delegate
let deviceManager = DeviceManager(bleTransport, delegate)
deviceManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate

// Send echo
deviceManger.echo("Hello World!", callback)

OSLog integration

McuMgrLogDelegate can be easily integrated with the Unified Logging System. An example is provided in the example app in the AppDelegate.swift. A McuMgrLogLevel extension that can be found in that file translates the log level to one of OSLogType levels. Similarly, McuMgrLogCategory extension converts the category to OSLog type.

Related Projects

We've heard demand from developers for a single McuMgr DFU library to target multiple platforms. So we've made available a Flutter library that acts as a wrapper for both Android and iOS.