mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
ui race condition fix
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-BNuqn1jZ.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CAUR32ig.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D2qTDWX0.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -401,6 +401,15 @@ function handleWorkerMessage(message = {}) {
|
||||
updateStatus('starting', 'Starting Bluetooth discovery.');
|
||||
} else if (workerState === 'discovering') {
|
||||
updateStatus('waiting-for-sync', 'Press the red Sync button underneath the board.');
|
||||
} else if (workerState === 'device-detected') {
|
||||
// Preserve the worker's exact identification stage instead of leaving the
|
||||
// panel apparently unchanged when an adapter sees only the board's address.
|
||||
// This is intentionally not a feed alert because ambient unresolved devices
|
||||
// can appear during commissioning and the state is already visible locally.
|
||||
updateStatus(
|
||||
'identifying',
|
||||
message.error || 'Bluetooth device detected; checking whether it is the Balance Board.',
|
||||
);
|
||||
} else if (workerState === 'pairing') {
|
||||
updateStatus('pairing', 'Board found. Pairing now.');
|
||||
} else if (workerState === 'connected') {
|
||||
|
||||
@@ -83,6 +83,8 @@ constexpr uint32_t kControllerFastConnectableSetting = 1U << 2;
|
||||
constexpr int kManagementCommandTimeoutMs = 2000;
|
||||
constexpr int kFrameIntervalMs = 50;
|
||||
constexpr int kDiscoveryRestartDelayMs = 1000;
|
||||
constexpr int kCandidateIdentityRetryMs = 500;
|
||||
constexpr int kCandidateIdleRetryMs = 5000;
|
||||
constexpr const char* kDiscoveryTimeoutSeconds = "86400";
|
||||
constexpr uint16_t kHidControlPsm = 0x0011;
|
||||
constexpr uint16_t kHidInterruptPsm = 0x0013;
|
||||
@@ -130,6 +132,12 @@ struct RunningCommand {
|
||||
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
|
||||
@@ -353,8 +361,9 @@ void stop_command(RunningCommand* command) {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<BluetoothAddress> take_discovered_board(RunningCommand* discovery,
|
||||
bool* discovery_started) {
|
||||
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) {
|
||||
@@ -368,14 +377,45 @@ std::optional<BluetoothAddress> take_discovered_board(RunningCommand* discovery,
|
||||
*discovery_started = true;
|
||||
}
|
||||
|
||||
// A Classic device is initially announced by address and receives its name
|
||||
// in a later change event. Parse every complete scan line so either BlueZ
|
||||
// form works, but require the exact Nintendo board name before accepting an
|
||||
// address. A nearby Wiimote must never become eligible for the raw PIN.
|
||||
if (line.find(kBoardBluetoothName) != std::string::npos) {
|
||||
const std::size_t device_prefix = line.find("Device ");
|
||||
if (device_prefix != std::string::npos && line.size() >= device_prefix + 24) {
|
||||
if (auto address = parse_address(line.substr(device_prefix + 7, 17))) return address;
|
||||
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');
|
||||
@@ -383,6 +423,23 @@ std::optional<BluetoothAddress> take_discovered_board(RunningCommand* discovery,
|
||||
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.
|
||||
const bool gaming_peripheral =
|
||||
info.output.find("Class: 0x00002504") != std::string::npos &&
|
||||
info.output.find("Icon: input-gaming") != std::string::npos;
|
||||
const bool legacy_pairing =
|
||||
info.output.find("LegacyPairing: yes") != std::string::npos;
|
||||
return gaming_peripheral && legacy_pairing;
|
||||
}
|
||||
|
||||
std::string command_error_summary(const std::string& raw, const std::string& fallback) {
|
||||
std::string summary;
|
||||
summary.reserve(std::min<std::size_t>(raw.size(), 400));
|
||||
@@ -472,16 +529,37 @@ void commissioning_loop(PairingSharedState* shared) {
|
||||
|
||||
std::optional<BluetoothAddress> address;
|
||||
bool discovery_started = false;
|
||||
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);
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user