feat: macOS compatibility
This commit is contained in:
12
package.json
12
package.json
@@ -10,23 +10,23 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.4.31",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
"vue3-snackbar": "^2.3.2"
|
"vue3-snackbar": "^2.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1",
|
"@tauri-apps/cli": "^1.6.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.5.3",
|
||||||
"unplugin-vue-components": "^0.27.2",
|
"unplugin-vue-components": "^0.27.2",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.3",
|
||||||
"vue-tsc": "^2.0.22"
|
"vue-tsc": "^2.0.26"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^1
|
specifier: ^1.6.0
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
'@vueuse/core':
|
'@vueuse/core':
|
||||||
specifier: ^10.11.0
|
specifier: ^10.11.0
|
||||||
@@ -18,7 +18,7 @@ importers:
|
|||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
|
version: 2.1.7(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.3.4
|
specifier: ^3.4.31
|
||||||
version: 3.4.31(typescript@5.5.3)
|
version: 3.4.31(typescript@5.5.3)
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
@@ -28,7 +28,7 @@ importers:
|
|||||||
version: 2.3.2(vue@3.4.31(typescript@5.5.3))
|
version: 2.3.2(vue@3.4.31(typescript@5.5.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^1
|
specifier: ^1.6.0
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^5.0.5
|
specifier: ^5.0.5
|
||||||
@@ -46,16 +46,16 @@ importers:
|
|||||||
specifier: ^3.4.4
|
specifier: ^3.4.4
|
||||||
version: 3.4.4
|
version: 3.4.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.2.2
|
specifier: ^5.5.3
|
||||||
version: 5.5.3
|
version: 5.5.3
|
||||||
unplugin-vue-components:
|
unplugin-vue-components:
|
||||||
specifier: ^0.27.2
|
specifier: ^0.27.2
|
||||||
version: 0.27.2(@babel/parser@7.24.8)(rollup@4.18.1)(vue@3.4.31(typescript@5.5.3))
|
version: 0.27.2(@babel/parser@7.24.8)(rollup@4.18.1)(vue@3.4.31(typescript@5.5.3))
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.3.1
|
specifier: ^5.3.3
|
||||||
version: 5.3.3(sass@1.77.8)
|
version: 5.3.3(sass@1.77.8)
|
||||||
vue-tsc:
|
vue-tsc:
|
||||||
specifier: ^2.0.22
|
specifier: ^2.0.26
|
||||||
version: 2.0.26(typescript@5.5.3)
|
version: 2.0.26(typescript@5.5.3)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
@@ -550,8 +550,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001642:
|
caniuse-lite@1.0.30001716:
|
||||||
resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==}
|
resolution: {integrity: sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
@@ -1477,7 +1477,7 @@ snapshots:
|
|||||||
autoprefixer@10.4.19(postcss@8.4.39):
|
autoprefixer@10.4.19(postcss@8.4.39):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.23.2
|
browserslist: 4.23.2
|
||||||
caniuse-lite: 1.0.30001642
|
caniuse-lite: 1.0.30001716
|
||||||
fraction.js: 4.3.7
|
fraction.js: 4.3.7
|
||||||
normalize-range: 0.1.2
|
normalize-range: 0.1.2
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
@@ -1498,14 +1498,14 @@ snapshots:
|
|||||||
|
|
||||||
browserslist@4.23.2:
|
browserslist@4.23.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001642
|
caniuse-lite: 1.0.30001716
|
||||||
electron-to-chromium: 1.4.827
|
electron-to-chromium: 1.4.827
|
||||||
node-releases: 2.0.14
|
node-releases: 2.0.14
|
||||||
update-browserslist-db: 1.1.0(browserslist@4.23.2)
|
update-browserslist-db: 1.1.0(browserslist@4.23.2)
|
||||||
|
|
||||||
camelcase-css@2.0.1: {}
|
camelcase-css@2.0.1: {}
|
||||||
|
|
||||||
caniuse-lite@1.0.30001642: {}
|
caniuse-lite@1.0.30001716: {}
|
||||||
|
|
||||||
chokidar@3.6.0:
|
chokidar@3.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
540
src-tauri/Cargo.lock
generated
540
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ tauri-build = { version = "1", features = [] }
|
|||||||
tauri = { version = "1", features = [ "window-create", "path-all", "dialog-all", "window-show", "window-hide", "window-maximize", "window-unmaximize", "window-unminimize", "window-start-dragging", "window-minimize", "window-close", "shell-open"] }
|
tauri = { version = "1", features = [ "window-create", "path-all", "dialog-all", "window-show", "window-hide", "window-maximize", "window-unmaximize", "window-unminimize", "window-start-dragging", "window-minimize", "window-close", "shell-open"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
btleplug = { version = "0.11.5", features = ["serde"] }
|
btleplug = { version = "0.11.8", features = ["serde"] }
|
||||||
tokio = { version = "1.38.0", features = ["full"] }
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
uuid = "1.10.0"
|
uuid = "1.10.0"
|
||||||
|
|||||||
8
src-tauri/Info.plist
Normal file
8
src-tauri/Info.plist
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>Request Bluetooth for recovery BLE devices.</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
use btleplug::api::bleuuid::uuid_from_u16;
|
use btleplug::api::bleuuid::uuid_from_u16;
|
||||||
use btleplug::api::CentralEvent::{DeviceDisconnected, DeviceDiscovered, DeviceUpdated};
|
use btleplug::api::CentralEvent::{DeviceDisconnected, DeviceDiscovered, DeviceUpdated};
|
||||||
use btleplug::api::{BDAddr, Central, Manager as _, Peripheral as _, ScanFilter};
|
use btleplug::api::{BDAddr, Central, Manager as _, Peripheral as _, ScanFilter};
|
||||||
use btleplug::platform::{Adapter, Manager as BtleManager, Peripheral, PeripheralId};
|
use btleplug::platform::{Adapter, Manager as BtleManager, Peripheral};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -14,6 +14,7 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
#[derive(serde::Serialize, Clone)]
|
#[derive(serde::Serialize, Clone)]
|
||||||
struct BleDevice {
|
struct BleDevice {
|
||||||
|
peripheral_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
address: BDAddr,
|
address: BDAddr,
|
||||||
rssi: i16,
|
rssi: i16,
|
||||||
@@ -67,14 +68,17 @@ impl BleConnection {
|
|||||||
peripheral.is_some()
|
peripheral.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(&self, address: BDAddr, app: &AppHandle) -> Result<(), Box<dyn Error>> {
|
pub async fn connect(&self, peripheral_id: String, app: &AppHandle) -> Result<(), Box<dyn Error>> {
|
||||||
self.stop_scan().await.unwrap();
|
self.stop_scan().await.unwrap();
|
||||||
let central = self.central.lock().await;
|
let central = self.central.lock().await;
|
||||||
let peripheral = central
|
let peripheral = central
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.peripheral(&PeripheralId::from(address))
|
.peripherals()
|
||||||
.await?;
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.id().to_string() == peripheral_id)
|
||||||
|
.ok_or_else(|| "5010")?;
|
||||||
peripheral.connect().await?;
|
peripheral.connect().await?;
|
||||||
peripheral.discover_services().await?;
|
peripheral.discover_services().await?;
|
||||||
// 如果 peripheral.services() 不包含 0x180D 服务,则返回错误
|
// 如果 peripheral.services() 不包含 0x180D 服务,则返回错误
|
||||||
@@ -83,13 +87,18 @@ impl BleConnection {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|s| s.uuid == uuid_from_u16(0x180D))
|
.any(|s| s.uuid == uuid_from_u16(0x180D))
|
||||||
{
|
{
|
||||||
return Err("Peripheral does not have the required service".into());
|
return Err("5011".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_peripheral(Some(peripheral)).await;
|
self.set_peripheral(Some(peripheral)).await;
|
||||||
|
|
||||||
let peripheral = self.peripheral.lock().await;
|
let peripheral = self.peripheral.lock().await;
|
||||||
let device = BleDevice {
|
let device = BleDevice {
|
||||||
|
peripheral_id: peripheral
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.id()
|
||||||
|
.to_string(),
|
||||||
name: peripheral
|
name: peripheral
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -106,7 +115,7 @@ impl BleConnection {
|
|||||||
.await?
|
.await?
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rssi
|
.rssi
|
||||||
.unwrap(),
|
.unwrap_or(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
let service = peripheral
|
let service = peripheral
|
||||||
@@ -136,7 +145,7 @@ impl BleConnection {
|
|||||||
while let Some(notification) = notification_stream.next().await {
|
while let Some(notification) = notification_stream.next().await {
|
||||||
if notification.uuid == uuid_from_u16(0x2A37) {
|
if notification.uuid == uuid_from_u16(0x2A37) {
|
||||||
let value = notification.value;
|
let value = notification.value;
|
||||||
let heart_rate = value[1] as u16;
|
let heart_rate = value[0] as u16;
|
||||||
app_clone.emit_all("heart-rate", heart_rate).unwrap();
|
app_clone.emit_all("heart-rate", heart_rate).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,27 +173,24 @@ impl BleConnection {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(event) = event_stream.next().await {
|
while let Some(event) = event_stream.next().await {
|
||||||
match event {
|
match event {
|
||||||
DeviceDiscovered(peripheral) | DeviceUpdated(peripheral) => {
|
DeviceDiscovered(peripheral_id) | DeviceUpdated(peripheral_id) => {
|
||||||
let p = central_clone
|
let p = central_clone
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.peripheral(&peripheral)
|
.peripheral(&peripheral_id)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let device = BleDevice {
|
if let Ok(Some(props)) = p.properties().await {
|
||||||
name: p
|
let name = props.local_name.unwrap_or("Unknown".to_string());
|
||||||
.properties()
|
let rssi = props.rssi.unwrap_or(0);
|
||||||
.await
|
let device = BleDevice {
|
||||||
.unwrap()
|
peripheral_id: peripheral_id.to_string(),
|
||||||
.unwrap()
|
name,
|
||||||
.local_name
|
address: props.address,
|
||||||
.unwrap_or("Unknown".to_string()),
|
rssi,
|
||||||
address: p.address(),
|
};
|
||||||
rssi: p.properties().await.unwrap().unwrap().rssi.unwrap(),
|
let _ = app_handle.emit_all("device-discovered", Some(device));
|
||||||
};
|
}
|
||||||
app_handle
|
|
||||||
.emit_all("device-discovered", Some(device))
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
DeviceDisconnected(peripheral) => {
|
DeviceDisconnected(peripheral) => {
|
||||||
let mut p = self_clone.peripheral.lock().await;
|
let mut p = self_clone.peripheral.lock().await;
|
||||||
@@ -256,6 +262,11 @@ async fn is_connected(connection: State<'_, BleConnection>) -> Result<bool, Stri
|
|||||||
async fn get_connected_device(connection: State<'_, BleConnection>) -> Result<BleDevice, String> {
|
async fn get_connected_device(connection: State<'_, BleConnection>) -> Result<BleDevice, String> {
|
||||||
let peripheral = connection.peripheral.lock().await;
|
let peripheral = connection.peripheral.lock().await;
|
||||||
let device = BleDevice {
|
let device = BleDevice {
|
||||||
|
peripheral_id: peripheral
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.id()
|
||||||
|
.to_string(),
|
||||||
name: peripheral
|
name: peripheral
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -274,18 +285,18 @@ async fn get_connected_device(connection: State<'_, BleConnection>) -> Result<Bl
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.rssi
|
.rssi
|
||||||
.unwrap(),
|
.unwrap_or(0),
|
||||||
};
|
};
|
||||||
Ok(device)
|
Ok(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn connect(
|
async fn connect(
|
||||||
address: BDAddr,
|
peripheral_id: String,
|
||||||
connection: State<'_, BleConnection>,
|
connection: State<'_, BleConnection>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
if let Err(e) = connection.connect(address, &app_handle).await {
|
if let Err(e) = connection.connect(peripheral_id, &app_handle).await {
|
||||||
Err(e.to_string())
|
Err(e.to_string())
|
||||||
} else {
|
} else {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ listen("device-disconnected", (_) => {
|
|||||||
</Transition>
|
</Transition>
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</DrawerContainer>
|
</DrawerContainer>
|
||||||
<Vue3Snackbar bottom right shadow :duration="5000"></Vue3Snackbar>
|
<Vue3Snackbar bottom right shadow dense :border="'left'" :duration="5000"></Vue3Snackbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PropType } from 'vue';
|
import { computed, PropType } from 'vue';
|
||||||
import { Device, useBrcatStore } from '../stores';
|
import { Device, useBrcatStore } from '../stores';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
device: {
|
device: {
|
||||||
type: Object as PropType<Device>,
|
type: Object as PropType<Device>,
|
||||||
required: true
|
required: true
|
||||||
@@ -10,10 +10,14 @@ defineProps({
|
|||||||
});
|
});
|
||||||
const emit = defineEmits(['connect']);
|
const emit = defineEmits(['connect']);
|
||||||
const store = useBrcatStore();
|
const store = useBrcatStore();
|
||||||
|
|
||||||
|
const displayAddress = computed(() => {
|
||||||
|
return props.device.address === '00:00:00:00:00:00' ? (props.device.peripheral_id || 'N/A') : props.device.address;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="item w-full px-3 py-3 flex justify-between items-center bg-white rounded" :key="device.address">
|
<div class="item w-full px-3 py-3 flex justify-between items-center bg-white rounded" :key="device.peripheral_id">
|
||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span class="flex items-center gap-1 text-sm leading-none">
|
<span class="flex items-center gap-1 text-sm leading-none">
|
||||||
@@ -21,12 +25,12 @@ const store = useBrcatStore();
|
|||||||
</span>
|
</span>
|
||||||
<span class="flex items-center gap-1 text-2xs text-neutral-400">
|
<span class="flex items-center gap-1 text-2xs text-neutral-400">
|
||||||
<SignalIndicator :rssi="device.rssi" />
|
<SignalIndicator :rssi="device.rssi" />
|
||||||
{{ device.address }}
|
<span class="font-mono">{{ displayAddress }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex items-center">
|
<div class="h-full flex items-center">
|
||||||
<button class="btn outline" :disabled="store.is_connected" @click="emit('connect', device.address)">连接</button>
|
<button class="btn outline" :disabled="store.is_connected" @click="emit('connect', device.peripheral_id)">连接</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
14
src/composables/useErrno.ts
Normal file
14
src/composables/useErrno.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const ERRNO: Record<number, string> = {
|
||||||
|
5010: '找不到指定 ID 的设备',
|
||||||
|
5011: '设备没有公开的心率服务'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useErrno = (errno: string | null | undefined) => {
|
||||||
|
if (errno) {
|
||||||
|
const err = ERRNO[Number(errno)]
|
||||||
|
if (err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '未知错误'
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { invoke } from '@tauri-apps/api/tauri';
|
|||||||
import { useBrcatStore } from '../stores';
|
import { useBrcatStore } from '../stores';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useSnackbar } from 'vue3-snackbar';
|
import { useSnackbar } from 'vue3-snackbar';
|
||||||
|
import { useErrno } from '../composables/useErrno';
|
||||||
|
|
||||||
const store = useBrcatStore();
|
const store = useBrcatStore();
|
||||||
const snackbar = useSnackbar();
|
const snackbar = useSnackbar();
|
||||||
@@ -10,14 +11,14 @@ const is_connecting = ref(false);
|
|||||||
|
|
||||||
const scanning_devices = computed(() => store.scanning_devices.filter(d => d.name !== 'Unknown'));
|
const scanning_devices = computed(() => store.scanning_devices.filter(d => d.name !== 'Unknown'));
|
||||||
|
|
||||||
async function connect(address: String) {
|
async function connect(peripheral_id: String) {
|
||||||
is_connecting.value = true;
|
is_connecting.value = true;
|
||||||
store.stopScan();
|
store.stopScan();
|
||||||
invoke('connect', { address })
|
invoke('connect', { peripheralId: peripheral_id })
|
||||||
.catch(err => {
|
.catch(errno => {
|
||||||
snackbar.add({
|
snackbar.add({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: `连接设备失败: ${err}`
|
text: useErrno(errno),
|
||||||
});
|
});
|
||||||
store.startScan();
|
store.startScan();
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -51,29 +52,24 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<button class="btn outline" @click="invoke('disconnect')">断开连接</button>
|
<button class="btn outline" @click="invoke('disconnect')">断开连接</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col justify-center items-center px-4 py-2">
|
<div class="flex-1 flex flex-col justify-center items-center px-4 py-2 gap-2">
|
||||||
<img src="/favicon_256.ico" class="w-40 aspect-square opacity-30" />
|
<img src="/favicon_256.ico" class="w-40 aspect-square opacity-30" />
|
||||||
<!-- <div class="w-full grid grid-cols-3 gap-4">
|
<!-- TODO: Features entry (grid) -->
|
||||||
|
<div class="w-7/12 grid-cols-2 gap-4 hidden">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="px-4 py-3 rounded shadow-sm hover:shadow-md cursor-pointer transition bg-gradient-to-br from-neutral-50 to-primary-100">
|
class="p-3 pr-6 flex items-center justify-between gap-2 rounded-lg shadow-sm hover:shadow-md cursor-pointer transition bg-gradient-to-br from-neutral-50 to-primary-100">
|
||||||
<TablerHeartbeat class="text-primary text-5xl opacity-80 bg-primary-100 p-2 rounded-xl" />
|
<TablerHeartbeat class="text-primary text-5xl opacity-80 bg-primary-100 p-2 rounded-xl" />
|
||||||
<span class="text-sm font-semibold text-neutral-500">心率</span>
|
<span class="text-lg text-primary-400 pl-1">心率</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="px-4 py-3 rounded shadow-sm hover:shadow-md cursor-pointer transition bg-gradient-to-br from-neutral-50 to-primary-100">
|
class="p-3 pr-6 flex items-center justify-between gap-2 rounded-lg shadow-sm hover:shadow-md cursor-pointer transition bg-gradient-to-br from-neutral-50 to-primary-100">
|
||||||
<TablerHeartbeat class="text-primary text-5xl opacity-80 bg-primary-100 p-2 rounded-xl" />
|
<TablerHeartbeat class="text-primary text-5xl opacity-80 bg-primary-100 p-2 rounded-xl" />
|
||||||
<span class="text-sm font-semibold text-neutral-500">心率</span>
|
<span class="text-lg text-primary-400 pl-1">心率</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
</div>
|
||||||
class="px-4 py-3 rounded shadow-sm hover:shadow-md cursor-pointer transition bg-gradient-to-br from-neutral-50 to-primary-100">
|
|
||||||
<TablerHeartbeat class="text-primary text-5xl opacity-80 bg-primary-100 p-2 rounded-xl" />
|
|
||||||
<span class="text-sm font-semibold text-neutral-500">心率</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="store.is_scanning && scanning_devices.length === 0"
|
<div v-else-if="store.is_scanning && scanning_devices.length === 0"
|
||||||
@@ -89,7 +85,7 @@ onMounted(() => {
|
|||||||
<div class="flex flex-col gap-2 relative">
|
<div class="flex flex-col gap-2 relative">
|
||||||
<TransitionGroup name="scan-device">
|
<TransitionGroup name="scan-device">
|
||||||
<ScanningDevice v-for="(device, _) in scanning_devices.filter(d => d.name !== 'Unknown')"
|
<ScanningDevice v-for="(device, _) in scanning_devices.filter(d => d.name !== 'Unknown')"
|
||||||
:key="device.address" :device="device" @connect="connect" />
|
:key="device.peripheral_id" :device="device" @connect="connect" />
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
import { useThrottleFn, useDebounceFn } from "@vueuse/core";
|
import { useThrottleFn, useDebounceFn } from "@vueuse/core";
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
|
peripheral_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
address: string;
|
address: string;
|
||||||
rssi: number;
|
rssi: number;
|
||||||
@@ -58,14 +59,16 @@ export const useBrcatStore = defineStore("brcat", () => {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
function pushDevice(device: Device) {
|
function pushDevice(device: Device) {
|
||||||
if (scanning_devices.value.some((d) => d.address === device.address)) {
|
if (scanning_devices.value.some((d) => d.peripheral_id === device.peripheral_id)) {
|
||||||
scanning_devices.value = scanning_devices.value.map((d) =>
|
scanning_devices.value = scanning_devices.value.map((d) =>
|
||||||
d.address === device.address ? device : d
|
d.peripheral_id === device.peripheral_id ? device : d
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
scanning_devices.value.push(device);
|
scanning_devices.value.push(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(device);
|
||||||
|
|
||||||
throttledSort();
|
throttledSort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user