-
-
Save conath/c606d95d58bbcb50e9715864eeeecf07 to your computer and use it in GitHub Desktop.
import Foundation | |
extension Data { | |
init?(hexString: String) { | |
let len = hexString.count / 2 | |
var data = Data(capacity: len) | |
for i in 0..<len { | |
let j = hexString.index(hexString.startIndex, offsetBy: i*2) | |
let k = hexString.index(j, offsetBy: 2) | |
let bytes = hexString[j..<k] | |
if var num = UInt8(bytes, radix: 16) { | |
data.append(&num, count: 1) | |
} else { | |
return nil | |
} | |
} | |
self = data | |
} | |
} |
import CoreBluetooth | |
import os | |
class MPBluetoothController: NSObject { | |
var shouldReconnect = true | |
private(set) var canSendData = false | |
private var peripheralManager: CBPeripheralManager! | |
private var characteristic: CBMutableCharacteristic? | |
private var connectedCentral: CBCentral? | |
override init() { | |
super.init() | |
peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) | |
} | |
// MARK: - Helper Methods | |
private func setupPeripheral() { | |
// Build our service. | |
// MARK: Generic Access Service | |
let genericAccessService = CBMutableService(type: CBUUID(string: "1800"), primary: false) | |
let appearenceUUID = CBUUID(string: "2A01") | |
let appearanceValue = Data(NSData(bytes: [0xC103] as [UInt16], length: 2)) | |
let appearenceCharacteristic = CBMutableCharacteristic(type: appearenceUUID, properties: [.read], value: appearanceValue, permissions: [.readable]) | |
genericAccessService.characteristics = [appearenceCharacteristic] | |
peripheralManager.add(genericAccessService) | |
// MARK: Device Information Service | |
let deviceInformationService = CBMutableService(type: CBUUID(string: "180A"), primary: false) | |
let pnpUUID = CBUUID(string: "2A50") | |
let pnpValue = Data(NSData(bytes: [0x00, 0x4700, 0xFFFF, 0xFFFF] as [UInt16], length: 8)) | |
let pnpCharacteristic = CBMutableCharacteristic(type: pnpUUID, properties: [.read], value: pnpValue, permissions: [.readable]) | |
deviceInformationService.characteristics = [pnpCharacteristic] | |
peripheralManager.add(deviceInformationService) | |
// MARK: Human Interface Device Service | |
// See https://www.bluetooth.com/xml-viewer/?src=https://www.bluetooth.com/wp-content/uploads/Sitecore-Media-Library/Gatt/Xml/Services/org.bluetooth.service.human_interface_device.xml | |
// Reference https://docs.silabs.com/bluetooth/latest/code-examples/applications/ble-hid-keyboard#gatt-database-for-keyboard-example | |
let hidService = CBMutableService(type: MPHIDService.serviceUUID, primary: true) | |
let hidInfoUUID = CBUUID(string: "2A4A") | |
let hidInfoValue = Data(NSData(bytes: [0x0111, 0x0002] as [UInt16], length: 4)) | |
let hidInfoCharacteristic = CBMutableCharacteristic(type: hidInfoUUID, properties: [.read], value: hidInfoValue, permissions: [.readable]) | |
let protocolModeUUID = CBUUID(string: "2A4E") | |
let protocolModeValue = Data(NSData(bytes: [0x00] as [UInt8], length: 1)) // report mode 00 is boot, 01 is report | |
let protocolModeCharacteristic = CBMutableCharacteristic(type: protocolModeUUID, properties: [.read], value: protocolModeValue, permissions: [.readable]) | |
let reportMapUUID = CBUUID(string: "2A4B") | |
let reportMapData = Data(MPHIDService.hidReportDescriptor) | |
let reportMapCharacteristic = CBMutableCharacteristic(type: reportMapUUID, properties: [.read], value: reportMapData, permissions: [.readable, .readEncryptionRequired]) | |
// let hidControlPointUUID = CBUUID(string: "2A4C") | |
// let hidControlPointCharacteristic = CBMutableCharacteristic(type: hidControlPointUUID, properties: [.writeWithoutResponse], value: nil, permissions: [.writeable]) | |
let reportUUID = CBUUID(string: "2A22")//"2A4D") would be report mode | |
let reportCharacteristic = CBMutableCharacteristic(type: reportUUID, properties: [.read, .notify], value: nil, permissions: [.readable]) | |
let reportDescriptorUUID = CBUUID(string: "2908") | |
let reportDescriptorValue = Data(NSData(bytes: [0x0001] as [UInt16], length: 2)) | |
let reportDescriptorCharacteristic = CBMutableCharacteristic(type: reportDescriptorUUID, properties: [.read], value: reportDescriptorValue, permissions: [.readable, .readEncryptionRequired]) | |
hidService.characteristics = [hidInfoCharacteristic, protocolModeCharacteristic, reportMapCharacteristic, reportCharacteristic, reportDescriptorCharacteristic] | |
// And add it to the peripheral manager. | |
peripheralManager.add(hidService) | |
// Save the characteristic for later. | |
self.characteristic = reportCharacteristic | |
let serviceUUIDs = [ | |
CBUUID(string: "1800"), | |
CBUUID(string: "180A"), | |
MPHIDService.serviceUUID | |
] | |
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: serviceUUIDs]) | |
print(serviceUUIDs) | |
// MARK: WORKING CODE but with limited functionality | |
/* | |
let characteristic = CBMutableCharacteristic(type: MPHIDService.characteristicUUID, | |
properties: [.notify, .writeWithoutResponse], | |
value: nil, | |
permissions: [.readable, .writeable]) | |
// Create a service from the characteristic. | |
let service = CBMutableService(type: MPHIDService.serviceUUID, primary: true) | |
// Add the characteristic to the service. | |
service.characteristics = [characteristic] | |
// And add it to the peripheral manager. | |
peripheralManager.add(service) | |
// Save the characteristic for later. | |
self.characteristic = characteristic | |
peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [MPHIDService.serviceUUID]]) | |
*/ | |
} | |
func sendKeystroke(_ keyStroke: MPHIDService.KeyStroke, _ keyState: MPHIDService.KeyState) { | |
guard let characteristic = characteristic else { | |
return | |
} | |
var didSend = true | |
while didSend { | |
// Get the data we need to send | |
let data = MPHIDService.getReportDataFor(keyStroke: keyStroke, keyState: keyState) | |
// Work out how big it can be | |
let amountToSend = data.count | |
if let mtu = connectedCentral?.maximumUpdateValueLength { | |
guard mtu > amountToSend else { | |
os_log("Central can't receive \(amountToSend) bytes at a time - this is not handled") | |
// todo disconnect? | |
fatalError("Central can't receive \(amountToSend) bytes at a time - this is not handled") | |
} | |
} | |
// TODO key up/down - rest of this function is not properly implemented | |
//let chunk = MPHIDService.Data(keyStroke: keyStroke, state: keyState).stringRepresentation.data(using: .utf8)! | |
// Send it | |
didSend = peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: nil) | |
// If it didn't work, drop out and wait for the callback | |
if !didSend { | |
print("Didn't send") | |
return | |
} | |
// let stringFromData = String(data: data, encoding: .ascii) | |
// os_log("Sent %d bytes: %s", data.count, String(describing: stringFromData)) | |
} | |
} | |
} | |
// MARK: Implementation of CBPeripheralManagerDelegate | |
extension MPBluetoothController: CBPeripheralManagerDelegate { | |
/* | |
* Required protocol method. A full app should take care of all the possible states, | |
* but we're just waiting for to know when the CBPeripheralManager is ready | |
* | |
* Starting from iOS 13.0, if the state is CBManagerStateUnauthorized, you | |
* are also required to check for the authorization state of the peripheral to ensure that | |
* your app is allowed to use bluetooth | |
*/ | |
internal func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { | |
// TODO tell UI if bluetooth is off | |
// advertisingSwitch.isEnabled = peripheral.state == .poweredOn | |
switch peripheral.state { | |
case .poweredOn: | |
// ... so start working with the peripheral | |
os_log("CBManager is powered on") | |
setupPeripheral() | |
case .poweredOff: | |
os_log("CBManager is not powered on") | |
// TODO deal with all the states accordingly | |
return | |
case .resetting: | |
os_log("CBManager is resetting") | |
// TODO deal with all the states accordingly | |
return | |
case .unauthorized: | |
// TODO deal with all the states accordingly | |
switch CBPeripheralManager.authorization { | |
case .denied: | |
os_log("You are not authorized to use Bluetooth") | |
case .restricted: | |
os_log("Bluetooth is restricted") | |
default: | |
os_log("Unexpected authorization") | |
} | |
return | |
case .unknown: | |
os_log("CBManager state is unknown") | |
// TODO deal with all the states accordingly | |
return | |
case .unsupported: | |
os_log("Bluetooth is not supported on this device") | |
// TODO deal with all the states accordingly | |
return | |
@unknown default: | |
os_log("A previously unknown peripheral manager state occurred") | |
// TODO deal with yet unknown cases that might occur in the future | |
return | |
} | |
} | |
/* | |
* Catch when someone subscribes to our characteristic, then start sending them data | |
*/ | |
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { | |
os_log("Central subscribed to characteristic") | |
// save central | |
connectedCentral = central | |
// Start sending | |
canSendData = true | |
} | |
/* | |
* Recognize when the central unsubscribes | |
*/ | |
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) { | |
os_log("Central unsubscribed from characteristic") | |
connectedCentral = nil | |
} | |
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) { | |
if let error = error { | |
print(String(describing: error)) | |
} else { | |
print("Did start advertising.") | |
} | |
} | |
/* | |
* This callback comes in when the PeripheralManager is ready to send the next chunk of data. | |
* This is to ensure that packets will arrive in the order they are sent | |
*/ | |
// func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { | |
// TODO do I need this? | |
// } | |
/* | |
* This callback comes in when the PeripheralManager received write to characteristics | |
*/ | |
// func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { | |
// for aRequest in requests { | |
// guard let requestValue = aRequest.value, | |
// let stringFromData = String(data: requestValue, encoding: .utf8) else { | |
// continue | |
// } | |
// | |
// os_log("Received write request of %d bytes: %s", requestValue.count, stringFromData) | |
// self.textView.text = stringFromData | |
// } | |
// } | |
} |
import Foundation | |
import CoreBluetooth | |
struct MPHIDService { | |
static let minRSSI = -70 | |
static let serviceUUID = CBUUID(string: "1812") | |
static let hidReportDescriptor = NSData(bytes: | |
[ | |
0x05, 0x01, // Usage Page (Generic Desktop) | |
0x09, 0x06, // Usage (Keyboard) | |
0xA1, 0x01, // Collection (Application) | |
0x05, 0x07, // Usage Page (Keyboard) | |
0x19, 0xE0, // Usage Minimum (Keyboard LeftControl) | |
0x29, 0xE7, // Usage Maximum (Keyboard Right GUI) | |
0x15, 0x00, // Logical Minimum (0) | |
0x25, 0x01, // Logical Maximum (1) | |
0x75, 0x01, // Report Size (1) | |
0x95, 0x08, // Report Count (9) | |
0x81, 0x02, // Input (Data, Variable, Absolute) Modifier byte | |
0x95, 0x01, // Report Size (1) | |
0x75, 0x08, // Report Count (8) | |
0x81, 0x03, // Input (Constant) Reserved byte | |
0x95, 0x06, // Report Count (6) | |
0x75, 0x08, // Report Size (8) | |
0x15, 0x00, // Logical Minimum (0) | |
0x25, 0x65, // Logical Maximum (101) | |
0x05, 0x07, // Usage Page (Key Codes) | |
0x05, 0x01, // Usage Minimum (Reserved (no event indicated)) | |
0x05, 0x01, // Usage Maximum (Keyboard Application) | |
0x05, 0x01, // Input (Data,Array) Key arrays (6 bytes) | |
0xC0 // End Collection | |
] as [UInt8], length: 45) | |
static func getReportDataFor(keyStroke: KeyStroke, keyState: KeyState) -> Data { | |
var chunk: UInt8! | |
switch keyState { | |
case .Down: | |
switch keyStroke { | |
case .Left: | |
chunk = 0x7F | |
case .Right: | |
chunk = 0x59 | |
} | |
default: | |
chunk = 0x00 | |
} | |
let values = [0x00, 0x00, chunk, 0x00, 0x00, 0x00, 0x00, 0x00] as [UInt8] | |
let data = Data(NSData(bytes: values, length: 8)) | |
return data | |
} | |
enum KeyStroke: UInt16 { | |
case Left = 79 | |
case Right = 89 | |
static let lengthBytes = 2 | |
} | |
enum KeyState: String { | |
case Up = "up" | |
case Down = "do" | |
case None = " " | |
} | |
} |
Thank you for the pointer @Taati89 !
I downloaded that app and attempted to connect to a Window PC, but was unsuccessful. In the logs of the app it seems like they do try to advertise a HID service but does not log what they are using exactly.
From the look of it it seems like a cross platform app so maybe the answer can be found in some flutter or other language bluetooth plugin.
You could also try running their app on iOS while connected to a Mac and capturing a system log. It could reveal additional information.
I tried your code and encountered an issue when calling peripheralManager.add(genericAccessService). The function public func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) gets called and receives the error "The specified UUID is not allowed for this operation."
@Taati89 exactly! Like I stated in the Gist description, the HID service UUID is blocked by iOS.
@conath
I tried changing the UUID from 1812 to 00001812-0000-1000-8000-00805F9B34FB and it successfully added the service.
Then, when I called peripheralManager.startAdvertising with UUID 00001812-0000-1000-8000-00805F9B34FB, the computer or mobile device would trigger the function public func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) upon connection.
The requests would have request.characteristic.uuid == Boot Keyboard Input Report (0x2A22) and request.characteristic.uuid == Report (0x2A4D) respectively.
After I responded with peripheralManager?.respond(to: request, withResult: .success), it seemed that my app was recognized as an input device.
Additionally, I tried connecting to an Android device. When connected, this function is called:
public func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
When I called peripheralManager.updateValue, it could type as expected.
I wonder why on Android it triggers didSubscribeTo but on a Windows computer it calls didReceiveRead.
@Taati89, I tried as you mentioned and changed the UUID from 1812 to 00001812-0000-1000-8000-00805F9B34FB on Mac. It got paired, but didReceiveRead or didSubscribeTo didn't get called. The last delegate method that got called is peripheralManagerDidStartAdvertising. Did you make any progress on this? There is an app called Blue Touch in the App Store that is working properly with this. I would highly appreciate any updates.
@Taati89, I tried as you mentioned and changed the UUID from 1812 to 00001812-0000-1000-8000-00805F9B34FB on Mac. It got paired, but didReceiveRead or didSubscribeTo didn't get called. The last delegate method that got called is peripheralManagerDidStartAdvertising. Did you make any progress on this? There is an app called Blue Touch in the App Store that is working properly with this. I would highly appreciate any updates.
I tested it with Windows, and I can now send typing data, but I still have an issue with reconnecting after killing the app.
One difference from the code is this:
let reportDescriptorUUID = CBUUID(string: "2908")
let reportDescriptorValue = Data(NSData(bytes: [0x0001] as [UInt16], length: 2))
let reportDescriptorCharacteristic = CBMutableCharacteristic(type: reportDescriptorUUID, properties: [.read], value: reportDescriptorValue, permissions: [.readable, .readEncryptionRequired])
hidService.characteristics = [hidInfoCharacteristic, protocolModeCharacteristic, reportMapCharacteristic, reportCharacteristic, reportDescriptorCharacteristic]
2908 is the descriptor, and it needs to be added to the reportCharacteristic like this:
let cccd = CBMutableDescriptor(type: CBUUID(string: "2908"), value: Data(NSData(bytes: [0x00, 0x01] as [UInt8], length: 2)))
reportCharacteristic.descriptors = [cccd]
And I use the hidReportDescriptor as follows, https://www.usb.org/sites/default/files/documents/hid1_11.pdf Page 79: E.6 Report Descriptor (Keyboard)
NSData(bytes:
[
0x05, 0x01, // USAGE_PAGE
0x09, 0x06, // USAGE
0xA1, 0x01, // COLLECTION
0x05, 0x07, // USAGE_PAGE
0x19, 0xE0, // USAGE_MINIMUM
0x29, 0xE7, // USAGE_MAXIMUM
0x15, 0x00, // LOGICAL_MINIMUM
0x25, 0x01, // LOGICAL_MAXIMUM
0x75, 0x01, // REPORT_SIZE 1 byte (Modifier)
0x95, 0x08, // REPORT_COUNT
0x81, 0x02, // INPUT Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position
0x95, 0x01, // REPORT_COUNT 1 byte (Reserved)
0x75, 0x08, // REPORT_SIZE
0x81, 0x01, // INPUT Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
0x95, 0x05, // REPORT_COUNT 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
0x75, 0x01, // REPORT_SIZE
0x05, 0x08, // USAGE_PAGE LEDs
0x19, 0x01, // USAGE_MINIMUM Num Lock
0x29, 0x05, // USAGE_MAXIMUM Kana
0x91, 0x02, // OUTPUT Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
0x95, 0x01, // REPORT_COUNT 3 bits (Padding)
0x75, 0x03, // REPORT_SIZE
0x91, 0x01, // OUTPUT Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
0x95, 0x06, // REPORT_COUNT 6 bytes (Keys)
0x75, 0x08, // REPORT_SIZE
0x15, 0x00, // LOGICAL_MINIMUM
0x25, 0x65, // LOGICAL_MAXIMUM 101 keys
0x05, 0x07, // USAGE_PAGE Kbrd/Keypad
0x19, 0x00, // USAGE_MINIMUM
0x29, 0x65, // USAGE_MAXIMUM
0x81, 0x00, // INPUT Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
0xC0, // END_COLLECTION
] as [UInt8], length: 63)
In my current tests, it works when I press 'Forget Device' and disconnect from both the computer and the phone first, as if they had never connected before, then start connecting from the computer. (I still have an issue with reconnecting after killing the app.)
@Taati89 , I highly appreciate your support. It's working for me on Mac.
@Taati89, I tried as you mentioned and changed the UUID from 1812 to 00001812-0000-1000-8000-00805F9B34FB on Mac. It got paired, but didReceiveRead or didSubscribeTo didn't get called. The last delegate method that got called is peripheralManagerDidStartAdvertising. Did you make any progress on this? There is an app called Blue Touch in the App Store that is working properly with this. I would highly appreciate any updates.
I tested it with Windows, and I can now send typing data, but I still have an issue with reconnecting after killing the app.
One difference from the code is this:
let reportDescriptorUUID = CBUUID(string: "2908") let reportDescriptorValue = Data(NSData(bytes: [0x0001] as [UInt16], length: 2)) let reportDescriptorCharacteristic = CBMutableCharacteristic(type: reportDescriptorUUID, properties: [.read], value: reportDescriptorValue, permissions: [.readable, .readEncryptionRequired]) hidService.characteristics = [hidInfoCharacteristic, protocolModeCharacteristic, reportMapCharacteristic, reportCharacteristic, reportDescriptorCharacteristic]
2908 is the descriptor, and it needs to be added to the reportCharacteristic like this:
let cccd = CBMutableDescriptor(type: CBUUID(string: "2908"), value: Data(NSData(bytes: [0x00, 0x01] as [UInt8], length: 2))) reportCharacteristic.descriptors = [cccd]
And I use the hidReportDescriptor as follows, https://www.usb.org/sites/default/files/documents/hid1_11.pdf Page 79: E.6 Report Descriptor (Keyboard)
NSData(bytes: [ 0x05, 0x01, // USAGE_PAGE 0x09, 0x06, // USAGE 0xA1, 0x01, // COLLECTION 0x05, 0x07, // USAGE_PAGE 0x19, 0xE0, // USAGE_MINIMUM 0x29, 0xE7, // USAGE_MAXIMUM 0x15, 0x00, // LOGICAL_MINIMUM 0x25, 0x01, // LOGICAL_MAXIMUM 0x75, 0x01, // REPORT_SIZE 1 byte (Modifier) 0x95, 0x08, // REPORT_COUNT 0x81, 0x02, // INPUT Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position 0x95, 0x01, // REPORT_COUNT 1 byte (Reserved) 0x75, 0x08, // REPORT_SIZE 0x81, 0x01, // INPUT Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position 0x95, 0x05, // REPORT_COUNT 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana) 0x75, 0x01, // REPORT_SIZE 0x05, 0x08, // USAGE_PAGE LEDs 0x19, 0x01, // USAGE_MINIMUM Num Lock 0x29, 0x05, // USAGE_MAXIMUM Kana 0x91, 0x02, // OUTPUT Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile 0x95, 0x01, // REPORT_COUNT 3 bits (Padding) 0x75, 0x03, // REPORT_SIZE 0x91, 0x01, // OUTPUT Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile 0x95, 0x06, // REPORT_COUNT 6 bytes (Keys) 0x75, 0x08, // REPORT_SIZE 0x15, 0x00, // LOGICAL_MINIMUM 0x25, 0x65, // LOGICAL_MAXIMUM 101 keys 0x05, 0x07, // USAGE_PAGE Kbrd/Keypad 0x19, 0x00, // USAGE_MINIMUM 0x29, 0x65, // USAGE_MAXIMUM 0x81, 0x00, // INPUT Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position 0xC0, // END_COLLECTION ] as [UInt8], length: 63)
In my current tests, it works when I press 'Forget Device' and disconnect from both the computer and the phone first, as if they had never connected before, then start connecting from the computer. (I still have an issue with reconnecting after killing the app.)
My mac cannot find my iPhone after 「Did start advertising」I dont know where am i wrong , here's my code :
https://github.com/shaotianchi/TakaKeyboard
I found the Bluetouch app on iOS. It can function as a mouse and keyboard. I'm not sure how they use Bluetooth. Do you happen to know?