Add English localization and MIT-licensed public release documentation
Tests / unit (de, 3.10) (push) Has been cancelled
Tests / unit (de, 3.14) (push) Has been cancelled
Tests / unit (en, 3.14) (push) Has been cancelled
Tests / unit (en, 3.10) (push) Has been cancelled

This commit is contained in:
Justin
2026-09-05 21:02:14 +02:00
parent 3579667c6e
commit dd0809e877
31 changed files with 1261 additions and 377 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""Independent Linux controls for the Sennheiser BTD 700."""
__version__ = "0.1.0"
__version__ = "0.2.0"
+25 -18
View File
@@ -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:
+49 -45
View File
@@ -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('416 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('416 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()
+21 -20
View File
@@ -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 416 Zeichen.')
raise ValueError(tr('Verschlüsselung benötigt ein Passwort mit 416 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 416 Zeichen eingeben.')
raise ValueError(tr('Bitte zuerst ein Passwort mit 416 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)
+2 -1
View File
@@ -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):
+157
View File
@@ -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',
'416 Buchstaben oder Ziffern. Leer lassen behält das bisherige Passwort.': '416 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 416 Zeichen.': 'Encryption requires a password of 416 characters.',
'Bitte zuerst ein Passwort mit 416 Zeichen eingeben.': 'Enter a password of 416 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: 416 Buchstaben oder Ziffern.': 'Password: 416 letters or digits.',
'Name: 416 Buchstaben, Ziffern oder innere Leerzeichen.': 'Name: 416 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)',
}
+2 -1
View File
@@ -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'
+7 -6
View File
@@ -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 416 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: 416 Buchstaben oder Ziffern.' if key else (
'Name: 416 Buchstaben, Ziffern oder innere Leerzeichen.')
what = tr('Passwort: 416 Buchstaben oder Ziffern.') if key else (
tr('Name: 416 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]])
+10 -9
View File
@@ -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
+19 -18
View File
@@ -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)
+3 -2
View File
@@ -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: