feat(ui): detect USB-CDC connection for Auto mute (BLE or USB)

USB client presence IS detectable on nRF52 after all: (bool)Serial ==
tud_cdc_n_connected() (DTR — a host has the CDC port open). Add
isClientConnected() = BLE-bonded OR USB-CDC-open; DualSerialInterface
overrides it, base defaults to isConnected() (single-transport unchanged).

Wire it so each consumer gets the right signal:
- hasConnection() ← isClientConnected(): Auto buzzer mute + message-wake now
  trigger on BLE or an open USB port (PR #14's intent, done correctly), but
  not on charging-only (no host → DTR low).
- BT status indicator + pairing PIN stay on isBLEConnected() (BLE-specific).

Caveat: a plain serial monitor also asserts DTR, so it counts as connected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-05 12:36:49 +02:00
parent 4ea67ee950
commit 3b795f7abc
4 changed files with 16 additions and 7 deletions

View File

@@ -2497,12 +2497,11 @@ void MyMesh::loop() {
}
#ifdef DISPLAY_CLASS
// Use BLE-bonded state, not isConnected(): on a dual (BLE+USB) interface
// isConnected() is hardcoded true (USB is always a send fallback), and USB
// client presence isn't detectable anyway. hasConnection() drives the BT
// status indicator, message-wake and the Auto buzzer mute — all of which
// mean "a companion app is connected", i.e. BLE bonded.
if (_ui) _ui->setHasConnection(_serial->isBLEConnected());
// "a companion app is connected over any transport" — BLE bonded or a host
// holding the USB-CDC port open. Drives Auto buzzer mute + message-wake.
// (Not isConnected(): a dual interface hardcodes that true as a send
// fallback. The BT status indicator and pairing PIN use isBLEConnected().)
if (_ui) _ui->setHasConnection(_serial->isClientConnected());
#endif
}

View File

@@ -456,7 +456,7 @@ class HomeScreen : public UIScreen {
int leftmostX = battLeftX;
if (_task->isSerialEnabled()) {
int btX = battLeftX - ind - ind_gap;
if (_task->hasConnection()) {
if (_task->isBLEConnected()) { // BT icon reflects BLE link, not USB
display.setColor(DisplayDriver::LIGHT);
display.fillRect(btX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK);

View File

@@ -18,6 +18,12 @@ public:
// isConnected(); override in dual-interface wrappers that always report
// isConnected()=true but still need to distinguish BLE from USB state.
virtual bool isBLEConnected() const { return isConnected(); }
// True when a companion app is actually connected over *any* transport
// (BLE bonded or a host holding the USB-CDC port open). Distinct from
// isConnected(), which a dual interface hardcodes true as a send fallback.
// Used for app-connected UI (Auto buzzer mute, message-wake). Default =
// isConnected() so single-transport interfaces are unchanged.
virtual bool isClientConnected() const { return isConnected(); }
virtual bool isWriteBusy() const = 0;
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;

View File

@@ -33,6 +33,10 @@ public:
bool isConnected() const override { return true; }
// True only when a BLE companion app is paired and connected.
bool isBLEConnected() const override { return _ble_enabled && _ble.isConnected(); }
// A companion app is actually connected if BLE is bonded OR a host holds the
// USB-CDC port open. (bool)Serial == tud_cdc_n_connected() (DTR asserted) — a
// serial monitor counts too, but charging-only (no host) does not.
bool isClientConnected() const override { return isBLEConnected() || (bool)Serial; }
bool isWriteBusy() const override {
return (_ble_enabled && _ble.isConnected()) ? _ble.isWriteBusy() : _usb.isWriteBusy();