Skip to content

Commit

Permalink
Merge pull request #38 from melianmiko/release/v0.14-2
Browse files Browse the repository at this point in the history
release/v0.14 2
  • Loading branch information
melianmiko authored Sep 28, 2024
2 parents 52d0192 + a96921b commit a03e5a4
Show file tree
Hide file tree
Showing 79 changed files with 3,902 additions and 990 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/on_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: 6.7.*
version: 6.7.2
- uses: actions/setup-python@v5
with:
python-version: '3.12'
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
/dist
/scripts/tools
/release.json
/accent.json

/.idea
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
- [HUAWEI FreeBuds 5i & other] Fix SQ preference switch;
- [Device compatibility] Add HUAWEI FreeLace Pro 2 compatibility;
- Custom equalizer preset configuration (should also work with Pro 3);
- [Linux] Flatpak installation option
- [Linux] Flatpak as installation option
- [i18n] Add Spanish translation, thanks to @Pedro-vk (GitHub)
- [i18n] Add partial Portuguese (Brazilian), thanks to @Lobo (Accent)

# Older releases
WIP
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
<p>
<img src="https://img.shields.io/github/v/release/melianmiko/openfreebuds" alt="Last release"/>
<img src="https://img.shields.io/aur/last-modified/openfreebuds" alt="Last AUR release"/>
<img src="https://badges.crowdin.net/openfreebuds/localized.svg" alt="Translation level"/>
<a href="https://translate.mmk.pw/projects/0a1e818d-92b0-4f43-a1be-570e2ec35c8a"><img alt="Translated" src="https://translate.mmk.pw/0a1e818d-92b0-4f43-a1be-570e2ec35c8a/percentage_reviewed_badge.svg" /></a>
<a href="https://github.com/melianmiko/OpenFreebuds/actions/workflows/on_push.yml">
<img src="https://github.com/melianmiko/OpenFreebuds/actions/workflows/on_push.yml/badge.svg" alt="Test build status"/>
</a>
</p>
<p>
<a href="https://mmk.pw/en/openfreebuds"><b>💿 Download binaries</b></a> | <a href="https://crowdin.com/project/openfreebuds">🌍 Suggest translation</a>
<a href="https://mmk.pw/en/openfreebuds"><b>💿 Download binaries</b></a> | <a href="https://mmk.pw/en/openfreebuds/help/"><b>❓ FAQ</b></a> | <a href="https://translate.mmk.pw/projects/0a1e818d-92b0-4f43-a1be-570e2ec35c8a">🌍 Suggest translation</a>
</p>
<p>
<img alt="Tray menu preview" src="docs/preview_0.png" />
</p>
</div>

![Tray menu preview](docs/preview_0.png)

This application allows to control HUAWEI FreeBuds earphone settings from PC. Check exact battery level, toggle noise cancellation, control built-in equalizer, change gestures, and all other in-device settings and features are now available without official mobile application.

Features
Expand Down Expand Up @@ -52,8 +53,17 @@ May also work with newer/older devices in same series. If you want to get better
Download & install
-----------------

