feat: switch firmware updates to bootloader OTA
This commit is contained in:
@ -2,8 +2,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:abawo_bt_app/model/shifter_types.dart';
|
||||
|
||||
const int _startPayloadLength = 11;
|
||||
const int _bootloaderStartPayloadLength = 19;
|
||||
const int _startPayloadLength = 19;
|
||||
|
||||
class BootloaderDfuStartPayload {
|
||||
const BootloaderDfuStartPayload({
|
||||
@ -37,143 +36,14 @@ class BootloaderDfuDataFrame {
|
||||
final Uint8List bytes;
|
||||
}
|
||||
|
||||
class DfuStartPayload {
|
||||
const DfuStartPayload({
|
||||
required this.totalLength,
|
||||
required this.imageCrc32,
|
||||
required this.sessionId,
|
||||
required this.flags,
|
||||
});
|
||||
|
||||
final int totalLength;
|
||||
final int imageCrc32;
|
||||
final int sessionId;
|
||||
final int flags;
|
||||
}
|
||||
|
||||
class DfuDataFrame {
|
||||
const DfuDataFrame({
|
||||
required this.sequence,
|
||||
required this.offset,
|
||||
required this.payloadLength,
|
||||
required this.bytes,
|
||||
});
|
||||
|
||||
final int sequence;
|
||||
final int offset;
|
||||
final int payloadLength;
|
||||
final Uint8List bytes;
|
||||
}
|
||||
|
||||
class DfuProtocol {
|
||||
const DfuProtocol._();
|
||||
|
||||
static Uint8List encodeStartPayload(DfuStartPayload payload) {
|
||||
final data = ByteData(_startPayloadLength);
|
||||
data.setUint8(0, universalShifterDfuOpcodeStart);
|
||||
data.setUint32(1, payload.totalLength, Endian.little);
|
||||
data.setUint32(5, payload.imageCrc32, Endian.little);
|
||||
data.setUint8(9, payload.sessionId);
|
||||
data.setUint8(10, payload.flags);
|
||||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
static Uint8List encodeFinishPayload() {
|
||||
return Uint8List.fromList([universalShifterDfuOpcodeFinish]);
|
||||
}
|
||||
|
||||
static Uint8List encodeAbortPayload() {
|
||||
return Uint8List.fromList([universalShifterDfuOpcodeAbort]);
|
||||
}
|
||||
|
||||
static List<DfuDataFrame> buildDataFrames(
|
||||
List<int> imageBytes, {
|
||||
int startSequence = 0,
|
||||
}) {
|
||||
final frames = <DfuDataFrame>[];
|
||||
var seq = _asU8(startSequence);
|
||||
var offset = 0;
|
||||
while (offset < imageBytes.length) {
|
||||
final remaining = imageBytes.length - offset;
|
||||
final chunkLength = remaining < universalShifterDfuFramePayloadSizeBytes
|
||||
? remaining
|
||||
: universalShifterDfuFramePayloadSizeBytes;
|
||||
|
||||
final frame = Uint8List(universalShifterDfuFrameSizeBytes);
|
||||
frame[0] = seq;
|
||||
frame.setRange(1, 1 + chunkLength, imageBytes, offset);
|
||||
|
||||
frames.add(
|
||||
DfuDataFrame(
|
||||
sequence: seq,
|
||||
offset: offset,
|
||||
payloadLength: chunkLength,
|
||||
bytes: frame,
|
||||
),
|
||||
);
|
||||
|
||||
offset += chunkLength;
|
||||
seq = nextSequence(seq);
|
||||
}
|
||||
|
||||
return frames;
|
||||
}
|
||||
|
||||
static int nextSequence(int sequence) {
|
||||
return _asU8(sequence + 1);
|
||||
}
|
||||
|
||||
static int rewindSequenceFromAck(int acknowledgedSequence) {
|
||||
return nextSequence(acknowledgedSequence);
|
||||
}
|
||||
|
||||
static int sequenceDistance(int from, int to) {
|
||||
return _asU8(to - from);
|
||||
}
|
||||
|
||||
static int parseAckPayload(List<int> payload) {
|
||||
if (payload.length != 1) {
|
||||
throw const FormatException('ACK payload must be exactly 1 byte.');
|
||||
}
|
||||
return _asU8(payload.first);
|
||||
}
|
||||
class BootloaderDfuProtocol {
|
||||
const BootloaderDfuProtocol._();
|
||||
|
||||
static const int crc32Initial = 0xFFFFFFFF;
|
||||
static const int _crc32PolynomialReflected = 0xEDB88320;
|
||||
|
||||
static int crc32Update(int crc, List<int> bytes) {
|
||||
var next = crc & 0xFFFFFFFF;
|
||||
for (final byte in bytes) {
|
||||
next ^= byte;
|
||||
for (var bit = 0; bit < 8; bit++) {
|
||||
if ((next & 0x1) != 0) {
|
||||
next = (next >> 1) ^ _crc32PolynomialReflected;
|
||||
} else {
|
||||
next >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32Finalize(int crc) {
|
||||
return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32(List<int> bytes) {
|
||||
return crc32Finalize(crc32Update(crc32Initial, bytes));
|
||||
}
|
||||
|
||||
static int _asU8(int value) {
|
||||
return value & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
class BootloaderDfuProtocol {
|
||||
const BootloaderDfuProtocol._();
|
||||
|
||||
static Uint8List encodeStartPayload(BootloaderDfuStartPayload payload) {
|
||||
final data = ByteData(_bootloaderStartPayloadLength);
|
||||
final data = ByteData(_startPayloadLength);
|
||||
data.setUint8(0, universalShifterDfuOpcodeStart);
|
||||
data.setUint32(1, payload.totalLength, Endian.little);
|
||||
data.setUint32(5, payload.imageCrc32, Endian.little);
|
||||
@ -296,17 +166,26 @@ class BootloaderDfuProtocol {
|
||||
);
|
||||
}
|
||||
|
||||
static const int crc32Initial = DfuProtocol.crc32Initial;
|
||||
|
||||
static int crc32Update(int crc, List<int> bytes) {
|
||||
return DfuProtocol.crc32Update(crc, bytes);
|
||||
var next = crc & 0xFFFFFFFF;
|
||||
for (final byte in bytes) {
|
||||
next ^= byte;
|
||||
for (var bit = 0; bit < 8; bit++) {
|
||||
if ((next & 0x1) != 0) {
|
||||
next = (next >> 1) ^ _crc32PolynomialReflected;
|
||||
} else {
|
||||
next >>= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32Finalize(int crc) {
|
||||
return DfuProtocol.crc32Finalize(crc);
|
||||
return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
static int crc32(List<int> bytes) {
|
||||
return DfuProtocol.crc32(bytes);
|
||||
return crc32Finalize(crc32Update(crc32Initial, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,29 +10,15 @@ final _log = Logger('ShifterService');
|
||||
|
||||
class ShifterService {
|
||||
ShifterService({
|
||||
BluetoothController? bluetooth,
|
||||
required BluetoothController bluetooth,
|
||||
required this.buttonDeviceId,
|
||||
DfuPreflightBluetoothAdapter? dfuPreflightBluetooth,
|
||||
}) : _bluetooth = bluetooth,
|
||||
_dfuPreflightBluetooth =
|
||||
dfuPreflightBluetooth ?? _BluetoothDfuPreflightAdapter(bluetooth!) {
|
||||
if (bluetooth == null && dfuPreflightBluetooth == null) {
|
||||
throw ArgumentError(
|
||||
'Either bluetooth or dfuPreflightBluetooth must be provided.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}) : _bluetooth = bluetooth;
|
||||
|
||||
final BluetoothController? _bluetooth;
|
||||
final BluetoothController _bluetooth;
|
||||
final String buttonDeviceId;
|
||||
final DfuPreflightBluetoothAdapter _dfuPreflightBluetooth;
|
||||
|
||||
BluetoothController get _requireBluetooth {
|
||||
final bluetooth = _bluetooth;
|
||||
if (bluetooth == null) {
|
||||
throw StateError('Bluetooth controller is not available.');
|
||||
}
|
||||
return bluetooth;
|
||||
return _bluetooth;
|
||||
}
|
||||
|
||||
final StreamController<CentralStatus> _statusController =
|
||||
@ -243,72 +229,6 @@ class ShifterService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Result<DfuPreflightResult>> runDfuPreflight({
|
||||
int requestedMtu = universalShifterDfuPreferredMtu,
|
||||
}) async {
|
||||
final currentConnection = _dfuPreflightBluetooth.currentConnectionState;
|
||||
final connectionStatus = currentConnection.$1;
|
||||
final connectedDeviceId = currentConnection.$2;
|
||||
|
||||
if (connectionStatus != ConnectionStatus.connected ||
|
||||
connectedDeviceId == null) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.deviceNotConnected,
|
||||
message:
|
||||
'No button connection is active. Connect the target button, then retry the firmware update.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedDeviceId != buttonDeviceId) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.wrongConnectedDevice,
|
||||
message:
|
||||
'Connected to a different button ($connectedDeviceId). Reconnect to $buttonDeviceId before starting the firmware update.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final mtuResult = await _dfuPreflightBluetooth.requestMtuAndGetValue(
|
||||
buttonDeviceId,
|
||||
mtu: requestedMtu,
|
||||
);
|
||||
if (mtuResult.isErr()) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
failureReason: DfuPreflightFailureReason.mtuRequestFailed,
|
||||
message:
|
||||
'Could not negotiate BLE MTU for DFU. Move closer to the button and retry. Details: ${mtuResult.unwrapErr()}',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final negotiatedMtu = mtuResult.unwrap();
|
||||
if (negotiatedMtu < universalShifterDfuMinimumMtu) {
|
||||
return Ok(
|
||||
DfuPreflightResult.failed(
|
||||
requestedMtu: requestedMtu,
|
||||
negotiatedMtu: negotiatedMtu,
|
||||
failureReason: DfuPreflightFailureReason.mtuTooLow,
|
||||
message:
|
||||
'Negotiated MTU $negotiatedMtu is too small for 64-byte DFU frames. Need at least $universalShifterDfuMinimumMtu bytes MTU (64-byte frame + $universalShifterAttWriteOverheadBytes-byte ATT overhead). Reconnect and retry.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(
|
||||
DfuPreflightResult.ready(
|
||||
requestedMtu: requestedMtu,
|
||||
negotiatedMtu: negotiatedMtu,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void startStatusNotifications() {
|
||||
if (_statusSubscription != null) {
|
||||
return;
|
||||
@ -369,32 +289,6 @@ class ShifterService {
|
||||
}
|
||||
}
|
||||
|
||||
abstract interface class DfuPreflightBluetoothAdapter {
|
||||
(ConnectionStatus, String?) get currentConnectionState;
|
||||
Future<Result<int>> requestMtuAndGetValue(
|
||||
String deviceId, {
|
||||
required int mtu,
|
||||
});
|
||||
}
|
||||
|
||||
class _BluetoothDfuPreflightAdapter implements DfuPreflightBluetoothAdapter {
|
||||
const _BluetoothDfuPreflightAdapter(this._bluetooth);
|
||||
|
||||
final BluetoothController _bluetooth;
|
||||
|
||||
@override
|
||||
(ConnectionStatus, String?) get currentConnectionState =>
|
||||
_bluetooth.currentConnectionState;
|
||||
|
||||
@override
|
||||
Future<Result<int>> requestMtuAndGetValue(
|
||||
String deviceId, {
|
||||
required int mtu,
|
||||
}) {
|
||||
return _bluetooth.requestMtuAndGetValue(deviceId, mtu: mtu);
|
||||
}
|
||||
}
|
||||
|
||||
class GearRatiosData {
|
||||
const GearRatiosData({
|
||||
required this.ratios,
|
||||
|
||||
Reference in New Issue
Block a user