Implementing Bluetooth Device Discovery and Connection in UniApp with Permission Management
Bluetooth Device Discovery and Connection
This implementation demonstrates how to search for, connect to, and manage Bluetooth Low Energy (BLE) devices in a UniApp application using Vuex for state management. The solution includes proper permission handling and device caching to avoid redundant connections.
Bluetooth Initialization and Connection
created() {
// #ifdef APP-PLUS
this.cachedDevices = [];
// Check if location permissions are granted
this.appPermissionStatus = uni.getAppAuthorizeSetting();
if (this.appPermissionStatus.locationAuthorized === 'denied' ||
this.appPermissionStatus.locationAuthorized === 'not determined') {
this.requestLocationPermission();
return;
}
// Initialize Bluetooth scanning
this.initializeBluetooth();
console.log(this.bluetoothStore, 'Checking cached Bluetooth data');
if(this.bluetoothStore.deviceInfo.name) {
this.cachedDevices.push(this.bluetoothStore.deviceInfo);
this.connectToDevice(this.bluetoothStore.deviceInfo);
}
// #endif
},
methods: {
// Check Bluetooth adapter status
checkBluetoothAvailability() {
const self = this;
uni.getBluetoothAdapterState({
success (res) {
if (res.available) {
self.isScanning = true;
if (res.discovering) {
uni.showToast({
title: 'Scanning for nearby printers',
icon: "none"
});
return;
}
// Start discovering Bluetooth devices
self.discoverBluetoothDevices();
} else {
uni.showModal({
title: 'Notice',
content: 'Bluetooth is not available on this device',
});
}
}
});
},
// Get available Bluetooth devices
discoverBluetoothDevices() {
const self = this;
self.availableDevices = [];
uni.startBluetoothDevicesDiscovery({
success (res) {
console.log('Bluetooth discovery started', res);
// Listen for discovered devices
uni.onBluetoothDeviceFound((result) => {
console.log('New device found:', result.devices);
let currentList = self.availableDevices;
let newDevices = [];
let discoveredList = result.devices;
for (let i = 0; i < discoveredList.length; ++i) {
if (discoveredList[i].name) {
// Check if device already exists
let existingDevice = currentList.filter((item) => {
return item.deviceId == discoveredList[i].deviceId;
});
if (existingDevice.length === 0) {
newDevices.push(discoveredList[i]);
}
}
}
self.availableDevices = currentList.concat(newDevices);
});
// Set timeout to get final list of devices
self.scanTimeout = setTimeout(() => {
uni.getBluetoothDevices({
success (res) {
console.log('Final device list:', res);
let devices = [];
let deviceList = res.devices;
// Filter out devices without names
for (let i = 0; i < deviceList.length; ++i) {
if (deviceList[i].name) {
devices.push(deviceList[i]);
}
}
self.availableDevices = devices;
},
fail (error) {
console.log('Failed to get devices:', error);
}
});
clearTimeout(self.scanTimeout);
}, 1000);
},
fail (error) {
console.log('Failed to start discovery:', error);
}
});
},
// Connect to a specific Bluetooth device
connectToDevice(device) {
const self = this;
// Close existing connection if any
if(self.currentDeviceId) {
this.terminateConnection(self.currentDeviceId);
}
let deviceId = device.deviceId;
let deviceName = device.name;
// Reset connection state
self.currentServiceId = 0;
self.hasWriteCharacteristic = false;
self.hasReadCharacteristic = false;
self.hasNotifyCharacteristic = false;
uni.showLoading({
title: 'Connecting',
});
console.log('Connecting to device:', deviceId);
uni.createBLEConnection({
deviceId: deviceId,
success (res) {
self.currentDeviceId = deviceId;
// Set MTU size
uni.setBLEMTU({
deviceId: deviceId,
mtu: 512,
success (res) {
console.log('MTU set successfully');
},
fail (error) {
console.log('Failed to set MTU:', error);
}
});
// Store device info in Vuex
self.$store.commit('updateDeviceInfo', {
deviceId: deviceId,
name: deviceName
});
// Get device services
self.getDeviceServices();
},
fail (error) {
self.isReadyToPrint = false;
if (error.code == 1002) {
uni.showToast({
title:'Connection timeout, please try again',
icon: "none"
});
} else {
uni.showToast({
title:'Please turn on your Bluetooth printer',
icon: "none"
});
}
console.log('Connection failed:', error);
uni.hideLoading();
}
});
},
// Get all services from a connected device
getDeviceServices() {
const self = this;
let { deviceInfo } = self.bluetoothStore;
// Set timeout to wait for services
let serviceTimeout = setTimeout(() => {
uni.getBLEDeviceServices({
deviceId: deviceInfo.deviceId,
success (res) {
uni.hideLoading();
if (res.services.length === 0) {
uni.showToast({
title:'Failed to connect to Bluetooth, please try again',
icon: "none"
});
return;
}
uni.showToast({
title:'Connected, ready to print',
icon: "none"
});
self.currentServiceIndex = self.selectedDeviceIndex;
self.availableServices = res.services;
self.cachedDevices = [];
if(deviceInfo.name) {
self.cachedDevices.push(deviceInfo);
}
self.getDeviceCharacteristics();
},
fail (error) {
uni.hideLoading();
console.log('Failed to get services:', error);
}
});
clearTimeout(serviceTimeout);
}, 4000);
},
// Get characteristics from a service
getDeviceCharacteristics() {
var self = this;
let {
services: serviceList,
currentServiceIndex: serviceIndex,
hasWriteCharacteristic: write,
hasReadCharacteristic: read,
hasNotifyCharacteristic: notify
} = self;
let { deviceInfo } = self.bluetoothStore;
uni.getBLEDeviceCharacteristics({
deviceId: deviceInfo.deviceId,
serviceId: serviceList[serviceIndex].uuid,
success (res) {
for (var i = 0; i < res.characteristics.length; ++i) {
var properties = res.characteristics[i].properties;
var item = res.characteristics[i].uuid;
if (!notify && properties.notify) {
deviceInfo.notifyCharaterId = item;
deviceInfo.notifyServiceId = serviceList[serviceIndex].uuid;
self.$store.commit('updateDeviceInfo', deviceInfo);
notify = true;
}
if (!write && properties.write) {
deviceInfo.writeCharaterId = item;
deviceInfo.writeServiceId = serviceList[serviceIndex].uuid;
self.$store.commit('updateDeviceInfo', deviceInfo);
write = true;
}
if (!read && properties.read) {
deviceInfo.readCharaterId = item;
deviceInfo.readServiceId = serviceList[serviceIndex].uuid;
self.$store.commit('updateDeviceInfo', deviceInfo);
read = true;
}
}
if (!write || !notify || !read) {
serviceIndex++;
self.hasWriteCharacteristic = write;
self.hasReadCharacteristic = read;
self.hasNotifyCharacteristic = notify;
self.currentServiceIndex = serviceIndex;
if (serviceIndex == serviceList.length) {
uni.showModal({
title: 'Notice',
content: 'Required characteristics not found',
});
} else {
self.getDeviceCharacteristics();
}
}
},
fail (error) {
console.log('Failed to get characteristics:', error);
}
})
},
// Terminate Bluetooth connection
terminateConnection(deviceId) {
uni.closeBLEConnection({
deviceId: deviceId,
success (res) {
console.log('Connection closed:', res);
},
fail(error) {
console.log('Failed to close connection:', error);
}
});
},
// Initialize Bluetooth adapter
initializeBluetooth() {
const self = this;
self.closeBluetoothAdapter();
uni.openBluetoothAdapter({
success (res) {
console.log('Bluetooth adapter initialized');
},
fail (error) {
uni.showToast({
title:'Failed to initialize Bluetooth, please enable Bluetooth',
icon: "none"
});
console.log('Failed to initialize Bluetooth:', error);
}
});
},
// Stop Bluetooth device discovery
stopScanning() {
uni.stopBluetoothDevicesDiscovery({
success: (res) => {
console.log('Stopped scanning:', res);
},
fail: (error) => {
console.log('Failed to stop scanning:', error);
}
});
},
// Close Bluetooth adapter
closeBluetoothAdapter() {
uni.closeBluetoothAdapter({
success (res) {
console.log('Bluetooth adapter closed');
},
fail (error) {
console.log('Failed to close Bluetooth adapter:', error);
}
});
}
}
Permission Management
Create a permission.js file to handle device permissions:
/**
* Permission management module for Android and iOS
* Handles permission checks, opening permission settings, and system service status
*/
var isIosDevice;
// #ifdef APP-PLUS
isIosDevice = (plus.os.name == "iOS");
// #endif
// Check iOS push notification permission
function checkIosPushPermission() {
var result = false;
var UIApplication = plus.ios.import("UIApplication");
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute("types");
if (enabledTypes == 0) {
console.log("Push notifications not enabled");
} else {
result = true;
console.log("Push notifications enabled");
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
console.log("Push notifications not enabled");
} else {
result = true;
console.log("Push notifications enabled");
}
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
// Check iOS location permission
function checkIosLocationPermission() {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
var status = cllocationManger.authorizationStatus();
result = (status != 2);
console.log("Location permission status:", result);
plus.ios.deleteObject(cllocationManger);
return result;
}
// Check iOS microphone permission
function checkIosMicrophonePermission() {
var result = false;
var avaudiosession = plus.ios.import("AVAudioSession");
var avaudio = avaudiosession.sharedInstance();
var permissionStatus = avaudio.recordPermission();
if (permissionStatus == 1684369017 || permissionStatus == 1970168948) {
console.log("Microphone permission not enabled");
} else {
result = true;
console.log("Microphone permission enabled");
}
plus.ios.deleteObject(avaudiosession);
return result;
}
// Check iOS camera permission
function checkIosCameraPermission() {
var result = false;
var AVCaptureDevice = plus.ios.import("AVCaptureDevice");
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
if (authStatus == 3) {
result = true;
console.log("Camera permission enabled");
} else {
console.log("Camera permission not enabled");
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
// Check iOS photo library permission
function checkIosPhotoLibraryPermission() {
var result = false;
var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
var authStatus = PHPhotoLibrary.authorizationStatus();
if (authStatus == 3) {
result = true;
console.log("Photo library permission enabled");
} else {
console.log("Photo library permission not enabled");
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
// Check iOS contacts permission
function checkIosContactsPermission() {
var result = false;
var CNContactStore = plus.ios.import("CNContactStore");
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus == 3) {
result = true;
console.log("Contacts permission enabled");
} else {
console.log("Contacts permission not enabled");
}
plus.ios.deleteObject(CNContactStore);
return result;
}
// Check iOS calendar permission
function checkIosCalendarPermission() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = true;
console.log("Calendar permission enabled");
} else {
console.log("Calendar permission not enabled");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// Check iOS notes permission
function checkIosNotesPermission() {
var result = false;
var EKEventStore = plus.ios.import("EKEventStore");
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = true;
console.log("Notes permission enabled");
} else {
console.log("Notes permission not enabled");
}
plus.ios.deleteObject(EKEventStore);
return result;
}
// Android permission request
function requestAndroidPermission(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID],
function(resultObj) {
var result = 0;
// Check granted permissions
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('Granted permission:', grantedPermission);
result = 1;
}
// Check denied permissions
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPermission = resultObj.deniedPresent[i];
console.log('Denied permission:', deniedPermission);
result = 0;
}
// Check permanently denied permissions
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var permanentlyDeniedPermission = resultObj.deniedAlways[i];
console.log('Permanently denied permission:', permanentlyDeniedPermission);
result = -1;
}
resolve(result);
},
function(error) {
console.log('Permission request error:', error.code, error.message);
resolve({
code: error.code,
message: error.message
});
}
);
});
}
// Generic permission checker based on type
function checkIosPermission(permissionType) {
switch(permissionType) {
case "location":
return checkIosLocationPermission();
case "camera":
return checkIosCameraPermission();
case "photoLibrary":
return checkIosPhotoLibraryPermission();
case "microphone":
return checkIosMicrophonePermission();
case "push":
return checkIosPushPermission();
case "contacts":
return checkIosContactsPermission();
case "calendar":
return checkIosCalendarPermission();
case "notes":
return checkIosNotesPermission();
default:
return false;
}
}
// Navigate to app permission settings
function openAppPermissionSettings() {
if (isIosDevice) {
var UIApplication = plus.ios.import("UIApplication");
var application = UIApplication.sharedApplication();
var NSURL = plus.ios.import("NSURL");
var settingsURL = NSURL.URLWithString("app-settings:");
application.openURL(settingsURL);
plus.ios.deleteObject(settingsURL);
plus.ios.deleteObject(NSURL);
plus.ios.deleteObject(application);
} else {
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var Uri = plus.android.importClass("android.net.Uri");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
// Check if system location services are enabled
function checkSystemLocationServices() {
if (isIosDevice) {
var result = false;
var cllocationManger = plus.ios.import("CLLocationManager");
result = cllocationManger.locationServicesEnabled();
console.log("System location services:", result);
plus.ios.deleteObject(cllocationManger);
return result;
} else {
var context = plus.android.importClass("android.content.Context");
var locationManager = plus.android.importClass("android.location.LocationManager");
var main = plus.android.runtimeMainActivity();
var locationService = main.getSystemService(context.LOCATION_SERVICE);
var result = locationService.isProviderEnabled(locationManager.GPS_PROVIDER);
console.log("System location services:", result);
return result;
}
}
module.exports = {
checkIosPermission: checkIosPermission,
requestAndroidPermission: requestAndroidPermission,
checkSystemLocationServices: checkSystemLocationServices,
openAppPermissionSettings: openAppPermissionSettings
};
Handling Permission Denial
When location permission is denied, prompt the user to navigate to settings:
// Request location permission if denied
requestLocationPermission() {
if (this.appPermissionStatus.locationAuthorized === 'denied' ||
this.appPermissionStatus.locationAuthorized === 'not determined') {
uni.showModal({
title: "Location Access Required",
content: "Location permission is needed for Bluetooth discovery. Please enable it in settings.",
confirmText: "Open Settings",
success (response) {
if (response.confirm) {
permissionModule.openAppPermissionSettings();
}
}
});
}
},