Will be available after release. For now, you can grab test
binaries from [GitHub Actions](https://github.com/melianmiko/OpenFreebuds/actions/workflows/on_push.yml).
- **Windows**: [Download here](https://mmk.pw/en/openfreebuds/).
- **Debian/Ubuntu**:

```shell
curl -s https://deb.mmk.pw/setup | sudo bash -
sudo apt install openfreebuds
```
- **Arch Linux**: `openfreebuds` [available in AUR](https://aur.archlinux.org/packages/openfreebuds).
- **Flatpak**: _Coming soon_

Most recent `dev`-binaries can be found in [GitHub Actions](https://github.com/melianmiko/OpenFreebuds/actions/workflows/on_push.yml) build artifacts.

Build or start from sources
-------------
Expand All @@ -63,10 +73,12 @@ Requirements:
- Windows 10/11, or enough modern Linux;
- Qt 6.0+ development tools, at least Linguist's `lrelease`;
- [Python](https://www.python.org/downloads/) (3.11+), [Poetry](https://python-poetry.org/docs/#installation) (1.8+);
- (Windows, optiona) [NSIS](https://nsis.sourceforge.io/Download), [UPX](https://upx.github.io/);
- (Windows, optional) [NSIS](https://nsis.sourceforge.io/Download), [UPX](https://upx.github.io/);
- (Linux, optional) Build essentials and some libraries.

Setup poetry before continue:
Also, some dev-scripts may have their own requirements, like `python3-polib` for
`./scripts/sync_translations.sh`. Setup poetry env and dependencies before
continue:

```shell
poetry install
Expand All @@ -75,7 +87,12 @@ poetry install
### Just launch without installation

```shell
# Compile Qt Designer & Linguist sources
./scripts/make_qt_parts.sh

# Launch
poetry run python -m openfreebuds_qt -vcs

# use --help for options description
```

Expand All @@ -89,7 +106,7 @@ If everything above is installed & added to `PATH`, just run:

Output binaries will be located in `scripts\build_win32\dist`

### Debian, Ubuntu
### Debian/Ubuntu

Install all packaging dependencies automated way:
`apt install build-essentials && ./scripts/install_dpkg_dependencies.sh`.
Expand Down
11 changes: 11 additions & 0 deletions docs/accent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"apiUrl": "https://translate.mmk.pw",
"apiKey": "",
"files": [
{
"format": "gettext",
"source": "openfreebuds_qt/assets/i18n/en.po",
"target": "openfreebuds_qt/assets/i18n/%slug%.po"
}
]
}
1 change: 0 additions & 1 deletion openfreebuds/driver/generic/spp.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ async def start(self):
sock.connect((self.device_address, self._spp_service_port))
reader, writer = await asyncio.open_connection(sock=sock)
except (ConnectionResetError, ConnectionRefusedError, ConnectionAbortedError, OSError, ValueError):
log.exception("Driver startup failed")
raise FbStartupError("Driver startup failed")

self.__task_recv = asyncio.create_task(self._loop_recv(reader))
Expand Down
4 changes: 2 additions & 2 deletions openfreebuds/driver/huawei/driver/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async def _handle_raw_pkg(self, pkg):
pkg = HuaweiSppPackage.from_bytes(pkg)
log.debug(f"RX {pkg}")
except (AssertionError, OfbPackageChecksumError):
log.exception(f"Got non-parsable package {pkg.hex()}, ignoring")
log.info(f"Got non-parsable package {pkg.hex()}, ignoring")
return

if pkg.command_id in self.__pending_responses:
Expand Down Expand Up @@ -159,7 +159,7 @@ async def init(self):
async with asyncio.timeout(self.init_timeout):
await self.on_init()
break
except TimeoutError:
except (TimeoutError, ConnectionResetError):
self.init_attempt += 1
except Exception:
log.exception(f"Unknown error on {self.handler_id} init")
Expand Down
7 changes: 3 additions & 4 deletions openfreebuds/driver/huawei/handler/anc.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,10 @@ async def on_package(self, pkg: HuaweiSppPackage):
# If cancellation turned on and support levels, list them
new_props["level"] = self.cancel_level_options.get(data[0], data[0])
new_props["level_options"] = ",".join(self.cancel_level_options.values())

if data[1] == 2 and self.w_voice_boost:
elif data[1] == 2 and self.w_voice_boost:
# If awareness turned on and support voice boost
new_props["level"] = "voice_boost" if data[0] != 0 else "normal"
new_props["level_options"] = "normal,voice_boost"
new_props["level"] = self.awareness_level_options.get(data[0], data[0])
new_props["level_options"] = ",".join(self.awareness_level_options.values())

await self.driver.put_property("anc", None, new_props)

Expand Down
13 changes: 11 additions & 2 deletions openfreebuds/driver/huawei/handler/config_equalizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(

self.changes_saved: bool = True
self.custom_preset_values: dict[int, bytes] = {}
self.current_rollback_data: bytes = b""
self.options: Optional[dict[int, str]] = None
if w_presets:
self.options = {i: f"equalizer_preset_{name}" for i, name in w_presets.items()}
Expand Down Expand Up @@ -77,7 +78,7 @@ async def _toggle_save(self, save: bool):
data = b"".join([x.to_bytes(1, byteorder="big", signed=True) for x in json.loads(data)])
log.info(f"Will save persistent preset data={data}, mode_id={mode_id}")
else:
data = self.custom_preset_values[mode_id]
data = self.current_rollback_data
log.info(f"Will restore saved data={data}, mode_id={mode_id}")

pkg = HuaweiSppPackage.change_rq(
Expand All @@ -88,14 +89,18 @@ async def _toggle_save(self, save: bool):
await self.driver.put_property("sound", "equalizer_rows",
json.dumps(_eq_bytes_to_array(data)))
await self.driver.put_property("sound", "equalizer_saved", "true")
self.changes_saved = True

async def _change_current_mode(self, value: str):
mode_str = await self.driver.get_property("sound", "equalizer_preset", None)
mode_id = reverse_dict(self.options).get(mode_str)
if mode_id is None:
log.info(f"Skip unknown mode {mode_str}/{mode_id}")
return

data = b"".join([x.to_bytes(1, byteorder="big", signed=True) for x in json.loads(value)])
if self.changes_saved:
self.current_rollback_data = self.custom_preset_values[mode_id]
log.info(f"Will replace id={mode_id}, label={mode_str} with data={data}")

pkg = HuaweiSppPackage.change_rq(
Expand All @@ -105,6 +110,7 @@ async def _change_current_mode(self, value: str):
await self.driver.send_package(pkg)
await self.driver.put_property("sound", "equalizer_rows", value)
await self.driver.put_property("sound", "equalizer_saved", "false")
self.changes_saved = False

async def _delete_current_mode(self):
mode_str = await self.driver.get_property("sound", "equalizer_preset", None)
Expand Down Expand Up @@ -159,14 +165,17 @@ async def _set_current_mode(self, mode_str):
[(1, mode_id)]
)

self.changes_saved = True

await self.driver.send_package(pkg)
await self.on_init()

async def on_package(self, package: HuaweiSppPackage):
new_props = {
"equalizer_rows": None,
"equalizer_saved": json.dumps(self.changes_saved),
"equalizer_rows_count": self.w_custom_max_count,
"equalizer_rows_count": str(self.w_custom_max_count),
"equalizer_max_custom_modes": str(self.w_custom_max_count) if self.w_custom else "0"
}

available_modes = package.find_param(3)
Expand Down
1 change: 0 additions & 1 deletion openfreebuds/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ async def execute(self, shortcut, *args, no_catch: bool = False):

return await handler(*args)
except Exception as e:
log.exception(f"While triggering shortcut {shortcut}")
if no_catch:
raise e

Expand Down
2 changes: 1 addition & 1 deletion openfreebuds_backend/linux/linux_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@ def set_run_at_boot(val):
def _get_autostart_file_path():
autostart_dir = pathlib.Path.home() / ".config/autostart"
if not autostart_dir.exists():
autostart_dir.mkdir()
autostart_dir.mkdir(parents=True)
return str(autostart_dir / "openfreebuds.desktop")
50 changes: 50 additions & 0 deletions openfreebuds_qt/app/dialog/first_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import sys
import webbrowser

from PyQt6.QtCore import pyqtSlot
from PyQt6.QtWidgets import QDialog
from qasync import asyncSlot

import openfreebuds_backend
from openfreebuds_qt.config import OfbQtConfigParser
from openfreebuds_qt.constants import LINK_WEBSITE_HELP
from openfreebuds_qt.designer.first_run_dialog import Ui_OfbQtFirstRunDialog
from openfreebuds_qt.utils import get_img_colored, qt_error_handler
from openfreebuds_qt.constants import WIN32_BODY_STYLE


class OfbQtFirstRunDialog(Ui_OfbQtFirstRunDialog, QDialog):
def __init__(self, ctx):
super().__init__(ctx.main_window)

self.ctx = ctx
self.config = OfbQtConfigParser.get_instance()

self.setupUi(self)
if sys.platform == "win32":
self.setStyleSheet(WIN32_BODY_STYLE)

self.autostart_checkbox.setChecked(not self.config.is_containerized_app)
self.autostart_checkbox.setEnabled(not self.config.is_containerized_app)
self.linux_notice.setVisible(sys.platform == 'linux')

preview_fn = "ofb_linux_preview" if sys.platform == 'linux' else "ofb_win32_preview"
preview_image = get_img_colored(preview_fn,
color=self.palette().text().color().getRgb(),
base_dir="image")
self.preview_root.setPixmap(preview_image)

@asyncSlot()
async def on_confirm(self):
async with qt_error_handler("OfbQtFirstRunDialog_Confirm", self.ctx):
self.hide()

if self.autostart_checkbox.isChecked():
openfreebuds_backend.set_run_at_boot(True)

self.config.set("ui", "first_run_finished", True)
self.config.save()

@pyqtSlot()
def on_faq_click(self):
webbrowser.open(LINK_WEBSITE_HELP)
8 changes: 6 additions & 2 deletions openfreebuds_qt/app/dialog/manual_connect.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import sys
from PyQt6.QtWidgets import QWidget

from openfreebuds.driver import DEVICE_TO_DRIVER_MAP
from openfreebuds_qt.constants import WIN32_BODY_STYLE
from openfreebuds_qt.utils.async_dialog import OfbQtAsyncDialog
from openfreebuds_qt.designer.dialog_manual_connect import Ui_Dialog
from openfreebuds_qt.designer.dialog_manual_connect import Ui_OfbQtManualConnectDialog


class OfbQtManualConnectDialog(Ui_Dialog, OfbQtAsyncDialog):
class OfbQtManualConnectDialog(Ui_OfbQtManualConnectDialog, OfbQtAsyncDialog):
def __init__(self, parent: QWidget):
super().__init__(parent)
self.setupUi(self)
if sys.platform == "win32":
self.setStyleSheet(WIN32_BODY_STYLE)

self.names = list(DEVICE_TO_DRIVER_MAP.keys())
self.profile_picker.addItems(self.names)
Expand Down
8 changes: 6 additions & 2 deletions openfreebuds_qt/app/dialog/rpc_config.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import json
import sys
from contextlib import suppress

from PyQt6.QtWidgets import QWidget

from openfreebuds import STORAGE_PATH
from openfreebuds.utils.logger import create_logger
from openfreebuds_qt.designer.stupid_rpc_setup import Ui_Dialog
from openfreebuds_qt.constants import WIN32_BODY_STYLE
from openfreebuds_qt.designer.stupid_rpc_setup import Ui_OfbQtRpcConfig
from openfreebuds_qt.utils import OfbQtAsyncDialog

log = create_logger("OfbQtRpcConfig")


class OfbQtRpcConfig(Ui_Dialog, OfbQtAsyncDialog):
class OfbQtRpcConfig(Ui_OfbQtRpcConfig, OfbQtAsyncDialog):
def __init__(self, parent: QWidget):
super().__init__(parent)
self.setupUi(self)
if sys.platform == "win32":
self.setStyleSheet(WIN32_BODY_STYLE)

self.current_config = {}
with suppress(Exception):
Expand Down
16 changes: 7 additions & 9 deletions openfreebuds_qt/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,13 @@
OfbQtHotkeysModule, OfbQtGesturesModule, OfbQtDualConnectModule, OfbQtDeviceOtherSettingsModule, \
OfbQtDeviceInfoModule, OfbQtCommonModule, OfbQtChooseDeviceModule, OfbQtUiSettingsModule
from openfreebuds_qt.config import ConfigLock, OfbQtConfigParser
from openfreebuds_qt.constants import ASSETS_PATH, LINK_RPC_HELP, LINK_WEBSITE_HELP
from openfreebuds_qt.constants import ASSETS_PATH, LINK_RPC_HELP, LINK_WEBSITE_HELP, WIN32_BODY_STYLE
from openfreebuds_qt.designer.main_window import Ui_OfbMainWindowDesign
from openfreebuds_qt.generic import IOfbQtApplication, IOfbMainWindow
from openfreebuds_qt.utils import qt_error_handler, OfbCoreEvent, OfbQtReportTool, get_qt_icon_colored
from openfreebuds_qt.utils import qt_error_handler, OfbCoreEvent, OfbQtReportTool, get_img_colored

log = create_logger("OfbQtMainWindow")

WIN32_BODY_STYLE = "QPushButton, QComboBox { padding: 6px 12px; }"


class OfbQtMainWindow(Ui_OfbMainWindowDesign, IOfbMainWindow):
def __init__(self, ctx: IOfbQtApplication):
Expand All @@ -45,7 +43,7 @@ def __init__(self, ctx: IOfbQtApplication):

# Extras button
self.extra_options_button.setIcon(
get_qt_icon_colored("settings", self.palette().text().color().getRgb())
QIcon(get_img_colored("settings", self.palette().text().color().getRgb()))
)

self.extra_menu = QMenu()
Expand Down Expand Up @@ -81,7 +79,7 @@ def __init__(self, ctx: IOfbQtApplication):
self._attach_module(self.tr("Keyboard shortcuts"), OfbQtHotkeysModule(self.tabs.root, self.ctx))
if sys.platform == "linux":
self._attach_module(self.tr("Linux-related"), OfbQtLinuxExtrasModule(self.tabs.root, self.ctx))
self._attach_module(self.tr("About..."), OfbQtAboutModule(self.tabs.root, self.ctx))
self._attach_module(self.tr("About"), OfbQtAboutModule(self.tabs.root, self.ctx))

# Finish
self.default_tab = 2, 0
Expand All @@ -97,19 +95,19 @@ def _fill_extras_menu(self):
# noinspection PyUnresolvedReferences
help_rpc_action.triggered.connect(lambda: webbrowser.open(LINK_RPC_HELP))

bugreport_action = self.extra_menu.addAction(self.tr("Bugreport..."))
bugreport_action = self.extra_menu.addAction(self.tr("Bugreport"))
bugreport_action.setShortcut("F2")
# noinspection PyUnresolvedReferences
bugreport_action.triggered.connect(self.on_bugreport)

self.check_updates_action = self.extra_menu.addAction(self.tr("Check for updates..."))
self.check_updates_action = self.extra_menu.addAction(self.tr("Check for updates"))
# noinspection PyUnresolvedReferences
self.check_updates_action.triggered.connect(self.on_check_updates)

self.extra_menu.addSeparator()

if self.ofb.role == "standalone" and ConfigLock.owned:
rpc_config_action = self.extra_menu.addAction(self.tr("Remote access..."))
rpc_config_action = self.extra_menu.addAction(self.tr("Remote access"))
# noinspection PyUnresolvedReferences
rpc_config_action.triggered.connect(self.on_rpc_config)

Expand Down
Loading

0 comments on commit a03e5a4

Please sign in to comment.