Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom use of iOS Bluetooth button #1

Open
merri-ment opened this issue Sep 19, 2024 · 1 comment
Open

Custom use of iOS Bluetooth button #1

merri-ment opened this issue Sep 19, 2024 · 1 comment

Comments

@merri-ment
Copy link

你好!,

Greetings from New Zealand, I am hoping you can assist me.
I am developing an iOS Application that requires a remote bluetooth button.

I purchased this bluetooth button from Temu;
https://www.temu.com/goods.html?_bg_fs=1&goods_id=601099537315419

I inspected the bluetooth device and found a link to your company.
The Application i am developing uses Swift to connect to the bluetooth button, name "ATG-SJL".
I can pair successfully with it, but i am struggling to capture pressed events from the device.

Is it possible to use your tech to communicate with custom Applications?
or just the native iOS Camera application?

After i pair the bluetooth button, i see the native Volume UI appear and disappear.
Is iOS intercepting the keypress events on a lower level and routing them somewhere else?

Here is my swift code, "processInputReport" is never logged to the xcode console upon button press.

import Foundation
import Capacitor
import CoreBluetooth

@objc(BluetoothButtonPlugin)
public class BluetoothButtonPlugin: CAPPlugin, CBCentralManagerDelegate, CBPeripheralDelegate {
private var centralManager: CBCentralManager!
private var connectedPeripheral: CBPeripheral?
private let deviceName = "ATG-SJL"
private let deviceUUID = "9303D0FF-69DE-0137-0F69-EA4BC58D46C0"
private var ae42Characteristic: CBCharacteristic?
private var ae41Characteristic: CBCharacteristic? // Added AE41 characteristic
private var inputBuffer: [Data] = []
private var readTimer: Timer?

override public func load() {
    centralManager = CBCentralManager(delegate: self, queue: nil)
    print("BluetoothButtonPlugin loaded")
}

@objc func connect(_ call: CAPPluginCall) {
    print("Attempting to connect to ATG-SJL")
    if centralManager.state == .poweredOn {
        centralManager.scanForPeripherals(withServices: nil, options: nil)
        call.resolve()
    } else {
        call.reject("Bluetooth is not powered on")
    }
}

public func centralManagerDidUpdateState(_ central: CBCentralManager) {
    print("Bluetooth state updated: \(central.state.rawValue)")
    if central.state == .poweredOn {
        notifyListeners("bluetoothReady", data: nil)
        centralManager.scanForPeripherals(withServices: nil, options: nil)
    }
}

public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    print("Discovered peripheral: \(peripheral.name ?? "Unknown"), ID: \(peripheral.identifier.uuidString)")
    
    if peripheral.name == deviceName || peripheral.identifier.uuidString == deviceUUID {
        print("Found ATG-SJL device")
        connectedPeripheral = peripheral
        centralManager.stopScan()
        centralManager.connect(peripheral, options: nil)
    }
}

public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    print("Connected to device: \(peripheral.name ?? "Unknown")")
    peripheral.delegate = self
    peripheral.discoverServices(nil)
    notifyListeners("deviceConnected", data: ["name": peripheral.name ?? ""])
}

public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let services = peripheral.services else { return }
    print("Discovered \(services.count) services")
    for service in services {
        print("Service: \(service.uuid)")
        peripheral.discoverCharacteristics(nil, for: service)
    }
}

public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    guard let characteristics = service.characteristics else { return }
    print("Discovered \(characteristics.count) characteristics for service: \(service.uuid)")
    
    for characteristic in characteristics {
        print("Characteristic: \(characteristic.uuid), Properties: \(characteristic.properties.rawValue)")
        
        if characteristic.uuid.uuidString == "AE42" {
            ae42Characteristic = characteristic
            peripheral.setNotifyValue(true, for: characteristic)
            print("Notifications enabled for AE42 characteristic")
        }
        
        if characteristic.uuid.uuidString == "AE41" {
            ae41Characteristic = characteristic
            peripheral.setNotifyValue(true, for: characteristic) // Enable notifications for AE41
            print("Notifications enabled for AE41 characteristic")
        }
    }
}

public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        print("Error changing notification state for \(characteristic.uuid): \(error.localizedDescription)")
    } else {
        print("Notification state updated for characteristic: \(characteristic.uuid), isNotifying: \(characteristic.isNotifying)")
    }
}

public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if let error = error {
        print("Error updating value for characteristic \(characteristic.uuid): \(error.localizedDescription)")
        return
    }
    
    guard let value = characteristic.value else {
        print("No value received for characteristic \(characteristic.uuid)")
        return
    }
    
    print("Value received for characteristic: \(characteristic.uuid), value: \(value.hexEncodedString())")
    
    if characteristic.uuid.uuidString == "AE42" {
        processInputReport(value)
    }
    
    if characteristic.uuid.uuidString == "AE41" {
        print("Value received for AE41: \(value.hexEncodedString())") // Log AE41 data
    }
}

