From 5f8235aea095a58e4d574461c80a80815e037b6e Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sun, 15 Mar 2026 15:53:54 -0600 Subject: [PATCH 1/3] feat: add WiFi security field and host board feature Add `security: heapless::String<64>` to WiFiEvent for WiGLE AuthMode output, cfg-gated behind `not(esp32)` to avoid DRAM cost on ESP32. Add `board-host` feature with desktop platform constants. Co-Authored-By: Claude Opus 4.6 --- Cargo.toml | 1 + src/board.rs | 15 ++++++++++++++- src/scanner.rs | 8 ++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eca20ab..035f0b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ esp32s3 = [ # Board constants only (no firmware deps — usable by external crates) board-xiao = [] board-m5stickc = [] +board-host = [] # Board-level features (constants + firmware deps) xiao = ["board-xiao", "esp32s3"] diff --git a/src/board.rs b/src/board.rs index abbdf26..9bb5d51 100644 --- a/src/board.rs +++ b/src/board.rs @@ -64,7 +64,20 @@ mod hw { pub const BUZZER_BEEP_MS: u64 = 150; } -#[cfg(not(any(feature = "board-xiao", feature = "board-m5stickc")))] +#[cfg(feature = "board-host")] +mod hw { + pub const BOARD_NAME: &str = "host"; + pub const HAS_DISPLAY: bool = false; + pub const HAS_BUZZER: bool = false; + pub const HAS_PSRAM: bool = false; + pub const HAS_GPS_HEADER: bool = false; +} + +#[cfg(not(any( + feature = "board-xiao", + feature = "board-m5stickc", + feature = "board-host" +)))] mod hw { pub const BOARD_NAME: &str = "unknown"; } diff --git a/src/scanner.rs b/src/scanner.rs index e5ad4eb..7747ac1 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -33,6 +33,12 @@ pub struct WiFiEvent { /// can't spare the ~65 bytes per scan event (8 slots × 65 = 520 bytes DRAM). #[cfg(not(feature = "esp32"))] pub raw_ies: Vec, + /// WiFi security mode string for WiGLE AuthMode (e.g. "[WPA2-PSK][WPA3-SAE]"). + /// Only present on non-ESP32 platforms — ESP32 promiscuous mode doesn't expose + /// security info and can't spare the ~65 bytes per scan event. + /// Populated by hostd from `wifi_scan` crate on desktop platforms. + #[cfg(not(feature = "esp32"))] + pub security: heapless::String<64>, } /// WiFi frame type classification @@ -193,6 +199,8 @@ fn build_wifi_event( frame_type, #[cfg(not(feature = "esp32"))] raw_ies, + #[cfg(not(feature = "esp32"))] + security: heapless::String::new(), } } From dcfa492b86e2710d4b94b55e2358172ad0134593 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sun, 15 Mar 2026 15:54:05 -0600 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20add=20hostd=20=E2=80=94=20desktop?= =?UTF-8?q?=20WiFi/BLE=20scanner=20with=20WiGLE=20CSV=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Host daemon that scans WiFi (CoreWLAN) and BLE (btleplug), logs ALL observations in WiGLE CSV v1.6 format, and emits NDJSON on stdout for matched surveillance devices. GPS tagging via CoreLocation on macOS. - BLE scanner via btleplug with CoreBluetooth backend - WiFi scanner via wifi_scan with CoreWLAN backend - CoreLocation GPS provider with authorization request - WiGLE CSV v1.6 writer using csv crate (proper RFC 4180 escaping) - macOS .app bundle for Location Services permission prompt - clap CLI with --output flag (defaults to airhound-YYYYMMDD-HHMMSS.csv) - Non-fatal scanner exits (BLE/WiFi dying doesn't kill the daemon) - Periodic CSV flush (every 5s) for crash durability Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + hostd/Cargo.lock | 2269 +++++++++++++++++++++++++++++++++++ hostd/Cargo.toml | 27 + hostd/macos/Info.plist | 22 + hostd/rust-toolchain.toml | 2 + hostd/src/ble.rs | 241 ++++ hostd/src/location_macos.rs | 81 ++ hostd/src/main.rs | 423 +++++++ hostd/src/wifi_macos.rs | 147 +++ hostd/src/wigle.rs | 350 ++++++ 10 files changed, 3564 insertions(+) create mode 100644 hostd/Cargo.lock create mode 100644 hostd/Cargo.toml create mode 100644 hostd/macos/Info.plist create mode 100644 hostd/rust-toolchain.toml create mode 100644 hostd/src/ble.rs create mode 100644 hostd/src/location_macos.rs create mode 100644 hostd/src/main.rs create mode 100644 hostd/src/wifi_macos.rs create mode 100644 hostd/src/wigle.rs diff --git a/.gitignore b/.gitignore index 6fe87f6..a6715b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /target /firmware-std/target +/hostd/target +/hostd/AirHound.app .embuild .claude/ diff --git a/hostd/Cargo.lock b/hostd/Cargo.lock new file mode 100644 index 0000000..8c579d7 --- /dev/null +++ b/hostd/Cargo.lock @@ -0,0 +1,2269 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "airhound" +version = "0.1.0" +dependencies = [ + "heapless 0.9.2", + "ieee80211", + "libm", + "log", + "serde", + "serde-json-core", +] + +[[package]] +name = "airhound-hostd" +version = "0.1.0" +dependencies = [ + "airhound", + "btleplug", + "chrono", + "clap", + "csv", + "env_logger", + "futures-lite", + "heapless 0.9.2", + "log", + "objc2 0.6.4", + "objc2-core-location", + "objc2-foundation 0.3.2", + "tokio", + "uuid", + "wifi_scan", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitfield-struct" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "bluez-async" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84ae4213cc2a8dc663acecac67bbdad05142be4d8ef372b6903abf878b0c690a" +dependencies = [ + "bitflags", + "bluez-generated", + "dbus", + "dbus-tokio", + "futures", + "itertools", + "log", + "serde", + "serde-xml-rs", + "thiserror 2.0.18", + "tokio", + "uuid", +] + +[[package]] +name = "bluez-generated" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9676783265eadd6f11829982792c6f303f3854d014edfba384685dcf237dd062" +dependencies = [ + "dbus", +] + +[[package]] +name = "btleplug" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a11621cb2c8c024e444734292482b1ad86fb50ded066cf46252e46643c8748" +dependencies = [ + "async-trait", + "bitflags", + "bluez-async", + "dashmap 6.1.0", + "dbus", + "futures", + "jni", + "jni-utils", + "log", + "objc2 0.5.2", + "objc2-core-bluetooth", + "objc2-foundation 0.2.2", + "once_cell", + "static_assertions", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "uuid", + "windows 0.61.3", + "windows-future", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link 0.2.1", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-tokio" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007688d459bc677131c063a3a77fb899526e17b7980f390b69644bdbc41fad13" +dependencies = [ + "dbus", + "libc", + "tokio", +] + +[[package]] +name = "defile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3324a455850286c803c1c16d3835a44a9457765899417775e4dc6080cd942d" +dependencies = [ + "defile-proc_macros", +] + +[[package]] +name = "defile-proc_macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87c5c1460bfb0f719e899feab6ae532e5e0f059cdc85913bab8e6a0d7980245e" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2 0.6.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ether-type" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e886b8e15547bd98f54a78b5ad1b381d825fa8cd09c088615fc00e26957fabd7" +dependencies = [ + "macro-bits", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "get-last-error" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce85cdf2afb6a63ee6eb6ae445afb5772b1e47b009e1433750bd5d33bd30bb2c" +dependencies = [ + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32", + "serde_core", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ieee80211" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7418f9be3522922d6e87647eacd5275641f6c225cf4a444a64c31e366ac37559" +dependencies = [ + "bitfield-struct", + "const_soft_float", + "crc32fast", + "llc-rs", + "mac-parser", + "macro-bits", + "num", + "scroll", + "tlv-rs", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jni-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259e9f2c3ead61de911f147000660511f07ab00adeed1d84f5ac4d0386e7a6c4" +dependencies = [ + "dashmap 5.5.3", + "futures", + "jni", + "log", + "once_cell", + "static_assertions", + "uuid", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libwifi" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a894393c2dd98e7569fe79aaf26ccef3f3fc54384712fe9e53d944d1d50d5e1e" +dependencies = [ + "bitvec", + "byteorder", + "crc", + "enum_dispatch", + "libwifi_macros", + "log", + "nom", + "rand", + "strum_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "libwifi_macros" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5cf9340ba39a76234195bac38ab571abeb8cb91cf4eec372376fe359197c49" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "llc-rs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33bd85503d851a892baeffb0c56799623e908f55a92d912eabbe568c6e1af45b" +dependencies = [ + "ether-type", + "macro-bits", + "scroll", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac-parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31995b891e21d023d0e7732e8ea7634ca58101f1352b159eb64234824d899f8" +dependencies = [ + "scroll", +] + +[[package]] +name = "macro-bits" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb7867e4591ecd9f96c60f33481a3a07783d6f1ab2de522d0f243c487260c9a" +dependencies = [ + "defile", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "neli" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93062a0dce6da2517ea35f301dfc88184ce18d3601ec786a727a87bf535deca9" +dependencies = [ + "byteorder", + "libc", + "log", + "neli-proc-macros", +] + +[[package]] +name = "neli-proc-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8034b7fbb6f9455b2a96c19e6edf8dc9fc34c70449938d8ee3b4df363f61fe" +dependencies = [ + "either", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "neli-wifi" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a72cd90a5d1ab54ed62cfe032972cb78303cfffd7d82c87c5da889a353fc451" +dependencies = [ + "neli", + "neli-proc-macros", +] + +[[package]] +name = "netlink-rust" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa4fc77d84e525e9c6ca6fd61bead985afb16a15672c757c2fe7c12fc34bde4" +dependencies = [ + "bitflags", + "byteorder", + "libc", +] + +[[package]] +name = "nl80211-buildtools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b461090ee9557fb25d6f811c0bd7a992e74c2ef72ba56faaedb8181f040e51" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "nl80211-rs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a4fe6a58d4cdc64c93d7c50417eb7a14e53ec5f6c933988896aca85c0900a4" +dependencies = [ + "bitflags", + "byteorder", + "encoding", + "netlink-rust", + "nl80211-buildtools", +] + +[[package]] +name = "no-panic" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f967505aabc8af5752d098c34146544a43684817cdba8f9725b292530cabbf53" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-contacts" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b034b578389f89a85c055eacc8d8b368be5f04a6c1b07f672bf3aec21d0ef621" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-bluetooth" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a644b62ffb826a5277f536cf0f701493de420b13d40e700c452c36567771111" +dependencies = [ + "bitflags", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", + "objc2-contacts", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-json-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b81787e655bd59cecadc91f7b6b8651330b2be6c33246039a65e5cd6f4e0828" +dependencies = [ + "heapless 0.8.0", + "ryu", + "serde", +] + +[[package]] +name = "serde-xml-rs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2215ce3e6a77550b80a1c37251b7d294febaf42e36e21b7b411e0bf54d540d" +dependencies = [ + "log", + "serde", + "thiserror 2.0.18", + "xml", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tlv-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6997294e7285bc4160c9bb47b49e120dec31b53143a7dc247392baa1d9d244d" +dependencies = [ + "heapless 0.8.0", + "no-panic", + "scroll", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wifi_scan" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10f2651039a44bf22d5703ca85b74eec3d0cc22d28f33f76a59243fe4f8f3d5" +dependencies = [ + "futures", + "libwifi", + "neli-wifi", + "netlink-rust", + "nl80211-rs", + "objc2-core-wlan", + "objc2-foundation 0.3.2", + "win32-wlan", +] + +[[package]] +name = "win32-wlan" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598b28d1b5f24bf0cee5ef121941a2987711eb1e214bac29464049e7ea2ff684" +dependencies = [ + "get-last-error", + "log", + "windows 0.48.0", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xml" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/hostd/Cargo.toml b/hostd/Cargo.toml new file mode 100644 index 0000000..5880668 --- /dev/null +++ b/hostd/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "airhound-hostd" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "AirHound host daemon — BLE surveillance detection for Mac/Linux" + +[dependencies] +airhound = { path = "..", default-features = false, features = ["board-host"] } +btleplug = "0.11" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "signal", "time"] } +futures-lite = "2" +log = "0.4" +env_logger = "0.11" +heapless = { version = "0.9", features = ["serde"] } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +clap = { version = "4", features = ["derive"] } +csv = "1" + +[target.'cfg(target_os = "macos")'.dependencies] +wifi_scan = "0.7" +objc2-core-location = { version = "0.3", features = ["CLLocationManager", "CLLocation"] } +objc2-foundation = { version = "0.3", features = ["NSObject"] } +objc2 = "0.6" + +[dev-dependencies] +uuid = "1" diff --git a/hostd/macos/Info.plist b/hostd/macos/Info.plist new file mode 100644 index 0000000..bfa17be --- /dev/null +++ b/hostd/macos/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleIdentifier + com.airhound.hostd + CFBundleName + AirHound + CFBundleExecutable + airhound-hostd + CFBundleVersion + 0.1.0 + CFBundlePackageType + APPL + LSUIElement + + NSLocationWhenInUseUsageDescription + AirHound needs your location to tag WiFi and BLE observations with GPS coordinates for WiGLE CSV export. + NSLocationUsageDescription + AirHound needs your location to tag WiFi and BLE observations with GPS coordinates for WiGLE CSV export. + + diff --git a/hostd/rust-toolchain.toml b/hostd/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/hostd/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/hostd/src/ble.rs b/hostd/src/ble.rs new file mode 100644 index 0000000..b4332c7 --- /dev/null +++ b/hostd/src/ble.rs @@ -0,0 +1,241 @@ +//! BLE scanner using btleplug — converts peripheral discoveries into AirHound BleEvents. + +use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter}; +use btleplug::platform::{Adapter, Manager}; +use futures_lite::StreamExt; +use tokio::sync::mpsc; + +use airhound::scanner::{BleEvent, ScanEvent}; + +/// Discover the first available BLE adapter. +pub async fn get_adapter() -> Result { + let manager = Manager::new().await?; + let adapters = manager.adapters().await?; + adapters + .into_iter() + .next() + .ok_or(btleplug::Error::DeviceNotFound) +} + +/// Run the BLE scan loop, converting btleplug events into AirHound `BleEvent`s. +/// +/// Sends discovered events on `tx`. Runs until the channel is closed or an error occurs. +pub async fn scan_loop( + adapter: Adapter, + tx: mpsc::Sender, +) -> Result<(), btleplug::Error> { + adapter.start_scan(ScanFilter::default()).await?; + log::info!("BLE scan started"); + + let mut events = adapter.events().await?; + + while let Some(event) = events.next().await { + use btleplug::api::CentralEvent; + match event { + CentralEvent::DeviceDiscovered(id) | CentralEvent::DeviceUpdated(id) => { + let peripheral = match adapter.peripheral(&id).await { + Ok(p) => p, + Err(_) => continue, + }; + let props = match peripheral.properties().await { + Ok(Some(p)) => p, + _ => continue, + }; + + let ble_event = convert_properties(&props); + + if tx.send(ScanEvent::Ble(ble_event)).await.is_err() { + log::debug!("BLE scan channel closed, stopping"); + break; + } + } + _ => {} + } + } + + Ok(()) +} + +/// Convert btleplug `PeripheralProperties` into an AirHound `BleEvent`. +fn convert_properties(props: &btleplug::api::PeripheralProperties) -> BleEvent { + let mac = address_to_bytes(&props.address); + + let mut name = heapless::String::new(); + if let Some(ref local_name) = props.local_name { + // Truncate to 33 bytes (NameString capacity), respecting UTF-8 char boundaries + let end = if local_name.len() <= 33 { + local_name.len() + } else { + local_name.floor_char_boundary(33) + }; + let _ = name.push_str(&local_name[..end]); + } + + let rssi = props + .rssi + .unwrap_or(0) + .clamp(i8::MIN as i16, i8::MAX as i16) as i8; + + // Extract 16-bit service UUIDs + let mut service_uuids_16 = heapless::Vec::new(); + for uuid in &props.services { + // Check if this is a 16-bit UUID (Bluetooth Base UUID pattern) + let uuid_bytes = uuid.as_bytes(); + // 16-bit UUIDs have the form 0000XXXX-0000-1000-8000-00805f9b34fb + if uuid_bytes[4..] + == [ + 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb, + ] + && uuid_bytes[0..2] == [0x00, 0x00] + { + let short = u16::from_be_bytes([uuid_bytes[2], uuid_bytes[3]]); + let _ = service_uuids_16.push(short); + } + } + + // Extract manufacturer ID (first key if any) + let manufacturer_id = props.manufacturer_data.keys().next().copied().unwrap_or(0); + + // raw_ad is not available from btleplug on macOS (CoreBluetooth abstracts it) + let raw_ad = heapless::Vec::new(); + + BleEvent { + mac, + name, + rssi, + service_uuids_16, + manufacturer_id, + raw_ad, + } +} + +/// Convert a btleplug `BDAddr` to a 6-byte array. +#[inline] +fn address_to_bytes(addr: &btleplug::api::BDAddr) -> [u8; 6] { + addr.into_inner() +} + +#[cfg(test)] +mod tests { + use super::*; + use btleplug::api::{BDAddr, PeripheralProperties}; + use std::collections::HashMap; + use uuid::Uuid; + + fn make_props() -> PeripheralProperties { + PeripheralProperties { + address: BDAddr::from([0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33]), + ..Default::default() + } + } + + #[test] + fn convert_empty_properties() { + let props = make_props(); + let event = convert_properties(&props); + assert_eq!(event.mac, [0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33]); + assert!(event.name.is_empty()); + assert_eq!(event.rssi, 0); + assert!(event.service_uuids_16.is_empty()); + assert_eq!(event.manufacturer_id, 0); + assert!(event.raw_ad.is_empty()); + } + + #[test] + fn convert_local_name() { + let props = PeripheralProperties { + local_name: Some("FS Ext Battery".to_string()), + ..make_props() + }; + let event = convert_properties(&props); + assert_eq!(event.name.as_str(), "FS Ext Battery"); + } + + #[test] + fn convert_long_name_truncated() { + let long_name = "A".repeat(50); + let props = PeripheralProperties { + local_name: Some(long_name), + ..make_props() + }; + let event = convert_properties(&props); + assert_eq!(event.name.len(), 33); + } + + #[test] + fn convert_multibyte_name_truncated_at_char_boundary() { + // 'é' is 2 bytes in UTF-8; 16 × 2 = 32 bytes, adding one more 'é' would be 34 + let name = "é".repeat(17); // 34 bytes + let props = PeripheralProperties { + local_name: Some(name), + ..make_props() + }; + let event = convert_properties(&props); + // Should truncate to 32 bytes (16 chars), not panic at byte 33 + assert_eq!(event.name.len(), 32); + assert!(core::str::from_utf8(event.name.as_bytes()).is_ok()); + } + + #[test] + fn convert_rssi() { + let props = PeripheralProperties { + rssi: Some(-65), + ..make_props() + }; + let event = convert_properties(&props); + assert_eq!(event.rssi, -65); + } + + #[test] + fn convert_16bit_service_uuid() { + // 0x3100 as a full 128-bit Bluetooth Base UUID + let uuid = Uuid::from_u128(0x00003100_0000_1000_8000_00805f9b34fb); + let props = PeripheralProperties { + services: vec![uuid], + ..make_props() + }; + let event = convert_properties(&props); + assert_eq!(event.service_uuids_16.len(), 1); + assert_eq!(event.service_uuids_16[0], 0x3100); + } + + #[test] + fn convert_non_16bit_uuid_skipped() { + // A custom 128-bit UUID (not from the Bluetooth Base range) + let uuid = Uuid::from_u128(0x4a690001_1c4a_4e3c_b5d8_f47b2e1c0a9d); + let props = PeripheralProperties { + services: vec![uuid], + ..make_props() + }; + let event = convert_properties(&props); + assert!(event.service_uuids_16.is_empty()); + } + + #[test] + fn convert_manufacturer_data() { + let mut mfr = HashMap::new(); + mfr.insert(0x09C8u16, vec![0x01, 0x02]); + let props = PeripheralProperties { + manufacturer_data: mfr, + ..make_props() + }; + let event = convert_properties(&props); + assert_eq!(event.manufacturer_id, 0x09C8); + } + + #[test] + fn convert_no_manufacturer_data() { + let props = make_props(); + let event = convert_properties(&props); + assert_eq!(event.manufacturer_id, 0); + } + + #[test] + fn convert_address_roundtrip() { + let addr = BDAddr::from([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]); + assert_eq!( + address_to_bytes(&addr), + [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE] + ); + } +} diff --git a/hostd/src/location_macos.rs b/hostd/src/location_macos.rs new file mode 100644 index 0000000..2313ec0 --- /dev/null +++ b/hostd/src/location_macos.rs @@ -0,0 +1,81 @@ +//! CoreLocation GPS provider for macOS. +//! +//! Uses `CLLocationManager` with continuous updates to keep a fresh GPS fix. +//! `start()` must be called once at startup; `get_current_location()` reads +//! the cached position on each scan cycle. +//! +//! Requires running from within an `.app` bundle whose `Info.plist` includes +//! `NSLocationWhenInUseUsageDescription` — otherwise macOS won't show the +//! authorization prompt. See `hostd/macos/Info.plist` and `just bundle-hostd`. + +use std::sync::{Mutex, OnceLock}; + +use objc2::rc::Retained; +use objc2_core_location::{CLAuthorizationStatus, CLLocationAccuracy, CLLocationManager}; + +use crate::wigle::Location; + +// kCLLocationAccuracyBest = -1.0 per Apple docs (request best available accuracy) +const ACCURACY_BEST: CLLocationAccuracy = -1.0; + +/// Wrapper to allow `Retained` in a static. +/// Safety: CLLocationManager is only accessed from the main thread or via +/// `location()` which reads a continuously-updated cached property. +struct ManagerWrapper(Retained); +unsafe impl Send for ManagerWrapper {} +unsafe impl Sync for ManagerWrapper {} + +/// Shared CLLocationManager — must persist to keep receiving updates. +static MANAGER: OnceLock> = OnceLock::new(); + +/// Initialize CoreLocation and start continuous location updates. +/// Call once at startup from the main thread. +pub fn start() { + MANAGER.get_or_init(|| { + let mgr = unsafe { CLLocationManager::new() }; + + // Check current authorization and request if needed + let status = unsafe { mgr.authorizationStatus() }; + match status { + CLAuthorizationStatus::NotDetermined => { + log::info!("Requesting Location Services authorization..."); + unsafe { mgr.requestWhenInUseAuthorization() }; + // macOS delivers the auth prompt asynchronously via the app + // bundle's Info.plist. No run loop spin needed. + } + CLAuthorizationStatus::Denied | CLAuthorizationStatus::Restricted => { + log::warn!( + "Location Services denied/restricted — enable for AirHound in \ + System Settings → Privacy & Security → Location Services" + ); + } + _ => { + log::info!("Location Services authorized"); + } + } + + unsafe { mgr.setDesiredAccuracy(ACCURACY_BEST) }; + unsafe { mgr.startUpdatingLocation() }; + log::info!("CoreLocation started (continuous updates)"); + Mutex::new(ManagerWrapper(mgr)) + }); +} + +/// Get the current location from the continuously-updated cache. +/// Returns None if GPS hasn't resolved yet. +pub fn get_current_location() -> Option { + let mutex = MANAGER.get()?; + let guard = mutex.lock().ok()?; + let loc = unsafe { guard.0.location() }?; + let coord = unsafe { loc.coordinate() }; + let accuracy = unsafe { loc.horizontalAccuracy() }; + if accuracy < 0.0 { + return None; // negative accuracy = invalid + } + Some(Location { + latitude: coord.latitude, + longitude: coord.longitude, + accuracy, + altitude: unsafe { loc.altitude() }, + }) +} diff --git a/hostd/src/main.rs b/hostd/src/main.rs new file mode 100644 index 0000000..92b4232 --- /dev/null +++ b/hostd/src/main.rs @@ -0,0 +1,423 @@ +//! AirHound host daemon — WiFi + BLE surveillance detection and WiGLE CSV logger. +//! +//! Scans for BLE advertisements and WiFi networks, logs ALL observations to +//! WiGLE CSV v1.6 format, and outputs NDJSON matches to stdout for matched +//! surveillance devices. + +mod ble; +#[cfg(target_os = "macos")] +mod location_macos; +#[cfg(target_os = "macos")] +mod wifi_macos; +mod wigle; + +use std::io::{self, BufRead, Write}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use chrono::Local; +use clap::Parser; +use tokio::sync::mpsc; + +use airhound::board::BOARD_NAME; +use airhound::comm; +use airhound::defaults; +use airhound::filter::{ + filter_ble_with_rules, filter_wifi_with_rules, BleScanInput, FilterConfig, WiFiScanInput, +}; +use airhound::mac_index::MacIndex; +use airhound::protocol::{self, DeviceMessage, HostCommand}; +use airhound::scanner::ScanEvent; + +use wigle::{Location, WigleWriter}; + +static SCANNING: AtomicBool = AtomicBool::new(true); + +#[derive(Parser)] +#[command( + name = "airhound-hostd", + about = "AirHound host daemon — WiFi/BLE scanner with WiGLE CSV output" +)] +struct Args { + /// Output WiGLE CSV file path [default: airhound-YYYYMMDD-HHMMSS.csv] + #[arg(short, long)] + output: Option, +} + +/// Serialize a `DeviceMessage` and write it as NDJSON to stdout. +fn emit_message(msg: &DeviceMessage<'_>) { + let mut buf = [0u8; protocol::MAX_MSG_LEN]; + if let Some(len) = comm::serialize_message(msg, &mut buf) { + let stdout = io::stdout(); + let mut out = stdout.lock(); + let _ = out.write_all(&buf[..len]); + let _ = out.flush(); + } +} + +/// Generate default output filename with current timestamp. +fn default_output_path() -> PathBuf { + let ts = Local::now().format("%Y%m%d-%H%M%S"); + PathBuf::from(format!("airhound-{ts}.csv")) +} + +/// Get the current GPS location (macOS: CoreLocation, other: always None). +fn get_location() -> Option { + #[cfg(target_os = "macos")] + { + location_macos::get_current_location() + } + #[cfg(not(target_os = "macos"))] + { + None + } +} + +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format_timestamp_millis() + .init(); + + let args = Args::parse(); + + log::info!( + "AirHound hostd v{} starting on {}", + protocol::VERSION, + BOARD_NAME + ); + + // Initialize CoreLocation for GPS + #[cfg(target_os = "macos")] + location_macos::start(); + + // Open WiGLE CSV output file + let output_path = args.output.unwrap_or_else(default_output_path); + let csv_file = match std::fs::File::create(&output_path) { + Ok(f) => f, + Err(e) => { + log::error!( + "Failed to create output file {}: {e}", + output_path.display() + ); + std::process::exit(1); + } + }; + let wigle_writer = match WigleWriter::new(csv_file) { + Ok(w) => w, + Err(e) => { + log::error!("Failed to write WiGLE header: {e}"); + std::process::exit(1); + } + }; + log::info!("WiGLE CSV output: {}", output_path.display()); + + // Shared state + let config = Arc::new(Mutex::new(FilterConfig::new())); + let mac_index = Arc::new(MacIndex::from_defaults()); + let start_time = Instant::now(); + + log::info!( + "Filter loaded: {} signatures, {} rules, {} MAC prefixes", + defaults::SIG_COUNT, + defaults::DEFAULT_RULE_DB.rules.len(), + defaults::MAC_PREFIXES.len(), + ); + + // Unified channel: scanners → filter task + let (scan_tx, scan_rx) = mpsc::channel::(32); + + // Spawn BLE scanner + let ble_tx = scan_tx.clone(); + let ble_handle = tokio::spawn(async move { + let adapter = match ble::get_adapter().await { + Ok(a) => a, + Err(e) => { + log::error!("No BLE adapter found: {e}"); + return; + } + }; + log::info!("BLE adapter found"); + + if let Err(e) = ble::scan_loop(adapter, ble_tx).await { + log::error!("BLE scan error: {e}"); + } + }); + + // Spawn WiFi scanner (macOS: CoreWLAN; other platforms: no-op) + let wifi_tx = scan_tx.clone(); + let wifi_handle = tokio::spawn(async move { + #[cfg(target_os = "macos")] + if let Err(e) = wifi_macos::scan_loop(wifi_tx).await { + log::error!("WiFi scan error: {e}"); + } + #[cfg(not(target_os = "macos"))] + { + let _ = wifi_tx; + log::info!("WiFi scanning not available on this platform"); + } + }); + + // Drop the original sender so the channel closes when all scanners exit + drop(scan_tx); + + // Spawn filter + output task (owns wigle writer exclusively — no Arc/Mutex needed) + let filter_config = config.clone(); + let filter_mac_index = mac_index.clone(); + let filter_start = start_time; + let filter_handle = tokio::spawn(async move { + filter_task( + scan_rx, + filter_config, + filter_mac_index, + filter_start, + wigle_writer, + ) + .await; + }); + + // Spawn stdin command reader + let cmd_config = config.clone(); + let cmd_start = start_time; + let stdin_handle = tokio::spawn(async move { + stdin_command_loop(cmd_config, cmd_start).await; + }); + + // Scanner exits are non-fatal — they drop their tx handles, which eventually + // closes the scan channel and lets filter_task drain and exit naturally. + tokio::spawn(async move { + let _ = ble_handle.await; + log::warn!("BLE scanner exited"); + }); + tokio::spawn(async move { + let _ = wifi_handle.await; + log::warn!("WiFi scanner exited"); + }); + tokio::spawn(async move { + let _ = stdin_handle.await; + log::debug!("Stdin reader exited"); + }); + + // Shut down on ctrl-c or when the filter task exits (all scanners gone) + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Shutting down..."); + } + _ = filter_handle => { + log::info!("All scanners exited, shutting down"); + } + } +} + +/// Filter task: receives ScanEvents, writes ALL to WiGLE CSV, emits NDJSON for matches. +async fn filter_task( + mut rx: mpsc::Receiver, + config: Arc>, + mac_index: Arc, + start_time: Instant, + mut wigle: WigleWriter, +) { + let mut mac_buf = protocol::MacString::new(); + let mut last_flush = Instant::now(); + + while let Some(event) = rx.recv().await { + if !SCANNING.load(Ordering::Relaxed) { + continue; + } + + let cfg = *config.lock().unwrap_or_else(|e| e.into_inner()); + let now = Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let location = get_location(); + + match event { + ScanEvent::Ble(ref ble_event) => { + // Write observation to WiGLE CSV (all networks, not just matches) + mac_buf.clear(); + protocol::format_mac(&ble_event.mac, &mut mac_buf); + if let Err(e) = wigle.write_ble( + &mac_buf, + ble_event.name.as_str(), + &now, + ble_event.rssi, + ble_event.manufacturer_id, + location.as_ref(), + ) { + log::warn!("WiGLE write error: {e}"); + } + + // Run filter and emit NDJSON if matched + handle_ble_event(ble_event, &cfg, &mac_index, start_time); + } + ScanEvent::WiFi(ref wifi_event) => { + // Write observation to WiGLE CSV (all networks, not just matches) + mac_buf.clear(); + protocol::format_mac(&wifi_event.mac, &mut mac_buf); + let freq = wigle::channel_to_frequency(wifi_event.channel); + if let Err(e) = wigle.write_wifi( + &mac_buf, + wifi_event.ssid.as_str(), + wifi_event.security.as_str(), + &now, + wifi_event.channel, + freq, + wifi_event.rssi, + location.as_ref(), + ) { + log::warn!("WiGLE write error: {e}"); + } + + // Run filter and emit NDJSON if matched + handle_wifi_event(wifi_event, &cfg, &mac_index, start_time); + } + #[allow(unreachable_patterns)] + _ => {} + } + + // Flush periodically (every 5s) to balance durability and I/O overhead + if last_flush.elapsed() >= std::time::Duration::from_secs(5) { + if let Err(e) = wigle.flush() { + log::warn!("WiGLE flush error: {e}"); + } + last_flush = Instant::now(); + } + } + + // Flush remaining buffered writes on shutdown + if let Err(e) = wigle.flush() { + log::warn!("WiGLE flush error on shutdown: {e}"); + } +} + +/// Filter and emit a BLE scan event (NDJSON on stdout if matched). +fn handle_ble_event( + event: &airhound::scanner::BleEvent, + cfg: &FilterConfig, + mac_index: &MacIndex, + start_time: Instant, +) { + let input = BleScanInput { + mac: &event.mac, + name: event.name.as_str(), + rssi: event.rssi, + service_uuids_16: &event.service_uuids_16, + manufacturer_id: event.manufacturer_id, + raw_ad: &event.raw_ad, + }; + + let result = filter_ble_with_rules(&input, cfg, &defaults::DEFAULT_RULE_DB, mac_index); + if !result.matched { + return; + } + + let mut mac_str = protocol::MacString::new(); + protocol::format_mac(&event.mac, &mut mac_str); + + let first_uuid = if !event.service_uuids_16.is_empty() { + let mut s = protocol::UuidString::new(); + let _ = core::fmt::Write::write_fmt( + &mut s, + format_args!("0x{:04X}", event.service_uuids_16[0]), + ); + Some(s) + } else { + None + }; + + let ts = start_time.elapsed().as_millis() as u32; + + let msg = DeviceMessage::BleScan { + mac: &mac_str, + name: &event.name, + rssi: event.rssi, + uuid: first_uuid.as_ref(), + mfr: event.manufacturer_id, + matches: &result.matches, + rule: result.rule_names.first().copied(), + ts, + }; + + emit_message(&msg); +} + +/// Filter and emit a WiFi scan event (NDJSON on stdout if matched). +fn handle_wifi_event( + event: &airhound::scanner::WiFiEvent, + cfg: &FilterConfig, + mac_index: &MacIndex, + start_time: Instant, +) { + let input = WiFiScanInput { + mac: &event.mac, + ssid: event.ssid.as_str(), + rssi: event.rssi, + }; + + let result = filter_wifi_with_rules(&input, cfg, &defaults::DEFAULT_RULE_DB, mac_index); + if !result.matched { + return; + } + + let mut mac_str = protocol::MacString::new(); + protocol::format_mac(&event.mac, &mut mac_str); + + let ts = start_time.elapsed().as_millis() as u32; + + let msg = DeviceMessage::WiFiScan { + mac: &mac_str, + ssid: &event.ssid, + rssi: event.rssi, + ch: event.channel, + frame: event.frame_type.as_str(), + matches: &result.matches, + rule: result.rule_names.first().copied(), + ts, + }; + + emit_message(&msg); +} + +/// Read NDJSON commands from stdin and dispatch them. +async fn stdin_command_loop(config: Arc>, start_time: Instant) { + // Run stdin reading in a blocking thread + let (cmd_tx, mut cmd_rx) = mpsc::channel::(8); + + std::thread::spawn(move || { + let stdin = io::stdin(); + let reader = stdin.lock(); + for line in reader.lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if let Some(cmd) = comm::parse_command(line.as_bytes()) { + if cmd_tx.blocking_send(cmd).is_err() { + break; + } + } + } + }); + + while let Some(cmd) = cmd_rx.recv().await { + let mut cfg = config.lock().unwrap_or_else(|e| e.into_inner()); + let mut scanning = SCANNING.load(Ordering::Relaxed); + + comm::handle_command(&cmd, &mut cfg, &mut scanning); + SCANNING.store(scanning, Ordering::Relaxed); + + // Handle status request + if matches!(cmd, HostCommand::GetStatus) { + let uptime_secs = start_time.elapsed().as_secs() as u32; + let msg = DeviceMessage::Status { + scanning, + uptime: uptime_secs, + heap_free: 0, // not meaningful on host + ble_clients: 0, + board: BOARD_NAME, + version: protocol::VERSION, + }; + emit_message(&msg); + } + } +} diff --git a/hostd/src/wifi_macos.rs b/hostd/src/wifi_macos.rs new file mode 100644 index 0000000..8bf700a --- /dev/null +++ b/hostd/src/wifi_macos.rs @@ -0,0 +1,147 @@ +//! WiFi scanner for macOS using `wifi_scan` crate (CoreWLAN wrapper). + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use tokio::sync::mpsc; + +use airhound::comm; +use airhound::scanner::{FrameType, ScanEvent, WiFiEvent}; +use wifi_scan::WifiSecurity; + +/// Polling interval between WiFi scans. +/// CoreWLAN active scans take ~3-5s, so effective cycle is ~4-6s. +const SCAN_INTERVAL: Duration = Duration::from_secs(1); + +/// Whether we've already warned about missing Location Services. +static LOCATION_WARNING_LOGGED: AtomicBool = AtomicBool::new(false); + +/// Run the WiFi scan loop, periodically scanning via CoreWLAN and sending WiFiEvents. +/// +/// Sends discovered events on `tx`. Runs until the channel is closed or an error occurs. +pub async fn scan_loop(tx: mpsc::Sender) -> Result<(), ScanError> { + log::info!("WiFi scan started (CoreWLAN via wifi_scan, interval {SCAN_INTERVAL:?})"); + + loop { + let networks = tokio::task::spawn_blocking(wifi_scan::scan) + .await + .map_err(|_| ScanError::TaskPanicked)?; + + match networks { + Ok(wifis) => { + // Check for Location Services on first scan + if !wifis.is_empty() + && !LOCATION_WARNING_LOGGED.load(Ordering::Relaxed) + && wifis.iter().all(|w| w.mac.is_empty()) + { + log::warn!( + "WiFi scan: all BSSIDs empty — grant Location Services permission \ + to the terminal app in System Settings → Privacy & Security → Location Services" + ); + LOCATION_WARNING_LOGGED.store(true, Ordering::Relaxed); + } + + let mut count = 0u32; + for wifi in &wifis { + if let Some(event) = convert_network(wifi) { + count += 1; + if tx.send(ScanEvent::WiFi(event)).await.is_err() { + log::debug!("WiFi scan channel closed, stopping"); + return Ok(()); + } + } + } + log::debug!("WiFi scan: {count} networks (of {} total)", wifis.len()); + } + Err(e) => { + log::warn!("WiFi scan failed: {e}"); + } + } + + tokio::time::sleep(SCAN_INTERVAL).await; + } +} + +/// Convert a `wifi_scan::Wifi` into an AirHound `WiFiEvent`. +fn convert_network(network: &wifi_scan::Wifi) -> Option { + // Skip networks with empty/unparseable BSSID (Location Services not granted) + let mac = comm::parse_mac_string(&network.mac)?; + + let mut ssid = heapless::String::new(); + if !network.ssid.is_empty() { + let s = &network.ssid; + let end = if s.len() <= 33 { + s.len() + } else { + s.floor_char_boundary(33) + }; + let _ = ssid.push_str(&s[..end]); + } + + let rssi = network.signal_level.clamp(i8::MIN as i32, i8::MAX as i32) as i8; + + let channel = network.channel as u8; + + let security = format_security(&network.security); + + Some(WiFiEvent { + mac, + ssid, + rssi, + channel, + frame_type: FrameType::Beacon, // CoreWLAN only returns beacon-equivalent data + raw_ies: heapless::Vec::new(), + security, + }) +} + +/// Format `wifi_scan::WifiSecurity` variants into WiGLE AuthMode bracket notation. +fn format_security(secs: &[WifiSecurity]) -> heapless::String<64> { + let mut s = heapless::String::new(); + if secs.is_empty() || secs.iter().all(|s| matches!(s, WifiSecurity::Unknown)) { + let _ = s.push_str("[?]"); + return s; + } + for sec in secs { + let tag = match sec { + WifiSecurity::Open => "[OPEN]", + WifiSecurity::Wep => "[WEP]", + WifiSecurity::WpaPersonal => "[WPA-PSK]", + WifiSecurity::Wpa2PersonalPsk => "[WPA2-PSK]", + WifiSecurity::Wpa3PersonalSae => "[WPA3-SAE]", + WifiSecurity::Wpa2EnterpriseEap => "[WPA2-EAP]", + WifiSecurity::Wpa3EnterpriseEap256 => "[WPA3-EAP256]", + WifiSecurity::Wpa3EnterpriseSuiteBEap256 => "[WPA3-SUITEB]", + WifiSecurity::Wpa2EnterpriseEapFt => "[WPA2-EAP-FT]", + WifiSecurity::Wpa3PersonalPsk256 => "[WPA3-PSK256]", + WifiSecurity::Wpa2PersonalPskFt => "[WPA2-PSK-FT]", + WifiSecurity::Wpa3PersonalSaeFt => "[WPA3-SAE-FT]", + WifiSecurity::WpaEnterprise => "[WPA-EAP]", + WifiSecurity::Personal => "[PSK]", + WifiSecurity::Enterprise => "[EAP]", + WifiSecurity::Tdls => "[TDLS]", + WifiSecurity::Unknown => continue, + _ => continue, + }; + let _ = s.push_str(tag); + } + if s.is_empty() { + let _ = s.push_str("[?]"); + } + s +} + +#[derive(Debug)] +pub enum ScanError { + TaskPanicked, +} + +impl std::fmt::Display for ScanError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScanError::TaskPanicked => write!(f, "scan task panicked"), + } + } +} + +// MAC parsing tests live in the library (airhound::comm::parse_mac_string). diff --git a/hostd/src/wigle.rs b/hostd/src/wigle.rs new file mode 100644 index 0000000..f20eae4 --- /dev/null +++ b/hostd/src/wigle.rs @@ -0,0 +1,350 @@ +//! WiGLE CSV v1.6 writer. +//! +//! Observation-based: each row is one sighting at a point in time and space. +//! The same BSSID seen from different locations/times produces multiple rows. +//! +//! Uses the `csv` crate for correct field escaping and column alignment. + +use std::io::{self, Write}; + +/// A GPS fix for WiGLE CSV rows. Platform-agnostic — populated by +/// `location_macos` on macOS, `None` elsewhere. +#[derive(Debug, Clone, Copy)] +pub struct Location { + pub latitude: f64, + pub longitude: f64, + pub accuracy: f64, + pub altitude: f64, +} + +/// WiGLE CSV v1.6 writer. +/// +/// The first header row is non-standard metadata (not a valid CSV record), +/// so we write it raw and use `csv::Writer` for the column header + data rows. +pub struct WigleWriter { + writer: csv::Writer, +} + +impl WigleWriter { + /// Create a new writer and emit the two WiGLE header rows. + pub fn new(mut inner: W) -> io::Result { + // Row 1: WiGLE metadata (non-standard, written raw) + writeln!( + inner, + "WigleWifi-1.6,appRelease=AirHound-{version},model=host,release={version},device=AirHound,display=,board=,brand=", + version = env!("CARGO_PKG_VERSION"), + )?; + inner.flush()?; + + // Row 2: column headers (written via csv::Writer so field count is locked in) + let mut writer = csv::WriterBuilder::new() + .has_headers(false) + .from_writer(inner); + writer.write_record(COLUMNS).map_err(io::Error::other)?; + writer.flush().map_err(io::Error::other)?; + Ok(Self { writer }) + } + + /// Write a WiFi observation row. + #[allow(clippy::too_many_arguments)] + pub fn write_wifi( + &mut self, + mac: &str, + ssid: &str, + auth: &str, + seen: &str, + channel: u8, + frequency: u32, + rssi: i8, + location: Option<&Location>, + ) -> io::Result<()> { + let ch = channel.to_string(); + let freq = frequency.to_string(); + let rssi_s = rssi.to_string(); + let (lat, lon, alt, acc) = format_location(location); + self.writer + .write_record([ + mac, ssid, auth, seen, &ch, &freq, &rssi_s, &lat, &lon, &alt, &acc, "", "", "WIFI", + ]) + .map_err(io::Error::other) + } + + /// Write a BLE observation row. + pub fn write_ble( + &mut self, + mac: &str, + name: &str, + seen: &str, + rssi: i8, + mfr_id: u16, + location: Option<&Location>, + ) -> io::Result<()> { + let rssi_s = rssi.to_string(); + let (lat, lon, alt, acc) = format_location(location); + let mfr_str = if mfr_id != 0 { + format!("0x{mfr_id:04X}") + } else { + String::new() + }; + self.writer + .write_record([ + mac, name, "[LE]", seen, "0", "0", &rssi_s, &lat, &lon, &alt, &acc, "", &mfr_str, + "BLE", + ]) + .map_err(io::Error::other) + } + + /// Flush buffered writes to the underlying writer. + pub fn flush(&mut self) -> io::Result<()> { + self.writer.flush().map_err(io::Error::other) + } +} + +/// The 14 WiGLE CSV v1.6 column names. +const COLUMNS: &[&str] = &[ + "MAC", + "SSID", + "AuthMode", + "FirstSeen", + "Channel", + "Frequency", + "RSSI", + "CurrentLatitude", + "CurrentLongitude", + "AltitudeMeters", + "AccuracyMeters", + "RCOIs", + "MfgrId", + "Type", +]; + +/// Format GPS location fields, returning empty strings when unavailable. +fn format_location(location: Option<&Location>) -> (String, String, String, String) { + match location { + Some(loc) => ( + loc.latitude.to_string(), + loc.longitude.to_string(), + loc.altitude.to_string(), + loc.accuracy.to_string(), + ), + None => (String::new(), String::new(), String::new(), String::new()), + } +} + +/// Convert WiFi channel number to frequency in MHz. +pub fn channel_to_frequency(ch: u8) -> u32 { + match ch as u32 { + 1..=13 => 2407 + ch as u32 * 5, + 14 => 2484, + 36..=177 => 5000 + ch as u32 * 5, + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn channel_to_freq_2ghz() { + assert_eq!(channel_to_frequency(1), 2412); + assert_eq!(channel_to_frequency(6), 2437); + assert_eq!(channel_to_frequency(11), 2462); + assert_eq!(channel_to_frequency(13), 2472); + } + + #[test] + fn channel_to_freq_5ghz() { + assert_eq!(channel_to_frequency(36), 5180); + assert_eq!(channel_to_frequency(149), 5745); + } + + #[test] + fn channel_to_freq_14() { + assert_eq!(channel_to_frequency(14), 2484); + } + + #[test] + fn channel_to_freq_unknown() { + assert_eq!(channel_to_frequency(0), 0); + assert_eq!(channel_to_frequency(200), 0); + } + + #[test] + fn wigle_header_format() { + let mut buf = Vec::new(); + { + let _writer = WigleWriter::new(&mut buf).unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + assert!(output.starts_with("WigleWifi-1.6,")); + assert!(output.contains("MAC,SSID,AuthMode,FirstSeen,Channel,Frequency,RSSI,")); + } + + #[test] + fn write_wifi_with_location() { + let mut buf = Vec::new(); + { + let mut w = WigleWriter::new(&mut buf).unwrap(); + let loc = Location { + latitude: 37.7749, + longitude: -122.4194, + altitude: 10.0, + accuracy: 5.0, + }; + w.write_wifi( + "AA:BB:CC:DD:EE:FF", + "TestNet", + "[WPA2-PSK]", + "2024-01-01 12:00:00", + 6, + 2437, + -65, + Some(&loc), + ) + .unwrap(); + w.flush().unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("AA:BB:CC:DD:EE:FF,TestNet,[WPA2-PSK],")); + assert!(output.contains("37.7749")); + assert!(output.contains(",WIFI\n")); + } + + #[test] + fn write_wifi_without_location() { + let mut buf = Vec::new(); + { + let mut w = WigleWriter::new(&mut buf).unwrap(); + w.write_wifi( + "AA:BB:CC:DD:EE:FF", + "TestNet", + "[WPA2-PSK]", + "2024-01-01 12:00:00", + 6, + 2437, + -65, + None, + ) + .unwrap(); + w.flush().unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains(",WIFI\n")); + } + + #[test] + fn write_ble_with_mfr() { + let mut buf = Vec::new(); + { + let mut w = WigleWriter::new(&mut buf).unwrap(); + w.write_ble( + "11:22:33:44:55:66", + "TestBLE", + "2024-01-01 12:00:00", + -72, + 0x004C, + None, + ) + .unwrap(); + w.flush().unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + assert!(output.contains("0x004C,BLE\n")); + } + + #[test] + fn csv_escapes_ssid_with_comma() { + let mut buf = Vec::new(); + { + let mut w = WigleWriter::new(&mut buf).unwrap(); + w.write_wifi( + "AA:BB:CC:DD:EE:FF", + "Net,work", + "[OPEN]", + "2024-01-01 12:00:00", + 1, + 2412, + -50, + None, + ) + .unwrap(); + w.flush().unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + // csv crate wraps the SSID in quotes + assert!(output.contains("\"Net,work\"")); + } + + /// Verify all row variants produce exactly 14 CSV fields (matching the header). + #[test] + fn all_rows_have_14_fields() { + let loc = Location { + latitude: 1.0, + longitude: 2.0, + altitude: 3.0, + accuracy: 4.0, + }; + + let cases: Vec<(&str, Box>)>)> = vec![ + ( + "wifi+loc", + Box::new(|w| { + w.write_wifi("M", "S", "A", "T", 6, 2437, -50, Some(&loc)) + .unwrap(); + }), + ), + ( + "wifi-no-loc", + Box::new(|w| { + w.write_wifi("M", "S", "A", "T", 6, 2437, -50, None) + .unwrap(); + }), + ), + ( + "ble+loc+mfr", + Box::new(|w| { + w.write_ble("M", "N", "T", -50, 0x004C, Some(&loc)).unwrap(); + }), + ), + ( + "ble+loc-no-mfr", + Box::new(|w| { + w.write_ble("M", "N", "T", -50, 0, Some(&loc)).unwrap(); + }), + ), + ( + "ble-no-loc+mfr", + Box::new(|w| { + w.write_ble("M", "N", "T", -50, 0x004C, None).unwrap(); + }), + ), + ( + "ble-no-loc-no-mfr", + Box::new(|w| { + w.write_ble("M", "N", "T", -50, 0, None).unwrap(); + }), + ), + ]; + + for (label, write_fn) in &cases { + let mut buf = Vec::new(); + { + let mut w = WigleWriter::new(&mut buf).unwrap(); + write_fn(&mut w); + w.flush().unwrap(); + } + let output = String::from_utf8(buf).unwrap(); + let lines: Vec<&str> = output.lines().collect(); + // Header (2 lines) + data (1 line) + assert_eq!(lines.len(), 3, "{label}: expected 3 lines"); + let header_fields = lines[1].split(',').count(); + let data_fields = lines[2].split(',').count(); + assert_eq!( + header_fields, data_fields, + "{label}: header has {header_fields} fields but data row has {data_fields}: {:?}", + lines[2] + ); + } + } +} From 5f3a361876486f0b9e532b66432ce711ff133175 Mon Sep 17 00:00:00 2001 From: Doug Borg Date: Sun, 15 Mar 2026 15:54:14 -0600 Subject: [PATCH 3/3] ci: add hostd build/test job, just recipes, and pre-commit hook - CI: hostd check, clippy, test job on ubuntu-latest; fmt and audit - justfile: build-hostd, bundle-hostd, run-hostd (macOS/Linux), check-hostd, test-hostd, fmt-hostd, clippy-hostd recipes - pre-commit: run hostd tests when hostd/ or src/ changes; make library tests conditional on staged src/ files Co-Authored-By: Claude Opus 4.6 --- .githooks/pre-commit | 23 ++++++++++++++---- .github/workflows/ci.yml | 25 ++++++++++++++++++++ justfile | 51 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 80306be..216aaa6 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -57,9 +57,22 @@ with open('$f', 'w') as fh: fi fi -# 5. Run unit tests (if cargo is available) -if has_cmd cargo; then - cargo test --lib --no-default-features --quiet -else - echo "WARN: cargo not found — skipping unit tests (run 'just setup-local')" +# Detect staged source files for conditional test runs +STAGED_HOSTD=$(git diff --cached --name-only --diff-filter=d | grep '^hostd/' || true) +STAGED_LIB=$(git diff --cached --name-only --diff-filter=d | grep '^src/' || true) + +# 5. Run unit tests when library sources change (if cargo is available) +if [ -n "$STAGED_LIB" ]; then + if has_cmd cargo; then + cargo test --lib --no-default-features --quiet + else + echo "WARN: cargo not found — skipping unit tests (run 'just setup-local')" + fi +fi + +# 6. Run hostd tests (uses stable toolchain via hostd/rust-toolchain.toml) +if [ -n "$STAGED_HOSTD" ] || [ -n "$STAGED_LIB" ]; then + if has_cmd cargo && [ -f hostd/Cargo.toml ]; then + (cd hostd && cargo test --quiet) + fi fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index add2787..788f932 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: - run: cargo fmt --check env: RUSTUP_TOOLCHAIN: nightly + - run: cd hostd && cargo fmt --check + env: + RUSTUP_TOOLCHAIN: nightly schemas: runs-on: ubuntu-latest @@ -75,6 +78,28 @@ jobs: run: cargo audit env: RUSTUP_TOOLCHAIN: nightly + - name: Audit hostd dependencies + run: cd hostd && cargo audit + env: + RUSTUP_TOOLCHAIN: nightly + + hostd: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: dtolnay/rust-toolchain@0f1b44df7e9cbb178d781a242338dfa5e243ad7f # stable + with: + components: clippy + - name: Install BLE dev dependencies + run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev + - name: Check + run: cd hostd && cargo check + - name: Clippy + run: cd hostd && cargo clippy -- -D warnings + - name: Test + run: cd hostd && cargo test build: runs-on: ubuntu-latest diff --git a/justfile b/justfile index 24ddec0..487ea35 100644 --- a/justfile +++ b/justfile @@ -86,6 +86,57 @@ flash-m5stickc-host: clean: cargo clean +# ── Desktop (hostd) ───────────────────────────────────── + +# Build the host daemon +[group('hostd')] +build-hostd: + cd hostd && cargo build --release + +# Assemble macOS .app bundle (needed for Location Services permission) +[group('hostd')] +[macos] +bundle-hostd: build-hostd + #!/usr/bin/env bash + set -euo pipefail + APP="hostd/AirHound.app" + rm -rf "$APP" + mkdir -p "$APP/Contents/MacOS" + cp hostd/macos/Info.plist "$APP/Contents/" + ln -s "$(pwd)/hostd/target/release/airhound-hostd" "$APP/Contents/MacOS/airhound-hostd" + +# Run the host daemon (macOS: via app bundle for Location Services) +[group('hostd')] +[macos] +run-hostd: bundle-hostd + RUST_LOG=info hostd/AirHound.app/Contents/MacOS/airhound-hostd + +# Run the host daemon (Linux) +[group('hostd')] +[linux] +run-hostd: build-hostd + RUST_LOG=info hostd/target/release/airhound-hostd + +# Type-check the host daemon +[group('hostd')] +check-hostd: + cd hostd && cargo check + +# Run host daemon unit tests +[group('hostd')] +test-hostd: + cd hostd && cargo test + +# Check host daemon formatting (requires rustfmt) +[group('hostd')] +fmt-hostd: + cd hostd && cargo fmt --check + +# Run clippy on the host daemon +[group('hostd')] +clippy-hostd: + cd hostd && cargo clippy -- -D warnings + # Configure git hooks for this repository [group('host')] setup-hooks: