Add in-app speaker discovery and connection guide
This commit is contained in:
7 files changed
+287
-28
No files matched your search
+79
-9
@@ -6,6 +6,7 @@ from tkinter import colorchooser, ttk
|
||||
|
||||
from controller import commands
|
||||
from controller.ble_link import BLELink
|
||||
from controller.discovery import ScanWorker, selected_address
|
||||
from controller.link import Link
|
||||
from controller.messages import ActivityLog, describe_reply
|
||||
from controller.protocol import EQ, LIGHTS
|
||||
@@ -21,8 +22,10 @@ class App:
|
||||
self.prepare = prepare
|
||||
self.transport = transport
|
||||
self.activity = ActivityLog()
|
||||
self.discovered = []
|
||||
self.scanning = False
|
||||
root.title("MX-T40 · Sound Tower")
|
||||
root.minsize(480, 640)
|
||||
root.minsize(520, 640)
|
||||
root.protocol("WM_DELETE_WINDOW", self.close)
|
||||
style = ttk.Style()
|
||||
if "vista" in style.theme_names():
|
||||
@@ -36,15 +39,27 @@ class App:
|
||||
)
|
||||
connection = ttk.Frame(outer)
|
||||
connection.pack(fill="x")
|
||||
ttk.Label(connection, text="Address / port").pack(side="left")
|
||||
self.port = ttk.Combobox(connection, width=20)
|
||||
connection.columnconfigure(0, weight=1)
|
||||
ttk.Label(connection, text="Speaker" if transport == "ble" else "Address / port").grid(
|
||||
row=0, column=0, sticky="w"
|
||||
)
|
||||
self.port = ttk.Combobox(connection, width=30)
|
||||
self.port.set(address)
|
||||
self.port.pack(side="left", padx=8)
|
||||
ttk.Button(connection, text="Refresh", command=self.refresh).pack(side="left")
|
||||
self.port.grid(row=1, column=0, sticky="ew", padx=(0, 8))
|
||||
self.scan_button = ttk.Button(
|
||||
connection, text="Scan" if transport == "ble" else "Refresh", command=self.refresh
|
||||
)
|
||||
self.scan_button.grid(row=1, column=1, padx=(0, 8))
|
||||
self.connect = ttk.Button(connection, text="Connect", command=self.toggle)
|
||||
self.connect.pack(side="right")
|
||||
self.connect.grid(row=1, column=2)
|
||||
if transport == "ble":
|
||||
ttk.Label(
|
||||
connection,
|
||||
text="Turn the speaker on → Scan → choose it → Connect. Or enter its Bluetooth address.",
|
||||
wraplength=440,
|
||||
).grid(row=2, column=0, columnspan=3, sticky="w", pady=(8, 0))
|
||||
self.status = tk.StringVar(
|
||||
value="Enter the speaker’s Bluetooth address. Using Samsung BLE control."
|
||||
value="Click Scan to find nearby speakers. Close the phone app if the speaker does not appear."
|
||||
if transport == "ble"
|
||||
else f"Enter the speaker’s Bluetooth address. Direct RFCOMM channel: {channel}."
|
||||
)
|
||||
@@ -94,7 +109,8 @@ class App:
|
||||
)
|
||||
self.log.pack(fill="both", expand=True, pady=(12, 0))
|
||||
self.enable(False)
|
||||
self.refresh()
|
||||
if transport != "ble":
|
||||
self.refresh()
|
||||
root.after(60, self.poll)
|
||||
|
||||
def selection(self, label, values, build):
|
||||
@@ -120,6 +136,16 @@ class App:
|
||||
visit(self.controls)
|
||||
|
||||
def refresh(self):
|
||||
if self.link or self.scanning:
|
||||
return
|
||||
if self.transport == "ble":
|
||||
self.scanning = True
|
||||
self.scan_button.configure(text="Scanning…", state="disabled")
|
||||
self.connect.configure(state="disabled")
|
||||
self.port.configure(state="disabled")
|
||||
self.status.set("Searching for nearby speakers…")
|
||||
ScanWorker(self.events).start()
|
||||
return
|
||||
try:
|
||||
from serial.tools import list_ports
|
||||
|
||||
@@ -139,8 +165,13 @@ class App:
|
||||
self.status.set("Disconnecting…")
|
||||
return
|
||||
port = self.port.get().strip()
|
||||
port = selected_address(port, self.discovered)
|
||||
if not port:
|
||||
self.status.set("Enter the MX-T40 Bluetooth address, or select its outgoing COM port.")
|
||||
self.status.set(
|
||||
"Click Scan and choose your speaker, or enter its Bluetooth address."
|
||||
if self.transport == "ble"
|
||||
else "Enter the Bluetooth address or outgoing COM port."
|
||||
)
|
||||
return
|
||||
if self.transport == "ble":
|
||||
self.link = BLELink(port, self.events)
|
||||
@@ -148,6 +179,7 @@ class App:
|
||||
self.link = Link(port, self.events, self.channel, self.prepare)
|
||||
self.activity.pending.clear()
|
||||
self.port.configure(state="disabled")
|
||||
self.scan_button.configure(state="disabled")
|
||||
self.connect.configure(text="Disconnect")
|
||||
self.status.set("Connecting to speaker…")
|
||||
self.link.start()
|
||||
@@ -179,6 +211,43 @@ class App:
|
||||
kind, value = self.events.get_nowait()
|
||||
if kind in ("status", "error"):
|
||||
self.status.set(value)
|
||||
elif kind in ("scan_results", "scan_error"):
|
||||
self.scanning = False
|
||||
self.scan_button.configure(text="Scan", state="normal")
|
||||
self.connect.configure(state="normal")
|
||||
self.port.configure(state="normal")
|
||||
if kind == "scan_error":
|
||||
self.status.set(
|
||||
f"Scan failed: {value}. Check Windows Bluetooth is on and try again."
|
||||
)
|
||||
continue
|
||||
previous = selected_address(self.port.get(), self.discovered)
|
||||
self.discovered = value
|
||||
self.port.configure(values=[speaker.label for speaker in value])
|
||||
matching = next(
|
||||
(
|
||||
speaker
|
||||
for speaker in value
|
||||
if speaker.address.casefold() == previous.casefold()
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching:
|
||||
self.port.set(matching.label)
|
||||
elif len(value) == 1 and not previous:
|
||||
self.port.set(value[0].label)
|
||||
elif previous:
|
||||
# Preserve a manual address or a prior device that is
|
||||
# temporarily absent; never silently select another.
|
||||
self.port.set(previous)
|
||||
if value:
|
||||
self.status.set(
|
||||
f"Found {len(value)} speaker(s). Choose yours, then Connect."
|
||||
)
|
||||
else:
|
||||
self.status.set(
|
||||
"No speaker found. Turn it on, close the phone app, and scan again. A manual address also works."
|
||||
)
|
||||
elif kind == "ready":
|
||||
self.ready = True
|
||||
self.enable(True)
|
||||
@@ -189,6 +258,7 @@ class App:
|
||||
self.enable(False)
|
||||
self.connect.configure(text="Connect", state="normal")
|
||||
self.port.configure(state="normal")
|
||||
self.scan_button.configure(state="normal")
|
||||
if not self.status.get().startswith(("No ", "Malformed", "could", "Install")):
|
||||
self.status.set("Disconnected. " + self.status.get())
|
||||
elif kind == "payload":
|
||||
|
||||
Reference in new issue
Block a user