private func processInputReport(_ data: Data) {
    print("Processing input report: \(data.hexEncodedString())")
    
    inputBuffer.append(data)
    
    print("Input buffer: \(inputBuffer.map { $0.hexEncodedString() })")
    
    // Check for button interactions like click, double click, long press
    if checkForSingleClick() {
        handleClick()
        inputBuffer.removeAll()
    } else if checkForDoubleClick() {
        handleDoubleClick()
        inputBuffer.removeAll()
    } else if checkForLongPress() {
        handleLongPress()
        inputBuffer.removeAll()
    }

    if inputBuffer.count > 20 {
        inputBuffer.removeAll()
    }
}

private func checkForSingleClick() -> Bool {
    let pattern: [UInt8] = [0x07, 0x06, 0x70, 0x07]
    return inputBuffer.contains { data in
        guard data.count >= pattern.count else { return false }
        return pattern.enumerated().allSatisfy { data[$0.offset] == $0.element }
    }
}

private func checkForDoubleClick() -> Bool {
    let pattern1: [UInt8] = [0x07, 0x07, 0x70, 0x07]
    let pattern2: [UInt8] = [0x00, 0x07, 0x70, 0x07]
    return inputBuffer.contains { data in
        guard data.count >= pattern1.count else { return false }
        return pattern1.enumerated().allSatisfy { data[$0.offset] == $0.element }
    } && inputBuffer.contains { data in
        guard data.count >= pattern2.count else { return false }
        return pattern2.enumerated().allSatisfy { data[$0.offset] == $0.element }
    }
}

private func checkForLongPress() -> Bool {
    let pressPattern: [UInt8] = [0x01, 0x00, 0x00]
    let releasePattern: [UInt8] = [0x00, 0x00, 0x00]
    return inputBuffer.contains { data in
        guard data.count >= pressPattern.count else { return false }
        return pressPattern.enumerated().allSatisfy { data[$0.offset] == $0.element }
    } && inputBuffer.contains { data in
        guard data.count >= releasePattern.count else { return false }
        return releasePattern.enumerated().allSatisfy { data[$0.offset] == $0.element }
    }
}

private func handleClick() {
    print("buttonSingleClick")
    notifyListeners("buttonSingleClick", data: [:])
}

private func handleDoubleClick() {
    print("buttonDoubleClick")
    notifyListeners("buttonDoubleClick", data: [:])
}

private func handleLongPress() {
    print("buttonLongPress")
    notifyListeners("buttonLongPress", data: [:])
}

@objc func disconnect(_ call: CAPPluginCall) {
    if let peripheral = connectedPeripheral {
        centralManager.cancelPeripheralConnection(peripheral)
        readTimer?.invalidate()
        call.resolve(["message": "Disconnected from ATG-SJL"])
    } else {
        call.reject("No device connected")
    }
}

@objc func isConnected(_ call: CAPPluginCall) {
    call.resolve(["connected": connectedPeripheral != nil])
}

}

extension Data {
func hexEncodedString() -> String {
return map { String(format: "%02hhx", $0) }.joined()
}
}

Here is my log;

Discovered peripheral: ATG-SJL, ID: 9303D0FF-69DE-0137-0F69-EA4BC58D46C0
Found ATG-SJL device
Connected to device: ATG-SJL
Discovered 3 services
Service: Device Information
Service: Battery
Service: AE40
Discovered 9 characteristics for service: Device Information
Characteristic: Manufacturer Name String, Properties: 2
Characteristic: Model Number String, Properties: 2
Characteristic: Serial Number String, Properties: 2
Characteristic: Hardware Revision String, Properties: 2
Characteristic: Firmware Revision String, Properties: 2
Characteristic: Software Revision String, Properties: 2
Characteristic: System ID, Properties: 2
Characteristic: IEEE Regulatory Certification, Properties: 2
Characteristic: PnP ID, Properties: 2
Discovered 1 characteristics for service: Battery
Characteristic: Battery Level, Properties: 18
Discovered 2 characteristics for service: AE40
Characteristic: AE41, Properties: 4
Notifications enabled for AE41 characteristic
Characteristic: AE42, Properties: 16
Notifications enabled for AE42 characteristic
Error changing notification state for AE41: The request is not supported.
Notification state updated for characteristic: AE42, isNotifying: true

Thanks in advance.

If you could supply any information,
it would be much appreciated!

@merri-ment
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant