mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
slop
This commit is contained in:
@@ -205,10 +205,12 @@ if [[ ! -f "$CONFIG_PATH" ]]; then
|
||||
fi
|
||||
|
||||
echo " Configuring BlueZ for the Wii Balance Board"
|
||||
if ! modinfo hid-wiimote >/dev/null 2>&1; then
|
||||
echo "The running kernel does not provide hid-wiimote; install a Fedora kernel with that module." >&2
|
||||
exit 1
|
||||
fi
|
||||
for balance_board_module in hidp hid-wiimote; do
|
||||
if ! modinfo "$balance_board_module" >/dev/null 2>&1; then
|
||||
echo "The running kernel does not provide $balance_board_module; install a Fedora kernel with that module." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
install -d -m 0755 /etc/bluetooth /etc/modules-load.d
|
||||
touch "$BLUEZ_INPUT_CONFIG"
|
||||
@@ -222,14 +224,17 @@ chmod 0644 "$BLUEZ_INPUT_CONFIG"
|
||||
crudini --set "$BLUEZ_INPUT_CONFIG" General UserspaceHID false
|
||||
crudini --set "$BLUEZ_INPUT_CONFIG" General ClassicBondedOnly false
|
||||
|
||||
# The service consumes calibrated evdev axes from the kernel driver. Load the
|
||||
# driver now and at every boot so a short board wake is never lost while an
|
||||
# operator manually prepares the server.
|
||||
# UserspaceHID=false hands the Bluetooth HID channels to the kernel's `hidp`
|
||||
# transport, which then creates a HID device for `hid-wiimote` to calibrate.
|
||||
# They are separate modules, so load and persist both explicitly; loading only
|
||||
# hid-wiimote leaves BlueZ with no kernel transport to create the input socket.
|
||||
cat > "$BALANCE_BOARD_MODULES_LOAD" <<'EOF'
|
||||
# MultiRoombaRover Wii Balance Board support.
|
||||
hidp
|
||||
hid-wiimote
|
||||
EOF
|
||||
chmod 0644 "$BALANCE_BOARD_MODULES_LOAD"
|
||||
modprobe hidp
|
||||
modprobe hid-wiimote
|
||||
|
||||
# BlueZ reads input.conf only at daemon startup. Restart it before the rover
|
||||
|
||||
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
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-O4nEBzvV.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-dx-kgaua.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dpy8jv9j.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -110,17 +110,10 @@ function processFrame(message = {}) {
|
||||
function diagnosticDetail(message = {}) {
|
||||
if (message.inputError) return `Input device: ${message.inputError}`;
|
||||
if (message.bluetoothError) return `Bluetooth: ${message.bluetoothError}`;
|
||||
if (message.reconnectDetail) {
|
||||
const reconnectDetail = String(message.reconnectDetail);
|
||||
// bluetoothctl can return a long transcript containing every property
|
||||
// change around one failed attempt. Translate the known Wii HID socket
|
||||
// failure into one useful sentence so the panel remains readable while the
|
||||
// worker continues its automatic retries in the background.
|
||||
if (reconnectDetail.includes('br-connection-create-socket')) {
|
||||
return 'Bluetooth rejected the input connection. The server is retrying automatically.';
|
||||
}
|
||||
return reconnectDetail;
|
||||
}
|
||||
// The native worker now reduces controller events and the matching journal
|
||||
// entry to one concrete stage-specific sentence. Preserve that evidence
|
||||
// verbatim instead of replacing it with generic retry wording here.
|
||||
if (message.reconnectDetail) return String(message.reconnectDetail);
|
||||
return 'Press the front power button. The server will keep trying to connect.';
|
||||
}
|
||||
|
||||
@@ -144,10 +137,14 @@ function handleWorkerMessage(message = {}) {
|
||||
if (message.type === 'diagnostics') {
|
||||
const bluetoothConnected = Boolean(message.bluetooth?.connected);
|
||||
const inputReady = message.inputState === 'ready';
|
||||
const reconnectStage = String(message.reconnectStage || 'waiting');
|
||||
if (bluetoothConnected && inputReady) {
|
||||
updateStatus('connected', 'Connected. Waiting for weight readings.');
|
||||
} else if (bluetoothConnected) {
|
||||
updateStatus('connecting', message.inputError || 'Bluetooth connected. Waiting for the input device.');
|
||||
} else if (reconnectStage === 'input-failed' || reconnectStage === 'connection-failed') {
|
||||
connected = false;
|
||||
updateStatus('connection-failed', diagnosticDetail(message));
|
||||
} else if (store.address) {
|
||||
connected = false;
|
||||
updateStatus('waiting', diagnosticDetail(message));
|
||||
@@ -168,6 +165,11 @@ function handleWorkerMessage(message = {}) {
|
||||
tareSamples = [];
|
||||
tareKg = 0;
|
||||
updateStatus('zeroing', 'Connected. Keep the board empty for one second while it zeros.');
|
||||
} else if (workerState === 'link-detected') {
|
||||
connected = false;
|
||||
// A controller connection proves the physical board answered, but Bluetooth
|
||||
// does not identify which physical button woke it. Keep the message exact.
|
||||
updateStatus('connecting', 'Board responded. Bluetooth connected; creating the input device.');
|
||||
} else if (workerState === 'waiting') {
|
||||
connected = false;
|
||||
latestFrame = null;
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
@@ -56,6 +57,9 @@ constexpr uint16_t kHciChannelControl = 3;
|
||||
constexpr uint16_t kHciDeviceNone = 0xffff;
|
||||
constexpr uint16_t kMgmtPinCodeRequestEvent = 0x000e;
|
||||
constexpr uint16_t kMgmtPinCodeReplyCommand = 0x0016;
|
||||
constexpr uint16_t kMgmtDeviceConnectedEvent = 0x000b;
|
||||
constexpr uint16_t kMgmtDeviceDisconnectedEvent = 0x000c;
|
||||
constexpr uint16_t kMgmtConnectFailedEvent = 0x000d;
|
||||
constexpr uint8_t kBluetoothClassicAddressType = 0;
|
||||
constexpr int kFrameIntervalMs = 50;
|
||||
constexpr int kDeviceScanIntervalMs = 500;
|
||||
@@ -63,6 +67,7 @@ constexpr int kDiscoveryRestartDelayMs = 1000;
|
||||
constexpr const char* kDiscoveryTimeoutSeconds = "86400";
|
||||
constexpr int kBluetoothMonitorIntervalMs = 2000;
|
||||
constexpr int kReconnectAttemptIntervalMs = 3000;
|
||||
constexpr int kConnectionEvidenceWindowMs = 5000;
|
||||
constexpr int kBatteryRefreshMs = 5000;
|
||||
|
||||
std::atomic<bool> running{true};
|
||||
@@ -82,6 +87,14 @@ struct PairingSharedState {
|
||||
std::optional<std::string> commissioned_address;
|
||||
std::string input_state = "not-detected";
|
||||
std::string input_error;
|
||||
// The management socket sees the actual controller-level events even when
|
||||
// bluetoothctl reduces them to a generic D-Bus failure. Timestamps let the
|
||||
// reconnect thread associate those events with one specific attempt.
|
||||
uint64_t last_radio_connected_at = 0;
|
||||
uint64_t last_radio_disconnected_at = 0;
|
||||
uint64_t last_connect_failed_at = 0;
|
||||
uint8_t last_disconnect_reason = 0;
|
||||
uint8_t last_connect_status = 0;
|
||||
bool commissioning = false;
|
||||
bool pairing_available = true;
|
||||
};
|
||||
@@ -178,6 +191,7 @@ void emit_diagnostics(const std::string& address,
|
||||
const BluetoothDeviceState& bluetooth,
|
||||
const std::string& input_state,
|
||||
const std::string& input_error,
|
||||
const std::string& reconnect_stage,
|
||||
const std::string& reconnect_detail) {
|
||||
// Bluetooth bonding, the current radio link, and Linux evdev readiness are
|
||||
// separate layers. Report each one explicitly so the server never has to
|
||||
@@ -198,6 +212,9 @@ void emit_diagnostics(const std::string& address,
|
||||
if (!input_error.empty()) {
|
||||
fields << ",\"inputError\":\"" << json_escape(input_error) << "\"";
|
||||
}
|
||||
if (!reconnect_stage.empty()) {
|
||||
fields << ",\"reconnectStage\":\"" << json_escape(reconnect_stage) << "\"";
|
||||
}
|
||||
if (!reconnect_detail.empty()) {
|
||||
fields << ",\"reconnectDetail\":\"" << json_escape(reconnect_detail) << "\"";
|
||||
}
|
||||
@@ -448,6 +465,120 @@ std::string command_error_summary(const std::string& raw, const std::string& fal
|
||||
return summary.empty() ? fallback : summary;
|
||||
}
|
||||
|
||||
std::string lowercase(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string relevant_log_detail(const std::string& raw) {
|
||||
std::istringstream lines(raw);
|
||||
std::string line;
|
||||
std::string selected;
|
||||
while (std::getline(lines, line)) {
|
||||
const std::string lowered = lowercase(line);
|
||||
// Bluetoothd and the kernel normally log the useful transport error on a
|
||||
// line containing one of these terms. Ignore routine property changes so
|
||||
// the panel receives the cause, not another multi-line command transcript.
|
||||
const bool relevant =
|
||||
lowered.find("hidp") != std::string::npos ||
|
||||
lowered.find("uhid") != std::string::npos ||
|
||||
lowered.find("wiimote") != std::string::npos ||
|
||||
lowered.find("profiles/input") != std::string::npos ||
|
||||
lowered.find("host is down") != std::string::npos ||
|
||||
lowered.find("permission denied") != std::string::npos ||
|
||||
lowered.find("not supported") != std::string::npos ||
|
||||
lowered.find("failed") != std::string::npos ||
|
||||
lowered.find("error") != std::string::npos;
|
||||
if (!relevant) continue;
|
||||
const std::string cleaned = command_error_summary(line, "");
|
||||
if (!cleaned.empty()) selected = cleaned;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
std::string hardware_setup_failure() {
|
||||
std::ifstream input_config("/etc/bluetooth/input.conf");
|
||||
std::ostringstream input_config_text;
|
||||
input_config_text << input_config.rdbuf();
|
||||
std::string normalized = lowercase(input_config_text.str());
|
||||
normalized.erase(std::remove_if(normalized.begin(), normalized.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch);
|
||||
}), normalized.end());
|
||||
|
||||
// These checks describe the machine state after a failed HID creation. They
|
||||
// match the transport path installed by install_server.sh and turn a missed
|
||||
// installer/restart step into an explicit panel error instead of speculation.
|
||||
if (normalized.find("userspacehid=false") == std::string::npos) {
|
||||
return "BlueZ UserspaceHID=false is not active in /etc/bluetooth/input.conf";
|
||||
}
|
||||
if (access("/sys/module/hidp", F_OK) != 0) {
|
||||
return "the kernel HIDP Bluetooth transport is not loaded";
|
||||
}
|
||||
if (access("/sys/module/hid_wiimote", F_OK) != 0) {
|
||||
return "the kernel hid-wiimote driver is not loaded";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string connection_log_detail(uint64_t attempt_started_epoch_seconds) {
|
||||
// Query only the tiny time window belonging to this connection attempt. On
|
||||
// Fedora the service owner normally has journal access through its ordinary
|
||||
// account groups; if it does not, the management-event diagnosis below still
|
||||
// remains available and no privileged helper is introduced.
|
||||
const uint64_t lookback_seconds = (kConnectionEvidenceWindowMs / 1000) + 1;
|
||||
const std::string since = "@" + std::to_string(
|
||||
attempt_started_epoch_seconds > lookback_seconds
|
||||
? attempt_started_epoch_seconds - lookback_seconds
|
||||
: 0);
|
||||
const CommandResult bluetooth_log = run_command({
|
||||
"journalctl", "--quiet", "--no-pager", "--output=cat",
|
||||
"--since", since, "--unit", "bluetooth.service"});
|
||||
if (bluetooth_log.exit_code == 0) {
|
||||
if (const std::string detail = relevant_log_detail(bluetooth_log.output);
|
||||
!detail.empty()) {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
const CommandResult kernel_log = run_command({
|
||||
"journalctl", "--quiet", "--no-pager", "--output=cat",
|
||||
"--since", since, "--dmesg"});
|
||||
return kernel_log.exit_code == 0 ? relevant_log_detail(kernel_log.output) : "";
|
||||
}
|
||||
|
||||
std::string disconnect_reason_detail(uint8_t reason) {
|
||||
switch (reason) {
|
||||
case 0x01: return "Bluetooth connection timed out";
|
||||
case 0x02: return "the local Bluetooth stack closed the connection";
|
||||
case 0x03: return "the board closed the connection";
|
||||
case 0x04: return "Bluetooth authentication failed";
|
||||
case 0x05: return "the local host suspended the connection";
|
||||
default: return "the Bluetooth connection closed for an unspecified reason";
|
||||
}
|
||||
}
|
||||
|
||||
std::string connect_status_detail(uint8_t status) {
|
||||
// These are the standard Bluetooth controller status values returned by the
|
||||
// kernel management API. Naming the common failures makes an unanswered wake
|
||||
// distinguishable from a bad stored key or a transport timeout.
|
||||
switch (status) {
|
||||
case 0x04: return "the board did not answer the Bluetooth page";
|
||||
case 0x05: return "Bluetooth authentication failed";
|
||||
case 0x06: return "the stored Bluetooth PIN or link key is missing";
|
||||
case 0x08: return "the Bluetooth connection timed out";
|
||||
case 0x10: return "the board did not accept the connection in time";
|
||||
default: {
|
||||
std::ostringstream detail;
|
||||
detail << "Bluetooth controller rejected the connection (status 0x"
|
||||
<< std::hex << std::setw(2) << std::setfill('0')
|
||||
<< static_cast<int>(status) << ")";
|
||||
return detail.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool command_succeeded(const CommandResult& result) {
|
||||
if (result.exit_code != 0) return false;
|
||||
return result.output.find("Failed") == std::string::npos &&
|
||||
@@ -504,25 +635,10 @@ std::string prepare_known_device(const BluetoothAddress& address) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string trust_and_connect(const BluetoothAddress& address) {
|
||||
if (const std::string prepare_error = prepare_known_device(address);
|
||||
!prepare_error.empty()) {
|
||||
return prepare_error;
|
||||
}
|
||||
// A board remains awake for only a short window after Sync. Connecting the
|
||||
// HID profile immediately is what teaches it to initiate future connections
|
||||
// when its front power button is pressed.
|
||||
const CommandResult connect = run_command({
|
||||
"bluetoothctl", "--timeout", "8", "connect", address.display});
|
||||
if (!command_succeeded(connect)) {
|
||||
return "initial connection failed: " +
|
||||
command_error_summary(connect.output, "BlueZ returned no detail");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void connection_monitor_loop(PairingSharedState* shared) {
|
||||
uint64_t last_reconnect_attempt_at = 0;
|
||||
std::string reconnect_stage = "waiting";
|
||||
std::string reconnect_detail = "No Bluetooth response from the board yet.";
|
||||
while (running.load()) {
|
||||
std::optional<std::string> address;
|
||||
std::string input_state;
|
||||
@@ -540,7 +656,10 @@ void connection_monitor_loop(PairingSharedState* shared) {
|
||||
}
|
||||
|
||||
BluetoothDeviceState bluetooth = inspect_bluetooth_device(*address);
|
||||
std::string reconnect_detail;
|
||||
if (!bluetooth.connected && input_state != "ready" && reconnect_stage == "input-ready") {
|
||||
reconnect_stage = "waiting";
|
||||
reconnect_detail = "Board disconnected. Waiting for the next front-button wake.";
|
||||
}
|
||||
const uint64_t now = monotonic_ms();
|
||||
if (bluetooth.available && !bluetooth.connected &&
|
||||
now - last_reconnect_attempt_at >= kReconnectAttemptIntervalMs) {
|
||||
@@ -550,22 +669,86 @@ void connection_monitor_loop(PairingSharedState* shared) {
|
||||
// server is waiting so the several-second blue-light wake window is caught
|
||||
// from either direction without requiring another red-Sync operation.
|
||||
last_reconnect_attempt_at = now;
|
||||
const uint64_t attempt_started_at = monotonic_ms();
|
||||
const uint64_t attempt_started_epoch_seconds =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
const CommandResult reconnect = run_command({
|
||||
"bluetoothctl", "--timeout", "4", "connect", *address});
|
||||
// Query again because Connect() may have changed several properties before
|
||||
// returning. The diagnostics should describe the resulting state, not the
|
||||
// stale snapshot taken immediately before the attempt.
|
||||
bluetooth = inspect_bluetooth_device(*address);
|
||||
if (bluetooth.connected) {
|
||||
uint64_t radio_connected_at = 0;
|
||||
uint64_t radio_disconnected_at = 0;
|
||||
uint64_t connect_failed_at = 0;
|
||||
uint8_t disconnect_reason = 0;
|
||||
uint8_t connect_status = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
radio_connected_at = shared->last_radio_connected_at;
|
||||
radio_disconnected_at = shared->last_radio_disconnected_at;
|
||||
connect_failed_at = shared->last_connect_failed_at;
|
||||
disconnect_reason = shared->last_disconnect_reason;
|
||||
connect_status = shared->last_connect_status;
|
||||
input_state = shared->input_state;
|
||||
input_error = shared->input_error;
|
||||
}
|
||||
|
||||
const bool radio_was_reached = radio_connected_at >= attempt_started_at ||
|
||||
reconnect.output.find("Connected: yes") != std::string::npos;
|
||||
// The board may initiate its own ACL connection while the monitor is
|
||||
// still running the preceding BlueZ property query. Include that short
|
||||
// pre-attempt window so an actual wake event cannot be lost merely due to
|
||||
// thread timing.
|
||||
const uint64_t evidence_window_started_at =
|
||||
attempt_started_at > kConnectionEvidenceWindowMs
|
||||
? attempt_started_at - kConnectionEvidenceWindowMs
|
||||
: 0;
|
||||
const bool recent_radio_connection =
|
||||
radio_connected_at >= evidence_window_started_at;
|
||||
const bool radio_closed_during_attempt =
|
||||
radio_disconnected_at >= evidence_window_started_at;
|
||||
const bool controller_rejected_attempt = connect_failed_at >= attempt_started_at;
|
||||
|
||||
if (input_state == "ready") {
|
||||
reconnect_stage = "input-ready";
|
||||
reconnect_detail = "Balance Board input device is ready.";
|
||||
} else if (bluetooth.connected) {
|
||||
reconnect_stage = "radio-connected";
|
||||
reconnect_detail = "Bluetooth link established; waiting for the Balance Board input device";
|
||||
} else if (radio_was_reached || recent_radio_connection ||
|
||||
reconnect.output.find("br-connection-create-socket") != std::string::npos) {
|
||||
reconnect_stage = "input-failed";
|
||||
const std::string setup_error = hardware_setup_failure();
|
||||
const std::string logged_error = setup_error.empty()
|
||||
? connection_log_detail(attempt_started_epoch_seconds)
|
||||
: "";
|
||||
if (!setup_error.empty()) {
|
||||
reconnect_detail = "Board reached the server, but HID input setup failed: " + setup_error + ".";
|
||||
} else if (!logged_error.empty()) {
|
||||
reconnect_detail = "Board reached the server, but HID input setup failed: " + logged_error;
|
||||
} else if (radio_closed_during_attempt) {
|
||||
reconnect_detail = "Board reached the server, but no input device was created before " +
|
||||
disconnect_reason_detail(disconnect_reason) + ".";
|
||||
} else {
|
||||
reconnect_detail = "Board reached the server, but BlueZ could not create its HID input connection: " +
|
||||
command_error_summary(reconnect.output, "no lower-level error was logged");
|
||||
}
|
||||
} else if (controller_rejected_attempt &&
|
||||
connect_status != 0x04 && connect_status != 0x08 && connect_status != 0x10) {
|
||||
// Page/connection timeouts are normal while the board sleeps. Preserve
|
||||
// the last meaningful hardware failure instead of replacing it every
|
||||
// three seconds with noise from an unanswered background page.
|
||||
reconnect_stage = "connection-failed";
|
||||
reconnect_detail = connect_status_detail(connect_status) + ".";
|
||||
} else if (!command_succeeded(reconnect)) {
|
||||
reconnect_detail = command_error_summary(
|
||||
reconnect.output, "Bluetooth reconnect attempt did not complete");
|
||||
} else {
|
||||
// bluetoothctl's timeout can end a command without a D-Bus error even
|
||||
// though the device never connected. Trust the resulting Connected
|
||||
// property rather than presenting process exit status as hardware success.
|
||||
reconnect_detail = "Reconnect attempt finished without establishing a Bluetooth link";
|
||||
const std::string command_error = command_error_summary(reconnect.output, "");
|
||||
if (!command_error.empty() &&
|
||||
command_error.find("not available") != std::string::npos) {
|
||||
reconnect_stage = "connection-failed";
|
||||
reconnect_detail = command_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +760,8 @@ void connection_monitor_loop(PairingSharedState* shared) {
|
||||
input_state = shared->input_state;
|
||||
input_error = shared->input_error;
|
||||
}
|
||||
emit_diagnostics(*address, bluetooth, input_state, input_error, reconnect_detail);
|
||||
emit_diagnostics(
|
||||
*address, bluetooth, input_state, input_error, reconnect_stage, reconnect_detail);
|
||||
|
||||
for (int elapsed = 0;
|
||||
elapsed < kBluetoothMonitorIntervalMs && running.load(); elapsed += 100) {
|
||||
@@ -700,14 +884,17 @@ void commissioning_loop(PairingSharedState* shared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string initial_connection_error = trust_and_connect(*address);
|
||||
const std::string prepare_error = prepare_known_device(*address);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
shared->commissioned_address = address->display;
|
||||
shared->commissioning = false;
|
||||
}
|
||||
emit_json("\"type\":\"paired\",\"address\":\"" + json_escape(address->display) + "\"");
|
||||
emit_status("waiting", address->display, initial_connection_error);
|
||||
// Use the same instrumented monitor for the first post-pair connection and
|
||||
// every later wake. A separate one-off connect here previously produced a
|
||||
// large opaque error before the monitor could observe its controller stages.
|
||||
emit_status("waiting", address->display, prepare_error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,24 +951,60 @@ void process_management_events(int fd, PairingSharedState* shared) {
|
||||
std::array<uint8_t, 1024> buffer{};
|
||||
ssize_t count = 0;
|
||||
while ((count = read(fd, buffer.data(), buffer.size())) > 0) {
|
||||
if (count < 14) continue;
|
||||
if (count < 6) continue;
|
||||
const uint16_t event = static_cast<uint16_t>(buffer[0] | (buffer[1] << 8));
|
||||
const uint16_t adapter_index = static_cast<uint16_t>(buffer[2] | (buffer[3] << 8));
|
||||
const uint16_t payload_size = static_cast<uint16_t>(buffer[4] | (buffer[5] << 8));
|
||||
if (event != kMgmtPinCodeRequestEvent || payload_size < 8 || count < 6 + payload_size) continue;
|
||||
if (count < 6 + payload_size) continue;
|
||||
|
||||
std::optional<BluetoothAddress> target;
|
||||
std::optional<BluetoothAddress> pin;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
target = shared->active_target;
|
||||
pin = shared->active_pin;
|
||||
}
|
||||
if (!target.has_value() || !pin.has_value() ||
|
||||
!std::equal(target->wire.begin(), target->wire.end(), buffer.begin() + 6)) {
|
||||
if (event == kMgmtPinCodeRequestEvent && payload_size >= 8) {
|
||||
std::optional<BluetoothAddress> target;
|
||||
std::optional<BluetoothAddress> pin;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
target = shared->active_target;
|
||||
pin = shared->active_pin;
|
||||
}
|
||||
if (target.has_value() && pin.has_value() &&
|
||||
std::equal(target->wire.begin(), target->wire.end(), buffer.begin() + 6)) {
|
||||
answer_pin_request(fd, adapter_index, *target, *pin);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
answer_pin_request(fd, adapter_index, *target, *pin);
|
||||
|
||||
std::optional<BluetoothAddress> commissioned;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
if (shared->commissioned_address.has_value()) {
|
||||
commissioned = parse_address(*shared->commissioned_address);
|
||||
}
|
||||
}
|
||||
if (!commissioned.has_value() || payload_size < 7 ||
|
||||
!std::equal(commissioned->wire.begin(), commissioned->wire.end(), buffer.begin() + 6)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t observed_at = monotonic_ms();
|
||||
bool announce_link = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(shared->mutex);
|
||||
if (event == kMgmtDeviceConnectedEvent && payload_size >= 13) {
|
||||
shared->last_radio_connected_at = observed_at;
|
||||
announce_link = true;
|
||||
} else if (event == kMgmtDeviceDisconnectedEvent && payload_size >= 8) {
|
||||
shared->last_radio_disconnected_at = observed_at;
|
||||
shared->last_disconnect_reason = buffer[13];
|
||||
} else if (event == kMgmtConnectFailedEvent && payload_size >= 8) {
|
||||
shared->last_connect_failed_at = observed_at;
|
||||
shared->last_connect_status = buffer[13];
|
||||
}
|
||||
}
|
||||
if (announce_link) {
|
||||
// This is the first trustworthy proof that the physical board answered
|
||||
// the adapter. Publish it immediately instead of guessing from a later
|
||||
// bluetoothctl timeout; Bluetooth does not identify which button woke it.
|
||||
emit_status("link-detected", commissioned->display);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,7 +1130,7 @@ void simulated_loop() {
|
||||
simulated_bluetooth.wake_allowed = true;
|
||||
// Exercise the same status contract as real hardware so development UI
|
||||
// builds cannot silently break merely because CI lacks a physical board.
|
||||
emit_diagnostics("SIMULATED", simulated_bluetooth, "ready", "", "");
|
||||
emit_diagnostics("SIMULATED", simulated_bluetooth, "ready", "", "input-ready", "");
|
||||
emit_status("waiting", "SIMULATED");
|
||||
const std::array<BoardReadings, 12> sequence{{
|
||||
{0, 0, 0, 0}, {0, 0, 0, 0}, {40, 30, 35, 25}, {95, 82, 90, 76},
|
||||
|
||||
@@ -22,6 +22,7 @@ function statusPresentation(value) {
|
||||
if (status === 'waiting-for-sync') return { label: 'Waiting for red Sync', tone: 'border-amber-600 bg-amber-950 text-amber-200' };
|
||||
if (status === 'waiting') return { label: 'Waiting for front button', tone: 'border-amber-600 bg-amber-950 text-amber-200' };
|
||||
if (status === 'connecting') return { label: 'Connecting', tone: 'border-sky-600 bg-sky-950 text-sky-200' };
|
||||
if (status === 'connection-failed') return { label: 'Connection failed', tone: 'border-red-600 bg-red-950 text-red-200' };
|
||||
if (status === 'error') return { label: 'Error', tone: 'border-red-600 bg-red-950 text-red-200' };
|
||||
return { label: 'Starting', tone: 'border-slate-600 bg-slate-900 text-slate-200' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user