slop board

This commit is contained in:
legop3
2026-08-19 00:10:41 -04:00
parent ea16e2c67a
commit 6ff60f7d0e
@@ -73,26 +73,25 @@ constexpr uint16_t kMgmtCommandCompleteEvent = 0x0001;
constexpr uint16_t kMgmtCommandStatusEvent = 0x0002;
constexpr uint16_t kMgmtNewSettingsEvent = 0x0006;
constexpr uint16_t kMgmtPinCodeRequestEvent = 0x000e;
constexpr uint16_t kMgmtDeviceFoundEvent = 0x0012;
constexpr uint16_t kMgmtDiscoveringEvent = 0x0013;
constexpr uint16_t kMgmtPinCodeReplyCommand = 0x0016;
constexpr uint16_t kMgmtSetConnectableCommand = 0x0007;
constexpr uint16_t kMgmtSetFastConnectableCommand = 0x0008;
constexpr uint16_t kMgmtStartDiscoveryCommand = 0x0023;
constexpr uint16_t kMgmtStopDiscoveryCommand = 0x0024;
constexpr uint16_t kPrimaryControllerIndex = 0;
constexpr uint8_t kBluetoothClassicAddressType = 0;
constexpr uint8_t kBluetoothClassicDiscoveryMask = 1U << 0;
constexpr uint32_t kDeviceFoundLegacyPairingFlag = 1U << 1;
constexpr uint8_t kEirClassOfDeviceType = 0x0d;
constexpr uint32_t kBalanceBoardClassOfDevice = 0x00002504;
constexpr uint32_t kControllerConnectableSetting = 1U << 1;
constexpr uint32_t kControllerFastConnectableSetting = 1U << 2;
constexpr int kManagementCommandTimeoutMs = 2000;
constexpr int kFrameIntervalMs = 50;
constexpr int kDiscoveryRestartDelayMs = 1000;
// A live bluetoothctl PID is not proof that BlueZ actually began inquiry. Some
// adapters can leave the command parked after accepting its discovery filter,
// which previously made commissioning look active for the command's full
// one-day lifetime while the controller remained `Discovering: no`. Bound that
// unconfirmed startup state so a transient adapter or BlueZ failure is repaired
// automatically instead of requiring a manual foreground scan.
constexpr int kDiscoveryStartDeadlineMs = 5000;
constexpr int kCandidateIdentityRetryMs = 500;
constexpr int kCandidateIdleRetryMs = 5000;
constexpr const char* kDiscoveryTimeoutSeconds = "86400";
constexpr uint16_t kHidControlPsm = 0x0011;
constexpr uint16_t kHidInterruptPsm = 0x0013;
constexpr int kCommissioningConnectWindowMs = 15000;
@@ -116,6 +115,16 @@ struct PairingSharedState {
std::optional<BluetoothAddress> active_target;
std::optional<BluetoothAddress> active_pin;
std::optional<std::string> commissioned_address;
// Discovery commands and events use the same kernel management socket as
// raw Wii PIN replies. The main thread owns socket reads while the
// commissioning thread consumes this small synchronized state, avoiding a
// second reader that could steal PIN or controller-setting events.
std::optional<BluetoothAddress> discovery_candidate;
std::string discovery_error;
bool discovery_start_pending = false;
bool discovery_stop_pending = false;
bool discovery_session_started = false;
bool discovery_active = false;
bool commissioning = false;
bool outbound_connection_requested = false;
};
@@ -132,19 +141,6 @@ struct CommandResult {
std::string output;
};
struct RunningCommand {
pid_t pid = -1;
int output_fd = -1;
std::string pending_output;
std::string transcript;
};
struct DiscoveryCandidate {
BluetoothAddress address;
uint64_t last_seen_at = 0;
uint64_t next_identity_check_at = 0;
};
struct ManagementRuntimeState {
// Runtime reassertions are asynchronous so a temporary controller setting
// change cannot block PIN or HID handling. Track each outstanding opcode to
@@ -230,6 +226,24 @@ std::optional<BluetoothAddress> parse_address(const std::string& raw) {
return address;
}
BluetoothAddress address_from_management_wire(const uint8_t* wire) {
BluetoothAddress address;
if (!wire) return address;
// Management packets carry Bluetooth addresses least-significant byte first,
// while every BlueZ command and user-facing status expects the conventional
// most-significant-byte-first representation. Preserve both forms because
// the original wire bytes are later compared with the kernel PIN request.
std::copy(wire, wire + address.wire.size(), address.wire.begin());
char address_buffer[18]{};
std::snprintf(
address_buffer, sizeof(address_buffer), "%02X:%02X:%02X:%02X:%02X:%02X",
address.wire[5], address.wire[4], address.wire[3],
address.wire[2], address.wire[1], address.wire[0]);
address.display = address_buffer;
return address;
}
CommandResult run_command(const std::vector<std::string>& args) {
CommandResult result;
if (args.empty()) return result;
@@ -275,170 +289,16 @@ CommandResult run_command(const std::vector<std::string>& args) {
return result;
}
RunningCommand start_command(const std::vector<std::string>& args) {
RunningCommand command;
if (args.empty()) return command;
int pipe_fds[2]{};
if (pipe(pipe_fds) != 0) {
command.transcript = std::strerror(errno);
return command;
}
const pid_t pid = fork();
if (pid == 0) {
dup2(pipe_fds[1], STDOUT_FILENO);
dup2(pipe_fds[1], STDERR_FILENO);
close(pipe_fds[0]);
close(pipe_fds[1]);
std::vector<char*> argv;
argv.reserve(args.size() + 1);
for (const auto& arg : args) argv.push_back(const_cast<char*>(arg.c_str()));
argv.push_back(nullptr);
execvp(argv[0], argv.data());
_exit(127);
}
close(pipe_fds[1]);
if (pid < 0) {
command.transcript = std::strerror(errno);
close(pipe_fds[0]);
return command;
}
// Discovery has no predetermined completion time: it must remain active until
// the user wakes the board. A nonblocking pipe lets the commissioning thread
// consume BlueZ events while still honoring server shutdown and maintenance
// commands promptly.
const int current_flags = fcntl(pipe_fds[0], F_GETFL, 0);
if (current_flags >= 0) fcntl(pipe_fds[0], F_SETFL, current_flags | O_NONBLOCK);
command.pid = pid;
command.output_fd = pipe_fds[0];
return command;
}
bool collect_command_output(RunningCommand* command) {
if (!command || command->pid < 0) return false;
std::array<char, 1024> buffer{};
ssize_t count = 0;
while ((count = read(command->output_fd, buffer.data(), buffer.size())) > 0) {
const std::string chunk(buffer.data(), static_cast<std::size_t>(count));
command->pending_output += chunk;
command->transcript += chunk;
// A busy Bluetooth environment can produce an unbounded stream of RSSI
// updates. Retain only the most recent output instead of allowing a
// commissioning session left open for days to grow the worker indefinitely.
constexpr std::size_t max_transcript_size = 8192;
if (command->transcript.size() > max_transcript_size) {
command->transcript.erase(0, command->transcript.size() - max_transcript_size);
}
}
int status = 0;
const pid_t waited = waitpid(command->pid, &status, WNOHANG);
if (waited == 0) return true;
if (waited == command->pid) {
command->pid = -1;
}
return false;
}
void stop_command(RunningCommand* command) {
if (!command) return;
if (command->pid > 0) {
// bluetoothctl normally exits immediately on SIGTERM. Bound that grace
// period so a wedged D-Bus client cannot prevent the server from stopping.
kill(command->pid, SIGTERM);
for (int attempt = 0; attempt < 50 && command->pid > 0; ++attempt) {
collect_command_output(command);
if (command->pid > 0) usleep(10000);
}
if (command->pid > 0) {
kill(command->pid, SIGKILL);
int status = 0;
while (waitpid(command->pid, &status, 0) < 0 && errno == EINTR) {}
command->pid = -1;
}
}
if (command->output_fd >= 0) {
close(command->output_fd);
command->output_fd = -1;
}
}
std::optional<BluetoothAddress> take_discovered_board(
RunningCommand* discovery, bool* discovery_started,
std::vector<DiscoveryCandidate>* candidates) {
if (!discovery) return std::nullopt;
std::size_t newline = discovery->pending_output.find('\n');
while (newline != std::string::npos) {
const std::string line = discovery->pending_output.substr(0, newline);
discovery->pending_output.erase(0, newline + 1);
// bluetoothctl reports filter setup before StartDiscovery completes. Treat
// only this explicit event as proof that button presses can now be seen;
// `SetDiscoveryFilter success` alone is not an active Bluetooth scan.
if (discovery_started && line.find("Discovery started") != std::string::npos) {
*discovery_started = true;
}
const std::size_t device_prefix = line.find("Device ");
if (device_prefix != std::string::npos && line.size() >= device_prefix + 24) {
const auto address = parse_address(line.substr(device_prefix + 7, 17));
if (!address.has_value()) {
newline = discovery->pending_output.find('\n');
continue;
}
// The exact remote name remains definitive whenever the adapter resolves
// it during the board's short red-Sync window.
if (line.find(kBoardBluetoothName) != std::string::npos) return address;
// Some adapters initially report only "Device <address> <address>" and
// never finish remote-name resolution before the board powers down. Keep
// those unresolved devices as candidates so their BlueZ properties can be
// inspected without stopping the discovery session. Named ambient devices
// such as TVs are not candidates, which prevents normal room traffic from
// replacing the panel's useful waiting-for-Sync message.
const std::string trailing = line.substr(device_prefix + 24);
const bool address_only = trailing.empty() ||
trailing.find(address->display) != std::string::npos;
if (address_only && candidates) {
const uint64_t now = monotonic_ms();
const auto existing = std::find_if(
candidates->begin(), candidates->end(),
[&](const DiscoveryCandidate& candidate) {
return candidate.address.display == address->display;
});
if (existing == candidates->end()) {
candidates->push_back({*address, now, now});
emit_status("device-detected", address->display,
"Bluetooth device detected; checking whether it is the Balance Board.");
} else {
// A fresh scan event generally means the physical button was pressed
// again. Recheck immediately even if this candidate had previously
// fallen back to the slower idle retry interval.
existing->last_seen_at = now;
existing->next_identity_check_at = now;
}
}
}
newline = discovery->pending_output.find('\n');
}
return std::nullopt;
}
bool candidate_is_balance_board(const BluetoothAddress& address) {
const CommandResult info = run_command({
"bluetoothctl", "--timeout", "2", "info", address.display});
if (info.output.find(kBoardBluetoothName) != std::string::npos) return true;
// Original Wii input devices identify as legacy-pairing gaming peripherals.
// This fallback is deliberately applied only to an address-only device seen
// during active commissioning. That physical red-Sync action is the selection
// boundary when an adapter cannot resolve Nintendo's remote name in time.
// This fallback is deliberately applied only to an address delivered by the
// kernel's legacy-pairing Device Found event during active commissioning.
// That physical red-Sync action is the selection boundary when an adapter
// cannot resolve Nintendo's remote name in time.
const bool gaming_peripheral =
info.output.find("Class: 0x00002504") != std::string::npos &&
info.output.find("Icon: input-gaming") != std::string::npos;
@@ -502,7 +362,11 @@ std::optional<BluetoothAddress> find_default_controller() {
return std::nullopt;
}
void commissioning_loop(PairingSharedState* shared) {
bool start_management_discovery(int fd, PairingSharedState* shared,
std::string* error);
void stop_management_discovery(int fd, PairingSharedState* shared);
void commissioning_loop(PairingSharedState* shared, int management_fd) {
while (running.load()) {
bool should_commission = false;
{
@@ -516,112 +380,62 @@ void commissioning_loop(PairingSharedState* shared) {
}
emit_status("commissioning");
// Commissioning must be listening before the board's short red-Sync window
// begins. Keep one BlueZ discovery client alive continuously and consume its
// own event stream. The previous bounded scan exited for twelve seconds at a
// time and then queried a second client, making successful discovery depend
// on when the physical button happened to be pressed.
// BlueZ's command-line client exits after the SetDiscoveryFilter callback
// unless non-interactive mode has a timeout. Its explicit monitor mode is
// equally important because current bluetoothctl versions otherwise omit
// the asynchronous `[NEW]` and `[CHG]` device lines parsed below.
//
// stdout and stderr are worker-owned pipes rather than a terminal. Fedora's
// bluetoothctl therefore block-buffers even `Discovery started`, leaving a
// genuinely active scan indistinguishable from a hung process until the
// one-day command exits. stdbuf replaces itself with bluetoothctl in the
// same PID while forcing both streams to flush each line immediately. That
// gives the watchdog and device parser live evidence without adding a
// pseudo-terminal or another process for the supervisor to manage.
//
// The one-day timeout keeps that monitored client alive for unattended
// commissioning; the worker normally stops it itself as soon as the board
// appears and restarts it if the day expires.
RunningCommand discovery = start_command({
"stdbuf", "-oL", "-eL", "bluetoothctl", "--monitor", "--timeout",
kDiscoveryTimeoutSeconds, "scan", "bredr"});
if (discovery.pid < 0) {
emit_status("error", "", "could not start Bluetooth discovery: " +
command_error_summary(discovery.transcript, "unknown process error"));
// Discovery is deliberately performed through the kernel management
// socket already required for Wii PIN replies. Long-running bluetoothctl
// output proved version- and terminal-dependent on the production server;
// MGMT Device Found events are the stable interface underneath BlueZ and
// arrive on this socket without parsing human-oriented terminal output.
std::string discovery_error;
if (!start_management_discovery(
management_fd, shared, &discovery_error)) {
emit_status("error", "", "Bluetooth discovery could not start: " +
discovery_error);
std::this_thread::sleep_for(std::chrono::milliseconds(kDiscoveryRestartDelayMs));
continue;
}
std::optional<BluetoothAddress> address;
bool discovery_started = false;
// Measure from the successful fork, not from the first output line. A
// wedged bluetoothctl may emit `SetDiscoveryFilter success` and then stay
// alive forever without producing the only confirmation that matters:
// `Discovery started`.
const uint64_t discovery_launched_at = monotonic_ms();
std::vector<DiscoveryCandidate> candidates;
while (running.load() && !address.has_value()) {
const bool discovery_running = collect_command_output(&discovery);
const bool was_started = discovery_started;
address = take_discovered_board(
&discovery, &discovery_started, &candidates);
if (!was_started && discovery_started) {
// This status clears any prior scanner error and tells the browser that
// the server is genuinely listening for the board's red Sync button.
emit_status("discovering");
}
if (address.has_value()) break;
const uint64_t now = monotonic_ms();
for (auto& candidate : candidates) {
if (now < candidate.next_identity_check_at) continue;
if (candidate_is_balance_board(candidate.address)) {
address = candidate.address;
break;
}
// Query quickly while a newly pressed board is still awake, then back
// off once it has been absent for several seconds. A later scan event
// resets this deadline immediately, so another button press never waits
// for the idle interval and an unidentified device cannot cause a
// permanent stream of bluetoothctl processes.
const bool recently_seen = now - candidate.last_seen_at < 10000;
candidate.next_identity_check_at = now +
(recently_seen ? kCandidateIdentityRetryMs : kCandidateIdleRetryMs);
}
if (address.has_value()) break;
if (!discovery_running) {
const std::string detail = command_error_summary(
discovery.transcript, "bluetoothctl exited unexpectedly");
emit_status("error", "", discovery_started
? "Bluetooth scanner stopped unexpectedly; retrying automatically: " + detail
: "Bluetooth scanner exited before discovery started; retrying automatically: " + detail);
break;
}
if (!discovery_started &&
now - discovery_launched_at >= kDiscoveryStartDeadlineMs) {
// Terminating this specific client is safe: stop_command() only owns
// the child created above, and the outer commissioning loop immediately
// starts a clean replacement. Preserve bluetoothctl's actual output in
// the status so the panel explains whether BlueZ stalled after filter
// setup or failed in some other adapter-specific way.
const std::string detail = command_error_summary(
discovery.transcript, "bluetoothctl produced no startup response");
emit_status(
"error", "",
"Bluetooth scanner did not start within 5 seconds; retrying automatically: " +
detail);
break;
}
std::optional<BluetoothAddress> candidate;
bool still_commissioning = false;
{
std::lock_guard<std::mutex> lock(shared->mutex);
still_commissioning = shared->commissioning &&
!shared->commissioned_address.has_value();
candidate = shared->discovery_candidate;
shared->discovery_candidate.reset();
discovery_error = shared->discovery_error;
}
if (!still_commissioning) break;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
if (!discovery_error.empty()) {
emit_status("error", "", discovery_error);
break;
}
if (candidate.has_value()) {
emit_status("device-detected", candidate->display,
"Classic Bluetooth device detected; checking whether it is the Balance Board.");
// Class, icon, and legacy-pairing properties can arrive just after the
// first raw inquiry result. Retry that bounded local property lookup at
// quarter-second intervals while the board is awake; this replaces the
// old dependence on a later human-readable bluetoothctl change line.
// The exact identity gate remains mandatory, so an unrelated controller
// can never arm the privileged Wii PIN response.
for (int attempt = 0; attempt < 5 && !address.has_value(); ++attempt) {
if (candidate_is_balance_board(*candidate)) {
address = candidate;
break;
}
if (attempt < 4) {
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(25));
}
if (!address.has_value()) {
stop_command(&discovery);
stop_management_discovery(management_fd, shared);
if (running.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(kDiscoveryRestartDelayMs));
}
@@ -630,8 +444,8 @@ void commissioning_loop(PairingSharedState* shared) {
const auto controller = find_default_controller();
if (!controller.has_value()) {
stop_command(&discovery);
emit_status("commissioning", address->display,
stop_management_discovery(management_fd, shared);
emit_status("error", address->display,
"no powered Bluetooth controller is available for pairing");
std::this_thread::sleep_for(std::chrono::milliseconds(kDiscoveryRestartDelayMs));
continue;
@@ -652,10 +466,10 @@ void commissioning_loop(PairingSharedState* shared) {
// charge of everything else.
const CommandResult pair_result = run_command({
"bluetoothctl", "--timeout", "12", "--agent", "NoInputNoOutput", "pair", address->display});
// Keep the discovery owner alive through Pair(). BlueZ documents pairing by
// address as requiring an active scan report, and the board may stop its
// Sync window before a new discovery client could be established.
stop_command(&discovery);
// Keep kernel discovery alive through Pair(). BlueZ pairing by address
// requires the fresh device record, and the board's Sync window is too
// short to stop and recreate discovery before bonding begins.
stop_management_discovery(management_fd, shared);
{
std::lock_guard<std::mutex> lock(shared->mutex);
@@ -664,7 +478,7 @@ void commissioning_loop(PairingSharedState* shared) {
}
if (!command_succeeded(pair_result)) {
emit_status("commissioning", address->display,
emit_status("error", address->display,
"pairing failed: " + command_error_summary(
pair_result.output, "BlueZ returned an unknown pairing error"));
std::this_thread::sleep_for(std::chrono::milliseconds(kDiscoveryRestartDelayMs));
@@ -727,6 +541,38 @@ uint32_t read_u32_le(const uint8_t* input) {
(static_cast<uint32_t>(input[3]) << 24);
}
bool management_event_has_balance_board_class(const uint8_t* payload,
uint16_t payload_size) {
// Device Found has a fixed 14-byte prefix followed by standard EIR fields.
// Each field begins with a byte count that includes its one-byte type. Parse
// defensively because this data originates over the radio and a malformed
// length must never let commissioning inspect beyond the management packet.
constexpr std::size_t fixed_size = 14;
if (!payload || payload_size < fixed_size) return false;
const uint16_t eir_size = read_u16_le(payload + 12);
if (eir_size > payload_size - fixed_size) return false;
const uint8_t* eir = payload + fixed_size;
std::size_t offset = 0;
while (offset < eir_size) {
const uint8_t field_size = eir[offset];
if (field_size == 0) break;
if (offset + 1 + field_size > eir_size) return false;
const uint8_t field_type = eir[offset + 1];
const std::size_t data_size = field_size - 1;
if (field_type == kEirClassOfDeviceType && data_size >= 3) {
const uint32_t device_class =
static_cast<uint32_t>(eir[offset + 2]) |
(static_cast<uint32_t>(eir[offset + 3]) << 8) |
(static_cast<uint32_t>(eir[offset + 4]) << 16);
return device_class == kBalanceBoardClassOfDevice;
}
offset += 1 + field_size;
}
return false;
}
std::string management_status_description(uint8_t status) {
// These are the management statuses that setting controller modes can
// realistically return. Retain the numeric value as well because it remains
@@ -761,6 +607,112 @@ bool write_management_boolean_command(int fd, uint16_t opcode, bool enabled) {
static_cast<ssize_t>(packet.size());
}
bool write_management_discovery_command(int fd, uint16_t opcode) {
if (fd < 0) return false;
constexpr std::size_t header_size = 6;
std::array<uint8_t, header_size + 1> packet{};
write_u16_le(packet.data(), opcode);
write_u16_le(packet.data() + 2, kPrimaryControllerIndex);
write_u16_le(packet.data() + 4, 1);
// The Balance Board is a Classic Bluetooth device. Restricting discovery to
// BR/EDR avoids irrelevant LE advertisements and ensures every Device Found
// event uses the address type expected by the Wii pairing path.
packet[header_size] = kBluetoothClassicDiscoveryMask;
return write(fd, packet.data(), packet.size()) ==
static_cast<ssize_t>(packet.size());
}
bool start_management_discovery(int fd, PairingSharedState* shared,
std::string* error) {
if (fd < 0 || !shared) {
if (error) *error = "Bluetooth management socket is unavailable";
return false;
}
{
std::lock_guard<std::mutex> lock(shared->mutex);
shared->discovery_candidate.reset();
shared->discovery_error.clear();
shared->discovery_start_pending = true;
shared->discovery_stop_pending = false;
shared->discovery_session_started = false;
shared->discovery_active = false;
}
if (!write_management_discovery_command(fd, kMgmtStartDiscoveryCommand)) {
const std::string detail = "could not send Start Discovery: " +
std::string(std::strerror(errno));
{
std::lock_guard<std::mutex> lock(shared->mutex);
shared->discovery_start_pending = false;
shared->discovery_error = detail;
}
if (error) *error = detail;
return false;
}
// Command Complete proves the kernel accepted the session, while the
// Discovering event proves inquiry is actually active on the controller.
// Require both so the UI can never repeat the earlier false "listening"
// state where a process existed but no radio scan was running.
const uint64_t deadline = monotonic_ms() + kDiscoveryStartDeadlineMs;
while (running.load() && monotonic_ms() < deadline) {
std::string discovery_error;
bool ready = false;
{
std::lock_guard<std::mutex> lock(shared->mutex);
discovery_error = shared->discovery_error;
ready = shared->discovery_session_started && shared->discovery_active;
}
if (!discovery_error.empty()) {
if (error) *error = discovery_error;
return false;
}
if (ready) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
if (error) *error = "kernel accepted no active BR/EDR discovery session within 5 seconds";
stop_management_discovery(fd, shared);
return false;
}
void stop_management_discovery(int fd, PairingSharedState* shared) {
if (fd < 0 || !shared) return;
bool should_stop = false;
{
std::lock_guard<std::mutex> lock(shared->mutex);
should_stop = shared->discovery_start_pending ||
shared->discovery_session_started || shared->discovery_active;
shared->discovery_candidate.reset();
if (should_stop) shared->discovery_stop_pending = true;
}
if (!should_stop) return;
if (!write_management_discovery_command(fd, kMgmtStopDiscoveryCommand)) {
std::lock_guard<std::mutex> lock(shared->mutex);
shared->discovery_stop_pending = false;
shared->discovery_error = "could not send Stop Discovery: " +
std::string(std::strerror(errno));
return;
}
// Pairing retries should not collide with a previous inquiry session. Wait
// briefly for the matching command response, but never let a misbehaving
// adapter hold server shutdown or commissioning indefinitely.
const uint64_t deadline = monotonic_ms() + kManagementCommandTimeoutMs;
while (running.load() && monotonic_ms() < deadline) {
bool stopped = false;
{
std::lock_guard<std::mutex> lock(shared->mutex);
stopped = !shared->discovery_stop_pending &&
!shared->discovery_session_started;
}
if (stopped) return;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
bool set_management_boolean_and_wait(int fd, uint16_t opcode,
const std::string& setting_name,
std::string* error) {
@@ -877,16 +829,44 @@ void process_management_events(int fd, PairingSharedState* shared,
if (count < 6 + payload_size) continue;
if ((event == kMgmtCommandCompleteEvent || event == kMgmtCommandStatusEvent) &&
payload_size >= 3 && management) {
payload_size >= 3) {
const uint16_t opcode = read_u16_le(buffer.data() + 6);
const uint8_t status = buffer[8];
if (opcode == kMgmtStartDiscoveryCommand ||
opcode == kMgmtStopDiscoveryCommand) {
std::lock_guard<std::mutex> lock(shared->mutex);
if (opcode == kMgmtStartDiscoveryCommand) {
shared->discovery_start_pending = false;
if (status == 0) {
shared->discovery_session_started = true;
} else {
shared->discovery_session_started = false;
shared->discovery_active = false;
shared->discovery_error = "Start Discovery was rejected: " +
management_status_description(status);
}
} else {
shared->discovery_stop_pending = false;
if (status == 0) {
shared->discovery_start_pending = false;
shared->discovery_session_started = false;
shared->discovery_active = false;
} else {
shared->discovery_error = "Stop Discovery was rejected: " +
management_status_description(status);
}
}
continue;
}
bool recognized = false;
std::string setting_name;
if (opcode == kMgmtSetConnectableCommand) {
if (management && opcode == kMgmtSetConnectableCommand) {
management->connectable_pending = false;
recognized = true;
setting_name = "connectable setting";
} else if (opcode == kMgmtSetFastConnectableCommand) {
} else if (management && opcode == kMgmtSetFastConnectableCommand) {
management->fast_connectable_pending = false;
recognized = true;
setting_name = "fast connectable setting";
@@ -898,6 +878,50 @@ void process_management_events(int fd, PairingSharedState* shared,
continue;
}
if (event == kMgmtDiscoveringEvent && payload_size >= 2 &&
adapter_index == kPrimaryControllerIndex) {
const uint8_t address_types = buffer[6];
const bool active = buffer[7] != 0;
bool announce_discovery = false;
{
std::lock_guard<std::mutex> lock(shared->mutex);
if (shared->commissioning &&
(address_types & kBluetoothClassicDiscoveryMask) != 0) {
announce_discovery = active && !shared->discovery_active;
shared->discovery_active = active;
}
}
if (announce_discovery) emit_status("discovering");
continue;
}
if (event == kMgmtDeviceFoundEvent && payload_size >= 14 &&
adapter_index == kPrimaryControllerIndex) {
const uint8_t* payload = buffer.data() + 6;
const uint8_t address_type = payload[6];
const uint32_t flags = read_u32_le(payload + 8);
const bool balance_board_class =
management_event_has_balance_board_class(payload, payload_size);
// Some controllers provide the gaming-device class in the first inquiry
// result and add Legacy Pairing only after name resolution; others do the
// reverse. Either radio-level signal is narrow enough to justify the
// bounded BlueZ property check, while ordinary Classic devices never
// disturb the panel or launch repeated identity commands.
if (address_type == kBluetoothClassicAddressType &&
(balance_board_class ||
(flags & kDeviceFoundLegacyPairingFlag) != 0)) {
const BluetoothAddress candidate =
address_from_management_wire(payload);
std::lock_guard<std::mutex> lock(shared->mutex);
if (shared->commissioning &&
!shared->commissioned_address.has_value()) {
shared->discovery_candidate = candidate;
}
}
continue;
}
if (event == kMgmtNewSettingsEvent && payload_size >= 4 &&
adapter_index == kPrimaryControllerIndex && management) {
const uint32_t settings = read_u32_le(buffer.data() + 6);
@@ -1438,7 +1462,8 @@ int main() {
std::thread commission_thread;
std::thread connection_thread;
if (bluetooth_startup_ready) {
commission_thread = std::thread(commissioning_loop, &pairing);
commission_thread = std::thread(
commissioning_loop, &pairing, management_fd);
connection_thread = std::thread(direct_connection_loop, &pairing, boards);
}
std::thread input_thread(stdin_loop, &pairing);