Skip to content

Commit

Permalink
Add HUAWEI FreeBuds SE 2
Browse files Browse the repository at this point in the history
  • Loading branch information
2Peti committed Aug 24, 2024
1 parent 084f16f commit 9511d47
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 16 deletions.
10 changes: 9 additions & 1 deletion lib/headphones/cubit/model_matching.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import 'package:the_last_bluetooth/the_last_bluetooth.dart';
import '../framework/bluetooth_headphones.dart';
import '../huawei/freebuds3i.dart';
import '../huawei/freebuds3i_impl.dart';
import '../huawei/freebuds3i_sim.dart';
import '../huawei/freebuds4i.dart';
import '../huawei/freebuds4i_impl.dart';
import '../huawei/freebuds4i_sim.dart';
import '../huawei/freebudsse2.dart';
import '../huawei/freebudsse2_impl.dart';
import '../huawei/freebudsse2_sim.dart';
import '../huawei/mbb.dart';

typedef HeadphonesBuilder = BluetoothHeadphones Function(
Expand All @@ -28,7 +32,11 @@ MatchedModel? matchModel(BluetoothDevice matchedDevice) {
) as MatchedModel,
_ when HuaweiFreeBuds3i.idNameRegex.hasMatch(name) => (
builder: (io, dev) => HuaweiFreeBuds3iImpl(mbbChannel(io), dev),
placeholder: const HuaweiFreeBuds4iSimPlaceholder(),
placeholder: const HuaweiFreeBuds3iSimPlaceholder(),
) as MatchedModel,
_ when HuaweiFreeBudsSE2.idNameRegex.hasMatch(name) => (
builder: (io, dev) => HuaweiFreeBudsSE2Impl(mbbChannel(io), dev),
placeholder: const HuaweiFreeBudsSE2SimPlaceholder(),
) as MatchedModel,
_ => null,
};
Expand Down
37 changes: 37 additions & 0 deletions lib/headphones/huawei/freebudsse2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:rxdart/rxdart.dart';
import '../framework/bluetooth_headphones.dart';
import '../framework/headphones_info.dart';
import '../framework/headphones_settings.dart';
import '../framework/lrc_battery.dart';
import 'settings.dart';

/// Base abstract class of 4i's. It contains static info like vendor names etc,
/// but no logic whatsoever.
///
/// It makes both a solid ground for actual implementation (by defining what
/// features they implement), and some basic info for easy simulation
abstract base class HuaweiFreeBudsSE2
implements
BluetoothHeadphones,
HeadphonesModelInfo,
LRCBattery,
HeadphonesSettings<HuaweiFreeBudsSE2Settings> {
const HuaweiFreeBudsSE2();

@override
String get vendor => "Huawei";

@override
String get name => "FreeBuds SE 2";

// NOTE/WARNING: Again as in HeadphonesModelInfo - i'm not sure if it's safe
// to just leave it like that, but I will 🥰🥰
@override
ValueStream<String> get imageAssetPath =>
BehaviorSubject.seeded('assets/app_icons/ic_launcher.png');

// As I said everywhere else - i have no good idea where to put this stuff :/
// This will be a bit of chaos for now 👍👍
static final idNameRegex =
RegExp(r'^(?=(HUAWEI FreeBuds SE 2))', caseSensitive: true);
}
174 changes: 174 additions & 0 deletions lib/headphones/huawei/freebudsse2_impl.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:the_last_bluetooth/the_last_bluetooth.dart' as tlb;

import '../../logger.dart';
import '../framework/lrc_battery.dart';
import 'freebudsse2.dart';
import 'mbb.dart';
import 'settings.dart';

