nRF Connect Device Manager

repository·main·Indexed 41 days ago

https://github.com/nordicsemi/ios-nrf-connect-device-manager

A library for managing and monitoring microcontrollers running Zephyr, Apache Mynewt, or SUIT via the McuManager protocol over Bluetooth LE. It provides capabilities for firmware updates (DFU), device observability, and file system management through two libraries: iOSMcuManagerLibrary and iOSOtaLibrary.

Tokens
5.6K
Snippets
11
Records
16
Agent score
1%

What's inside nRF Connect Device Manager

  1. Perform firmware upgrades with FirmwareUpgradeManager

    main

    The FirmwareUpgradeManager is the primary class for performing firmware updates on a device. It requires an McuMgrTransport (which defines the transport scheme and the device) to be initialized.

    Lifecycle and Control:

    • Start: Use start(package: McuMgrPackage) to begin an upgrade.
    • Control: You can pause(), resume(), or cancel() an ongoing upgrade.
    • Recommendation: While you can reuse a FirmwareUpgradeManager and McuMgrTransport pair for multiple operations, it is recommended to create a new pair for each DFU attempt to avoid issues.

    Supported Package Formats:

    • .bin files (Single-Core nRF52xxx MCUboot or Bare Metal nRF54Lxx MCUboot)
    • .suit files (SUIT Update)
    • .zip files (DirectXIP, Multi-Image MCUboot, or Multi-Image 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)
    
        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.
    }
  2. Understand the library structure: iOSMcuManagerLibrary vs iOSOtaLibrary

    main

    The project is distributed as a single SPM package containing two distinct libraries. They are independent; you can use one without the other.

    • iOSMcuManagerLibrary: Provides the core SMP Protocol, McuManager, SUIT, and DFU capabilities. It is available via both SPM and CocoaPods.
    • iOSOtaLibrary: (SPM Only) Provides Observability (via nRF Cloud powered by Memfault) and OTA (Over-the-Air) capabilities. It can be used to fetch DFU packages which can then be passed to iOSMcuManagerLibrary for a seamless OTA DFU process.

    Note: iOSOtaLibrary requires network availability for its features to work as expected, whereas iOSMcuManagerLibrary primarily relies on Bluetooth LE.

  3. Critical Threading Requirement for API Calls

    main

    ⚠️ IMPORTANT: Main Thread Requirement

    Always make your API calls from the Main Thread.

    Unless a specific call is explicitly documented as allowing background threads, all DFU, FileSystem, and other API operations must be executed on the Main Thread. Failure to follow this requirement may result in a deliberate application crash designed to alert you to the violation of library invariants.

  4. Perform custom multi-image uploads using ImageManager.Image

    main

    If you need to manually unpack a package or upload specific images/resources to specific cores, use the start(images:) method with an array of ImageManager.Image structs.

    Core Mapping:

    • image: 0 represents the App Core.
    • image: 1 represents the Net Core.

    Slot Mapping (for McuMgr/MCUboot):

    • Typically, you set slot: 1 (the alternate slot). After the device resets and swaps slots, the new firmware becomes active in slot 0.

    DirectXIP Note: For DirectXIP packages, you may need to provide multiple images for the same core (one for each available slot, e.g., slot 0 and slot 1) because DirectXIP firmware can boot from either slot without a swap.

    import iOSMcuManagerLibrary
    
    try {
        let bleTransport = McuMgrBleTransport(cbPeripheral)
        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.
    }
  5. Perform SUIT upgrades with resource handling

    main

    SUIT (Software Update for Internet Things) shifts logic to the target device. The sender provides a SUIT Envelope (a sequence of instructions).

    Key Requirements:

    • You must provide the Envelope's Hash.
    • iOSMcuManagerLibrary only supports SHA256 for SUIT hashing.
    • If the SUIT package is a .zip, the target may request additional resources. To handle this, you must use a SuitFirmwareUpgradeDelegate (which extends FirmwareUpgradeDelegate) and implement the uploadRequestsResource(_:) method.

    Implementation Pattern:

    1. Parse the envelope using McuMgrSuitEnvelope(from:).
    2. Extract the SHA256 hash from the envelope digest.
    3. Create an ImageManager.Image using the envelope data and hash.
    4. Call dfuManager.start(images:).
    import iOSMcuManagerLibrary
    
    do {
        let bleTransport = McuMgrBleTransport(cbPeripheral)
        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
    }
  6. Use Observability for device telemetry

    main

    Observability provides a constant stream of telemetry data from the device to nRF Cloud. This requires a continuous Bluetooth LE connection.

    Continuous Streaming

    Use ObservabilityManager.connectToDevice(observabilityIdentifier) to obtain an AsyncObservabilityStream. You can iterate over this stream using for try await event in observabilityStream to process incoming telemetry.

    Handling Connectivity Issues

    If network connectivity is lost, the library automatically stores received data chunks to disk and retries sending them. If you want to manually trigger a resume of pending uploads (e.g., via a user action), call continuePendingUploads(for:).

    import iOSOtaLibrary
    
    let identifier: UUID = // Target Peripheral UUID
    let observabilityManager = ObservabilityManager()
    
    // Connect and listen to the stream
    let observabilityStream = observabilityManager.connectToDevice(identifier)
    
    dot {
        for try await event in observabilityStream {
            // Process events
        }
    } catch let obsError as ObservabilityError {
        // Handle errors like pairing requirements
    } catch {
        // Handle other errors
    }
    
    // Manually resume uploads if needed
    try? observabilityManager.continuePendingUploads(for: identifier)
  7. Use the McuMgrPackage API for standard upgrades

    main

    The McuMgrPackage API is the recommended approach for 99% of use cases. It acts as a high-level wrapper that handles the parsing of a package (often a .zip containing a manifest.json) and automatically prepares the necessary image data and metadata for the FirmwareUpgradeManager.

    import iOSMcuManagerLibrary
    
    do {
        let bleTransport = McuMgrBleTransport(cbPeripheral)
        let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)
    
        let packageURL = /* URL to the package */
        let package = try McuMgrPackage(from: packageURL)
    
        dfuManager.start(package: package)
    } catch {
        // Handle errors
    }
  8. Perform OTA updates with iOSOtaLibrary

    main

    The iOSOtaLibrary handles Over-the-Air updates by interacting with nRF Cloud (powered by Memfault).

    Workflow:

    1. Use DeviceInfoManager to obtain a DeviceInfoToken and a ProjectKey.
    2. Use OTAManager.getLatestReleaseInfo(deviceInfo:projectKey:) to check for available updates.
    3. If an update is available, use OTAManager.download(artifact:) to download the update package to a local fileURL.
    4. Once downloaded, if the file is compatible with McuManager, use iOSMcuManagerLibrary's FirmwareUpgradeManager to perform the actual update.
    import iOSOtaLibrary
    import iOSMcuManagerLibrary
    
    do {
        let identifier: UUID = // Target Peripheral UUID
        let deviceInfoManager = DeviceInfoManager(identifier)
        
        let deviceInfo = try await deviceInfoManager.getDeviceInfoToken()
        let projectKey = try await deviceInfoManager.getProjectKey()
        
        let otaManager = OTAManager()
        switch otaManager.getLatestReleaseInfo(deviceInfo: deviceInfo, projectKey: projectKey) {
        case .success(let resultInfo):
            let artifact: ReleaseArtifact = // select from resultInfo
            
            switch try await otaManager.download(artifact: artifact) {
            case .success(let fileURL):
                // Use iOSMcuManagerLibrary to update
                let bleTransport = McuMgrBleTransport(cbPeripheral)
                let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)
                let package = try McuMgrPackage(from: fileURL)
                dfuManager.start(package: package)
            case .failure(let error):
                // Handle Error
                break
            }
        case .failure(let otaError):
            // Handle Error (e.g., OTAManagerError.deviceIsUpToDate)
            break
        }
    } catch {
        // Process errors
    }
  9. Install nRF Connect Device Manager via SPM or CocoaPods

    main

    You can integrate the library into your iOS, iPadOS, or macOS project using either Swift Package Manager (recommended) or CocoaPods.

    Swift Package Manager (SPM)

    1. Open your Xcode project.
    2. Select your Project in the Project Navigator.
    3. Go to the Package Dependencies tab.
    4. Click the + button.
    5. Paste the following URL into the search box: https://github.com/nordicsemi/IOS-nRF-Connect-Device-Manager
    6. Click Add Package.
    7. Once fetched, you can use import iOSMcuManagerLibrary and/or import iOSOtaLibrary in your Swift files.

    CocoaPods

    Add the following line to your Podfile:

    pod 'iOSMcuManagerLibrary'
    pod 'iOSMcuManagerLibrary'
  10. Enable logging in iOSMcuManagerLibrary

    main

    You can access low-level logs by setting the logDelegate property on managers (such as McuMgrBleTransport or DeviceManager).

    Logs are provided at 6 levels (from .debug to .error) and include a McuMgrLogCategory to identify the originating component. This is useful for debugging both your application and the connected device. The McuMgrBleTransport also provides access to BLE-specific transport logs.

    import iOSMcuManagerLibrary
    
    // Initialize the BLE transport
    let bleTransport = McuMgrBleTransport(cbPeripheral)
    bleTransport.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate
    
    // Initialize the DeviceManager
    let deviceManager = DeviceManager(bleTransport, delegate)
    deviceManager.logDelegate = UIApplication.shared.delegate as? McuMgrLogDelegate
  11. Perform a Firmware Upgrade using FirmwareUpgradeManager

    main

    A firmware upgrade typically involves a four-step process: upload, test, reset, and confirm using the image and default command groups.

    To simplify this, use the FirmwareUpgradeManager. This convenience manager automatically handles the logic by forwarding McuMgr/McuBoot-specific commands to the ImageManager. If it detects a SUIT upgrade procedure (like a SUIT Envelope), it will redirect the commands to the SuitManager instead.

  12. Configure FirmwareUpgradeConfiguration for performance and behavior

    main

    Use FirmwareUpgradeConfiguration to tune the upgrade process, specifically for speed and data handling.

    Available Properties:

    • pipelineDepth: Enables SMP Pipelining. Set to a value $> 1$ to send multiple write packets concurrently. Default is 1 (Disabled).
    • byteAlignment: Required for SMP Pipelining on older nRF Connect SDK (v1.7 or older). Use .fourByte or .disabled. Default is .disabled.
    • reassemblyBufferSize: Enables SMP Reassembly (NCS 2.0+). If the device supports it, the library automatically chunks data to match the device's buffer size. Must not exceed UInt16.max (65535).
    • eraseAppSettings: If true, erases all app data (Bond info, login, etc.) on the device. Default is false.
    • upgradeMode: Sets the firmware upgrade mode (see Firmware Upgrade Modes).
    • bootloaderMode: Informs the library of the supported operations by the Bootloader.
    import iOSMcuManagerLibrary
    
    // Setup
    let bleTransport = McuMgrBleTransport(cbPeripheral)
    let dfuManager = FirmwareUpgradeManager(bleTransport, delegate)
    
    // Pipelined Example
    let pipelinedConfiguration = FirmwareUpgradeConfiguration(
        estimatedSwapTime: 10.0, 
        eraseAppSettings: true, 
        pipelineDepth: 4,
        byteAlignment: .fourByte
    )
    
    dfuManager.start(package: package, using: pipelinedConfiguration)