This guide is written for app developers using open_earable_flutter. It explains how to list firmware versions, let a user select a device and firmware file, start an update, and render progress in your own UI.
The FOTA API gives you the building blocks for a firmware update flow:
FotaCapabilityas the device-level abstraction layer for firmware updatesFirmwareImageRepositoryto load stable firmware releases from GitHubUnifiedFirmwareRepositoryto load stable releases and optional beta buildsRemoteFirmwareandLocalFirmwareto represent selectable firmware filesSingleImageFirmwareUpdateRequestandMultiImageFirmwareUpdateRequestto describe the update jobUpdateBlocto execute the update and emit UI-friendly progress states
Make sure your app can already:
- Discover and connect to a wearable with
WearableManager - Hold on to the connected
Wearable - Ask the user which firmware they want to install
If you have not connected to a device yet, start there first. A firmware update needs a connected Wearable so you can obtain its FotaCapability.
Once your app has a connected wearable, obtain its FotaCapability:
final fota = wearable.getCapability<FotaCapability>();
if (fota == null) {
// This wearable does not support firmware updates.
return;
}This is the abstraction layer your app should use for device-specific firmware operations. The wearable may implement it using mcumgr today and a different backend in the future.
You can let the user choose firmware from a remote repository, from a local file, or both.
Use FirmwareImageRepository if you only want official releases:
final repository = FirmwareImageRepository();
final List<RemoteFirmware> firmwares = await repository.getFirmwareImages();Each RemoteFirmware contains:
name: a UI-friendly labelversion: the release versionurl: the download URLtype:FirmwareType.singleImageorFirmwareType.multiImage
You can use that list in any widget:
ListView.builder(
itemCount: firmwares.length,
itemBuilder: (context, index) {
final firmware = firmwares[index];
return ListTile(
title: Text(firmware.name),
subtitle: Text(firmware.version),
onTap: () {
// store the selected firmware in your state
},
);
},
);If your app should optionally expose preview firmware:
final repository = UnifiedFirmwareRepository();
final List<FirmwareEntry> entries = await repository.getAllFirmwares(
includeBeta: true,
);FirmwareEntry tells you whether an item is stable or beta:
for (final entry in entries) {
final firmware = entry.firmware;
final sourceLabel = entry.isBeta ? 'Beta' : 'Stable';
print('${firmware.name} [$sourceLabel]');
}If the user already has a firmware file, create a LocalFirmware object from the file bytes. The important part is setting the correct FirmwareType.
final bytes = await file.readAsBytes();
final localFirmware = LocalFirmware(
name: 'my_firmware.zip',
data: bytes,
type: FirmwareType.multiImage,
);Use:
FirmwareType.singleImagefor raw single-image files such as.binFirmwareType.multiImagefor archive-based FOTA bundles such as.zip
Ask the wearable's FotaCapability to create the request for the selected
firmware:
final request = fota.createFirmwareUpdateRequest(selectedFirmware);For the current mcumgr-backed implementation, this returns:
MultiImageFirmwareUpdateRequestfor remote firmware and local.zipfilesSingleImageFirmwareUpdateRequestfor local.binfiles
Apps should depend on FotaCapability for request creation instead of building
device-specific request objects themselves.
Create the bloc with the prepared request and dispatch BeginUpdateProcess.
final updateBloc = UpdateBloc(
firmwareUpdateRequest: request,
);
updateBloc.add(BeginUpdateProcess());In a Flutter screen this is usually done with BlocProvider:
BlocProvider(
create: (_) => UpdateBloc(firmwareUpdateRequest: request),
child: const FirmwareUpdateScreen(),
)UpdateBloc emits UpdateState objects you can map directly to your UI.
The most important states are:
UpdateInitial: nothing has started yetUpdateFirmwareStateHistory: the update is running or has completedUpdateCompleteSuccess: appears inside the history as the successful end stateUpdateCompleteFailure: appears inside the history as the failed end stateUpdateCompleteAborted: appears inside the history when the user aborts the update
The simplest integration pattern is:
BlocBuilder<UpdateBloc, UpdateState>(
builder: (context, state) {
switch (state) {
case UpdateInitial():
return ElevatedButton(
onPressed: () {
context.read<UpdateBloc>().add(BeginUpdateProcess());
},
child: const Text('Start update'),
);
case UpdateFirmwareStateHistory():
if (state.currentState is UpdateProgressFirmware) {
final progressState = state.currentState as UpdateProgressFirmware;
return Column(
children: [
Text('Uploading ${progressState.progress}%'),
ElevatedButton(
onPressed: () {
context.read<UpdateBloc>().add(AbortUpdate());
},
child: const Text('Abort update'),
),
],
);
}
if (state.isComplete) {
final lastState = state.history.isNotEmpty ? state.history.last : null;
if (lastState is UpdateCompleteFailure) {
return Text('Update failed: ${lastState.error}');
}
if (lastState is UpdateCompleteAborted) {
return const Text('Update aborted');
}
return const Text('Update completed');
}
return Text(state.currentState?.stage ?? 'Preparing update');
default:
return const Text('Unknown update state');
}
},
)To abort a running update, dispatch:
context.read<UpdateBloc>().add(AbortUpdate());Some wearables also expose FotaSlotInfoCapability for implementations that
have a slot or image-table concept. This is separate from FotaCapability,
because not every firmware update backend uses the same slot model.
final slotInfo = wearable.getCapability<FotaSlotInfoCapability>();
if (slotInfo != null) {
final slots = await slotInfo.readFirmwareSlots();
for (final slot in slots) {
print(
'image=${slot.image} slot=${slot.slot} '
'version=${slot.version} active=${slot.active} '
'confirmed=${slot.confirmed} pending=${slot.pending}',
);
}
}Each FirmwareSlotInfo contains:
imageslotversionhashandhashStringbootablependingconfirmedactivepermanent
This is useful when you want to show the current primary and secondary images before or after an update.
Slot-aware wearables can erase an inactive firmware image slot through
FotaSlotInfoCapability:
final slotInfo = wearable.getCapability<FotaSlotInfoCapability>();
if (slotInfo != null) {
await slotInfo.eraseFirmwareSlot();
await slotInfo.eraseFirmwareSlot(channel: 1);
}When channel is omitted, the firmware backend erases its default secondary
image slot. When channel is provided, the erase request targets that raw
mcumgr image slot channel. Devices reject erase requests for slots that contain
a confirmed image, an image pending test on the next reboot, or an active
split-image slot.
You do not need to call the lower-level handler classes directly, but it helps to know what UpdateBloc is doing:
FirmwareDownloaderdownloads remote firmware filesFirmwareUnpackerextracts.zipbundles and readsmanifest.jsonFirmwareUpdatersends the prepared image data to the device
This means:
- remote firmware files are downloaded automatically
- multi-image archives are unpacked automatically
- local firmware files skip the download step
This is the minimal end-to-end shape of a typical integration:
final repository = FirmwareImageRepository();
final firmwares = await repository.getFirmwareImages();
final selectedFirmware = firmwares.first;
final request = MultiImageFirmwareUpdateRequest(
peripheral: SelectedPeripheral(
name: wearable.name,
identifier: wearable.deviceId,
),
firmware: selectedFirmware,
);
BlocProvider(
create: (_) => UpdateBloc(firmwareUpdateRequest: request),
child: BlocBuilder<UpdateBloc, UpdateState>(
builder: (context, state) {
if (state is UpdateInitial) {
return ElevatedButton(
onPressed: () {
context.read<UpdateBloc>().add(BeginUpdateProcess());
},
child: const Text('Install firmware'),
);
}
if (state is UpdateFirmwareStateHistory) {
final current = state.currentState;
if (current is UpdateProgressFirmware) {
return Text('Uploading ${current.progress}%');
}
if (state.isComplete) {
final last = state.history.isNotEmpty ? state.history.last : null;
if (last is UpdateCompleteFailure) {
return Text('Update failed: ${last.error}');
}
return const Text('Update complete');
}
return Text(current?.stage ?? 'Working...');
}
return const SizedBox.shrink();
},
),
);The library also exposes FirmwareUpdateRequestProvider. It is a convenience helper used by the example app to collect:
- the selected firmware
- the selected wearable
- the current step in a stepper-style UI
You can use it if it matches your UI, but it is not required. Many apps will prefer their own state management and only use:
- the repository classes
- the request models
UpdateBloc
Your UI should be prepared for these cases:
- no firmware selected
- no device selected
- network failure while loading remote firmware
- network failure while downloading a remote firmware file
- invalid or unsupported archive contents
- upload failure reported by the device or transport layer
Recommended UX:
- Disable the update button until both firmware and device are selected
- Show a clear loading state while releases are being fetched
- Show the current stage text from
UpdateFirmwareStateHistory.currentState - Show the failure message from
UpdateCompleteFailure.error - Allow the user to retry after a failure
- Multi-image
.zipupdates are expected to contain a validmanifest.json - If a manifest contains multiple images, each file entry must define an image index
UnifiedFirmwareRepositorycaches results for 15 minutes unless you request a refresh- The current upload path uses
mcumgr_flutterunder the hood FotaSlotInfoCapabilityis optional and only available on wearables whose firmware backend exposes slot-style state- The current local
mcumgr_flutterintegration exposes slot erase throughFotaSlotInfoCapability.eraseFirmwareSlot
If you want to inspect the implementation behind the public APIs, these are the main files:
lib/src/fota/model/firmware_update_request.dartlib/src/fota/repository/firmware_image_repository.dartlib/src/fota/repository/unified_firmware_image_repository.dartlib/src/fota/bloc/update_bloc.dartlib/src/fota/providers/firmware_update_request_provider.dartlib/src/models/capabilities/fota_capability.dartlib/src/models/capabilities/fota_slot_info_capability.dart