diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index 80e0e7f6..c158020b 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -60,6 +60,7 @@ export 'src/models/capabilities/jingle_player.dart'; export 'src/models/capabilities/audio_player_controls.dart'; export 'src/models/capabilities/storage_path_audio_player.dart'; export 'src/models/capabilities/audio_mode_manager.dart'; +export 'src/models/capabilities/microphone_gain_manager.dart'; export 'src/models/capabilities/microphone_manager.dart'; export 'src/models/capabilities/power_saving_mode_manager.dart'; export 'src/models/capabilities/stereo_device.dart'; diff --git a/lib/src/fota/handlers/firmware_update_handler.dart b/lib/src/fota/handlers/firmware_update_handler.dart index 9e7ec1fa..6c705123 100644 --- a/lib/src/fota/handlers/firmware_update_handler.dart +++ b/lib/src/fota/handlers/firmware_update_handler.dart @@ -147,6 +147,17 @@ class FirmwareUnpacker extends FirmwareUpdateHandler { class FirmwareUpdater extends FirmwareUpdateHandler { final UpdateManagerFactory _updateManagerFactory = FirmwareUpdateManagerFactory(); + static const FirmwareUpgradeConfiguration _defaultUpgradeConfiguration = + FirmwareUpgradeConfiguration( + // Ask MCUboot to test the image and reboot into it. The firmware + // confirms itself very early during boot, so confirming again from the + // phone is unnecessary and can race with the swap flow. + estimatedSwapTime: Duration(seconds: 90), + // Preserve persisted device state such as Bluetooth bond information + // during ordinary firmware upgrades. + eraseAppSettings: false, + firmwareUpgradeMode: FirmwareUpgradeMode.testOnly, + ); @override Future handleFirmwareUpdate( @@ -168,18 +179,14 @@ class FirmwareUpdater extends FirmwareUpdateHandler { final fwImage = request.firmwareImage; await updateManager.updateWithImageData( imageData: fwImage!, - configuration: const FirmwareUpgradeConfiguration( - firmwareUpgradeMode: FirmwareUpgradeMode.testOnly, - ), + configuration: _defaultUpgradeConfiguration, ); return updateManager; } else { final multiImageRequest = request as MultiImageFirmwareUpdateRequest; updateManager.update( multiImageRequest.firmwareImages!, - configuration: const FirmwareUpgradeConfiguration( - firmwareUpgradeMode: FirmwareUpgradeMode.testOnly, - ), + configuration: _defaultUpgradeConfiguration, ); } diff --git a/lib/src/models/capabilities/microphone_gain_manager.dart b/lib/src/models/capabilities/microphone_gain_manager.dart new file mode 100644 index 00000000..ca29fe17 --- /dev/null +++ b/lib/src/models/capabilities/microphone_gain_manager.dart @@ -0,0 +1,70 @@ +abstract class MicrophoneGainManager { + Future getMicrophoneGain(); + + Future setMicrophoneGain(MicrophoneGain gain); +} + +class MicrophoneGain { + static const int maxGainRegister = 0x00; + static const int zeroDbRegister = 0x40; + static const int minGainRegister = 0xFE; + static const int muteRegister = 0xFF; + static const int defaultRegister = 0x20; + static const double stepDb = 0.375; + static const double maxGainDb = 24.0; + static const double minGainDb = -71.25; + + final int outerRegister; + final int innerRegister; + + const MicrophoneGain({ + required this.outerRegister, + required this.innerRegister, + }) : assert(outerRegister >= 0 && outerRegister <= muteRegister), + assert(innerRegister >= 0 && innerRegister <= muteRegister); + + const MicrophoneGain.stereo(int register) + : this(outerRegister: register, innerRegister: register); + + const MicrophoneGain.muted() + : this.stereo(muteRegister); + + bool get isMuted => + outerRegister == muteRegister && innerRegister == muteRegister; + + double? get outerDb => registerToDb(outerRegister); + + double? get innerDb => registerToDb(innerRegister); + + MicrophoneGain copyWith({ + int? outerRegister, + int? innerRegister, + }) { + return MicrophoneGain( + outerRegister: outerRegister ?? this.outerRegister, + innerRegister: innerRegister ?? this.innerRegister, + ); + } + + static double? registerToDb(int register) { + _validateRegister(register); + if (register == muteRegister) { + return null; + } + return maxGainDb - (register * stepDb); + } + + static int dbToRegister(double db) { + final clampedDb = db.clamp(minGainDb, maxGainDb).toDouble(); + return ((maxGainDb - clampedDb) / stepDb) + .round() + .clamp(maxGainRegister, minGainRegister) + .toInt(); + } + + static void _validateRegister(int register) { + if (register < 0 || register > muteRegister) { + throw RangeError.range(register, 0, muteRegister, 'register'); + } + } +} diff --git a/lib/src/models/capabilities/microphone_manager.dart b/lib/src/models/capabilities/microphone_manager.dart index 00cbf5e9..535ab3d6 100644 --- a/lib/src/models/capabilities/microphone_manager.dart +++ b/lib/src/models/capabilities/microphone_manager.dart @@ -5,7 +5,8 @@ abstract class MicrophoneManager { required this.availableMicrophones, }); - void setMicrophone(MC microphone); + Future setMicrophone(MC microphone); + Future getMicrophone(); } diff --git a/lib/src/models/devices/open_earable_v2.dart b/lib/src/models/devices/open_earable_v2.dart index deb7a063..bde6e57b 100644 --- a/lib/src/models/devices/open_earable_v2.dart +++ b/lib/src/models/devices/open_earable_v2.dart @@ -33,6 +33,8 @@ const String _micSelectCharacteristicUuid = "0x1410df97-5f68-4ebb-a7c7-5e0fb9ae7557"; const String _audioModeCharacteristicUuid = "0x1410df96-5f68-4ebb-a7c7-5e0fb9ae7557"; +const String _dmicGainCharacteristicUuid = + "1410df99-5f68-4ebb-a7c7-5e0fb9ae7557"; const String _buttonServiceUuid = "29c10bdc-4773-11ee-be56-0242ac120002"; const String _buttonCharacteristicUuid = "29c10f38-4773-11ee-be56-0242ac120002"; @@ -76,6 +78,7 @@ class OpenEarableV2 extends BluetoothWearable DeviceFirmwareVersion, DeviceHardwareVersion, MicrophoneManager, + MicrophoneGainManager, AudioModeManager, EdgeRecorderManager, ButtonManager, @@ -455,12 +458,12 @@ class OpenEarableV2 extends BluetoothWearable // MARK: MicrophoneManager @override - void setMicrophone(OpenEarableV2Mic microphone) { + Future setMicrophone(OpenEarableV2Mic microphone) { if (!availableMicrophones.contains(microphone)) { throw ArgumentError('Microphone not available: ${microphone.key}'); } - bleManager.write( + return bleManager.write( deviceId: deviceId, serviceId: _audioConfigServiceUuid, characteristicId: _micSelectCharacteristicUuid, @@ -486,6 +489,38 @@ class OpenEarableV2 extends BluetoothWearable return availableMicrophones.firstWhere((mic) => mic.id == microphoneId); } + // MARK: MicrophoneGainManager + + @override + Future getMicrophoneGain() async { + final gainBytes = await bleManager.read( + deviceId: deviceId, + serviceId: _audioConfigServiceUuid, + characteristicId: _dmicGainCharacteristicUuid, + ); + + if (gainBytes.length != 2) { + throw StateError( + 'Microphone gain characteristic expected 2 values, but got ${gainBytes.length}', + ); + } + + return MicrophoneGain( + outerRegister: gainBytes[0], + innerRegister: gainBytes[1], + ); + } + + @override + Future setMicrophoneGain(MicrophoneGain gain) { + return bleManager.write( + deviceId: deviceId, + serviceId: _audioConfigServiceUuid, + characteristicId: _dmicGainCharacteristicUuid, + byteData: [gain.outerRegister, gain.innerRegister], + ); + } + // MARK: AudioModeManager @override