注意

本仓库是 McuManager iOS 库 的一个分支,该库已不再由其原始维护者支持。自 2021 年起,我们已接管该库的所有权,因此所有新功能和错误修复都将在此处添加。请将您的项目迁移到指向此 Git 仓库,以便获取未来的更新。请参阅迁移指南

nRF Connect 设备管理器

Swift Platforms License Release Swift Package Manager CocoaPods

nRF Connect 设备管理器库与 McuManager (或简称 McuMgr)SUIT (物联网软件更新的缩写) 兼容。McuManager 是 nRF Connect SDKZephyr 和 Apache Mynewt 支持的管理子系统。McuManager 依赖于其自身的 MCUboot 引导加载程序,用于固件更新后的安全引导,并使用 简单管理协议,或 SMP,通过蓝牙 LE 进行通信。此库实现的蓝牙低功耗 SMP 传输定义 可以在这里找到

SUIT 和 McuManager 是相关的,但不可互换。SUIT 依赖于其自身的引导加载程序,但通过 SMP 服务进行通信。此外,SUIT 支持 McuManager 的某些功能,但不保证全部支持。最好始终检查 McuManager 功能是否受支持,方法是发送请求,而不是假设它受支持。

该库提供了 McuManager 协议的传输无关实现。它包含 BLE 传输的默认实现。

最低要求的 iOS 版本为 12.0,最初于 2018 年秋季发布。

警告

此库是 Nordic Semiconductor 的设备固件更新的默认和主要 API,不应与之前的协议 NordicDFU 混淆,后者由 旧 DFU 库 提供服务。

兼容设备

nRF52 系列 nRF53 系列 nRF54 系列 nRF91 系列

此库旨在与基于 BLE 的 SMP 传输一起使用。它由 Nordic Semiconductor 实现和维护,但它应该适用于任何通过 SMP 协议通信的设备。如果您在使用任何芯片(不仅限于 Nordic)的设备进行通信时遇到问题,请提交 Issue

将库添加到现有项目 (安装)

SPM 或 Swift Package Manager (推荐)

在 Xcode 中,打开您的根项目文件。然后,切换到Package Dependencies选项卡,并点击您添加的软件包列表下方的+按钮。将弹出一个新的模态窗口。在此新窗口的右上角,有一个搜索框。粘贴此 GitHub 项目的 URL https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-ManagerAdd Package按钮应启用。

在 Xcode 获取您的新项目依赖项后,您现在应该能够在您想要调用此库的 Swift 文件中添加 import iOSMcuManagerLibrary。这样您就可以开始了。

CocoaPods

pod 'iOSMcuManagerLibrary'

构建示例项目 (需要 Xcode & CocoaPods)

"Cocoapods?"

不用担心,我们为您提供了支持。只需按照这里的说明操作即可。

说明

首先,克隆项目

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

然后,打开项目的目录,导航到Example文件夹,并运行 pod install

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

输出应类似于此

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.

您现在应该能够通过打开 nRF Connect Device Manager.xcworkspace 文件来打开、构建和运行示例项目

open nRF\ Connect\ Device\ Manager.xcworkspace

介绍

McuManager 是一种应用程序层协议,用于管理和监控运行 Apache Mynewt 和 Zephyr 的微控制器。更具体地说,McuManager 实现了空中 (OTA) 固件升级、日志、统计信息、文件系统和配置管理。运行 SUIT 作为其引导加载程序的设备可能会响应 McuManager 命令,但这不能保证。

命令组

McuManager 按功能组织成命令组。在 mcumgr-ios 中,命令组称为管理器,并扩展 McuManager 类。在 mcumgr-ios 中实现的管理器(组)有

固件升级

固件升级通常是一个四步过程,使用来自 imagedefault 命令组的命令执行:uploadtestresetconfirm

此库提供 FirmwareUpgradeManager,以便于升级设备上运行的镜像。FirmwareUpgradeManager 将 McuMgr/McuBoot 特定的命令转发到 ImageManager,或者如果检测到 SUIT 升级过程(例如上传是 SUIT Envelope),则将它们重定向到 SuitManager

FirmwareUpgradeManager

FirmwareUpgradeManager 提供了一种在设备上执行固件升级的简便方法。FirmwareUpgradeManager 必须使用 McuMgrTransport 初始化,后者定义了传输方案和设备。初始化后,FirmwareUpgradeManager 一次可以执行一个固件升级。固件升级使用 start(package: McuMgrPackage) 函数启动,可以使用 pause()resume()cancel() 分别暂停、恢复和取消。

