From 6020a7c777354a38d2d7974121aa3915570156ef Mon Sep 17 00:00:00 2001 From: lambda Date: Fri, 23 Oct 2026 17:28:42 +0300 Subject: [PATCH 1/3] UNKNOWN CHANGES AFTER LONG DEVELOPMENT PAUSE --- src-tauri/src/lib.rs | 6 +-- src-tauri/tauri.conf.json | 5 --- src/assets/javascript.svg | 1 - src/assets/tauri.svg | 6 --- src/colors.css | 8 ++++ src/connect.html | 18 --------- src/connect.js | 7 ---- src/index.html | 12 +++--- src/main.js | 6 +++ src/reset.css | 78 +++++++++++++++++++++++++++++++++++++++ src/splashscreen.html | 8 +--- src/styles.css | 65 +++++++++++++++++++++----------- 12 files changed, 148 insertions(+), 72 deletions(-) delete mode 100644 src/assets/javascript.svg delete mode 100644 src/assets/tauri.svg create mode 100644 src/colors.css delete mode 100644 src/connect.html delete mode 100644 src/connect.js create mode 100644 src/reset.css diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 74d7a09..e0f4e2e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -14,15 +14,15 @@ use btleplug::platform::Manager as BleManager; async fn init(app: AppHandle) -> Result<(),()> { println!("init rideinn app"); let splash = app.get_webview_window("splashscreen").unwrap(); - let connect = app.get_webview_window("connect").unwrap(); + let main = app.get_webview_window("main").unwrap(); let state = app.state::(); let adapter = state.adapter.clone(); let repo = repo_from_devices((scan_devices(&adapter, 5).await).unwrap_or_default()); splash.close().unwrap(); - connect.show().unwrap(); + main.show().unwrap(); - app.emit_to("connect", "devices-loaded", repo).unwrap(); + app.emit("devices-loaded", repo).unwrap(); Ok(()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c702206..94b60a3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,11 +16,6 @@ { "label": "splashscreen", "url": "splashscreen" - }, - { - "label": "connect", - "url": "connect", - "visible": false } ], "security": { diff --git a/src/assets/javascript.svg b/src/assets/javascript.svg deleted file mode 100644 index f9abb2b..0000000 --- a/src/assets/javascript.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/tauri.svg b/src/assets/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/src/assets/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/colors.css b/src/colors.css new file mode 100644 index 0000000..ef7cad9 --- /dev/null +++ b/src/colors.css @@ -0,0 +1,8 @@ +:root { + --jet: hsla(0, 0%, 21%, 1); + --gunmetal: hsla(216, 28%, 20%, 1); + --cadet-gray: hsla(212, 8%, 58%, 1); + --nyanza: hsla(114, 100%, 93%, 1); + --rojo: hsla(357, 73%, 50%, 1); + --basewhite: #f6f6f6; +} diff --git a/src/connect.html b/src/connect.html deleted file mode 100644 index b0c5389..0000000 --- a/src/connect.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - Tauri App - - - - -
-

Connect

-
-
-
- - diff --git a/src/connect.js b/src/connect.js deleted file mode 100644 index d63a8e0..0000000 --- a/src/connect.js +++ /dev/null @@ -1,7 +0,0 @@ -const { once } = window.__TAURI__.event; -const { getCurrentWebviewWindow } = window.__TAURI__.webviewWindow; - -const appWebview = getCurrentWebviewWindow(); -appWebview.once("devices-loaded", (devices) => { - console.log("Devices are loaded", devices); -}); diff --git a/src/index.html b/src/index.html index 58437f5..cb08f48 100644 --- a/src/index.html +++ b/src/index.html @@ -9,10 +9,12 @@ -
-

Welcome to RideInn

-
-
-
+
+

подключение датчиков

+

чсс

+

каденс

+

станок

+ +
diff --git a/src/main.js b/src/main.js index 906574e..4acf346 100644 --- a/src/main.js +++ b/src/main.js @@ -1 +1,7 @@ +const { once } = window.__TAURI__.event; + +once("devices-loaded", (devices) => { + console.log("Devices are loaded", devices); +}); + console.log("hello from main.js"); diff --git a/src/reset.css b/src/reset.css new file mode 100644 index 0000000..7d4619c --- /dev/null +++ b/src/reset.css @@ -0,0 +1,78 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + vertical-align: baseline; + box-sizing: border-box; + background: transparent; +} + +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +ol, ul { + list-style: none; +} + +img, picture, svg, video, iframe { + max-width: 100%; + height: auto; + display: block; +} + +button, input, select, textarea { + font: inherit; + color: inherit; + background: transparent; + border: none; + padding: 0; + margin: 0; + outline: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +:focus { + outline: 2px solid Highlight; + outline-offset: 2px; +} + +button, [role="button"], a[type="button"] { + cursor: pointer; +} + +a { + color: inherit; + text-decoration: none; + background-color: transparent; +} + +:disabled, [disabled] { + cursor: not-allowed; + opacity: 0.6; + pointer-events: none; +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } +} diff --git a/src/splashscreen.html b/src/splashscreen.html index 0e5f935..1410473 100644 --- a/src/splashscreen.html +++ b/src/splashscreen.html @@ -8,11 +8,7 @@ - -
-

SplashScreen

-
-
-
+ +

поиск перефирии

diff --git a/src/styles.css b/src/styles.css index e5864df..cf10645 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,11 +1,14 @@ +@import "./reset.css"; +@import "./colors.css"; + :root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; line-height: 24px; font-weight: 400; - color: #0f0f0f; - background-color: #f6f6f6; + color: var(--basewhite); + background-color: var(--jet); font-synthesis: none; text-rendering: optimizeLegibility; @@ -14,22 +17,42 @@ -webkit-text-size-adjust: 100%; } -@media (prefers-color-scheme: dark) { - :root { - color: #f6f6f6; - background-color: #2f2f2f; - } - - a:hover { - color: #24c8db; - } - - input, - button { - color: #ffffff; - background-color: #0f0f0f98; - } - button:active { - background-color: #0f0f0f69; - } +body, +screen { + overflow: hidden; +} + +.screen { + align-content: center; + display: grid; + height: 100vh; + justify-content: center; + width: 100vw; +} + +#connect { + gap: 2em; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(3, 1fr); + padding: 2em; +} + +#connect h1 { + grid-column: 1/-1; + justify-self: center; + align-self: end; +} + +#connect button { + grid-column: 1/-1; + justify-self: center; + align-self: start; +} + +.device-selection { + border: 1px solid var(--basewhite); + border-radius: 2%; + display: grid; + justify-content: center; + padding: 0.5em; } -- 2.36.5 From 548a517391b650b902321c6e3e6cd404ead85707 Mon Sep 17 00:00:00 2001 From: lambda Date: Sun, 25 Oct 2026 19:56:57 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=BB=D0=B3=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D1=80=D1=8B=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/ble/error.rs | 18 +++-- src-tauri/src/ble/mod.rs | 1 + src-tauri/src/ble/scanner.rs | 118 +++++++++++++++++------------- src-tauri/src/device/device.rs | 111 ---------------------------- src-tauri/src/device/mod.rs | 113 +++++++++++++++++++++++++++- src-tauri/src/device_repo/mod.rs | 2 +- src-tauri/src/device_repo/repo.rs | 17 ++--- src-tauri/src/lib.rs | 43 ++++++++--- 10 files changed, 234 insertions(+), 191 deletions(-) delete mode 100644 src-tauri/src/device/device.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d12fe54..bf1e879 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3197,6 +3197,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "thiserror 1.0.69", "tokio", "uuid", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 10be68a..fed8d08 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,6 +26,7 @@ btleplug = "0.11.8" tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } hex = "0.4" uuid = "1.16.0" +thiserror = "1" [profile.dev] incremental = true # Compile your binary in smaller steps. diff --git a/src-tauri/src/ble/error.rs b/src-tauri/src/ble/error.rs index 6ee1ef8..d7be963 100644 --- a/src-tauri/src/ble/error.rs +++ b/src-tauri/src/ble/error.rs @@ -1,10 +1,14 @@ -#[derive(Debug)] +use thiserror::Error; + +/// Ошибки BLE-слоя. В отличие от прежней заглушки сохраняют источник +/// (`btleplug::Error`), чтобы при отладке было видно, что именно пошло не так. +#[derive(Debug, Error)] pub enum BleError { - SystemBleError -} + /// Не удалось запустить сканирование. + #[error("не удалось запустить сканирование BLE: {0}")] + ScanFailed(btleplug::Error), -impl From for BleError { - fn from(_: btleplug::Error) -> Self { - BleError::SystemBleError - } + /// Не удалось получить список найденных устройств. + #[error("не удалось получить список BLE-устройств: {0}")] + PeripheralsFailed(btleplug::Error), } diff --git a/src-tauri/src/ble/mod.rs b/src-tauri/src/ble/mod.rs index 3edc455..ad8f3fa 100644 --- a/src-tauri/src/ble/mod.rs +++ b/src-tauri/src/ble/mod.rs @@ -2,5 +2,6 @@ mod error; mod utils; mod scanner; +pub use error::BleError; pub use scanner::scan_devices; pub use utils::normalize_uuid; diff --git a/src-tauri/src/ble/scanner.rs b/src-tauri/src/ble/scanner.rs index c613da9..ad8309f 100644 --- a/src-tauri/src/ble/scanner.rs +++ b/src-tauri/src/ble/scanner.rs @@ -4,75 +4,95 @@ use std::time::Duration; use tokio::time; use crate::ble::error::BleError; use crate::device::Device; -use hex; -use uuid::{Uuid}; +use uuid::Uuid; +/// Сервисы, по которым ищем датчики. Только целевые (HR / CSC / FTMS / мощность). +/// Battery (0x180F) и Device Info (0x180A) убраны: их рекламирует почти любое +/// BLE-устройство, из-за чего скан подхватывал много лишнего и подключался к нему. static TARGET_UUIDS: &[Uuid] = &[ uuid_from_u16(0x180D), // Heart Rate uuid_from_u16(0x1816), // Cycling Speed and Cadence uuid_from_u16(0x1826), // Fitness Machine (FTMS) - uuid_from_u16(0x180F), // Battery Level - uuid_from_u16(0x180A), // Device info - uuid_from_u16(0x1818), // Power meter + uuid_from_u16(0x1818), // Cycling Power ]; +const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +const DISCOVER_TIMEOUT: Duration = Duration::from_secs(5); +const DISCONNECT_TIMEOUT: Duration = Duration::from_secs(2); + pub async fn scan_devices(adapter: &Adapter, scan_seconds: u64) -> Result, BleError> { - let target_vec: Vec = TARGET_UUIDS.to_vec(); + adapter + .start_scan(ScanFilter { services: TARGET_UUIDS.to_vec() }) + .await + .map_err(BleError::ScanFailed)?; - adapter.start_scan(ScanFilter { services: target_vec.clone() }).await?; time::sleep(Duration::from_secs(scan_seconds)).await; + let peripherals = adapter.peripherals().await.map_err(BleError::PeripheralsFailed)?; + let mut devices = Vec::new(); + for peripheral in peripherals { + // Свойства берём из рекламы; если устройство уже пропало — пропускаем. + let Some(props) = peripheral.properties().await.unwrap_or(None) else { + continue; + }; - for peripheral in adapter.peripherals().await? { - if let Some(props) = peripheral.properties().await? { - let svc_match = props.services.iter().any(|s| TARGET_UUIDS.iter().any(|t| t == s)); - if !svc_match { continue; } + let svc_match = props.services.iter().any(|s| TARGET_UUIDS.contains(s)); + if !svc_match { + continue; + } - let manuf = props.manufacturer_data.iter().next().map(|(_,v)| hex::encode(v)); + // Для классификации по типу нужны характеристики — подключаемся, читаем, + // отключаемся (best-effort: при сбое устройство всё равно попадёт в список, + // просто без характеристик). + let characteristics = discover_characteristics(&peripheral).await; - let mut characteristics: Vec = Vec::new(); + let manuf = props.manufacturer_data.iter().next().map(|(_, v)| hex::encode(v)); - let is_connected = peripheral.is_connected().await.unwrap_or(false); - if !is_connected { - let _ = tokio::time::timeout(Duration::from_secs(5), peripheral.connect()).await; - } + devices.push(Device { + id: peripheral.id().to_string(), + address: props.address.to_string(), + display_name: props.local_name, + rssi: props.rssi, + service_uuids: props.services.iter().map(|u| u.to_string()).collect(), + manufacturer_data: manuf, + metadata: None, + characteristics, + }); + } - let _ = tokio::time::timeout(Duration::from_secs(5), peripheral.discover_services()).await; - - let chars = peripheral.characteristics(); - if !chars.is_empty() { - for ch in chars.iter() { - let entry = format!("{}::{}", ch.service_uuid, ch.uuid); - characteristics.push(entry); - } - } else { - for svc in peripheral.services().iter() { - for ch in svc.characteristics.iter() { - let entry = format!("{}::{}", svc.uuid, ch.uuid); - characteristics.push(entry); - } - } - } + adapter.stop_scan().await.ok(); + Ok(devices) +} - if !is_connected { - let _ = tokio::time::timeout(Duration::from_secs(2), peripheral.disconnect()).await; - } +/// Подключается к устройству, читает характеристики и отключается. +/// Возвращает список характеристик в формате «service::characteristic». +/// Best-effort: неудачи connect/discover игнорируются. +async fn discover_characteristics(peripheral: &impl Peripheral) -> Vec { + let was_connected = peripheral.is_connected().await.unwrap_or(false); + + if !was_connected { + let _ = time::timeout(CONNECT_TIMEOUT, peripheral.connect()).await; + } + let _ = time::timeout(DISCOVER_TIMEOUT, peripheral.discover_services()).await; - devices.push(Device { - id: peripheral.id().to_string(), - address: props.address.to_string(), - display_name: props.local_name, - rssi: props.rssi, - service_uuids: props.services.iter().map(|u| u.to_string()).collect(), - manufacturer_data: manuf, - metadata: None, - characteristics, - }); + let mut characteristics = Vec::new(); + let chars = peripheral.characteristics(); + if !chars.is_empty() { + for ch in chars.iter() { + characteristics.push(format!("{}::{}", ch.service_uuid, ch.uuid)); + } + } else { + for svc in peripheral.services().iter() { + for ch in svc.characteristics.iter() { + characteristics.push(format!("{}::{}", svc.uuid, ch.uuid)); + } } } - adapter.stop_scan().await.ok(); - println!("{:?}", devices); - Ok(devices) + if !was_connected { + let _ = time::timeout(DISCONNECT_TIMEOUT, peripheral.disconnect()).await; + } + + characteristics } diff --git a/src-tauri/src/device/device.rs b/src-tauri/src/device/device.rs deleted file mode 100644 index 24051a8..0000000 --- a/src-tauri/src/device/device.rs +++ /dev/null @@ -1,111 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::ble::normalize_uuid; - -#[derive(Debug, Serialize, Clone, Deserialize)] -pub struct Device { - pub id: String, - pub address: String, - pub display_name: Option, - pub rssi: Option, - pub service_uuids: Vec, - pub characteristics: Vec, - pub manufacturer_data: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option, -} - -impl Device {} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DeviceType { - HrSensor, - CadSensor, - Trainer -} - -pub fn get_device_types(device: &Device) -> Vec { - let hr_services = ["180d"]; - let hr_characteristics = ["2a37"]; - - let csc_services = ["1816"]; - let csc_characteristics = ["2a5b"]; - - // trainer / cycling power / fitness machine / indoor bike - let trainer_services = ["1818", "1826"]; - let trainer_characteristics = ["2a63", "2ad2", "2acc"]; // 2AD2 = Indoor Bike Data, 2ACC = Fitness Machine Characteristics, 2A63 = Cycling Power Measurement - - // любые 2ADx характеристики (indoor bike related) — учитывать как признак каденса доступного в тренажёре - let indoor_bike_chars_prefix = "2ad"; // covers 2ad2,2ad5,2ad6,2ad8,2ad9,2ada etc. - - // Нормализуем все UUIDs - let mut norms = Vec::with_capacity(device.service_uuids.len() + device.characteristics.len()); - for u in device.service_uuids.iter().chain(device.characteristics.iter()) { - // Для характеристик, которые приходят в виде "service::characteristic", извлечём часть после "::" - let part = if let Some(idx) = u.find("::") { - &u[idx + 2..] - } else { - u.as_str() - }; - norms.push(normalize_uuid(part)); - } - - let contains_any = |candidates: &[&str]| -> bool { - for cand in candidates { - let cand = cand.to_ascii_lowercase(); - if norms.iter().any(|n| n == &cand || n.ends_with(&cand)) { - return true; - } - } - false - }; - - let mut out = Vec::new(); - - // Heart rate - if contains_any(&hr_services) || contains_any(&hr_characteristics) { - out.push(DeviceType::HrSensor); - } - - // CSC / Cadence - if contains_any(&csc_services) || contains_any(&csc_characteristics) { - out.push(DeviceType::CadSensor); - } - - // Trainer/Cycling power/Indoor bike - let is_trainer = contains_any(&trainer_services) || contains_any(&trainer_characteristics); - if is_trainer { - out.push(DeviceType::Trainer); - } - - // Доп. логика: если найдены Indoor Bike Data (любые 2ADx characteristics), считаем, что устройство может отдавать cadence — - // добавляем CadSensor, даже если CSC (0x1816/0x2A5B) отсутствует. - let has_indoor_bike_char = norms.iter().any(|n| n.starts_with(indoor_bike_chars_prefix)); - if has_indoor_bike_char && !out.contains(&DeviceType::CadSensor) { - out.push(DeviceType::CadSensor); - } - - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_service_types_for_magene_speedcadence() { - let magene = Device { - id: "hci0/dev_F1_F2_7C_77_14_BE".into(), - address: "F1:F2:7C:77:14:BE".into(), - display_name: Some("10911-17".into()), - rssi: Some(-42), - service_uuids: vec!["00001816-0000-1000-8000-00805f9b34fb".into()], - characteristics: vec![], - manufacturer_data: Some("6b009f2a110300030072".into()), - metadata: None, - }; - - assert_eq!(get_device_types(&magene), [DeviceType::CadSensor]); - } -} diff --git a/src-tauri/src/device/mod.rs b/src-tauri/src/device/mod.rs index 4e30d8a..631f103 100644 --- a/src-tauri/src/device/mod.rs +++ b/src-tauri/src/device/mod.rs @@ -1,2 +1,111 @@ -pub mod device; -pub use device::Device; \ No newline at end of file +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::ble::normalize_uuid; + +#[derive(Debug, Serialize, Clone, Deserialize)] +pub struct Device { + pub id: String, + pub address: String, + pub display_name: Option, + pub rssi: Option, + pub service_uuids: Vec, + pub characteristics: Vec, + pub manufacturer_data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +impl Device {} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DeviceType { + HrSensor, + CadSensor, + Trainer, +} + +pub fn get_device_types(device: &Device) -> Vec { + let hr_services = ["180d"]; + let hr_characteristics = ["2a37"]; + + let csc_services = ["1816"]; + let csc_characteristics = ["2a5b"]; + + // trainer / cycling power / fitness machine / indoor bike + let trainer_services = ["1818", "1826"]; + let trainer_characteristics = ["2a63", "2ad2", "2acc"]; // 2AD2 = Indoor Bike Data, 2ACC = Fitness Machine Characteristics, 2A63 = Cycling Power Measurement + + // любые 2ADx характеристики (indoor bike related) — учитывать как признак каденса доступного в тренажёре + let indoor_bike_chars_prefix = "2ad"; // covers 2ad2,2ad5,2ad6,2ad8,2ad9,2ada etc. + + // Нормализуем все UUIDs + let mut norms = Vec::with_capacity(device.service_uuids.len() + device.characteristics.len()); + for u in device.service_uuids.iter().chain(device.characteristics.iter()) { + // Для характеристик, которые приходят в виде "service::characteristic", извлечём часть после "::" + let part = if let Some(idx) = u.find("::") { + &u[idx + 2..] + } else { + u.as_str() + }; + norms.push(normalize_uuid(part)); + } + + let contains_any = |candidates: &[&str]| -> bool { + for cand in candidates { + let cand = cand.to_ascii_lowercase(); + if norms.iter().any(|n| n == &cand || n.ends_with(&cand)) { + return true; + } + } + false + }; + + let mut out = Vec::new(); + + // Heart rate + if contains_any(&hr_services) || contains_any(&hr_characteristics) { + out.push(DeviceType::HrSensor); + } + + // CSC / Cadence + if contains_any(&csc_services) || contains_any(&csc_characteristics) { + out.push(DeviceType::CadSensor); + } + + // Trainer/Cycling power/Indoor bike + let is_trainer = contains_any(&trainer_services) || contains_any(&trainer_characteristics); + if is_trainer { + out.push(DeviceType::Trainer); + } + + // Доп. логика: если найдены Indoor Bike Data (любые 2ADx characteristics), считаем, что устройство может отдавать cadence — + // добавляем CadSensor, даже если CSC (0x1816/0x2A5B) отсутствует. + let has_indoor_bike_char = norms.iter().any(|n| n.starts_with(indoor_bike_chars_prefix)); + if has_indoor_bike_char && !out.contains(&DeviceType::CadSensor) { + out.push(DeviceType::CadSensor); + } + + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_service_types_for_magene_speedcadence() { + let magene = Device { + id: "hci0/dev_F1_F2_7C_77_14_BE".into(), + address: "F1:F2:7C:77:14:BE".into(), + display_name: Some("10911-17".into()), + rssi: Some(-42), + service_uuids: vec!["00001816-0000-1000-8000-00805f9b34fb".into()], + characteristics: vec![], + manufacturer_data: Some("6b009f2a110300030072".into()), + metadata: None, + }; + + assert_eq!(get_device_types(&magene), [DeviceType::CadSensor]); + } +} diff --git a/src-tauri/src/device_repo/mod.rs b/src-tauri/src/device_repo/mod.rs index 66d6016..d2a027e 100644 --- a/src-tauri/src/device_repo/mod.rs +++ b/src-tauri/src/device_repo/mod.rs @@ -1,3 +1,3 @@ mod repo; -pub use repo::repo_from_devices; +pub use repo::{repo_from_devices, Repo}; diff --git a/src-tauri/src/device_repo/repo.rs b/src-tauri/src/device_repo/repo.rs index 1c6848f..096bd34 100644 --- a/src-tauri/src/device_repo/repo.rs +++ b/src-tauri/src/device_repo/repo.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::{device::{device::{get_device_types, DeviceType}, Device}}; +use crate::device::{Device, DeviceType, get_device_types}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Repo { @@ -8,7 +8,7 @@ pub struct Repo { #[serde(default)] cad_sensors: Vec, #[serde(default)] - trainers: Vec + trainers: Vec, } impl Repo { @@ -61,7 +61,7 @@ mod tests { use super::*; #[test] - fn test_service_types_for_magene_speedcadence() { + fn test_repo_classifies_magene_as_cadence_sensor() { let magene = Device { id: "hci0/dev_F1_F2_7C_77_14_BE".into(), address: "F1:F2:7C:77:14:BE".into(), @@ -73,13 +73,10 @@ mod tests { metadata: None, }; - let devices = vec![magene]; - let x = repo_from_devices(devices); + let repo = repo_from_devices(vec![magene]); - println!("{:?}", x); - - //assert_eq!(repo_from_devices(devices), Repo::new()); - - assert_eq!(2, 3); + assert_eq!(repo.cad_sensors.len(), 1); + assert!(repo.hr_sensors.is_empty()); + assert!(repo.trainers.is_empty()); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e0f4e2e..42cee58 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,24 +5,45 @@ mod state; use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager, async_runtime}; -use crate::{ble::scan_devices, device_repo::{repo_from_devices}}; +use crate::{ble::scan_devices, device_repo::{repo_from_devices, Repo}}; use crate::state::State; -use btleplug::api::{Manager as ApiManager}; +use btleplug::api::Manager as ApiManager; use btleplug::platform::Manager as BleManager; +/// Сколько секунд сканируем BLE на старте. +const SCAN_SECONDS: u64 = 5; + #[tauri::command] -async fn init(app: AppHandle) -> Result<(),()> { +async fn init(app: AppHandle) -> Result<(), String> { println!("init rideinn app"); - let splash = app.get_webview_window("splashscreen").unwrap(); - let main = app.get_webview_window("main").unwrap(); + + let repo = scan_and_classify(&app).await.map_err(|e| e.to_string())?; + present_repo(&app, repo).map_err(|e| e.to_string())?; + + Ok(()) +} + +/// Этап 1: скан BLE-устройств и раскладка по типам датчиков. +async fn scan_and_classify(app: &AppHandle) -> Result { let state = app.state::(); let adapter = state.adapter.clone(); - let repo = repo_from_devices((scan_devices(&adapter, 5).await).unwrap_or_default()); + let devices = scan_devices(&adapter, SCAN_SECONDS).await?; + Ok(repo_from_devices(devices)) +} - splash.close().unwrap(); - main.show().unwrap(); +/// Этап 2: закрываем сплэш, показываем главное окно, отдаём список датчиков на фронт. +fn present_repo(app: &AppHandle, repo: Repo) -> Result<(), String> { + let splash = app + .get_webview_window("splashscreen") + .ok_or("окно splashscreen не найдено")?; + let main = app + .get_webview_window("main") + .ok_or("окно main не найдено")?; - app.emit("devices-loaded", repo).unwrap(); + splash.close().map_err(|e| format!("не удалось закрыть splashscreen: {e}"))?; + main.show().map_err(|e| format!("не удалось показать main: {e}"))?; + app.emit("devices-loaded", repo) + .map_err(|e| format!("не удалось отправить событие devices-loaded: {e}"))?; Ok(()) } @@ -36,8 +57,8 @@ pub fn run() { let adapters = manager.adapters().await?; let adapter = adapters.into_iter().next() .ok_or_else(|| Box::::from("No BLE adapter found"))?; - - app.manage(State { adapter: Arc::new(adapter) } ); + + app.manage(State { adapter: Arc::new(adapter) }); Ok::<(), Box>(()) }) }) -- 2.36.5 From 29d709d8e2e1b1b4b883a5f313a7aa086491ea45 Mon Sep 17 00:00:00 2001 From: lambda Date: Sun, 25 Oct 2026 20:31:27 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/ble/scanner.rs | 6 +++--- src-tauri/src/device/mod.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/ble/scanner.rs b/src-tauri/src/ble/scanner.rs index ad8309f..1165fc6 100644 --- a/src-tauri/src/ble/scanner.rs +++ b/src-tauri/src/ble/scanner.rs @@ -32,7 +32,7 @@ pub async fn scan_devices(adapter: &Adapter, scan_seconds: u64) -> Result Result Result Vec { let was_connected = peripheral.is_connected().await.unwrap_or(false); diff --git a/src-tauri/src/device/mod.rs b/src-tauri/src/device/mod.rs index 631f103..9277d58 100644 --- a/src-tauri/src/device/mod.rs +++ b/src-tauri/src/device/mod.rs @@ -36,7 +36,7 @@ pub fn get_device_types(device: &Device) -> Vec { let trainer_services = ["1818", "1826"]; let trainer_characteristics = ["2a63", "2ad2", "2acc"]; // 2AD2 = Indoor Bike Data, 2ACC = Fitness Machine Characteristics, 2A63 = Cycling Power Measurement - // любые 2ADx характеристики (indoor bike related) — учитывать как признак каденса доступного в тренажёре + // любые 2ADx характеристики (indoor bike related) - учитывать как признак каденса доступного в тренажёре let indoor_bike_chars_prefix = "2ad"; // covers 2ad2,2ad5,2ad6,2ad8,2ad9,2ada etc. // Нормализуем все UUIDs @@ -79,7 +79,7 @@ pub fn get_device_types(device: &Device) -> Vec { out.push(DeviceType::Trainer); } - // Доп. логика: если найдены Indoor Bike Data (любые 2ADx characteristics), считаем, что устройство может отдавать cadence — + // Доп. логика: если найдены Indoor Bike Data (любые 2ADx characteristics), считаем, что устройство может отдавать cadence - // добавляем CadSensor, даже если CSC (0x1816/0x2A5B) отсутствует. let has_indoor_bike_char = norms.iter().any(|n| n.starts_with(indoor_bike_chars_prefix)); if has_indoor_bike_char && !out.contains(&DeviceType::CadSensor) { -- 2.36.5