final class HuaweiFreeBudsSE2Impl extends HuaweiFreeBudsSE2 {
final tlb.BluetoothDevice _bluetoothDevice;

/// Bluetooth serial port that we communicate over
final StreamChannel<MbbCommand> _mbb;

// * stream controllers
final _batteryLevelCtrl = BehaviorSubject<int>();
final _bluetoothAliasCtrl = BehaviorSubject<String>();
final _bluetoothNameCtrl = BehaviorSubject<String>();
final _lrcBatteryCtrl = BehaviorSubject<LRCBatteryLevels>();
final _settingsCtrl = BehaviorSubject<HuaweiFreeBudsSE2Settings>();

// stream controllers *

/// This watches if we are still missing any info and re-requests it
late StreamSubscription _watchdogStreamSub;

HuaweiFreeBudsSE2Impl(this._mbb, this._bluetoothDevice) {
// hope this will nicely play with closing, idk honestly
final aliasStreamSub = _bluetoothDevice.alias
.listen((alias) => _bluetoothAliasCtrl.add(alias));
_bluetoothAliasCtrl.onCancel = () => aliasStreamSub.cancel();

_mbb.stream.listen(
(e) {
try {
_evalMbbCommand(e);
} catch (e, s) {
logg.e(e, stackTrace: s);
}
},
onError: logg.onError,
onDone: () {
_watchdogStreamSub.cancel();

// close all streams
_batteryLevelCtrl.close();
_bluetoothAliasCtrl.close();
_bluetoothNameCtrl.close();
_lrcBatteryCtrl.close();
_settingsCtrl.close();
},
);
_initRequestInfo();
_watchdogStreamSub =
Stream.periodic(const Duration(seconds: 3)).listen((_) {
if ([
batteryLevel.valueOrNull,
// no alias because it's okay to be null 👍
lrcBattery.valueOrNull,
settings.valueOrNull,
].any((e) => e == null)) {
_initRequestInfo();
}
});
}

void _evalMbbCommand(MbbCommand cmd) {
final lastSettings =
_settingsCtrl.valueOrNull ?? const HuaweiFreeBudsSE2Settings();
switch (cmd.args) {
// # BatteryLevels
case {2: var level, 3: var status}
when cmd.serviceId == 1 &&
(cmd.commandId == 39 || cmd.commandId == 8):
_lrcBatteryCtrl.add(LRCBatteryLevels(
level[0] == 0 ? null : level[0],
level[1] == 0 ? null : level[1],
level[2] == 0 ? null : level[2],
status[0] == 1,
status[1] == 1,
status[2] == 1,
));
break;
// # Settings(gestureDoubleTap)
case {1: [var leftCode, ...], 2: [var rightCode, ...]}
when cmd.isAbout(_Cmd.getGestureDoubleTap):
_settingsCtrl.add(
lastSettings.copyWith(
doubleTapLeft:
DoubleTap.values.firstWhereOrNull((e) => e.mbbCode == leftCode),
doubleTapRight: DoubleTap.values
.firstWhereOrNull((e) => e.mbbCode == rightCode),
),
);
break;
}
}

Future<void> _initRequestInfo() async {
_mbb.sink.add(_Cmd.getBattery);
_mbb.sink.add(_Cmd.getGestureDoubleTap);
}

@override
ValueStream<int> get batteryLevel => _bluetoothDevice.battery;

// i could pass btDevice.alias directly here, but Headphones take care
// of closing everything
@override
ValueStream<String> get bluetoothAlias => _bluetoothAliasCtrl.stream;

// huh, my past self thought that names will not change... and my future
// (implementing TLB) thought otherwise 🤷🤷
@override
String get bluetoothName => _bluetoothDevice.name.valueOrNull ?? "Unknown";

@override
String get macAddress => _bluetoothDevice.mac;

@override
ValueStream<LRCBatteryLevels> get lrcBattery => _lrcBatteryCtrl.stream;

@override
ValueStream<HuaweiFreeBudsSE2Settings> get settings => _settingsCtrl.stream;

@override
Future<void> setSettings(newSettings) async {
final prev = _settingsCtrl.valueOrNull ?? const HuaweiFreeBudsSE2Settings();
// this is VERY much a boilerplate
// ...and, bloat...
// and i don't think there is a need to export it somewhere else 🤷,
// or make some other abstraction for it - maybe some day
if ((newSettings.doubleTapLeft ?? prev.doubleTapLeft) !=
prev.doubleTapLeft) {
_mbb.sink.add(_Cmd.gestureDoubleTap(left: newSettings.doubleTapLeft!));
_mbb.sink.add(_Cmd.getGestureDoubleTap);
}
if ((newSettings.doubleTapRight ?? prev.doubleTapRight) !=
prev.doubleTapRight) {
_mbb.sink.add(_Cmd.gestureDoubleTap(right: newSettings.doubleTapRight!));
_mbb.sink.add(_Cmd.getGestureDoubleTap);
}
}
}

/// This is just a holder for magic numbers
/// This isn't very pretty, or eliminates all of the boilerplate... but i
/// feel like nothing will so let's love it as it is <3
abstract class _Cmd {
static const getBattery = MbbCommand(1, 8);

static const getGestureDoubleTap = MbbCommand(1, 32);

static MbbCommand gestureDoubleTap({DoubleTap? left, DoubleTap? right}) =>
MbbCommand(1, 31, {
if (left != null) 1: [left.mbbCode],
if (right != null) 2: [right.mbbCode],
});
}

extension _FBSE2DoubleTap on DoubleTap {
int get mbbCode => switch (this) {
DoubleTap.nothing => 255,
DoubleTap.voiceAssistant => 0,
DoubleTap.playPause => 1,
DoubleTap.next => 2,
DoubleTap.previous => 7
};
}
48 changes: 48 additions & 0 deletions lib/headphones/huawei/freebudsse2_sim.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:rxdart/rxdart.dart';

import '../simulators/bluetooth_headphones_sim.dart';
import '../simulators/lrc_battery_sim.dart';
import 'freebudsse2.dart';
import 'settings.dart';

