Skip to content

Move to btleplug 0.13.5 - #14

Merged
cameronrye merged 21 commits into
mainfrom
build/btleplug-0.13.5
Oct 12, 2026
Merged

cameronrye merged 21 commits into
mainfrom
build/btleplug-0.13.5

Conversation

@cameronrye

@cameronrye cameronrye commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Description

This moves aranet from btleplug 0.11.8 to 0.13.5 (on Linux, bluez-async 0.8.2 to 0.9.0). The aim is that users see no change. Every difference the new library makes unavoidable on macOS or Linux is listed under "What users may notice", each with what it is measured against. Those that differ from the last release, 0.2.1, are in two lists there: the ones that CHANGELOG.md has an entry for, and the ones that are described only here. The second list holds differences that are neutral, rare or negligible, the Windows ones, which were never run, and btleplug's two warnings in the CLI, for which a changelog entry is offered to the owner. Differences from today's unreleased main are only here, because no release had the behaviour they are measured against.

btleplug 0.13.5 is the first release with upstream's fixes for the two library defects that aranet works around: memory that grows while scanning on macOS (btleplug's issue 494), and a BlueZ connect that fails 5 s after BlueZ has answered it, while BlueZ is still discovering the services (btleplug's issue 496; the library now waits 30 s). Both workarounds stay in this pull request: the filter that makes a search for a sensor on macOS ask only for Aranet sensors, and the wait for BlueZ's service discovery on Linux. Whether either can be simplified is a later change, decided from measurements on the new library.

Everything said here about the library's behaviour comes from reading its source, unless a line says it was seen on hardware.

Where it stands

  • 21 commits on main at 688974d; head 72ae11e (23 files changed, 1742 insertions(+), 310 deletions(-)). Every commit builds, and each passed the per-commit gate when it was made; the first three commits of the review's fixes (a6be5df, 629a73e and bd1999f: two of comments only, one test assertion) passed it in one run on their combined tree. The branch is merged by rebase, so each commit lands on main as itself.
  • The local gate is clean (numbers under Testing), and CI is green on all 18 jobs on the head.
  • Each task was reviewed when it was done, and the whole branch was reviewed at the end. That review found no code defect. It had no critical finding, seven graded important and 38 minor.
    • The seven are five distinct points. Four are statements in text that were wrong or too wide, in two comments and two changelog entries, and all four are corrected on the branch. The fifth is the Aranet4 on macOS, which changes nothing in the tree: it is the one question for the owner, in the last point under "Where it stands".
    • The 38 are almost all wording, in comments, the changelog, docs/ARCHITECTURE.md and commit messages. The rest asked for test lines, which were added, and for a log directive in two examples, which is a follow-up.
    • A second reader re-read every serious finding against the sources and could refute none.
    • The fixes are eight commits on the branch (a6be5df to 25d9b0e): comments, the changelog and docs/ARCHITECTURE.md, and five test lines (one assertion in the soak example, four lines for the GUI's log filter). A second review read the first seven and found nothing serious left. The eighth narrows two changelog phrases with that review's own words.
  • Every hardware check that blocks the merge passed, against a pass line written before the run (table under Testing).
  • The one question for the owner: the Aranet4 on macOS. No check has measured an Aranet4's service discovery from a Mac. This upgrade makes the discovery limit effective on macOS, 10 s (8 s in the TUI), and no option or config key of the aranet programs sets it. If an Aranet4 needs longer than that on a Mac, every read of one there fails with "Operation 'discover services' timed out". No check saw that: the two sensor types that were measured took 1.015 to 1.287 s in the head's 34 discoveries on the Mac. But the project's only figure for an Aranet4 is 9 to 22 s, computed from a request rate measured on Linux, and the review names the Aranet4 as the most common sensor. M9, the one read of an Aranet4 from the Mac that the review asked for, was not run, because no Mac run may connect to that sensor without the owner's word. The choice is to run M9 before the merge, or to merge with the limit unmeasured for that sensor. The facts on both sides are under "Not run, and why". The owner merged the pull request on 2026-10-12 without M9. So the limit is unmeasured for the Aranet4 on macOS, and the rule that no Mac run connects to that sensor stands. This text was written before the merge and put on the pull request after it.

What changes in the code

  • Dependency. btleplug = "0.13.5" in the workspace manifest. It is a floor, not a pin: "0.13" would let a project that already has 0.13.1 to 0.13.4 in its lock file build aranet-core against a release with neither fix. Cargo.lock is regenerated for btleplug alone: 3 packages updated, 9 added, 4 removed, 763 to 768 in all. One test-only struct literal gains the new service_uuid field. The manifest changes in that one line and in the comment above it, and nowhere else.
  • Names. btleplug 0.13 gives PeripheralProperties::local_name a different meaning on each OS; on Linux it is now BlueZ's alias. The new public function aranet_core::peripheral_name keeps a sensor's name what it was: the advertised name, and on macOS the system's name when none was advertised. Every place in aranet-core that needs a name goes through it, and a test fails if any file under crates/aranet-core/src but util.rs reads local_name or advertisement_name itself.
  • Name matching is exact again (apart from case and surrounding spaces). Since Phase 3a, the Bluetooth work that reached main after 0.2.1, unreleased main also matched either half of a macOS combined name such as Kitchen [Aranet4 1A2B3]. btleplug 0.13 never builds such a name, so the rule could match nothing. It is removed in a commit of its own and was never in a release.
  • Linux connect path: no production code changes. The BlueZ test double now waits 30 s, as bluez-async 0.9.0 does. The tests are restated against it, tests are added for the paths that became the usual ones, and a guard test fails if the double's wait is ever set to the default connect limit or below again.
  • macOS: no connect, scan or disconnect logic changes. A unit test pins the zero-services retry, which the new library makes reachable on macOS. CLOSES_TIME_OUT_AFTER_DROP keeps its value (true on macOS, false elsewhere), and its comment says what 0.13.5 does. A hardware check on the first commit (H2) saw a close through a handle from before a drop run into its time limit, so the classifier that this constant feeds still has one real case. M6 saw that case once more on the head (difference 5 below).
  • Logging. On macOS, btleplug 0.13.5 writes Shouldn't get anything but Ok! at error level after every disconnect that macOS confirms; check M4 saw it on the first commit. The aranet CLI, aranet-service and the GUI now switch off that one log target, btleplug::corebluetooth::peripheral, at their default log settings.
    • In the CLI, -v and a user's own RUST_LOG still show the line. In the GUI a RUST_LOG of the user's own does too.
    • In the service no RUST_LOG directive for that target, or for btleplug as a whole, brings that module's lines back. That is the cost of hiding it there. A directive that names a span is not covered by this: see "For reviewers".
    • The module has no other warning or error line. A program that uses aranet-core with its own logger still gets the line. That includes the two examples read_sensor and download_history: each installs its own logger and ends with a disconnect, so on macOS each prints the line after a successful run. They are not changed here; a directive for them is offered to the owner under Follow-ups.
    • The GUI's filter is covered by unit tests only; no hardware check looks at the GUI. By source, a RUST_LOG that holds no valid directive makes the GUI print tracing-subscriber's own "ignoring …" line to stderr, as before this branch, and fall back to errors only, with btleplug's line; a unit test pins that fallback. The CLI treats the same value as unset.
  • Text. Comments, rustdoc, docs/ARCHITECTURE.md and CHANGELOG.md describe the new library. A comment states the library's behaviour with its version and a source line; none claims a measured effect on 0.13.5, except the comment of CLOSES_TIME_OUT_AFTER_DROP, which cites hardware check H2.

No value in ConnectionConfig, no timeout constant and no budget derived from them changes.

What users may notice

Differences from today's main. These are not in the changelog, because no release had the behaviour they are measured against. The Linux numbers come from the code and from the paused-clock tests; none was measured on a radio.

  1. Linux, better: BlueZ gets more time when it answers quickly. When BlueZ answers the connect within about 10 s, its service discovery may now run until 35 s after the start of the connect at default limits (33 s after an answer at 2 s; 25 s on main), and until 50 s in aranet watch. On a link that takes more than 10 s to come up nothing changes. Not in the GUI: its own 30 s limit wraps the whole attempt.
  2. Linux, worse in time: a discovery that never finishes fails later. With BlueZ's answer 2 s after the call: 35 s at default limits, 50 s in watch, 62 s with ConnectionConfig::challenging_environment() (main: about 27, 27 and 37 s). No change at default limits when the answer takes 10 s or more. BlueZ's own 30 s timeout now falls inside aranet's wait, so the attempt can instead end as "the device reported no GATT services" after up to about 77 s at default limits, inside the documented 122 s bound, or about 107 s in watch. That ending was already seen once on main.
  3. Linux, worse: once BlueZ has answered the connect, a link that drops is not re-dialled inside the connect at limits of 30 s or less (the 15 s default connect limit; watch's 30 s).
    • In 0.2.1 such a connect failed with "Service discovery timed out" about 5 s after BlueZ's answer, and nothing was re-dialled.
    • On main the library gives up 5 s after the answer, aranet asks again, and BlueZ opens a new link inside the same connect. That re-ask was added in Phase 3a and has never been released.
    • Now the library keeps waiting for the services and does not notice the drop. If the link drops before aranet's connect limit ends, that limit fires, the link is down, and the connect fails as "connect to device timed out" with no re-ask. If BlueZ still reports the dead link as connected, the attempt ends as "discover services timed out" 35 s in. If the link drops later, during aranet's own wait for BlueZ's discovery (20 s at default limits), nothing notices that either: the wait has made one call to BlueZ, the library's 30 s wait inside that call is longer than aranet's 20 s, and the attempt ends as "discover services timed out", 35 s in at default limits.
    • A re-dial inside the connect still happens in three cases. While BlueZ has not yet answered the connect and refuses aranet's re-asks with "In Progress", the next re-ask it accepts after a drop opens a new link. With a connect limit above 30 s the library gives up first and aranet asks again, 25 s later than on main. With a discovery_timeout above 30 s the wait's next call reopens a link that dropped during the wait.
    • All of this is about what aranet's connect and its wait do. BlueZ can reconnect a device by itself when the device is set to connect automatically.
    • A caller that retries, as aranet watch and the service do, dials again with its next attempt. aranet read and the other one-shot commands do not retry: for them the command fails with the timeout.
    • The case this hits every time is a first connect after a cancelled pairing on Linux: that connect now ends as a timeout after 15 s.
    • It is not restored here: that needs new logic inside the workaround, and in every case recorded on the Linux test host the re-ask failed too. Two tests pin the new behaviour, no check before the merge exercises it, and the Linux soak before 0.3.0 counts it.
  4. Linux, unsettled: "In Progress" from another Bluetooth client during the wait is retried each second instead of ending the connect at once. The connect may succeed, or fail up to 20 s later as a "discover services" timeout.
  5. macOS, better: calls on a dropped link are answered at once instead of running into aranet's 3 s and 5 s limits. By source that holds with one exception, and hardware showed the exception. In the 30 min lifecycle run (M6), "timed out after 5s" appeared 1 time and "Failed to query connection state" 0 times. The one line is a close of the old link to the AranetRn+ on the reconnecting path (ReconnectingDevice), after macOS had ended that link by itself: the close got no answer within 5 s, and the reconnect went on at once and succeeded 3 s later. The other 21 closes of an already ended link in that run returned within a millisecond. It is the case H2 saw on the first commit, a close through a handle from before a drop once a scan has found the sensor again, and it is why CLOSES_TIME_OUT_AFTER_DROP stays.

Differences from 0.2.1 that are in the changelog.

  • Library users (breaking): aranet-core's API carries btleplug 0.13 types; btleplug::Error has a new variant, NoAdapterAvailable; code that reads local_name itself gets a different value on each OS.
  • macOS names: the combined form <system name> [<advertised name>] is gone. Lookup by the system's name, or by the whole old string, stops working; use the advertised name or the identifier that aranet scan prints. No record shows a combined name on any sensor here.
  • macOS, service discovery has its own limit. Link-up and discovery shared the connect limit; now link-up has it and discovery has discovery_timeout. In total that is more time: 25 s where it was 15 s by default, 40 s for 30 s in watch, 18 s for 10 s in the TUI. It is stricter for discovery alone: 10 s, and 8 s in the TUI.
  • macOS, errors: an error that CoreBluetooth reports for a read, write or subscribe is returned; in 0.2.1 a read or a write ran into its time limit instead.
  • macOS, two new failure texts: "Service no longer available" for reads and writes of a service that a "services changed" event named, until the next connect ("Service invalidated; rediscovery required" for one in progress at that moment); and "Characteristic not found" after a characteristic discovery that failed, which ReconnectingDevice returns without reconnecting.
  • Linux adapters: adapters that BlueZ reports with a bluetooth: modalias are now listed. BlueZ gives every adapter of a host the same modalias, from DeviceID in its main.conf (usb:… by default), so only a host that sets DeviceID = bluetooth:… is affected: 0.2.1 found no adapter there and its adapters are now found. The design for this upgrade first said that the adapter aranet uses could change on a host with several. BlueZ 5.82's source rules that out, and the design and the changelog entry were corrected after the review. On a host with the default DeviceID the upgrade does not change which adapters are listed. Which of several adapters aranet takes was arbitrary before (bluez-async collects them from a hash map) and still is. Not testable here: the test host has the default (L1 records its line, Modalias: usb:v1D6Bp0246d0552).
  • macOS, library users: btleplug's error line after every disconnect that macOS confirms. aranet's programs hide it; a program with its own logger must filter the target btleplug::corebluetooth::peripheral itself.

Differences from 0.2.1 that are described only here.

  • macOS, zero services: a discovery that finds no services is reachable on macOS now. The retry's reconnect on the same handle fails after the 2 s pause with "Peripheral no longer available", a retryable error. Before, this ended as a 15 s connect timeout. Rare; pinned by a unit test.
  • macOS, CLI: two of btleplug's own warnings are visible at the CLI's default level and stay visible, because they are true: Adapter powered off, canceling all pending operations when Bluetooth is off (also at start), and Error discovering characteristics for service … when a characteristic discovery fails. Each is one more line beside aranet's own message. The changelog does not mention them; an entry is offered to the owner under Follow-ups.
  • macOS, Bluetooth switched off: pending calls fail at once and the adapter's list is emptied, so the next search must scan.
  • macOS, library users: a handle taken from the adapter's list right after a disconnect can be stale; its connect fails at once with a retryable error, "Peripheral no longer available". Those of aranet's own callers that retry do so by identifier, so each retry searches for the sensor again. A retry on the same handle (Device::from_peripheral and its variants) is refused in the same way until btleplug knows the sensor again. By source there are two ways to that: a scan that finds the sensor, and a call of Central::retrieve_peripherals, which a library user can make and aranet never does. The comments in link.rs name the scan alone.
  • macOS, scans: scan reports still queued when a scan window stops are dropped. Negligible; the search attempt counts are in M7's record and under "Seen on hardware" below.
  • Windows: by source, names can change after a connect (the name helper keeps them), service lists come from the system's cache, there is one GATT session per device, and scan filtering moved into the library. None of it has been run: the project has no Windows hardware.

Related Issue

None in this repository.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test improvement

A dependency upgrade. It is breaking for programs that pass their own btleplug types to aranet-core. For users of the aranet programs, the differences are the ones listed above.

Checklist

  • My code follows the project's coding standards
  • I have run cargo fmt and cargo clippy
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass (cargo test --workspace)
  • I have updated documentation as needed
  • I have updated CHANGELOG.md (if applicable)

Testing

Test Environment

  • OS: macOS 27.2 on arm64; Linux arm64 in a Debian 12 container (the local gate); a Raspberry Pi 5 with Debian 13 and BlueZ 5.82, ocean (the Linux hardware checks)
  • Rust version: 1.90.0 (the MSRV) and 1.99.0 (stable)
  • Aranet device (if applicable): Aranet2 2751B and AranetRn+ 306B8, both unpaired, from the Mac; the AranetRn+, paired, from ocean

Test Results

cargo test --locked --workspace                   macOS, Rust 1.90   1622 passed, 0 failed, 83 ignored
cargo test --locked --workspace --all-features    macOS, Rust 1.99   1666 passed, 0 failed, 83 ignored
cargo test --locked --workspace                   Linux, Rust 1.90   1627 passed, 0 failed, 84 ignored
cargo test --locked --workspace --all-features    Linux, Rust 1.90   1671 passed, 0 failed, 84 ignored

Local gate, on 25d9b0e. The one commit after it, 72ae11e, changes only a doc comment in the hardware tests; for it the format check, clippy and aranet-core's tests were run again.

  • The lock: git diff --numstat origin/main -- Cargo.lock prints 116 74 Cargo.lock, and the lock equals the one a trial build compiled before the work started.
  • cargo fmt --all --check with Rust 1.90's rustfmt, the one CI uses.
  • Clippy with -D warnings, with and without --all-features: on 1.90 and on 1.99 on the Mac, on 1.90 in the Linux container.
  • The docs build with -D warnings, on the Mac and in the container.
  • cargo audit: exit=0.
  • The feature matrix with cargo-hack, in the container: exit=0.
  • aranet-core's own tests gave 423 passed and 59 ignored on the Mac before the first commit and after it. The later commits add tests: 447 passed and 59 ignored at the head.

Tests whose values were computed by hand. The restated and the new BlueZ connect tests carry elapsed times and call sequences that were computed from the code before any of them ran. Every one matched when run.

Mutation checks. Each task that added or changed a decision point in non-test code ended with a hand mutation check of at most eight mutants. Every mutant was caught by a test: eight in the name helper and its guard, five in the name matching, eight in the Linux connect path and its test double, eight in the log filters. One copy of the name matching, in the hardware tests, was not mutated: it needs a sensor, and M5 runs it.

CI. All 18 jobs are green on 72ae11e: the 13 of the CI workflow, Security Audit, the three of Deployment files, and the Release workflow's plan. The three Windows jobs first ran on the draft with only the first commit: Clippy (windows-latest, stable): pass, 2 min; Clippy (windows-latest, msrv): pass, 2 min; Build and Test (windows-latest): pass, 5 min.

Hardware checks

Pass lines were fixed before any run. "First commit" is the commit that only moves the dependency, c9e5f27; its results chose between alternatives the design had named in advance. The pass lines call it the bump-only commit, and the checks that ran on it the smoke. "Head" is the head after the review and its fixes, 25d9b0e, the same commit the local gate ran on. The branch's last commit, 72ae11e, came after the hardware checks and changes only a doc comment in the hardware tests. The two result columns hold each check's result line as the hardware record has it.

Check What Pass line First commit Head
M1 aranet scan on the Mac, old and new binary Both sensors listed as Aranet2 2751B and AranetRn+ 306B8, no [ in any name, same identifiers as the old binary PASS matched old=2 new=2; names with [: 0; identifiers: same PASS matched old=2 new=2; names with [: 0; identifiers: same
M2 Lookup by name, by identifier, and by a partial name Exit 0, exit 0, and exit 1 with "did you mean" PASS exits name=0 id=0 partial=1; did-you-mean lines=1 PASS exits name=0 id=0 partial=1; did-you-mean lines=1
M3 Service-discovery time on macOS. Ten aranet read runs, five per sensor. A span runs from Discovering services... to Found N services. On the head the spans in M6's log are added, and the number of spans is recorded. Precondition: at least 9 of the 10 reads exit 0 with a reading; otherwise the check is repeated once, and a second shortfall blocks. FAIL: any "discover services" timeout, or a span over 8 s. REVIEW (the numbers go to the owner before merge): more than 10% of spans over 5 s. PASS: otherwise. PASS spans=10 max=1.260s over_5s=0 (0.0%) over_8s=0; reads=10/10; timeouts=0 PASS spans=34 max=1.287s over_5s=0 (0.0%) over_8s=0; from the reads 10, from M6 24; reads=10/10; timeouts=0
M4 The error line: one aranet read, and a 200 s service run, with RUST_LOG unset Preconditions on both commits: the read exits 0 with a reading; just before the service is stopped, its /api/status shows a success_count of at least 2 for the sensor (the log has no line per successful poll at this level). Bump-only commit: the count in each output is recorded; the line "appears" if either count is at least 1 (expected: 1 for the read, success_count for the service). Head, with the directive: 0 in both under the same preconditions, and at least 1 in aranet read -v. Head, without it because the smoke showed no line: 0 in both. RECORDED read lines=1; service lines=4; success_count=4; M4_LINE_SEEN=yes PASS read lines=0; service lines=0; success_count=4; read -v lines=1
M5 The hardware test suite without the tests that open a pairing window 28 passed, 0 failed, and exactly five SKIP lines in the output: four ARANET4_DEVICE not set and the one of test_concurrent_finds_share_one_scan. Then that test alone passes and prints no SKIP line. One rerun is allowed for a test that fails at its own search. — PASS suite: 28 passed, 0 failed, SKIP lines 5 (4 + 1); shared-scan test alone: 1 passed, SKIP lines 0; reruns: 0
M6 A 30 min lifecycle run with a link cut every 5 min soak.sh passes (a) threads, (b) file descriptors, (d) orphans, (e) link cuts. Recorded, not judged: the counts of timed out after 5s, Failed to query connection state and Shouldn't get anything but Ok. — PASS criteria 4 of 4; timed out after 5s: 1; Failed to query connection state: 0; Shouldn't get anything but Ok: 7
M7 A 1 h service run with the macOS search filter on, as committed: a search asks macOS only for Aranet sensors RESULT: PASS; at most 2 failed polls; slope at most 0.5 MB/h; growth from warm-up to end at most 5% (0.11.8: 0.21 MB/h, 2.4%). On a miss, one repeat at 2 h. — PASS RESULT: PASS; failed polls 0; slope 0.10 MB/h; growth 0.6%
M9 Added by the review, and not run: one aranet read of Aranet4 17C3C from the Mac, a sensor type that M3 does not measure. It needs the owner's word, because the Aranet4 is ocean's sensor. The first connection is the measurement. Judged is the span from Discovering services... to Found N services, not the exit code. FAIL: a "discover services" timeout, or a span over 8 s. REVIEW: a span over 5 s. — NOT RUN the owner has not allowed a Mac connection to the Aranet4 (asked 2026-10-11, no answer when the block ran)
H2 A throwaway example built from the bump-only commit in a scratch worktree, never committed. It connects to the Aranet2, makes no read, waits until the link has ended, scans until the sensor is listed again, then calls is_connected() and disconnect() on the old handle, each under a 10 s limit. Recorded only. For each call: the result and the time taken (under 1 s, or the limit); whether Shouldn't get anything but Ok! was logged; whether the adapter still lists the sensor afterwards. RECORDED disconnect no answer within 10s after 10.002s (no-answer-within-10s); is_connected Ok(false) after 19.5µs; error line during is_connected/disconnect/other: 0/0/0; listed after the calls: yes; H2_OLD_HANDLE=timed-out —
L1 State of ocean, read-only One adapter; the AranetRn+ is paired and not connected; no HCI link — PASS adapters: 1 (hci0); paired: yes; connected: no; HCI links: 0
L2 aranet scan on ocean Each Aranet row shows the name BlueZ prints as Name, with the right type. This shows that names are unchanged when no alias is set; it does not test the name rule. L7 does. — PASS rows 4; same 4; DIFFERENT 0; wrong or empty types 0
L3 Three reads of the bonded AranetRn+ under dbus-monitor At least 2 of 3 exit 0 with a reading. No "Service discovery timed out". The DEBUG line BlueZ is still discovering services (link.rs:722, the line that does not go on with ; waiting up to) appears 0 times; the INFO line that ends waiting up to 20s in all may appear, at most once per connect attempt, and is recorded. Connected: no five times after every run. Recorded: which path ran, and the times. — PASS readings 2 of 3 (exits 1/0/0; run 1 failed at its connect with le-connection-abort-by-local); lib_timeout 0/0/0; debug_still 0/0/0; info_wait 0/0/0; connects 1/1/1; Connected: no 15 of 15
L4 The two forced-failure tests on the bonded sensor Both print ok and no SKIP line; Connected: no five times; no HCI link — PASS tests ok 2 of 2; SKIP lines 0; Connected: no 10 of 10; HCI links 0
L5 The three scan tests 3 passed, no SKIP line; Discovering: no — PASS 3 passed; SKIP lines 0; Discovering: no
L6 aranet watch -n 1 at its default 30 s Exit 0 with one reading; Connected: no afterwards — PASS exit=0; readings 1; Connected: no 5 of 5
L7 The name rule end to end: set a BlueZ alias on the AranetRn+'s record, scan, read by name, clear the alias With the alias set, the scan row still reads AranetRn+ 306B8 with its type, and aranet read -d "AranetRn+ 306B8" exits 0 with a radon value. Afterwards bluetoothctl info shows Alias equal to Name again. — PASS scan row with the alias set: name kept, type AranetRadon; read by name exit=0 with a radon value; Alias equals Name afterwards: yes
L8 History: aranet history --count 5 for the bonded AranetRn+ on ocean Recorded only: the exit code and the number of records. It is the only hardware check of a write and of history reads on 0.13.5. — RECORDED exit=0 records=5

Three notes on reading the results. In L3's result, lib_timeout, debug_still and info_wait count, for each of the three reads, the lines its pass line names: the library's "Service discovery timed out", the DEBUG line and the INFO line. L3's pass line cites link.rs:722, where the DEBUG line stood when the pass line was written; at the head it is line 796. M4_LINE_SEEN=yes and H2_OLD_HANDLE=timed-out are the two answers that the first commit's checks handed to the later work: the error line is real, and the close through the old handle ran into its limit.

Two read-only audits re-judged every check of the head from the raw files and agreed with every result. For both blocks of the head's checks, the Mac's and ocean's, the raw output of the idle check and of the Mac's list of paired devices was saved: no aranet process was running on either host, and no Aranet sensor was paired with the Mac before the Mac's checks, after them, or before ocean's.

Seen on hardware, outside the pass lines.

  • L3's first read on ocean failed at its connect with BlueZ's le-connection-abort-by-local, 0.7 s after the link came up; the records of this host hold the same failure with btleplug 0.11.8, the pass line allows one failed read in three, and every later connect of the checks worked at once.
  • A Paired signal in L3's second read: BlueZ sent one change signal for the AranetRn+ with ServicesResolved true and Paired true. By the records it is not a new pairing: Paired read true at L1 and before each connect, no recording holds Paired false, and no log holds a pairing line or a line of aranet's BlueZ pairing agent.
  • The rssi column of aranet scan is empty on ocean in every row (L2, L7); the released 0.2.1 binary and the head's binary then each scanned twice there, and the column was empty in all 16 rows, so it is not from this change, and it is a follow-up.
  • M1's one scan on the Mac without the Aranet4: the head binary's 15 s scan did not list Aranet4 17C3C, which the old binary's scan 16 s earlier did; the head's scan test listed it later (M5), every aranet scan on ocean listed it (L2, L7 and the four scans of the rssi check), and the pass line names the Mac's two sensors only.
  • Searches that needed a second scan window: 4 of M3's 10 reads, M4's read, and 21 of M7's 60 searches (found in window 1, 2 and 3: 39, 20 and 1); the record on 0.11.8 has 21 of 60 as well (39, 16 and 5), and every read succeeded.
  • M6's count of btleplug's error line, 7, is one line for each disconnect of a live link, at the 5 link cuts and twice at shutdown, and none after the 22 closes of a link that had already ended; the lifecycle run is an example with its own logger, so the default directive does not apply to it.

Watch list. Every hardware log was searched for Service no longer available, Service invalidated, Characteristic not found, Error discovering characteristics and returned 0 services. No log had a hit.

Changelog against hardware. The four changelog entries about run-time behaviour (three for macOS, one for Linux adapters) say "by btleplug's source code". Each was compared with these results. No result contradicts any of them, and they keep those words in this pull request. What the checks did see of them is one thing: the separate discovery step on macOS, 1.07 to 1.26 s in the ten reads on the first commit and 1.015 to 1.287 s in the 34 discoveries on the head. They did not see the discovery limit applied, an error that macOS reported for a read, a write or a subscribe, either of the two new failure texts, or a host with a bluetooth: modalias. The changelog's older entry on macOS memory (under Fixed: long-running programs "no longer grow by about 1-2 MB an hour") was measured on 0.11.8 and will ship with 0.13.5; M7 (0.10 MB/h and 0.6% in its hour, with the search filter on as committed) does not contradict it, so it stands as it is.

Not run, and why

  • Windows at run time. The project has no Windows hardware. CI compiles the workspace and runs the unit tests on Windows; the Windows name rule rests on reading btleplug's source.
  • A TUI connect on macOS. It opens macOS pairing windows and needs a person at the Mac. M3's measurement covers its 8 s discovery limit, for the two sensor types M3 measured.
  • Any Mac connection to the Aranet4. This is the one question for the owner ("Where it stands"). The Aranet4 is ocean's sensor, and the rules for the hardware checks let no Mac run connect to it without the owner's word. The review asked for one read of it from the Mac (M9); the owner was asked on 2026-10-11 and had not answered when the checks ran, so it was not run. The pull request was merged without it. No record shows the Mac ever connecting to it.
    • What that leaves open: the discovery limit that this upgrade makes effective on macOS, 10 s (8 s in the TUI), rests on no measurement for that sensor. M3 rests on two of the four sensor types, the Aranet2 and the AranetRn+, whose 34 discoveries on the head took 1.015 to 1.287 s. No check connected to the fourth type either, the Aranet Radiation sensor (Aranet☢ 30ED1).
    • What is at stake: if an Aranet4's discovery takes more than 10 s on a Mac, every read of one there fails with "Operation 'discover services' timed out", and no option or config key of the aranet programs sets the limit. The review names the Aranet4 as the most common sensor. In 0.2.1 link-up and discovery shared 15 s. This is a risk that no check measured, not a failure that a check saw.
    • The project's one figure for an Aranet4's service discovery is 9 to 22 s. It is computed from a request rate measured on ocean's BlueZ, not measured as a discovery. The 10 s limit lies at the low end of that range, and the TUI's 8 s lies below it.
    • Three facts bear on whether a Mac would be quicker. An Aranet2 that the host had not cached resolved in 1.3 s on ocean's BlueZ; on the Mac its discovery took 1.07 to 1.14 s in the first commit's five reads and 1.015 to 1.226 s in the head's 17 discoveries, so the Mac is not shown to be faster. The Aranet4's table is about the same size as the Aranet2's (25 characteristics against 23), so the table does not explain its slowness on ocean, and whether that slowness carries over to a Mac is unknown. And the notes that planned the hardware checks call the Aranet4 "the worst case for the new discovery limits".
  • A history download or any write on the Mac. It opens a pairing window. GATT writes ran on hardware only in L8.
  • Anything that uses notifications. No shipped code subscribes.
  • Bluetooth switched off and on during a connection, and a read that an unpaired sensor refuses.
  • A Linux host that sets DeviceID = bluetooth:… in BlueZ's main.conf, the only kind the adapters change affects. ocean has the default.
  • A link that drops after BlueZ's answer (difference 3 above). Nothing before the merge exercises it on a radio.
  • macOS memory on the new library. Three 130 min runs after the merge decide whether upstream's fix for the memory growth (btleplug's issue 494) works here and whether the search filter may go in a later change. The changelog entries that this pull request adds say nothing about memory, the search filter's comment in scan.rs says that upstream's fix has not been measured, and nothing new about memory is claimed until then.
  • The 0.3.0 release gates: the Aranet4 first-connect proof on ocean and both 24 h soaks. They run once, on this stack, after the follow-up pull request.
  • Locally: Windows (3 jobs), x86_64 Linux, clippy on stable on Linux, the Docker image, coverage, the systemd and launchd jobs and dist plan are checked by CI only.

Screenshots (if applicable)

None.

Additional Notes

Decisions a reader may not expect

  • Both workarounds stay, although upstream fixed what they work around. Removing or redesigning the macOS search filter, removing CLOSES_TIME_OUT_AFTER_DROP, and changing the Linux wait are later changes, each backed by a measurement on the new library.
  • The re-dial after a dropped link on Linux is not restored (difference 3 above).
  • Two macOS failure modes that the new library adds are accepted, not guarded against: the "Service no longer available" and "Characteristic not found" texts above. A guard is a later change.
  • A query in the old combined form, -d "Kitchen [Aranet4 1A2B3]", gets no special rule.
  • The README is unchanged. Upstream now documents macOS 10.15 as its floor; the README names no floor for the CLI. The website is unchanged too: it deploys from main and presents 0.2.1.

Follow-ups

  • After the merge: the macOS memory measurements, which decide whether a changelog sentence about memory may be written and whether the search filter may be removed later.
  • Its own pull request, before the soaks: the five code changes that Service hardening, secure defaults, config files and deployment (Phase 3b) #11 lists as "before 0.3.0 is tagged".
  • Then the release gates for 0.3.0, on this stack.
  • The first release-profile builds of the new library for the five release targets, the Windows one included, happen at the first tag.
  • A report of the error line for upstream is drafted for the owner. It has not been filed.
  • Linux, the empty rssi column of aranet scan: aranet reads a device's properties after the scan has stopped, and BlueZ drops RSSI then. 0.2.1 has the fault too. It is not fixed here, because this pull request changes no scan logic.
  • The owner's option: the log directive for the two examples read_sensor and download_history, so that they stop printing btleplug's error line after a successful run on macOS. The design named three places for the directive (the CLI, the service and the GUI), and this pull request keeps to them.
  • The owner's option: a changelog entry for btleplug's two new warnings at the CLI's default level. They are described only in this text.

Corrections to commit messages. The messages cannot be changed, so the corrections stand here. The first message leaves something out; the other four say something wider than the code or the records bear out.

  • c9e5f27, the first commit, builds and passes its checks, but it is not a state to ship or to bisect behaviour on. Its message does not say two things that hold at that commit. A sensor's name on Linux is BlueZ's alias, and on Windows it can change after a connect; 8b04ec3 puts that right. And the aranet programs print btleplug's error line after a disconnect on macOS; 3c8e075 hides it.
  • b7142e4 says "a link that drops during the wait is not reopened". That is wider than the code. It holds once BlueZ has answered the connect, at limits of 30 s or less (difference 3 above).
  • 24e3e01 says that the macOS example in DeviceManager's comment for a close that runs out of time "was no longer true". It is still true in one case, the one H2 and M6 saw: a close through an old handle after a scan has found the sensor again. The message also says that each statement about 0.13.5 was read from its source; the sentence about H2 in connector.rs is a hardware observation.
  • 064a3c1 says of the four source-based changelog entries that "no hardware check has seen that behaviour yet". For the discovery_timeout entry that is too wide: the checks on the first commit had already measured the separate discovery step on macOS (1.07 to 1.26 s in ten reads). They had not seen the limit applied, nor anything of the other three entries. The words it quotes for those entries, "by the library's source", became "by btleplug's source code" in c11c27e.
  • c11c27e says of the adapter aranet uses on a host with several: "It cannot" change. That is meant of this upgrade only. The upgrade does not change which adapters are listed on a host with the default DeviceID. Which of several adapters aranet uses was arbitrary before and still is.

For reviewers. The first commit is the dependency alone. The removal of the bracket matching is one commit, 0e7c352. It can no longer be reverted on its own: bd1999f, which the review added, puts one assertion into the soak example's name test that holds only with the removal, so a revert has to take both. The commit that sets the test double to 30 s also restates the tests, because they fail without each other. Four wordings were left as they are after the second review, because none is false to a user: in link.rs, "every such callback … empties the set that this handle keeps" is wider than the source for a handle that btleplug has replaced; in util.rs, "Two kinds of read get past it" is not a complete list of what the name guard misses; in gui/mod.rs, "the directive" does not say which one; and the changelog's "whatever RUST_LOG says" does not cover a span directive, which could still show the line inside a connect span.

…both library defects aranet works around

aranet works around two defects of btleplug 0.11.8. On macOS its
CoreBluetooth thread never drained an autorelease pool, so memory grew
while scanning (btleplug's issue 494). On Linux a connect failed with
"Service discovery timed out" when BlueZ needed more than 5 s to
resolve a sensor's services (btleplug's issue 496). By its changelog,
btleplug 0.13.5 fixes both: it drains a pool for every message, and
through bluez-async 0.9.0 it waits 30 s. Neither effect has been
measured here yet.

This commit moves the dependency and changes nothing else. Both
workarounds stay as they are. The BlueZ test double still waits 5 s;
the double, the tests, the comments and the changelog follow in later
commits of this pull request.

The requirement is `"0.13.5"`, a floor and not a pin: it accepts
0.13.6. `"0.13"` would let a project that already has 0.13.1 to 0.13.4
in its lock file build aranet-core against a release with neither fix.
0.13.0 is yanked.

`Cargo.lock` comes from `cargo update -p btleplug --precise 0.13.5`.
btleplug, bluez-async (0.8.2 to 0.9.0) and objc2-core-bluetooth (0.2.2
to 0.3.2) move. itertools 0.15.0, which bluez-async 0.9.0 uses on Linux,
is added, and so are eight `windows` crates, which only btleplug's
Windows backend uses. cesu8, dashmap 5.5.3, jni 0.19.0 and jni-utils go.
No other package moves, `cargo update --workspace` changes nothing more,
and the lock goes from 763 to 768 packages.

`ValueNotification` has a new field, `service_uuid`. The one literal in
the tree is in the test double `link/fake.rs`, which now names the
service that its characteristics already use. With the new lock and
without that line, the test build of aranet-core stops at E0063; no
other code needs a change.

Checked on macOS arm64: rustfmt 1.90; clippy with `-D warnings` on Rust
1.99, with and without `--all-features`, and on 1.90; `cargo test
--locked --workspace` (1,591 passed, 83 ignored); aranet-core's own
tests, which give 423 passed and 59 ignored before and after the
change; each feature of aranet-core with cargo-hack; the docs build
with `-D warnings`; and `cargo audit`. In a Linux arm64 container with
Rust 1.90: clippy with `--all-features` and aranet-core's tests (431
passed, 60 ignored). Nothing Windows-specific has been compiled yet:
this pull request's Windows jobs are the first to do that.
…alias never replaces it

btleplug 0.13.5 gives `PeripheralProperties::local_name` a different
meaning on each operating system. On Linux it is BlueZ's `Alias`: a
sensor that the user renamed in a Bluetooth settings dialog would lose
its Aranet name and with it its detected type, and a sensor without a
type is read and parsed as an Aranet4. A device that BlueZ has no name
for would be listed under its address with dashes. On Windows a connect
replaces the field with the system's name for the device. On both, the
new field `advertisement_name` holds what `local_name` held in 0.11.8.

`aranet_core::peripheral_name` is now the one rule for a name:
`advertisement_name`, and on macOS only, `local_name` when no name was
advertised, which there is CoreBluetooth's own name for the peripheral.
The scan, the device lookup, the connect, the passive monitor, the
`discover_services` example and the hardware tests' peripheral lookup
all take the name from it. It is public because the example and the
hardware tests are separate crates, and because a program that calls
`properties()` on a peripheral it got from aranet needs the same rule.
On macOS it returns what `local_name` holds, so nothing changes there.

The test double of a peripheral reports the name in both fields, as the
backends do for a named sensor, and the link test reads it through
`peripheral_name`: with `local_name` alone it would fail on Linux.

Tests: the rule for each backend, through a private function that takes
the choice per OS as a flag; one test of the constant that makes the
choice, which expects the fallback on macOS and nowhere else;
`is_aranet_device`, which had no test; and a guard that reads the
sources under `crates/aranet-core/src` and fails if any file but
`util.rs` reads `local_name` or `advertisement_name`. Five of the six
places that need a name take a real peripheral and cannot be
unit-tested, and a read of `advertisement_name` would pass every other
test and lose the system's name on macOS for a sensor that has not
advertised one. The example and the hardware tests are outside `src`,
so the guard does not cover them.

Eight hand mutants of the constant, the rule and two read sites, one
reading `local_name` again and one reading `advertisement_name`, each
failed a test. The mutant that sets the fallback on every OS is the
same program on macOS and was run in the Linux container.
…, which btleplug 0.13.5 never builds

btleplug 0.11.8 named a macOS peripheral `"<system name> [<advertised
name>]"` when CoreBluetooth's name for it differed from the name it
advertised, and since f04ade7 a device lookup also matched either half
of such a name. 0.13.5 never builds that form: the name is the
advertised one. The rule could only still match a device whose own name
ends in `]`, and no release has had it: f04ade7 is later than v0.2.1.

`name_matches` now compares the whole name and nothing else, ignoring
case and the spaces around it as before. Its two copies go too, in the
`lifecycle_soak` example and in the hardware tests' peripheral lookup.
The rustdoc of `find_device` loses the sentence that described the rule
and points to `peripheral_name` for what a device's name is.

Tests: a name with a bracketed part is found by the whole name and by
neither half, in `lookup` and in the example, and both tests failed
against the old code. A name with spaces around it is still found. With
the branch put back, the trim removed, the comparison made
case-sensitive or `==` turned into `contains`, a lookup test fails each
time. The copy in the hardware tests needs a sensor and was only
compiled.

Nothing else in the upgrade depends on this commit: without it the
branch is dead code for every name btleplug 0.13.5 reports.
btleplug 0.13.5 brings bluez-async 0.9.0, whose `connect` waits 30 s
for BlueZ's `ServicesResolved` where 0.8.2 waited 5 s. The test double
still modelled 5 s, so the tests described a library that aranet no
longer uses. `BLUEZ_ASYNC_WAIT` is now 30 s, and its doc says how a
first connect must be scripted at a 30 s connect limit.

With the constant alone at 30 s, seven tests of
`link::bluez_discovery_tests` fail: each scripts a library timeout
that aranet's own 15 s connect limit or 20 s wait now ends first.
They are restated:

- Five keep their path, which starts with the library's timeout, by
  setting a 45 s connect limit (`LONG_CONNECT_LIMIT`). One of them
  now sees exactly two connects where it saw at least five.
- Two start at aranet's connect limit. They script the one re-ask
  that the wait makes there now, in place of 5 s rounds.

`a_longer_discovery_timeout_waits_longer` passed on a tie: the end of
the wait and the end of a library round fell on the same instant. It
now uses a 40 s `discovery_timeout`, so the wait ends between two
rounds. `a_connect_timeout_with_the_link_up_gives_up_after_the_wait`
asserts its exact calls. Two tests are new, the default-limit twins
of `another_error_ends_the_wait` and
`the_zero_services_retry_waits_too`.

No production code changes: everything above `mod tests` in `link.rs`
is unchanged. The module's 18 tests pass, the crate's tests pass, and
the format check and clippy on stable and on 1.90 are clean.
…e usual ones

With a 30 s wait in the library, a slow service discovery at the
default limits ends at aranet's own 15 s connect limit with the link
up, and the wait after it makes one re-ask. No test scripted that as
its own case. Eight tests are added, on the 30 s double:

- the usual slow first connect, and the same after a pairing step
  that used its whole budget;
- `aranet watch`'s 30 s limit: a 20 s discovery needs no wait, and a
  link that is down at the limit is not asked again inside the
  connect;
- a 90 s limit: the library gives up first and the re-ask connects;
- the library's 30 s limit on the D-Bus call, through a new outcome
  of the double, `DbusTimeout`: the connect fails with "Timeout
  waiting for reply", without a connection check or a wait;
- a discovery that BlueZ ends itself with no services goes into the
  zero-services retry;
- a guard that fails if the double's wait is ever set to the default
  connect limit or below.

No production code changes: everything above `mod tests` in `link.rs`
is unchanged. `link::bluez_discovery_tests` has 25 tests and
`link::pairing_tests` 6; they and the crate's other tests pass, and
the format check and clippy on stable, on 1.90 and for each feature
of aranet-core are clean.
…not 5 s

The comments on the BlueZ wait in `link.rs` and on its test double
gave bluez-async's wait for `ServicesResolved` as 5 s, and cited
bluez-async 0.8.2 and btleplug 0.11.8 by line. With btleplug 0.13.5
the wait is 30 s (bluez-async 0.9.0 `src/lib.rs:61`), and that turns
around which limit ends a slow connect: under a `connection_timeout`
of 30 s or less, the 15 s default included, aranet's own limit ends
it first, and the library's "Service discovery timed out" needs a
longer limit.

The comments now say so, with the upstream lines of the new versions.
The reason given for the 20 s minimum wait is rewritten around what
BlueZ's discovery gets in all, 35 s less the time BlueZ takes to
answer at the default limits; the value does not change. The docs of
the wait also say what follows from one 30 s re-ask inside a 20 s
wait: a link that drops during the wait is not reopened, and an
`In Progress` from another client is asked again. The double's
`Hang`, `DiscoveryTimedOut` and `After` say what each stands for now.
btleplug's issue 496, about the 5 s wait, is cited where the wait is
explained.

Comments and rustdoc only: with comment lines left out, the three
files are unchanged. The task gate is clean: the format check, clippy
on stable, on 1.90 and for each feature of aranet-core, the crate's
tests, the docs build, and clippy and the tests in the Linux
container.
…used

On macOS with btleplug 0.13.5 a service discovery can succeed with no
services. 0.11.8 could not report that: its connect never returned
then. aranet's retry for an empty discovery disconnects, waits 2 s and
connects again on the same handle. The disconnect makes btleplug's
CoreBluetooth backend forget the peripheral, so 0.13.5 refuses that
reconnect at once with "Peripheral no longer available"
(src/corebluetooth/internal.rs:1300-1304).

The new test scripts that sequence on the test double: a connect that
succeeds, one empty discovery, then a connect that fails at once. It
expects the Bluetooth error 2 s after the start and the calls connect,
discover, disconnect, connect, disconnect. Until now only a reconnect
that never answers was covered.

No production code changes, and the test passed as soon as it was
written. With the pause in `connect_and_discover` set to 3 s it fails
on the elapsed time (left: 3s, right: 2s); that edit was undone.
…s it

Comments and rustdoc in aranet-core still described btleplug 0.11.8 on
macOS, where a call on a peripheral the backend had forgotten was never
answered and `connect()` included the service discovery. Only comments
change; no code, constant or test does.

- `GattLink::is_connected`, `Device::is_connected`, the hardware tests
  and the lifecycle soak: 0.13.5 answers "not connected" at once for a
  forgotten peripheral, and the time limits stay as guards.
- `connect_and_discover`: on macOS the zero-services retry can now be
  reached, by an empty discovery or by a "services changed" callback
  during one, and its reconnect on the same handle is refused at once.
- `CLOSES_TIME_OUT_AFTER_DROP`: what the constant does, that 0.13.5
  answers the disconnect it was written for, the one case its source
  does not settle, and the state of the hardware check of that case.
  The link to issue 494 is gone: that issue is about memory. The value
  is unchanged.
- `DeviceManager`: a close can fail by running out of time. The macOS
  example given for it was no longer true.
- `ConnectionConfig::discovery_timeout`: it now limits the service
  discovery on macOS.
- `scan.rs`: the adapter-thread check loses its example of a panic that
  0.13.5 no longer has. The search filter's comment says which of its
  reasons still holds: every scanned device is still kept, the growth
  per advertisement is fixed by source and not yet measured, and its
  figures are from 0.11.8.

Each statement about 0.13.5 was read from its source and names the file
and line. A diff check shows that every changed line is a comment line,
and the docs build with `-D warnings` passes.
…disconnect on macOS

On macOS, btleplug 0.13.5 logs `Shouldn't get anything but Ok!` at
error level for every disconnect that CoreBluetooth confirms, and then
returns Ok (src/corebluetooth/peripheral.rs:468-476): the disconnect's
own reply is answered with "Device disconnected", like every other
pending operation (src/corebluetooth/internal.rs:678-698). 0.11.8 had
the same statement and never reached it. A hardware run on the commit
that only moves to 0.13.5 showed the line.

Every `aranet` command that connects ends with a disconnect, and the
service disconnects after every poll. So each successful `aranet read`
would print an error on stderr, `--quiet` included, and the service
would log one per poll per device.

The directive `btleplug::corebluetooth::peripheral=off` joins three
default filters:

- the CLI's default and `--quiet`. `-v` and a `RUST_LOG` of the user's
  own are used as before and still show the line;
- the GUI's default, which stays at errors only. `fmt::init()` becomes
  the same filter written out, so that the directive can be added when
  `RUST_LOG` is unset or empty;
- the service's `DEFAULT_LOG_DIRECTIVES`. There a default directive
  replaces a `RUST_LOG` directive for the same target, so no `RUST_LOG`
  directive for that target, or for a shorter one such as
  `btleplug=trace`, can show the module's lines.

The line is that module's only warning or error, and the target is not
widened: btleplug's warnings about Bluetooth being off, a missing
permission and a failed characteristic discovery come from other
modules and stay visible. A program that uses aranet-core with its own
logger still gets the line. Each site says when the directive goes:
with the btleplug release that no longer logs the line.

The CLI and the GUI take their default from one function,
`log_filter::default_directives`. Its file is a module of the library,
for the GUI, and of the `aranet` binary, for the CLI, because the
binary cannot call a private function of the library and the filter is
not public API.

New tests log one line per btleplug module under each filter and
compare what is shown: the default shows what its level shows minus
that one line, `--quiet` equals the default, `-v` and `RUST_LOG` are
untouched, and the service drops the line under four `RUST_LOG`
values. Each test of a changed filter failed before its change. Eight
hand-made mutants (the target widened to `btleplug`, `=off` as
`=error`, the directive dropped from a site or added to `-v`, the
GUI's condition inverted) each fail a test.
Comments in three test modules (four comment sites) gave reasons for
not making a `PeripheralId` that the source of btleplug and of
bluez-async does not bear out. The Windows reasons were wrong on 0.11.8
too; the Linux reason in format.rs stopped being true with bluez-async
0.9.0.

- scan.rs said that the Windows `PeripheralId` wraps a u64. It wraps a
  `BDAddr`.
- format.rs said that the tests which build a `DiscoveredDevice` run on
  macOS only because the constructors on Linux and Windows are private.
  On Windows `PeripheralId` has a public `From<BDAddr>`; no helper was
  written for it. On Linux it is made from a `bluez_async::DeviceId`,
  a type aranet-cli can't name: btleplug doesn't re-export bluez-async
  and aranet-cli doesn't depend on it.
- util.rs said, in two places, that a test can't easily create a
  `PeripheralId`. Only Linux lacks a conversion that aranet-core can
  reach.

Each module's note now gives its reason with the btleplug 0.13.5 source
line; the second comment in util.rs refers to the note above it.

Comments only: every changed line is a `//` comment, and the tests of
aranet-core and aranet-cli pass as before.
…CHITECTURE.md

The dependency policy said to pin to major versions only, with
`btleplug = "0.11"` as its example. The workspace now asks for
`btleplug = "0.13.5"`, the first release with two fixes aranet relies
on, so the example named the old version and the rule contradicted the
manifest.

The policy now states the rule the btleplug requirement follows and
new requirements should: ask for the major version, or the minor
version of a 0.x crate, and for a later release only when the code
needs something that release added, with a comment that says why. Its
examples are lines of the root Cargo.toml. `tempfile = "3.10"` and the
other requirements with a minor or patch version predate it and carry
no comment.

"What's Working Now" names btleplug 0.13, and the minimum version in
the requirements table is 0.13.5. The two "(current)" labels in the
survey of other Aranet crates are gone: they compared those crates with
btleplug's newest release at the time the table was written.

Checked in the rendered file: the block quote is one paragraph, and
both tables keep their four columns.
The unreleased section said nothing about the move from btleplug 0.11
to 0.13. It gains seven bullets, written against 0.2.1.

Added: `aranet_core::peripheral_name`.

Changed: on macOS, `discovery_timeout` now limits service discovery,
errors that macOS reports for a read, a write or a subscribe are
returned, and two failure texts are new; the combined macOS device name
is gone; on Linux, adapters with a `bluetooth:` modalias are listed.
One bullet marked "Breaking (library)" says that aranet-core's public
API now carries btleplug 0.13 types, that `btleplug::Error` has a new
variant, and that `local_name` means something different on each
platform.

The three bullets about macOS behaviour and the Linux adapters bullet
carry the words "by the library's source": no hardware check has seen
that behaviour yet. The early hardware check saw btleplug's error log
line on a macOS disconnect, so the library bullet names the line and
the log target that aranet's binaries leave out of their default
output.

The Linux connect path gets no new bullet: the entry about first
connections to an Aranet4 is the statement against 0.2.1 and is still
true. No existing line changed (7 lines added, none removed), and the
file still has exactly one "Unreleased" heading.
…retries

A review of the branch found no defect in the connect path, but
comments in link.rs and in its test double that say something false or
leave a case out. Two of them are about a link that drops while
bluez-async waits for the services.

- The doc of the test that pins a 30 s connect timeout with the link
  down said the caller's own retry dials again. `aranet watch` and the
  service retry. `aranet read` and the other one-shot commands connect
  once, so for them the command fails.
- The doc of `wait_for_bluez_discovery` said that at the default
  limits a link that drops during the wait is not reopened. That holds
  once BlueZ has accepted a call of the wait. While BlueZ still
  refuses with `In Progress`, the next call it accepts opens a new
  link.
- The doc of `BLUEZ_DISCOVERY_TIMED_OUT` named only the first call of
  a connect. A call made again during the wait gets the text too,
  under a `discovery_timeout` above 30 s.
- The two comments at the zero-services retry said a caller that
  retries searches for the sensor again. aranet's own callers that
  retry connect by identifier and do; a retry on a kept handle is
  refused until a scan has found the sensor. The inline comment also
  counted two ways in which btleplug 0.13.5 returns an empty service
  set on macOS. There are three: a "services changed" callback also
  empties the set that the handle keeps.
- The doc of `LONG_CONNECT_LIMIT` said the tests of the paths that
  start with the library's timeout use that limit. Five do; the others
  set 90 s, `challenging_environment()` or no limit. The module doc
  now names the 90 s as well.
- A test comment gave the wait's limit (100 s) as the time the wait
  lasts; the test asserts 60 s.
- The fake's scripting rule named only a first connect under a 30 s
  limit. The same order that cannot happen arises for the retry's
  reconnect and for a call made again in a wait with exactly 30 s
  left.
- Three test docs stated facts about bluez-async with no version or
  line, and two comments said bluez-async waits 30 s for the services
  where it waits up to 30 s.

Comments only: every changed line is a `//` comment, and the functions
and constants of the wait are as they were. Each statement was read
against the source it names: btleplug 0.13.5 (the CoreBluetooth
`internal.rs` and `peripheral.rs`), bluez-async 0.9.0 `src/lib.rs`
(lines 57, 711-726, 748-751 and 759-760), BlueZ 5.82 `src/device.c`,
tokio's `Timeout`, and the callers in aranet-cli and aranet-service.
rustfmt (Rust 1.90), clippy on stable and on 1.90, the tests of
aranet-core (410 unit tests) and the workspace docs pass.
Three comments about device names were incomplete or gave a reason
that does not hold.

- A test comment in scan.rs said both name fields hold the name "as
  every backend reports a named sensor that has no BlueZ alias". By
  btleplug 0.13.5's source WinRT puts the system's name in
  `local_name` after a connect, and CoreBluetooth holds a name in
  `local_name` alone until one is advertised. The comment now names
  the three cases in which both fields hold the name, and the btleplug
  version.
- The doc of the guard test in util.rs did not say what gets past it.
  A pattern that takes a field out of the struct has no dot before the
  name, and util.rs is skipped as a whole, so a second read in it
  would pass. Neither exists today.
- The comment on the split search texts said the split keeps the test
  from finding itself. The test skips its own file. What the split
  does is keep a text search of the crate down to the two reads in
  `name_from`.

Comments only: every changed line is a `//` comment. Checked against
btleplug 0.13.5 (`src/winrtble/peripheral.rs:523-531`,
`src/corebluetooth/internal.rs:1003-1008`,
`src/bluez/peripheral.rs:155-156`) and by a search of aranet-core for
the two field names: the only reads are the two in `name_from`, and no
pattern takes a field out of the struct. rustfmt (Rust 1.90), clippy
on stable and on 1.90, the tests of aranet-core and the workspace docs
pass.
The example's name test checked that a bracketed name matches as a
whole and that its first half ("hall") does not. The second half was
not checked, although the commit that removed the bracket matching
says the name is matched by neither half in the example.

One assertion adds the second half ("aranet2 2751b").

Checked: the example's 20 tests pass. With the query changed to the
whole name in lower case the new assertion failed, so it can fail;
that change was undone before this commit. rustfmt (Rust 1.90), clippy
on stable and on 1.90 with --all-targets, the tests of aranet-core and
the workspace docs pass.
The module doc of log_filter.rs said the filter is the one the
commands and the GUI use "when the user has chosen none", and the doc
of `default_directives` said it is for "when neither a flag nor
`RUST_LOG` chooses the filter". `--quiet` is a flag that chooses the
filter, and it builds it from these directives. Someone who reads the
file alone would conclude that `--quiet` shows btleplug's disconnect
error. Both docs now name who uses the directives: the CLI under
`--quiet` and when neither `--verbose` nor `RUST_LOG` chooses a
filter, and the GUI when `RUST_LOG` is unset or empty.

The doc of `lines_shown_by` says the `log` bridge asks the subscriber
about each record with the record's own target, and cited only the
conversion that builds the metadata (tracing-log 0.2.0
`src/lib.rs:227-242`). It now cites the place that asks,
`src/log_tracer.rs:170-201`, and keeps the conversion as what that
call is made with.

Comments only: every changed line is a `//` comment. Checked against
`cli_log_filter` in main.rs (the `--quiet` branch and the default
both call `default_directives`), `gui_log_filter` and `init_logging`
in gui/mod.rs (an unset `RUST_LOG` is read as an empty one), and the
two ranges in tracing-log 0.2.0, the version in Cargo.lock. rustfmt
(Rust 1.90), clippy on stable and on 1.90, the tests of aranet-cli
with default and with all features (335 and 363) and the workspace
docs pass.
…o valid directive

`gui_log_filter` adds the directive that hides btleplug's disconnect
error only when `RUST_LOG` is unset or empty. A value that is not
empty but holds no valid directive, such as `aranet_core=loud`, goes
to `EnvFilter::new`, which drops what it cannot parse and falls back
to errors only by itself (tracing-subscriber 0.3.23
`src/filter/env/builder.rs:146-159` and `334-338`). The GUI then logs
errors with the line, as it did when it called `fmt::init()`. The CLI
treats the same value as unset and hides the line, and has a test for
it. Neither the doc nor a test said which of the two the GUI does, so
a reader could not tell whether the difference is meant.

The behaviour stays as it is. The doc of `gui_log_filter` now states
it, and `rust_log_replaces_the_default_gui_log_filter` pins it: with
such a value the filter shows the line and exactly what `error` alone
shows. No line of the function's code changes.

The new assertions passed on the first run, since they pin what the
code already does. To see that they can fail, the test ran twice more
with one temporary change each, edited back afterwards: with the
value replaced by the empty string, and with a `gui_log_filter` that
gives a value that does not parse the default directives. Both runs
failed on the first new assertion. rustfmt (Rust 1.90), clippy on
stable and on 1.90, the tests of aranet-cli with default and with all
features (335 and 363, as before) and the workspace docs pass.
The whole-branch review found two false sentences and three incomplete
ones in the Unreleased bullets that this branch added.

Linux adapters: the bullet said that on a host with more than one
adapter the one aranet uses can change. It cannot. BlueZ builds every
adapter's modalias from the daemon-wide `DeviceID` setting, so all
adapters of a host carry the same one. What changes is a host that
sets `DeviceID = bluetooth:…`: 0.2.1 found no adapter there, and its
adapters are now found. The bullet says that instead.

Bluetooth errors on macOS: the bullet said that a failed subscribe ran
into its time limit before. In 0.2.1 a subscribe had none, so the
sentence about 0.2.1 now speaks of reads and writes only. The review's
text went on to say that such a subscribe never returned. That clause
is left out: btleplug 0.11.8 answers a pending subscribe with "Device
disconnected" when the link ends, so "never" is too wide.

Two new failures on macOS: the bullet now names the second error text
("Service invalidated; rediscovery required"), says that writes fail
as reads do, and says who does not retry a missing characteristic:
`ReconnectingDevice`, while `aranet watch` and the service connect
again at their next poll.

`discovery_timeout` on macOS: the bullet now says that no option or
config key sets it, and that `--timeout` and the `timeout` key of
`aranet watch` limit only the time the link takes to come up.

The disconnect log line: the bullet now says that the disconnect has
succeeded, that `aranet --verbose` and a `RUST_LOG` of the user's own
can still show the line, and that `aranet-service` and `aranet server`
keep the target off whatever `RUST_LOG` says.

The four bullets that state behaviour read from code said "by the
library's source". Everywhere else in that section "library" means
aranet's own crates, so they now say "by btleplug's source code".

Each sentence was checked before it was written: against btleplug
0.13.5 and 0.11.8 (`src/corebluetooth`), bluez-async 0.9.0 and 0.8.2
(`src/modalias.rs`, `src/lib.rs`, `src/adapter.rs`), BlueZ 5.82
(`src/adapter.c`, `src/main.c`), aranet 0.2.1 (`device.rs`, `scan.rs`,
`error.rs`) and the code at this commit (`watch.rs`, `collector.rs`,
`error.rs`, `reconnect.rs` and the three log filters). Five lines
change, all above the first released section, and the file keeps one
Unreleased heading. No Rust file changes; rustfmt (Rust 1.90) passes.
…thing

The rule said to ask for a later release only when the code needs
something that release added. Its one example, btleplug 0.13.5, is a
release that fixed something, so the rule did not cover its own
example. It now says "added or fixed".

The rule also read as if every manifest followed it. Some requirement
lines name a later release without a comment, and they are older than
the rule. A maintainer who compared the rule with the manifests could
not tell which side is right, and might copy the form without a
comment. The rule now names two such lines, `tempfile = "3.10"` in the
workspace manifest and `proptest = "1.9.0"` in aranet-core and
aranet-types, and says to give each its reason when it is next
changed.

The review proposed `indicatif = "0.18.3"` as the second example. In
aranet-cli's manifest that requirement is an inline table with
`version = "0.18.3"` and `optional = true`, so a search for the quoted
text would find nothing. `proptest = "1.9.0"` stands in two manifests
exactly so.

Checked against the manifests: both example lines stand as quoted,
with no comment above them, and `btleplug = "0.13.5"` has its comment.
On this branch the only requirement line that changed is btleplug's,
so the two examples are older than the rule. No manifest changes, and
no other line of the document.
The entry on `discovery_timeout` said that `--timeout` and the `timeout`
key of `aranet watch` limit only the time the link takes to come up.
That holds for a connect. `watch --passive` uses the same value as the
length of each scan, and `timeout` is the general key of config.toml,
not a key of `watch`.

The entry on the two new macOS failures said that reads and writes "on
that connection" fail once macOS reports that services changed.
btleplug 0.13.5 removes only the services macOS names
(src/corebluetooth/internal.rs:977-981), and a read or a write fails
only when its own service is gone.

Both were found by the re-read of the changelog against the sources.
The file keeps one Unreleased heading and four entries marked as read
from btleplug's source code.
…stops, in the shared-scan hardware test

The doc comment of `test_concurrent_finds_share_one_scan` explained the
test's rare double `CacheHit`, and its rare failure, by CoreBluetooth
reporting the sensor just after the scan had stopped. That describes
btleplug 0.11.8. btleplug 0.13.5 drops a scan report that its
CoreBluetooth thread takes up after the stop
(`src/corebluetooth/internal.rs:1776-1784`).

A report taken up just before the stop can still reach the adapter's
list after `stop_scan` has returned: `stop_scan` only queues the stop
(`src/corebluetooth/adapter.rs:176-182`), and a second task fills the
list (`adapter.rs:59-82`). So the comment keeps the rare case and the
advice to rerun the test once, and says where each fact is in the
library's source.

The comment was left until the hardware tests had run on the new
library; the test passed when run on its own against an Aranet2. A
comment only: the test and its assertion are unchanged.
@cameronrye cameronrye self-assigned this Oct 12, 2026
@cameronrye
cameronrye marked this pull request as ready for review October 12, 2026 12:39
@cameronrye
cameronrye merged commit e6410de into main Oct 12, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant