I’m making an attempt to implement background BLE (Bluetooth Low Power) scanning in an iOS app utilizing CoreBluetooth. When my app is within the foreground, I can efficiently scan and acquire BLE machine information. Nonetheless, when the app goes into the background or the telephone is locked, the scanning doesn’t operate as anticipated. The app doesn’t obtain BLE information within the background. I need to scan and ship the BLE to a server each 20 seconds when a location background replace is made. Right here is an summary of my setup:
The BluetoothManager:
import Basis
import CoreBluetooth
struct BluetoothDevice {
var peripheral: CBPeripheral
var rssi: NSNumber
var advertisementData: [String: Any]
var manufacturerData: Information?
var main: UInt16?
var minor: UInt16?
}
class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
@Revealed var discoveredDevices: [BluetoothDevice] = []
@Revealed var connectedPeripherals: [CBPeripheral: [CBService]] = [:] // To retailer companies for every peripheral
@Revealed var isScanning: Bool = false
personal var centralManager: CBCentralManager!
override init() {
tremendous.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
func startScanning() {
if !isScanning {
isScanning = true
discoveredDevices.removeAll()
centralManager.scanForPeripherals(withServices: nil)
// Cease scanning after 2 seconds
DispatchQueue.major.asyncAfter(deadline: .now() + 2) {
self.stopScanning()
}
}
}
func stopScanning() {
if isScanning {
centralManager.stopScan()
isScanning = false
print("Stopped scanning for gadgets")
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
startScanning()
} else {
print("Bluetooth will not be out there.")
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
// Extract producer information if out there
guard let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Information else {
return
}
// Assuming that is an iBeacon-like machine
let manufacturerDataHex = manufacturerData.hexEncodedString().lowercased()
// Filter: Examine if producer information accommodates "7e10ca7"
if manufacturerDataHex.accommodates("exampleHexNumber") {
var majorValue: UInt16? = nil
var minorValue: UInt16? = nil
// Examine if the information size is sufficient to comprise main and minor values
if manufacturerData.depend >= 24 {
// Extract main and minor values
let majorRange = 20..<22
let majorData = manufacturerData.subdata(in: majorRange)
majorValue = UInt16(bigEndian: majorData.withUnsafeBytes { $0.load(as: UInt16.self) })
let minorRange = 22..<24
let minorData = manufacturerData.subdata(in: minorRange)
minorValue = UInt16(bigEndian: minorData.withUnsafeBytes { $0.load(as: UInt16.self) })
// Examine if this machine is already in discoveredDevices
if let existingIndex = discoveredDevices.firstIndex(the place: { $0.peripheral.identifier == peripheral.identifier }) {
// Replace RSSI of the present machine
discoveredDevices[existingIndex].rssi = RSSI
} else {
// Add new machine if it is not already in discoveredDevices
let machine = BluetoothDevice(
peripheral: peripheral,
rssi: RSSI,
advertisementData: advertisementData,
manufacturerData: manufacturerData,
main: majorValue,
minor: minorValue
)
discoveredDevices.append(machine)
}
}
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
//print("Linked to (peripheral.title ?? "Unknown Machine")")
peripheral.discoverServices(nil)
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if let error = error {
print("Error discovering companies: (error.localizedDescription)")
return
}
if let companies = peripheral.companies {
DispatchQueue.major.async {
self.connectedPeripherals[peripheral] = companies // Retailer the found companies
self.updateDeviceList() // Notify that the record must be up to date
}
}
}
func updateDeviceList() {
objectWillChange.ship() // Notify the UI that the information has modified
}
// Helper operate to extract serial quantity (customized logic relying in your machine information)
func extractSerialNumber(from manufacturerData: Information?, advertisementData: [String: Any]) -> String {
// Instance logic: Attempt to extract from native title or parse particular a part of producer information
if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
return "SN-(localName)"
} else if let manufacturerData = manufacturerData {
// Instance: Assume the serial quantity is saved in bytes 3-8 of producer information (customise as per your machine)
let serialNumberData = manufacturerData.subdata(in: 2..<8)
return serialNumberData.hexEncodedString().uppercased()
} else {
return "Unknown"
}
}
}
extension Information {
/// Convert the producer information to a hex string for simpler studying
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}
And the Location Supervisor:
import CoreLocation
import Basis
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
personal var locationManager = CLLocationManager()
personal var bluetoothManager = BluetoothManager()
personal var lastSentTimestamp: Date? // Observe the final time information was despatched
@Revealed var locationStatus: CLAuthorizationStatus?
@Revealed var lastLocation: CLLocation?
@Revealed var lastSuccessfulRequestDate: Date? // New printed property for final profitable request
override init() {
tremendous.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
locationManager.showsBackgroundLocationIndicator = true
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
locationManager.startUpdatingLocation()
}
func locationManager(
_ supervisor: CLLocationManager,
didUpdateLocations areas: [CLLocation]
) {
guard let location = areas.final else { return }
lastLocation = location
// Examine if we should always ship information primarily based on the 20-second interval
if shouldSendLocationData() {
bluetoothManager.startScanning() // Begin Bluetooth scan
// Delay sending location information to permit BLE scan to finish
DispatchQueue.major.asyncAfter(deadline: .now() + 2) {
self.sendLocationData(location)
}
}
}
func locationManager(
_ supervisor: CLLocationManager,
didChangeAuthorization standing: CLAuthorizationStatus
) {
self.locationStatus = standing
}
personal func shouldSendLocationData() -> Bool {
if let lastSent = lastSentTimestamp {
return Date().timeIntervalSince(lastSent) >= 20
}
return true
}
personal func sendLocationData(_ location: CLLocation) {
// Guarantee we're respecting the interval examine
guard shouldSendLocationData() else { return }
// Replace the lastSentTimestamp after passing the interval examine
lastSentTimestamp = Date()
guard let url = URL(string: "https://exampleurl.com/submit") else {
print("Invalid URL")
return
}
let timestamp = Int(location.timestamp.timeIntervalSince1970)
let truncatedLatitude = location.coordinate.latitude
let truncatedLongitude = location.coordinate.longitude
// Gather BLE measurements with a single date for the BLE part
let bleMeasurements = bluetoothManager.discoveredDevices.map { machine -> [String: Any] in
var measurement: [String: Any] = ["rssi": device.rssi.intValue]
if let minor = machine.minor {
measurement["minor"] = minor
}
return measurement
}
// Create BLE information with one date and a number of measurements
let bleData: [String: Any] = [
"date": Int(Date().timeIntervalSince1970),
"measurements": bleMeasurements
]
// Create GNSS information
let gnssData: [String: Any] = [
"date": timestamp,
"data": [
"latitude": truncatedLatitude,
"longitude": truncatedLongitude
]
]
// Assemble the ultimate JSON dict with each GNSS and BLE information
let jsonDict: [String: Any] = [
"ble": [bleData], // Single BLE entry with one date and a number of measurements
"gnss": [gnssData]
]
// Convert information to JSON
guard let jsonData = attempt? JSONSerialization.information(withJSONObject: jsonDict, choices: .prettyPrinted) else {
print("Didn't serialize JSON")
return
}
// Print JSON as a debug output
if let jsonString = String(information: jsonData, encoding: .utf8) {
print("JSON Payload:n(jsonString)")
}
guard let username = KeychainHelper.load(key: "title"),
let password = KeychainHelper.load(key: "password") else {
print("Credentials not discovered")
return
}
let loginString = "(username):(password)"
guard let loginData = loginString.information(utilizing: .utf8) else {
print("Didn't encode credentials")
return
}
let base64LoginString = loginData.base64EncodedString()
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("utility/json", forHTTPHeaderField: "Content material-Sort")
request
.setValue(
"Primary (base64LoginString)",
forHTTPHeaderField: "Authorization"
)
request.httpBody = jsonData
let job = URLSession.shared.dataTask(
with: request
) {
information,
response,
error in
if let error = error {
print(
"Error sending location information: (error.localizedDescription)"
)
return
}
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
// Replace final profitable request date on success
DispatchQueue.major.async {
self.lastSuccessfulRequestDate = Date()
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone.present
let readableDate = dateFormatter.string(from: Date())
print(
"Location and BLE information despatched efficiently at (readableDate):"
)
print(
"Latitude: (truncatedLatitude), Longitude: (truncatedLongitude), Timestamp: (timestamp)"
)
} else if let httpResponse = response as? HTTPURLResponse {
print(
"Didn't ship location information. Standing code: (httpResponse.statusCode)"
)
}
}
job.resume()
}
}
My PLIST seems like:
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-le</string>
<string>fetch</string>
<string>processing</string>
<string>location</string>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
I’m nonetheless unable to scan BLE gadgets reliably when the app is within the background.
Glad if somebody may help right here!