final class HuaweiFreeBudsSE2Sim extends HuaweiFreeBudsSE2
with BluetoothHeadphonesSim, LRCBatteryAlwaysFullSim {
// ehhhhhh...

final _settingsCtrl = BehaviorSubject<HuaweiFreeBudsSE2Settings>.seeded(
const HuaweiFreeBudsSE2Settings(
doubleTapLeft: DoubleTap.playPause,
doubleTapRight: DoubleTap.playPause,
),
);

@override
ValueStream<HuaweiFreeBudsSE2Settings> get settings => _settingsCtrl.stream;

@override
Future<void> setSettings(HuaweiFreeBudsSE2Settings newSettings) async {
_settingsCtrl.add(
_settingsCtrl.value.copyWith(
doubleTapLeft: newSettings.doubleTapLeft,
doubleTapRight: newSettings.doubleTapRight,
),
);
}
}

/// Class to use as placeholder for Disabled() widget
// this is not done with mixins because we may want to fill it with
// last-remembered values in future, and we will pretty much override
// all of this
//
// ...or not. I just don't know yet 🤷
final class HuaweiFreeBudsSE2SimPlaceholder extends HuaweiFreeBudsSE2
with BluetoothHeadphonesSimPlaceholder, LRCBatteryAlwaysFullSimPlaceholder {
const HuaweiFreeBudsSE2SimPlaceholder();

@override
ValueStream<HuaweiFreeBudsSE2Settings> get settings => BehaviorSubject();

@override
Future<void> setSettings(HuaweiFreeBudsSE2Settings newSettings) async {}
}
22 changes: 22 additions & 0 deletions lib/headphones/huawei/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ class HuaweiFreeBuds3iSettings {
);
}

class HuaweiFreeBudsSE2Settings {
// hey hey hay, not only settings are gonna be duplicate spaghetti shithole,
// but all the fields are gonna be nullable too!
final DoubleTap? doubleTapLeft;
final DoubleTap? doubleTapRight;

const HuaweiFreeBudsSE2Settings({
this.doubleTapLeft,
this.doubleTapRight,
});

// don't want to use codegen *yet*
HuaweiFreeBudsSE2Settings copyWith({
DoubleTap? doubleTapLeft,
DoubleTap? doubleTapRight,
}) =>
HuaweiFreeBudsSE2Settings(
doubleTapLeft: doubleTapLeft ?? this.doubleTapLeft,
doubleTapRight: doubleTapRight ?? this.doubleTapRight,
);
}

// i don't have idea how to public/privatise those and how to name them
// let's assume that any screen/logic that uses them at all is already
// model-specific so generic names are okay
Expand Down
2 changes: 1 addition & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"pageAboutOpenSourceLicensesBtn": "Open source licenses",
"pageIntroTitle": "Welcome to FreeBuddy 👋",
"pageIntroWhatIsThis": "FreeBuddy is open source app for your headphones 🎧",
"pageIntroSupported": "Currently supported are:\n - Huawei FreeBuds 4i\n - Huawei FreeBuds 3i",
"pageIntroSupported": "Currently supported are:\n - Huawei FreeBuds 4i\n - Huawei FreeBuds 3i\n - Huawei FreeBuds SE 2",
"pageIntroShortPrivacyPolicy": "This app doesn't collect any emails, identifiers, or any personal data 🎉 You can read more about it here: ",
"pageIntroAnyQuestions": "If you have any questions, feel free to contact me 💌 Look at \"Settings->About\" for my socials!",
"pageIntroQuit": "Okay 👍"
Expand Down
2 changes: 1 addition & 1 deletion lib/l10n/app_pl.arb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"pageAboutOpenSourceLicensesBtn": "Licencje open source",
"pageIntroTitle": "Witaj we FreeBuddy 👋",
"pageIntroWhatIsThis": "FreeBuddy to open-source aplikacja do twoich słuchaweczek 🎧",
"pageIntroSupported": "Obecnie wspierane są:\n - Huawei FreeBuds 4i\n - Huawei FreeBuds 3i",
"pageIntroSupported": "Obecnie wspierane są:\n - Huawei FreeBuds 4i\n - Huawei FreeBuds 3i\n - Huawei FreeBuds SE 2",
"pageIntroShortPrivacyPolicy": "Ta aplikacja nie zbiera żadnych maili, identyfikatorów, czy innych osobistych danych 🎉 Możesz o tym poczytać tutaj: ",
"pageIntroAnyQuestions": "Jeśli masz jakiekolwiek pytania, napisz do mnie śmiało 💌 Wejdź w \"Ustawienia->O aplikacji\" po moje socjale!",
"pageIntroQuit": "Oki doki 👍"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ List<Widget> widgetsForModel(HeadphonesSettings settings) {
HoldSection(settings),
const SizedBox(height: 64),
];
} else if (settings is HeadphonesSettings<HuaweiFreeBudsSE2Settings>) {
return [
DoubleTapSection(settings),
];
} else {
throw "You shouldn't be on this screen if you don't have settings!";
}
Expand Down
Loading

0 comments on commit 9511d47

Please sign in to comment.