fix: improve Bluetooth reliability
This commit is contained in:
@@ -56,7 +56,10 @@ namespace montauk::abi {
|
|||||||
(Drivers::Audio::Mixer::Output)value);
|
(Drivers::Audio::Mixer::Output)value);
|
||||||
if (cmd == AUDIO_CTL_BT_STATUS) {
|
if (cmd == AUDIO_CTL_BT_STATUS) {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
|
||||||
return (int64_t)Drivers::USB::Bluetooth::A2dp::GetState();
|
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
|
||||||
|
return Drivers::USB::Bluetooth::A2dp::GetState()
|
||||||
|
== Drivers::USB::Bluetooth::A2dp::State::Idle
|
||||||
|
? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int64_t)Drivers::Audio::Mixer::Control(handle, cmd, value);
|
return (int64_t)Drivers::Audio::Mixer::Control(handle, cmd, value);
|
||||||
|
|||||||
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 30
|
#define MONTAUK_BUILD_NUMBER 33
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ namespace montauk::abi {
|
|||||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
|
static constexpr int AUDIO_CTL_BT_STATUS = 6; // 0=unavailable, 1=setup, 2=ready
|
||||||
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
||||||
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
||||||
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "IntelHda.hpp"
|
#include "IntelHda.hpp"
|
||||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||||
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
#include <Drivers/USB/Bluetooth/A2dp.hpp>
|
||||||
|
#include <Drivers/USB/Bluetooth/Avrcp.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <Memory/HHDM.hpp>
|
#include <Memory/HHDM.hpp>
|
||||||
#include <Sched/Scheduler.hpp>
|
#include <Sched/Scheduler.hpp>
|
||||||
@@ -142,10 +143,7 @@ namespace Drivers::Audio::Mixer {
|
|||||||
|
|
||||||
static bool BluetoothReady() {
|
static bool BluetoothReady() {
|
||||||
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
if (!Drivers::USB::Bluetooth::IsInitialized()) return false;
|
||||||
auto state = Drivers::USB::Bluetooth::A2dp::GetState();
|
return Drivers::USB::Bluetooth::A2dp::IsReady();
|
||||||
return state == Drivers::USB::Bluetooth::A2dp::State::Configured ||
|
|
||||||
state == Drivers::USB::Bluetooth::A2dp::State::Open ||
|
|
||||||
state == Drivers::USB::Bluetooth::A2dp::State::Streaming;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void SyncHdaMasterMute() {
|
static void SyncHdaMasterMute() {
|
||||||
@@ -664,6 +662,8 @@ namespace Drivers::Audio::Mixer {
|
|||||||
if (changed) BumpSerialLocked();
|
if (changed) BumpSerialLocked();
|
||||||
g_lock.Release();
|
g_lock.Release();
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
Drivers::USB::Bluetooth::Avrcp::NotifyVolumeChanged(percent);
|
||||||
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
if (changed) Sched::WakeObjectWaiters((void*)&g_serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,11 +32,22 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
// handshakes), NOT from an event/interrupt path. Leaves the stream Open.
|
// handshakes), NOT from an event/interrupt path. Leaves the stream Open.
|
||||||
bool StartSource(uint32_t timeoutMs = 5000);
|
bool StartSource(uint32_t timeoutMs = 5000);
|
||||||
|
|
||||||
|
// Reset all per-link AVDTP state for a newly established ACL connection.
|
||||||
|
// This is deliberately separate from StartSource(): the peer may open its
|
||||||
|
// AVDTP channels during authentication/the post-encryption settle window,
|
||||||
|
// and StartSource must preserve those already-live inbound channels.
|
||||||
|
void OnConnected(uint16_t aclHandle);
|
||||||
|
|
||||||
// Called by L2CAP when an AVDTP channel becomes ready
|
// Called by L2CAP when an AVDTP channel becomes ready
|
||||||
void OnChannelReady(uint16_t l2capCid);
|
void OnChannelReady(uint16_t l2capCid);
|
||||||
|
|
||||||
// Process an AVDTP signaling packet
|
// Process an AVDTP signaling packet
|
||||||
void ProcessAvdtp(const uint8_t* data, uint16_t len);
|
void ProcessAvdtp(uint16_t localCid, const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
|
// L2CAP channel teardown can happen while the ACL link remains alive.
|
||||||
|
// Clear stale signaling/media CIDs so subsequent writes do not target a
|
||||||
|
// dead channel and a manual reconnect can rebuild the A2DP path.
|
||||||
|
void OnChannelClosed(uint16_t localCid);
|
||||||
|
|
||||||
// Process an SDP packet (on any SDP L2CAP channel). Dispatches by PDU:
|
// Process an SDP packet (on any SDP L2CAP channel). Dispatches by PDU:
|
||||||
// requests (the headset querying OUR services) are answered by the minimal
|
// requests (the headset querying OUR services) are answered by the minimal
|
||||||
@@ -80,6 +91,10 @@ namespace Drivers::USB::Bluetooth::A2dp {
|
|||||||
// Get current state
|
// Get current state
|
||||||
State GetState();
|
State GetState();
|
||||||
|
|
||||||
|
// True only when signaling and media transports are both live and the SEP
|
||||||
|
// has reached a state from which audio can be started.
|
||||||
|
bool IsReady();
|
||||||
|
|
||||||
// Check if currently streaming
|
// Check if currently streaming
|
||||||
bool IsStreaming();
|
bool IsStreaming();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <Drivers/Audio/Mixer.hpp>
|
#include <Drivers/Audio/Mixer.hpp>
|
||||||
#include <Terminal/Terminal.hpp>
|
#include <Terminal/Terminal.hpp>
|
||||||
#include <CppLib/Stream.hpp>
|
#include <CppLib/Stream.hpp>
|
||||||
|
#include <CppLib/Spinlock.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
@@ -28,6 +29,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
constexpr uint8_t AVC_RSP_ACCEPTED = 0x9;
|
constexpr uint8_t AVC_RSP_ACCEPTED = 0x9;
|
||||||
constexpr uint8_t AVC_RSP_REJECTED = 0xA;
|
constexpr uint8_t AVC_RSP_REJECTED = 0xA;
|
||||||
constexpr uint8_t AVC_RSP_STABLE = 0xC;
|
constexpr uint8_t AVC_RSP_STABLE = 0xC;
|
||||||
|
constexpr uint8_t AVC_RSP_CHANGED = 0xD;
|
||||||
constexpr uint8_t AVC_RSP_INTERIM = 0xF;
|
constexpr uint8_t AVC_RSP_INTERIM = 0xF;
|
||||||
|
|
||||||
// AV/C opcodes
|
// AV/C opcodes
|
||||||
@@ -43,33 +45,35 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
constexpr uint8_t PDU_SET_ABS_VOLUME = 0x50;
|
constexpr uint8_t PDU_SET_ABS_VOLUME = 0x50;
|
||||||
|
|
||||||
// AVRCP notification events
|
// AVRCP notification events
|
||||||
constexpr uint8_t EVT_PLAYBACK_STATUS = 0x01;
|
|
||||||
constexpr uint8_t EVT_TRACK_CHANGED = 0x02;
|
|
||||||
constexpr uint8_t EVT_VOLUME_CHANGED = 0x0D;
|
constexpr uint8_t EVT_VOLUME_CHANGED = 0x0D;
|
||||||
|
|
||||||
|
static kcp::Spinlock g_notifyLock;
|
||||||
|
static uint16_t g_volumeNotifyCid = 0;
|
||||||
|
static uint8_t g_volumeNotifyTransaction = 0;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Send helpers
|
// Send helpers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
// AVCTP single-packet response: byte0 = transaction<<4 | pktType<<2 |
|
// AVCTP single-packet response: byte0 = transaction<<4 | pktType<<2 |
|
||||||
// C/R(1=response)<<1 | IPID; bytes 1-2 = PID big-endian.
|
// C/R(1=response)<<1 | IPID; bytes 1-2 = PID big-endian.
|
||||||
static void SendAvctp(uint16_t cid, uint8_t transaction, uint16_t pid,
|
static bool SendAvctp(uint16_t cid, uint8_t transaction, uint16_t pid,
|
||||||
bool invalidPid, const uint8_t* avc, uint16_t avcLen) {
|
bool invalidPid, const uint8_t* avc, uint16_t avcLen) {
|
||||||
uint8_t buf[96] = {};
|
uint8_t buf[96] = {};
|
||||||
if (3u + avcLen > sizeof(buf)) return;
|
if (3u + avcLen > sizeof(buf)) return false;
|
||||||
buf[0] = (uint8_t)((transaction << 4) | 0x02 | (invalidPid ? 0x01 : 0));
|
buf[0] = (uint8_t)((transaction << 4) | 0x02 | (invalidPid ? 0x01 : 0));
|
||||||
buf[1] = (uint8_t)(pid >> 8);
|
buf[1] = (uint8_t)(pid >> 8);
|
||||||
buf[2] = (uint8_t)(pid & 0xFF);
|
buf[2] = (uint8_t)(pid & 0xFF);
|
||||||
if (avc && avcLen) memcpy(&buf[3], avc, avcLen);
|
if (avc && avcLen) memcpy(&buf[3], avc, avcLen);
|
||||||
L2cap::SendData(cid, buf, (uint16_t)(3 + avcLen));
|
return L2cap::SendData(cid, buf, (uint16_t)(3 + avcLen));
|
||||||
}
|
}
|
||||||
|
|
||||||
// AVRCP vendor-dependent response frame:
|
// AVRCP vendor-dependent response frame:
|
||||||
// [rsp][subunit 0x48 panel][opcode 0x00][company 00 19 58][pdu][pkt 0][len BE][params]
|
// [rsp][subunit 0x48 panel][opcode 0x00][company 00 19 58][pdu][pkt 0][len BE][params]
|
||||||
static void SendVendorRsp(uint16_t cid, uint8_t transaction, uint8_t rspCode,
|
static bool SendVendorRsp(uint16_t cid, uint8_t transaction, uint8_t rspCode,
|
||||||
uint8_t pdu, const uint8_t* p, uint16_t n) {
|
uint8_t pdu, const uint8_t* p, uint16_t n) {
|
||||||
uint8_t avc[64] = {};
|
uint8_t avc[64] = {};
|
||||||
if (10u + n > sizeof(avc)) return;
|
if (10u + n > sizeof(avc)) return false;
|
||||||
avc[0] = rspCode;
|
avc[0] = rspCode;
|
||||||
avc[1] = 0x48; // PANEL subunit
|
avc[1] = 0x48; // PANEL subunit
|
||||||
avc[2] = AVC_OP_VENDOR;
|
avc[2] = AVC_OP_VENDOR;
|
||||||
@@ -79,7 +83,8 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
avc[8] = (uint8_t)(n >> 8);
|
avc[8] = (uint8_t)(n >> 8);
|
||||||
avc[9] = (uint8_t)(n & 0xFF);
|
avc[9] = (uint8_t)(n & 0xFF);
|
||||||
if (p && n) memcpy(&avc[10], p, n);
|
if (p && n) memcpy(&avc[10], p, n);
|
||||||
SendAvctp(cid, transaction, 0x110E, false, avc, (uint16_t)(10 + n));
|
return SendAvctp(cid, transaction, 0x110E, false,
|
||||||
|
avc, (uint16_t)(10 + n));
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -126,14 +131,13 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case AVC_OP_PASSTHROUGH: {
|
case AVC_OP_PASSTHROUGH: {
|
||||||
// Accept every pass-through (play/pause/etc.). Echo the frame
|
// No media-session control bridge exists yet. Claiming these
|
||||||
// with the response code swapped in. Wiring the operation ids
|
// commands were ACCEPTED made headset buttons appear broken and
|
||||||
// to the Music app is a later feature; ACCEPTED keeps the
|
// left the controller believing an action had happened.
|
||||||
// controller-side state machine happy.
|
|
||||||
uint8_t rsp[16] = {};
|
uint8_t rsp[16] = {};
|
||||||
uint16_t cp = (alen > sizeof(rsp)) ? (uint16_t)sizeof(rsp) : alen;
|
uint16_t cp = (alen > sizeof(rsp)) ? (uint16_t)sizeof(rsp) : alen;
|
||||||
memcpy(rsp, avc, cp);
|
memcpy(rsp, avc, cp);
|
||||||
rsp[0] = AVC_RSP_ACCEPTED;
|
rsp[0] = AVC_RSP_NOT_IMPL;
|
||||||
SendAvctp(localCid, transaction, pid, false, rsp, cp);
|
SendAvctp(localCid, transaction, pid, false, rsp, cp);
|
||||||
if (alen >= 4) {
|
if (alen >= 4) {
|
||||||
KernelLogStream(INFO, "BT-AVRCP") << "pass-through op="
|
KernelLogStream(INFO, "BT-AVRCP") << "pass-through op="
|
||||||
@@ -156,7 +160,12 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
uint8_t pdu = avc[6];
|
uint8_t pdu = avc[6];
|
||||||
uint16_t plen = ((uint16_t)avc[8] << 8) | avc[9];
|
uint16_t plen = ((uint16_t)avc[8] << 8) | avc[9];
|
||||||
const uint8_t* p = &avc[10];
|
const uint8_t* p = &avc[10];
|
||||||
if (10u + plen > alen) plen = (uint16_t)(alen - 10);
|
if (10u + plen > alen) {
|
||||||
|
uint8_t err = 0x01; // invalid parameter
|
||||||
|
SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED,
|
||||||
|
pdu, &err, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (pdu == PDU_GET_CAPABILITIES && ctype == AVC_CTYPE_STATUS && plen >= 1) {
|
if (pdu == PDU_GET_CAPABILITIES && ctype == AVC_CTYPE_STATUS && plen >= 1) {
|
||||||
if (p[0] == 0x02) { // CompanyID list
|
if (p[0] == 0x02) { // CompanyID list
|
||||||
@@ -164,8 +173,7 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
SendVendorRsp(localCid, transaction, AVC_RSP_STABLE,
|
SendVendorRsp(localCid, transaction, AVC_RSP_STABLE,
|
||||||
pdu, rp, sizeof(rp));
|
pdu, rp, sizeof(rp));
|
||||||
} else if (p[0] == 0x03) { // EventsSupported
|
} else if (p[0] == 0x03) { // EventsSupported
|
||||||
uint8_t rp[5] = {0x03, 0x03, EVT_PLAYBACK_STATUS,
|
uint8_t rp[3] = {0x03, 0x01, EVT_VOLUME_CHANGED};
|
||||||
EVT_TRACK_CHANGED, EVT_VOLUME_CHANGED};
|
|
||||||
SendVendorRsp(localCid, transaction, AVC_RSP_STABLE,
|
SendVendorRsp(localCid, transaction, AVC_RSP_STABLE,
|
||||||
pdu, rp, sizeof(rp));
|
pdu, rp, sizeof(rp));
|
||||||
} else {
|
} else {
|
||||||
@@ -181,22 +189,18 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
pdu, rp, sizeof(rp));
|
pdu, rp, sizeof(rp));
|
||||||
} else if (pdu == PDU_REGISTER_NOTIFY && ctype == AVC_CTYPE_NOTIFY && plen >= 1) {
|
} else if (pdu == PDU_REGISTER_NOTIFY && ctype == AVC_CTYPE_NOTIFY && plen >= 1) {
|
||||||
// INTERIM response with the current value. (A CHANGED
|
// INTERIM response with the current value. (A CHANGED
|
||||||
// follow-up on actual change is a later feature.)
|
// response is sent by NotifyVolumeChanged; registrations
|
||||||
|
// are one-shot as required by AVRCP.)
|
||||||
if (p[0] == EVT_VOLUME_CHANGED) {
|
if (p[0] == EVT_VOLUME_CHANGED) {
|
||||||
uint8_t rp[2] = {EVT_VOLUME_CHANGED,
|
uint8_t rp[2] = {EVT_VOLUME_CHANGED,
|
||||||
(uint8_t)((Drivers::Audio::Mixer::GetMasterVolume() * 127) / 100)};
|
(uint8_t)((Drivers::Audio::Mixer::GetMasterVolume() * 127) / 100)};
|
||||||
SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
|
g_notifyLock.Acquire();
|
||||||
pdu, rp, sizeof(rp));
|
if (SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
|
||||||
} else if (p[0] == EVT_PLAYBACK_STATUS) {
|
pdu, rp, sizeof(rp))) {
|
||||||
uint8_t rp[2] = {EVT_PLAYBACK_STATUS,
|
g_volumeNotifyCid = localCid;
|
||||||
(uint8_t)(A2dp::IsStreaming() ? 0x01 : 0x00)};
|
g_volumeNotifyTransaction = transaction;
|
||||||
SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
|
}
|
||||||
pdu, rp, sizeof(rp));
|
g_notifyLock.Release();
|
||||||
} else if (p[0] == EVT_TRACK_CHANGED) {
|
|
||||||
uint8_t rp[9] = {EVT_TRACK_CHANGED,
|
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
|
||||||
SendVendorRsp(localCid, transaction, AVC_RSP_INTERIM,
|
|
||||||
pdu, rp, sizeof(rp));
|
|
||||||
} else {
|
} else {
|
||||||
uint8_t err = 0x01;
|
uint8_t err = 0x01;
|
||||||
SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED,
|
SendVendorRsp(localCid, transaction, AVC_RSP_REJECTED,
|
||||||
@@ -228,4 +232,28 @@ namespace Drivers::USB::Bluetooth::Avrcp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NotifyVolumeChanged(int percent) {
|
||||||
|
if (percent < 0) percent = 0;
|
||||||
|
if (percent > 100) percent = 100;
|
||||||
|
|
||||||
|
g_notifyLock.Acquire();
|
||||||
|
uint16_t cid = g_volumeNotifyCid;
|
||||||
|
uint8_t transaction = g_volumeNotifyTransaction;
|
||||||
|
g_volumeNotifyCid = 0; // one-shot, regardless of transport outcome
|
||||||
|
if (cid != 0) {
|
||||||
|
uint8_t rp[2] = {
|
||||||
|
EVT_VOLUME_CHANGED, (uint8_t)((percent * 127) / 100)
|
||||||
|
};
|
||||||
|
SendVendorRsp(cid, transaction, AVC_RSP_CHANGED,
|
||||||
|
PDU_REGISTER_NOTIFY, rp, sizeof(rp));
|
||||||
|
}
|
||||||
|
g_notifyLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnChannelClosed(uint16_t localCid) {
|
||||||
|
g_notifyLock.Acquire();
|
||||||
|
if (g_volumeNotifyCid == localCid) g_volumeNotifyCid = 0;
|
||||||
|
g_notifyLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,18 @@
|
|||||||
namespace Drivers::USB::Bluetooth::Avrcp {
|
namespace Drivers::USB::Bluetooth::Avrcp {
|
||||||
|
|
||||||
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
// Process an AVCTP packet arriving on an AVRCP control channel (PSM 0x0017).
|
||||||
// Implements a minimal AVRCP Target: unit/subunit info, pass-through accept,
|
// Implements a minimal AVRCP Target: unit/subunit info, honest rejection of
|
||||||
// GetCapabilities, RegisterNotification (interim), absolute volume. Runs
|
// unimplemented pass-through controls, capabilities, one-shot absolute-
|
||||||
|
// volume notification, and absolute-volume control. Runs
|
||||||
// from the ACL receive path (DrainEvents, top-level) -- sending is safe
|
// from the ACL receive path (DrainEvents, top-level) -- sending is safe
|
||||||
// there, blocking waits are not (none are used).
|
// there, blocking waits are not (none are used).
|
||||||
void ProcessAvctp(uint16_t localCid, const uint8_t* data, uint16_t len);
|
void ProcessAvctp(uint16_t localCid, const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
|
// Complete an outstanding EVENT_VOLUME_CHANGED registration. AVRCP
|
||||||
|
// notifications are one-shot: after CHANGED the controller registers again.
|
||||||
|
void NotifyVolumeChanged(int percent);
|
||||||
|
|
||||||
|
// Drop registrations tied to a channel that L2CAP has torn down.
|
||||||
|
void OnChannelClosed(uint16_t localCid);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,58 +157,45 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Intel Bluetooth firmware detection
|
// Intel Bluetooth firmware detection
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
static bool InitIntelBluetooth(uint8_t slotId) {
|
static bool InitIntelBluetooth() {
|
||||||
KernelLogStream(INFO, "BT") << "Intel Bluetooth adapter detected";
|
KernelLogStream(INFO, "BT") << "Intel Bluetooth adapter detected";
|
||||||
|
|
||||||
// Intel BT controllers require HCI Reset before they respond to
|
// Read the Intel TLV version first. It is the mode probe accepted by
|
||||||
// vendor-specific commands. This mirrors the Linux btintel driver
|
// both Intel's bootloader and operational firmware. A standard HCI
|
||||||
// sequence: Reset → Read Version → (firmware load) → Reset.
|
// Reset must not precede it: the bootloader answers Reset with status
|
||||||
if (!Hci::Reset()) {
|
// 0x01 (Unknown HCI Command), which is normal rather than a fatal
|
||||||
KernelLogStream(ERROR, "BT") << "Initial HCI Reset failed";
|
// transport failure.
|
||||||
return false;
|
//
|
||||||
}
|
// If the controller is in bootloader mode, this loads the matching SFI
|
||||||
|
// image from the ramdisk, secure-sends it, boots operational firmware,
|
||||||
// Read standard HCI version -- if this fails, the controller is likely
|
|
||||||
// in bootloader mode where only vendor commands are accepted.
|
|
||||||
Hci::LocalVersion lver = {};
|
|
||||||
bool hciVersionOk = Hci::ReadLocalVersion(&lver);
|
|
||||||
if (hciVersionOk) {
|
|
||||||
KernelLogStream(INFO, "BT") << "HCI version=" << (uint64_t)lver.HciVersion
|
|
||||||
<< " rev=" << base::hex << (uint64_t)lver.HciRevision
|
|
||||||
<< " LMP=" << (uint64_t)lver.LmpVersion
|
|
||||||
<< " manufacturer=" << (uint64_t)lver.Manufacturer
|
|
||||||
<< " subver=" << (uint64_t)lver.LmpSubversion << base::dec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read legacy Intel version for diagnostics (TLV parts return this in
|
|
||||||
// a different layout; the authoritative state check happens inside the
|
|
||||||
// firmware download path below via the TLV version).
|
|
||||||
Hci::IntelVersion ver = {};
|
|
||||||
if (!Hci::ReadIntelVersion(&ver)) {
|
|
||||||
KernelLogStream(WARNING, "BT") << "Failed to read Intel BT version";
|
|
||||||
} else {
|
|
||||||
KernelLogStream(INFO, "BT") << "Intel BT: HW variant=" << (uint64_t)ver.HwVariant
|
|
||||||
<< " FW variant=" << base::hex << (uint64_t)ver.FwVariant
|
|
||||||
<< " FW rev=" << (uint64_t)ver.FwRevision << "."
|
|
||||||
<< (uint64_t)ver.FwBuildNum << base::dec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the firmware download path. This reads the TLV version, and if
|
|
||||||
// the controller is in bootloader mode, loads the matching .sfi image
|
|
||||||
// from the ramdisk, secure-sends it, boots the operational firmware
|
|
||||||
// and applies DDC parameters. Returns true if the controller ends up
|
// and applies DDC parameters. Returns true if the controller ends up
|
||||||
// operational (either already loaded, or freshly downloaded).
|
// operational (either already loaded, or freshly downloaded).
|
||||||
if (!DownloadIntelFirmware()) {
|
if (!DownloadIntelFirmware()) {
|
||||||
|
// Older Intel parts may lack the TLV command while already running
|
||||||
|
// usable operational firmware. Preserve that compatibility only
|
||||||
|
// when standard HCI proves it is genuinely operational.
|
||||||
|
if (Hci::Reset()) {
|
||||||
KernelLogStream(WARNING, "BT")
|
KernelLogStream(WARNING, "BT")
|
||||||
<< "Intel BT firmware not loaded; limited functionality";
|
<< "Intel firmware query failed; continuing with operational HCI";
|
||||||
// Standard init already issued an HCI Reset above.
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
KernelLogStream(ERROR, "BT")
|
||||||
|
<< "Intel controller remains in bootloader mode";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The operational firmware just (re)booted. Give it a clean reset and
|
// Whether it was already present or was just booted, operational
|
||||||
// re-enable the Intel vendor event mask before the generic HCI setup.
|
// firmware must now accept standard HCI. Do not mark the adapter ready
|
||||||
Hci::Reset();
|
// if this transition did not actually happen.
|
||||||
Hci::IntelSetEventMask();
|
if (!Hci::Reset()) {
|
||||||
|
KernelLogStream(ERROR, "BT")
|
||||||
|
<< "Operational firmware did not accept HCI Reset";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Hci::IntelSetEventMask()) {
|
||||||
|
KernelLogStream(WARNING, "BT")
|
||||||
|
<< "Intel vendor event mask was not accepted";
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,10 +259,15 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Intel-specific initialization (firmware download + HCI Reset)
|
// Intel-specific initialization (firmware download + HCI Reset)
|
||||||
bool didReset = false;
|
bool didReset = false;
|
||||||
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
if (IsIntelBt(dev->VendorId, dev->ProductId)) {
|
||||||
if (InitIntelBluetooth(g_slotId)) {
|
if (InitIntelBluetooth()) {
|
||||||
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
didReset = true; // InitIntelBluetooth already sent HCI Reset
|
||||||
} else {
|
} else {
|
||||||
KernelLogStream(WARNING, "BT") << "Intel BT init failed, continuing with basic HCI";
|
// A recognized Intel part that failed its vendor initialization
|
||||||
|
// is normally still a bootloader, where a second standard Reset
|
||||||
|
// only repeats status 0x01. Stop with an accurate failure
|
||||||
|
// instead of pretending a basic-HCI fallback exists.
|
||||||
|
KernelLogStream(ERROR, "BT") << "Intel BT initialization failed";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +307,10 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
KernelLogStream(INFO, "BT") << "ACL buffer: " << (uint64_t)aclLen
|
KernelLogStream(INFO, "BT") << "ACL buffer: " << (uint64_t)aclLen
|
||||||
<< " bytes x " << (uint64_t)aclNum;
|
<< " bytes x " << (uint64_t)aclNum;
|
||||||
}
|
}
|
||||||
|
// Bulk IN was armed before Intel firmware loading, but bootloader runts
|
||||||
|
// are not HCI ACL traffic and an absorbed firmware-phase USB error may
|
||||||
|
// have stopped the endpoint. Start framed ACL reception only now.
|
||||||
|
Hci::EnableAclDataReception();
|
||||||
|
|
||||||
// Set local name
|
// Set local name
|
||||||
Hci::WriteLocalName("MontaukOS");
|
Hci::WriteLocalName("MontaukOS");
|
||||||
@@ -363,10 +359,15 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Request (0x33), Simple Pairing Complete (0x36) all live there. It was
|
// Request (0x33), Simple Pairing Complete (0x36) all live there. It was
|
||||||
// 0x00 -> the controller started SSP but the IO-Capability Request event
|
// 0x00 -> the controller started SSP but the IO-Capability Request event
|
||||||
// never reached us, so pairing always timed out with auth failure 0x05.
|
// never reached us, so pairing always timed out with auth failure 0x05.
|
||||||
uint8_t eventMask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0x20};
|
uint8_t eventMask[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20};
|
||||||
Hci::SendCommand(Hci::OP_SET_EVENT_MASK, eventMask, 8);
|
Hci::SendCommand(Hci::OP_SET_EVENT_MASK, eventMask, 8);
|
||||||
Hci::WaitCommandComplete(Hci::OP_SET_EVENT_MASK);
|
Hci::WaitCommandComplete(Hci::OP_SET_EVENT_MASK);
|
||||||
|
|
||||||
|
// Request Extended Inquiry Results so scan entries carry EIR names and
|
||||||
|
// RSSI. Older controllers may support only mode 1 (RSSI); the parser
|
||||||
|
// handles both result layouts and the fallback preserves discovery.
|
||||||
|
if (!Hci::WriteInquiryMode(2)) Hci::WriteInquiryMode(1);
|
||||||
|
|
||||||
// Enable inquiry + page scan (discoverable and connectable)
|
// Enable inquiry + page scan (discoverable and connectable)
|
||||||
Hci::WriteScanEnable(0x03);
|
Hci::WriteScanEnable(0x03);
|
||||||
|
|
||||||
@@ -420,8 +421,11 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
void ServiceEvents() {
|
void ServiceEvents() {
|
||||||
if (!g_initialized) return;
|
if (!g_initialized) return;
|
||||||
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
if (Xhci::InPollContext()) return; // never nest under PollEvents
|
||||||
Hci::ProcessPendingCommands();
|
|
||||||
A2dp::ServiceMedia(); // reap events and feed queued media
|
A2dp::ServiceMedia(); // reap events and feed queued media
|
||||||
|
// DrainEvents may just have queued an accept/auth/encryption reply.
|
||||||
|
// Send it in this same service pass instead of adding a scheduler-turn
|
||||||
|
// delay to the controller's security timeout.
|
||||||
|
Hci::ProcessPendingCommands();
|
||||||
Drivers::Audio::Mixer::OnBluetoothWritable();
|
Drivers::Audio::Mixer::OnBluetoothWritable();
|
||||||
int requestedVolume;
|
int requestedVolume;
|
||||||
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
|
if (A2dp::ConsumeVolumeRequest(&requestedVolume))
|
||||||
@@ -472,13 +476,22 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Let the disconnection(s) complete before reprogramming the address.
|
// Let the disconnection(s) complete before reprogramming the address.
|
||||||
|
// A fixed 300 ms pause was merely hopeful and could change the identity
|
||||||
|
// under a still-live encrypted link.
|
||||||
if (droppedLink) {
|
if (droppedLink) {
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 300) {
|
bool anyActive = true;
|
||||||
|
while (anyActive && Timekeeping::GetMilliseconds() - t0 < 2000) {
|
||||||
Xhci::PollEvents();
|
Xhci::PollEvents();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
|
anyActive = false;
|
||||||
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||||
|
auto* conn = Hci::GetConnectionByIndex(i);
|
||||||
|
if (conn && conn->Active) { anyActive = true; break; }
|
||||||
|
}
|
||||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||||
}
|
}
|
||||||
|
if (anyActive) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Program the new address. Do NOT issue an HCI Reset afterwards: the
|
// Program the new address. Do NOT issue an HCI Reset afterwards: the
|
||||||
@@ -515,9 +528,10 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
Hci::ClearInquiryResults();
|
Hci::ClearInquiryResults();
|
||||||
|
|
||||||
// Convert timeout to 1.28s units (min 1, max 30)
|
// Convert timeout to 1.28s units (min 1, max 30)
|
||||||
uint8_t duration = (uint8_t)(timeoutMs / 1280);
|
uint32_t durationUnits = timeoutMs / 1280;
|
||||||
if (duration < 1) duration = 1;
|
if (durationUnits < 1) durationUnits = 1;
|
||||||
if (duration > 30) duration = 30;
|
if (durationUnits > 30) durationUnits = 30;
|
||||||
|
uint8_t duration = (uint8_t)durationUnits;
|
||||||
|
|
||||||
if (!Hci::StartInquiry(duration)) return -1;
|
if (!Hci::StartInquiry(duration)) return -1;
|
||||||
|
|
||||||
@@ -534,7 +548,21 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
|
|
||||||
// Cancel if still running
|
// Cancel if still running
|
||||||
if (Hci::IsInquiryActive()) {
|
if (Hci::IsInquiryActive()) {
|
||||||
Hci::CancelInquiry();
|
// A failed cancel must not be hidden: Create Connection while the
|
||||||
|
// controller is still in Inquiry is commonly rejected as Command
|
||||||
|
// Disallowed. Give a command that briefly lost HCI ownership one
|
||||||
|
// retry, continuing to service the completion event in between.
|
||||||
|
if (!Hci::CancelInquiry()) {
|
||||||
|
uint64_t cancelStart = Timekeeping::GetMilliseconds();
|
||||||
|
while (Hci::IsInquiryActive()
|
||||||
|
&& Timekeeping::GetMilliseconds() - cancelStart < 250) {
|
||||||
|
Xhci::PollEvents();
|
||||||
|
Hci::DrainEvents();
|
||||||
|
for (int j = 0; j < 100; j++)
|
||||||
|
asm volatile("pause" ::: "memory");
|
||||||
|
}
|
||||||
|
if (Hci::IsInquiryActive() && !Hci::CancelInquiry()) return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Hci::GetInquiryResults(buf, maxCount);
|
return Hci::GetInquiryResults(buf, maxCount);
|
||||||
@@ -544,89 +572,82 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
// Connect — initiate ACL connection
|
// Connect — initiate ACL connection
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
static bool SameAddress(const uint8_t* a, const uint8_t* b) {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Hci::ConnectionInfo* FindAclConnection(const uint8_t* bdAddr) {
|
||||||
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||||
|
auto* conn = Hci::GetConnectionByIndex(i);
|
||||||
|
if (conn && conn->Active && conn->LinkType == 0x01
|
||||||
|
&& SameAddress(conn->BdAddr, bdAddr)) return conn;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs) {
|
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs) {
|
||||||
if (!g_initialized || !bdAddr) return -1;
|
if (!g_initialized || !bdAddr) return -1;
|
||||||
|
|
||||||
if (!Hci::CreateConnection(bdAddr)) return -1;
|
// A previous attempt can leave a healthy encrypted ACL link with A2DP
|
||||||
|
// incomplete. Treat another click as an A2DP repair attempt on that
|
||||||
|
// link; issuing HCI Create Connection again just returns "connection
|
||||||
|
// already exists" and made manual recovery impossible.
|
||||||
|
Hci::ConnectionInfo* target = FindAclConnection(bdAddr);
|
||||||
|
bool reusedAcl = target != nullptr;
|
||||||
|
|
||||||
|
if (!target && !Hci::CreateConnection(bdAddr)) return -1;
|
||||||
|
|
||||||
// Wait for Connection Complete event
|
// Wait for Connection Complete event
|
||||||
uint64_t start = Timekeeping::GetMilliseconds();
|
uint64_t start = Timekeeping::GetMilliseconds();
|
||||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
while (!target && Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||||
Xhci::PollEvents();
|
Xhci::PollEvents();
|
||||||
Hci::DrainEvents();
|
Hci::DrainEvents();
|
||||||
|
target = FindAclConnection(bdAddr);
|
||||||
// Check connection table for matching BD_ADDR
|
|
||||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
|
||||||
auto* conn = Hci::GetConnectionByIndex(i);
|
|
||||||
if (conn && conn->Active) {
|
|
||||||
bool match = true;
|
|
||||||
for (int j = 0; j < 6; j++) {
|
|
||||||
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
// The headset (in pairing mode) drives Secure Simple
|
|
||||||
// Pairing itself right after the ACL link comes up. Let
|
|
||||||
// authentication + encryption finish BEFORE opening any
|
|
||||||
// L2CAP/AVDTP channels: doing A2DP on a not-yet-
|
|
||||||
// authenticated link races with the pairing handshake and
|
|
||||||
// the headset drops us (reason 0x05). Drain events here
|
|
||||||
// so the IO-capability / user-confirm / link-key / encrypt
|
|
||||||
// events all get serviced.
|
|
||||||
//
|
|
||||||
// We are the initiator: request authentication so the
|
|
||||||
// controller starts Secure Simple Pairing (Link Key
|
|
||||||
// Request -> our negative reply -> IO Capability Request
|
|
||||||
// -> ... ). The headset does not start this on its own.
|
|
||||||
// NB this only works now that octet 6 of the event mask
|
|
||||||
// is enabled so the IO-Capability Request event actually
|
|
||||||
// reaches us; before that this produced 03 17 06 05.
|
|
||||||
Hci::AuthenticateLink(conn->Handle);
|
|
||||||
|
|
||||||
uint64_t t0 = Timekeeping::GetMilliseconds();
|
|
||||||
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
|
||||||
Xhci::PollEvents();
|
|
||||||
Hci::DrainEvents();
|
|
||||||
// Send queued pairing replies reliably (top-level,
|
|
||||||
// not nested under PollEvents).
|
|
||||||
Hci::ProcessPendingCommands();
|
|
||||||
if (!conn->Active) break; // link dropped during pairing
|
|
||||||
if (conn->Encrypted) break; // authenticated + encrypted -> ready
|
|
||||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bring up the A2DP source stream (signaling + media
|
|
||||||
// channels, SBC negotiation) only once the link is
|
|
||||||
// secured. Without a media stream the headset also drops
|
|
||||||
// the link (reason 0x13), so this keeps it engaged too.
|
|
||||||
if (conn->Active) {
|
|
||||||
// Let the link settle after Encryption Change before
|
|
||||||
// dialing L2CAP: some sinks ignore a CONN_REQ that
|
|
||||||
// arrives the instant encryption completes. Drain
|
|
||||||
// (don't blind-sleep) so the ACL RX ring stays live.
|
|
||||||
uint64_t st = Timekeeping::GetMilliseconds();
|
|
||||||
while (Timekeeping::GetMilliseconds() - st < 300) {
|
|
||||||
Xhci::PollEvents();
|
|
||||||
Hci::DrainEvents();
|
|
||||||
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
|
||||||
}
|
|
||||||
A2dp::StartSource();
|
|
||||||
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
|
||||||
}
|
|
||||||
// Persist any new link key now (process context), even if
|
|
||||||
// the link later dropped, so the disk write never stalls
|
|
||||||
// the nested pairing event handler.
|
|
||||||
Hci::FlushLinkKeys();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int j = 0; j < 200; j++) {
|
for (int j = 0; j < 200; j++) {
|
||||||
asm volatile("pause" ::: "memory");
|
asm volatile("pause" ::: "memory");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!target) return -1;
|
||||||
|
|
||||||
return -1; // Timeout
|
// Connection Complete queues authentication for both incoming and
|
||||||
|
// outgoing ACLs. Deliver that common request and wait for encryption;
|
||||||
|
// a manual A2DP repair on an encrypted ACL skips this entire wait.
|
||||||
|
if (!target->Encrypted) {
|
||||||
|
// A link that predates this syscall may have exhausted or missed its
|
||||||
|
// earlier security attempt; explicitly restart it. A freshly-created
|
||||||
|
// link already has the request queued by Connection Complete.
|
||||||
|
if (reusedAcl) Hci::AuthenticateLink(target->Handle);
|
||||||
|
uint64_t t0 = Timekeeping::GetMilliseconds();
|
||||||
|
while (Timekeeping::GetMilliseconds() - t0 < 5000) {
|
||||||
|
Xhci::PollEvents();
|
||||||
|
Hci::DrainEvents();
|
||||||
|
Hci::ProcessPendingCommands();
|
||||||
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) break;
|
||||||
|
if (target->Encrypted) break;
|
||||||
|
for (int k = 0; k < 200; k++) asm volatile("pause" ::: "memory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool targetAlive = target->Active
|
||||||
|
&& SameAddress(target->BdAddr, bdAddr);
|
||||||
|
bool a2dpReady = false;
|
||||||
|
if (targetAlive) {
|
||||||
|
a2dpReady = A2dp::StartSource();
|
||||||
|
Drivers::Audio::Mixer::OnBluetoothStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist any new link key now (process context), even if the link later
|
||||||
|
// dropped, so disk I/O never stalls the nested pairing event handler.
|
||||||
|
Hci::FlushLinkKeys();
|
||||||
|
if (!target->Active || !SameAddress(target->BdAddr, bdAddr)) return -1;
|
||||||
|
if (!a2dpReady) {
|
||||||
|
KernelLogStream(WARNING, "BT")
|
||||||
|
<< "ACL connected but A2DP source setup failed";
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -640,11 +661,7 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
for (int i = 0; i < Hci::MAX_CONNECTIONS; i++) {
|
||||||
auto* conn = Hci::GetConnectionByIndex(i);
|
auto* conn = Hci::GetConnectionByIndex(i);
|
||||||
if (conn && conn->Active) {
|
if (conn && conn->Active) {
|
||||||
bool match = true;
|
if (SameAddress(conn->BdAddr, bdAddr)) {
|
||||||
for (int j = 0; j < 6; j++) {
|
|
||||||
if (conn->BdAddr[j] != bdAddr[j]) { match = false; break; }
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
Hci::Disconnect(conn->Handle, 0x13); // 0x13 = Remote User Terminated
|
Hci::Disconnect(conn->Handle, 0x13); // 0x13 = Remote User Terminated
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ namespace Drivers::USB::Bluetooth {
|
|||||||
int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs);
|
int Scan(Hci::InquiryDevice* buf, int maxCount, uint32_t timeoutMs);
|
||||||
|
|
||||||
// Initiate connection to a remote device by BD_ADDR
|
// Initiate connection to a remote device by BD_ADDR
|
||||||
// Returns 0 on success (connection established), -1 on failure
|
// Returns 0 when ACL + A2DP are ready, -1 when the ACL connection failed,
|
||||||
|
// or -2 when ACL connected but A2DP setup did not complete.
|
||||||
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs = 10000);
|
int Connect(const uint8_t* bdAddr, uint32_t timeoutMs = 10000);
|
||||||
|
|
||||||
// Disconnect a device by BD_ADDR
|
// Disconnect a device by BD_ADDR
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -181,6 +181,11 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// and keeps the event pipe alive -- see the definition).
|
// and keeps the event pipe alive -- see the definition).
|
||||||
void StartEventPipe();
|
void StartEventPipe();
|
||||||
|
|
||||||
|
// Enable operational ACL receive framing after firmware/HCI setup. Bulk IN
|
||||||
|
// is armed earlier to protect Intel firmware loading; pre-operational runts
|
||||||
|
// are intentionally ignored and any endpoint error is recovered here.
|
||||||
|
void EnableAclDataReception();
|
||||||
|
|
||||||
// Send an HCI command via USB control transfer (EP0)
|
// Send an HCI command via USB control transfer (EP0)
|
||||||
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
bool SendCommand(uint16_t opcode, const uint8_t* params, uint8_t paramLen);
|
||||||
|
|
||||||
@@ -308,6 +313,9 @@ namespace Drivers::USB::Bluetooth::Hci {
|
|||||||
// Write Simple Secure Pairing mode
|
// Write Simple Secure Pairing mode
|
||||||
bool WriteSSPMode(uint8_t mode);
|
bool WriteSSPMode(uint8_t mode);
|
||||||
|
|
||||||
|
// Select inquiry result format: 0=standard, 1=RSSI, 2=extended/EIR.
|
||||||
|
bool WriteInquiryMode(uint8_t mode);
|
||||||
|
|
||||||
// Accept an incoming connection
|
// Accept an incoming connection
|
||||||
bool AcceptConnection(const uint8_t* bdAddr, uint8_t role);
|
bool AcceptConnection(const uint8_t* bdAddr, uint8_t role);
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
static bool g_initialized = false;
|
static bool g_initialized = false;
|
||||||
static uint8_t g_sigIdentifier = 1;
|
static uint8_t g_sigIdentifier = 1;
|
||||||
|
|
||||||
// Diagnostic: result of the most recent L2CAP Connection Response we got for
|
|
||||||
// an outgoing connection (0=success, 1=pending, 2=PSM-unsupported,
|
|
||||||
// 3=security-block, 4=no-resources). 0xFFFF = none received yet.
|
|
||||||
static volatile uint16_t g_lastConnRspResult = 0xFFFF;
|
|
||||||
|
|
||||||
// Channel table
|
// Channel table
|
||||||
static ChannelInfo g_channels[MAX_CHANNELS] = {};
|
static ChannelInfo g_channels[MAX_CHANNELS] = {};
|
||||||
static uint16_t g_nextCid = CID_DYNAMIC_START;
|
static uint16_t g_nextCid = CID_DYNAMIC_START;
|
||||||
@@ -39,8 +34,29 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// Helpers
|
// Helpers
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
static uint8_t NextIdentifier() {
|
||||||
|
uint8_t id = g_sigIdentifier++;
|
||||||
|
if (id == 0) id = g_sigIdentifier++;
|
||||||
|
if (g_sigIdentifier == 0) g_sigIdentifier = 1;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
static uint16_t AllocCid() {
|
static uint16_t AllocCid() {
|
||||||
return g_nextCid++;
|
// Dynamic CIDs are 0x0040..0xFFFF and must not collide with an active
|
||||||
|
// channel after wraparound.
|
||||||
|
for (uint32_t tries = 0; tries <= 0xFFC0; tries++) {
|
||||||
|
if (g_nextCid < CID_DYNAMIC_START) g_nextCid = CID_DYNAMIC_START;
|
||||||
|
uint16_t candidate = g_nextCid++;
|
||||||
|
bool used = false;
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active && g_channels[i].LocalCid == candidate) {
|
||||||
|
used = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!used) return candidate;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ChannelInfo* AllocChannel(uint16_t psm) {
|
static ChannelInfo* AllocChannel(uint16_t psm) {
|
||||||
@@ -48,12 +64,18 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
if (!g_channels[i].Active) {
|
if (!g_channels[i].Active) {
|
||||||
g_channels[i].Active = true;
|
g_channels[i].Active = true;
|
||||||
g_channels[i].LocalCid = AllocCid();
|
g_channels[i].LocalCid = AllocCid();
|
||||||
|
if (g_channels[i].LocalCid == 0) {
|
||||||
|
g_channels[i].Active = false;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
g_channels[i].RemoteCid = 0;
|
g_channels[i].RemoteCid = 0;
|
||||||
g_channels[i].Psm = psm;
|
g_channels[i].Psm = psm;
|
||||||
g_channels[i].RemoteMtu = 672; // Default L2CAP MTU
|
g_channels[i].RemoteMtu = 672; // Default L2CAP MTU
|
||||||
g_channels[i].Configured = false;
|
g_channels[i].Configured = false;
|
||||||
g_channels[i].LocalConfigDone = false;
|
g_channels[i].LocalConfigDone = false;
|
||||||
g_channels[i].RemoteConfigDone = false;
|
g_channels[i].RemoteConfigDone = false;
|
||||||
|
g_channels[i].ConnRspResult = 0xFFFF;
|
||||||
|
g_channels[i].ConnRspStatus = 0;
|
||||||
return &g_channels[i];
|
return &g_channels[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,13 +83,14 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send L2CAP signaling command
|
// Send L2CAP signaling command
|
||||||
static void SendSignal(uint8_t code, uint8_t identifier,
|
static bool SendSignal(uint8_t code, uint8_t identifier,
|
||||||
const uint8_t* payload, uint16_t payloadLen) {
|
const uint8_t* payload, uint16_t payloadLen) {
|
||||||
// L2CAP header + Signal header + payload
|
// L2CAP header + Signal header + payload
|
||||||
uint16_t sigLen = sizeof(SignalHeader) + payloadLen;
|
uint16_t sigLen = sizeof(SignalHeader) + payloadLen;
|
||||||
uint16_t totalPayload = sizeof(L2capHeader) + sigLen;
|
uint16_t totalPayload = sizeof(L2capHeader) + sigLen;
|
||||||
|
|
||||||
uint8_t buf[128] = {};
|
uint8_t buf[128] = {};
|
||||||
|
if (totalPayload > sizeof(buf)) return false;
|
||||||
auto* l2hdr = (L2capHeader*)buf;
|
auto* l2hdr = (L2capHeader*)buf;
|
||||||
l2hdr->Length = sigLen;
|
l2hdr->Length = sigLen;
|
||||||
l2hdr->ChannelId = CID_SIGNALING;
|
l2hdr->ChannelId = CID_SIGNALING;
|
||||||
@@ -81,7 +104,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
memcpy(buf + sizeof(L2capHeader) + sizeof(SignalHeader), payload, payloadLen);
|
memcpy(buf + sizeof(L2capHeader) + sizeof(SignalHeader), payload, payloadLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
Hci::SendAcl(g_aclHandle, Hci::ACL_PB_FIRST_FLUSH,
|
return Hci::SendAcl(g_aclHandle, Hci::ACL_PB_FIRST_NON_FLUSH,
|
||||||
buf, totalPayload);
|
buf, totalPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,17 +119,31 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
g_nextCid = CID_DYNAMIC_START;
|
g_nextCid = CID_DYNAMIC_START;
|
||||||
|
|
||||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active)
|
||||||
|
Avrcp::OnChannelClosed(g_channels[i].LocalCid);
|
||||||
g_channels[i].Active = false;
|
g_channels[i].Active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
KernelLogStream(OK, "BT-L2CAP") << "Initialized for ACL handle " << (uint64_t)aclHandle;
|
KernelLogStream(OK, "BT-L2CAP") << "Initialized for ACL handle " << (uint64_t)aclHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OnDisconnected(uint16_t aclHandle) {
|
||||||
|
if (!g_initialized || aclHandle != g_aclHandle) return;
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active)
|
||||||
|
Avrcp::OnChannelClosed(g_channels[i].LocalCid);
|
||||||
|
g_channels[i].Active = false;
|
||||||
|
}
|
||||||
|
g_aclHandle = 0;
|
||||||
|
g_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// ProcessPacket
|
// ProcessPacket
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len) {
|
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len) {
|
||||||
|
if (!g_initialized || aclHandle != g_aclHandle) return;
|
||||||
if (len < sizeof(L2capHeader)) return;
|
if (len < sizeof(L2capHeader)) return;
|
||||||
|
|
||||||
auto* l2hdr = (const L2capHeader*)data;
|
auto* l2hdr = (const L2capHeader*)data;
|
||||||
@@ -145,8 +182,41 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
|
|
||||||
// Accept connections for AVDTP, SDP, and AVRCP control
|
// Accept connections for AVDTP, SDP, and AVRCP control
|
||||||
if (psm == PSM_AVDTP || psm == PSM_SDP || psm == PSM_AVCTP) {
|
if (psm == PSM_AVDTP || psm == PSM_SDP || psm == PSM_AVCTP) {
|
||||||
auto* ch = AllocChannel(psm);
|
// Detect a duplicate source CID instead of creating
|
||||||
if (ch) {
|
// two local channels that address the same endpoint.
|
||||||
|
// Same-PSM duplicates are normal retransmissions;
|
||||||
|
// a different PSM reusing the CID is invalid.
|
||||||
|
ChannelInfo* existing = nullptr;
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active
|
||||||
|
&& g_channels[i].RemoteCid == srcCid) {
|
||||||
|
existing = &g_channels[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto* ch = existing ? nullptr : AllocChannel(psm);
|
||||||
|
if (existing && existing->Psm == psm) {
|
||||||
|
// Retransmission of a request whose response was
|
||||||
|
// lost: repeat SUCCESS for the original channel,
|
||||||
|
// and repeat our config request as well.
|
||||||
|
uint8_t rsp[8] = {
|
||||||
|
(uint8_t)existing->LocalCid,
|
||||||
|
(uint8_t)(existing->LocalCid >> 8),
|
||||||
|
(uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||||
|
0, 0, 0, 0
|
||||||
|
};
|
||||||
|
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||||
|
uint8_t cfgReq[4] = {
|
||||||
|
(uint8_t)srcCid, (uint8_t)(srcCid >> 8), 0, 0
|
||||||
|
};
|
||||||
|
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||||
|
} else if (existing) {
|
||||||
|
uint8_t rsp[8] = {
|
||||||
|
0, 0, (uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||||
|
0x07, 0x00, 0, 0 // source CID already allocated
|
||||||
|
};
|
||||||
|
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||||
|
} else if (ch) {
|
||||||
ch->RemoteCid = srcCid;
|
ch->RemoteCid = srcCid;
|
||||||
|
|
||||||
// Send Connection Response (success)
|
// Send Connection Response (success)
|
||||||
@@ -168,7 +238,13 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
cfgReq[0] = (uint8_t)(srcCid & 0xFF);
|
cfgReq[0] = (uint8_t)(srcCid & 0xFF);
|
||||||
cfgReq[1] = (uint8_t)(srcCid >> 8);
|
cfgReq[1] = (uint8_t)(srcCid >> 8);
|
||||||
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
||||||
SendSignal(SIG_CONFIG_REQ, g_sigIdentifier++, cfgReq, 4);
|
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||||
|
} else {
|
||||||
|
uint8_t rsp[8] = {
|
||||||
|
0, 0, (uint8_t)srcCid, (uint8_t)(srcCid >> 8),
|
||||||
|
0x04, 0x00, 0, 0 // no resources
|
||||||
|
};
|
||||||
|
SendSignal(SIG_CONN_RSP, sig->Identifier, rsp, 8);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reject: PSM not supported
|
// Reject: PSM not supported
|
||||||
@@ -193,8 +269,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// info, 1=authentication pending, 2=authorization pending.
|
// info, 1=authentication pending, 2=authorization pending.
|
||||||
uint16_t status = (sigPayloadLen >= 8)
|
uint16_t status = (sigPayloadLen >= 8)
|
||||||
? ((uint16_t)sigPayload[6] | ((uint16_t)sigPayload[7] << 8)) : 0;
|
? ((uint16_t)sigPayload[6] | ((uint16_t)sigPayload[7] << 8)) : 0;
|
||||||
g_lastConnRspResult = result;
|
|
||||||
|
|
||||||
KernelLogStream(INFO, "BT-L2CAP") << "Connection Response: dstCID="
|
KernelLogStream(INFO, "BT-L2CAP") << "Connection Response: dstCID="
|
||||||
<< base::hex << (uint64_t)dstCid << " result=" << (uint64_t)result
|
<< base::hex << (uint64_t)dstCid << " result=" << (uint64_t)result
|
||||||
<< " status=" << (uint64_t)status << base::dec;
|
<< " status=" << (uint64_t)status << base::dec;
|
||||||
@@ -203,6 +277,8 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// Find our channel by srcCid (which is our local CID)
|
// Find our channel by srcCid (which is our local CID)
|
||||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
||||||
|
g_channels[i].ConnRspResult = result;
|
||||||
|
g_channels[i].ConnRspStatus = status;
|
||||||
g_channels[i].RemoteCid = dstCid;
|
g_channels[i].RemoteCid = dstCid;
|
||||||
|
|
||||||
// Send Configuration Request
|
// Send Configuration Request
|
||||||
@@ -210,7 +286,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
cfgReq[0] = (uint8_t)(dstCid & 0xFF);
|
cfgReq[0] = (uint8_t)(dstCid & 0xFF);
|
||||||
cfgReq[1] = (uint8_t)(dstCid >> 8);
|
cfgReq[1] = (uint8_t)(dstCid >> 8);
|
||||||
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
cfgReq[2] = 0; cfgReq[3] = 0; // Flags
|
||||||
SendSignal(SIG_CONFIG_REQ, g_sigIdentifier++, cfgReq, 4);
|
SendSignal(SIG_CONFIG_REQ, NextIdentifier(), cfgReq, 4);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,10 +299,21 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// a Disconnect for the half-open remote endpoint.
|
// a Disconnect for the half-open remote endpoint.
|
||||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
if (g_channels[i].Active && g_channels[i].LocalCid == srcCid) {
|
||||||
|
g_channels[i].ConnRspResult = result;
|
||||||
|
g_channels[i].ConnRspStatus = status;
|
||||||
g_channels[i].RemoteCid = dstCid;
|
g_channels[i].RemoteCid = dstCid;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active
|
||||||
|
&& g_channels[i].LocalCid == srcCid) {
|
||||||
|
g_channels[i].ConnRspResult = result;
|
||||||
|
g_channels[i].ConnRspStatus = status;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -333,7 +420,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
|
|
||||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
if (g_channels[i].Active && g_channels[i].LocalCid == dstCid) {
|
if (g_channels[i].Active && g_channels[i].LocalCid == dstCid) {
|
||||||
|
uint16_t closedCid = g_channels[i].LocalCid;
|
||||||
g_channels[i].Active = false;
|
g_channels[i].Active = false;
|
||||||
|
A2dp::OnChannelClosed(closedCid);
|
||||||
|
Avrcp::OnChannelClosed(closedCid);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,6 +468,13 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// SDP client channel after a query); nothing left to do.
|
// SDP client channel after a query); nothing left to do.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SIG_COMMAND_REJECT:
|
||||||
|
case SIG_ECHO_RSP:
|
||||||
|
case SIG_INFO_RSP:
|
||||||
|
// Terminal responses to requests sent by us. No response
|
||||||
|
// is required; in particular, never reject a response.
|
||||||
|
break;
|
||||||
|
|
||||||
case SIG_ECHO_REQ: {
|
case SIG_ECHO_REQ: {
|
||||||
// Echo Request must be answered (echo the payload back) or
|
// Echo Request must be answered (echo the payload back) or
|
||||||
// a peer probing the link times out on us.
|
// a peer probing the link times out on us.
|
||||||
@@ -392,6 +489,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
KernelLogStream(INFO, "BT-L2CAP") << "Unhandled signaling code="
|
KernelLogStream(INFO, "BT-L2CAP") << "Unhandled signaling code="
|
||||||
<< base::hex << (uint64_t)sig->Code << base::dec
|
<< base::hex << (uint64_t)sig->Code << base::dec
|
||||||
<< " len=" << (uint64_t)sigPayloadLen;
|
<< " len=" << (uint64_t)sigPayloadLen;
|
||||||
|
// Command Reject, reason 0x0000 = command not understood.
|
||||||
|
// Silence leaves the peer waiting for its signaling timer.
|
||||||
|
uint8_t reject[2] = {0x00, 0x00};
|
||||||
|
SendSignal(SIG_COMMAND_REJECT, sig->Identifier, reject, 2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,7 +503,7 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
for (int i = 0; i < MAX_CHANNELS; i++) {
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
if (g_channels[i].Active && g_channels[i].LocalCid == cid) {
|
if (g_channels[i].Active && g_channels[i].LocalCid == cid) {
|
||||||
if (g_channels[i].Psm == PSM_AVDTP) {
|
if (g_channels[i].Psm == PSM_AVDTP) {
|
||||||
A2dp::ProcessAvdtp(payload, l2len);
|
A2dp::ProcessAvdtp(cid, payload, l2len);
|
||||||
} else if (g_channels[i].Psm == PSM_SDP) {
|
} else if (g_channels[i].Psm == PSM_SDP) {
|
||||||
A2dp::ProcessSdp(cid, payload, l2len);
|
A2dp::ProcessSdp(cid, payload, l2len);
|
||||||
} else if (g_channels[i].Psm == PSM_AVCTP) {
|
} else if (g_channels[i].Psm == PSM_AVCTP) {
|
||||||
@@ -421,8 +522,6 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
uint16_t Connect(uint16_t psm) {
|
uint16_t Connect(uint16_t psm) {
|
||||||
if (!g_initialized) return 0;
|
if (!g_initialized) return 0;
|
||||||
|
|
||||||
g_lastConnRspResult = 0xFFFF; // reset diagnostic for this attempt
|
|
||||||
|
|
||||||
auto* ch = AllocChannel(psm);
|
auto* ch = AllocChannel(psm);
|
||||||
if (!ch) return 0;
|
if (!ch) return 0;
|
||||||
|
|
||||||
@@ -432,7 +531,10 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
req[1] = (uint8_t)(psm >> 8);
|
req[1] = (uint8_t)(psm >> 8);
|
||||||
req[2] = (uint8_t)(ch->LocalCid & 0xFF);
|
req[2] = (uint8_t)(ch->LocalCid & 0xFF);
|
||||||
req[3] = (uint8_t)(ch->LocalCid >> 8);
|
req[3] = (uint8_t)(ch->LocalCid >> 8);
|
||||||
SendSignal(SIG_CONN_REQ, g_sigIdentifier++, req, 4);
|
if (!SendSignal(SIG_CONN_REQ, NextIdentifier(), req, 4)) {
|
||||||
|
ch->Active = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return ch->LocalCid;
|
return ch->LocalCid;
|
||||||
}
|
}
|
||||||
@@ -518,9 +620,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
req[1] = (uint8_t)(g_channels[i].RemoteCid >> 8);
|
req[1] = (uint8_t)(g_channels[i].RemoteCid >> 8);
|
||||||
req[2] = (uint8_t)(g_channels[i].LocalCid & 0xFF);
|
req[2] = (uint8_t)(g_channels[i].LocalCid & 0xFF);
|
||||||
req[3] = (uint8_t)(g_channels[i].LocalCid >> 8);
|
req[3] = (uint8_t)(g_channels[i].LocalCid >> 8);
|
||||||
SendSignal(SIG_DISCONN_REQ, g_sigIdentifier++, req, 4);
|
SendSignal(SIG_DISCONN_REQ, NextIdentifier(), req, 4);
|
||||||
}
|
}
|
||||||
g_channels[i].Active = false;
|
g_channels[i].Active = false;
|
||||||
|
A2dp::OnChannelClosed(localCid);
|
||||||
|
Avrcp::OnChannelClosed(localCid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,12 +651,28 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint16_t FindAvdtpChannelExcept(uint16_t exceptCid) {
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++) {
|
||||||
|
if (g_channels[i].Active && g_channels[i].Psm == PSM_AVDTP
|
||||||
|
&& g_channels[i].LocalCid != exceptCid) {
|
||||||
|
return g_channels[i].LocalCid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t GetAclHandle() {
|
uint16_t GetAclHandle() {
|
||||||
return g_aclHandle;
|
return g_aclHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t LastConnRspResult() {
|
uint16_t ConnRspResult(uint16_t localCid) {
|
||||||
return g_lastConnRspResult;
|
auto* ch = GetChannel(localCid);
|
||||||
|
return ch ? ch->ConnRspResult : 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ConnRspStatus(uint16_t localCid) {
|
||||||
|
auto* ch = GetChannel(localCid);
|
||||||
|
return ch ? ch->ConnRspStatus : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
bool Configured; // Both sides configured
|
bool Configured; // Both sides configured
|
||||||
bool LocalConfigDone;
|
bool LocalConfigDone;
|
||||||
bool RemoteConfigDone;
|
bool RemoteConfigDone;
|
||||||
|
uint16_t ConnRspResult; // 0xFFFF until this outgoing dial is answered
|
||||||
|
uint16_t ConnRspStatus; // meaningful while result == CONN_PENDING
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int MAX_CHANNELS = 8;
|
constexpr int MAX_CHANNELS = 8;
|
||||||
@@ -89,6 +91,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// Initialize L2CAP for a new HCI connection
|
// Initialize L2CAP for a new HCI connection
|
||||||
void Initialize(uint16_t aclHandle);
|
void Initialize(uint16_t aclHandle);
|
||||||
|
|
||||||
|
// Invalidate every dynamic channel when its ACL link disappears. Without
|
||||||
|
// this, AVRCP or media code can send to stale CIDs until the next connection
|
||||||
|
// happens to reinitialize the table.
|
||||||
|
void OnDisconnected(uint16_t aclHandle);
|
||||||
|
|
||||||
// Process an L2CAP packet (called from HCI ACL processing)
|
// Process an L2CAP packet (called from HCI ACL processing)
|
||||||
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len);
|
void ProcessPacket(uint16_t aclHandle, const uint8_t* data, uint16_t len);
|
||||||
|
|
||||||
@@ -114,6 +121,11 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// it (we dial it after AVDTP_OPEN, but some sinks open it inbound instead).
|
// it (we dial it after AVDTP_OPEN, but some sinks open it inbound instead).
|
||||||
uint16_t FindConfiguredAvdtpChannelExcept(uint16_t exceptCid);
|
uint16_t FindConfiguredAvdtpChannelExcept(uint16_t exceptCid);
|
||||||
|
|
||||||
|
// Find any active AVDTP channel except the signaling channel, including a
|
||||||
|
// still-pending outgoing media dial. Manual retries reuse that channel
|
||||||
|
// instead of leaking another slot/CID while authorization is in progress.
|
||||||
|
uint16_t FindAvdtpChannelExcept(uint16_t exceptCid);
|
||||||
|
|
||||||
// Reclaim a dialed channel during A2DP bring-up retries so a failed connect
|
// Reclaim a dialed channel during A2DP bring-up retries so a failed connect
|
||||||
// doesn't leak the fixed-size channel table. If the peer had acknowledged
|
// doesn't leak the fixed-size channel table. If the peer had acknowledged
|
||||||
// the channel (RemoteCid != 0) a Disconnect Request is sent first; an
|
// the channel (RemoteCid != 0) a Disconnect Request is sent first; an
|
||||||
@@ -125,10 +137,9 @@ namespace Drivers::USB::Bluetooth::L2cap {
|
|||||||
// Get the ACL handle
|
// Get the ACL handle
|
||||||
uint16_t GetAclHandle();
|
uint16_t GetAclHandle();
|
||||||
|
|
||||||
// Result code of the most recent outgoing L2CAP Connection Response
|
// Per-channel connection response state. A global "last result" is unsafe:
|
||||||
// (0xFFFF if none yet). 0=success, 1=pending, 2=PSM-unsupported,
|
// an unrelated SDP, AVDTP, or AVRCP response can arrive concurrently.
|
||||||
// 3=security-block, 4=no-resources. StartSource() retries only when the
|
uint16_t ConnRspResult(uint16_t localCid);
|
||||||
// remote ignored the dial entirely (0xFFFF).
|
uint16_t ConnRspStatus(uint16_t localCid);
|
||||||
uint16_t LastConnRspResult();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ namespace Drivers::USB::Bluetooth::Sbc {
|
|||||||
headerBits += enc->Subbands; // join bits
|
headerBits += enc->Subbands; // join bits
|
||||||
}
|
}
|
||||||
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
||||||
|
if (enc->ChannelMode == MODE_DUAL_CHANNEL) dataBits *= enc->Channels;
|
||||||
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ namespace Drivers::USB::Bluetooth::Sbc {
|
|||||||
uint32_t headerBits = 32 + (4 * enc->Subbands * enc->Channels);
|
uint32_t headerBits = 32 + (4 * enc->Subbands * enc->Channels);
|
||||||
if (enc->ChannelMode == MODE_JOINT_STEREO) headerBits += enc->Subbands;
|
if (enc->ChannelMode == MODE_JOINT_STEREO) headerBits += enc->Subbands;
|
||||||
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
uint32_t dataBits = enc->Blocks * enc->Bitpool;
|
||||||
|
if (enc->ChannelMode == MODE_DUAL_CHANNEL) dataBits *= enc->Channels;
|
||||||
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
enc->FrameSize = (headerBits + dataBits + 7) / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -988,6 +988,38 @@ namespace Drivers::USB::Xhci {
|
|||||||
SendCommand(deqTrb);
|
SendCommand(deqTrb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// ResetBulkOutEndpoint - clear a halted bulk OUT endpoint
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void ResetBulkOutEndpoint(uint8_t slotId) {
|
||||||
|
if (slotId == 0 || slotId > MAX_SLOTS || !g_devices[slotId].Active) return;
|
||||||
|
UsbDeviceInfo& dev = g_devices[slotId];
|
||||||
|
if (dev.BulkOutEpNum == 0 || !dev.BulkOutRing) return;
|
||||||
|
|
||||||
|
uint8_t dci = dev.BulkOutEpNum * 2;
|
||||||
|
|
||||||
|
TRB resetTrb = {};
|
||||||
|
resetTrb.Control = (TRB_RESET_ENDPOINT << TRB_TYPE_SHIFT)
|
||||||
|
| ((uint32_t)slotId << 24)
|
||||||
|
| ((uint32_t)dci << 16);
|
||||||
|
SendCommand(resetTrb);
|
||||||
|
|
||||||
|
// Skip the errored TRB and everything queued behind it. The class
|
||||||
|
// driver releases the corresponding DMA slots before it sends again.
|
||||||
|
uint64_t newDeq = dev.BulkOutRingPhys
|
||||||
|
+ (uint64_t)dev.BulkOutRingEnqueue * sizeof(TRB);
|
||||||
|
if (dev.BulkOutRingCCS) newDeq |= 1; // DCS bit
|
||||||
|
|
||||||
|
TRB deqTrb = {};
|
||||||
|
deqTrb.Parameter0 = (uint32_t)(newDeq & 0xFFFFFFFF);
|
||||||
|
deqTrb.Parameter1 = (uint32_t)(newDeq >> 32);
|
||||||
|
deqTrb.Control = (TRB_SET_TR_DEQUEUE << TRB_TYPE_SHIFT)
|
||||||
|
| ((uint32_t)slotId << 24)
|
||||||
|
| ((uint32_t)dci << 16);
|
||||||
|
SendCommand(deqTrb);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// QueueBulkInTransfer
|
// QueueBulkInTransfer
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -326,6 +326,11 @@ namespace Drivers::USB::Xhci {
|
|||||||
// Used for SDR stream stall recovery (RTL2832 bulk IN can STALL on start).
|
// Used for SDR stream stall recovery (RTL2832 bulk IN can STALL on start).
|
||||||
void ResetBulkInEndpoint(uint8_t slotId);
|
void ResetBulkInEndpoint(uint8_t slotId);
|
||||||
|
|
||||||
|
// Clear a halted bulk OUT endpoint and discard the errored/queued TRBs.
|
||||||
|
// Must be called from process context; the class driver reconciles its
|
||||||
|
// outstanding-transfer accounting before submitting fresh work.
|
||||||
|
void ResetBulkOutEndpoint(uint8_t slotId);
|
||||||
|
|
||||||
// True while PollEvents() is draining the event ring. A command submitter
|
// True while PollEvents() is draining the event ring. A command submitter
|
||||||
// reached from inside an event callback must fire-and-forget (not wait),
|
// reached from inside an event callback must fire-and-forget (not wait),
|
||||||
// since a nested PollEvents() is a no-op.
|
// since a nested PollEvents() is a no-op.
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ namespace montauk::abi {
|
|||||||
static constexpr int AUDIO_CTL_PAUSE = 3;
|
static constexpr int AUDIO_CTL_PAUSE = 3;
|
||||||
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
static constexpr int AUDIO_CTL_GET_OUTPUT = 4; // 0=HDA, 1=Bluetooth
|
||||||
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
static constexpr int AUDIO_CTL_SET_OUTPUT = 5; // Switch audio output
|
||||||
static constexpr int AUDIO_CTL_BT_STATUS = 6; // Get Bluetooth status
|
static constexpr int AUDIO_CTL_BT_STATUS = 6; // 0=unavailable, 1=setup, 2=ready
|
||||||
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
static constexpr int AUDIO_CTL_SET_MASTER_VOLUME = 7; // 0-100
|
||||||
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
static constexpr int AUDIO_CTL_GET_MASTER_VOLUME = 8;
|
||||||
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
static constexpr int AUDIO_CTL_SET_MUTE = 9; // 0/1, per-stream
|
||||||
|
|||||||
@@ -550,7 +550,7 @@
|
|||||||
audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth
|
audio_get_output AUDIO_CTL_GET_OUTPUT (4): 0=HDA, 1=Bluetooth
|
||||||
audio_set_output AUDIO_CTL_SET_OUTPUT (5): switch all streams
|
audio_set_output AUDIO_CTL_SET_OUTPUT (5): switch all streams
|
||||||
(SET_OUTPUT, 5) switch a stream's output route
|
(SET_OUTPUT, 5) switch a stream's output route
|
||||||
audio_bt_status AUDIO_CTL_BT_STATUS (6)
|
audio_bt_status AUDIO_CTL_BT_STATUS (6): 0=unavailable, 1=setup, 2=ready
|
||||||
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
|
audio_set_master_volume, _get_ AUDIO_CTL_{SET,GET}_MASTER_VOLUME (7/8), 0-100
|
||||||
audio_set_mute, audio_get_mute AUDIO_CTL_{SET,GET}_MUTE (9/10), per-stream
|
audio_set_mute, audio_get_mute AUDIO_CTL_{SET,GET}_MUTE (9/10), per-stream
|
||||||
audio_set_master_mute, _get_ AUDIO_CTL_{SET,GET}_MASTER_MUTE (11/12)
|
audio_set_master_mute, _get_ AUDIO_CTL_{SET,GET}_MASTER_MUTE (11/12)
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ static void finish_scan() {
|
|||||||
static void do_connect(const uint8_t* addr) {
|
static void do_connect(const uint8_t* addr) {
|
||||||
int r = montauk::bt_connect(addr);
|
int r = montauk::bt_connect(addr);
|
||||||
if (r >= 0) set_status("Connected");
|
if (r >= 0) set_status("Connected");
|
||||||
|
else if (r == -2) set_status("Connected; audio setup failed - use Retry");
|
||||||
else set_status("Connection failed");
|
else set_status("Connection failed");
|
||||||
refresh_devices();
|
refresh_devices();
|
||||||
clamp_scroll();
|
clamp_scroll();
|
||||||
@@ -354,6 +355,10 @@ static bool is_connected(const uint8_t* addr) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool is_audio_ready() {
|
||||||
|
return montauk::audio_bt_status(-1) >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Format helpers
|
// Format helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -565,8 +570,14 @@ static bool handle_click(int mx, int my) {
|
|||||||
if (!row.contains(mx, my)) continue;
|
if (!row.contains(mx, my)) continue;
|
||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
|
Rect retry = row_forget_button_rect(row);
|
||||||
|
bool connected = is_connected(g_scan[i].bdAddr);
|
||||||
|
if (connected && !is_audio_ready() && retry.contains(mx, my)) {
|
||||||
|
do_connect(g_scan[i].bdAddr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (action.contains(mx, my)) {
|
if (action.contains(mx, my)) {
|
||||||
if (is_connected(g_scan[i].bdAddr))
|
if (connected)
|
||||||
do_disconnect(g_scan[i].bdAddr);
|
do_disconnect(g_scan[i].bdAddr);
|
||||||
else
|
else
|
||||||
do_connect(g_scan[i].bdAddr);
|
do_connect(g_scan[i].bdAddr);
|
||||||
@@ -580,6 +591,11 @@ static bool handle_click(int mx, int my) {
|
|||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
if (g_devices[i].connected) {
|
if (g_devices[i].connected) {
|
||||||
|
Rect retry = row_forget_button_rect(row);
|
||||||
|
if (!is_audio_ready() && retry.contains(mx, my)) {
|
||||||
|
do_connect(g_devices[i].bdAddr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (action.contains(mx, my)) {
|
if (action.contains(mx, my)) {
|
||||||
do_disconnect(g_devices[i].bdAddr);
|
do_disconnect(g_devices[i].bdAddr);
|
||||||
return true;
|
return true;
|
||||||
@@ -672,6 +688,7 @@ static bool handle_adapter_key(const montauk::abi::KeyEvent& key) {
|
|||||||
static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
||||||
Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)};
|
Rect list = {0, devices_list_top(), g_win_w, gui_max(devices_list_bottom() - devices_list_top(), 0)};
|
||||||
int fh = system_font_height();
|
int fh = system_font_height();
|
||||||
|
bool bt_audio_ready = is_audio_ready();
|
||||||
|
|
||||||
if (g_device_count == 0) {
|
if (g_device_count == 0) {
|
||||||
draw_centered_text(canvas, list.y, list.h, "No paired devices", theme.text_subtle);
|
draw_centered_text(canvas, list.y, list.h, "No paired devices", theme.text_subtle);
|
||||||
@@ -688,15 +705,17 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
mtk::draw_list_row(canvas, row, false, (i & 1) != 0, theme);
|
||||||
|
|
||||||
bool connected = g_devices[i].connected;
|
bool connected = g_devices[i].connected;
|
||||||
|
bool audio_ready = !connected || bt_audio_ready;
|
||||||
Color dot_color = connected ? GREEN : theme.text_subtle;
|
Color dot_color = connected ? GREEN : theme.text_subtle;
|
||||||
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color);
|
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, dot_color);
|
||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
Rect forget = row_forget_button_rect(row);
|
Rect forget = row_forget_button_rect(row);
|
||||||
int text_x = PAD + 20;
|
int text_x = PAD + 20;
|
||||||
// Connected rows have one button (Disconnect); paired rows have two
|
// A connected row whose media path failed exposes Retry + Disconnect;
|
||||||
// (Connect + Forget), so the text must stop before the leftmost button.
|
// otherwise connected rows need only Disconnect.
|
||||||
int text_w_max = (connected ? action.x : forget.x) - text_x - 10;
|
int text_w_max = (connected && audio_ready ? action.x : forget.x)
|
||||||
|
- text_x - 10;
|
||||||
draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text);
|
draw_text_fit(canvas, text_x, row.y + 8, g_device_names[i], text_w_max, theme.text);
|
||||||
|
|
||||||
char addr_str[24];
|
char addr_str[24];
|
||||||
@@ -704,6 +723,10 @@ static void render_devices_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle);
|
draw_text_fit(canvas, text_x, row.y + 8 + fh + 2, addr_str, text_w_max, theme.text_subtle);
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
if (!audio_ready) {
|
||||||
|
mtk::draw_button(canvas, forget, "Retry", mtk::BUTTON_PRIMARY,
|
||||||
|
button_state(forget, true), theme);
|
||||||
|
}
|
||||||
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
|
mtk::draw_button(canvas, action, "Disconnect", mtk::BUTTON_DANGER,
|
||||||
button_state(action, true), theme);
|
button_state(action, true), theme);
|
||||||
} else {
|
} else {
|
||||||
@@ -721,6 +744,7 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
Rect scan_btn = scan_button_rect();
|
Rect scan_btn = scan_button_rect();
|
||||||
Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)};
|
Rect list = {0, scan_list_top(), g_win_w, gui_max(scan_list_bottom() - scan_list_top(), 0)};
|
||||||
int fh = system_font_height();
|
int fh = system_font_height();
|
||||||
|
bool bt_audio_ready = is_audio_ready();
|
||||||
|
|
||||||
if (g_scanning) {
|
if (g_scanning) {
|
||||||
mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY,
|
mtk::draw_button(canvas, scan_btn, "Scanning...", mtk::BUTTON_SECONDARY,
|
||||||
@@ -747,8 +771,12 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent);
|
fill_circle(canvas, PAD + 6, row.y + ROW_H / 2, 5, theme.accent);
|
||||||
|
|
||||||
Rect action = row_action_button_rect(row);
|
Rect action = row_action_button_rect(row);
|
||||||
|
Rect retry = row_forget_button_rect(row);
|
||||||
int text_x = PAD + 20;
|
int text_x = PAD + 20;
|
||||||
int text_w_max = action.x - text_x - 10;
|
bool conn = is_connected(g_scan[i].bdAddr);
|
||||||
|
bool audio_ready = !conn || bt_audio_ready;
|
||||||
|
int text_w_max = (conn && !audio_ready ? retry.x : action.x)
|
||||||
|
- text_x - 10;
|
||||||
|
|
||||||
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
const char* display_name = g_scan[i].name[0] ? g_scan[i].name : "Unknown Device";
|
||||||
draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text);
|
draw_text_fit(canvas, text_x, row.y + 4, display_name, text_w_max, theme.text);
|
||||||
@@ -760,7 +788,10 @@ static void render_scan_tab(Canvas& canvas, const mtk::Theme& theme) {
|
|||||||
device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi));
|
device_class_str(g_scan[i].classOfDevice), addr_str, rssi_bar(g_scan[i].rssi));
|
||||||
draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle);
|
draw_text_fit(canvas, text_x, row.y + 4 + fh + 2, detail, text_w_max, theme.text_subtle);
|
||||||
|
|
||||||
bool conn = is_connected(g_scan[i].bdAddr);
|
if (conn && !audio_ready) {
|
||||||
|
mtk::draw_button(canvas, retry, "Retry", mtk::BUTTON_PRIMARY,
|
||||||
|
button_state(retry, true), theme);
|
||||||
|
}
|
||||||
mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect",
|
mtk::draw_button(canvas, action, conn ? "Disconnect" : "Connect",
|
||||||
conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY,
|
conn ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY,
|
||||||
button_state(action, true), theme);
|
button_state(action, true), theme);
|
||||||
|
|||||||
Reference in New Issue
Block a user