注意

始终从主线程调用您的 start/pause/cancel DFU API。

McuMgrPackage API

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 packageURL = /* Obtain URL to the file user wants to Upload */
    let package = try McuMgrPackage(from: packageURL)

    // Start the firmware upgrade with the given package
    dfuManager.start(package: package)
} catch {
    // Package initialisation errors here.
}

这是我们新的、改进的、全能的 API。您创建一个 McuMgrPackage,并将其提供给 FirmwareUpgradeManager没有第三步。此 API 支持

这是您应该在 99% 的时间使用的 API,除非您想做一些特定的事情。例如,您想解压缩您自己的软件包,并且仅上传特定核心的某些镜像/资源,这种情况非常罕见。

请查看示例项目中的 FirmwareUpgradeViewController.swift,以获取更详细的用法示例。

自定义多镜像上传示例

public class ImageManager: McuManager {
    
    public struct Image {
        public let name: String?
        public let image: Int
        public let slot: Int
        public let content: McuMgrManifest.File.ContentType
        public let hash: Data
        public let data: Data

        /* ... */
    }
}

以上是基于镜像的 API 调用的输入类型,其中 image 参数值为 0 表示 App Core,输入 1 表示 Net Core。这些表示最初是为基于 McuMgr/MCUboot 的产品设计的,而不是 SUIT。在 SUIT 中,没有“镜像”或“插槽”的概念,因此它们被忽略。但是为了保持相同的 API 可重用于 McuMgr/MCUboot 和 SUIT 设备,我们保留了它们以实现向后兼容性。

对于 McuMgr/MCUboot,您通常希望将 slot 参数设置为 1,这是当前未被该特定核心使用的备用插槽。然后,在上传后,固件设备将重置以交换其插槽,使先前上传到插槽 1 的内容(在交换后现在位于插槽 0 中)变为活动状态,反之亦然。

有了 Image 结构,就可以直接调用来启动任一或两个核心的 DFU

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

DirectXIP 配置

非 DirectXIP 软件包的目标是辅助/非活动插槽,也称为每个 ImageManager.Image 的插槽 1,而 DirectXIP 软件包必须特别注意。由于它们为同一个 ImageManager.Image 提供了多个哈希值,每个可用插槽一个。这是因为支持 DirectXIP 的固件可以从任一插槽启动,而无需交换。因此,对于 DirectXIP,[ImageManager.Image] 数组可能更接近于

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

多镜像 DFU 格式

通常,在执行多镜像 DFU 甚至 SUIT 更新时,每个核心的附加镜像的交付格式将为 .zip 文件。这是因为 .zip 文件允许我们捆绑必要的信息,包括每个核心的镜像以及应将哪个镜像上传到哪个核心。镜像文件(通常为 .bin 格式)与应将它们上传到的核心之间的这种关联写在一个强制性的 JSON 格式中,称为 Manifest。此 manifest.json 由我们的 nRF Connect SDK 生成,作为我们 Zephyr 构建系统的一部分,如此处文档所示。您可以查看库中的 McuMgrManifest 结构定义,以深入了解清单中包含的信息。

为了弥合自定义镜像上传 API 与我们 Zephyr 构建系统的输出之间的差距,我们编写了 McuMgrPackage,它在其 init() 函数中接受 URL。由于 McuMgrPackage 方法的 JSON Manifest 解析性质,您可能会遇到边缘情况/崩溃。如果您发现这些情况,请向我们报告。但无论如何,McuMgrPackage 快捷方式是一个包装器,它初始化了前面提到的 [ImageManager.Image] 数组 API。因此,您始终可以回退到该 API。

告诉我关于 SUIT 的信息

与 McuManager 不同,SUIT 将固件更新的大部分逻辑(读取:责任)放在目标设备而不是发送者(又名“您”,API 用户)上。这简化了内部流程,但也使解析原始数据及其内容变得更加复杂。例如,我们无法确定发送到固件的每个组件(文件)的正确哈希签名,因为 SUIT 不是为每个插槽或核心提供固定的二进制文件,而是旨在表示引导加载程序要执行的一系列指令。这意味着要在目标设备端的固件更新期间动态更改要刷新的最终二进制文件的哈希值。

从发送者的角度来看,我们只需要完整发送“数据”,并让目标设备自行解决问题。这组字节表示我们所说的 SUIT Envelope,它是固件要运行的一系列指令,类似于我们在将其馈送到编译器之前编写的代码。这些指令可能需要 Envelope 本身之外的其他文件,称为资源,这些资源将通过 API 回调请求。这些资源通常是 .zip 软件包的一部分,该软件包包含 SUIT Envelope 和来自 McuManager 的 Manifest 文件衍生物。

