This commit is contained in:
legop3
2026-07-16 22:09:53 -04:00
parent 0d352d326d
commit a6c569ada4
6 changed files with 58 additions and 26 deletions
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
+1 -1
View File
@@ -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-DrGDUWQo.js"></script>
<script type="module" crossorigin src="/assets/index-0ANoPaoC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D6ALZM8p.css">
</head>
<body>
@@ -60,6 +60,7 @@ constexpr uint8_t kBluetoothClassicAddressType = 0;
constexpr int kFrameIntervalMs = 50;
constexpr int kDeviceScanIntervalMs = 500;
constexpr int kDiscoveryRestartDelayMs = 1000;
constexpr const char* kDiscoveryTimeoutSeconds = "86400";
constexpr int kBatteryRefreshMs = 5000;
std::atomic<bool> running{true};
@@ -334,13 +335,21 @@ void stop_command(RunningCommand* command) {
}
}
std::optional<BluetoothAddress> take_discovered_board(RunningCommand* discovery) {
std::optional<BluetoothAddress> take_discovered_board(RunningCommand* discovery,
bool* discovery_started) {
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;
}
// 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
@@ -427,7 +436,12 @@ void commissioning_loop(PairingSharedState* shared) {
// 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.
RunningCommand discovery = start_command({"bluetoothctl", "scan", "bredr"});
// BlueZ's command-line client exits after the SetDiscoveryFilter callback
// unless non-interactive mode has a timeout. A one-day timeout keeps the
// 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({
"bluetoothctl", "--timeout", kDiscoveryTimeoutSeconds, "scan", "bredr"});
if (discovery.pid < 0) {
emit_status("error", "", "could not start Bluetooth discovery: " +
command_error_summary(discovery.transcript, "unknown process error"));
@@ -436,13 +450,23 @@ void commissioning_loop(PairingSharedState* shared) {
}
std::optional<BluetoothAddress> address;
bool discovery_started = false;
while (running.load() && !address.has_value()) {
const bool discovery_running = collect_command_output(&discovery);
address = take_discovered_board(&discovery);
const bool was_started = discovery_started;
address = take_discovered_board(&discovery, &discovery_started);
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;
if (!discovery_running) {
emit_status("error", "", "Bluetooth discovery stopped: " +
command_error_summary(discovery.transcript, "bluetoothctl exited unexpectedly"));
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;
}
@@ -31,12 +31,29 @@ function formatWeight(value) {
function describePhase(status, frame) {
const phase = frame?.phase || status?.phase || 'waiting';
if (phase === 'commissioning') {
if (phase === 'error' || status?.lastError) {
const message = status?.lastError || 'The Balance Board bridge reported an error.';
// Name the failed subsystem in the badge. Generic labels such as “Needs
// attention” force an operator to read implementation details to understand
// whether the problem is scanning, pairing, or the sensor bridge itself.
const label = /scanner|discovery/i.test(message)
? 'Bluetooth scanner stopped'
: (/pair/i.test(message) ? 'Board pairing failed' : 'Balance Board error');
return {
label: 'Pairing setup',
// Discovery retries automatically, so retain and show the last concrete
// BlueZ failure while the worker keeps listening for another Sync press.
instruction: status?.lastError || 'Press the red Sync button underneath the board. The server will pair it automatically.',
label,
instruction: message,
className: 'border-red-500/60 bg-red-950/60 text-red-200',
};
}
if (phase === 'commissioning') {
const scannerReady = status?.hardwareState === 'discovering';
return {
label: scannerReady ? 'Waiting for red Sync' : 'Starting Bluetooth scan',
// Pairing is automatic once discovery is active. State that directly so
// the UI cannot imply that a second software pairing action is required.
instruction: status?.lastError || (scannerReady
? 'Bluetooth is listening. Press the red Sync button underneath the board once.'
: 'The server is starting Bluetooth discovery. Wait for “Waiting for red Sync” before pressing the board button.'),
className: 'border-amber-500/60 bg-amber-950/60 text-amber-200',
};
}
@@ -47,13 +64,6 @@ function describePhase(status, frame) {
className: 'border-sky-500/60 bg-sky-950/60 text-sky-200',
};
}
if (phase === 'error' || status?.lastError) {
return {
label: 'Needs attention',
instruction: status?.lastError || 'The Balance Board bridge reported an error.',
className: 'border-red-500/60 bg-red-950/60 text-red-200',
};
}
if (!status?.connected) {
return {
label: 'Sleeping',
@@ -236,11 +246,9 @@ function BalanceBoardPanelContent() {
<div className="border-t border-slate-700 pt-1">
<div className="mb-0.5 text-[0.62rem] text-slate-500">Admin maintenance</div>
<div className="flex flex-wrap gap-0.5">
{!status?.paired ? (
<button type="button" className="button-dark px-1 py-0.5 text-xs" disabled={Boolean(actionPending)} onClick={() => runAction('pair')}>
Pair board
</button>
) : null}
{/* An unpaired server scans automatically. A separate “Pair
board” action was redundant and incorrectly suggested that
commissioning required two software/physical pair steps. */}
<button type="button" className="button-dark px-1 py-0.5 text-xs" disabled={Boolean(actionPending) || !status?.connected} onClick={() => runAction('tare')}>
Tare
</button>