vite-uni-dev-tool 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/const.cjs +1 -1
- package/dist/const.d.ts +1 -0
- package/dist/const.d.ts.map +1 -1
- package/dist/const.js +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +2 -2
- package/dist/i18n/locales/en.cjs +1 -1
- package/dist/i18n/locales/en.d.ts +5 -0
- package/dist/i18n/locales/en.d.ts.map +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.cjs +1 -1
- package/dist/i18n/locales/zh-Hans.d.ts +5 -0
- package/dist/i18n/locales/zh-Hans.d.ts.map +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/modules/devIntercept/index.cjs +9 -9
- package/dist/modules/devIntercept/index.d.ts +1 -1
- package/dist/modules/devIntercept/index.d.ts.map +1 -1
- package/dist/modules/devIntercept/index.js +9 -9
- package/dist/modules/devStore/index.cjs +1 -1
- package/dist/modules/devStore/index.d.ts.map +1 -1
- package/dist/modules/devStore/index.js +1 -1
- package/dist/plugins/uniDevTool/transform/transformMain.cjs +3 -3
- package/dist/plugins/uniDevTool/transform/transformMain.d.ts +2 -1
- package/dist/plugins/uniDevTool/transform/transformMain.d.ts.map +1 -1
- package/dist/plugins/uniDevTool/transform/transformMain.js +3 -3
- package/dist/plugins/uniDevTool/transform/transformVue.cjs +31 -25
- package/dist/plugins/uniDevTool/transform/transformVue.d.ts +2 -1
- package/dist/plugins/uniDevTool/transform/transformVue.d.ts.map +1 -1
- package/dist/plugins/uniDevTool/transform/transformVue.js +30 -24
- package/dist/plugins/uniDevTool/uniDevTool.cjs +3 -3
- package/dist/plugins/uniDevTool/uniDevTool.d.ts +3 -1
- package/dist/plugins/uniDevTool/uniDevTool.d.ts.map +1 -1
- package/dist/plugins/uniDevTool/uniDevTool.js +3 -3
- package/dist/type.d.ts +3 -0
- package/dist/type.d.ts.map +1 -1
- package/dist/v3/DevTool/components/BluetoothList/BluetoothItem.vue +199 -199
- package/dist/v3/DevTool/components/BluetoothList/BluetoothTool.vue +730 -730
- package/dist/v3/DevTool/components/BluetoothList/index.vue +167 -167
- package/dist/v3/DevTool/components/CaptureScreen/index.vue +109 -109
- package/dist/v3/DevTool/components/ConsoleList/ConsoleItem.vue +230 -225
- package/dist/v3/DevTool/components/ConsoleList/RunJSInput.vue +247 -247
- package/dist/v3/DevTool/components/ConsoleList/index.vue +171 -171
- package/dist/v3/DevTool/components/ConsoleList/staticTips.ts +1145 -1145
- package/dist/v3/DevTool/components/DevToolTitle/index.vue +24 -24
- package/dist/v3/DevTool/components/DevToolWindow/DevToolOverlay.vue +197 -197
- package/dist/v3/DevTool/components/DevToolWindow/hooks/dataUtils.ts +48 -48
- package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolData.ts +387 -387
- package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolHandlers.ts +629 -629
- package/dist/v3/DevTool/components/DevToolWindow/hooks/useDevToolOverlay.ts +201 -197
- package/dist/v3/DevTool/components/ElEvent/ElEventItem.vue +105 -105
- package/dist/v3/DevTool/components/ElEvent/index.vue +106 -106
- package/dist/v3/DevTool/components/Instance/components/InstanceTreeNode.vue +265 -265
- package/dist/v3/DevTool/components/Instance/flatten.ts +226 -226
- package/dist/v3/DevTool/components/Instance/index.vue +94 -94
- package/dist/v3/DevTool/components/Instance/registry.ts +49 -49
- package/dist/v3/DevTool/components/Instance/transformTree.ts +375 -375
- package/dist/v3/DevTool/components/Instance/transformTreeCtx.ts +268 -268
- package/dist/v3/DevTool/components/Instance/typing.d.ts +43 -43
- package/dist/v3/DevTool/components/InstanceDetail/index.vue +485 -485
- package/dist/v3/DevTool/components/JsonDetail/index.vue +70 -70
- package/dist/v3/DevTool/components/NFCList/NFCItem.vue +112 -112
- package/dist/v3/DevTool/components/NFCList/NFCTool.vue +454 -454
- package/dist/v3/DevTool/components/NFCList/const.ts +56 -56
- package/dist/v3/DevTool/components/NFCList/index.vue +94 -94
- package/dist/v3/DevTool/components/NetworkList/InterceptConfig.vue +624 -624
- package/dist/v3/DevTool/components/NetworkList/InterceptItem.vue +140 -140
- package/dist/v3/DevTool/components/NetworkList/NetworkDetail.vue +287 -287
- package/dist/v3/DevTool/components/NetworkList/NetworkIntercept.vue +88 -88
- package/dist/v3/DevTool/components/NetworkList/NetworkItem.vue +163 -163
- package/dist/v3/DevTool/components/NetworkList/NetworkSend.vue +589 -589
- package/dist/v3/DevTool/components/NetworkList/const.ts +4 -4
- package/dist/v3/DevTool/components/NetworkList/hooks/useNetworkForm.ts +86 -86
- package/dist/v3/DevTool/components/NetworkList/index.vue +160 -160
- package/dist/v3/DevTool/components/NetworkList/utils.ts +101 -101
- package/dist/v3/DevTool/components/Performance/index.vue +498 -498
- package/dist/v3/DevTool/components/Performance/modules/PerformanceMetrics.vue +153 -153
- package/dist/v3/DevTool/components/Performance/modules/usePerformanceChart.ts +460 -460
- package/dist/v3/DevTool/components/Performance/modules/usePerformanceData.ts +258 -258
- package/dist/v3/DevTool/components/PiniaList/index.vue +93 -93
- package/dist/v3/DevTool/components/RunJS/index.vue +148 -148
- package/dist/v3/DevTool/components/ScanCodeList/ScanCodeItem.vue +97 -97
- package/dist/v3/DevTool/components/ScanCodeList/index.vue +100 -100
- package/dist/v3/DevTool/components/SettingButton/index.vue +45 -45
- package/dist/v3/DevTool/components/SettingList/index.vue +218 -218
- package/dist/v3/DevTool/components/SettingList/modules/SettingBarrage.vue +304 -304
- package/dist/v3/DevTool/components/SettingList/modules/SettingDevTool.vue +212 -212
- package/dist/v3/DevTool/components/SettingList/modules/SettingInfo.vue +157 -157
- package/dist/v3/DevTool/components/SettingList/modules/SettingLanguage.vue +74 -74
- package/dist/v3/DevTool/components/SettingList/modules/SettingLog.vue +230 -230
- package/dist/v3/DevTool/components/SettingList/typing.d.ts +2 -2
- package/dist/v3/DevTool/components/SourceCode/Line.vue +127 -127
- package/dist/v3/DevTool/components/SourceCode/parseCode.ts +609 -609
- package/dist/v3/DevTool/components/StorageList/index.vue +174 -174
- package/dist/v3/DevTool/components/TransferList/TransferDetail.vue +268 -268
- package/dist/v3/DevTool/components/VuexList/index.vue +84 -84
- package/dist/v3/DevTool/index.vue +1 -0
- package/dist/v3/components/AppTransition/index.vue +176 -176
- package/dist/v3/components/AutoSizer/index.vue +192 -192
- package/dist/v3/components/AutoSizer/index1.vue +184 -184
- package/dist/v3/components/AutoSizer/utils.ts +49 -49
- package/dist/v3/components/Barrage/BarrageItem.vue +137 -137
- package/dist/v3/components/Barrage/index.vue +202 -202
- package/dist/v3/components/CircularButton/index.vue +84 -84
- package/dist/v3/components/CustomSwiper/CustomSwiperItem.vue +49 -49
- package/dist/v3/components/CustomSwiper/index.vue +104 -104
- package/dist/v3/components/DevErrorBoundary/index.vue +380 -0
- package/dist/v3/components/Empty/index.vue +29 -29
- package/dist/v3/components/FilterSelect/index.vue +179 -179
- package/dist/v3/components/JsonPretty/components/Brackets/index.vue +27 -27
- package/dist/v3/components/JsonPretty/components/Carets/index.vue +59 -59
- package/dist/v3/components/JsonPretty/components/CheckController/index.vue +136 -136
- package/dist/v3/components/JsonPretty/components/TreeNode/index.vue +387 -387
- package/dist/v3/components/JsonPretty/hooks/useClipboard.ts +21 -21
- package/dist/v3/components/JsonPretty/hooks/useError.ts +21 -21
- package/dist/v3/components/JsonPretty/type.ts +127 -127
- package/dist/v3/components/JsonPretty/utils/index.ts +169 -169
- package/dist/v3/components/MovableContainer/index.vue +8 -4
- package/dist/v3/components/Pick/index.vue +322 -322
- package/dist/v3/components/Tag/index.vue +113 -113
- package/dist/v3/components/VirtualList/AutoSize.vue +40 -40
- package/dist/v3/components/VirtualList/index.vue +416 -416
- package/dist/v3/hooks/useBluetooth/index.ts +561 -561
- package/dist/v3/hooks/useContainerStyle.ts +153 -153
- package/dist/v3/hooks/useNFC/index.ts +107 -107
- package/dist/v3/hooks/useNFC/typing.d.ts +396 -396
- package/dist/v3/hooks/useNFC/useNFCAndroid.ts +966 -966
- package/dist/v3/hooks/useNFC/useNFCMpWeiXin.ts +812 -812
- package/dist/v3/hooks/useNFC/utils.ts +754 -754
- package/dist/v3/hooks/useRequest/index.ts +586 -586
- package/dist/v3/hooks/useRequest/utils.ts +267 -267
- package/dist/v3/hooks/useScanCode/index.ts +206 -206
- package/dist/v3/hooks/useWebsocket/index.ts +253 -253
- package/dist/v3/styles/theme.ts +12 -12
- package/package.json +9 -1
|
@@ -1,561 +1,561 @@
|
|
|
1
|
-
import type { DevTool } from '../../../type';
|
|
2
|
-
import { ref, onUnmounted, watch } from 'vue';
|
|
3
|
-
|
|
4
|
-
const useBluetooth = () => {
|
|
5
|
-
const devices = ref<DevTool.BluetoothItem[]>([]);
|
|
6
|
-
const isSearching = ref(false);
|
|
7
|
-
const error = ref<string | null>(null);
|
|
8
|
-
const adapterOpened = ref(false);
|
|
9
|
-
const connectedDeviceIds = ref<Set<string>>(new Set());
|
|
10
|
-
const pairedDeviceIds = ref<Set<string>>(new Set());
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 检查当前平台是否支持蓝牙 API
|
|
14
|
-
*/
|
|
15
|
-
const checkSupport = (_tag?: string) => {
|
|
16
|
-
const isSupported = typeof uni.openBluetoothAdapter === 'function';
|
|
17
|
-
if (!isSupported) {
|
|
18
|
-
error.value = '当前环境不支持蓝牙功能';
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
return true;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 初始化蓝牙适配器
|
|
26
|
-
* 在 uni-app 中,开启适配器会同时拉起经典和 BLE 的底层准备。
|
|
27
|
-
*/
|
|
28
|
-
const openBluetoothAdapter = () => {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
if (!checkSupport('openBluetoothAdapter')) {
|
|
31
|
-
return reject(new Error('不支持蓝牙'));
|
|
32
|
-
}
|
|
33
|
-
uni.openBluetoothAdapter({
|
|
34
|
-
// 当 mode 为 central 时不仅限于低功耗,默认会尽可能扫双模
|
|
35
|
-
mode: 'central',
|
|
36
|
-
success: (res) => {
|
|
37
|
-
adapterOpened.value = true;
|
|
38
|
-
error.value = null;
|
|
39
|
-
|
|
40
|
-
// 顺便获取本机的蓝牙适配器状态
|
|
41
|
-
if (typeof uni.getBluetoothAdapterState === 'function') {
|
|
42
|
-
uni.getBluetoothAdapterState({
|
|
43
|
-
success: (stateRes) => {
|
|
44
|
-
if (!stateRes.available) {
|
|
45
|
-
error.value = '本机蓝牙不可用,请检查设置';
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
resolve(res);
|
|
52
|
-
},
|
|
53
|
-
fail: (err) => {
|
|
54
|
-
error.value = `初始化蓝牙适配器失败: ${err.errMsg}`;
|
|
55
|
-
reject(err);
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 开始搜索附近的蓝牙外围设备
|
|
63
|
-
*/
|
|
64
|
-
const startBluetoothDevicesDiscovery = () => {
|
|
65
|
-
if (!checkSupport('startDiscovery')) return;
|
|
66
|
-
if (isSearching.value) return;
|
|
67
|
-
|
|
68
|
-
const start = () => {
|
|
69
|
-
onBluetoothDeviceFound();
|
|
70
|
-
|
|
71
|
-
uni.startBluetoothDevicesDiscovery({
|
|
72
|
-
allowDuplicatesKey: false, // 改为 false,部分手机开启为 true 会搜不到经典蓝牙比如耳机
|
|
73
|
-
interval: 0, // 上报设备的间隔,0 为立即上报,部分低版本对并发有限制
|
|
74
|
-
powerLevel: 'high', // 尝试使用高功率扫描以囊括更远或信号较弱的经典蓝牙设备
|
|
75
|
-
success: () => {
|
|
76
|
-
isSearching.value = true;
|
|
77
|
-
error.value = null;
|
|
78
|
-
|
|
79
|
-
// 延迟获取一次补偿,以防部分设备被系统缓存而不会触发 onBluetoothDeviceFound
|
|
80
|
-
// 给到更宽裕的时间(3秒)让底层蓝牙扫描有足够时间建立连接与发现回调
|
|
81
|
-
setTimeout(() => {
|
|
82
|
-
if (isSearching.value) {
|
|
83
|
-
getBluetoothDevices();
|
|
84
|
-
}
|
|
85
|
-
}, 3000);
|
|
86
|
-
},
|
|
87
|
-
fail: (err: any) => {
|
|
88
|
-
let errorMsg = err.errMsg || '未知错误';
|
|
89
|
-
// 针对小程序及跨端环境补充典型的权限及硬件未开错误解释
|
|
90
|
-
if (err.errCode === 10001) {
|
|
91
|
-
errorMsg = '当前设备蓝牙未开或无权访问,请检查开关与位置权限';
|
|
92
|
-
}
|
|
93
|
-
error.value = `开始搜索失败: ${errorMsg} (${Date.now()})`; // 加时间戳强制触发 watch
|
|
94
|
-
uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 });
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (!adapterOpened.value) {
|
|
100
|
-
openBluetoothAdapter()
|
|
101
|
-
.then(() => {
|
|
102
|
-
fetchConnectedDevices(); // 初始化蓝牙后立即拉取一次已连接
|
|
103
|
-
start();
|
|
104
|
-
})
|
|
105
|
-
.catch(() => {});
|
|
106
|
-
} else {
|
|
107
|
-
fetchConnectedDevices(); // 直接开启也会拉取一次
|
|
108
|
-
start();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 停止搜索附近的蓝牙外围设备
|
|
114
|
-
*/
|
|
115
|
-
const stopBluetoothDevicesDiscovery = () => {
|
|
116
|
-
if (!checkSupport('stopDiscovery')) return;
|
|
117
|
-
if (!isSearching.value) return;
|
|
118
|
-
uni.stopBluetoothDevicesDiscovery({
|
|
119
|
-
success: () => {
|
|
120
|
-
isSearching.value = false;
|
|
121
|
-
},
|
|
122
|
-
fail: (err) => {
|
|
123
|
-
error.value = `停止搜索失败: ${err.errMsg}`;
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 猜测设备类型
|
|
130
|
-
*/
|
|
131
|
-
const guessDeviceType = (rawDevice: unknown) => {
|
|
132
|
-
const device = rawDevice as Record<string, unknown>;
|
|
133
|
-
const name = (
|
|
134
|
-
(device.name as string) ||
|
|
135
|
-
(device.localName as string) ||
|
|
136
|
-
''
|
|
137
|
-
).toLowerCase();
|
|
138
|
-
|
|
139
|
-
// 耳机、音响类特征
|
|
140
|
-
if (
|
|
141
|
-
name.includes('airpods') ||
|
|
142
|
-
name.includes('buds') ||
|
|
143
|
-
name.includes('headphones') ||
|
|
144
|
-
name.includes('ear') ||
|
|
145
|
-
name.includes('headset') ||
|
|
146
|
-
name.includes('audio') ||
|
|
147
|
-
name.includes('speaker') ||
|
|
148
|
-
/[\u8033\u673a\u97f3\u54cd]/.test(name)
|
|
149
|
-
) {
|
|
150
|
-
return '蓝牙耳机/音响';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 手机、平板电脑类特征
|
|
154
|
-
if (
|
|
155
|
-
name.includes('iphone') ||
|
|
156
|
-
name.includes('ipad') ||
|
|
157
|
-
name.includes('mac') ||
|
|
158
|
-
name.includes('android') ||
|
|
159
|
-
name.includes('phone') ||
|
|
160
|
-
name.includes('tablet') ||
|
|
161
|
-
name.includes('pixel') ||
|
|
162
|
-
name.includes('galaxy') ||
|
|
163
|
-
/[\u624b\u673a\u5e73\u677f\u7535\u8111]/.test(name)
|
|
164
|
-
) {
|
|
165
|
-
return '手机/电脑/平板';
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// 手表、手环类特征
|
|
169
|
-
if (
|
|
170
|
-
name.includes('watch') ||
|
|
171
|
-
name.includes('band') ||
|
|
172
|
-
name.includes('bracelet') ||
|
|
173
|
-
/[\u624b\u8868\u624b\u73af]/.test(name)
|
|
174
|
-
) {
|
|
175
|
-
return '智能手表/手环';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 如果没有任何特征且没有低功耗服务,我们再降级到经典蓝牙
|
|
179
|
-
const isProbablyClassic =
|
|
180
|
-
!(device.advertisServiceUUIDs as string[])?.length &&
|
|
181
|
-
!device.localName &&
|
|
182
|
-
!device.name;
|
|
183
|
-
|
|
184
|
-
if (isProbablyClassic) {
|
|
185
|
-
return '未知外设(经典蓝牙?)';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return 'BLE/常规外设';
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 监听寻找到新设备的事件
|
|
193
|
-
*/
|
|
194
|
-
const onBluetoothDeviceFound = () => {
|
|
195
|
-
if (!checkSupport('onDeviceFound')) return;
|
|
196
|
-
uni.onBluetoothDeviceFound((res) => {
|
|
197
|
-
if (res.devices) {
|
|
198
|
-
res.devices.forEach((device) => {
|
|
199
|
-
const enhancedDevice = {
|
|
200
|
-
...device,
|
|
201
|
-
_guessType: guessDeviceType(device),
|
|
202
|
-
_isConnected: connectedDeviceIds.value.has(device.deviceId),
|
|
203
|
-
isPaired:
|
|
204
|
-
!!(device as unknown as Record<string, unknown>).isPaired ||
|
|
205
|
-
pairedDeviceIds.value.has(device.deviceId),
|
|
206
|
-
} as unknown as DevTool.BluetoothItem;
|
|
207
|
-
|
|
208
|
-
const index = devices.value.findIndex(
|
|
209
|
-
(d) => d.deviceId === device.deviceId,
|
|
210
|
-
);
|
|
211
|
-
if (index === -1) {
|
|
212
|
-
devices.value.push(
|
|
213
|
-
enhancedDevice as unknown as DevTool.BluetoothItem,
|
|
214
|
-
);
|
|
215
|
-
} else {
|
|
216
|
-
devices.value[index] =
|
|
217
|
-
enhancedDevice as unknown as DevTool.BluetoothItem;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* 获取在蓝牙模块生效期间所有已发现的蓝牙设备
|
|
226
|
-
*/
|
|
227
|
-
const getBluetoothDevices = () => {
|
|
228
|
-
if (!checkSupport('getBluetoothDevices')) return;
|
|
229
|
-
uni.getBluetoothDevices({
|
|
230
|
-
success: (res) => {
|
|
231
|
-
if (res.devices) {
|
|
232
|
-
res.devices.forEach((device) => {
|
|
233
|
-
const enhancedDevice = {
|
|
234
|
-
...device,
|
|
235
|
-
_guessType: guessDeviceType(device),
|
|
236
|
-
_isConnected: connectedDeviceIds.value.has(device.deviceId),
|
|
237
|
-
isPaired:
|
|
238
|
-
!!(device as unknown as Record<string, unknown>).isPaired ||
|
|
239
|
-
pairedDeviceIds.value.has(device.deviceId),
|
|
240
|
-
} as unknown as DevTool.BluetoothItem;
|
|
241
|
-
|
|
242
|
-
const index = devices.value.findIndex(
|
|
243
|
-
(d) => d.deviceId === device.deviceId,
|
|
244
|
-
);
|
|
245
|
-
if (index === -1) {
|
|
246
|
-
devices.value.push(
|
|
247
|
-
enhancedDevice as unknown as DevTool.BluetoothItem,
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
devices.value[index] =
|
|
251
|
-
enhancedDevice as unknown as DevTool.BluetoothItem;
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
fail: (err) => {
|
|
257
|
-
error.value = `获取设备列表失败: ${err.errMsg}`;
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* 获取当前处于已连接状态的设备
|
|
264
|
-
*/
|
|
265
|
-
const fetchConnectedDevices = () => {
|
|
266
|
-
if (!checkSupport()) return;
|
|
267
|
-
// 微信小程序平台不传 services 可以获取当前所以已经配对或者连接的设备
|
|
268
|
-
// @ts-ignore
|
|
269
|
-
uni.getConnectedBluetoothDevices({
|
|
270
|
-
success: (res) => {
|
|
271
|
-
const ids = new Set(res.devices.map((d) => d.deviceId));
|
|
272
|
-
connectedDeviceIds.value = ids;
|
|
273
|
-
|
|
274
|
-
// 无论如何,这些在连接系统池里拿到的绝对存在配对关系
|
|
275
|
-
res.devices.forEach((d) => pairedDeviceIds.value.add(d.deviceId));
|
|
276
|
-
|
|
277
|
-
// 如果获取到了已连接(或已配对)的设备,即便它没有出现在 discovery 广播里,也可以强行把它加入了展示列表中
|
|
278
|
-
res.devices.forEach((device) => {
|
|
279
|
-
const deviceExt = device as unknown as Record<string, unknown>;
|
|
280
|
-
|
|
281
|
-
const enhancedDevice = {
|
|
282
|
-
...deviceExt,
|
|
283
|
-
_guessType: guessDeviceType(deviceExt),
|
|
284
|
-
_isConnected: true,
|
|
285
|
-
isPaired: true, // 能获取到 connected 肯定也代表配对或者已链接过
|
|
286
|
-
} as unknown as DevTool.BluetoothItem;
|
|
287
|
-
|
|
288
|
-
const index = devices.value.findIndex(
|
|
289
|
-
(d) => d.deviceId === device.deviceId,
|
|
290
|
-
);
|
|
291
|
-
if (index === -1) {
|
|
292
|
-
devices.value.push(enhancedDevice);
|
|
293
|
-
} else {
|
|
294
|
-
// 如果已经在列表中,只需更新连接与配对状态
|
|
295
|
-
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
296
|
-
true;
|
|
297
|
-
(devices.value[index] as Record<string, unknown>).isPaired = true;
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
},
|
|
301
|
-
fail: (err) => {
|
|
302
|
-
console.error(
|
|
303
|
-
'[Bluetooth Debug] fetchConnectedDevices 获取由于底层限制失败: ',
|
|
304
|
-
err,
|
|
305
|
-
);
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* 连接蓝牙设备 (低功耗蓝牙)
|
|
312
|
-
*/
|
|
313
|
-
const connectDevice = (deviceId: string) => {
|
|
314
|
-
return new Promise((resolve, reject) => {
|
|
315
|
-
if (!checkSupport()) return reject(new Error('未支持 API'));
|
|
316
|
-
|
|
317
|
-
// 在尝试连接前最好先停止搜索,可以提高连接成功率
|
|
318
|
-
stopBluetoothDevicesDiscovery();
|
|
319
|
-
|
|
320
|
-
uni.createBLEConnection({
|
|
321
|
-
deviceId,
|
|
322
|
-
timeout: 5000,
|
|
323
|
-
success: (res) => {
|
|
324
|
-
connectedDeviceIds.value.add(deviceId);
|
|
325
|
-
|
|
326
|
-
// 更新本地列表状态
|
|
327
|
-
const index = devices.value.findIndex((d) => d.deviceId === deviceId);
|
|
328
|
-
if (index !== -1) {
|
|
329
|
-
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
330
|
-
true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
resolve(res);
|
|
334
|
-
},
|
|
335
|
-
fail: (err) => {
|
|
336
|
-
error.value = `连接设备失败: ${err.errMsg || '未知错误'}`;
|
|
337
|
-
reject(err);
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* 断开蓝牙设备连接
|
|
345
|
-
*/
|
|
346
|
-
const disconnectDevice = (deviceId: string) => {
|
|
347
|
-
return new Promise((resolve, reject) => {
|
|
348
|
-
uni.closeBLEConnection({
|
|
349
|
-
deviceId,
|
|
350
|
-
success: (res) => {
|
|
351
|
-
connectedDeviceIds.value.delete(deviceId);
|
|
352
|
-
const index = devices.value.findIndex((d) => d.deviceId === deviceId);
|
|
353
|
-
if (index !== -1) {
|
|
354
|
-
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
355
|
-
false;
|
|
356
|
-
}
|
|
357
|
-
resolve(res);
|
|
358
|
-
},
|
|
359
|
-
fail: (err) => {
|
|
360
|
-
error.value = `断开连接失败: ${err.errMsg || '未知错误'}`;
|
|
361
|
-
reject(err);
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* 发起蓝牙配对
|
|
369
|
-
* 注意:在 uni-app/小程序 底层通常不支持前台主动进行单纯的"配对"(makePair)。
|
|
370
|
-
* 标准流程是通过直接发起 `createBLEConnection` (这里复用 connectDevice),
|
|
371
|
-
* 底层系统底层需要权限时会自动弹出配对 PIN 输入框。
|
|
372
|
-
*/
|
|
373
|
-
const pairDevice = (deviceId: string) => {
|
|
374
|
-
// 某些特定的 App 或 框架可能支持 `uni.makeBluetoothPair`,这里做平滑兼容
|
|
375
|
-
if (typeof (uni as any).makeBluetoothPair === 'function') {
|
|
376
|
-
return new Promise((resolve, reject) => {
|
|
377
|
-
(uni as any).makeBluetoothPair({
|
|
378
|
-
deviceId,
|
|
379
|
-
success: (res: any) => {
|
|
380
|
-
pairedDeviceIds.value.add(deviceId);
|
|
381
|
-
const index = devices.value.findIndex(
|
|
382
|
-
(d) => d.deviceId === deviceId,
|
|
383
|
-
);
|
|
384
|
-
if (index !== -1) {
|
|
385
|
-
(devices.value[index] as Record<string, unknown>).isPaired = true;
|
|
386
|
-
}
|
|
387
|
-
resolve(res);
|
|
388
|
-
},
|
|
389
|
-
fail: (err: any) => {
|
|
390
|
-
error.value = `请求配对失败: ${err.errMsg || '未知错误'}`;
|
|
391
|
-
reject(err);
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
} else {
|
|
396
|
-
// 否则回退到直接调用连接,系统在必要的 GATT 特征下会自动转为系统级配对弹窗
|
|
397
|
-
return connectDevice(deviceId);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* 获取蓝牙设备的所有服务 (Services)
|
|
403
|
-
*/
|
|
404
|
-
const getBLEServices = (deviceId: string) => {
|
|
405
|
-
return new Promise<unknown[]>((resolve, reject) => {
|
|
406
|
-
uni.getBLEDeviceServices({
|
|
407
|
-
deviceId,
|
|
408
|
-
success: (res) => resolve(res.services),
|
|
409
|
-
fail: (err) => {
|
|
410
|
-
error.value = `获取服务失败: ${err.errMsg || '未知错误'}`;
|
|
411
|
-
reject(err);
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* 获取某个服务下的所有特征值 (Characteristics)
|
|
419
|
-
*/
|
|
420
|
-
const getBLECharacteristics = (deviceId: string, serviceId: string) => {
|
|
421
|
-
return new Promise<unknown[]>((resolve, reject) => {
|
|
422
|
-
uni.getBLEDeviceCharacteristics({
|
|
423
|
-
deviceId,
|
|
424
|
-
serviceId,
|
|
425
|
-
success: (res) => resolve(res.characteristics as unknown[]),
|
|
426
|
-
fail: (err) => {
|
|
427
|
-
error.value = `获取特征值失败: ${err.errMsg || '未知错误'}`;
|
|
428
|
-
reject(err);
|
|
429
|
-
},
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* 向指定的特征值发送(写入)消息
|
|
436
|
-
*/
|
|
437
|
-
const writeBLEMessage = (
|
|
438
|
-
deviceId: string,
|
|
439
|
-
serviceId: string,
|
|
440
|
-
characteristicId: string,
|
|
441
|
-
buffer: ArrayBuffer,
|
|
442
|
-
writeType?: 'write' | 'writeNoResponse',
|
|
443
|
-
) => {
|
|
444
|
-
return new Promise((resolve, reject) => {
|
|
445
|
-
// 强制克隆一份彻底规避 Proxy,同时确保它是普通 ArrayBuffer 的底层形态
|
|
446
|
-
const cloneBuffer = new ArrayBuffer(buffer.byteLength);
|
|
447
|
-
const dataView = new DataView(cloneBuffer);
|
|
448
|
-
const sourceView = new Uint8Array(buffer);
|
|
449
|
-
for (let i = 0; i < sourceView.length; i++) {
|
|
450
|
-
dataView.setUint8(i, sourceView[i]);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const params: any = {
|
|
454
|
-
deviceId,
|
|
455
|
-
serviceId,
|
|
456
|
-
characteristicId,
|
|
457
|
-
value: cloneBuffer, // 送入完全干净的原生构造 Buffer
|
|
458
|
-
success: (res: any) => resolve(res),
|
|
459
|
-
fail: (err: any) => {
|
|
460
|
-
error.value = `发送消息失败: ${err.errMsg || '未知错误'}`;
|
|
461
|
-
reject(err);
|
|
462
|
-
},
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
if (writeType) {
|
|
466
|
-
params.writeType = writeType;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
uni.writeBLECharacteristicValue(params);
|
|
470
|
-
});
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* 订阅特征值的消息变化 (Notify)
|
|
475
|
-
*/
|
|
476
|
-
const subscribeBLEMessage = (
|
|
477
|
-
deviceId: string,
|
|
478
|
-
serviceId: string,
|
|
479
|
-
characteristicId: string,
|
|
480
|
-
state = true,
|
|
481
|
-
) => {
|
|
482
|
-
return new Promise((resolve, reject) => {
|
|
483
|
-
uni.notifyBLECharacteristicValueChange({
|
|
484
|
-
deviceId,
|
|
485
|
-
serviceId,
|
|
486
|
-
characteristicId,
|
|
487
|
-
state,
|
|
488
|
-
success: (res) => resolve(res),
|
|
489
|
-
fail: (err) => {
|
|
490
|
-
error.value = `订阅消息失败: ${err.errMsg || '未知错误'}`;
|
|
491
|
-
reject(err);
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* 监听全局接收到的蓝牙设备消息
|
|
499
|
-
*/
|
|
500
|
-
const onBLEMessageReceived = (callback: (res: unknown) => void) => {
|
|
501
|
-
if (typeof uni.onBLECharacteristicValueChange === 'function') {
|
|
502
|
-
uni.onBLECharacteristicValueChange(callback as any);
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* 关闭蓝牙模块
|
|
508
|
-
*/
|
|
509
|
-
const closeBluetoothAdapter = () => {
|
|
510
|
-
if (!checkSupport()) return;
|
|
511
|
-
uni.closeBluetoothAdapter({
|
|
512
|
-
success: () => {
|
|
513
|
-
adapterOpened.value = false;
|
|
514
|
-
isSearching.value = false;
|
|
515
|
-
devices.value = [];
|
|
516
|
-
connectedDeviceIds.value.clear();
|
|
517
|
-
pairedDeviceIds.value.clear();
|
|
518
|
-
},
|
|
519
|
-
});
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
onUnmounted(() => {
|
|
523
|
-
if (typeof uni.stopBluetoothDevicesDiscovery === 'function') {
|
|
524
|
-
stopBluetoothDevicesDiscovery();
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
watch(error, (newMsg) => {
|
|
529
|
-
if (newMsg) {
|
|
530
|
-
uni.showToast({
|
|
531
|
-
title: newMsg,
|
|
532
|
-
icon: 'none',
|
|
533
|
-
duration: 2000,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
return {
|
|
539
|
-
devices,
|
|
540
|
-
isSearching,
|
|
541
|
-
error,
|
|
542
|
-
adapterOpened,
|
|
543
|
-
checkSupport,
|
|
544
|
-
openBluetoothAdapter,
|
|
545
|
-
startBluetoothDevicesDiscovery,
|
|
546
|
-
stopBluetoothDevicesDiscovery,
|
|
547
|
-
getBluetoothDevices,
|
|
548
|
-
fetchConnectedDevices,
|
|
549
|
-
connectDevice,
|
|
550
|
-
disconnectDevice,
|
|
551
|
-
pairDevice,
|
|
552
|
-
getBLEServices,
|
|
553
|
-
getBLECharacteristics,
|
|
554
|
-
writeBLEMessage,
|
|
555
|
-
subscribeBLEMessage,
|
|
556
|
-
onBLEMessageReceived,
|
|
557
|
-
closeBluetoothAdapter,
|
|
558
|
-
};
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
export default useBluetooth;
|
|
1
|
+
import type { DevTool } from '../../../type';
|
|
2
|
+
import { ref, onUnmounted, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
const useBluetooth = () => {
|
|
5
|
+
const devices = ref<DevTool.BluetoothItem[]>([]);
|
|
6
|
+
const isSearching = ref(false);
|
|
7
|
+
const error = ref<string | null>(null);
|
|
8
|
+
const adapterOpened = ref(false);
|
|
9
|
+
const connectedDeviceIds = ref<Set<string>>(new Set());
|
|
10
|
+
const pairedDeviceIds = ref<Set<string>>(new Set());
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 检查当前平台是否支持蓝牙 API
|
|
14
|
+
*/
|
|
15
|
+
const checkSupport = (_tag?: string) => {
|
|
16
|
+
const isSupported = typeof uni.openBluetoothAdapter === 'function';
|
|
17
|
+
if (!isSupported) {
|
|
18
|
+
error.value = '当前环境不支持蓝牙功能';
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 初始化蓝牙适配器
|
|
26
|
+
* 在 uni-app 中,开启适配器会同时拉起经典和 BLE 的底层准备。
|
|
27
|
+
*/
|
|
28
|
+
const openBluetoothAdapter = () => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
if (!checkSupport('openBluetoothAdapter')) {
|
|
31
|
+
return reject(new Error('不支持蓝牙'));
|
|
32
|
+
}
|
|
33
|
+
uni.openBluetoothAdapter({
|
|
34
|
+
// 当 mode 为 central 时不仅限于低功耗,默认会尽可能扫双模
|
|
35
|
+
mode: 'central',
|
|
36
|
+
success: (res) => {
|
|
37
|
+
adapterOpened.value = true;
|
|
38
|
+
error.value = null;
|
|
39
|
+
|
|
40
|
+
// 顺便获取本机的蓝牙适配器状态
|
|
41
|
+
if (typeof uni.getBluetoothAdapterState === 'function') {
|
|
42
|
+
uni.getBluetoothAdapterState({
|
|
43
|
+
success: (stateRes) => {
|
|
44
|
+
if (!stateRes.available) {
|
|
45
|
+
error.value = '本机蓝牙不可用,请检查设置';
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolve(res);
|
|
52
|
+
},
|
|
53
|
+
fail: (err) => {
|
|
54
|
+
error.value = `初始化蓝牙适配器失败: ${err.errMsg}`;
|
|
55
|
+
reject(err);
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 开始搜索附近的蓝牙外围设备
|
|
63
|
+
*/
|
|
64
|
+
const startBluetoothDevicesDiscovery = () => {
|
|
65
|
+
if (!checkSupport('startDiscovery')) return;
|
|
66
|
+
if (isSearching.value) return;
|
|
67
|
+
|
|
68
|
+
const start = () => {
|
|
69
|
+
onBluetoothDeviceFound();
|
|
70
|
+
|
|
71
|
+
uni.startBluetoothDevicesDiscovery({
|
|
72
|
+
allowDuplicatesKey: false, // 改为 false,部分手机开启为 true 会搜不到经典蓝牙比如耳机
|
|
73
|
+
interval: 0, // 上报设备的间隔,0 为立即上报,部分低版本对并发有限制
|
|
74
|
+
powerLevel: 'high', // 尝试使用高功率扫描以囊括更远或信号较弱的经典蓝牙设备
|
|
75
|
+
success: () => {
|
|
76
|
+
isSearching.value = true;
|
|
77
|
+
error.value = null;
|
|
78
|
+
|
|
79
|
+
// 延迟获取一次补偿,以防部分设备被系统缓存而不会触发 onBluetoothDeviceFound
|
|
80
|
+
// 给到更宽裕的时间(3秒)让底层蓝牙扫描有足够时间建立连接与发现回调
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
if (isSearching.value) {
|
|
83
|
+
getBluetoothDevices();
|
|
84
|
+
}
|
|
85
|
+
}, 3000);
|
|
86
|
+
},
|
|
87
|
+
fail: (err: any) => {
|
|
88
|
+
let errorMsg = err.errMsg || '未知错误';
|
|
89
|
+
// 针对小程序及跨端环境补充典型的权限及硬件未开错误解释
|
|
90
|
+
if (err.errCode === 10001) {
|
|
91
|
+
errorMsg = '当前设备蓝牙未开或无权访问,请检查开关与位置权限';
|
|
92
|
+
}
|
|
93
|
+
error.value = `开始搜索失败: ${errorMsg} (${Date.now()})`; // 加时间戳强制触发 watch
|
|
94
|
+
uni.showToast({ title: errorMsg, icon: 'none', duration: 3000 });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (!adapterOpened.value) {
|
|
100
|
+
openBluetoothAdapter()
|
|
101
|
+
.then(() => {
|
|
102
|
+
fetchConnectedDevices(); // 初始化蓝牙后立即拉取一次已连接
|
|
103
|
+
start();
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {});
|
|
106
|
+
} else {
|
|
107
|
+
fetchConnectedDevices(); // 直接开启也会拉取一次
|
|
108
|
+
start();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 停止搜索附近的蓝牙外围设备
|
|
114
|
+
*/
|
|
115
|
+
const stopBluetoothDevicesDiscovery = () => {
|
|
116
|
+
if (!checkSupport('stopDiscovery')) return;
|
|
117
|
+
if (!isSearching.value) return;
|
|
118
|
+
uni.stopBluetoothDevicesDiscovery({
|
|
119
|
+
success: () => {
|
|
120
|
+
isSearching.value = false;
|
|
121
|
+
},
|
|
122
|
+
fail: (err) => {
|
|
123
|
+
error.value = `停止搜索失败: ${err.errMsg}`;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 猜测设备类型
|
|
130
|
+
*/
|
|
131
|
+
const guessDeviceType = (rawDevice: unknown) => {
|
|
132
|
+
const device = rawDevice as Record<string, unknown>;
|
|
133
|
+
const name = (
|
|
134
|
+
(device.name as string) ||
|
|
135
|
+
(device.localName as string) ||
|
|
136
|
+
''
|
|
137
|
+
).toLowerCase();
|
|
138
|
+
|
|
139
|
+
// 耳机、音响类特征
|
|
140
|
+
if (
|
|
141
|
+
name.includes('airpods') ||
|
|
142
|
+
name.includes('buds') ||
|
|
143
|
+
name.includes('headphones') ||
|
|
144
|
+
name.includes('ear') ||
|
|
145
|
+
name.includes('headset') ||
|
|
146
|
+
name.includes('audio') ||
|
|
147
|
+
name.includes('speaker') ||
|
|
148
|
+
/[\u8033\u673a\u97f3\u54cd]/.test(name)
|
|
149
|
+
) {
|
|
150
|
+
return '蓝牙耳机/音响';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 手机、平板电脑类特征
|
|
154
|
+
if (
|
|
155
|
+
name.includes('iphone') ||
|
|
156
|
+
name.includes('ipad') ||
|
|
157
|
+
name.includes('mac') ||
|
|
158
|
+
name.includes('android') ||
|
|
159
|
+
name.includes('phone') ||
|
|
160
|
+
name.includes('tablet') ||
|
|
161
|
+
name.includes('pixel') ||
|
|
162
|
+
name.includes('galaxy') ||
|
|
163
|
+
/[\u624b\u673a\u5e73\u677f\u7535\u8111]/.test(name)
|
|
164
|
+
) {
|
|
165
|
+
return '手机/电脑/平板';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 手表、手环类特征
|
|
169
|
+
if (
|
|
170
|
+
name.includes('watch') ||
|
|
171
|
+
name.includes('band') ||
|
|
172
|
+
name.includes('bracelet') ||
|
|
173
|
+
/[\u624b\u8868\u624b\u73af]/.test(name)
|
|
174
|
+
) {
|
|
175
|
+
return '智能手表/手环';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 如果没有任何特征且没有低功耗服务,我们再降级到经典蓝牙
|
|
179
|
+
const isProbablyClassic =
|
|
180
|
+
!(device.advertisServiceUUIDs as string[])?.length &&
|
|
181
|
+
!device.localName &&
|
|
182
|
+
!device.name;
|
|
183
|
+
|
|
184
|
+
if (isProbablyClassic) {
|
|
185
|
+
return '未知外设(经典蓝牙?)';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return 'BLE/常规外设';
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 监听寻找到新设备的事件
|
|
193
|
+
*/
|
|
194
|
+
const onBluetoothDeviceFound = () => {
|
|
195
|
+
if (!checkSupport('onDeviceFound')) return;
|
|
196
|
+
uni.onBluetoothDeviceFound((res) => {
|
|
197
|
+
if (res.devices) {
|
|
198
|
+
res.devices.forEach((device) => {
|
|
199
|
+
const enhancedDevice = {
|
|
200
|
+
...device,
|
|
201
|
+
_guessType: guessDeviceType(device),
|
|
202
|
+
_isConnected: connectedDeviceIds.value.has(device.deviceId),
|
|
203
|
+
isPaired:
|
|
204
|
+
!!(device as unknown as Record<string, unknown>).isPaired ||
|
|
205
|
+
pairedDeviceIds.value.has(device.deviceId),
|
|
206
|
+
} as unknown as DevTool.BluetoothItem;
|
|
207
|
+
|
|
208
|
+
const index = devices.value.findIndex(
|
|
209
|
+
(d) => d.deviceId === device.deviceId,
|
|
210
|
+
);
|
|
211
|
+
if (index === -1) {
|
|
212
|
+
devices.value.push(
|
|
213
|
+
enhancedDevice as unknown as DevTool.BluetoothItem,
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
devices.value[index] =
|
|
217
|
+
enhancedDevice as unknown as DevTool.BluetoothItem;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取在蓝牙模块生效期间所有已发现的蓝牙设备
|
|
226
|
+
*/
|
|
227
|
+
const getBluetoothDevices = () => {
|
|
228
|
+
if (!checkSupport('getBluetoothDevices')) return;
|
|
229
|
+
uni.getBluetoothDevices({
|
|
230
|
+
success: (res) => {
|
|
231
|
+
if (res.devices) {
|
|
232
|
+
res.devices.forEach((device) => {
|
|
233
|
+
const enhancedDevice = {
|
|
234
|
+
...device,
|
|
235
|
+
_guessType: guessDeviceType(device),
|
|
236
|
+
_isConnected: connectedDeviceIds.value.has(device.deviceId),
|
|
237
|
+
isPaired:
|
|
238
|
+
!!(device as unknown as Record<string, unknown>).isPaired ||
|
|
239
|
+
pairedDeviceIds.value.has(device.deviceId),
|
|
240
|
+
} as unknown as DevTool.BluetoothItem;
|
|
241
|
+
|
|
242
|
+
const index = devices.value.findIndex(
|
|
243
|
+
(d) => d.deviceId === device.deviceId,
|
|
244
|
+
);
|
|
245
|
+
if (index === -1) {
|
|
246
|
+
devices.value.push(
|
|
247
|
+
enhancedDevice as unknown as DevTool.BluetoothItem,
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
devices.value[index] =
|
|
251
|
+
enhancedDevice as unknown as DevTool.BluetoothItem;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
fail: (err) => {
|
|
257
|
+
error.value = `获取设备列表失败: ${err.errMsg}`;
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 获取当前处于已连接状态的设备
|
|
264
|
+
*/
|
|
265
|
+
const fetchConnectedDevices = () => {
|
|
266
|
+
if (!checkSupport()) return;
|
|
267
|
+
// 微信小程序平台不传 services 可以获取当前所以已经配对或者连接的设备
|
|
268
|
+
// @ts-ignore
|
|
269
|
+
uni.getConnectedBluetoothDevices({
|
|
270
|
+
success: (res) => {
|
|
271
|
+
const ids = new Set(res.devices.map((d) => d.deviceId));
|
|
272
|
+
connectedDeviceIds.value = ids;
|
|
273
|
+
|
|
274
|
+
// 无论如何,这些在连接系统池里拿到的绝对存在配对关系
|
|
275
|
+
res.devices.forEach((d) => pairedDeviceIds.value.add(d.deviceId));
|
|
276
|
+
|
|
277
|
+
// 如果获取到了已连接(或已配对)的设备,即便它没有出现在 discovery 广播里,也可以强行把它加入了展示列表中
|
|
278
|
+
res.devices.forEach((device) => {
|
|
279
|
+
const deviceExt = device as unknown as Record<string, unknown>;
|
|
280
|
+
|
|
281
|
+
const enhancedDevice = {
|
|
282
|
+
...deviceExt,
|
|
283
|
+
_guessType: guessDeviceType(deviceExt),
|
|
284
|
+
_isConnected: true,
|
|
285
|
+
isPaired: true, // 能获取到 connected 肯定也代表配对或者已链接过
|
|
286
|
+
} as unknown as DevTool.BluetoothItem;
|
|
287
|
+
|
|
288
|
+
const index = devices.value.findIndex(
|
|
289
|
+
(d) => d.deviceId === device.deviceId,
|
|
290
|
+
);
|
|
291
|
+
if (index === -1) {
|
|
292
|
+
devices.value.push(enhancedDevice);
|
|
293
|
+
} else {
|
|
294
|
+
// 如果已经在列表中,只需更新连接与配对状态
|
|
295
|
+
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
296
|
+
true;
|
|
297
|
+
(devices.value[index] as Record<string, unknown>).isPaired = true;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
fail: (err) => {
|
|
302
|
+
console.error(
|
|
303
|
+
'[Bluetooth Debug] fetchConnectedDevices 获取由于底层限制失败: ',
|
|
304
|
+
err,
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 连接蓝牙设备 (低功耗蓝牙)
|
|
312
|
+
*/
|
|
313
|
+
const connectDevice = (deviceId: string) => {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
if (!checkSupport()) return reject(new Error('未支持 API'));
|
|
316
|
+
|
|
317
|
+
// 在尝试连接前最好先停止搜索,可以提高连接成功率
|
|
318
|
+
stopBluetoothDevicesDiscovery();
|
|
319
|
+
|
|
320
|
+
uni.createBLEConnection({
|
|
321
|
+
deviceId,
|
|
322
|
+
timeout: 5000,
|
|
323
|
+
success: (res) => {
|
|
324
|
+
connectedDeviceIds.value.add(deviceId);
|
|
325
|
+
|
|
326
|
+
// 更新本地列表状态
|
|
327
|
+
const index = devices.value.findIndex((d) => d.deviceId === deviceId);
|
|
328
|
+
if (index !== -1) {
|
|
329
|
+
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
330
|
+
true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
resolve(res);
|
|
334
|
+
},
|
|
335
|
+
fail: (err) => {
|
|
336
|
+
error.value = `连接设备失败: ${err.errMsg || '未知错误'}`;
|
|
337
|
+
reject(err);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 断开蓝牙设备连接
|
|
345
|
+
*/
|
|
346
|
+
const disconnectDevice = (deviceId: string) => {
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
uni.closeBLEConnection({
|
|
349
|
+
deviceId,
|
|
350
|
+
success: (res) => {
|
|
351
|
+
connectedDeviceIds.value.delete(deviceId);
|
|
352
|
+
const index = devices.value.findIndex((d) => d.deviceId === deviceId);
|
|
353
|
+
if (index !== -1) {
|
|
354
|
+
(devices.value[index] as Record<string, unknown>)._isConnected =
|
|
355
|
+
false;
|
|
356
|
+
}
|
|
357
|
+
resolve(res);
|
|
358
|
+
},
|
|
359
|
+
fail: (err) => {
|
|
360
|
+
error.value = `断开连接失败: ${err.errMsg || '未知错误'}`;
|
|
361
|
+
reject(err);
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 发起蓝牙配对
|
|
369
|
+
* 注意:在 uni-app/小程序 底层通常不支持前台主动进行单纯的"配对"(makePair)。
|
|
370
|
+
* 标准流程是通过直接发起 `createBLEConnection` (这里复用 connectDevice),
|
|
371
|
+
* 底层系统底层需要权限时会自动弹出配对 PIN 输入框。
|
|
372
|
+
*/
|
|
373
|
+
const pairDevice = (deviceId: string) => {
|
|
374
|
+
// 某些特定的 App 或 框架可能支持 `uni.makeBluetoothPair`,这里做平滑兼容
|
|
375
|
+
if (typeof (uni as any).makeBluetoothPair === 'function') {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
(uni as any).makeBluetoothPair({
|
|
378
|
+
deviceId,
|
|
379
|
+
success: (res: any) => {
|
|
380
|
+
pairedDeviceIds.value.add(deviceId);
|
|
381
|
+
const index = devices.value.findIndex(
|
|
382
|
+
(d) => d.deviceId === deviceId,
|
|
383
|
+
);
|
|
384
|
+
if (index !== -1) {
|
|
385
|
+
(devices.value[index] as Record<string, unknown>).isPaired = true;
|
|
386
|
+
}
|
|
387
|
+
resolve(res);
|
|
388
|
+
},
|
|
389
|
+
fail: (err: any) => {
|
|
390
|
+
error.value = `请求配对失败: ${err.errMsg || '未知错误'}`;
|
|
391
|
+
reject(err);
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
} else {
|
|
396
|
+
// 否则回退到直接调用连接,系统在必要的 GATT 特征下会自动转为系统级配对弹窗
|
|
397
|
+
return connectDevice(deviceId);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 获取蓝牙设备的所有服务 (Services)
|
|
403
|
+
*/
|
|
404
|
+
const getBLEServices = (deviceId: string) => {
|
|
405
|
+
return new Promise<unknown[]>((resolve, reject) => {
|
|
406
|
+
uni.getBLEDeviceServices({
|
|
407
|
+
deviceId,
|
|
408
|
+
success: (res) => resolve(res.services),
|
|
409
|
+
fail: (err) => {
|
|
410
|
+
error.value = `获取服务失败: ${err.errMsg || '未知错误'}`;
|
|
411
|
+
reject(err);
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* 获取某个服务下的所有特征值 (Characteristics)
|
|
419
|
+
*/
|
|
420
|
+
const getBLECharacteristics = (deviceId: string, serviceId: string) => {
|
|
421
|
+
return new Promise<unknown[]>((resolve, reject) => {
|
|
422
|
+
uni.getBLEDeviceCharacteristics({
|
|
423
|
+
deviceId,
|
|
424
|
+
serviceId,
|
|
425
|
+
success: (res) => resolve(res.characteristics as unknown[]),
|
|
426
|
+
fail: (err) => {
|
|
427
|
+
error.value = `获取特征值失败: ${err.errMsg || '未知错误'}`;
|
|
428
|
+
reject(err);
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 向指定的特征值发送(写入)消息
|
|
436
|
+
*/
|
|
437
|
+
const writeBLEMessage = (
|
|
438
|
+
deviceId: string,
|
|
439
|
+
serviceId: string,
|
|
440
|
+
characteristicId: string,
|
|
441
|
+
buffer: ArrayBuffer,
|
|
442
|
+
writeType?: 'write' | 'writeNoResponse',
|
|
443
|
+
) => {
|
|
444
|
+
return new Promise((resolve, reject) => {
|
|
445
|
+
// 强制克隆一份彻底规避 Proxy,同时确保它是普通 ArrayBuffer 的底层形态
|
|
446
|
+
const cloneBuffer = new ArrayBuffer(buffer.byteLength);
|
|
447
|
+
const dataView = new DataView(cloneBuffer);
|
|
448
|
+
const sourceView = new Uint8Array(buffer);
|
|
449
|
+
for (let i = 0; i < sourceView.length; i++) {
|
|
450
|
+
dataView.setUint8(i, sourceView[i]);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const params: any = {
|
|
454
|
+
deviceId,
|
|
455
|
+
serviceId,
|
|
456
|
+
characteristicId,
|
|
457
|
+
value: cloneBuffer, // 送入完全干净的原生构造 Buffer
|
|
458
|
+
success: (res: any) => resolve(res),
|
|
459
|
+
fail: (err: any) => {
|
|
460
|
+
error.value = `发送消息失败: ${err.errMsg || '未知错误'}`;
|
|
461
|
+
reject(err);
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (writeType) {
|
|
466
|
+
params.writeType = writeType;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
uni.writeBLECharacteristicValue(params);
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 订阅特征值的消息变化 (Notify)
|
|
475
|
+
*/
|
|
476
|
+
const subscribeBLEMessage = (
|
|
477
|
+
deviceId: string,
|
|
478
|
+
serviceId: string,
|
|
479
|
+
characteristicId: string,
|
|
480
|
+
state = true,
|
|
481
|
+
) => {
|
|
482
|
+
return new Promise((resolve, reject) => {
|
|
483
|
+
uni.notifyBLECharacteristicValueChange({
|
|
484
|
+
deviceId,
|
|
485
|
+
serviceId,
|
|
486
|
+
characteristicId,
|
|
487
|
+
state,
|
|
488
|
+
success: (res) => resolve(res),
|
|
489
|
+
fail: (err) => {
|
|
490
|
+
error.value = `订阅消息失败: ${err.errMsg || '未知错误'}`;
|
|
491
|
+
reject(err);
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* 监听全局接收到的蓝牙设备消息
|
|
499
|
+
*/
|
|
500
|
+
const onBLEMessageReceived = (callback: (res: unknown) => void) => {
|
|
501
|
+
if (typeof uni.onBLECharacteristicValueChange === 'function') {
|
|
502
|
+
uni.onBLECharacteristicValueChange(callback as any);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 关闭蓝牙模块
|
|
508
|
+
*/
|
|
509
|
+
const closeBluetoothAdapter = () => {
|
|
510
|
+
if (!checkSupport()) return;
|
|
511
|
+
uni.closeBluetoothAdapter({
|
|
512
|
+
success: () => {
|
|
513
|
+
adapterOpened.value = false;
|
|
514
|
+
isSearching.value = false;
|
|
515
|
+
devices.value = [];
|
|
516
|
+
connectedDeviceIds.value.clear();
|
|
517
|
+
pairedDeviceIds.value.clear();
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
onUnmounted(() => {
|
|
523
|
+
if (typeof uni.stopBluetoothDevicesDiscovery === 'function') {
|
|
524
|
+
stopBluetoothDevicesDiscovery();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
watch(error, (newMsg) => {
|
|
529
|
+
if (newMsg) {
|
|
530
|
+
uni.showToast({
|
|
531
|
+
title: newMsg,
|
|
532
|
+
icon: 'none',
|
|
533
|
+
duration: 2000,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
devices,
|
|
540
|
+
isSearching,
|
|
541
|
+
error,
|
|
542
|
+
adapterOpened,
|
|
543
|
+
checkSupport,
|
|
544
|
+
openBluetoothAdapter,
|
|
545
|
+
startBluetoothDevicesDiscovery,
|
|
546
|
+
stopBluetoothDevicesDiscovery,
|
|
547
|
+
getBluetoothDevices,
|
|
548
|
+
fetchConnectedDevices,
|
|
549
|
+
connectDevice,
|
|
550
|
+
disconnectDevice,
|
|
551
|
+
pairDevice,
|
|
552
|
+
getBLEServices,
|
|
553
|
+
getBLECharacteristics,
|
|
554
|
+
writeBLEMessage,
|
|
555
|
+
subscribeBLEMessage,
|
|
556
|
+
onBLEMessageReceived,
|
|
557
|
+
closeBluetoothAdapter,
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
export default useBluetooth;
|