注意

资源不需要附加有效的哈希值,因为如上所述,只有目标设备知道正确的哈希值。但是 Envelope 的哈希值是必需的,并且它支持不同的模式,也称为类型或算法。SUIT 算法列表包括 SHA256、SHAKE128、SHA384、SHA512 和 SHAKE256。其中,目前唯一支持的模式是 SHA256

如果您想使用 ImageManager.Image API 设置 SUIT 升级,请参阅以下示例代码

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
    }

    let suitImage = ImageManager.Image(image: 0, hash: sha256Hash, data: envelope.data)
    try dfuManager.start(images: [suitImage])
} catch {
    // Handle errors from McuMgrSuitEnvelope init, start() API call, etc.
}

SuitFirmwareUpgradeDelegate

您通常提供给 FirmwareUpgradeManager 的委托类型是 FirmwareUpgradeDelegate。这将涵盖 McuMgr/McuBoot 升级以及 SUIT 的“规范”变体的任何需求,这意味着只需要发送 Envelope。但是,当升级文件是 .zip 文件时,可能会有其他资源(例如文件)是目标固件可能请求的。发生这种情况时,需要 SuitFirmwareUpgradeDelegate,它是 FirmwareUpgradeDelegate 的扩展。SuitFirmwareUpgradeDelegate 添加了一个新函数,以通知您何时需要资源。在大多数情况下,请求的资源将是 .zip 软件包的一部分,因此它将是一个非常简单的实现。这是一个例子

func uploadRequestsResource(_ resource: FirmwareUpgradeResource) {
    let image: ImageManager.Image! = package?.image(forResource: resource)
    firmwareUpgradeManager.uploadResource(resource, data: image.data)
}

固件升级模式

McuManager 固件升级可以通过略有不同的程序执行。这些不同的升级模式决定了在 upload 步骤之后发送的命令。可以通过在 FirmwareUpgradeManagerconfiguration 属性中设置 upgradeMode 来配置 FirmwareUpgradeManager 以执行这些升级变体,如下所述。(注意:这以前是使用 FirmwareUpgradeManagermode 属性设置的,现在已删除)不同的固件升级模式如下

固件升级状态

FirmwareUpgradeManager 充当一个简单的、大部分是线性的状态机,它由 mode 决定。当管理器在固件升级过程中移动时,状态更改通过 FirmwareUpgradeDelegateupgradeStateDidChange 方法提供。

FirmwareUpgradeManager 包含一个附加状态 validate,它在上传之前。validate 状态检查设备的当前镜像状态,以尝试绕过固件升级的某些状态。例如,如果要升级到的镜像已存在于设备上的插槽 1 中,则 FirmwareUpgradeManager 将跳过 upload,并从 validate 直接移动到 test(或如果已设置 .confirmOnly 模式,则移动到 confirm)。如果上传的镜像已激活并在插槽 0 中确认,则升级将立即成功。简而言之,validate 状态使重新尝试升级变得容易,而无需重新上传镜像或手动确定从哪里开始。

固件升级配置

在 1.2 版本中,引入了新功能来加速上传速度,镜像了 Android 端的首次完成的工作,它们都通过新的 FirmwareUpgradeConfiguration 结构提供。

配置示例

这是使用您自己的自定义 FirmwareUpgradeConfiguration 启动 DFU 的方法

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,
)
dfuManager.start(package: package, using: nonPipelinedConfiguration)

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

日志记录

在管理器中设置 logDelegate 属性可以访问低级别日志,这可以帮助调试应用程序和您的设备。消息记录在 6 个日志级别上,从 .debug.error,并且还包含一个 McuMgrLogCategory,用于标识原始组件。此外,McuMgrBleTransportlogDelegate 属性提供对 BLE 传输日志的访问。

示例

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 集成

McuMgrLogDelegate 可以轻松地与 统一日志记录系统 集成。示例应用程序中的 AppDelegate.swift 中提供了一个示例。在该文件中可以找到的 McuMgrLogLevel 扩展将日志级别转换为 OSLogType 级别之一。类似地,McuMgrLogCategory 扩展将类别转换为 OSLog 类型。

相关项目

我们已经听到开发人员要求使用单个 McuMgr DFU 库来面向多个平台。因此,我们提供了 一个 Flutter 库,它充当 Android 和 iOS 的包装器。