From dd0809e877c898c552377be47e19460301363609 Mon Sep 17 00:00:00 2001 From: Justin <39440218+justin-eckenweber@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:02:14 +0200 Subject: [PATCH] Add English localization and MIT-licensed public release documentation --- .github/ISSUE_TEMPLATE/bug_report.md | 24 ++ .github/workflows/tests.yml | 31 +++ CHANGELOG.md | 17 ++ CONTRIBUTING.md | 61 +++++ LICENSE | 21 ++ MANIFEST.in | 6 + README.de.md | 203 +++++++++++++++++ README.md | 301 +++++++++++++++++-------- THIRD_PARTY_NOTICES.md | 21 ++ btd700/__init__.py | 2 +- btd700/__main__.py | 43 ++-- btd700/app.py | 94 ++++---- btd700/controller.py | 41 ++-- btd700/demo.py | 3 +- btd700/i18n.py | 157 +++++++++++++ btd700/integration.py | 3 +- btd700/protocol.py | 13 +- btd700/transport.py | 19 +- btd700/tray.py | 37 +-- btd700/worker.py | 5 +- docs/PROTOCOL.md | 221 +++++++++--------- docs/VALIDATION.md | 82 ++++--- docs/screenshots/auracast-settings.png | Bin 0 -> 77249 bytes docs/screenshots/main-window.png | Bin 0 -> 70756 bytes install.py | 35 ++- pyproject.toml | 19 +- tests/test_controller.py | 3 +- tests/test_i18n.py | 67 ++++++ tests/test_integration.py | 46 ++++ tools/capture_screenshots.py | 60 +++++ tools/check_gui.py | 3 +- 31 files changed, 1261 insertions(+), 377 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/workflows/tests.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.de.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 btd700/i18n.py create mode 100644 docs/screenshots/auracast-settings.png create mode 100644 docs/screenshots/main-window.png create mode 100644 tests/test_i18n.py create mode 100644 tests/test_integration.py create mode 100644 tools/capture_screenshots.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e820166 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug or compatibility report +about: Report a reproducible problem or a tested hardware combination +--- + +**System** +- Distribution and version: +- Desktop environment: +- App version or commit: +- BTD 700 firmware: +- Headphones and firmware: + +**What happened?** +Describe your steps, expected result and actual result. + +**Demo mode** +Does the issue also occur with `./run.sh --demo`? + +**Checks performed** +List which controls you tested and whether this was a real dongle or a simulation. + +**Logs or screenshots** +Remove passwords, serial numbers and personal broadcast names. Do not upload +vendor executables or firmware images. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ca03461 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + +permissions: + contents: read + +jobs: + unit: + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: ['3.10', '3.14'] + language: [en, de] + env: + BTD700_LANGUAGE: ${{ matrix.language }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: ${{ matrix.python-version }} + - name: Run tests without USB access + run: python -m unittest discover -s tests -v + - name: Check CLI startup + run: python -m btd700 --language en --demo status diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..34b1fe4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## 0.2.0 — 2026-09-05 + +- English and German app, tray, CLI help and error messages, selected by system + locale or `--language` / `BTD700_LANGUAGE`. +- English README with genuine app screenshots and a separate German guide. +- MIT license, AI-development disclosure and explicit no-warranty statement. +- Public installation, uninstall, contribution and compatibility documentation. +- Automated protocol/localization tests on GitHub; no hardware writes in CI. + +## 0.1.0 — 2026-09-05 + +- Initial GTK 4 / libadwaita control app and StatusNotifier tray menu. +- Audio modes, codec/transport selection, connection and Auracast settings. +- Independent HID control protocol implementation; no firmware updater. +- Real-device status reads and demo-backed UI verification on Bazzite/GNOME. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..17ddeca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing + +This is an experimental, AI-developed project. Independent review and real-world +compatibility reports are particularly useful. English and German contributions +are welcome. + +## Report a problem + +Open a [GitHub issue](https://github.com/justin-eckenweber/btd700linux/issues) with: + +- Distribution, desktop environment and application version/commit. +- BTD 700 firmware version and headphone model/firmware, if known. +- The steps you took, what you expected, and what actually happened. +- Whether the issue also occurs in `./run.sh --demo`. +- Relevant terminal output after removing private information. + +Do not post broadcast passwords, Bluetooth/device identifiers, original vendor +executables or firmware images. `devices` includes a serial number: redact it. +The regular `status` output excludes passwords, but its broadcast name can still +contain personal information. Never say a hardware write is tested if it was +only simulated. + +## Make a change + +1. Fork the repository and create a focused branch. +2. Keep firmware download, update and bootloader functionality out of scope. +3. Preserve the control-report allowlist and validate input before writing to USB. +4. Keep blocking USB operations off the GTK main loop. +5. Add a meaningful regression test for protocol or behavior changes. +6. Run the checks below and explain what was tested in your pull request. + +```bash +BTD700_LANGUAGE=en python3 -m unittest discover -s tests -v +BTD700_LANGUAGE=de python3 -m unittest discover -s tests -v + +# Optional, with GTK libraries and a working desktop tray: +BTD700_LANGUAGE=en python3 tools/check_gui.py +BTD700_LANGUAGE=de python3 tools/check_gui.py +``` + +The GUI test uses a simulated device and D-Bus menu events, not the real USB dongle. +Hardware tests must be deliberate: the optional `tools/check_hardware.py --run` +changes settings and can interrupt audio. Do not add it to unattended CI. + +## Translations and screenshots + +`btd700/i18n.py` contains the English/German catalog. German source strings are +retained from the first version. Use `tr()` for user-facing messages; preserve +format placeholders and keep numeric protocol values independent of translation. +The CLI accepts `--language en|de`; `BTD700_LANGUAGE` also controls test runs. + +Regenerate genuine English screenshots with `python3 tools/capture_screenshots.py`. +The helper uses demo data and writes to `docs/screenshots/`. Inspect the resulting +images before committing them. Do not substitute generated mockups for screenshots. + +## Licensing and AI disclosure + +Contributions are accepted under this project's MIT license. Do not submit code +or assets you are not permitted to share. Explain any AI-generated portions of +substantial contributions and review them before submission. The project's +AI-development and no-warranty notices remain part of its public documentation. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e4950cb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Justin Eckenweber and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..3081f93 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include README.md README.de.md LICENSE THIRD_PARTY_NOTICES.md CONTRIBUTING.md CHANGELOG.md +include run.sh install.py +recursive-include packaging *.svg *.rules +recursive-include docs *.md *.png +recursive-include tools *.py +recursive-include tests *.py diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..a1b3c4e --- /dev/null +++ b/README.de.md @@ -0,0 +1,203 @@ +# BTD 700 Control für Linux + +[English](README.md) · [Quellcode v0.2.0 herunterladen](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip) · [Fehler melden](https://github.com/justin-eckenweber/btd700linux/issues) + +Native GTK-4-App zur Steuerung des **Sennheiser BTD 700**, mit einem Menü im +Infobereich von GNOME/KDE. Eigenständige Implementierung des HID-Steuerprotokolls, +analysiert anhand der offiziellen Windows-Anwendung **Dongle Control 1.0.5**. + +**Keine Firmware-Updates, Firmware-Downloads oder Update-Schnittstellen.** Die App +benötigt weder Windows/Wine noch eine Netzwerkverbindung. Sennheiser-Programmcode, +Logos und Firmware werden nicht mitgeliefert. Dies ist ein unabhängiges Projekt. + +> **Mit KI entwickelt · Experimentell · Ohne Gewähr** +> +> Dieses Projekt entstand mit KI-Unterstützung durch OpenAI Codex, einschließlich +> Protokollanalyse, Implementierung, Dokumentation und Tests. Es gibt **keine +> Garantie, dass es mit deinem Dongle, deiner Firmware oder deinem Linux-System +> funktioniert**. Nutzung auf eigenes Risiko; die Software wird im vorhandenen +> Zustand ohne Gewährleistung unter der [MIT-Lizenz](LICENSE) bereitgestellt. +> Das Projekt ist unabhängig von Sennheiser und Sonova. + +## Bilder der App + +Echte Fensteraufnahmen unter Linux mit ausdrücklich simulierten Demo-Daten. +Die Bilder zeigen die englische Oberfläche; Deutsch ist ebenfalls enthalten. + +

+ Hauptfenster mit Modus, Codec und Verbindung + Auracast-Einstellungen und Infobereich-Optionen +

+ +## Herunterladen + +```bash +git clone https://github.com/justin-eckenweber/btd700linux.git +cd btd700linux +``` + +Alternativ die oben verlinkte ZIP-Datei entpacken und `bash run.sh` im Projektordner +ausführen. Die aktuelle Version wird als Quellcode verteilt; es gibt noch kein +AppImage, Flatpak oder eigenständiges Binärpaket. + +## Starten + +```bash +./run.sh +``` + +Das Fenster zeigt den echten Dongle-Status. Beim Schließen bleibt die App im +Infobereich aktiv. „Beenden“ im Symbolmenü oder das Beenden-Symbol im Fenster +beendet die App vollständig. Wenn der Desktop keinen Infobereich anbietet, +beendet das Schließen des Fensters die App. + +```bash +python3 install.py # Anwendungsmenü-Eintrag für diesen Projektordner +./run.sh --background # Nur im Infobereich starten +./run.sh --demo # Simulierter Dongle, keinerlei USB-Zugriff +``` + +Mit `python3 install.py --uninstall` entfernst du den Menüeintrag und Autostart. +Den Projektordner nach der Installation behalten. „Beim Anmelden starten“ in der +App aktiviert bei Bedarf den Autostart im Infobereich. Er ist standardmäßig aus. + +## Sprache + +Die App, das Infobereich-Menü, CLI-Hilfe und Fehlermeldungen verwenden auf deutschen +Systemen Deutsch und sonst Englisch. Bei Bedarf explizit festlegen: + +```bash +./run.sh --language de +./run.sh --language en +BTD700_LANGUAGE=de ./run.sh --background +``` + +Eine bereits laufende Instanz vorher über „Beenden“ schließen. Ein weiterer Start +öffnet sonst das bestehende Fenster in seiner bisherigen Sprache. + +## Steuerung + +- **Standard / Gaming / Auracast** direkt im Fenster oder Symbolmenü wählen. +- Aktiven **Codec, Auflösung, Abtastrate und Verbindungsstatus** ablesen. +- **Codecs** auswählen, die der Dongle für die aktuelle Verbindung meldet. + Die Auswahl steht im Standard-Modus zur Verfügung; Gaming setzt seinen Codec selbst. +- **Bluetooth Classic / LE Audio / Automatisch** auswählen, entsprechend den + vom angeschlossenen Kopfhörer gemeldeten Möglichkeiten. +- Bereits gekoppelte Kopfhörer **verbinden / trennen**. +- **Auracast:** Name, Passwortschutz, öffentliche Auffindbarkeit und Qualität. + Das Symbolmenü bietet Schalter und Untermenüs; für Name/Passwort öffnet es die Eingabe im Fenster. +- **Werkseinstellungen** nur nach ausdrücklicher Bestätigung im Dialog. Dabei + werden die Kopplungen gelöscht. Die App führt niemals automatisch einen Reset aus. + +Das Passwort wird verdeckt eingegeben und weder gespeichert noch protokolliert. +Ein leeres Passwortfeld behält das bisherige Passwort; „Passwortschutz“ ausschalten +sendet ohne Passwortschutz. Der Name erlaubt 4–16 ASCII-Buchstaben/Ziffern mit +inneren Leerzeichen; ein leerer Name stellt den Gerätenamen wieder her. Passwörter +verwenden 4–16 ASCII-Buchstaben/Ziffern, entsprechend den Original-Eingabegrenzen. + +**„Öffentlich auffindbar“ ist die Auracast-Ankündigung, kein Ein/Aus-Schalter für +Audio.** Zum Starten/Beenden der Übertragung den Audiomodus umstellen. +Die angezeigten Bit-/kHz-Werte stammen vom Dongle. Die USB-Abtastrate wird vom +Audiosystem (z. B. PipeWire) bestimmt und ist kein hier nachgewiesener Stellbefehl. +Koppeln erfolgt über die Taste am Dongle; „Verbinden“ versucht die bestehende Kopplung. + +## Voraussetzungen + +Python ≥ 3.10, PyGObject, GTK ≥ 4.10, libadwaita ≥ 1.5 und libdbusmenu mit +GObject-Introspection. Auf dem hier verwendeten Bazzite sind die benötigten +Bibliotheken bereits vorhanden; keine zusätzlichen Pakete waren erforderlich. + +Für andere Systeme typischerweise: + +```bash +# Fedora (klassisch, veränderliches System) +sudo dnf install python3-gobject gtk4 libadwaita libdbusmenu + +# Debian / Ubuntu +sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gir1.2-dbusmenu-glib-0.4 +``` + +Auf GNOME muss der Desktop StatusNotifier/AppIndicator-Symbole unterstützen; +beispielsweise mit „AppIndicator and KStatusNotifierItem Support“. Auf dem +getesteten GNOME ist derselbe Dienst bereits für JetBrains Toolbox und andere Apps aktiv. +KDE stellt diese Schnittstelle über seinen Infobereich bereit. + +Fehlt die Menü-Bibliothek, bleibt das Fenster nutzbar. Das Terminalinterface +benötigt ausschließlich die Python-Standardbibliothek. + +## USB-Zugriff + +Es wird ausschließlich die passende HID-Steuerschnittstelle des Geräts +`3542:3001` geöffnet, identifiziert über ihren Report-Deskriptor. Die zweite +Schnittstelle für Updates bleibt geschlossen. Audiotreiber werden nicht getrennt. + +Falls die App fehlende Berechtigungen meldet: + +```bash +sudo install -m 0644 packaging/70-btd700-control.rules /etc/udev/rules.d/70-btd700-control.rules +sudo udevadm control --reload-rules +``` + +Anschließend den Dongle aus- und wieder einstecken. Die Regel beschränkt den Zugriff +auf die Steuerungsschnittstelle und den aktiven lokalen Benutzer. Die App nicht +als root starten. Auf dem getesteten System ist bereits Zugriff vorhanden. + +## Terminal + +```bash +./run.sh devices +./run.sh status +./run.sh mode standard +./run.sh mode gaming +./run.sh mode standard --transport auto +./run.sh codec adaptive +./run.sh disconnect +./run.sh connect +./run.sh auracast --name 'Wohnzimmer' --quality high --public on +./run.sh auracast --password --encryption on # Passwort verdeckt abfragen +./run.sh mode auracast +``` + +`status` gibt JSON aus und liest kein Passwort. Nur eine Instanz darf den Dongle +halten: Vor CLI-Befehlen eine laufende GUI über „Beenden“ schließen. Mehrere Dongles +lassen sich mit `--device /dev/hidrawN` vor dem Unterbefehl auswählen. + +## Verifikation + +```bash +python3 -m unittest discover -s tests -v +python3 tools/check_gui.py +``` + +`check_gui.py` verwendet **ausschließlich einen simulierten Dongle**, öffnet eine +Demo-Oberfläche in der bestehenden Sitzung und prüft die Menüaktionen über den +echten D-Bus-Menüexport. Es speichert Fensterbilder unter `/tmp/btd700-gui-test*.png`. + +Nachgewiesen am angeschlossenen BTD 700 mit Firmware **3.11.0**: + +- Erkennung und Öffnen der korrekten HID-Schnittstelle ohne Treiberwechsel. +- Echte Antworten auf Status-, Modus-, Codec-, Audioqualitäts-, LE-Zustands-, + Transport-, Auracast-Konfigurations-, Namens- und Firmwareversionsabfragen. +- Anzeige im GTK-Fenster und Registrierung im GNOME-Infobereich. + +Schreibbefehle wurden aus der Originalsoftware rekonstruiert und in Simulation +geprüft. Der Nutzer hat die Funktion der App am eigenen Dongle bestätigt. Eine +Aufschlüsselung aller dabei getesteten Funktionen liegt nicht vor; der automatisierte +Hardware-Umschalttest wurde bisher nicht ausgeführt. + +Das vorbereitete `tools/check_hardware.py --run` schaltet Modi und Codec um, +prüft Auracast-Werte und versucht anschließend die gesicherten Originalwerte +wiederherzustellen. Es führt weder Firmware-Updates noch einen Werksreset aus. +Den Hardwaretest nur bewusst starten: Er unterbricht kurz die Audiowiedergabe +und verändert vorübergehend gespeicherte Einstellungen einschließlich des Passworts. Bei USB-Trennung während +einer Änderung ist eine Wiederherstellung nicht garantiert. + +Protokoll, Herkunft und bekannte Grenzen: [docs/PROTOCOL.md](docs/PROTOCOL.md). + + +## Lizenz und Mitarbeit + +Eigener Projektcode und Dokumentation stehen unter [MIT](LICENSE). Drittsoftware +und Herstellerkennzeichen behalten ihre eigenen Rechte; siehe +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Beiträge, Übersetzungen und +Kompatibilitätsberichte sind willkommen: [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/README.md b/README.md index 11312dd..64a97a0 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,186 @@ -# BTD 700 Control für Linux +

+ BTD 700 Control icon +

-Native GTK-4-App zur Steuerung des **Sennheiser BTD 700**, mit einem Menü im -Infobereich von GNOME/KDE. Eigenständige Implementierung des HID-Steuerprotokolls, -analysiert anhand der offiziellen Windows-Anwendung **Dongle Control 1.0.5**. +

BTD 700 Control for Linux

-**Keine Firmware-Updates, Firmware-Downloads oder Update-Schnittstellen.** Die App -benötigt weder Windows/Wine noch eine Netzwerkverbindung. Sennheiser-Programmcode, -Logos und Firmware werden nicht mitgeliefert. Dies ist ein unabhängiges Projekt. +

Your dongle. Your codecs. A native Linux app and system tray menu.

-## Starten +

+ MIT license + Linux + English and German interface + Tests +

+ +

+ Install · + Download source v0.2.0 · + Deutsche Anleitung · + Report an issue +

+ +An independent, open-source control app for the **Sennheiser BTD 700** USB Bluetooth +transmitter. Choose audio modes and codecs, manage the headphone connection, and +configure Auracast without Windows or Wine. Built with Python, GTK 4 and libadwaita. + +**Controls only. No firmware updates, firmware downloads or update mode.** Once +installed, the app works offline and does not send telemetry. + +> **AI-developed · Experimental · No warranty** +> +> This project was developed with AI (OpenAI Codex), including protocol research, +> implementation, documentation and tests. It is an independent community project, +> not official Sennheiser software. **There is no guarantee that it will work with +> your hardware, firmware or Linux setup.** It is provided as is, without warranty, +> under the [MIT license](LICENSE). Use it at your own risk. + +## Screenshots + +Actual application windows on Linux, using **fictional demo data**. No mockups or +AI-generated images. The app follows your desktop's light/dark appearance. + +

+ English app: active codec, audio mode and headphone connection + English app: Auracast name, password protection, broadcast quality and startup settings +

+ +## What you can control + +| Feature | Window | System tray | +|---|:---:|:---:| +| Standard, Gaming and Auracast modes | ✓ | ✓ | +| Active codec, audio format and connection status | ✓ | ✓ | +| Codec selection supported by the current connection | ✓ | ✓ | +| Bluetooth Classic, LE Audio or automatic transport | ✓ | ✓ | +| Connect / disconnect previously paired headphones | ✓ | ✓ | +| Auracast discovery, quality and password protection | ✓ | ✓ | +| Auracast name and password | ✓ | Opens the editor | +| Factory reset, with confirmation | ✓ | Opens confirmation | + +The tray menu stays available when you close the window. On GNOME, it uses the +same StatusNotifier/AppIndicator mechanism as apps such as Discord and JetBrains +Toolbox. Use **Quit** to close the app completely. Without a tray host, closing the +window quits the app instead. + +## Install + +Version **0.2.0** is distributed as source. You do not need to compile the app or +install Python packages with pip. There is currently no AppImage, Flatpak or +self-contained binary download. + +### 1. Install the system libraries + +Requirements: **Python 3.10+**, PyGObject, **GTK 4.10+**, **libadwaita 1.5+** and +libdbusmenu with GObject introspection. A graphical desktop session is needed for +the window and tray; the CLI only needs Python's standard library. + +**Ubuntu 24.04+ / Debian with sufficiently recent GTK and libadwaita:** ```bash +sudo apt install git python3 python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gir1.2-dbusmenu-glib-0.4 +``` + +**Fedora Workstation:** + +```bash +sudo dnf install git python3 python3-gobject gtk4 libadwaita libdbusmenu +``` + +**Bazzite / other immutable desktops:** try running the app first. The tested +Bazzite system already included every required library. The Fedora command above +is for a mutable Fedora installation, not an instruction to layer packages on Bazzite. + +On **GNOME**, enable a StatusNotifier/AppIndicator extension if you do not already +have a working tray, for example +[AppIndicator and KStatusNotifierItem Support](https://extensions.gnome.org/extension/615/appindicator-support/). +KDE Plasma provides a tray host. Other desktops may work but have not been tested. + +Package references: [Fedora libdbusmenu](https://packages.fedoraproject.org/pkgs/libdbusmenu/libdbusmenu/), +[Ubuntu introspection package](https://packages.ubuntu.com/noble/gir1.2-dbusmenu-glib-0.4). + +### 2. Download and run + +```bash +git clone https://github.com/justin-eckenweber/btd700linux.git +cd btd700linux ./run.sh ``` -Das Fenster zeigt den echten Dongle-Status. Beim Schließen bleibt die App im -Infobereich aktiv. „Beenden“ im Symbolmenü oder das Beenden-Symbol im Fenster -beendet die App vollständig. Wenn der Desktop keinen Infobereich anbietet, -beendet das Schließen des Fensters die App. +Or [download the v0.2.0 source ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip), +extract it, and run `bash run.sh` inside the extracted folder. + +Want to explore without touching any hardware? ```bash -python3 install.py # Anwendungsmenü-Eintrag für diesen Projektordner -./run.sh --background # Nur im Infobereich starten -./run.sh --demo # Simulierter Dongle, keinerlei USB-Zugriff +./run.sh --demo ``` -Den Projektordner nach der Installation behalten. „Beim Anmelden starten“ in der -App aktiviert bei Bedarf den Autostart im Infobereich. Er ist standardmäßig aus. - -## Steuerung - -- **Standard / Gaming / Auracast** direkt im Fenster oder Symbolmenü wählen. -- Aktiven **Codec, Auflösung, Abtastrate und Verbindungsstatus** ablesen. -- **Codecs** auswählen, die der Dongle für die aktuelle Verbindung meldet. - Die Auswahl steht im Standard-Modus zur Verfügung; Gaming setzt seinen Codec selbst. -- **Bluetooth Classic / LE Audio / Automatisch** auswählen, entsprechend den - vom angeschlossenen Kopfhörer gemeldeten Möglichkeiten. -- Bereits gekoppelte Kopfhörer **verbinden / trennen**. -- **Auracast:** Name, Passwortschutz, öffentliche Auffindbarkeit und Qualität. - Das Symbolmenü bietet Schalter und Untermenüs; für Name/Passwort öffnet es die Eingabe im Fenster. -- **Werkseinstellungen** nur nach ausdrücklicher Bestätigung im Dialog. Dabei - werden die Kopplungen gelöscht. Die App führt niemals automatisch einen Reset aus. - -Das Passwort wird verdeckt eingegeben und weder gespeichert noch protokolliert. -Ein leeres Passwortfeld behält das bisherige Passwort; „Passwortschutz“ ausschalten -sendet ohne Passwortschutz. Der Name erlaubt 4–16 ASCII-Buchstaben/Ziffern mit -inneren Leerzeichen; ein leerer Name stellt den Gerätenamen wieder her. Passwörter -verwenden 4–16 ASCII-Buchstaben/Ziffern, entsprechend den Original-Eingabegrenzen. - -**„Öffentlich auffindbar“ ist die Auracast-Ankündigung, kein Ein/Aus-Schalter für -Audio.** Zum Starten/Beenden der Übertragung den Audiomodus umstellen. -Die angezeigten Bit-/kHz-Werte stammen vom Dongle. Die USB-Abtastrate wird vom -Audiosystem (z. B. PipeWire) bestimmt und ist kein hier nachgewiesener Stellbefehl. -Koppeln erfolgt über die Taste am Dongle; „Verbinden“ versucht die bestehende Kopplung. - -## Voraussetzungen - -Python ≥ 3.10, PyGObject, GTK ≥ 4.10, libadwaita ≥ 1.5 und libdbusmenu mit -GObject-Introspection. Auf dem hier verwendeten Bazzite sind die benötigten -Bibliotheken bereits vorhanden; keine zusätzlichen Pakete waren erforderlich. - -Für andere Systeme typischerweise: +### 3. Add it to your application menu ```bash -# Fedora (klassisch, veränderliches System) -sudo dnf install python3-gobject gtk4 libadwaita libdbusmenu - -# Debian / Ubuntu -sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 gir1.2-dbusmenu-glib-0.4 +python3 install.py ``` -Auf GNOME muss der Desktop StatusNotifier/AppIndicator-Symbole unterstützen; -beispielsweise mit „AppIndicator and KStatusNotifierItem Support“. Auf dem -getesteten GNOME ist derselbe Dienst bereits für JetBrains Toolbox und andere Apps aktiv. -KDE stellt diese Schnittstelle über seinen Infobereich bereit. +Open **BTD 700 Control** from the application menu. Keep the downloaded project +folder in place: the launcher points to it. Enable **Start at login** inside the +app if you want it to start quietly in the tray. Autostart is off by default. -Fehlt die Menü-Bibliothek, bleibt das Fenster nutzbar. Das Terminalinterface -benötigt ausschließlich die Python-Standardbibliothek. +```bash +./run.sh --background # Start directly in the tray +python3 install.py --uninstall # Remove launcher and autostart entry +``` -## USB-Zugriff +### USB permissions -Es wird ausschließlich die passende HID-Steuerschnittstelle des Geräts -`3542:3001` geöffnet, identifiziert über ihren Report-Deskriptor. Die zweite -Schnittstelle für Updates bleibt geschlossen. Audiotreiber werden nicht getrennt. - -Falls die App fehlende Berechtigungen meldet: +If the app reports that USB access is denied: ```bash sudo install -m 0644 packaging/70-btd700-control.rules /etc/udev/rules.d/70-btd700-control.rules sudo udevadm control --reload-rules ``` -Anschließend den Dongle aus- und wieder einstecken. Die Regel beschränkt den Zugriff -auf die Steuerungsschnittstelle und den aktiven lokalen Benutzer. Die App nicht -als root starten. Auf dem getesteten System ist bereits Zugriff vorhanden. +Unplug and reconnect the dongle afterwards. The rule grants the active local user +access to the **control interface** of USB device `3542:3001`; it excludes the second +interface used for updates. Run the app as your normal user, not with `sudo`. +No audio driver is detached or replaced. -## Terminal +## English and German + +The app, tray, CLI help and application error messages follow the system language: +German for German locales, English otherwise. You can override this explicitly: + +```bash +./run.sh --language en +./run.sh --language de +BTD700_LANGUAGE=en ./run.sh --background +``` + +Quit an already running instance before changing the language; launching it again +normally brings the existing window to the front. + +## Using the controls + +- **Codecs:** the dongle reports the choices available for the current connection. + Select a codec in Standard mode; Gaming mode manages its own codec. A codec + supported in principle is not necessarily offered with every pair of headphones. +- **Audio format:** bit depth and sample rate are read from the dongle. Set the USB + output sample rate in your audio system, such as PipeWire; this app does not + force 96 kHz or claim bit-perfect or lossless transmission. +- **Pairing:** use the physical dongle button to pair headphones. **Connect** reuses + an existing pairing. +- **Auracast:** select Auracast mode to start broadcasting. **Publicly discoverable** + controls advertising/discovery, not whether audio is transmitted. +- **Name and password:** 4–16 ASCII letters/digits; names may contain internal + spaces. An empty name restores the device's default name. Leave the password + field blank to keep the current password; turn off Password protection to + broadcast without it. Passwords are not stored on disk or included in status output. +- **Factory reset:** deletes saved settings and pairings. It always requires + confirmation in the UI. + +## Command line + +Quit the tray app before using the CLI: one instance owns the dongle at a time. ```bash ./run.sh devices @@ -106,43 +191,59 @@ als root starten. Auf dem getesteten System ist bereits Zugriff vorhanden. ./run.sh codec adaptive ./run.sh disconnect ./run.sh connect -./run.sh auracast --name 'Wohnzimmer' --quality high --public on -./run.sh auracast --password --encryption on # Passwort verdeckt abfragen +./run.sh auracast --name 'Living Room' --quality high --public on +./run.sh auracast --password --encryption on # Hidden password prompt ./run.sh mode auracast ``` -`status` gibt JSON aus und liest kein Passwort. Nur eine Instanz darf den Dongle -halten: Vor CLI-Befehlen eine laufende GUI über „Beenden“ schließen. Mehrere Dongles -lassen sich mit `--device /dev/hidrawN` vor dem Unterbefehl auswählen. +`status` prints JSON without reading a password. With multiple dongles, place +`--device /dev/hidrawN` before the subcommand; use the path reported by `devices`. +The numeric fields in JSON retain their protocol meanings across languages. -## Verifikation +## Compatibility and limitations + +Developed and tested on **Bazzite / GNOME with a BTD 700 running firmware 3.11.0**. +The owner confirmed that the app works with their dongle. That is not a complete +compatibility matrix or a guarantee for other firmware and receivers. + +- Real hardware: device discovery, state/configuration reads, native window and + tray integration verified. +- Automated tests: protocol parsing, allowed commands, readback, input validation, + localization and demo-backed GTK/D-Bus menu interactions. +- Still needed: systematic hardware write tests, more receivers/firmware versions, + other desktops, and independent testing by more users. +- BTD 600 and other Sennheiser devices are **not supported** by this driver. +- Reconfiguring modes or broadcasts can interrupt audio. Multi-step settings are + not atomic; unplugging the dongle midway can leave partially applied changes. + +[Validation notes](docs/VALIDATION.md) · [Protocol and research sources](docs/PROTOCOL.md) + +## Contribute + +Bug reports, compatibility reports, translations and reviewed patches are welcome. +See [CONTRIBUTING.md](CONTRIBUTING.md) for what to include and how to run tests. +Please do not attach broadcast passwords, device serial numbers, original vendor +executables or firmware images. ```bash python3 -m unittest discover -s tests -v -python3 tools/check_gui.py +BTD700_LANGUAGE=en python3 tools/check_gui.py # Desktop session; simulated dongle +BTD700_LANGUAGE=de python3 tools/check_gui.py +python3 tools/capture_screenshots.py # Regenerate the README screenshots ``` -`check_gui.py` verwendet **ausschließlich einen simulierten Dongle**, öffnet eine -Demo-Oberfläche in der bestehenden Sitzung und prüft die Menüaktionen über den -echten D-Bus-Menüexport. Es speichert Fensterbilder unter `/tmp/btd700-gui-test*.png`. +The separate `tools/check_hardware.py --run` is an opt-in hardware test that changes +settings, including the broadcast password, and attempts to restore the originals. +It is never run by CI or the demo tests. Read it before deliberately using it. -Nachgewiesen am angeschlossenen BTD 700 mit Firmware **3.11.0**: +## License and independence -- Erkennung und Öffnen der korrekten HID-Schnittstelle ohne Treiberwechsel. -- Echte Antworten auf Status-, Modus-, Codec-, Audioqualitäts-, LE-Zustands-, - Transport-, Auracast-Konfigurations-, Namens- und Firmwareversionsabfragen. -- Anzeige im GTK-Fenster und Registrierung im GNOME-Infobereich. +The project's own code and documentation are licensed under **[MIT](LICENSE)**. +The AI-development and no-warranty notice above is intentional; please keep it +visible when describing this project. -Schreibbefehle wurden aus der Originalsoftware rekonstruiert und in Simulation -geprüft. Der Nutzer hat die Funktion der App am eigenen Dongle bestätigt. Eine -Aufschlüsselung aller dabei getesteten Funktionen liegt nicht vor; der automatisierte -Hardware-Umschalttest wurde bisher nicht ausgeführt. - -Das vorbereitete `tools/check_hardware.py --run` schaltet Modi und Codec um, -prüft Auracast-Werte und versucht anschließend die gesicherten Originalwerte -wiederherzustellen. Es führt weder Firmware-Updates noch einen Werksreset aus. -Den Hardwaretest nur bewusst starten: Er unterbricht kurz die Audiowiedergabe -und verändert vorübergehend gespeicherte Einstellungen einschließlich des Passworts. Bei USB-Trennung während -einer Änderung ist eine Wiederherstellung nicht garantiert. - -Protokoll, Herkunft und bekannte Grenzen: [docs/PROTOCOL.md](docs/PROTOCOL.md). +Sennheiser, BTD 700, aptX and Auracast names identify the compatible product and +technologies. This project is not affiliated with, endorsed by or supported by +Sennheiser, Sonova or Qualcomm. No vendor code, firmware or logos are included. +System libraries and original vendor software retain their own licenses; +see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..cf025b4 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,21 @@ +# Third-party notices + +The MIT license in this repository covers this project's own implementation, +documentation and artwork. It does not relicense third-party software or trademarks. + +- Python, PyGObject, GTK, libadwaita and libdbusmenu are provided by your system, + not bundled here. Their respective licenses continue to apply. +- The control protocol was reconstructed by examining the official Windows + Sennheiser Dongle Control 1.0.5 application. The vendor application, decompiled + sources, firmware and vendor graphics are not included. Provenance and references + are recorded in [docs/PROTOCOL.md](docs/PROTOCOL.md). +- The optional extraction helper reads the documented .NET bundle format. Its + references are linked in the protocol documentation. Extracted assemblies are + private analysis material, not part of this distribution. +- Sennheiser, Sonova, Qualcomm, BTD 700, aptX and Auracast names are used to describe + compatibility and functionality. Their use does not imply endorsement. +- The app icon is original project artwork. README screenshots capture this app + with demo data, using system-provided GTK/libadwaita styling and icons. + +The project was developed using AI (OpenAI Codex). The source and tests are available +for review; passing tests does not guarantee operation with any particular device. diff --git a/btd700/__init__.py b/btd700/__init__.py index c1f1ef6..6e56d1a 100644 --- a/btd700/__init__.py +++ b/btd700/__init__.py @@ -1,3 +1,3 @@ """Independent Linux controls for the Sennheiser BTD 700.""" -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/btd700/__main__.py b/btd700/__main__.py index d877b4d..8526509 100644 --- a/btd700/__main__.py +++ b/btd700/__main__.py @@ -1,3 +1,4 @@ +from .i18n import tr, set_language import argparse import getpass import json @@ -5,28 +6,34 @@ import sys def main(): - parser = argparse.ArgumentParser(description='Native Linux-Steuerung für den BTD 700; ohne Firmware-Updates.') - parser.add_argument('--device', help='Steuergerät aus dem Befehl devices, z. B. /dev/hidraw6') - parser.add_argument('--background', action='store_true', help='Nur im Infobereich starten') - parser.add_argument('--demo', action='store_true', help='Vorführmodus ohne USB-Zugriff') + language_parser = argparse.ArgumentParser(add_help=False) + language_parser.add_argument('--language', choices=['en', 'de']) + language_args, _ = language_parser.parse_known_args() + if language_args.language: + set_language(language_args.language) + parser = argparse.ArgumentParser(description=tr('Native Linux-Steuerung für den BTD 700; ohne Firmware-Updates.')) + parser.add_argument('--language', choices=['en', 'de'], help=tr('Sprache der App (Standard: Systemsprache)')) + parser.add_argument('--device', help=tr('Steuergerät aus dem Befehl devices, z. B. /dev/hidraw6')) + parser.add_argument('--background', action='store_true', help=tr('Nur im Infobereich starten')) + parser.add_argument('--demo', action='store_true', help=tr('Vorführmodus ohne USB-Zugriff')) parser.add_argument('--smoke-seconds', type=int, default=0, help=argparse.SUPPRESS) subs = parser.add_subparsers(dest='command') - subs.add_parser('devices', help='BTD-700-Steuerschnittstellen auflisten') - subs.add_parser('status', help='Aktuellen Status als JSON ausgeben (ohne Passwort)') - mode = subs.add_parser('mode', help='Audiomodus wählen') + subs.add_parser('devices', help=tr('BTD-700-Steuerschnittstellen auflisten')) + subs.add_parser('status', help=tr('Aktuellen Status als JSON ausgeben (ohne Passwort)')) + mode = subs.add_parser('mode', help=tr('Audiomodus wählen')) mode.add_argument('value', choices=['standard', 'gaming', 'auracast']) mode.add_argument('--transport', choices=['auto', 'classic', 'le']) - codec = subs.add_parser('codec', help='Codec im Standard-Modus wählen') + codec = subs.add_parser('codec', help=tr('Codec im Standard-Modus wählen')) codec.add_argument('value', choices=['sbc', 'aptx', 'adaptive', 'lossless', 'qmap', 'lc3']) - subs.add_parser('connect', help='Gekoppelte Kopfhörer verbinden') - subs.add_parser('disconnect', help='Kopfhörer trennen') - broadcast = subs.add_parser('auracast', help='Auracast-Einstellungen speichern') + subs.add_parser('connect', help=tr('Gekoppelte Kopfhörer verbinden')) + subs.add_parser('disconnect', help=tr('Kopfhörer trennen')) + broadcast = subs.add_parser('auracast', help=tr('Auracast-Einstellungen speichern')) broadcast.add_argument('--name') - broadcast.add_argument('--password', action='store_true', help='Neues Passwort verdeckt abfragen') + broadcast.add_argument('--password', action='store_true', help=tr('Neues Passwort verdeckt abfragen')) broadcast.add_argument('--public', choices=['on', 'off']) broadcast.add_argument('--encryption', choices=['on', 'off']) broadcast.add_argument('--quality', choices=['16k', '24k', 'high']) - reset = subs.add_parser('reset', help='Alle Kopplungen und Einstellungen löschen') + reset = subs.add_parser('reset', help=tr('Alle Kopplungen und Einstellungen löschen')) reset.add_argument('--confirm', action='store_true') args = parser.parse_args() if not args.command: @@ -34,7 +41,7 @@ def main(): from .app import run return run(background=args.background, demo=args.demo, device=args.device, smoke_seconds=args.smoke_seconds) except (ImportError, ValueError) as exc: - print(f'GTK-Oberfläche nicht verfügbar: {exc}\nAbhängigkeiten: siehe README.md. CLI: ./run.sh status', file=sys.stderr) + print(tr('GTK-Oberfläche nicht verfügbar: {error}\nAbhängigkeiten: siehe README.md. CLI: ./run.sh status').format(error=exc), file=sys.stderr) return 1 from .transport import DeviceError, Hidraw, discover from .controller import Controller @@ -51,7 +58,7 @@ def main(): else: devices = [d for d in discover() if not args.device or d.path == args.device] if len(devices) != 1: - raise DeviceError('Kein eindeutiger BTD 700 gefunden. devices ausführen; bei mehreren Dongles --device angeben.') + raise DeviceError(tr('Kein eindeutiger BTD 700 gefunden. devices ausführen; bei mehreren Dongles --device angeben.')) transport = Hidraw(devices[0]) controller = Controller(transport) if args.command == 'mode': @@ -63,18 +70,18 @@ def main(): controller.set_connection(args.command == 'connect') elif args.command == 'auracast': controller.set_broadcast(name=args.name, - password=getpass.getpass('Neues Auracast-Passwort: ') if args.password else None, + password=getpass.getpass(tr('Neues Auracast-Passwort: ')) if args.password else None, public=None if args.public is None else args.public == 'on', encrypted=None if args.encryption is None else args.encryption == 'on', quality={'16k': 0, '24k': 1, 'high': 2}.get(args.quality)) elif args.command == 'reset': controller.factory_reset(confirmed=args.confirm) - print('Zurücksetzen angefordert. Kopfhörer danach neu koppeln.') + print(tr('Zurücksetzen angefordert. Kopfhörer danach neu koppeln.')) return 0 print(json.dumps(controller.snapshot().public_dict(), indent=2, ensure_ascii=False)) return 0 except (OSError, DeviceError, ProtocolError, ValueError) as exc: - print(f'Fehler: {exc}', file=sys.stderr) + print(tr('Fehler: {error}').format(error=exc), file=sys.stderr) return 1 finally: if transport: diff --git a/btd700/app.py b/btd700/app.py index b6d7eea..b2ccc3c 100644 --- a/btd700/app.py +++ b/btd700/app.py @@ -1,4 +1,5 @@ """Native GTK 4 / libadwaita controls and a persistent notification-area menu.""" +from .i18n import tr, set_language import sys import gi @@ -11,7 +12,9 @@ from .worker import Worker class Application(Adw.Application): - def __init__(self, *, background=False, demo=False, device=None, smoke_seconds=0): + def __init__(self, *, background=False, demo=False, device=None, smoke_seconds=0, language=None): + if language: + set_language(language) super().__init__(application_id=APP_ID + ('.Demo' if demo else ''), flags=Gio.ApplicationFlags.DEFAULT_FLAGS) self.background = background self.demo = demo @@ -39,7 +42,7 @@ class Application(Adw.Application): from .tray import Tray self.tray = Tray(self) except (ImportError, ValueError, GLib.Error) as exc: - self.last_error = f'Infobereich nicht verfügbar: {exc}' + self.last_error = tr('Infobereich nicht verfügbar: {error}').format(error=exc) self.worker = Worker(self._on_status, demo=self.demo, device_path=self.device) self.worker.start() self.worker.commands.put(None) @@ -79,11 +82,11 @@ class Application(Adw.Application): toolbar = Adw.ToolbarView() header = Adw.HeaderBar() header.set_title_widget(Adw.WindowTitle(title='BTD 700', subtitle='Linux Control' + (' · DEMO' if self.demo else ''))) - refresh = Gtk.Button(icon_name='view-refresh-symbolic', tooltip_text='Status aktualisieren') + refresh = Gtk.Button(icon_name='view-refresh-symbolic', tooltip_text=tr('Status aktualisieren')) refresh.connect('clicked', lambda *_: self.worker.commands.put(None)) header.pack_start(refresh) self.refresh_button = refresh - quit_button = Gtk.Button(icon_name='application-exit-symbolic', tooltip_text='App vollständig beenden') + quit_button = Gtk.Button(icon_name='application-exit-symbolic', tooltip_text=tr('App vollständig beenden')) quit_button.connect('clicked', lambda *_: self.quit()) header.pack_end(quit_button) toolbar.add_top_bar(header) @@ -102,10 +105,10 @@ class Application(Adw.Application): heading = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) icon = Gtk.Image(icon_name='audio-headphones-symbolic', pixel_size=48) heading.append(icon) - self.codec_label = Gtk.Label(label='BTD 700 verbinden') + self.codec_label = Gtk.Label(label=tr('BTD 700 verbinden')) self.codec_label.add_css_class('title-1') heading.append(self.codec_label) - self.state_label = Gtk.Label(label='Suche nach deinem USB-Dongle …', wrap=True) + self.state_label = Gtk.Label(label=tr('Suche nach deinem USB-Dongle …'), wrap=True) self.state_label.add_css_class('dim-label') heading.append(self.state_label) self.quality_label = Gtk.Label(label='') @@ -120,7 +123,7 @@ class Application(Adw.Application): self.device_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=22, sensitive=False) content.append(self.device_box) - mode_group = Adw.PreferencesGroup(title='Audiomodus', description='Der Modus wird direkt am Dongle umgeschaltet.') + mode_group = Adw.PreferencesGroup(title=tr('Audiomodus'), description=tr('Der Modus wird direkt am Dongle umgeschaltet.')) mode_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0, homogeneous=True) mode_box.add_css_class('linked') self.mode_buttons = [] @@ -134,50 +137,50 @@ class Application(Adw.Application): mode_group.add(mode_box) self.device_box.append(mode_group) - audio = Adw.PreferencesGroup(title='Kopfhörer') + audio = Adw.PreferencesGroup(title=tr('Kopfhörer')) self.codec_row = Adw.ComboRow(title='Codec') self.codec_row.connect('notify::selected', self._codec_selected) audio.add(self.codec_row) - self.transport_row = Adw.ComboRow(title='Bluetooth-Transport') + self.transport_row = Adw.ComboRow(title=tr('Bluetooth-Transport')) self.transport_row.connect('notify::selected', self._transport_selected) audio.add(self.transport_row) - self.connection_row = Adw.ActionRow(title='Verbindung') - self.connection_button = Gtk.Button(label='Verbinden', valign=Gtk.Align.CENTER) + self.connection_row = Adw.ActionRow(title=tr('Verbindung')) + self.connection_button = Gtk.Button(label=tr('Verbinden'), valign=Gtk.Align.CENTER) self.connection_button.connect('clicked', lambda *_: self.perform('set_connection', self.status.state < 2) if self.status else None) self.connection_row.add_suffix(self.connection_button) audio.add(self.connection_row) self.device_box.append(audio) self.auracast_group = Adw.PreferencesGroup(title='Auracast', - description='Der Audiomodus „Auracast“ startet die Übertragung an mehrere Empfänger.') - self.name_entry = Adw.EntryRow(title='Name der Übertragung') + description=tr('Der Audiomodus „Auracast“ startet die Übertragung an mehrere Empfänger.')) + self.name_entry = Adw.EntryRow(title=tr('Name der Übertragung')) self.name_entry.connect('changed', self._form_changed) self.auracast_group.add(self.name_entry) - self.public_row = Adw.SwitchRow(title='Öffentlich auffindbar', - subtitle='Übertragung in der Auracast-Suche anzeigen') + self.public_row = Adw.SwitchRow(title=tr('Öffentlich auffindbar'), + subtitle=tr('Übertragung in der Auracast-Suche anzeigen')) self.public_row.connect('notify::active', self._form_changed) self.auracast_group.add(self.public_row) - self.encryption_row = Adw.SwitchRow(title='Passwortschutz') + self.encryption_row = Adw.SwitchRow(title=tr('Passwortschutz')) self.encryption_row.connect('notify::active', self._form_changed) self.auracast_group.add(self.encryption_row) - self.password_entry = Adw.PasswordEntryRow(title='Neues Passwort') - self.password_entry.set_tooltip_text('4–16 Buchstaben oder Ziffern. Leer lassen behält das bisherige Passwort.') + self.password_entry = Adw.PasswordEntryRow(title=tr('Neues Passwort')) + self.password_entry.set_tooltip_text(tr('4–16 Buchstaben oder Ziffern. Leer lassen behält das bisherige Passwort.')) self.password_entry.connect('changed', self._password_changed) self.auracast_group.add(self.password_entry) - self.broadcast_quality = Adw.ComboRow(title='Übertragungsqualität', - model=Gtk.StringList.new(['Standard · 16 kHz', 'Standard · 24 kHz', 'Hohe Qualität'])) + self.broadcast_quality = Adw.ComboRow(title=tr('Übertragungsqualität'), + model=Gtk.StringList.new([tr('Standard · 16 kHz'), tr('Standard · 24 kHz'), tr('Hohe Qualität')])) self.broadcast_quality.connect('notify::selected', self._form_changed) self.auracast_group.add(self.broadcast_quality) actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8, halign=Gtk.Align.END, margin_top=12) - self.discard_button = Gtk.Button(label='Verwerfen', sensitive=False) + self.discard_button = Gtk.Button(label=tr('Verwerfen'), sensitive=False) self.discard_button.connect('clicked', self._discard) - self.save_button = Gtk.Button(label='Speichern', sensitive=False) + self.save_button = Gtk.Button(label=tr('Speichern'), sensitive=False) self.save_button.add_css_class('suggested-action') self.save_button.connect('clicked', self._save_broadcast) actions.append(self.discard_button) actions.append(self.save_button) self.auracast_group.add(actions) - hint = Gtk.Label(label='Passwort leer lassen, um das bisherige zu behalten.', + hint = Gtk.Label(label=tr('Passwort leer lassen, um das bisherige zu behalten.'), wrap=True, xalign=0, margin_top=8) hint.add_css_class('caption') hint.add_css_class('dim-label') @@ -185,19 +188,19 @@ class Application(Adw.Application): self.device_box.append(self.auracast_group) integration = Adw.PreferencesGroup(title='App') - self.tray_row = Adw.ActionRow(title='Im Infobereich weiterlaufen', subtitle='Infobereich wird verbunden …') + self.tray_row = Adw.ActionRow(title=tr('Im Infobereich weiterlaufen'), subtitle=tr('Infobereich wird verbunden …')) integration.add(self.tray_row) - autostart = Adw.SwitchRow(title='Beim Anmelden starten', subtitle='Startet unauffällig im Infobereich', - active=autostart_path().exists()) + autostart = Adw.SwitchRow(title=tr('Beim Anmelden starten'), subtitle=tr('Startet unauffällig im Infobereich'), + active=False if self.demo else autostart_path().exists()) autostart.set_sensitive(not self.demo) autostart.connect('notify::active', self._autostart_changed) integration.add(autostart) content.append(integration) - self.firmware_label = Gtk.Label(label='Unabhängige Steuerungs-App · Keine Firmware-Updates', wrap=True) + self.firmware_label = Gtk.Label(label=tr('Unabhängige Steuerungs-App · Keine Firmware-Updates'), wrap=True) self.firmware_label.add_css_class('caption') self.firmware_label.add_css_class('dim-label') content.append(self.firmware_label) - reset = Gtk.Button(label='Dongle auf Werkseinstellungen zurücksetzen …', halign=Gtk.Align.CENTER) + reset = Gtk.Button(label=tr('Dongle auf Werkseinstellungen zurücksetzen …'), halign=Gtk.Align.CENTER) reset.add_css_class('flat') reset.connect('clicked', lambda *_: self.confirm_reset()) self.reset_button = reset @@ -220,8 +223,8 @@ class Application(Adw.Application): def tray_changed(self): available = bool(self.tray and self.tray.available) - self.tray_row.set_subtitle('Beim Schließen bleibt das Symbol in der oberen Leiste.' if available else - 'Kein Infobereich verfügbar; Schließen beendet die App.') + self.tray_row.set_subtitle(tr('Beim Schließen bleibt das Symbol in der oberen Leiste.') if available else + tr('Kein Infobereich verfügbar; Schließen beendet die App.')) def perform(self, method, *args, **kwargs): if self.busy or not self.status: @@ -233,6 +236,7 @@ class Application(Adw.Application): self._render() def _on_status(self, status, error, completed): + was_disconnected = self.status is None self.status = status self.received_status |= status is not None if completed: @@ -245,10 +249,10 @@ class Application(Adw.Application): self.password_entry.set_text('') self.updating = False self.form_submitted = False - self.toast.add_toast(Adw.Toast.new(error or 'Vom Dongle bestätigt')) + self.toast.add_toast(Adw.Toast.new(error or tr('Vom Dongle bestätigt'))) elif not status: self.last_error = error - elif self.last_error.startswith(('Kein BTD 700', 'USB-Zugriff fehlt', '[Errno')): + elif was_disconnected: self.last_error = '' self._render() return False @@ -262,7 +266,7 @@ class Application(Adw.Application): self.error_label.set_visible(bool(self.last_error)) if s: self.codec_label.set_label('Auracast' if s.mode == 2 else s.codec_name) - self.state_label.set_label('Einstellung wird übernommen …' if self.busy else STATES.get(s.state, f'Status {s.state}')) + self.state_label.set_label(tr('Einstellung wird übernommen …') if self.busy else tr(STATES.get(s.state, f'Status {s.state}'))) self.quality_label.set_label(s.quality) for mode, button in enumerate(self.mode_buttons): button.set_active(mode == s.mode) @@ -273,28 +277,28 @@ class Application(Adw.Application): self.codec_row.set_model(Gtk.StringList.new([CODECS[v] for v in values])) self.codec_row.set_selected(next((i for i, v in enumerate(values) if v & s.codec), Gtk.INVALID_LIST_POSITION)) self.codec_row.set_sensitive(s.mode == 0 and s.state >= 2 and len(values) > 1) - self.codec_row.set_subtitle('Auswahl im Standard-Modus' if s.mode != 0 else 'Vom Dongle angebotene Codecs') + self.codec_row.set_subtitle(tr('Auswahl im Standard-Modus') if s.mode != 0 else tr('Vom Dongle angebotene Codecs')) values = [3] + [v for v in (1, 2) if s.transports & v] if s.transport in (1, 2) and s.transport not in values: values.append(s.transport) if values != self.transport_values: self.transport_values = values - names = {3: 'Automatisch', 1: 'Bluetooth Classic', 2: 'LE Audio'} + names = {3: tr('Automatisch'), 1: 'Bluetooth Classic', 2: 'LE Audio'} self.transport_row.set_model(Gtk.StringList.new([names[v] for v in values])) self.transport_row.set_selected(values.index(s.transport) if s.transport in values else Gtk.INVALID_LIST_POSITION) self.transport_row.set_sensitive(s.mode == 0) - self.connection_row.set_subtitle(STATES.get(s.state, 'Unbekannt')) - self.connection_button.set_label('Trennen' if s.state >= 2 else 'Verbinden') + self.connection_row.set_subtitle(tr(STATES.get(s.state, 'Unbekannt'))) + self.connection_button.set_label(tr('Trennen') if s.state >= 2 else tr('Verbinden')) self.connection_button.set_sensitive(s.mode != 2) if not self.dirty: self.name_entry.set_text(s.broadcast_name) self.public_row.set_active(bool(s.broadcast_public)) self.encryption_row.set_active(bool(s.broadcast_encrypted)) self.broadcast_quality.set_selected(s.broadcast_quality) - self.firmware_label.set_label(f'Firmware {s.firmware} · Unabhängige App · Keine Firmware-Updates') + self.firmware_label.set_label(tr('Firmware {version} · Unabhängige App · Keine Firmware-Updates').format(version=s.firmware)) else: - self.codec_label.set_label('BTD 700 verbinden') - self.state_label.set_label('Warte auf den USB-Dongle …') + self.codec_label.set_label(tr('BTD 700 verbinden')) + self.state_label.set_label(tr('Warte auf den USB-Dongle …')) self.quality_label.set_label('') self.save_button.set_sensitive(self.dirty and not self.busy) self.discard_button.set_sensitive(self.dirty and not self.busy) @@ -356,10 +360,10 @@ class Application(Adw.Application): self.show_window() if not self.status or self.busy: return - dialog = Adw.AlertDialog(heading='BTD 700 zurücksetzen?', - body='Alle Kopplungen und gespeicherten Einstellungen werden gelöscht. Danach musst du die Kopfhörer erneut koppeln.') - dialog.add_response('cancel', 'Abbrechen') - dialog.add_response('reset', 'Zurücksetzen') + dialog = Adw.AlertDialog(heading=tr('BTD 700 zurücksetzen?'), + body=tr('Alle Kopplungen und gespeicherten Einstellungen werden gelöscht. Danach musst du die Kopfhörer erneut koppeln.')) + dialog.add_response('cancel', tr('Abbrechen')) + dialog.add_response('reset', tr('Zurücksetzen')) dialog.set_response_appearance('reset', Adw.ResponseAppearance.DESTRUCTIVE) dialog.set_default_response('cancel') dialog.set_close_response('cancel') @@ -370,7 +374,7 @@ class Application(Adw.Application): try: set_autostart(row.get_active()) except OSError as exc: - self.last_error = f'Autostart konnte nicht gespeichert werden: {exc}' + self.last_error = tr('Autostart konnte nicht gespeichert werden: {error}').format(error=exc) self._render() diff --git a/btd700/controller.py b/btd700/controller.py index 6a16051..55ebf8b 100644 --- a/btd700/controller.py +++ b/btd700/controller.py @@ -1,4 +1,5 @@ """Device operations with readback and capability checks.""" +from .i18n import tr from dataclasses import asdict, dataclass import time @@ -31,8 +32,8 @@ class Status: @property def quality(self): - bits = {1: '16 Bit', 2: '24 Bit'}.get(self.resolution, '—') - frequency = {1: '44,1 kHz', 2: '48 kHz', 3: '96 kHz'}.get(self.frequency, '—') + bits = {1: tr('16 Bit'), 2: tr('24 Bit')}.get(self.resolution, '—') + frequency = {1: tr('44,1 kHz'), 2: '48 kHz', 3: '96 kHz'}.get(self.frequency, '—') return f'{bits} / {frequency}' @property @@ -43,10 +44,10 @@ class Status: def public_dict(self): result = asdict(self) - result.update(state_name=STATES.get(self.state, f'Unbekannt ({self.state})'), - mode_name=MODES.get(self.mode, f'Unbekannt ({self.mode})'), + result.update(state_name=tr(STATES.get(self.state, tr('Unbekannt ({value})').format(value=self.state))), + mode_name=tr(MODES.get(self.mode, tr('Unbekannt ({value})').format(value=self.mode))), codec_name=self.codec_name, audio_quality=self.quality, - transport_name=TRANSPORTS.get(self.transport, f'Unbekannt ({self.transport})')) + transport_name=tr(TRANSPORTS.get(self.transport, tr('Unbekannt ({value})').format(value=self.transport)))) return result @@ -70,7 +71,7 @@ class Controller: try: self.firmware = '.'.join(map(str, q(C.GET_VERSION)[:3])) except TimeoutError: - self.firmware = 'Unbekannt' + self.firmware = tr('Unbekannt') # Firmware 3.11 does not respond to GET_GAMING; use events/fallback, # matching the original application's capability logic. event = self.transport.events.get(23, b'') @@ -87,20 +88,20 @@ class Controller: if matches: return if time.monotonic() >= deadline: - raise DeviceError('Der Dongle hat die angeforderte Einstellung nicht bestätigt. Bitte den aktuellen Status prüfen.') + raise DeviceError(tr('Der Dongle hat die angeforderte Einstellung nicht bestätigt. Bitte den aktuellen Status prüfen.')) time.sleep(0.15) def set_mode(self, mode: int, transport: int | None = None): status = self.snapshot() if mode not in MODES: - raise ValueError('Unbekannter Audiomodus.') + raise ValueError(tr('Unbekannter Audiomodus.')) if mode == 1 and not status.gaming_allowed: - raise ValueError('Gaming ist für die aktuelle Verbindung nicht verfügbar.') + raise ValueError(tr('Gaming ist für die aktuelle Verbindung nicht verfügbar.')) selected = status.transport if transport is None else transport if selected not in (1, 2, 3): - raise ValueError('Unbekannter Bluetooth-Transport.') + raise ValueError(tr('Unbekannter Bluetooth-Transport.')) if transport is not None and selected in (1, 2) and status.transports and not status.transports & selected: - raise ValueError('Die Kopfhörer unterstützen diesen Transport nicht.') + raise ValueError(tr('Die Kopfhörer unterstützen diesen Transport nicht.')) payload = bytes((mode, selected)) self.transport.request(C.SET_MODE, payload) self._verify(C.GET_MODE, payload, exact=False) @@ -108,9 +109,9 @@ class Controller: def set_codec(self, codec: int): status = self.snapshot() if status.mode != 0 or status.state < 2: - raise ValueError('Codec-Auswahl benötigt verbundene Kopfhörer im Standard-Modus.') + raise ValueError(tr('Codec-Auswahl benötigt verbundene Kopfhörer im Standard-Modus.')) if codec not in CODECS or not status.codecs & codec: - raise ValueError('Dieser Codec wird für die aktuelle Verbindung nicht angeboten.') + raise ValueError(tr('Dieser Codec wird für die aktuelle Verbindung nicht angeboten.')) payload = bytes((codec,)) self.transport.request(C.SET_CODEC, payload) self._verify(C.GET_CODEC, payload) @@ -123,7 +124,7 @@ class Controller: if (state >= 2) == connected: return if time.monotonic() >= deadline: - raise DeviceError('Verbindung wurde angefordert, aber noch nicht bestätigt. Sind die gekoppelten Kopfhörer eingeschaltet?') + raise DeviceError(tr('Verbindung wurde angefordert, aber noch nicht bestätigt. Sind die gekoppelten Kopfhörer eingeschaltet?')) time.sleep(0.25) def set_broadcast(self, *, name=None, password=None, public=None, quality=None, encrypted=None): @@ -131,9 +132,9 @@ class Controller: name_data = validate_text(name) if name is not None else None key_data = validate_text(password, key=True) if password is not None else None if any(value is not None and not isinstance(value, bool) for value in (public, encrypted)): - raise ValueError('Auracast-Schalter benötigen einen booleschen Wert.') + raise ValueError(tr('Auracast-Schalter benötigen einen booleschen Wert.')) if quality is not None and quality not in (0, 1, 2): - raise ValueError('Unbekannte Auracast-Qualität.') + raise ValueError(tr('Unbekannte Auracast-Qualität.')) before = self.snapshot() info = [before.broadcast_public, before.broadcast_quality, before.broadcast_encrypted] if public is not None: @@ -145,11 +146,11 @@ class Controller: if encrypted is not None: info[2] = int(encrypted) if info[2] and key_data == b'': - raise ValueError('Verschlüsselung benötigt ein Passwort mit 4–16 Zeichen.') + raise ValueError(tr('Verschlüsselung benötigt ein Passwort mit 4–16 Zeichen.')) if info[2] and key_data is None: existing = self.transport.request(C.GET_KEY).rstrip(b'\0') if len(existing) < 4: - raise ValueError('Bitte zuerst ein Passwort mit 4–16 Zeichen eingeben.') + raise ValueError(tr('Bitte zuerst ein Passwort mit 4–16 Zeichen eingeben.')) # Preserve the original settings while changing a key, then write # key, name, and final settings in the order used by the original app. try: @@ -169,9 +170,9 @@ class Controller: except Exception as exc: # Do not silently repeat writes or claim atomicity: settings persist # independently on the dongle. - raise DeviceError('Auracast wurde nur teilweise oder nicht übernommen. Aktuellen Namen und Passwortschutz prüfen.') from exc + raise DeviceError(tr('Auracast wurde nur teilweise oder nicht übernommen. Aktuellen Namen und Passwortschutz prüfen.')) from exc def factory_reset(self, *, confirmed=False): if not confirmed: - raise ValueError('Zurücksetzen muss ausdrücklich bestätigt werden; alle Kopplungen werden gelöscht.') + raise ValueError(tr('Zurücksetzen muss ausdrücklich bestätigt werden; alle Kopplungen werden gelöscht.')) self.transport.request(C.FACTORY_RESET) diff --git a/btd700/demo.py b/btd700/demo.py index cc3d3db..8eb175f 100644 --- a/btd700/demo.py +++ b/btd700/demo.py @@ -1,4 +1,5 @@ """Explicit, isolated demonstration device; never opens USB.""" +from .i18n import tr from .protocol import Command as C, encode @@ -30,7 +31,7 @@ class DemoTransport: elif command == C.FACTORY_RESET: self.__init__() else: - raise TimeoutError('Nicht unterstützte Demo-Abfrage') + raise TimeoutError(tr('Nicht unterstützte Demo-Abfrage')) return b'' def close(self): diff --git a/btd700/i18n.py b/btd700/i18n.py new file mode 100644 index 0000000..c26b776 --- /dev/null +++ b/btd700/i18n.py @@ -0,0 +1,157 @@ +"""Small, dependency-free English/German catalog shared by GUI, tray and CLI.""" +import os + + +def detect_language(environ=None): + env = os.environ if environ is None else environ + override = env.get('BTD700_LANGUAGE', '') + if override in ('en', 'de'): + return override + language = (env.get('LANGUAGE') or env.get('LC_ALL') or env.get('LC_MESSAGES') or env.get('LANG') or 'en') + return 'de' if language.split(':', 1)[0].lower().startswith('de') else 'en' + + +_language = detect_language() + + +def set_language(language): + global _language + if language not in ('en', 'de'): + raise ValueError('Supported languages: en, de') + _language = language + + +def get_language(): + return _language + + +def tr(message): + return ENGLISH.get(message, message) if _language == 'en' else message + + +# German source messages are retained for compatibility with the first release. +# Keep placeholders identical in both languages; never translate protocol values. +ENGLISH = { + 'Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.': 'Removed the launcher and start-at-login entry. The project folder is unchanged.', + 'Status aktualisieren': 'Refresh status', + 'App vollständig beenden': 'Quit application', + 'BTD 700 verbinden': 'Connect your BTD 700', + 'Suche nach deinem USB-Dongle …': 'Looking for your USB dongle…', + 'Audiomodus': 'Audio mode', + 'Der Modus wird direkt am Dongle umgeschaltet.': 'Switch the audio mode directly on your dongle.', + 'Kopfhörer': 'Headphones', + 'Bluetooth-Transport': 'Bluetooth transport', + 'Verbindung': 'Connection', + 'Verbinden': 'Connect', + 'Trennen': 'Disconnect', + 'Der Audiomodus „Auracast“ startet die Übertragung an mehrere Empfänger.': 'Choose Auracast mode to broadcast audio to multiple receivers.', + 'Name der Übertragung': 'Broadcast name', + 'Öffentlich auffindbar': 'Publicly discoverable', + 'Übertragung in der Auracast-Suche anzeigen': 'Show this broadcast in Auracast discovery', + 'Passwortschutz': 'Password protection', + 'Neues Passwort': 'New password', + '4–16 Buchstaben oder Ziffern. Leer lassen behält das bisherige Passwort.': '4–16 letters or digits. Leave blank to keep the current password.', + 'Übertragungsqualität': 'Broadcast quality', + 'Standard · 16 kHz': 'Standard · 16 kHz', + 'Standard · 24 kHz': 'Standard · 24 kHz', + 'Hohe Qualität': 'High quality', + 'Verwerfen': 'Discard', + 'Speichern': 'Save', + 'Passwort leer lassen, um das bisherige zu behalten.': 'Leave the password blank to keep the current one.', + 'Im Infobereich weiterlaufen': 'Keep running in the system tray', + 'Infobereich wird verbunden …': 'Connecting to the system tray…', + 'Beim Anmelden starten': 'Start at login', + 'Startet unauffällig im Infobereich': 'Start quietly in the system tray', + 'Unabhängige Steuerungs-App · Keine Firmware-Updates': 'Independent control app · No firmware updates', + 'Dongle auf Werkseinstellungen zurücksetzen …': 'Reset dongle to factory settings…', + 'Beim Schließen bleibt das Symbol in der oberen Leiste.': 'Closing the window keeps the tray icon available.', + 'Kein Infobereich verfügbar; Schließen beendet die App.': 'No system tray available; closing the window quits the app.', + 'Vom Dongle bestätigt': 'Confirmed by the dongle', + 'Einstellung wird übernommen …': 'Applying setting…', + 'Auswahl im Standard-Modus': 'Available in Standard mode', + 'Vom Dongle angebotene Codecs': 'Codecs reported by the dongle', + 'Automatisch': 'Automatic', + 'Nicht verbunden': 'Disconnected', + 'Bereit': 'Ready', + 'Kopfhörer getrennt': 'Headphones disconnected', + 'Kopfhörer verbunden': 'Headphones connected', + 'Musikwiedergabe': 'Playing audio', + 'Sprachanruf': 'Voice call', + 'Unbekannt': 'Unknown', + 'Unbekannter Status': 'Unknown status', + 'Warte auf den USB-Dongle …': 'Waiting for the USB dongle…', + 'BTD 700 zurücksetzen?': 'Reset BTD 700?', + 'Alle Kopplungen und gespeicherten Einstellungen werden gelöscht. Danach musst du die Kopfhörer erneut koppeln.': 'This removes all pairings and saved settings. You will need to pair your headphones again.', + 'Abbrechen': 'Cancel', + 'Zurücksetzen': 'Reset', + 'Infobereich nicht verfügbar: {error}': 'System tray unavailable: {error}', + 'Autostart konnte nicht gespeichert werden: {error}': 'Could not save the start-at-login setting: {error}', + 'Firmware {version} · Unabhängige App · Keine Firmware-Updates': 'Firmware {version} · Independent app · No firmware updates', + 'BTD 700 anschließen': 'Connect a BTD 700', + 'Keine verbundenen Kopfhörer': 'No headphones connected', + 'Name und Passwort …': 'Name and password…', + 'Kopfhörer trennen': 'Disconnect headphones', + 'Kopfhörer verbinden': 'Connect headphones', + 'Werkseinstellungen …': 'Factory reset…', + 'Dongle nicht verbunden': 'Dongle disconnected', + 'USB-Zugriff prüfen – Fenster öffnen': 'Check USB access — open window', + 'Fehler anzeigen …': 'Show error…', + 'Fenster öffnen': 'Open window', + 'Beenden': 'Quit', + '16 Bit': '16-bit', + '24 Bit': '24-bit', + '44,1 kHz': '44.1 kHz', + 'Unbekannt ({value})': 'Unknown ({value})', + 'Der Dongle hat die angeforderte Einstellung nicht bestätigt. Bitte den aktuellen Status prüfen.': 'The dongle did not confirm the requested setting. Please check its current status.', + 'Unbekannter Audiomodus.': 'Unknown audio mode.', + 'Gaming ist für die aktuelle Verbindung nicht verfügbar.': 'Gaming mode is unavailable for this connection.', + 'Unbekannter Bluetooth-Transport.': 'Unknown Bluetooth transport.', + 'Die Kopfhörer unterstützen diesen Transport nicht.': 'Your headphones do not support this transport.', + 'Codec-Auswahl benötigt verbundene Kopfhörer im Standard-Modus.': 'Codec selection requires connected headphones in Standard mode.', + 'Dieser Codec wird für die aktuelle Verbindung nicht angeboten.': 'This codec is not offered for the current connection.', + 'Verbindung wurde angefordert, aber noch nicht bestätigt. Sind die gekoppelten Kopfhörer eingeschaltet?': 'Connection requested but not yet confirmed. Are your paired headphones switched on?', + 'Auracast-Schalter benötigen einen booleschen Wert.': 'Auracast switches require a boolean value.', + 'Unbekannte Auracast-Qualität.': 'Unknown Auracast quality.', + 'Verschlüsselung benötigt ein Passwort mit 4–16 Zeichen.': 'Encryption requires a password of 4–16 characters.', + 'Bitte zuerst ein Passwort mit 4–16 Zeichen eingeben.': 'Enter a password of 4–16 characters first.', + 'Auracast wurde nur teilweise oder nicht übernommen. Aktuellen Namen und Passwortschutz prüfen.': 'Auracast settings were only partially applied or not applied. Check the current name and password protection.', + 'Zurücksetzen muss ausdrücklich bestätigt werden; alle Kopplungen werden gelöscht.': 'Reset requires explicit confirmation; all pairings will be removed.', + 'Passwort: 4–16 Buchstaben oder Ziffern.': 'Password: 4–16 letters or digits.', + 'Name: 4–16 Buchstaben, Ziffern oder innere Leerzeichen.': 'Name: 4–16 letters, digits or internal spaces.', + 'Unbekannter Steuerbefehl; kein Zugriff auf Update-Befehle.': 'Unknown control command; update commands are not accessible.', + 'Ungültige Antwort des Dongles.': 'Invalid response from the dongle.', + 'Ungültige Parameter für {command}.': 'Invalid parameters for {command}.', + 'Unbekannte Benachrichtigung: {event:#x}': 'Unknown notification: {event:#x}', + 'Gerätepfad gehört nicht mehr zum BTD 700.': 'The device path no longer belongs to a BTD 700.', + 'Der Dongle ist bereits in einer anderen Instanz geöffnet.': 'Another instance already has this dongle open.', + 'USB-Zugriff fehlt. Die mitgelieferte udev-Regel installieren und den Dongle neu einstecken.': 'USB access denied. Install the included udev rule and reconnect the dongle.', + 'Dongle ist nicht geöffnet.': 'The dongle is not open.', + 'USB-Schreibzugriff hat nicht geantwortet.': 'USB write timed out.', + 'Unvollständiger USB-Schreibvorgang.': 'Incomplete USB write.', + 'Dongle wurde entfernt.': 'The dongle was unplugged.', + 'Unvollständige Antwort auf {command}.': 'Incomplete response to {command}.', + 'Der Dongle antwortet nicht auf {command}.': 'The dongle did not respond to {command}.', + 'Kein BTD 700 gefunden. Bitte den Dongle einstecken.': 'No BTD 700 found. Please connect the dongle.', + 'Mehrere BTD 700 gefunden. Mit --device /dev/hidrawN auswählen.': 'Multiple BTD 700 dongles found. Select one with --device /dev/hidrawN.', + 'Nicht unterstützte Demo-Abfrage': 'Unsupported demo query', + 'Native Linux-Steuerung für den BTD 700; ohne Firmware-Updates.': 'Native Linux controls for the BTD 700; no firmware updates.', + 'Steuergerät aus dem Befehl devices, z. B. /dev/hidraw6': 'Control device listed by devices, e.g. /dev/hidraw6', + 'Nur im Infobereich starten': 'Start in the system tray only', + 'Vorführmodus ohne USB-Zugriff': 'Demo mode without USB access', + 'BTD-700-Steuerschnittstellen auflisten': 'List BTD 700 control interfaces', + 'Aktuellen Status als JSON ausgeben (ohne Passwort)': 'Print current status as JSON (without passwords)', + 'Audiomodus wählen': 'Select an audio mode', + 'Codec im Standard-Modus wählen': 'Select a codec in Standard mode', + 'Gekoppelte Kopfhörer verbinden': 'Connect paired headphones', + 'Auracast-Einstellungen speichern': 'Save Auracast settings', + 'Neues Passwort verdeckt abfragen': 'Prompt for a new password without echoing it', + 'Alle Kopplungen und Einstellungen löschen': 'Remove all pairings and settings', + 'Kein eindeutiger BTD 700 gefunden. devices ausführen; bei mehreren Dongles --device angeben.': 'No unique BTD 700 found. Run devices; use --device if multiple dongles are connected.', + 'GTK-Oberfläche nicht verfügbar: {error}\nAbhängigkeiten: siehe README.md. CLI: ./run.sh status': 'GTK interface unavailable: {error}\nDependencies: see README.md. CLI: ./run.sh status', + 'Neues Auracast-Passwort: ': 'New Auracast password: ', + 'Zurücksetzen angefordert. Kopfhörer danach neu koppeln.': 'Reset requested. Pair your headphones again afterwards.', + 'Fehler: {error}': 'Error: {error}', + 'Installiert: {path}': 'Installed: {path}', + 'Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.': 'Open “BTD 700 Control” from your application menu. Keep the project folder in place.', + 'Sprache der App (Standard: Systemsprache)': 'App language (default: system language)', +} diff --git a/btd700/integration.py b/btd700/integration.py index dba10d8..a75d411 100644 --- a/btd700/integration.py +++ b/btd700/integration.py @@ -15,7 +15,8 @@ def desktop_entry(*, background=False): # Desktop Entry Exec quoting is not shell quoting. escaped = path.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`').replace('$', '\\$').replace('%', '%%') return ('[Desktop Entry]\nType=Application\nName=BTD 700 Control\n' - 'Comment=Linux-Steuerung für den Sennheiser BTD 700\n' + 'Comment=Linux controls for the Sennheiser BTD 700\n' + 'Comment[de]=Linux-Steuerung für den Sennheiser BTD 700\n' f'Exec="{escaped}"{module_args}' + (' --background' if background else '') + '\n' f'Icon={icon}\nTerminal=false\n' 'Categories=AudioVideo;Audio;\n' diff --git a/btd700/protocol.py b/btd700/protocol.py index 3f792a6..dfdd9ea 100644 --- a/btd700/protocol.py +++ b/btd700/protocol.py @@ -1,4 +1,5 @@ """BTD 700 control protocol. No updater transport or arbitrary commands.""" +from .i18n import tr from dataclasses import dataclass from enum import IntEnum import re @@ -51,8 +52,8 @@ def validate_text(value: str, *, key: bool = False) -> bytes: """Match the original app's 4–16 ASCII character input bounds; empty resets.""" pattern = r'[A-Za-z0-9]{4,16}' if key else r'[A-Za-z0-9 ]{4,16}' if value and (not re.fullmatch(pattern, value) or value != value.strip()): - what = 'Passwort: 4–16 Buchstaben oder Ziffern.' if key else ( - 'Name: 4–16 Buchstaben, Ziffern oder innere Leerzeichen.') + what = tr('Passwort: 4–16 Buchstaben oder Ziffern.') if key else ( + tr('Name: 4–16 Buchstaben, Ziffern oder innere Leerzeichen.')) raise ValueError(what) return value.encode('ascii') @@ -61,7 +62,7 @@ def encode(command: Command, payload: bytes = b'') -> bytes: try: command = Command(command) except ValueError as exc: - raise ValueError('Unbekannter Steuerbefehl; kein Zugriff auf Update-Befehle.') from exc + raise ValueError(tr('Unbekannter Steuerbefehl; kein Zugriff auf Update-Befehle.')) from exc if command in READ_COMMANDS or command == Command.FACTORY_RESET: valid = not payload elif command == Command.SET_MODE: @@ -79,13 +80,13 @@ def encode(command: Command, payload: bytes = b'') -> bytes: else: valid = False if not valid: - raise ValueError(f'Ungültige Parameter für {command.name}.') + raise ValueError(tr('Ungültige Parameter für {command}.').format(command=command.name)) return bytes((REPORT_ID, 0xFE, command, len(payload))) + payload + bytes(60 - len(payload)) def acknowledge(event: int) -> bytes: if event not in EVENTS: - raise ProtocolError(f'Unbekannte Benachrichtigung: {event:#x}') + raise ProtocolError(tr('Unbekannte Benachrichtigung: {event:#x}').format(event=event)) return bytes((REPORT_ID, 0xFD, event, 0)) + bytes(60) @@ -100,5 +101,5 @@ def decode(report: bytes) -> Message | None: if not report or report[0] != REPORT_ID: return None # Media keys share the same interface. if len(report) < 4 or report[1] not in (0xFC, 0xFF) or report[3] > 60 or len(report) < 4 + report[3]: - raise ProtocolError('Ungültige Antwort des Dongles.') + raise ProtocolError(tr('Ungültige Antwort des Dongles.')) return Message(report[1], report[2], report[4:4 + report[3]]) diff --git a/btd700/transport.py b/btd700/transport.py index 507e94d..0cb5c9b 100644 --- a/btd700/transport.py +++ b/btd700/transport.py @@ -1,4 +1,5 @@ """Linux hidraw access, limited to the BTD 700 control interface.""" +from .i18n import tr from dataclasses import dataclass from pathlib import Path import fcntl @@ -49,14 +50,14 @@ class Hidraw: fcntl.ioctl(self.fd, 0x80084803, info, True) # HIDIOCGRAWINFO bus, vendor, product = struct.unpack('IHH', info) if (bus, vendor, product) != (3, 0x3542, 0x3001): - raise DeviceError('Gerätepfad gehört nicht mehr zum BTD 700.') + raise DeviceError(tr('Gerätepfad gehört nicht mehr zum BTD 700.')) try: fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError as exc: - raise DeviceError('Der Dongle ist bereits in einer anderen Instanz geöffnet.') from exc + raise DeviceError(tr('Der Dongle ist bereits in einer anderen Instanz geöffnet.')) from exc except PermissionError as exc: self.close() - raise DeviceError('USB-Zugriff fehlt. Die mitgelieferte udev-Regel installieren und den Dongle neu einstecken.') from exc + raise DeviceError(tr('USB-Zugriff fehlt. Die mitgelieferte udev-Regel installieren und den Dongle neu einstecken.')) from exc except BaseException: self.close() raise @@ -74,16 +75,16 @@ class Hidraw: def _write(self, report): if self.fd is None: - raise DeviceError('Dongle ist nicht geöffnet.') + raise DeviceError(tr('Dongle ist nicht geöffnet.')) if not select.select([], [self.fd], [], self.timeout)[1]: - raise TimeoutError('USB-Schreibzugriff hat nicht geantwortet.') + raise TimeoutError(tr('USB-Schreibzugriff hat nicht geantwortet.')) if os.write(self.fd, report) != len(report): - raise DeviceError('Unvollständiger USB-Schreibvorgang.') + raise DeviceError(tr('Unvollständiger USB-Schreibvorgang.')) def _receive(self): report = os.read(self.fd, 1024) if not report: - raise DeviceError('Dongle wurde entfernt.') + raise DeviceError(tr('Dongle wurde entfernt.')) message = decode(report) if message and message.kind == 0xFC and message.command in EVENTS: self.events[message.command] = message.payload @@ -107,9 +108,9 @@ class Hidraw: message = self._receive() if message and message.kind == 0xFF and message.command == command: if len(message.payload) < MIN_LENGTHS.get(command, 0): - raise ProtocolError(f'Unvollständige Antwort auf {command.name}.') + raise ProtocolError(tr('Unvollständige Antwort auf {command}.').format(command=command.name)) return message.payload - raise TimeoutError(f'Der Dongle antwortet nicht auf {command.name}.') + raise TimeoutError(tr('Der Dongle antwortet nicht auf {command}.').format(command=command.name)) except (OSError, ProtocolError): self.close() raise diff --git a/btd700/tray.py b/btd700/tray.py index daabbf1..aeae4d7 100644 --- a/btd700/tray.py +++ b/btd700/tray.py @@ -1,4 +1,5 @@ """StatusNotifierItem + libdbusmenu, compatible with GNOME AppIndicator and KDE.""" +from .i18n import tr import gi from gi.repository import Gio, GLib from .protocol import CODECS, MODES, STATES @@ -65,7 +66,7 @@ class Tray: self.app.tray_changed() def _property(self, _bus, _sender, _path, _iface, name): - tooltip = 'BTD 700 anschließen' + tooltip = tr('BTD 700 anschließen') if self.status: tooltip = f'{MODES.get(self.status.mode, "—")} · {self.status.codec_name} · {self.status.quality}' props = { @@ -117,8 +118,8 @@ class Tray: add(self._item('BTD 700 Control · DEMO' if self.app.demo else 'BTD 700 Control', self.app.show_window)) if status: add(self._item(f'{status.codec_name} · {status.quality}', enabled=False)) - add(self._item(STATES.get(status.state, 'Unbekannter Status'), enabled=False)) - modes = self._submenu(root, 'Audiomodus') + add(self._item(tr(STATES.get(status.state, 'Unbekannter Status')), enabled=False)) + modes = self._submenu(root, tr('Audiomodus')) for value, label in MODES.items(): modes.child_append(self._item(label, lambda v=value: self.app.perform('set_mode', v), @@ -132,41 +133,41 @@ class Tray: enabled=not busy and status.mode == 0 and status.state >= 2, checked=bool(status.codec & value), radio=True)) if not codecs.get_children(): - codecs.child_append(self._item('Keine verbundenen Kopfhörer', enabled=False)) - transports = self._submenu(root, 'Bluetooth-Transport') - for value, label in ((3, 'Automatisch'), (1, 'Bluetooth Classic'), (2, 'LE Audio')): + codecs.child_append(self._item(tr('Keine verbundenen Kopfhörer'), enabled=False)) + transports = self._submenu(root, tr('Bluetooth-Transport')) + for value, label in ((3, tr('Automatisch')), (1, 'Bluetooth Classic'), (2, 'LE Audio')): transports.child_append(self._item(label, lambda v=value: self.app.perform('set_mode', status.mode, transport=v), enabled=not busy and status.mode == 0 and (value == 3 or bool(status.transports & value)), checked=status.transport == value, radio=True)) auracast = self._submenu(root, 'Auracast') - auracast.child_append(self._item('Name und Passwort …', lambda: self.app.show_window('auracast'))) - auracast.child_append(self._item('Öffentlich auffindbar', + auracast.child_append(self._item(tr('Name und Passwort …'), lambda: self.app.show_window('auracast'))) + auracast.child_append(self._item(tr('Öffentlich auffindbar'), lambda: self.app.perform('set_broadcast', public=not status.broadcast_public), enabled=not busy, checked=bool(status.broadcast_public))) - auracast.child_append(self._item('Passwortschutz', + auracast.child_append(self._item(tr('Passwortschutz'), lambda: self.app.toggle_encryption(), enabled=not busy, checked=bool(status.broadcast_encrypted))) - quality = self._submenu(auracast, 'Übertragungsqualität') - for value, label in enumerate(('Standard · 16 kHz', 'Standard · 24 kHz', 'Hohe Qualität')): + quality = self._submenu(auracast, tr('Übertragungsqualität')) + for value, label in enumerate((tr('Standard · 16 kHz'), tr('Standard · 24 kHz'), tr('Hohe Qualität'))): quality.child_append(self._item(label, lambda v=value: self.app.perform('set_broadcast', quality=v), enabled=not busy, checked=status.broadcast_quality == value, radio=True)) - add(self._item('Kopfhörer trennen' if status.state >= 2 else 'Kopfhörer verbinden', + add(self._item(tr('Kopfhörer trennen') if status.state >= 2 else tr('Kopfhörer verbinden'), lambda: self.app.perform('set_connection', status.state < 2), enabled=not busy and status.mode != 2)) - add(self._item('Werkseinstellungen …', self.app.confirm_reset, enabled=not busy)) + add(self._item(tr('Werkseinstellungen …'), self.app.confirm_reset, enabled=not busy)) else: - add(self._item('Dongle nicht verbunden' if not error else 'USB-Zugriff prüfen – Fenster öffnen', + add(self._item(tr('Dongle nicht verbunden') if not error else tr('USB-Zugriff prüfen – Fenster öffnen'), self.app.show_window)) if busy: - add(self._item('Einstellung wird übernommen …', enabled=False)) + add(self._item(tr('Einstellung wird übernommen …'), enabled=False)) if error: - add(self._item('Fehler anzeigen …', self.app.show_window)) + add(self._item(tr('Fehler anzeigen …'), self.app.show_window)) separator = Dbusmenu.Menuitem.new() separator.property_set('type', 'separator') add(separator) - add(self._item('Fenster öffnen', self.app.show_window)) - add(self._item('Beenden', self.app.quit)) + add(self._item(tr('Fenster öffnen'), self.app.show_window)) + add(self._item(tr('Beenden'), self.app.quit)) self.root = root # Keep Python signal closures alive. self.server.set_root(root) self.bus.emit_signal(None, '/StatusNotifierItem', INTERFACE, 'NewToolTip', None) diff --git a/btd700/worker.py b/btd700/worker.py index ebc8f34..f70c21d 100644 --- a/btd700/worker.py +++ b/btd700/worker.py @@ -1,4 +1,5 @@ """One thread owns the USB handle; the GTK main loop never waits for USB.""" +from .i18n import tr import queue import threading from gi.repository import GLib @@ -44,9 +45,9 @@ class Worker(threading.Thread): if self.device_path: devices = [d for d in devices if d.path == self.device_path] if not devices: - raise DeviceError('Kein BTD 700 gefunden. Bitte den Dongle einstecken.') + raise DeviceError(tr('Kein BTD 700 gefunden. Bitte den Dongle einstecken.')) if len(devices) > 1: - raise DeviceError('Mehrere BTD 700 gefunden. Mit --device /dev/hidrawN auswählen.') + raise DeviceError(tr('Mehrere BTD 700 gefunden. Mit --device /dev/hidrawN auswählen.')) self.transport = Hidraw(devices[0]) self.controller = Controller(self.transport) if action: diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 65a30f4..edb54c1 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -1,142 +1,145 @@ -# Rekonstruiertes BTD-700-Steuerprotokoll +# Reconstructed BTD 700 control protocol -Stand der Untersuchung: 5. September 2026. Zweck: unabhängige Linux-Steuerung des -eigenen USB-Dongles. Die Windows-Anwendung wurde statisch untersucht, nicht ausgeführt. +Investigation date: 5 September 2026. Purpose: independently control a user's +USB dongle on Linux. The official Windows application was examined statically; +it was not executed. Research and implementation were carried out with AI +(OpenAI Codex). This is a working protocol reconstruction, not a vendor specification. -## Herkunft und Reproduzierbarkeit +## Provenance and reproducibility -- [Offizielle Produktseite](https://uk.sennheiser-hearing.com/products/btd-700) -- [Offizieller Dongle-Control-Download](https://uk.sennheiser-hearing.com/pages/sennheiser-dongle-control) -- [Dort verlinktes Windows-ZIP](https://eu-central-1-akqa.graphassets.com/AGz66yvUcQ42Ggm7CrXdgz/cmgrvi8excrci07uu3ivz166x) -- Archiv: `windows-signed-v1.0.5/Sennheiser Dongle Control.exe`, Version 1.0.5.0, +- [Official product page](https://uk.sennheiser-hearing.com/products/btd-700) +- [Official Dongle Control download page](https://uk.sennheiser-hearing.com/pages/sennheiser-dongle-control) +- [Windows ZIP linked from that page](https://eu-central-1-akqa.graphassets.com/AGz66yvUcQ42Ggm7CrXdgz/cmgrvi8excrci07uu3ivz166x) +- Archive entry: `windows-signed-v1.0.5/Sennheiser Dongle Control.exe`, version 1.0.5.0, ProductVersion `1.0.5+eac62d73c43c8572999e7e68cbaec6e4536a1318`. -- SHA256 ZIP: `1d1057b7eb64ab08e41d76723c343c691196f0affe1d5fc9168f01f9908a8cc7` -- SHA256 EXE: `e176f1ab7d4aae40308c152b0bd95227ac5a16fedb99efef7e9229345d8014c0` -- SHA256 eingebettete App-Assembly: +- ZIP SHA256: `1d1057b7eb64ab08e41d76723c343c691196f0affe1d5fc9168f01f9908a8cc7` +- EXE SHA256: `e176f1ab7d4aae40308c152b0bd95227ac5a16fedb99efef7e9229345d8014c0` +- Embedded application assembly SHA256: `2e8c89ea0333b0a9dd5bb0e851cd685810edaa6e32caee7645511d646ee970b4` -Die EXE ist ein .NET-Single-File-Bundle, Manifestversion 6 mit 451 Einträgen. -Die benötigte Assembly kann mit `tools/extract_control_assembly.py` aus einer -lokalen Original-EXE extrahiert werden. Das Bundleformat wurde mit den -[Manifest](https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs)- -und [FileEntry](https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs)- -Definitionen des .NET-Runtimes abgeglichen. +The executable is a .NET single-file bundle, manifest version 6, with 451 entries. +`tools/extract_control_assembly.py` can extract only the application assembly from +a locally supplied original EXE. Its format was checked against the .NET runtime's +[Manifest](https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs) +and [FileEntry](https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/FileEntry.cs) +definitions. -Analysewerkzeug: ILSpy CLI 11.0.0.9375. Relevante Typen: -`BTDTool.BTD700Tool`, `_BTD700_HOSTCMD`, `_BTD700_DONGLECMD`, `_BTD700_*`-Enums, +Analysis tool: ILSpy CLI 11.0.0.9375. Relevant original types: +`BTDTool.BTD700Tool`, `_BTD700_HOSTCMD`, `_BTD700_DONGLECMD`, the `_BTD700_*` enums, `BTD700Context`, `HidDeviceExt.sendGenericCommand`, -`ViewModels.MainAppWindowViewModel`, `Views.AppBtd700Features`. +`ViewModels.MainAppWindowViewModel` and `Views.AppBtd700Features`. -Die neue Implementierung enthält nur rekonstruierte Protokollfakten und eigenen -Code. Original-Binaries, dekompilierte Originalquellen und Ressourcen gehören -nicht zum Projekt. Die Herstellerlizenz der Originalsoftware bleibt davon getrennt. +This repository contains protocol facts and an independent implementation. +Original executables, decompiled original sources and vendor resources are not +included. The original software retains its own license. Extracted assemblies +are not required to run this app and should not be added to the repository. -## USB und Framing +## USB interface and framing -VID `0x3542`, PID `0x3001`, USB-HID-Interface 0. Die Steuersammlung verwendet -Vendor Usage Page `0xFFA2`, Report ID **0x34**. Auf dem Testsystem liegt sie unter -`/dev/hidraw6`; die Nummer wird dynamisch ermittelt. Interface 1 ist für diese -Steuer-App nicht erforderlich und wird nicht geöffnet. +VID `0x3542`, PID `0x3001`, USB HID interface 0. The control collection uses vendor +usage page `0xFFA2` and report ID **0x34**. It appeared as `/dev/hidraw6` on the test +system; the application discovers the path dynamically from its descriptor. +Interface 1 is not opened by this control app. -64-Byte-Output-Report, ungenutzte Bytes mit Null gefüllt: +Output reports are 64 bytes, with unused bytes padded with zero: -| Byte | Bedeutung | +| Byte | Meaning | |---|---| -| 0 | Report-ID `34` | -| 1 | `FE` Host-Befehl; `FF` Dongle-Antwort; `FC` Dongle-Ereignis; `FD` Ereignisbestätigung | -| 2 | Befehls- oder Ereignisnummer | -| 3 | Nutzdatenlänge, maximal 60 | -| 4… | Nutzdaten | +| 0 | Report ID `34` | +| 1 | `FE`: host command; `FF`: dongle response; `FC`: dongle event; `FD`: event acknowledgement | +| 2 | Command or event number | +| 3 | Payload length, at most 60 | +| 4… | Payload | -Host-Abfrage Beispiel: `34 FE 06 00` + 60 Nullbytes. -Antwort Beispiel: `34 FF 06 01 03` (Audio läuft). -Bestätigung des Ereignisses 15: `34 FD 0F 00` + 60 Nullbytes. +Example query: `34 FE 06 00` followed by 60 zero bytes. +Example reply: `34 FF 06 01 03` (audio is playing). +Acknowledgement for event 15: `34 FD 0F 00` followed by 60 zero bytes. -Ereignisse 2/3/4/15/16/17/22/23 werden bestätigt. Andere Reports können von -Medientasten auf derselben Schnittstelle stammen und werden ignoriert. Antworten -werden nach Richtung und Befehlsnummer zugeordnet; Längen werden geprüft. -Nur eine Anfrage ist gleichzeitig aktiv. Schreibbefehle werden nicht blind -wiederholt; für Einstellungen wird anschließend der Wert erneut abgefragt. -Es gibt keine Transaktions-ID und keine garantierte Atomizität mehrerer Einstellungen. +Events 2/3/4/15/16/17/22/23 are acknowledged. Other report IDs can carry media keys +on the same interface and are ignored. Replies are matched by direction and +command number, with length validation. Only one request is outstanding at a time. +Setters are not blindly retried; settings are queried again for confirmation. +There is no transaction ID or guarantee of atomic multi-setting updates. -## Befehle +## Commands -Alle Nummern hexadezimal. Bei Lesezugriffen ist die Anfrage-Nutzlast leer. -Die nachfolgende Nutzlast beschreibt bei `GET` die Antwort, bei `SET` die Anfrage. +IDs below are hexadecimal. GET requests have an empty payload. The payload column +means the response for GET operations and the request for SET operations. -| ID | Operation | Nutzlast | +| ID | Operation | Payload | |---|---|---| -| 01 | GET Modus/Transport | Modus, konfigurierter Transport, optional aktuell verbundener Transport | -| 02 | SET Modus/Transport | Modus, Transport | -| 03 | GET verfügbare Codecs | Codec-Bitmaske | -| 04 | SET Codec | Einzelnes Codec-Bit, nicht Bitindex | -| 05 | GET aktiver Codec | Codec-Bitmaske | -| 06 | GET Dongle-Zustand | Zustand | -| 07 | GET LE-Audio-Zustand | LE-Zustand | -| 08 | GET Audioqualität | Auflösung, Frequenz | -| 09 | GET Auracast-Konfiguration | öffentlich, Qualität, Verschlüsselung | -| 0A | SET Auracast-Konfiguration | öffentlich, Qualität, Verschlüsselung | -| 0B | GET Auracast-Schlüssel | Zeichenbytes; nur bei expliziter Passwort-Operation gelesen | -| 0C | SET Auracast-Schlüssel | 0–16 Bytes; UI beschränkt auf leer oder 4–16 ASCII-Zeichen | -| 0D | GET Auracast-Name | Zeichenbytes; am Gerät 32 Bytes, mit Null aufgefüllt | -| 0E | SET Auracast-Name | bis 16 Zeichenbytes; leer setzt Gerätenamen zurück | -| 12 | GET Firmwareversion | drei Versionsbytes, nur Anzeige | -| 13 | Werksreset | leer; nur nach Nutzerbestätigung | -| 14 | Bluetooth verbinden/trennen | 1 / 0 | -| 15 | GET Kopfhörer-Transportmöglichkeiten | Bitmaske | -| 17 | GET Gaming-Verfügbarkeit | im Original definiert; Firmware 3.11 antwortet nicht, daher nicht regelmäßig abgefragt | +| 01 | GET mode/transport | Mode, configured transport, optional connected transport | +| 02 | SET mode/transport | Mode, transport | +| 03 | GET available codecs | Codec bitmask | +| 04 | SET codec | One codec bit, not its bit index | +| 05 | GET active codec | Codec bitmask | +| 06 | GET dongle state | State | +| 07 | GET LE Audio state | LE state | +| 08 | GET audio quality | Resolution, frequency | +| 09 | GET Auracast configuration | Public discovery, quality, encryption | +| 0A | SET Auracast configuration | Public discovery, quality, encryption | +| 0B | GET Auracast key | Character bytes; only read by explicit password operations | +| 0C | SET Auracast key | 0–16 bytes; UI accepts empty or 4–16 ASCII characters | +| 0D | GET Auracast name | Character bytes; 32 bytes with zero padding on the tested device | +| 0E | SET Auracast name | Up to 16 character bytes; empty restores the device default | +| 12 | GET firmware version | Three version bytes, display only | +| 13 | Factory reset | Empty; requires user confirmation | +| 14 | Bluetooth connect/disconnect | 1 / 0 | +| 15 | GET supported headphone transports | Bitmask | +| 17 | GET Gaming availability | Defined by the original app; firmware 3.11 did not reply, so not polled | -Modus: 0 Standard, 1 Gaming, 2 Auracast. -Transport: 0 getrennt, 1 BR/EDR, 2 LE Audio, 3 Dual/automatisch. Setzen von 0 wird -in der neuen App nicht angeboten; Trennen hat einen eigenen Befehl. +Mode: 0 Standard, 1 Gaming, 2 Auracast. +Transport: 0 disconnected, 1 BR/EDR, 2 LE Audio, 3 dual/automatic. The app does not +set transport 0; disconnection has a separate command. -Codec-Bits: `01` SBC, `02` aptX Classic, `04` aptX Adaptive/Low Latency, -`08` aptX Lossless, `10` aptX Lite/QMAP, `20` LC3. Nur die vom Dongle angebotenen -Bits werden im Menü angezeigt. Die Firmware kann die Liste abhängig vom Modus ändern. +Codec bits: `01` SBC, `02` aptX Classic, `04` aptX Adaptive/Low Latency, +`08` aptX Lossless, `10` aptX Lite/QMAP, `20` LC3. Only offered bits appear as +choices. Firmware can change the available list depending on the current mode. -Dongle-Zustand: 0 keiner/bereit, 1 getrennt, 2 verbunden, 3 Audio, 4 Sprache. -LE-Zustand: 0 keiner, 1 getrennt, 2 verbunden, 3 Unicast, 4 Broadcast. -Auflösung: 1 = 16 Bit, 2 = 24 Bit. -Frequenz: 1 = 44,1 kHz, 2 = 48 kHz, 3 = 96 kHz. -Auracast: öffentlich 0/1; Qualität 0 = SQ 16 kHz, 1 = SQ 24 kHz, 2 = HQ; -Verschlüsselung 0/1. „Öffentlich“ bezeichnet die Ankündigung/Auffindbarkeit, -nicht das Aktivieren des Audiomodus. +Dongle state: 0 none/ready, 1 disconnected, 2 connected, 3 audio, 4 voice. +LE state: 0 none, 1 disconnected, 2 connected, 3 unicast, 4 broadcast. +Resolution: 1 = 16-bit, 2 = 24-bit. +Frequency: 1 = 44.1 kHz, 2 = 48 kHz, 3 = 96 kHz. +Auracast: public discovery 0/1; quality 0 = SQ 16 kHz, 1 = SQ 24 kHz, 2 = HQ; +encryption 0/1. Public discovery advertises the broadcast; it does not select +the audio mode or start/stop audio by itself. -Gaming-Verfügbarkeit fällt wie in der Original-App auf den Verbindungsstatus, -aptX-Adaptive-Bit und LE-Transport zurück, solange kein Ereignis 23 eingetroffen ist. +Gaming availability falls back to the current connection, aptX Adaptive bit and +LE transport, following the original app, unless event 23 has supplied a value. +Display strings may be translated; command IDs and numeric values never are. -## Tatsächlich gelesene Antworten (Firmware 3.11.0) +## Observed hardware responses: firmware 3.11.0 -| Anfrage | Antwort ohne Null-Padding | Interpretation | +| Query | Response without padding | Interpretation | |---|---|---| -| 06 | `34 FF 06 01 03` | Audio läuft | -| 01 | `34 FF 01 03 01 03 01` | Gaming, automatisch, verbunden per Classic | -| 03 | `34 FF 03 01 04` | aktuell aptX Adaptive angeboten | -| 05 | `34 FF 05 01 04` | aptX Adaptive aktiv | -| 07 | `34 FF 07 01 01` | LE getrennt | -| 08 | `34 FF 08 02 02 02` | 24 Bit / 48 kHz | -| 09 | `34 FF 09 03 01 02 00` | öffentlich, HQ, unverschlüsselt konfiguriert | +| 06 | `34 FF 06 01 03` | Audio playing | +| 01 | `34 FF 01 03 01 03 01` | Gaming, automatic, connected using Classic | +| 03 | `34 FF 03 01 04` | aptX Adaptive currently offered | +| 05 | `34 FF 05 01 04` | aptX Adaptive active | +| 07 | `34 FF 07 01 01` | LE disconnected | +| 08 | `34 FF 08 02 02 02` | 24-bit / 48 kHz | +| 09 | `34 FF 09 03 01 02 00` | Public discovery, HQ, no encryption configured | | 12 | `34 FF 12 03 03 0B 00` | Firmware 3.11.0 | -| 15 | `34 FF 15 01 01` | Kopfhörer unterstützt Classic | +| 15 | `34 FF 15 01 01` | Headphones support Classic | -Auch der Auracast-Name wurde erfolgreich gelesen. Private Kennungen/Schlüssel -werden hier nicht dokumentiert. Die verfügbare Audiokonfiguration belegt nicht, -dass Audio mit diesem Profil bitgenau übertragen wird. +The broadcast name was also read successfully. Private device identifiers and keys +are omitted. The reported audio configuration does not prove bit-perfect playback. -## Getrennte Update-Funktion und Grenzen +## Update exclusion and remaining limitations -Die App implementiert ausschließlich Report `0x34` auf der Kontrollschnittstelle. -Sie enthält keine DFU-/Upgrade-Kommandos, Firmwaredateien, Firmwareparser, -Firmware-Download-URLs oder Umschaltung in einen Update-Modus. -Die Firmwareversion wird nur über den Kontrollbefehl `0x12` gelesen. +Only control report `0x34` is implemented. The runtime has no DFU/upgrade commands, +firmware files, firmware parser, firmware-download endpoints or update-mode switch. +Firmware version is read using control command `0x12` solely for display. -Der Nutzer hat die Funktion der App am eigenen Dongle bestätigt. Der automatisierte -Hardware-Umschalttest wurde noch nicht ausgeführt; die Rückmeldung belegt keine -vollständige Prüfung jedes Schreibbefehls. Insbesondere Wiederverbindung, echte -Auracast-Empfänger und Firmwareunterschiede benötigen weitere systematische Tests. +The device owner reported that the app works. The automated hardware mutation test +has not been run, and that feedback is not a complete verification of every setter. +Reconnection, real Auracast receivers and differing firmware require more systematic +testing. The original app and simulator are not substitutes for device validation. -Infobereich: `org.kde.StatusNotifierItem` plus `com.canonical.dbusmenu` über -libdbusmenu. Grundlage ist die -[StatusNotifier-Spezifikation](https://specifications.freedesktop.org/status-notifier-item/latest-single/). -GNOME-Registrierung und D-Bus-Menüaktionen wurden lokal geprüft. Name/Passwort -verwenden fokussierte GTK-Eingaben, da D-Bus-Menüs keine Texteingabefelder vorsehen. +## Desktop tray + +`org.kde.StatusNotifierItem` with `com.canonical.dbusmenu`, exported using libdbusmenu, +following the [StatusNotifier specification](https://specifications.freedesktop.org/status-notifier-item/latest-single/). +Registration with GNOME and menu actions over D-Bus were tested locally in English +and German. Name/password actions focus the GTK editor because the menu protocol +does not provide text-entry fields. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index cf0ec0a..3c649f7 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,37 +1,53 @@ -# Validierung am 5. September 2026 +# Validation — 5 September 2026 -Getestetes System: Bazzite, GNOME, Python 3.14, GTK 4.22, libadwaita 1.9. -Angeschlossener BTD 700: USB `3542:3001`, Firmware 3.11.0. +This document separates observed hardware behavior from simulated tests. +The project was developed using AI (OpenAI Codex); these results are not a warranty. -- 21 Tests mit `python3 -m unittest discover -s tests -q`: bestanden. -- Fenster-/Infobereich-Test `python3 tools/check_gui.py`: bestanden, ausschließlich - mit einem simulierten Dongle. Aktionen wurden über `com.canonical.dbusmenu.Event` - ausgelöst, nicht nur direkt gegen Controller-Methoden. -- Geprüfte Demo-Abläufe: Standard/Gaming/Auracast, Codec, Transport, Trennen/Verbinden, - öffentliche Auffindbarkeit, Broadcastqualität, Name/Passwort speichern, - Passwortschutz ausschalten, Fenster schließen/aus Menü wieder öffnen, - ungespeicherte Eingaben bei Statusabfragen erhalten und verwerfen. -- Eigene Fensterbilder bei 620 × 800 und 420 × 600 Pixeln gerendert und visuell - geprüft. Oberfläche scrollbar, Bedienelemente erreichbar. Die zuvor überlange - Passwortbeschriftung wurde gekürzt und um einen sichtbaren Hinweis ergänzt. -- Echte Hardwareabfragen: erfolgreich, siehe PROTOCOL.md. Anzeige: - Gaming / aptX Adaptive / 24 Bit / 48 kHz / laufende Musikwiedergabe. -- Live-Symbol exportiert StatusNotifierItem mit aktuellem Tooltip und einem - D-Bus-Menü. GNOME-StatusNotifierWatcher nimmt die Registrierung an. -- App über persönlichen Desktop-Eintrag installierbar; Desktop-Datei validiert. - Dauerhafter Start in der Benutzersitzung mit transienter Unit - `btd700-control.service`; kein Systemdienst und kein aktivierter Autostart. -- Zweiter USB-Zugriff während laufender App: korrekt mit verständlicher Meldung - abgelehnt, ohne die laufende Instanz zu beeinträchtigen. -- Extraktionsskript gegen die offizielle Windows-EXE geprüft; SHA256 der - extrahierten Assembly entspricht der bei der Analyse verwendeten Datei. +## Environment -Der Nutzer hat anschließend bestätigt, dass die App am eigenen Dongle funktioniert. -Welche einzelnen Funktionen dabei getestet wurden, wurde nicht näher aufgeschlüsselt. +Bazzite / GNOME, Python 3.14, GTK 4.22, libadwaita 1.9. +Connected BTD 700: USB `3542:3001`, firmware 3.11.0. -**Noch offen:** automatisierte Hardware-Schreibtests sowie eine systematische -Prüfung mit Audio-/Auracast-Empfängern. `tools/check_hardware.py --run` wurde bisher -nicht ausgeführt. Dieser Test unterbricht kurz Audio und verändert vorübergehend -Auracast-Einstellungen einschließlich des Passworts; anschließend versucht er, -die ursprünglichen Werte wiederherzustellen. Ein Werksreset wird dabei nicht -ausgeführt; Firmware-Updates sind nicht implementiert. +## Observed on the real device + +- Correct control-interface discovery and opening without detaching audio drivers. +- Successful state, mode, codec, quality, transport, Auracast configuration/name + and firmware-version reads. See [PROTOCOL.md](PROTOCOL.md) for response bytes. +- Reported live state: Gaming, aptX Adaptive, 24-bit / 48 kHz, audio playing. +- Native GTK window and registered GNOME tray item with a live status tooltip. +- A second USB client is rejected with a clear message while the app owns the device. +- The device owner subsequently confirmed that the app works. The individual + functions exercised by the owner were not enumerated. + +## Automated verification + +- Protocol/controller/transport tests: command allowlist, malformed frames, response + correlation, event acknowledgements, readback, capability checks, input validation, + reset confirmation and password handling using a simulated device. +- Localization tests: English/German detection, explicit CLI override, message + placeholders, localized status/errors and unchanged numeric protocol data. +- Desktop integration tests: isolated installation/uninstallation and autostart + behavior without changing the real user's launchers. +- `tools/check_gui.py` exercised English and German windows and actual D-Bus menu + events against a demo device: modes, codec, transport, connection, broadcast + discovery/quality, name/password saving, encryption, closing/reopening and + preserving/discarding unsaved edits. +- Window layouts rendered and inspected at 620 × 800 and 420 × 600 pixels. +- README screenshots captured from the real English GTK window at 620 × 880 with + fictional demo data. No real USB access or generated mockups were used. +- Extraction helper output matched the SHA256 of the original assembly used for + protocol research. No vendor binaries were added to the distribution. + +The CI workflow runs the unit tests on Python 3.10 and 3.14, in both languages. +It does not run real hardware tests or claim that a particular receiver works. + +## Not yet systematically verified + +- Hardware writes across different firmware revisions and all individual commands. +- Real Auracast receiver compatibility, audio quality and reconnect behavior. +- Other distributions/desktops beyond the tested Bazzite/GNOME setup. + +`tools/check_hardware.py --run` has not been executed. It deliberately interrupts +audio, changes settings including the broadcast password, and attempts to restore +the original values. It does not perform a factory reset and cannot perform +firmware updates. Unplugging midway can prevent restoration. diff --git a/docs/screenshots/auracast-settings.png b/docs/screenshots/auracast-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..87c9406aa2c5f85251e3f131079e21b708ed9d6b GIT binary patch literal 77249 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 literal 0 HcmV?d00001 diff --git a/install.py b/install.py index 6482540..89185e3 100644 --- a/install.py +++ b/install.py @@ -1,13 +1,28 @@ #!/usr/bin/env python3 -"""Install a launcher for this checkout, without root or system packages.""" -from pathlib import Path +"""Install/remove a launcher for this checkout, without root or system packages.""" +import argparse import os -from btd700.integration import APP_ID, desktop_entry +from pathlib import Path +from btd700.i18n import tr +from btd700.integration import APP_ID, autostart_path, desktop_entry -base = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local/share')) -applications = base / 'applications' -applications.mkdir(parents=True, exist_ok=True) -destination = applications / f'{APP_ID}.desktop' -destination.write_text(desktop_entry()) -print(f'Installiert: {destination}') -print('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.') + +def main(): + parser = argparse.ArgumentParser(description='Add or remove the BTD 700 Control application-menu entry.') + parser.add_argument('--uninstall', action='store_true', help='Remove the launcher and start-at-login entry') + args = parser.parse_args() + base = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local/share')) + destination = base / 'applications' / f'{APP_ID}.desktop' + if args.uninstall: + destination.unlink(missing_ok=True) + autostart_path().unlink(missing_ok=True) + print(tr('Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.')) + return + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(desktop_entry()) + print(tr('Installiert: {path}').format(path=destination)) + print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.')) + + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml index 98450fb..2221c4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,28 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] name = "btd700-control" -version = "0.1.0" +version = "0.2.0" description = "Independent native Linux controls and tray menu for the Sennheiser BTD 700" requires-python = ">=3.10" readme = "README.md" +license = "MIT" +license-files = ["LICENSE", "THIRD_PARTY_NOTICES.md"] +authors = [{ name = "Justin Eckenweber" }] +keywords = ["sennheiser", "btd700", "linux", "bluetooth", "auracast", "gtk"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Sound/Audio", +] + +[project.urls] +Homepage = "https://github.com/justin-eckenweber/btd700linux" +Issues = "https://github.com/justin-eckenweber/btd700linux/issues" +Changelog = "https://github.com/justin-eckenweber/btd700linux/blob/main/CHANGELOG.md" [project.scripts] btd700-control = "btd700.__main__:main" diff --git a/tests/test_controller.py b/tests/test_controller.py index 70ed30b..d66e43c 100644 --- a/tests/test_controller.py +++ b/tests/test_controller.py @@ -1,4 +1,5 @@ import unittest +from btd700.i18n import tr from btd700.controller import Controller from btd700.demo import DemoTransport from btd700.protocol import Command as C @@ -25,7 +26,7 @@ class ControllerTests(unittest.TestCase): self.device.values.pop(C.GET_KEY) status = self.controller.snapshot() self.assertEqual(status.codec_name, 'aptX Adaptive') - self.assertEqual(status.quality, '24 Bit / 48 kHz') + self.assertEqual(status.quality, tr('24 Bit') + ' / 48 kHz') self.assertEqual(status.firmware, '3.11.0') self.assertEqual(self.device.writes, []) self.assertNotIn('password', status.public_dict()) diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..574b601 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,67 @@ +import json +import os +from pathlib import Path +import string +import subprocess +import sys +import unittest + +from btd700.controller import Controller +from btd700.demo import DemoTransport +from btd700.i18n import ENGLISH, detect_language, get_language, set_language, tr + +ROOT = Path(__file__).resolve().parents[1] + + +class TranslationTests(unittest.TestCase): + def setUp(self): + self.language = get_language() + + def tearDown(self): + set_language(self.language) + + def test_language_detection(self): + for env, expected in (({}, 'en'), ({'LANG': 'de_DE.UTF-8'}, 'de'), + ({'LANG': 'fr_FR.UTF-8'}, 'en'), + ({'LANGUAGE': 'de:en', 'LANG': 'en_GB.UTF-8'}, 'de'), + ({'BTD700_LANGUAGE': 'en', 'LANG': 'de_DE.UTF-8'}, 'en')): + self.assertEqual(detect_language(env), expected) + + def test_templates_keep_all_fields(self): + formatter = string.Formatter() + def fields(text): + return sorted((field, spec, conversion) for _, field, spec, conversion in formatter.parse(text) if field is not None) + for source, translated in ENGLISH.items(): + self.assertTrue(translated) + self.assertEqual(fields(source), fields(translated), source) + + def test_status_translates_without_changing_device_values(self): + controller = Controller(DemoTransport()) + set_language('en') + english = controller.snapshot().public_dict() + self.assertEqual(english['state_name'], 'Playing audio') + self.assertEqual(english['transport_name'], 'Automatic') + self.assertEqual(english['audio_quality'], '24-bit / 48 kHz') + set_language('de') + german = controller.snapshot().public_dict() + self.assertEqual(german['state_name'], 'Musikwiedergabe') + self.assertEqual(german['audio_quality'], '24 Bit / 48 kHz') + for field in ('mode', 'transport', 'codec', 'firmware', 'broadcast_name'): + self.assertEqual(english[field], german[field]) + + def test_english_error(self): + set_language('en') + with self.assertRaisesRegex(ValueError, 'Unknown audio mode'): + Controller(DemoTransport()).set_mode(255) + set_language('de') + self.assertEqual(tr('Speichern'), 'Speichern') + self.assertEqual(tr('aptX Adaptive'), 'aptX Adaptive') + + def test_cli_override_and_help(self): + env = dict(os.environ, BTD700_LANGUAGE='de') + command = [sys.executable, '-m', 'btd700', '--language', 'en'] + result = subprocess.run(command + ['--demo', 'status'], cwd=ROOT, env=env, check=True, text=True, capture_output=True) + self.assertEqual(json.loads(result.stdout)['state_name'], 'Playing audio') + result = subprocess.run(command + ['--help'], cwd=ROOT, env=env, check=True, text=True, capture_output=True) + self.assertIn('Start in the system tray only', result.stdout) + self.assertNotIn('Nur im Infobereich', result.stdout) diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..8b1fe81 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,46 @@ +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + +from btd700.integration import APP_ID, autostart_path, desktop_entry, set_autostart + +ROOT = Path(__file__).resolve().parents[1] + + +class IntegrationTests(unittest.TestCase): + def test_install_and_remove_only_own_entries(self): + with tempfile.TemporaryDirectory() as tmp: + env = dict(os.environ, XDG_DATA_HOME=tmp+'/data', XDG_CONFIG_HOME=tmp+'/config', BTD700_LANGUAGE='en') + desktop = Path(tmp) / 'data/applications' / f'{APP_ID}.desktop' + other = desktop.parent / 'other-app.desktop' + subprocess.run([sys.executable, 'install.py'], cwd=ROOT, env=env, check=True, capture_output=True) + self.assertIn('Comment=Linux controls', desktop.read_text()) + self.assertIn('Comment[de]=', desktop.read_text()) + other.write_text('do not remove') + with patch.dict(os.environ, env): + set_autostart(True) + startup = autostart_path() + self.assertTrue(startup.exists()) + self.assertIn('--background', startup.read_text()) + subprocess.run([sys.executable, 'install.py', '--uninstall'], cwd=ROOT, env=env, check=True, capture_output=True) + self.assertFalse(desktop.exists()) + self.assertFalse(startup.exists()) + self.assertEqual(other.read_text(), 'do not remove') + + def test_launcher_quotes_special_path_characters(self): + with patch('btd700.integration.ROOT', Path('/tmp/BTD "Control" $home 100%')): + with patch.object(Path, 'is_file', return_value=True): + text = desktop_entry() + self.assertIn('\\"Control\\"', text) + self.assertIn('\\$home', text) + self.assertIn('100%%', text) + + def test_installed_package_launcher_uses_python_module(self): + with patch.object(Path, 'is_file', return_value=False): + text = desktop_entry(background=True) + self.assertIn(' -m btd700 --background', text) + self.assertIn('Icon=audio-headphones', text) diff --git a/tools/capture_screenshots.py b/tools/capture_screenshots.py new file mode 100644 index 0000000..763de27 --- /dev/null +++ b/tools/capture_screenshots.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Capture the actual GTK application with fictional demo data, without USB access.""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from btd700.app import Application +from gi.repository import GLib, Gtk + +output = Path(__file__).resolve().parents[1] / 'docs/screenshots' +output.mkdir(parents=True, exist_ok=True) +app = Application(demo=True, language='en') +phase = 0 +attempts = 0 +success = False + + +def capture(name): + window = app.window + snapshot = Gtk.Snapshot.new() + Gtk.WidgetPaintable.new(window).snapshot(snapshot, window.get_width(), window.get_height()) + node = snapshot.to_node() + if node is None: + return False + texture = window.get_renderer().render_texture(node, None) + if not texture.save_to_png(str(output / name)): + raise RuntimeError('Could not save screenshot') + print(f'{name}: {window.get_width()} × {window.get_height()}', flush=True) + return True + + +def tick(): + global phase, attempts, success + attempts += 1 + try: + if attempts > 40: + raise TimeoutError('Screenshot window did not become ready') + if not app.status or app.busy: + return True + if phase == 0: + app.window.set_default_size(620, 880) + phase = 1 + elif phase == 1: + if capture('main-window.png'): + adjustment = app.scroll.get_vadjustment() + adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size()) + phase = 2 + elif phase == 2: + if capture('auracast-settings.png'): + success = True + app.quit() + return False + except Exception as exc: + print(f'Capture failed: {exc}', file=sys.stderr) + app.quit() + return False + return True + +GLib.timeout_add(500, tick) +app.run(['btd700-screenshots']) +raise SystemExit(0 if success else 1) diff --git a/tools/check_gui.py b/tools/check_gui.py index b3523f7..0c09e7a 100644 --- a/tools/check_gui.py +++ b/tools/check_gui.py @@ -4,6 +4,7 @@ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from btd700.app import Application +from btd700.i18n import tr from gi.repository import Gio, GLib, Gtk app = Application(demo=True) @@ -16,7 +17,7 @@ bus_pending = False def menu_item(labels): root = app.tray.root for label in labels: - root = next(item for item in root.get_children() if item.property_get('label') == label) + root = next(item for item in root.get_children() if item.property_get('label') == tr(label)) return root