Merge pull request 'connection-refactoring-ai' (#2) from connection-refactoring-ai into main
Reviewed-on: #2main
commit
b187811649
@ -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<String>,
|
|
||||||
pub rssi: Option<i16>,
|
|
||||||
pub service_uuids: Vec<String>,
|
|
||||||
pub characteristics: Vec<String>,
|
|
||||||
pub manufacturer_data: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub metadata: Option<Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Device {}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub enum DeviceType {
|
|
||||||
HrSensor,
|
|
||||||
CadSensor,
|
|
||||||
Trainer
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_device_types(device: &Device) -> Vec<DeviceType> {
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +1,111 @@
|
|||||||
pub mod device;
|
use serde::{Deserialize, Serialize};
|
||||||
pub use device::Device;
|
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<String>,
|
||||||
|
pub rssi: Option<i16>,
|
||||||
|
pub service_uuids: Vec<String>,
|
||||||
|
pub characteristics: Vec<String>,
|
||||||
|
pub manufacturer_data: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub metadata: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
pub enum DeviceType {
|
||||||
|
HrSensor,
|
||||||
|
CadSensor,
|
||||||
|
Trainer,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_types(device: &Device) -> Vec<DeviceType> {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
mod repo;
|
mod repo;
|
||||||
|
|
||||||
pub use repo::repo_from_devices;
|
pub use repo::{repo_from_devices, Repo};
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 2.5 KiB |
@ -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;
|
||||||
|
}
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="stylesheet" href="styles.css" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Tauri App</title>
|
|
||||||
<script type="module" src="/connect.js" defer></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main class="container">
|
|
||||||
<h1>Connect</h1>
|
|
||||||
<div id="app">
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -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);
|
|
||||||
});
|
|
||||||
@ -1 +1,7 @@
|
|||||||
|
const { once } = window.__TAURI__.event;
|
||||||
|
|
||||||
|
once("devices-loaded", (devices) => {
|
||||||
|
console.log("Devices are loaded", devices);
|
||||||
|
});
|
||||||
|
|
||||||
console.log("hello from main.js");
|
console.log("hello from main.js");
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue