DFU SDK
The dfu module performs Device Firmware Upgrades on a Corsano bracelet. The firmware images ship inside the module itself, so an upgrade needs no download and no network connection — you tell the SDK which device to upgrade and it uploads the bundled image over BLE.
implementation "com.corsano.sdk:dfu:$corsanoSdkVersion"
The module declares its own DfuService and the Bluetooth and foreground-service permissions it needs, and these are merged into your app at build time. You do not have to declare the service yourself.
Setting up
Pass a CorsanoDfu.Config when you initialise the SDK. See Getting Started.
CorsanoDfu.Config
| Parameter | Type | Default | Description |
|---|---|---|---|
notificationTarget | Class<out Activity>? | null | The activity opened when the user taps the DFU notification. |
logger | SdkLogger? | null | Module-specific logger. Falls back to the logger on CorsanoSdkConfig. |
Once initialised, everything goes through the singleton:
val dfu = CorsanoDfu.getInstance()
Note:
getInstance()throwsIllegalArgumentExceptionif the DFU module was not initialised — that is, if noCorsanoDfu.Configwas added toCorsanoSdkConfig.
The hardwareId parameter
Every method below takes a hardwareId as an Int, not as the enum. Read it from the connected device and pass its .value:
val hardwareId = currentDevice.deviceInfo.hardwareId?.value ?: HardwareId.UNKNOWN.value
HardwareId | Value | Device |
|---|---|---|
WATCH_284N | 38 | Watch 284N |
WATCH_287N | 50 | Watch 287N |
BRACELET_287N | 51 | CardioWatch 287-1B ("B1") |
BRACELET_287N_2B | 54 | CardioWatch 287-2B ("B2") |
UNKNOWN | -1 | Unrecognised |
Warning: Firmware images are bundled only for B1 (51) and B2 (54). Any other value — including
UNKNOWN— makesneedDfu,getLatestFirmwareVersionandgetAvailableVersionthrow. Always check the hardware ID is one of the two supported bracelets before calling them.
Methods
needDfu
fun needDfu(hardwareId: Int, currentVersion: FirmwareVersion): Boolean
Whether a newer firmware than currentVersion is bundled for this hardware. Equivalent to currentVersion < getLatestFirmwareVersion(hardwareId).
| Parameter | Type | Description |
|---|---|---|
hardwareId | Int | Hardware ID of the bracelet |
currentVersion | FirmwareVersion | Firmware currently on the bracelet |
Returns Boolean — true if an upgrade is available.
Throws Error if hardwareId is not a supported bracelet.
val needsUpgrade = CorsanoDfu.getInstance().needDfu(
currentDevice.deviceInfo.hardwareId!!.value,
currentDevice.deviceInfo.firmwareVersion!!
)
getLatestFirmwareVersion
fun getLatestFirmwareVersion(hardwareId: Int): FirmwareVersion
The newest firmware version bundled for this hardware.
Returns FirmwareVersion.
Throws Error if hardwareId is not a supported bracelet.
val latest = CorsanoDfu.getInstance().getLatestFirmwareVersion(hardwareId)
Log.d("Corsano-SDK", "Latest available: ${latest.text}")
getAvailableVersion
@JvmStatic
fun getAvailableVersion(hardwareId: Int): FirmwareVersion
Identical result to getLatestFirmwareVersion, but static — usable before the DFU module has been initialised.
Returns FirmwareVersion.
Throws Error if hardwareId is not a supported bracelet.
val latest = CorsanoDfu.getAvailableVersion(hardwareId)
startDfu
fun startDfu(address: String, hardwareId: Int)
Uploads the latest bundled firmware for this hardware.
| Parameter | Type | Description |
|---|---|---|
address | String | MAC address of the bracelet |
hardwareId | Int | Hardware ID of the bracelet |
Returns nothing. Progress and completion arrive through DfuListener.
Throws IllegalStateException if a DFU is already in progress; Error if hardwareId is not a supported bracelet.
CorsanoDfu.getInstance().startDfu(address, hardwareId)
Sample app: DfuFragment.kt in the sample module (sdk/sample/src/main/java/com/corsano/sdk/sample/screens/dfu/) is a complete working example. It calls startDfu(address, hardwareId) from the start button's click listener, and shows the surrounding lifecycle you need:
override fun onStart() {
super.onStart()
val corsanoDfu = CorsanoDfu.getInstance()
// restore the UI from the current state, in case a DFU is already running
renderState(corsanoDfu.getDfuState())
corsanoDfu.addDfuListener(this)
binding.startButton.setOnClickListener {
corsanoDfu.startDfu(args.address, args.hardwareId)
}
}
override fun onStop() {
CorsanoDfu.getInstance().removeDfuStatusListener(this)
super.onStop()
}
The fragment implements DfuListener and every callback simply re-reads getDfuState() and re-renders — a single rendering path for both live updates and state restored after the screen is recreated.
startDfu (specific version)
fun startDfu(address: String, hardwareId: Int, specificVersion: FirmwareVersion)
Uploads a specific bundled firmware version instead of the latest — useful for pinning a fleet to a validated release, or for downgrading.
| Parameter | Type | Description |
|---|---|---|
address | String | MAC address of the bracelet |
hardwareId | Int | Hardware ID of the bracelet |
specificVersion | FirmwareVersion | The exact version to install |
Returns nothing.
Throws IllegalStateException if a DFU is already in progress; IllegalArgumentException if that version is not bundled for that hardware.
Warning: Specific versions are available for B2 only. Calling this for a B1 always throws
IllegalArgumentException, whatever version you ask for.
Bundled B2 versions: 5.39, 5.72, 5.82, 6.45, 6.65, 6.87, 7.14, 7.20, 7.23, 7.34, 7.48, 7.65, 7.97, 8.12, 8.26, 8.48.
CorsanoDfu.getInstance().startDfu(address, hardwareId, FirmwareVersion(8, 26))
Sample app: the same DfuFragment.kt carries this overload as a commented-out alternative next to the call to the latest-version one.
getDfuState
fun getDfuState(): DfuState
The current state, for when you need to poll rather than listen — for example to restore your UI after a configuration change.
Returns DfuState. Before any DFU has run, its status is DfuStatus.NONE.
val state = CorsanoDfu.getInstance().getDfuState()
if (state.isInProgress) showProgress(state.progressPercentage)
addDfuListener / removeDfuStatusListener
fun addDfuListener(listener: DfuListener)
fun removeDfuStatusListener(listener: DfuListener)
Register and unregister a DfuListener. Note the asymmetric names: you add a DfuListener but remove a DfuStatusListener.
override fun onStart() {
super.onStart()
CorsanoDfu.getInstance().addDfuListener(this)
}
override fun onStop() {
CorsanoDfu.getInstance().removeDfuStatusListener(this)
super.onStop()
}
extractDfuDeviceAddress
@JvmStatic
fun extractDfuDeviceAddress(intent: Intent?): String?
Reads the bracelet's MAC address out of the intent delivered when the user taps the DFU notification, so the activity named by notificationTarget can tell which device the notification referred to.
Returns the MAC address, or null if the intent carries none.
val address = CorsanoDfu.extractDfuDeviceAddress(intent)
DfuListener
interface DfuListener {
fun onDfuStatusChanged(address: String, status: DfuStatus)
fun onUploadProgress(address: String, percentage: Int, part: Int, totalParts: Int)
fun onDfuCompleted(address: String)
fun onDfuFailed(address: String, error: DfuError)
fun onDfuAborted(address: String)
}
| Callback | Parameters | When |
|---|---|---|
onDfuStatusChanged | address, status | Every time the status changes. Fires for each transition in the table below. |
onUploadProgress | address, percentage (0–100), part, totalParts | Repeatedly while the image uploads. |
onDfuCompleted | address | The upgrade finished successfully. |
onDfuFailed | address, error | The upgrade failed. |
onDfuAborted | address | See the warning below. |
Warning: In the current SDK version
onDfuAbortedis never called. When a DFU is aborted the SDK notifies listeners throughonDfuCompletedinstead, even though the upgrade did not succeed. To detect an abort reliably, check the status rather than trusting the callback: eitheronDfuStatusChangedreportingDfuStatus.DFU_ABORTED, orgetDfuState().isAborted. Do not treatonDfuCompletedalone as proof of success.
DfuState
data class DfuState(
val status: DfuStatus,
val address: String? = null,
val error: DfuError? = null,
val progressPercentage: Int = 0
)
| Member | Type | Description |
|---|---|---|
status | DfuStatus | Where the process currently is |
address | String? | MAC address of the bracelet being upgraded |
error | DfuError? | Set when status is DFU_FAILED |
progressPercentage | Int | Upload progress, 0–100 |
isInProgress | Boolean | The process is still running |
isSuccessful | Boolean | status == DFU_COMPLETED |
isFailed | Boolean | status == DFU_FAILED |
isAborted | Boolean | status == DFU_ABORTED |
DfuStatus
Each value carries inProgress, which is what DfuState.isInProgress reports.
| Status | In progress | Meaning |
|---|---|---|
NONE | no | No DFU has been started |
INITIATED | yes | startDfu accepted the request |
CONNECTING | yes | Connecting to the bracelet |
CONNECTED | yes | Connected |
ENABLING_DFU_MODE | yes | Switching the bracelet into DFU mode |
DFU_STARTING | yes | Upgrade starting |
DFU_STARTED | yes | Upgrade started |
DFU_UPLOADING | yes | Uploading the image; progressPercentage updates |
VALIDATING_FIRMWARE | yes | Bracelet validating the uploaded image |
DISCONNECTING | yes | Disconnecting |
DISCONNECTED | yes | Disconnected |
DFU_COMPLETED | no | Finished successfully |
DFU_FAILED | no | Failed; see DfuError |
DFU_ABORTED | no | Aborted |
DfuError
data class DfuError(val errorCode: Int, val message: String)
| Member | Type | Description |
|---|---|---|
errorCode | Int | Error code from the underlying Nordic DFU library |
message | String | Human-readable description |
FirmwareVersion
FirmwareVersion comes from the ble module and is Comparable, so versions can be compared directly.
| Member | Type | Description |
|---|---|---|
major | Int | Major number |
minor | Int | Minor number |
number | Int | major * 10000 + minor, used for ordering |
text | String | "major.minor", e.g. "8.48" |
if (currentVersion < FirmwareVersion(8, 26)) {
// firmware predates 8.26
}
Upload behaviour
The upload is configured by the SDK and is not adjustable:
- The existing bond is kept, so the bracelet does not need re-pairing afterwards.
- Failed uploads are retried up to 3 times before
onDfuFailed. - DFU runs as a background service; a notification channel is created for it.
DFU recovery
A recovery process is necessary if a DFU started but was interrupted midway (Bluetooth switched off, app killed). It is rare but it happens. The bracelet will appear dead — lights off, even on the charger. Restarting the DFU from the app restores it.
Scenarios:
- DFU interrupted, or app killed. Prompt the user to restart the DFU.
- The patient uninstalled the app mid-upgrade. During scanning, look for a bracelet in recovery mode and prompt to restart the DFU.
A bracelet needing recovery is reported during the scan with ScanDeviceType.BRACELET_1B_NEED_RECOVERY or ScanDeviceType.BRACELET_2B_NEED_RECOVERY, and ScanDeviceType.needsRecovery is true for both. See Pairing for the scanning flow.
Recovery is an ordinary startDfu call against the recovered bracelet's address.