# Architecture The protocol module has no Bluetooth or GUI dependencies. Tk exists only in `controller/gui.py`, which `cli.py` imports after parsing arguments. Importing the package or running `--help` does not load Tk or Bleak. | Module | Responsibility | | --- | --- | | `protocol.py` | Packet encoding, stream framing, observed checksum variants | | `commands.py` | Semantic command builders; no packet layout in Tk callbacks | | `session.py` | Handshake, outgoing sequence numbers, connection readiness | | `messages.py` | English command/reply descriptions and bounded activity history | | `worker.py` | Thread lifecycle and event/command queues | | `ble_link.py` | Bleak connection, notifications, characteristic writes | | `link.py`, `transport.py` | Experimental RFCOMM and serial adapters | | `gui.py` | Tk controls and event rendering | | `discovery.py` | Read-only BLE discovery, unnamed model matching, and selectable labels | | `cli.py`, `__main__.py` | Installed command and backwards-compatible module entry | | `ble_probe.py`, `probe.py` | Endpoint diagnostics | | `windows_bluetooth.py` | Experimental RFCOMM initialization | ## Session interface Create one `Session` per connection. Write `session.start()` to begin. Pass received chunks to `session.receive(chunk)` and write every returned `outgoing` packet. `Update.frames` carries decoded packets; `became_ready` is emitted once when the speaker acknowledges the link-complete request. Use `session.command(payload)` for subsequent outgoing packets. Both transports cross this same seam. The session owns framing and the state machine, so fixes do not need to be repeated in separate Bluetooth loops. A transport connection, a GATT write response, a Samsung packet acknowledgment, and a speaker state report are distinct events. Only the link-complete ACK makes the session ready. Speaker notifications have an independent sequence counter and cannot substitute for that ACK. ## Threads and ownership Tk runs on the main thread. Each connection worker owns its transport. BLE's async event loop lives entirely within the worker thread. The GUI submits payloads through a bounded command queue and consumes events through another queue; workers never call Tk methods. Discovery uses a separate bounded-duration worker and the same event queue. It returns plain speaker records without opening a GATT connection. Tk maps a selected label to its address; manual addresses remain supported. Scanning is disabled while a connection is active so scans do not compete with BLE traffic. BLE notifications enter a bounded async queue and can contain partial or combined packets. The decoder retains framing state between notifications. Queue overflow terminates the connection rather than silently losing framing. Writes are spaced and never automatically retried. Retrying a volume or mute key could perform the action twice. The GUI enables controls only after the protocol handshake and disables them on connection closure. ## Testing Protocol tests use captured byte fixtures independently of the outgoing encoder. Session tests cover split replies, duplicate replies, and ACK matching. Transport tests verify subscription order, write mode, disconnects, resource cleanup, and readable error phases. Presentation tests verify the difference between acknowledgment and a reported state. No test opens physical Bluetooth.