注意
本仓库是 McuManager iOS 库 的一个分支,该库已不再由其原始维护者支持。自 2021 年起,我们已接管该库的所有权,因此所有新功能和错误修复都将在此处添加。请将您的项目迁移到指向此 Git 仓库,以便获取未来的更新。请参阅迁移指南。
nRF Connect 设备管理器库与 McuManager (或简称 McuMgr) 和 SUIT (物联网软件更新的缩写) 兼容。McuManager 是 nRF Connect SDK、Zephyr 和 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。
在 Xcode 中,打开您的根项目文件。然后,切换到Package Dependencies选项卡,并点击您添加的软件包列表下方的+按钮。将弹出一个新的模态窗口。在此新窗口的右上角,有一个搜索框。粘贴此 GitHub 项目的 URL https://github.com/NordicSemiconductor/IOS-nRF-Connect-Device-Manager,Add Package按钮应启用。
在 Xcode 获取您的新项目依赖项后,您现在应该能够在您想要调用此库的 Swift 文件中添加 import iOSMcuManagerLibrary。这样您就可以开始了。
pod 'iOSMcuManagerLibrary'
不用担心,我们为您提供了支持。只需按照这里的说明操作即可。
首先,克隆项目
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 中实现的管理器(组)有
DefaultManager: 包含与操作系统相关的命令。这包括任务和内存池统计信息、设备时间读取和写入以及设备重置。ImageManager: 管理设备上的镜像状态并执行镜像上传。StatsManager: 从设备读取统计信息。SettingsManager: 读取/写入设备上的配置值。LogManager: 从设备收集日志。CrashManager: 在设备上运行崩溃测试。RunTestManager: 在设备上运行测试。FileSystemManager: 从设备文件系统下载/上传文件。BasicManager: 向设备发送“擦除应用程序设置”命令。ShellManager: 向设备发送 McuMgr Shell 命令。SuitManager: 向设备发送特定于 SUIT(物联网软件更新)的命令。这适用于运行 SUIT 作为其引导加载程序的设备。固件升级通常是一个四步过程,使用来自 image 和 default 命令组的命令执行:upload、test、reset 和 confirm。
此库提供 FirmwareUpgradeManager,以便于升级设备上运行的镜像。FirmwareUpgradeManager 将 McuMgr/McuBoot 特定的命令转发到 ImageManager,或者如果检测到 SUIT 升级过程(例如上传是 SUIT Envelope),则将它们重定向到 SuitManager。
FirmwareUpgradeManager 提供了一种在设备上执行固件升级的简便方法。FirmwareUpgradeManager 必须使用 McuMgrTransport 初始化,后者定义了传输方案和设备。初始化后,FirmwareUpgradeManager 一次可以执行一个固件升级。固件升级使用 start(package: McuMgrPackage) 函数启动,可以使用 pause()、resume() 和 cancel() 分别暂停、恢复和取消。
注意
始终从主线程调用您的 start/pause/cancel DFU 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 软件包的目标是辅助/非活动插槽,也称为每个 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 甚至 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。
与 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.
}
您通常提供给 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 步骤之后发送的命令。可以通过在 FirmwareUpgradeManager 的 configuration 属性中设置 upgradeMode 来配置 FirmwareUpgradeManager 以执行这些升级变体,如下所述。(注意:这以前是使用 FirmwareUpgradeManager 的 mode 属性设置的,现在已删除)不同的固件升级模式如下
.testAndConfirm: 此模式是执行升级的默认和推荐模式,因为它能够从错误的固件升级中恢复。此模式的过程是 upload、test、reset、confirm。.confirmOnly: 此模式不推荐使用,除非对于多镜像 DFU,它是唯一支持的模式。如果设备无法启动到新镜像,则它将无法恢复,并且需要重新刷写。此模式的过程是 upload、confirm、reset。.testOnly: 如果您想在新镜像运行后手动确认其为主启动镜像之前对其运行测试,则此模式很有用。此模式的过程是 upload、test、reset。.uploadOnly: 这是一种非常特殊的模式。它不监听或确认引导加载程序信息,并且仅通过 upload,然后是 reset 来完成升级过程。就这样。是否决定使用此模式取决于用户,因为它不是默认模式。FirmwareUpgradeManager 充当一个简单的、大部分是线性的状态机,它由 mode 决定。当管理器在固件升级过程中移动时,状态更改通过 FirmwareUpgradeDelegate 的 upgradeStateDidChange 方法提供。
FirmwareUpgradeManager 包含一个附加状态 validate,它在上传之前。validate 状态检查设备的当前镜像状态,以尝试绕过固件升级的某些状态。例如,如果要升级到的镜像已存在于设备上的插槽 1 中,则 FirmwareUpgradeManager 将跳过 upload,并从 validate 直接移动到 test(或如果已设置 .confirmOnly 模式,则移动到 confirm)。如果上传的镜像已激活并在插槽 0 中确认,则升级将立即成功。简而言之,validate 状态使重新尝试升级变得容易,而无需重新上传镜像或手动确定从哪里开始。
在 1.2 版本中,引入了新功能来加速上传速度,镜像了 Android 端的首次完成的工作,它们都通过新的 FirmwareUpgradeConfiguration 结构提供。
pipelineDepth: (在示例应用程序 UI 中表示为“缓冲区数量”。) 对于大于 1 的值,这将启用 SMP 流水线功能。这意味着并发发送多个写入数据包,从而在接收设备配置的缓冲区数量越高时提供更大的速度提升。默认设置为 1(缓冲区数量 = 禁用)。byteAlignment: 当与 SMP 流水线结合使用时,这是必需的。通过固定为固件升级发送的每个数据块的大小,我们可以预测接收设备的偏移量跳跃,从而平稳地同时发送多个数据包。当不使用 SMP 流水线时(pipelineDepth 设置为 1),如果设置了字节对齐,库仍然会执行字节对齐,但这不是更新工作所必需的。默认设置为 ImageUploadAlignment.disabled。DefaultManager 发送请求,请求 MCU Manager 参数。如果收到,则意味着固件可以接受大于 MTU 大小的数据块,因此也提高了速度。此属性将反映接收设备上缓冲区的大小,并且 McuMgrBleTransport 将设置为在同一序列号内对数据进行分块,从而将每个数据包传输保持在 MTU 边界内。SMP 重组不需要任何工作即可工作 - 在不支持它的设备上,MCU Manager 参数请求将失败,并且上传将继续进行,假设没有重组功能。不得大于 UInt16.max (65535)eraseAppSettings: 这不是与速度相关的功能。相反,将其设置为 true 意味着设备上的所有应用程序数据,包括绑定信息、步数、登录名或任何其他内容都将被擦除。如果在更新后新固件有任何重大数据更改,例如功能的完全更改或具有不同保存结构的新更新,则建议这样做。默认设置为 false。upgradeMode: 固件升级模式。请参阅上面的章节,详细解释所有可能的升级模式。bootloaderMode: 引导加载程序模式不一定旨在作为设置。如果目标固件未对引导加载程序信息请求提供有效响应,例如,如果不支持,则其行为类似于设置。它的作用是告知 iOSMcuMgrLibrary 引导加载程序支持的操作。例如,如果 upgradeMode 设置为 confirmOnly,但引导加载程序处于 DirectXIP 且没有恢复模式,则发送 Confirm 命令将返回错误。这意味着,尽管 upgradeMode 设置如此,但不会发送 Confirm 命令。所以是的,这是我们必须处理的 SMP / McuManager 的又一层复杂性。这是使用您自己的自定义 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,用于标识原始组件。此外,McuMgrBleTransport 的 logDelegate 属性提供对 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)
McuMgrLogDelegate 可以轻松地与 统一日志记录系统 集成。示例应用程序中的 AppDelegate.swift 中提供了一个示例。在该文件中可以找到的 McuMgrLogLevel 扩展将日志级别转换为 OSLogType 级别之一。类似地,McuMgrLogCategory 扩展将类别转换为 OSLog 类型。
我们已经听到开发人员要求使用单个 McuMgr DFU 库来面向多个平台。因此,我们提供了 一个 Flutter 库,它充当 Android 和 iOS 的包装器。