feat: macOS compatibility
This commit is contained in:
12
package.json
12
package.json
@@ -10,23 +10,23 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.3.4",
|
||||
"vue": "^3.4.31",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue3-snackbar": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1",
|
||||
"@tauri-apps/cli": "^1.6.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.39",
|
||||
"sass": "^1.77.8",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.5.3",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"vite": "^5.3.1",
|
||||
"vue-tsc": "^2.0.22"
|
||||
"vite": "^5.3.3",
|
||||
"vue-tsc": "^2.0.26"
|
||||
}
|
||||
}
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^1
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
'@vueuse/core':
|
||||
specifier: ^10.11.0
|
||||
@@ -18,7 +18,7 @@ importers:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(typescript@5.5.3)(vue@3.4.31(typescript@5.5.3))
|
||||
vue:
|
||||
specifier: ^3.3.4
|
||||
specifier: ^3.4.31
|
||||
version: 3.4.31(typescript@5.5.3)
|
||||
vue-router:
|
||||
specifier: ^4.4.0
|
||||
@@ -28,7 +28,7 @@ importers:
|
||||
version: 2.3.2(vue@3.4.31(typescript@5.5.3))
|
||||
devDependencies:
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^1
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.0.5
|
||||
@@ -46,16 +46,16 @@ importers:
|
||||
specifier: ^3.4.4
|
||||
version: 3.4.4
|
||||
typescript:
|
||||
specifier: ^5.2.2
|
||||
specifier: ^5.5.3
|
||||
version: 5.5.3
|
||||
unplugin-vue-components:
|
||||
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))
|
||||
vite:
|
||||
specifier: ^5.3.1
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3(sass@1.77.8)
|
||||
vue-tsc:
|
||||
specifier: ^2.0.22
|
||||
specifier: ^2.0.26
|
||||
version: 2.0.26(typescript@5.5.3)
|
||||
|
||||
packages:
|
||||
@@ -550,8 +550,8 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
caniuse-lite@1.0.30001642:
|
||||
resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==}
|
||||
caniuse-lite@1.0.30001716:
|
||||
resolution: {integrity: sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
@@ -1477,7 +1477,7 @@ snapshots:
|
||||
autoprefixer@10.4.19(postcss@8.4.39):
|
||||
dependencies:
|
||||
browserslist: 4.23.2
|
||||
caniuse-lite: 1.0.30001642
|
||||
caniuse-lite: 1.0.30001716
|
||||
fraction.js: 4.3.7
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.1
|
||||
@@ -1498,14 +1498,14 @@ snapshots:
|
||||
|
||||
browserslist@4.23.2:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001642
|
||||
caniuse-lite: 1.0.30001716
|
||||
electron-to-chromium: 1.4.827
|
||||
node-releases: 2.0.14
|
||||
update-browserslist-db: 1.1.0(browserslist@4.23.2)
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001642: {}
|
||||
caniuse-lite@1.0.30001716: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
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"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
btleplug = { version = "0.11.5", features = ["serde"] }
|
||||
btleplug = { version = "0.11.8", features = ["serde"] }
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
futures = "0.3.30"
|
||||
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::CentralEvent::{DeviceDisconnected, DeviceDiscovered, DeviceUpdated};
|
||||
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 std::error::Error;
|
||||
use std::sync::Arc;
|
||||
@@ -14,6 +14,7 @@ use tokio::sync::Mutex;
|
||||
|
||||
#[derive(serde::Serialize, Clone)]
|
||||
struct BleDevice {
|
||||
peripheral_id: String,
|
||||
name: String,
|
||||
address: BDAddr,
|
||||
rssi: i16,
|
||||
@@ -67,14 +68,17 @@ impl BleConnection {
|
||||
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();
|
||||
let central = self.central.lock().await;
|
||||
let peripheral = central
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.peripheral(&PeripheralId::from(address))
|
||||
.await?;
|
||||
.peripherals()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|p| p.id().to_string() == peripheral_id)
|
||||
.ok_or_else(|| "5010")?;
|
||||
peripheral.connect().await?;
|
||||
peripheral.discover_services().await?;
|
||||
// 如果 peripheral.services() 不包含 0x180D 服务,则返回错误
|
||||
@@ -83,13 +87,18 @@ impl BleConnection {
|
||||
.iter()
|
||||
.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;
|
||||
|
||||
let peripheral = self.peripheral.lock().await;
|
||||
let device = BleDevice {
|
||||
peripheral_id: peripheral
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.id()
|
||||
.to_string(),
|
||||
name: peripheral
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -106,7 +115,7 @@ impl BleConnection {
|
||||
.await?
|
||||
.unwrap()
|
||||
.rssi
|
||||
.unwrap(),
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let service = peripheral
|
||||
@@ -136,7 +145,7 @@ impl BleConnection {
|
||||
while let Some(notification) = notification_stream.next().await {
|
||||
if notification.uuid == uuid_from_u16(0x2A37) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -164,27 +173,24 @@ impl BleConnection {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = event_stream.next().await {
|
||||
match event {
|
||||
DeviceDiscovered(peripheral) | DeviceUpdated(peripheral) => {
|
||||
DeviceDiscovered(peripheral_id) | DeviceUpdated(peripheral_id) => {
|
||||
let p = central_clone
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.peripheral(&peripheral)
|
||||
.peripheral(&peripheral_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let device = BleDevice {
|
||||
name: p
|
||||
.properties()
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.local_name
|
||||
.unwrap_or("Unknown".to_string()),
|
||||
address: p.address(),
|
||||
rssi: p.properties().await.unwrap().unwrap().rssi.unwrap(),
|
||||
};
|
||||
app_handle
|
||||
.emit_all("device-discovered", Some(device))
|
||||
.unwrap();
|
||||
if let Ok(Some(props)) = p.properties().await {
|
||||
let name = props.local_name.unwrap_or("Unknown".to_string());
|
||||
let rssi = props.rssi.unwrap_or(0);
|
||||
let device = BleDevice {
|
||||
peripheral_id: peripheral_id.to_string(),
|
||||
name,
|
||||
address: props.address,
|
||||
rssi,
|
||||
};
|
||||
let _ = app_handle.emit_all("device-discovered", Some(device));
|
||||
}
|
||||
}
|
||||
DeviceDisconnected(peripheral) => {
|
||||
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> {
|
||||
let peripheral = connection.peripheral.lock().await;
|
||||
let device = BleDevice {
|
||||
peripheral_id: peripheral
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.id()
|
||||
.to_string(),
|
||||
name: peripheral
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -274,18 +285,18 @@ async fn get_connected_device(connection: State<'_, BleConnection>) -> Result<Bl
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.rssi
|
||||
.unwrap(),
|
||||
.unwrap_or(0),
|
||||
};
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn connect(
|
||||
address: BDAddr,
|
||||
peripheral_id: String,
|
||||
connection: State<'_, BleConnection>,
|
||||
app_handle: AppHandle,
|
||||
) -> 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())
|
||||
} else {
|
||||
Ok(true)
|
||||
|
||||
@@ -32,7 +32,7 @@ listen("device-disconnected", (_) => {
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</DrawerContainer>
|
||||
<Vue3Snackbar bottom right shadow :duration="5000"></Vue3Snackbar>
|
||||
<Vue3Snackbar bottom right shadow dense :border="'left'" :duration="5000"></Vue3Snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { computed, PropType } from 'vue';
|
||||
import { Device, useBrcatStore } from '../stores';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object as PropType<Device>,
|
||||
required: true
|
||||
@@ -10,10 +10,14 @@ defineProps({
|
||||
});
|
||||
const emit = defineEmits(['connect']);
|
||||
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>
|
||||
|
||||
<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="flex flex-col gap-1">
|
||||
<span class="flex items-center gap-1 text-sm leading-none">
|
||||
@@ -21,12 +25,12 @@ const store = useBrcatStore();
|
||||
</span>
|
||||
<span class="flex items-center gap-1 text-2xs text-neutral-400">
|
||||
<SignalIndicator :rssi="device.rssi" />
|
||||
{{ device.address }}
|
||||
<span class="font-mono">{{ displayAddress }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</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 { computed, onMounted, ref } from 'vue';
|
||||
import { useSnackbar } from 'vue3-snackbar';
|
||||
import { useErrno } from '../composables/useErrno';
|
||||
|
||||
const store = useBrcatStore();
|
||||
const snackbar = useSnackbar();
|
||||
@@ -10,14 +11,14 @@ const is_connecting = ref(false);
|
||||
|
||||
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;
|
||||
store.stopScan();
|
||||
invoke('connect', { address })
|
||||
.catch(err => {
|
||||
invoke('connect', { peripheralId: peripheral_id })
|
||||
.catch(errno => {
|
||||
snackbar.add({
|
||||
type: 'error',
|
||||
text: `连接设备失败: ${err}`
|
||||
text: useErrno(errno),
|
||||
});
|
||||
store.startScan();
|
||||
}).finally(() => {
|
||||
@@ -51,29 +52,24 @@ onMounted(() => {
|
||||
</div>
|
||||
<button class="btn outline" @click="invoke('disconnect')">断开连接</button>
|
||||
</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" />
|
||||
<!-- <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
|
||||
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" />
|
||||
<span class="text-sm font-semibold text-neutral-500">心率</span>
|
||||
<span class="text-lg text-primary-400 pl-1">心率</span>
|
||||
</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" />
|
||||
<span class="text-sm font-semibold text-neutral-500">心率</span>
|
||||
<span class="text-lg text-primary-400 pl-1">心率</span>
|
||||
</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 v-else-if="store.is_scanning && scanning_devices.length === 0"
|
||||
@@ -89,7 +85,7 @@ onMounted(() => {
|
||||
<div class="flex flex-col gap-2 relative">
|
||||
<TransitionGroup name="scan-device">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { useThrottleFn, useDebounceFn } from "@vueuse/core";
|
||||
|
||||
export interface Device {
|
||||
peripheral_id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
rssi: number;
|
||||
@@ -58,14 +59,16 @@ export const useBrcatStore = defineStore("brcat", () => {
|
||||
}, 2000);
|
||||
|
||||
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) =>
|
||||
d.address === device.address ? device : d
|
||||
d.peripheral_id === device.peripheral_id ? device : d
|
||||
);
|
||||
} else {
|
||||
scanning_devices.value.push(device);
|
||||
}
|
||||
|
||||
console.log(device);
|
||||
|
||||
throttledSort();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user