├── BasicBrowser ├── bluetooth.html ├── Images.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── JSRequest.swift ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── PopUpPickerView.swift ├── ViewController.swift ├── WebBluetooth.swift ├── WebBluetoothManager.swift └── WebBluetooth.js ├── README.md ├── BasicBrowserTests ├── Info.plist └── BleBrowserTests.swift └── BleBrowser.xcodeproj └── project.pbxproj /BasicBrowser/bluetooth.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BasicBrowser/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BleBrowser 2 | 3 | Experimental implementation of the [Web Bluetooth](https://webbluetoothcg.github.io/web-bluetooth/) spec on WKWebView for IOS 4 | - Working, just enough to read values: 5 | - bluetooth:requestDevice 6 | - BluetoothRemoteGATTServer.connect 7 | - BluetoothRemoteGATTServer.getPrimaryService 8 | - BluetoothGATTService.getCharacteristic 9 | - BluetoothGATTCharacteristic.readValue 10 | - Todo: Everything else 11 | -------------------------------------------------------------------------------- /BasicBrowserTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BasicBrowserTests/BleBrowserTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicBrowserTests.swift 3 | // BasicBrowserTests 4 | // 5 | // Created by Stefan Arentz (Mozilla) on 2014-12-08. 6 | // Copyright (c) 2014 Stefan Arentz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class BasicBrowserTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /BasicBrowser/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "83.5x83.5", 66 | "scale" : "2x" 67 | } 68 | ], 69 | "info" : { 70 | "version" : 1, 71 | "author" : "xcode" 72 | } 73 | } -------------------------------------------------------------------------------- /BasicBrowser/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /BasicBrowser/JSRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSRequest.swift 3 | // BleBrowser 4 | // 5 | // Created by Paul Theriault on 15/03/2016. 6 | // Copyright © 2016 Stefan Arentz. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreBluetooth 11 | import WebKit 12 | 13 | class JSRequest{ 14 | 15 | var id:Int 16 | var type:String 17 | var data:[String:AnyObject] 18 | var webView:WKWebView 19 | var resolved:Bool = false 20 | 21 | var deviceId:String{ 22 | get{ 23 | return (data["deviceId"] ?? "") as! String 24 | } 25 | } 26 | var method:String { 27 | get{ 28 | return (data["method"] ?? "") as! String 29 | } 30 | } 31 | var args:[String]{ 32 | get{ 33 | return (data["args"] ?? [String]()) as! [String] 34 | } 35 | } 36 | 37 | var origin:String{ 38 | get{ 39 | if let URL = webView.URL{ 40 | return URL.scheme + ":" + URL.host! + ":" + String(URL.port) 41 | }else{ 42 | return "" 43 | } 44 | } 45 | } 46 | 47 | init(id:Int,type:String,data:[String:AnyObject],webView:WKWebView){ 48 | self.id = id 49 | self.type = type 50 | self.data = data 51 | self.webView = webView 52 | } 53 | 54 | func sendMessage(type:String, success:Bool, result:String, requestId:Int = -1){ 55 | if(self.resolved){ 56 | print("Warning: attempt to send a second response to the same message") 57 | return 58 | } 59 | let commandString = "recieveMessage('\(type)', \(success), '\(result)',\(requestId))" 60 | print("-->:",commandString) 61 | webView.evaluateJavaScript(commandString, completionHandler: nil) 62 | self.resolved = true 63 | } 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /BasicBrowser/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BasicBrowser 4 | // 5 | // Created by Stefan Arentz (Mozilla) on 2014-12-08. 6 | // Copyright (c) 2014 Stefan Arentz. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /BasicBrowser/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BasicBrowser/PopUpPickerView.swift: -------------------------------------------------------------------------------- 1 | // from https://github.com/tottokotkd/PopUpPickerView 2 | 3 | import UIKit 4 | 5 | class PopUpPickerView: UIView { 6 | var pickerView: UIPickerView! 7 | var pickerToolbar: UIToolbar! 8 | var toolbarItems: [UIBarButtonItem]! 9 | 10 | var delegate: PopUpPickerViewDelegate? { 11 | didSet { 12 | pickerView.delegate = delegate 13 | } 14 | } 15 | private var selectedRows: [Int]? 16 | 17 | // MARK: Initializer 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | initFunc() 22 | } 23 | required init(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder)! 25 | initFunc() 26 | } 27 | private func initFunc() { 28 | let screenSize = UIScreen.mainScreen().bounds.size 29 | self.backgroundColor = UIColor.blackColor() 30 | 31 | pickerToolbar = UIToolbar() 32 | pickerView = UIPickerView() 33 | toolbarItems = [] 34 | 35 | pickerToolbar.translucent = true 36 | pickerView.showsSelectionIndicator = true 37 | pickerView.backgroundColor = UIColor.whiteColor() 38 | 39 | self.bounds = CGRectMake(0, 0, screenSize.width, 260) 40 | self.frame = CGRectMake(0, screenSize.height, screenSize.width, 260) 41 | pickerToolbar.bounds = CGRectMake(0, 0, screenSize.width, 44) 42 | pickerToolbar.frame = CGRectMake(0, 0, screenSize.width, 44) 43 | pickerView.bounds = CGRectMake(0, 0, screenSize.width, 216) 44 | pickerView.frame = CGRectMake(0, 44, screenSize.width, 216) 45 | 46 | let space = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil) 47 | space.width = 12 48 | let cancelItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Cancel, target: self, action: "cancelPicker") 49 | let flexSpaceItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FlexibleSpace, target: self, action: nil) 50 | let doneButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action: Selector("endPicker")) 51 | toolbarItems! += [space, cancelItem, flexSpaceItem, doneButtonItem, space] 52 | 53 | pickerToolbar.setItems(toolbarItems, animated: false) 54 | self.addSubview(pickerToolbar) 55 | self.addSubview(pickerView) 56 | } 57 | func showPicker() { 58 | if selectedRows == nil { 59 | selectedRows = getSelectedRows() 60 | } 61 | let screenSize = UIScreen.mainScreen().bounds.size 62 | UIView.animateWithDuration(0.2) { 63 | self.frame = CGRectMake(0, screenSize.height - 260.0, screenSize.width, 260.0) 64 | } 65 | } 66 | func cancelPicker() { 67 | hidePicker() 68 | restoreSelectedRows() 69 | selectedRows = nil 70 | } 71 | func endPicker() { 72 | hidePicker() 73 | delegate?.pickerView?(pickerView, didSelect: getSelectedRows()) 74 | selectedRows = nil 75 | } 76 | 77 | func updatePicker() { 78 | pickerView.reloadAllComponents() 79 | } 80 | 81 | private func hidePicker() { 82 | let screenSize = UIScreen.mainScreen().bounds.size 83 | UIView.animateWithDuration(0.2) { 84 | self.frame = CGRectMake(0, screenSize.height, screenSize.width, 260.0) 85 | } 86 | } 87 | private func getSelectedRows() -> [Int] { 88 | var selectedRows = [Int]() 89 | for i in 0.. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /BasicBrowser/ViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import WebKit 3 | 4 | class ViewController: UIViewController, UITextFieldDelegate, WKNavigationDelegate,WKUIDelegate { 5 | 6 | @IBOutlet weak var locationTextField: UITextField! 7 | @IBOutlet weak var containerView: UIView! 8 | 9 | var devicePicker: PopUpPickerView! 10 | 11 | var webView: WKWebView! 12 | var webBluetoothManager:WebBluetoothManager! 13 | 14 | override func viewDidLoad() { 15 | 16 | super.viewDidLoad() 17 | locationTextField.delegate = self 18 | 19 | //load polyfill script 20 | var script:String? 21 | if let filePath:String = NSBundle(forClass: ViewController.self).pathForResource("WebBluetooth", ofType:"js") { 22 | do { 23 | script = try NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding) as String 24 | } catch _ { 25 | print("Error loading polyfil") 26 | return 27 | } 28 | } 29 | 30 | //create bluetooth object, and set it to listen to messages 31 | webBluetoothManager = WebBluetoothManager(); 32 | let webCfg:WKWebViewConfiguration = WKWebViewConfiguration() 33 | let userController:WKUserContentController = WKUserContentController() 34 | userController.addScriptMessageHandler(webBluetoothManager, name: "bluetooth") 35 | 36 | // connect picker 37 | devicePicker = PopUpPickerView() 38 | devicePicker.delegate = webBluetoothManager 39 | self.view.addSubview(devicePicker) 40 | webBluetoothManager.devicePicker = devicePicker 41 | 42 | //add the bluetooth script prior to loading all frames 43 | let userScript:WKUserScript = WKUserScript(source: script!, injectionTime: WKUserScriptInjectionTime.AtDocumentEnd, forMainFrameOnly: false) 44 | userController.addUserScript(userScript) 45 | webCfg.userContentController = userController; 46 | 47 | 48 | webView = WKWebView( 49 | frame: self.containerView.bounds, 50 | configuration:webCfg 51 | ) 52 | webView.UIDelegate = self 53 | 54 | webView.translatesAutoresizingMaskIntoConstraints = false 55 | webView.allowsBackForwardNavigationGestures = true 56 | webView.navigationDelegate = self 57 | containerView.addSubview(webView) 58 | 59 | let views = ["webView": webView] 60 | containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[webView]|", 61 | options: NSLayoutFormatOptions(), metrics: nil, views: views)) 62 | containerView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[webView]|", 63 | options: NSLayoutFormatOptions(), metrics: nil, views: views)) 64 | 65 | loadLocation("https://pauljt.github.io/bletest/") 66 | } 67 | 68 | 69 | func textFieldShouldReturn(textField: UITextField) -> Bool { 70 | textField.resignFirstResponder() 71 | loadLocation(textField.text!) 72 | return true 73 | } 74 | 75 | func loadLocation(var location: String) { 76 | if !location.hasPrefix("http://") && !location.hasPrefix("https://") { 77 | location = "http://" + location 78 | } 79 | locationTextField.text = location 80 | webView.loadRequest(NSURLRequest(URL: NSURL(string: location)!)) 81 | 82 | } 83 | 84 | func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) { 85 | locationTextField.text = webView.URL?.absoluteString 86 | 87 | } 88 | 89 | func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) { 90 | locationTextField.text = webView.URL?.absoluteString 91 | webView.loadHTMLString("

Fail Navigation: \(error.localizedDescription)

", baseURL: nil) 92 | } 93 | 94 | func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) { 95 | locationTextField.text = webView.URL?.absoluteString 96 | webView.loadHTMLString("

Fail Provisional Navigation: \(error.localizedDescription)

", baseURL: nil) 97 | } 98 | 99 | func webView(webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: (() -> Void)) { 100 | print("webView:\(webView) runJavaScriptAlertPanelWithMessage:\(message) initiatedByFrame:\(frame) completionHandler:\(completionHandler)") 101 | 102 | let alertController = UIAlertController(title: frame.request.URL?.host, message: message, preferredStyle: .Alert) 103 | alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in 104 | completionHandler() 105 | })) 106 | self.presentViewController(alertController, animated: true, completion: nil) 107 | } 108 | 109 | 110 | } 111 | -------------------------------------------------------------------------------- /BasicBrowser/WebBluetooth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebBluetooth.swift 3 | // BleBrowser 4 | // 5 | // Created by Paul Theriault on 7/03/2016. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | import WebKit 11 | 12 | 13 | 14 | public class BluetoothDevice: NSObject, CBPeripheralDelegate { 15 | var deviceId:String; //generated ID used instead of internal IOS name 16 | var peripheral:CBPeripheral 17 | var adData:BluetoothAdvertisingData 18 | var gattRequests:[CBUUID:JSRequest] = [CBUUID:JSRequest]() 19 | 20 | init(deviceId:String,peripheral:CBPeripheral,advertisementData:[String : AnyObject] = [String : AnyObject](),RSSI:NSNumber = 0){ 21 | self.deviceId = deviceId 22 | self.peripheral = peripheral 23 | self.adData = BluetoothAdvertisingData(advertisementData:advertisementData,RSSI: RSSI) 24 | super.init() 25 | self.peripheral.delegate = self 26 | } 27 | 28 | func toJSON()->String?{ 29 | let props:[String:AnyObject] = [ 30 | "id": deviceId, 31 | "name": peripheral.name != nil ? peripheral.name! : NSNull(), 32 | "adData":self.adData.toDict(), 33 | "deviceClass": 0, 34 | "vendorIDSource": 0, 35 | "vendorID": 0, 36 | "productID": 0, 37 | "productVersion": 0, 38 | "uuids": [] 39 | ] 40 | 41 | do { 42 | let jsonData = try NSJSONSerialization.dataWithJSONObject(props, 43 | options: NSJSONWritingOptions(rawValue: 0)) 44 | return String(data: jsonData, encoding: NSUTF8StringEncoding) 45 | } catch let error { 46 | print("error converting to json: \(error)") 47 | return nil 48 | } 49 | } 50 | 51 | 52 | // connect services 53 | public func peripheral(peripheral: CBPeripheral, didDiscoverServices error: NSError?) { 54 | for service in peripheral.services! { 55 | print("found service:"+service.UUID.UUIDString) 56 | if let matchedRequest = gattRequests[service.UUID]{ 57 | matchedRequest.sendMessage("response", success:true, result:service.UUID.UUIDString, requestId:matchedRequest.id) 58 | } 59 | } 60 | } 61 | 62 | // connect characteristics 63 | public func peripheral(peripheral: CBPeripheral, didDiscoverCharacteristicsForService service: CBService, error: NSError?) { 64 | for char in (service.characteristics as [CBCharacteristic]!) { 65 | print("found char:" + char.UUID.UUIDString) 66 | if let matchedRequest = gattRequests[char.UUID]{ 67 | matchedRequest.sendMessage("response", success:true, result:"{}", requestId:matchedRequest.id) 68 | } 69 | } 70 | } 71 | 72 | 73 | // characteristic updates 74 | public func peripheral(peripheral: CBPeripheral, didUpdateValueForCharacteristic characteristic: CBCharacteristic, error: NSError?) { 75 | print("Characteristic Updated:",characteristic.UUID," ->",characteristic.value) 76 | 77 | if let matchedRequest = gattRequests[characteristic.UUID]{ 78 | if let data = characteristic.value{ 79 | let b64data = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) 80 | matchedRequest.sendMessage("response", success:true, result:b64data, requestId:matchedRequest.id) 81 | return 82 | }else{ 83 | matchedRequest.sendMessage("response", success:false, result:"{}", requestId:matchedRequest.id) 84 | } 85 | } 86 | } 87 | 88 | func getService(uuid:CBUUID)->CBService?{ 89 | if(self.peripheral.services == nil){ 90 | return nil 91 | } 92 | for service in peripheral.services!{ 93 | if(service.UUID == uuid){ 94 | return service 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | func getCharacteristic(serviceUUID:CBUUID,uuid:CBUUID)->CBCharacteristic?{ 101 | print(peripheral) 102 | if(self.peripheral.services == nil){ 103 | return nil 104 | } 105 | var service:CBService? = nil 106 | for s in self.peripheral.services!{ 107 | if(s.UUID == serviceUUID){ 108 | service = s 109 | } 110 | } 111 | 112 | guard let chars = service?.characteristics else { 113 | return nil 114 | } 115 | 116 | for char in chars{ 117 | if(char.UUID == uuid){ 118 | return char 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | 125 | func recieve(req:JSRequest){ 126 | switch req.method{ 127 | case "BluetoothRemoteGATTServer.getPrimaryService": 128 | let targetService:CBUUID = CBUUID(string:req.args[0]) 129 | 130 | // check peripherals.services first to see if we already discovered services 131 | if (peripheral.services != nil ){ 132 | if peripheral.services!.contains({$0.UUID == targetService}) { 133 | req.sendMessage("response", success:true, result:"{}", requestId:req.id) 134 | return 135 | }else{ 136 | req.sendMessage("response", success:false, result:"{}", requestId:req.id) 137 | return 138 | } 139 | } 140 | 141 | print("Discovering service:"+targetService.UUIDString) 142 | gattRequests[targetService] = req 143 | peripheral.discoverServices([targetService]) 144 | 145 | case "BluetoothGATTService.getCharacteristic": 146 | 147 | let targetService:CBUUID = CBUUID(string:req.args[0]) 148 | let targetChar:CBUUID = CBUUID(string:req.args[1]) 149 | guard let service = getService(targetService) else { 150 | req.sendMessage("response", success:false, result:"{}", requestId:req.id) 151 | return 152 | } 153 | 154 | if service.characteristics != nil{ 155 | for char in service.characteristics!{ 156 | if(char.UUID == targetChar){ 157 | req.sendMessage("response", success:true, result:"{}", requestId:req.id) 158 | return 159 | }else{ 160 | req.sendMessage("response", success:false, result:"{}", requestId:req.id) 161 | return 162 | } 163 | } 164 | } 165 | 166 | print("Discovering service:"+targetService.UUIDString) 167 | gattRequests[targetChar] = req 168 | peripheral.discoverCharacteristics(nil, forService: service) 169 | case "BluetoothGATTCharacteristic.readValue": 170 | let targetService:CBUUID = CBUUID(string:req.args[0]) 171 | let targetChar:CBUUID = CBUUID(string:req.args[1]) 172 | 173 | guard let char = getCharacteristic(targetService,uuid: targetChar) else{ 174 | req.sendMessage("response", success:false, result:"{}", requestId:req.id) 175 | return 176 | } 177 | 178 | gattRequests[char.UUID] = req 179 | self.peripheral.readValueForCharacteristic(char) 180 | 181 | default: 182 | print("Unrecognized method requested") 183 | } 184 | } 185 | } 186 | 187 | class BluetoothAdvertisingData{ 188 | var appearance:String 189 | var txPower:NSNumber 190 | var rssi:String 191 | var manufacturerData:String 192 | var serviceData:[String] 193 | 194 | init(advertisementData: [String : AnyObject] = [String : AnyObject](), RSSI: NSNumber = 0){ 195 | self.appearance = "fakeappearance" 196 | self.txPower = (advertisementData[CBAdvertisementDataTxPowerLevelKey] ?? 0) as! NSNumber 197 | self.rssi=String(RSSI) 198 | let data = advertisementData[CBAdvertisementDataManufacturerDataKey] 199 | self.manufacturerData = "" 200 | if data != nil{ 201 | if let dataString = NSString(data: data as! NSData, encoding: NSUTF8StringEncoding) as? String { 202 | self.manufacturerData = dataString 203 | } else { 204 | print("Error parsing advertisement data: not a valid UTF-8 sequence") 205 | } 206 | } 207 | 208 | var uuids = [String]() 209 | if advertisementData["kCBAdvDataServiceUUIDs"] != nil { 210 | uuids = (advertisementData["kCBAdvDataServiceUUIDs"] as! [CBUUID]).map{$0.UUIDString.lowercaseString} 211 | } 212 | self.serviceData = uuids 213 | } 214 | 215 | func toDict()->[String:AnyObject]{ 216 | let dict:[String:AnyObject] = [ 217 | "appearance": self.appearance, 218 | "txPower": self.txPower, 219 | "rssi": self.rssi, 220 | "manufacturerData": self.manufacturerData, 221 | "serviceData": self.serviceData 222 | ] 223 | return dict 224 | } 225 | 226 | } 227 | 228 | -------------------------------------------------------------------------------- /BasicBrowser/WebBluetoothManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebBluetooth.swift 3 | // BasicBrowser 4 | // 5 | // Created by Paul Theriault on 10/01/2016. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | import WebKit 11 | 12 | public class WebBluetoothManager: NSObject, CBCentralManagerDelegate, WKScriptMessageHandler, PopUpPickerViewDelegate { 13 | 14 | override init(){ 15 | super.init() 16 | centralManager.delegate = self 17 | } 18 | 19 | // BLE 20 | var centralManager:CBCentralManager! = CBCentralManager(delegate: nil, queue: nil) 21 | var devicePicker:PopUpPickerView! 22 | 23 | var BluetoothDeviceOption_filters:[CBUUID]? 24 | var BluetoothDeviceOption_optionalService:[CBUUID]? 25 | 26 | // Stores references to devices while scanning. Key is the system provided UUID (peripheral.id) 27 | var foundDevices:[String:BluetoothDevice] = [String:BluetoothDevice]() 28 | var deviceRequest:JSRequest? //stores the last requestID for device requests (i.e. subsequent request replace unfinished requests) 29 | var connectionRequest:JSRequest? // stores last conncetion request, to resolve when connected/disconnected 30 | var disconnectionRequest:JSRequest? // stores last conncetion request, to resolve when connected/disconnected 31 | 32 | 33 | // Allowed Devices Map 34 | // See https://webbluetoothcg.github.io/web-bluetooth/#per-origin-device-properties 35 | // Stores a dictionary for each origin which holds a mappping between Device ID and the actual BluetoothDevice 36 | // For example, if a user grants access for https://example.com would be something like: 37 | // allowedDevices["https://example.com"]?[NSUUID().UUIDString] = new BluetoothDevice(peripheral) 38 | var allowedDevices:[String:[String:BluetoothDevice]] = [String:[String:BluetoothDevice]]() 39 | 40 | // recieve message from javascript 41 | public func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage){ 42 | 43 | let messageBody = message.body as! NSDictionary 44 | let callbackID:Int = messageBody["callbackID"] as! Int 45 | let type = messageBody["type"] as! String 46 | let data = messageBody["data"] as! [String:AnyObject] 47 | 48 | //todo add safety 49 | let req = JSRequest(id: callbackID,type:type,data:data,webView:message.webView!); 50 | print("<-- #\(callbackID) to dispatch \(type) with data:\(data)") 51 | processRequest(req) 52 | 53 | /*var args:[AnyObject]=[AnyObject]() 54 | if(messageBody["arguments"] != nil){ 55 | args = transformArguments(messageBody["arguments"] as! [AnyObject]) 56 | }*/ 57 | } 58 | 59 | func processRequest(req:JSRequest){ 60 | switch req.type{ 61 | case "bluetooth:requestDevice": 62 | if scanForPeripherals(req.data){ 63 | deviceRequest = req 64 | devicePicker.showPicker() 65 | } 66 | else{ 67 | req.sendMessage("response", success:false, result:"\"Bluetooth is currently disabled\"", requestId:req.id) 68 | } 69 | 70 | case "bluetooth:deviceMessage": 71 | print("DeviceMessage for \(req.deviceId)") 72 | print("Calling \(req.method) with \(req.args)") 73 | print(req.args) 74 | 75 | if let device = allowedDevices[req.origin]?[req.deviceId]{ 76 | //connecting/disconnecting GATT server has to be handle by the manager 77 | if(req.method == "BluetoothRemoteGATTServer.connect"){ 78 | centralManager.connectPeripheral(device.peripheral,options: nil) 79 | connectionRequest = req //resolved when connected 80 | }else if (req.method == "BluetoothRemoteGATTServer.disconnect"){ 81 | centralManager.cancelPeripheralConnection(device.peripheral) 82 | disconnectionRequest = req //resolved when connected 83 | }else{ 84 | device.recieve(req) 85 | } 86 | } 87 | else{ 88 | req.sendMessage("response", success:false, result:"\"Device not found\"", requestId:req.id) 89 | } 90 | default: 91 | let error="\"Unknown method: \(req.type)\""; 92 | req.sendMessage("response", success:false, result:error, requestId:req.id) 93 | } 94 | 95 | } 96 | 97 | 98 | 99 | func transformArguments(args: [AnyObject]) -> [AnyObject!] { 100 | return args.map { arg in 101 | if arg is NSNull { 102 | return nil 103 | } else { 104 | return arg 105 | } 106 | } 107 | } 108 | 109 | 110 | // Check status of BLE hardware 111 | public func centralManagerDidUpdateState(central: CBCentralManager) { 112 | if central.state == CBCentralManagerState.PoweredOn { 113 | print("Bluetooth is powered on") 114 | } 115 | else { 116 | print("Error:Bluetooth switched off or not initialized") 117 | } 118 | } 119 | 120 | func scanForPeripherals(options:[String:AnyObject]) -> Bool{ 121 | if centralManager.state != CBCentralManagerState.PoweredOn{ 122 | return false 123 | } 124 | 125 | let filters = options["filters"] as! [AnyObject] 126 | let filterOne = filters[0] 127 | 128 | print("Filters",filters) 129 | print("Services",filterOne["services"]) 130 | print("name:",filters[0]["name"]) 131 | print("prefix:",filters[0]["namePrefix"]) 132 | 133 | let services = filters[0]["services"] as! [String] 134 | 135 | let servicesCBUUID:[CBUUID] 136 | 137 | //todo validate CBUUID (js does this already but security should be here since 138 | //messageHandler can be called directly. 139 | // (if the string is invalid, it causes app to crash with NSexception) 140 | 141 | //todo: determine if uppercase is the standard (bb-b uses uppercase UUID) 142 | servicesCBUUID = services.map {return CBUUID(string:$0.uppercaseString)} 143 | 144 | foundDevices.removeAll(); 145 | centralManager.scanForPeripheralsWithServices(servicesCBUUID, options: nil) 146 | return true 147 | } 148 | 149 | public func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) { 150 | let deviceId = NSUUID().UUIDString; 151 | foundDevices[peripheral.identifier.UUIDString] = BluetoothDevice(deviceId:deviceId,peripheral: peripheral, 152 | advertisementData: advertisementData, 153 | RSSI: RSSI) 154 | updatePickerData() 155 | } 156 | 157 | public func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) { 158 | print("Connected") 159 | if(connectionRequest != nil){ 160 | connectionRequest!.sendMessage("response", success:true, result:"{}", requestId:connectionRequest!.id) 161 | connectionRequest = nil 162 | } 163 | 164 | 165 | } 166 | 167 | public func centralManager(central: CBCentralManager, didFailToConnectPeripheral peripheral: CBPeripheral) { 168 | print("Failed to connect") 169 | connectionRequest!.sendMessage("response", success:false, result:"'Failed to connect'", requestId:connectionRequest!.id) 170 | connectionRequest = nil 171 | 172 | } 173 | 174 | //UIPickerView protocols 175 | 176 | //2d array of devices & corresponding names 177 | var pickerNames:[String] = [String]() 178 | var pickerIds:[String] = [String]() 179 | 180 | func updatePickerData(){ 181 | pickerNames.removeAll() 182 | pickerIds.removeAll() 183 | for (id, device) in foundDevices { 184 | pickerNames.append(device.peripheral.name ?? "Unknown") 185 | pickerIds.append(id) 186 | } 187 | devicePicker.updatePicker() 188 | } 189 | 190 | 191 | func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { 192 | return 1 193 | } 194 | 195 | // The number of rows of data 196 | func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 197 | return pickerNames.count 198 | } 199 | 200 | // The data to return for the row and component (column) that's being passed in 201 | public func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 202 | return pickerNames[row] 203 | } 204 | 205 | func pickerView(pickerView: UIPickerView, didSelect numbers: [Int]) { 206 | 207 | if(pickerIds.count<1){ 208 | return 209 | } 210 | let deviceId = pickerIds[numbers[0]] 211 | centralManager.stopScan() 212 | 213 | if deviceRequest == nil{ 214 | print("Picker UI initiated with a request, this should never happen") 215 | return 216 | } 217 | let req = deviceRequest! 218 | deviceRequest = nil 219 | 220 | if self.foundDevices[deviceId] == nil{ 221 | print("DEVICE OUT OF RANGE") 222 | return 223 | } 224 | let device = self.foundDevices[deviceId]! 225 | let deviceJSON = device.toJSON()! 226 | 227 | if allowedDevices[req.origin] == nil{ 228 | allowedDevices[req.origin] = [String:BluetoothDevice]() 229 | } 230 | //add device to allowed list, and resolve requestDevice promise 231 | allowedDevices[req.origin]![device.deviceId] = device 232 | req.sendMessage("response", success:true, result:deviceJSON, requestId:req.id) 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /BleBrowser.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BB303DF11C41EF2F00FCAF24 /* WebBluetooth.js in Resources */ = {isa = PBXBuildFile; fileRef = BB303DF01C41EF2F00FCAF24 /* WebBluetooth.js */; }; 11 | BB303DF31C42070C00FCAF24 /* WebBluetoothManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB303DF21C42070C00FCAF24 /* WebBluetoothManager.swift */; }; 12 | BB4DBE651C97F577000D2C35 /* JSRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4DBE641C97F577000D2C35 /* JSRequest.swift */; }; 13 | BB4DE1F41C8D0A44003151BF /* WebBluetooth.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4DE1F31C8D0A44003151BF /* WebBluetooth.swift */; }; 14 | BBA6730E1C535B8C00076CBA /* PopUpPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBA6730D1C535B8C00076CBA /* PopUpPickerView.swift */; }; 15 | E44C185C1A36203E00734F8B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44C185B1A36203E00734F8B /* AppDelegate.swift */; }; 16 | E44C185E1A36203E00734F8B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44C185D1A36203E00734F8B /* ViewController.swift */; }; 17 | E44C18611A36203E00734F8B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E44C185F1A36203E00734F8B /* Main.storyboard */; }; 18 | E44C18631A36203E00734F8B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E44C18621A36203E00734F8B /* Images.xcassets */; }; 19 | E44C18661A36203E00734F8B /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = E44C18641A36203E00734F8B /* LaunchScreen.xib */; }; 20 | E44C18721A36203E00734F8B /* BleBrowserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44C18711A36203E00734F8B /* BleBrowserTests.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | E44C186C1A36203E00734F8B /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = E44C184E1A36203E00734F8B /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = E44C18551A36203E00734F8B; 29 | remoteInfo = BasicBrowser; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | BB303DF01C41EF2F00FCAF24 /* WebBluetooth.js */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.javascript; lineEnding = 0; path = WebBluetooth.js; sourceTree = ""; tabWidth = 2; }; 35 | BB303DF21C42070C00FCAF24 /* WebBluetoothManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebBluetoothManager.swift; sourceTree = ""; }; 36 | BB4DBE641C97F577000D2C35 /* JSRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSRequest.swift; sourceTree = ""; }; 37 | BB4DE1F31C8D0A44003151BF /* WebBluetooth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebBluetooth.swift; sourceTree = ""; }; 38 | BBA6730D1C535B8C00076CBA /* PopUpPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopUpPickerView.swift; sourceTree = ""; }; 39 | E44C18561A36203E00734F8B /* BleBrowser.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BleBrowser.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | E44C185A1A36203E00734F8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | E44C185B1A36203E00734F8B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | E44C185D1A36203E00734F8B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 43 | E44C18601A36203E00734F8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 44 | E44C18621A36203E00734F8B /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 45 | E44C18651A36203E00734F8B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 46 | E44C186B1A36203E00734F8B /* BleBrowser.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BleBrowser.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | E44C18701A36203E00734F8B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | E44C18711A36203E00734F8B /* BleBrowserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BleBrowserTests.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | E44C18531A36203E00734F8B /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | E44C18681A36203E00734F8B /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | E44C184D1A36203E00734F8B = { 70 | isa = PBXGroup; 71 | children = ( 72 | E44C18581A36203E00734F8B /* BleBrowser */, 73 | E44C186E1A36203E00734F8B /* BleBrowserTests */, 74 | E44C18571A36203E00734F8B /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | E44C18571A36203E00734F8B /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | E44C18561A36203E00734F8B /* BleBrowser.app */, 82 | E44C186B1A36203E00734F8B /* BleBrowser.xctest */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | E44C18581A36203E00734F8B /* BleBrowser */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | E44C185B1A36203E00734F8B /* AppDelegate.swift */, 91 | BBA6730D1C535B8C00076CBA /* PopUpPickerView.swift */, 92 | E44C185D1A36203E00734F8B /* ViewController.swift */, 93 | E44C185F1A36203E00734F8B /* Main.storyboard */, 94 | E44C18621A36203E00734F8B /* Images.xcassets */, 95 | E44C18641A36203E00734F8B /* LaunchScreen.xib */, 96 | E44C18591A36203E00734F8B /* Supporting Files */, 97 | BB303DF01C41EF2F00FCAF24 /* WebBluetooth.js */, 98 | BB303DF21C42070C00FCAF24 /* WebBluetoothManager.swift */, 99 | BB4DE1F31C8D0A44003151BF /* WebBluetooth.swift */, 100 | BB4DBE641C97F577000D2C35 /* JSRequest.swift */, 101 | ); 102 | name = BleBrowser; 103 | path = BasicBrowser; 104 | sourceTree = ""; 105 | }; 106 | E44C18591A36203E00734F8B /* Supporting Files */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | E44C185A1A36203E00734F8B /* Info.plist */, 110 | ); 111 | name = "Supporting Files"; 112 | sourceTree = ""; 113 | }; 114 | E44C186E1A36203E00734F8B /* BleBrowserTests */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | E44C18711A36203E00734F8B /* BleBrowserTests.swift */, 118 | E44C186F1A36203E00734F8B /* Supporting Files */, 119 | ); 120 | name = BleBrowserTests; 121 | path = BasicBrowserTests; 122 | sourceTree = ""; 123 | }; 124 | E44C186F1A36203E00734F8B /* Supporting Files */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | E44C18701A36203E00734F8B /* Info.plist */, 128 | ); 129 | name = "Supporting Files"; 130 | sourceTree = ""; 131 | }; 132 | /* End PBXGroup section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | E44C18551A36203E00734F8B /* BleBrowser */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = E44C18751A36203E00734F8B /* Build configuration list for PBXNativeTarget "BleBrowser" */; 138 | buildPhases = ( 139 | E44C18521A36203E00734F8B /* Sources */, 140 | E44C18531A36203E00734F8B /* Frameworks */, 141 | E44C18541A36203E00734F8B /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = BleBrowser; 148 | productName = BasicBrowser; 149 | productReference = E44C18561A36203E00734F8B /* BleBrowser.app */; 150 | productType = "com.apple.product-type.application"; 151 | }; 152 | E44C186A1A36203E00734F8B /* BleBrowserTests */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = E44C18781A36203E00734F8B /* Build configuration list for PBXNativeTarget "BleBrowserTests" */; 155 | buildPhases = ( 156 | E44C18671A36203E00734F8B /* Sources */, 157 | E44C18681A36203E00734F8B /* Frameworks */, 158 | E44C18691A36203E00734F8B /* Resources */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | E44C186D1A36203E00734F8B /* PBXTargetDependency */, 164 | ); 165 | name = BleBrowserTests; 166 | productName = BasicBrowserTests; 167 | productReference = E44C186B1A36203E00734F8B /* BleBrowser.xctest */; 168 | productType = "com.apple.product-type.bundle.unit-test"; 169 | }; 170 | /* End PBXNativeTarget section */ 171 | 172 | /* Begin PBXProject section */ 173 | E44C184E1A36203E00734F8B /* Project object */ = { 174 | isa = PBXProject; 175 | attributes = { 176 | LastSwiftMigration = 0720; 177 | LastSwiftUpdateCheck = 0720; 178 | LastUpgradeCheck = 0610; 179 | ORGANIZATIONNAME = "Stefan Arentz"; 180 | TargetAttributes = { 181 | E44C18551A36203E00734F8B = { 182 | CreatedOnToolsVersion = 6.1.1; 183 | DevelopmentTeam = L4H495YSHK; 184 | }; 185 | E44C186A1A36203E00734F8B = { 186 | CreatedOnToolsVersion = 6.1.1; 187 | TestTargetID = E44C18551A36203E00734F8B; 188 | }; 189 | }; 190 | }; 191 | buildConfigurationList = E44C18511A36203E00734F8B /* Build configuration list for PBXProject "BleBrowser" */; 192 | compatibilityVersion = "Xcode 3.2"; 193 | developmentRegion = English; 194 | hasScannedForEncodings = 0; 195 | knownRegions = ( 196 | en, 197 | Base, 198 | ); 199 | mainGroup = E44C184D1A36203E00734F8B; 200 | productRefGroup = E44C18571A36203E00734F8B /* Products */; 201 | projectDirPath = ""; 202 | projectRoot = ""; 203 | targets = ( 204 | E44C18551A36203E00734F8B /* BleBrowser */, 205 | E44C186A1A36203E00734F8B /* BleBrowserTests */, 206 | ); 207 | }; 208 | /* End PBXProject section */ 209 | 210 | /* Begin PBXResourcesBuildPhase section */ 211 | E44C18541A36203E00734F8B /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | E44C18611A36203E00734F8B /* Main.storyboard in Resources */, 216 | BB303DF11C41EF2F00FCAF24 /* WebBluetooth.js in Resources */, 217 | E44C18661A36203E00734F8B /* LaunchScreen.xib in Resources */, 218 | E44C18631A36203E00734F8B /* Images.xcassets in Resources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | E44C18691A36203E00734F8B /* Resources */ = { 223 | isa = PBXResourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXResourcesBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | E44C18521A36203E00734F8B /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | BB4DBE651C97F577000D2C35 /* JSRequest.swift in Sources */, 237 | E44C185E1A36203E00734F8B /* ViewController.swift in Sources */, 238 | BBA6730E1C535B8C00076CBA /* PopUpPickerView.swift in Sources */, 239 | E44C185C1A36203E00734F8B /* AppDelegate.swift in Sources */, 240 | BB303DF31C42070C00FCAF24 /* WebBluetoothManager.swift in Sources */, 241 | BB4DE1F41C8D0A44003151BF /* WebBluetooth.swift in Sources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | E44C18671A36203E00734F8B /* Sources */ = { 246 | isa = PBXSourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | E44C18721A36203E00734F8B /* BleBrowserTests.swift in Sources */, 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | }; 253 | /* End PBXSourcesBuildPhase section */ 254 | 255 | /* Begin PBXTargetDependency section */ 256 | E44C186D1A36203E00734F8B /* PBXTargetDependency */ = { 257 | isa = PBXTargetDependency; 258 | target = E44C18551A36203E00734F8B /* BleBrowser */; 259 | targetProxy = E44C186C1A36203E00734F8B /* PBXContainerItemProxy */; 260 | }; 261 | /* End PBXTargetDependency section */ 262 | 263 | /* Begin PBXVariantGroup section */ 264 | E44C185F1A36203E00734F8B /* Main.storyboard */ = { 265 | isa = PBXVariantGroup; 266 | children = ( 267 | E44C18601A36203E00734F8B /* Base */, 268 | ); 269 | name = Main.storyboard; 270 | sourceTree = ""; 271 | }; 272 | E44C18641A36203E00734F8B /* LaunchScreen.xib */ = { 273 | isa = PBXVariantGroup; 274 | children = ( 275 | E44C18651A36203E00734F8B /* Base */, 276 | ); 277 | name = LaunchScreen.xib; 278 | sourceTree = ""; 279 | }; 280 | /* End PBXVariantGroup section */ 281 | 282 | /* Begin XCBuildConfiguration section */ 283 | E44C18731A36203E00734F8B /* Debug */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_WARN_BOOL_CONVERSION = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 294 | CLANG_WARN_EMPTY_BODY = YES; 295 | CLANG_WARN_ENUM_CONVERSION = YES; 296 | CLANG_WARN_INT_CONVERSION = YES; 297 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 298 | CLANG_WARN_UNREACHABLE_CODE = YES; 299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 300 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 301 | COPY_PHASE_STRIP = NO; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_DYNAMIC_NO_PIC = NO; 305 | GCC_OPTIMIZATION_LEVEL = 0; 306 | GCC_PREPROCESSOR_DEFINITIONS = ( 307 | "DEBUG=1", 308 | "$(inherited)", 309 | ); 310 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 318 | MTL_ENABLE_DEBUG_INFO = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = iphoneos; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | TARGETED_DEVICE_FAMILY = "1,2"; 323 | }; 324 | name = Debug; 325 | }; 326 | E44C18741A36203E00734F8B /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 331 | CLANG_CXX_LIBRARY = "libc++"; 332 | CLANG_ENABLE_MODULES = YES; 333 | CLANG_ENABLE_OBJC_ARC = YES; 334 | CLANG_WARN_BOOL_CONVERSION = YES; 335 | CLANG_WARN_CONSTANT_CONVERSION = YES; 336 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 337 | CLANG_WARN_EMPTY_BODY = YES; 338 | CLANG_WARN_ENUM_CONVERSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 341 | CLANG_WARN_UNREACHABLE_CODE = YES; 342 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 343 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 344 | COPY_PHASE_STRIP = YES; 345 | ENABLE_NS_ASSERTIONS = NO; 346 | ENABLE_STRICT_OBJC_MSGSEND = YES; 347 | GCC_C_LANGUAGE_STANDARD = gnu99; 348 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 349 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 350 | GCC_WARN_UNDECLARED_SELECTOR = YES; 351 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 352 | GCC_WARN_UNUSED_FUNCTION = YES; 353 | GCC_WARN_UNUSED_VARIABLE = YES; 354 | IPHONEOS_DEPLOYMENT_TARGET = 8.1; 355 | MTL_ENABLE_DEBUG_INFO = NO; 356 | SDKROOT = iphoneos; 357 | TARGETED_DEVICE_FAMILY = "1,2"; 358 | VALIDATE_PRODUCT = YES; 359 | }; 360 | name = Release; 361 | }; 362 | E44C18761A36203E00734F8B /* Debug */ = { 363 | isa = XCBuildConfiguration; 364 | buildSettings = { 365 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 366 | CODE_SIGN_IDENTITY = "iPhone Developer"; 367 | ENABLE_TESTABILITY = YES; 368 | FRAMEWORK_SEARCH_PATHS = ""; 369 | INFOPLIST_FILE = BasicBrowser/Info.plist; 370 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 372 | New_Setting = ""; 373 | PRODUCT_BUNDLE_IDENTIFIER = org.cm.blebrowser; 374 | PRODUCT_NAME = BleBrowser; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Debug; 378 | }; 379 | E44C18771A36203E00734F8B /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | CODE_SIGN_IDENTITY = "iPhone Developer"; 384 | FRAMEWORK_SEARCH_PATHS = ""; 385 | INFOPLIST_FILE = BasicBrowser/Info.plist; 386 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 387 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 388 | New_Setting = ""; 389 | PRODUCT_BUNDLE_IDENTIFIER = org.cm.blebrowser; 390 | PRODUCT_NAME = BleBrowser; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | }; 393 | name = Release; 394 | }; 395 | E44C18791A36203E00734F8B /* Debug */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | BUNDLE_LOADER = "$(TEST_HOST)"; 399 | FRAMEWORK_SEARCH_PATHS = ( 400 | "$(SDKROOT)/Developer/Library/Frameworks", 401 | "$(inherited)", 402 | ); 403 | GCC_PREPROCESSOR_DEFINITIONS = ( 404 | "DEBUG=1", 405 | "$(inherited)", 406 | ); 407 | INFOPLIST_FILE = BasicBrowserTests/Info.plist; 408 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 409 | PRODUCT_NAME = BleBrowser; 410 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BleBrowser.app/BleBrowser"; 411 | }; 412 | name = Debug; 413 | }; 414 | E44C187A1A36203E00734F8B /* Release */ = { 415 | isa = XCBuildConfiguration; 416 | buildSettings = { 417 | BUNDLE_LOADER = "$(TEST_HOST)"; 418 | FRAMEWORK_SEARCH_PATHS = ( 419 | "$(SDKROOT)/Developer/Library/Frameworks", 420 | "$(inherited)", 421 | ); 422 | INFOPLIST_FILE = BasicBrowserTests/Info.plist; 423 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 424 | PRODUCT_NAME = BleBrowser; 425 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BleBrowser.app/BleBrowser"; 426 | }; 427 | name = Release; 428 | }; 429 | /* End XCBuildConfiguration section */ 430 | 431 | /* Begin XCConfigurationList section */ 432 | E44C18511A36203E00734F8B /* Build configuration list for PBXProject "BleBrowser" */ = { 433 | isa = XCConfigurationList; 434 | buildConfigurations = ( 435 | E44C18731A36203E00734F8B /* Debug */, 436 | E44C18741A36203E00734F8B /* Release */, 437 | ); 438 | defaultConfigurationIsVisible = 0; 439 | defaultConfigurationName = Release; 440 | }; 441 | E44C18751A36203E00734F8B /* Build configuration list for PBXNativeTarget "BleBrowser" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | E44C18761A36203E00734F8B /* Debug */, 445 | E44C18771A36203E00734F8B /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | defaultConfigurationName = Release; 449 | }; 450 | E44C18781A36203E00734F8B /* Build configuration list for PBXNativeTarget "BleBrowserTests" */ = { 451 | isa = XCConfigurationList; 452 | buildConfigurations = ( 453 | E44C18791A36203E00734F8B /* Debug */, 454 | E44C187A1A36203E00734F8B /* Release */, 455 | ); 456 | defaultConfigurationIsVisible = 0; 457 | defaultConfigurationName = Release; 458 | }; 459 | /* End XCConfigurationList section */ 460 | }; 461 | rootObject = E44C184E1A36203E00734F8B /* Project object */; 462 | } 463 | -------------------------------------------------------------------------------- /BasicBrowser/WebBluetooth.js: -------------------------------------------------------------------------------- 1 | // adapted from chrome app polyfill https://github.com/WebBluetoothCG/chrome-app-polyfill 2 | 3 | (function () { 4 | "use strict"; 5 | 6 | if (navigator.bluetooth) { 7 | //already exists, don't polyfill 8 | console.log('navigator.bluetooth already exists, skipping polyfill') 9 | return; 10 | } 11 | 12 | // https://webbluetoothcg.github.io/web-bluetooth/ interface 13 | function BluetoothDevice(deviceJSON) { 14 | console.log("got device:", deviceJSON.id) 15 | this._id = deviceJSON.id; 16 | this._name = deviceJSON.name; 17 | 18 | this._adData = {}; 19 | if (deviceJSON.adData) { 20 | this._adData.appearance = deviceJSON.adData.appearance || ""; 21 | this._adData.txPower = deviceJSON.adData.txPower || 0; 22 | this._adData.rssi = deviceJSON.adData.rssi || 0; 23 | this._adData.manufacturerData = deviceJSON.adData.manufacturerData || []; 24 | this._adData.serviceData = deviceJSON.adData.serviceData || []; 25 | } 26 | 27 | this._deviceClass = deviceJSON.deviceClass || 0; 28 | this._vendorIdSource = deviceJSON.vendorIdSource || "bluetooth"; 29 | this._vendorId = deviceJSON.vendorId || 0; 30 | this._productId = deviceJSON.productId || 0; 31 | this._productVersion = deviceJSON.productVersion || 0; 32 | this._gatt = new BluetoothRemoteGATTServer(this); 33 | this._uuids = deviceJSON.uuids; 34 | }; 35 | 36 | BluetoothDevice.prototype = { 37 | 38 | get id() { 39 | return this._id; 40 | }, 41 | get name() { 42 | return this._name; 43 | }, 44 | get adData() { 45 | return this._adData; 46 | }, 47 | get deviceClass() { 48 | return this._deviceClass; 49 | }, 50 | get vendorIdSource() { 51 | return this._vendorIdSource; 52 | }, 53 | get vendorId() { 54 | return this._vendorId; 55 | }, 56 | get productId() { 57 | return this._productId; 58 | }, 59 | get productVersion() { 60 | return this._productVersion; 61 | }, 62 | get gatt() { 63 | return this._gatt; 64 | }, 65 | get uuids() { 66 | return this._uuids; 67 | }, 68 | toString: function () { 69 | return this._id; 70 | } 71 | }; 72 | 73 | function BluetoothRemoteGATTServer(webBluetoothDevice) { 74 | this._device = webBluetoothDevice; 75 | this._connected = false; 76 | 77 | this._callRemote = function (method) { 78 | var self = this; 79 | var args = Array.prototype.slice.call(arguments).slice(1, arguments.length) 80 | return sendMessage("bluetooth:deviceMessage", {method: method, args: args, deviceId: self._device.id}) 81 | } 82 | 83 | }; 84 | BluetoothRemoteGATTServer.prototype = { 85 | get device() { 86 | return this._device; 87 | }, 88 | get connected() { 89 | return this._connected; 90 | }, 91 | connect: function () { 92 | var self = this; 93 | return self._callRemote("BluetoothRemoteGATTServer.connect") 94 | .then(function () { 95 | self._connected = true; 96 | return self; 97 | }); 98 | }, 99 | disconnect: function () { 100 | var self = this; 101 | return self._callRemote("BluetoothRemoteGATTServer.disconnect") 102 | .then(function () { 103 | self._connected = false; 104 | }); 105 | }, 106 | getPrimaryService: function (UUID) { 107 | var self = this; 108 | var canonicalUUID = window.BluetoothUUID.getService(UUID) 109 | return self._callRemote("BluetoothRemoteGATTServer.getPrimaryService", canonicalUUID) 110 | .then(function (service) { 111 | console.log("GOT SERVICE:"+service) 112 | return new BluetoothGATTService(self._device, canonicalUUID, true); 113 | }) 114 | }, 115 | 116 | getPrimaryServices: function (UUID) { 117 | var self = this; 118 | var canonicalUUID = window.BluetoothUUID.getService(UUID) 119 | return self._callRemote("BluetoothRemoteGATTServer.getPrimaryService", canonicalUUID) 120 | .then(function (servicesJSON) { 121 | var servicesData = JSON.parse(servicesJSON); 122 | var services = []; 123 | 124 | // this is a problem - all services will have the same information (UUID) so no way for this side of the code to differentiate. 125 | // we need to add an identifier GUID to tell them apart 126 | servicesData.forEach(function (service) { 127 | services.push(new BluetoothGATTService(self._device, canonicalUUID, characteristicUuid, true)) 128 | }); 129 | return services; 130 | }); 131 | }, 132 | toString: function () { 133 | return "BluetoothRemoteGATTServer"; 134 | } 135 | }; 136 | 137 | function BluetoothGATTService(device, uuid, isPrimary) { 138 | if (device == null || uuid == null || isPrimary == null) { 139 | throw Error("Invalid call to BluetoothGATTService constructor") 140 | } 141 | this._device = device 142 | this._uuid = uuid; 143 | this._isPrimary = isPrimary; 144 | 145 | this._callRemote = function (method) { 146 | var self = this; 147 | var args = Array.prototype.slice.call(arguments).slice(1, arguments.length) 148 | return sendMessage("bluetooth:deviceMessage", { 149 | method: method, 150 | args: args, 151 | deviceId: self._device.id, 152 | uuid: self._uuid 153 | }) 154 | } 155 | } 156 | 157 | BluetoothGATTService.prototype = { 158 | get device() { 159 | return this._device; 160 | }, 161 | get uuid() { 162 | return this._uuid; 163 | }, 164 | get isPrimary() { 165 | return this._isPrimary 166 | }, 167 | getCharacteristic: function (uuid) { 168 | var self = this; 169 | var canonicalUUID = BluetoothUUID.getCharacteristic(uuid) 170 | 171 | return self._callRemote("BluetoothGATTService.getCharacteristic", 172 | self.uuid, canonicalUUID) 173 | .then(function (CharacteristicJSON) { 174 | //todo check we got the correct char UUID back. 175 | return new BluetoothGATTCharacteristic(self, canonicalUUID, CharacteristicJSON.properties); 176 | }); 177 | }, 178 | getCharacteristics: function (uuid) { 179 | var self = this; 180 | var canonicalUUID = BluetoothUUID.getCharacteristic(uuid) 181 | 182 | return callRemote("BluetoothGATTService.getCharacteristic", 183 | self.uuid, canonicalUUID) 184 | .then(function (CharacteristicJSON) { 185 | //todo check we got the correct char UUID back. 186 | var characteristic = JSON.parse(CharacteristicJSON); 187 | return new BluetoothGATTCharacteristic(self, canonicalUUID, CharacteristicJSON.properties); 188 | }); 189 | }, 190 | getIncludedService: function (uuid) { 191 | throw new Error('Not implemented'); 192 | }, 193 | getIncludedServices: function (uuids) { 194 | throw new Error('Not implemented'); 195 | } 196 | }; 197 | 198 | function BluetoothGATTCharacteristic(service, uuid, properties) { 199 | this._service = service; 200 | this._uuid = uuid; 201 | this._properties = properties; 202 | this._value = null; 203 | 204 | this._callRemote = function (method) { 205 | var self = this; 206 | var args = Array.prototype.slice.call(arguments).slice(1, arguments.length) 207 | return sendMessage("bluetooth:deviceMessage", { 208 | method: method, 209 | args: args, 210 | deviceId: self._service.device.id, 211 | uuid: self._uuid 212 | }) 213 | } 214 | } 215 | 216 | BluetoothGATTCharacteristic.prototype = { 217 | get service() { 218 | return this._service; 219 | }, 220 | get uuid() { 221 | return this._uuid; 222 | }, 223 | get properties() { 224 | return this._properties; 225 | }, 226 | get value() { 227 | return this._value; 228 | }, 229 | getDescriptor: function (descriptor) { 230 | var self = this; 231 | throw new Error('Not implemented'); 232 | }, 233 | getDescriptors: function (descriptor) { 234 | var self = this; 235 | }, 236 | readValue: function () { 237 | var self = this; 238 | return self._callRemote("BluetoothGATTCharacteristic.readValue", self._service.uuid, self._uuid) 239 | .then(function (valueEncoded) { 240 | self._value = str2ab(atob(valueEncoded)) 241 | console.log(valueEncoded,":",self._value) 242 | return new DataView(self._value,0); 243 | }); 244 | }, 245 | writeValue: function () { 246 | var self = this; 247 | }, 248 | startNotifications: function () { 249 | var self = this; 250 | return self._callRemote("BluetoothGATTCharacteristic.startNotifications") 251 | }, 252 | stopNotifications: function () { 253 | var self = this; 254 | return self._callRemote("BluetoothGATTCharacteristic.stopNotifications") 255 | } 256 | }; 257 | 258 | function BluetoothCharacteristicProperties() { 259 | 260 | } 261 | 262 | BluetoothCharacteristicProperties.prototype = { 263 | get broadcast() { 264 | return this._broadcast; 265 | }, 266 | get read() { 267 | return this._read; 268 | }, 269 | get writeWithoutResponse() { 270 | return this._writeWithoutResponse; 271 | }, 272 | get write() { 273 | return this._write; 274 | }, 275 | get notify() { 276 | return this._notify; 277 | }, 278 | get indicate() { 279 | return this._indicate; 280 | }, 281 | get authenticatedSignedWrites() { 282 | return this._authenticatedSignedWrites; 283 | }, 284 | get reliableWrite() { 285 | return this._reliableWrite; 286 | }, 287 | get writableAuxiliaries() { 288 | return this._writableAuxiliaries; 289 | } 290 | } 291 | 292 | function BluetoothGATTDescriptor(characteristic, uuid) { 293 | this._characteristic = characteristic; 294 | this._uuid = uuid; 295 | 296 | this._callRemote = function (method) { 297 | var self = this; 298 | var args = Array.prototype.slice.call(arguments).slice(1, arguments.length) 299 | return sendMessage("bluetooth:deviceMessage", { 300 | method: method, 301 | args: args, 302 | deviceId: self._characteristic.service.device.id, 303 | uuid: self._uuid 304 | }) 305 | } 306 | } 307 | 308 | BluetoothGATTDescriptor.prototype = { 309 | get characteristic() { 310 | return this._characteristic; 311 | }, 312 | get uuid() { 313 | return this._uuid; 314 | }, 315 | get writableAuxiliaries() { 316 | return this._value; 317 | }, 318 | readValue: function () { 319 | return callRemote("BluetoothGATTDescriptor.startNotifications") 320 | }, 321 | writeValue: function () { 322 | return callRemote("BluetoothGATTDescriptor.startNotifications") 323 | } 324 | }; 325 | 326 | function canonicalUUID(uuidAlias) { 327 | uuidAlias >>>= 0; // Make sure the number is positive and 32 bits. 328 | var strAlias = "0000000" + uuidAlias.toString(16); 329 | strAlias = strAlias.substr(-8); 330 | return strAlias + "-0000-1000-8000-00805f9b34fb" 331 | } 332 | 333 | var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; 334 | 335 | var BluetoothUUID = {}; 336 | BluetoothUUID.canonicalUUID = canonicalUUID; 337 | BluetoothUUID.service = { 338 | alert_notification: canonicalUUID(0x1811), 339 | automation_io: canonicalUUID(0x1815), 340 | battery_service: canonicalUUID(0x180F), 341 | blood_pressure: canonicalUUID(0x1810), 342 | body_composition: canonicalUUID(0x181B), 343 | bond_management: canonicalUUID(0x181E), 344 | continuous_glucose_monitoring: canonicalUUID(0x181F), 345 | current_time: canonicalUUID(0x1805), 346 | cycling_power: canonicalUUID(0x1818), 347 | cycling_speed_and_cadence: canonicalUUID(0x1816), 348 | device_information: canonicalUUID(0x180A), 349 | environmental_sensing: canonicalUUID(0x181A), 350 | generic_access: canonicalUUID(0x1800), 351 | generic_attribute: canonicalUUID(0x1801), 352 | glucose: canonicalUUID(0x1808), 353 | health_thermometer: canonicalUUID(0x1809), 354 | heart_rate: canonicalUUID(0x180D), 355 | human_interface_device: canonicalUUID(0x1812), 356 | immediate_alert: canonicalUUID(0x1802), 357 | indoor_positioning: canonicalUUID(0x1821), 358 | internet_protocol_support: canonicalUUID(0x1820), 359 | link_loss: canonicalUUID(0x1803), 360 | location_and_navigation: canonicalUUID(0x1819), 361 | next_dst_change: canonicalUUID(0x1807), 362 | phone_alert_status: canonicalUUID(0x180E), 363 | pulse_oximeter: canonicalUUID(0x1822), 364 | reference_time_update: canonicalUUID(0x1806), 365 | running_speed_and_cadence: canonicalUUID(0x1814), 366 | scan_parameters: canonicalUUID(0x1813), 367 | tx_power: canonicalUUID(0x1804), 368 | user_data: canonicalUUID(0x181C), 369 | weight_scale: canonicalUUID(0x181D) 370 | } 371 | 372 | BluetoothUUID.characteristic = { 373 | "aerobic_heart_rate_lower_limit": canonicalUUID(0x2A7E), 374 | "aerobic_heart_rate_upper_limit": canonicalUUID(0x2A84), 375 | "aerobic_threshold": canonicalUUID(0x2A7F), 376 | "age": canonicalUUID(0x2A80), 377 | "aggregate": canonicalUUID(0x2A5A), 378 | "alert_category_id": canonicalUUID(0x2A43), 379 | "alert_category_id_bit_mask": canonicalUUID(0x2A42), 380 | "alert_level": canonicalUUID(0x2A06), 381 | "alert_notification_control_point": canonicalUUID(0x2A44), 382 | "alert_status": canonicalUUID(0x2A3F), 383 | "altitude": canonicalUUID(0x2AB3), 384 | "anaerobic_heart_rate_lower_limit": canonicalUUID(0x2A81), 385 | "anaerobic_heart_rate_upper_limit": canonicalUUID(0x2A82), 386 | "anaerobic_threshold": canonicalUUID(0x2A83), 387 | "analog": canonicalUUID(0x2A58), 388 | "apparent_wind_direction": canonicalUUID(0x2A73), 389 | "apparent_wind_speed": canonicalUUID(0x2A72), 390 | "gap.appearance": canonicalUUID(0x2A01), 391 | "barometric_pressure_trend": canonicalUUID(0x2AA3), 392 | "battery_level": canonicalUUID(0x2A19), 393 | "blood_pressure_feature": canonicalUUID(0x2A49), 394 | "blood_pressure_measurement": canonicalUUID(0x2A35), 395 | "body_composition_feature": canonicalUUID(0x2A9B), 396 | "body_composition_measurement": canonicalUUID(0x2A9C), 397 | "body_sensor_location": canonicalUUID(0x2A38), 398 | "bond_management_control_point": canonicalUUID(0x2AA4), 399 | "bond_management_feature": canonicalUUID(0x2AA5), 400 | "boot_keyboard_input_report": canonicalUUID(0x2A22), 401 | "boot_keyboard_output_report": canonicalUUID(0x2A32), 402 | "boot_mouse_input_report": canonicalUUID(0x2A33), 403 | "gap.central_address_resolution_support": canonicalUUID(0x2AA6), 404 | "cgm_feature": canonicalUUID(0x2AA8), 405 | "cgm_measurement": canonicalUUID(0x2AA7), 406 | "cgm_session_run_time": canonicalUUID(0x2AAB), 407 | "cgm_session_start_time": canonicalUUID(0x2AAA), 408 | "cgm_specific_ops_control_point": canonicalUUID(0x2AAC), 409 | "cgm_status": canonicalUUID(0x2AA9), 410 | "csc_feature": canonicalUUID(0x2A5C), 411 | "csc_measurement": canonicalUUID(0x2A5B), 412 | "current_time": canonicalUUID(0x2A2B), 413 | "cycling_power_control_point": canonicalUUID(0x2A66), 414 | "cycling_power_feature": canonicalUUID(0x2A65), 415 | "cycling_power_measurement": canonicalUUID(0x2A63), 416 | "cycling_power_vector": canonicalUUID(0x2A64), 417 | "database_change_increment": canonicalUUID(0x2A99), 418 | "date_of_birth": canonicalUUID(0x2A85), 419 | "date_of_threshold_assessment": canonicalUUID(0x2A86), 420 | "date_time": canonicalUUID(0x2A08), 421 | "day_date_time": canonicalUUID(0x2A0A), 422 | "day_of_week": canonicalUUID(0x2A09), 423 | "descriptor_value_changed": canonicalUUID(0x2A7D), 424 | "gap.device_name": canonicalUUID(0x2A00), 425 | "dew_point": canonicalUUID(0x2A7B), 426 | "digital": canonicalUUID(0x2A56), 427 | "dst_offset": canonicalUUID(0x2A0D), 428 | "elevation": canonicalUUID(0x2A6C), 429 | "email_address": canonicalUUID(0x2A87), 430 | "exact_time_256": canonicalUUID(0x2A0C), 431 | "fat_burn_heart_rate_lower_limit": canonicalUUID(0x2A88), 432 | "fat_burn_heart_rate_upper_limit": canonicalUUID(0x2A89), 433 | "firmware_revision_string": canonicalUUID(0x2A26), 434 | "first_name": canonicalUUID(0x2A8A), 435 | "five_zone_heart_rate_limits": canonicalUUID(0x2A8B), 436 | "floor_number": canonicalUUID(0x2AB2), 437 | "gender": canonicalUUID(0x2A8C), 438 | "glucose_feature": canonicalUUID(0x2A51), 439 | "glucose_measurement": canonicalUUID(0x2A18), 440 | "glucose_measurement_context": canonicalUUID(0x2A34), 441 | "gust_factor": canonicalUUID(0x2A74), 442 | "hardware_revision_string": canonicalUUID(0x2A27), 443 | "heart_rate_control_point": canonicalUUID(0x2A39), 444 | "heart_rate_max": canonicalUUID(0x2A8D), 445 | "heart_rate_measurement": canonicalUUID(0x2A37), 446 | "heat_index": canonicalUUID(0x2A7A), 447 | "height": canonicalUUID(0x2A8E), 448 | "hid_control_point": canonicalUUID(0x2A4C), 449 | "hid_information": canonicalUUID(0x2A4A), 450 | "hip_circumference": canonicalUUID(0x2A8F), 451 | "humidity": canonicalUUID(0x2A6F), 452 | "ieee_11073-20601_regulatory_certification_data_list": canonicalUUID(0x2A2A), 453 | "indoor_positioning_configuration": canonicalUUID(0x2AAD), 454 | "intermediate_blood_pressure": canonicalUUID(0x2A36), 455 | "intermediate_temperature": canonicalUUID(0x2A1E), 456 | "irradiance": canonicalUUID(0x2A77), 457 | "language": canonicalUUID(0x2AA2), 458 | "last_name": canonicalUUID(0x2A90), 459 | "latitude": canonicalUUID(0x2AAE), 460 | "ln_control_point": canonicalUUID(0x2A6B), 461 | "ln_feature": canonicalUUID(0x2A6A), 462 | "local_east_coordinate.xml": canonicalUUID(0x2AB1), 463 | "local_north_coordinate": canonicalUUID(0x2AB0), 464 | "local_time_information": canonicalUUID(0x2A0F), 465 | "location_and_speed": canonicalUUID(0x2A67), 466 | "location_name": canonicalUUID(0x2AB5), 467 | "longitude": canonicalUUID(0x2AAF), 468 | "magnetic_declination": canonicalUUID(0x2A2C), 469 | "magnetic_flux_density_2D": canonicalUUID(0x2AA0), 470 | "magnetic_flux_density_3D": canonicalUUID(0x2AA1), 471 | "manufacturer_name_string": canonicalUUID(0x2A29), 472 | "maximum_recommended_heart_rate": canonicalUUID(0x2A91), 473 | "measurement_interval": canonicalUUID(0x2A21), 474 | "model_number_string": canonicalUUID(0x2A24), 475 | "navigation": canonicalUUID(0x2A68), 476 | "new_alert": canonicalUUID(0x2A46), 477 | "gap.peripheral_preferred_connection_parameters": canonicalUUID(0x2A04), 478 | "gap.peripheral_privacy_flag": canonicalUUID(0x2A02), 479 | "plx_continuous_measurement": canonicalUUID(0x2A5F), 480 | "plx_features": canonicalUUID(0x2A60), 481 | "plx_spot_check_measurement": canonicalUUID(0x2A5E), 482 | "pnp_id": canonicalUUID(0x2A50), 483 | "pollen_concentration": canonicalUUID(0x2A75), 484 | "position_quality": canonicalUUID(0x2A69), 485 | "pressure": canonicalUUID(0x2A6D), 486 | "protocol_mode": canonicalUUID(0x2A4E), 487 | "rainfall": canonicalUUID(0x2A78), 488 | "gap.reconnection_address": canonicalUUID(0x2A03), 489 | "record_access_control_point": canonicalUUID(0x2A52), 490 | "reference_time_information": canonicalUUID(0x2A14), 491 | "report": canonicalUUID(0x2A4D), 492 | "report_map": canonicalUUID(0x2A4B), 493 | "resting_heart_rate": canonicalUUID(0x2A92), 494 | "ringer_control_point": canonicalUUID(0x2A40), 495 | "ringer_setting": canonicalUUID(0x2A41), 496 | "rsc_feature": canonicalUUID(0x2A54), 497 | "rsc_measurement": canonicalUUID(0x2A53), 498 | "sc_control_point": canonicalUUID(0x2A55), 499 | "scan_interval_window": canonicalUUID(0x2A4F), 500 | "scan_refresh": canonicalUUID(0x2A31), 501 | "sensor_location": canonicalUUID(0x2A5D), 502 | "serial_number_string": canonicalUUID(0x2A25), 503 | "gatt.service_changed": canonicalUUID(0x2A05), 504 | "software_revision_string": canonicalUUID(0x2A28), 505 | "sport_type_for_aerobic_and_anaerobic_thresholds": canonicalUUID(0x2A93), 506 | "supported_new_alert_category": canonicalUUID(0x2A47), 507 | "supported_unread_alert_category": canonicalUUID(0x2A48), 508 | "system_id": canonicalUUID(0x2A23), 509 | "temperature": canonicalUUID(0x2A6E), 510 | "temperature_measurement": canonicalUUID(0x2A1C), 511 | "temperature_type": canonicalUUID(0x2A1D), 512 | "three_zone_heart_rate_limits": canonicalUUID(0x2A94), 513 | "time_accuracy": canonicalUUID(0x2A12), 514 | "time_source": canonicalUUID(0x2A13), 515 | "time_update_control_point": canonicalUUID(0x2A16), 516 | "time_update_state": canonicalUUID(0x2A17), 517 | "time_with_dst": canonicalUUID(0x2A11), 518 | "time_zone": canonicalUUID(0x2A0E), 519 | "true_wind_direction": canonicalUUID(0x2A71), 520 | "true_wind_speed": canonicalUUID(0x2A70), 521 | "two_zone_heart_rate_limit": canonicalUUID(0x2A95), 522 | "tx_power_level": canonicalUUID(0x2A07), 523 | "uncertainty": canonicalUUID(0x2AB4), 524 | "unread_alert_status": canonicalUUID(0x2A45), 525 | "user_control_point": canonicalUUID(0x2A9F), 526 | "user_index": canonicalUUID(0x2A9A), 527 | "uv_index": canonicalUUID(0x2A76), 528 | "vo2_max": canonicalUUID(0x2A96), 529 | "waist_circumference": canonicalUUID(0x2A97), 530 | "weight": canonicalUUID(0x2A98), 531 | "weight_measurement": canonicalUUID(0x2A9D), 532 | "weight_scale_feature": canonicalUUID(0x2A9E), 533 | "wind_chill": canonicalUUID(0x2A79) 534 | }; 535 | 536 | BluetoothUUID.descriptor = { 537 | "gatt.characteristic_extended_properties": canonicalUUID(0x2900), 538 | "gatt.characteristic_user_description": canonicalUUID(0x2901), 539 | "gatt.client_characteristic_configuration": canonicalUUID(0x2902), 540 | "gatt.server_characteristic_configuration": canonicalUUID(0x2903), 541 | "gatt.characteristic_presentation_format": canonicalUUID(0x2904), 542 | "gatt.characteristic_aggregate_format": canonicalUUID(0x2905), 543 | "valid_range": canonicalUUID(0x2906), 544 | "external_report_reference": canonicalUUID(0x2907), 545 | "report_reference": canonicalUUID(0x2908), 546 | "value_trigger_setting": canonicalUUID(0x290A), 547 | "es_configuration": canonicalUUID(0x290B), 548 | "es_measurement": canonicalUUID(0x290C), 549 | "es_trigger_setting": canonicalUUID(0x290D) 550 | }; 551 | 552 | function ResolveUUIDName(tableName) { 553 | var table = BluetoothUUID[tableName]; 554 | return function (name) { 555 | if (typeof name === "number") { 556 | return canonicalUUID(name); 557 | } else if (uuidRegex.test(name.toLowerCase())) { 558 | //note native IOS bridges converts to uppercase since IOS seems to demand this. 559 | return name.toLowerCase(); 560 | } else if (table.hasOwnProperty(name)) { 561 | return table[name]; 562 | } else { 563 | throw new Error('SyntaxError: "' + name + '" is not a known ' + tableName + ' name.'); 564 | } 565 | } 566 | } 567 | 568 | BluetoothUUID.getService = ResolveUUIDName('service'); 569 | BluetoothUUID.getCharacteristic = ResolveUUIDName('characteristic'); 570 | BluetoothUUID.getDescriptor = ResolveUUIDName('descriptor'); 571 | 572 | 573 | var bluetooth = {}; 574 | bluetooth.requestDevice = function (requestDeviceOptions) { 575 | if (!requestDeviceOptions.filters || requestDeviceOptions.filters.length === 0) { 576 | throw new TypeError('The first argument to navigator.bluetooth.requestDevice() must have a non-zero length filters parameter'); 577 | } 578 | var validatedDeviceOptions = {} 579 | 580 | var filters = requestDeviceOptions.filters; 581 | filters = filters.map(function (filter) { 582 | return { 583 | services: filter.services.map(window.BluetoothUUID.getService), 584 | name: filter.name, 585 | namePrefix: filter.namePrefix 586 | }; 587 | }); 588 | validatedDeviceOptions.filters = filters; 589 | validatedDeviceOptions.name = filters; 590 | validatedDeviceOptions.filters = filters; 591 | 592 | 593 | var optionalServices = requestDeviceOptions.optionalService; 594 | if (optionalServices) { 595 | optionalServices = optionalServices.services.map(window.BluetoothUUID.getService) 596 | validatedDeviceOptions.optionalServices = optionalServices; 597 | } 598 | 599 | return sendMessage("bluetooth:requestDevice", validatedDeviceOptions) 600 | .then(function (deviceJSON) { 601 | var device = JSON.parse(deviceJSON); 602 | return new BluetoothDevice(device); 603 | }).catch(function (e) { 604 | console.log("Error starting to search for device", e); 605 | }); 606 | } 607 | 608 | 609 | ////////////Communication with Native 610 | var _messageCount = 0; 611 | var _callbacks = {}; // callbacks for responses to requests 612 | 613 | function sendMessage(type, data) { 614 | 615 | var callbackID, message; 616 | callbackID = _messageCount; 617 | 618 | if (typeof type == 'undefined') { 619 | throw "CallRemote should never be called without a type!" 620 | } 621 | 622 | message = { 623 | type: type, 624 | data: data, 625 | callbackID: callbackID 626 | }; 627 | 628 | console.log("<--", message); 629 | window.webkit.messageHandlers.bluetooth.postMessage(message); 630 | 631 | _messageCount++; 632 | return new Promise(function (resolve, reject) { 633 | _callbacks[callbackID] = function (success, result) { 634 | if (success) { 635 | resolve(result); 636 | } else { 637 | reject(result); 638 | } 639 | return delete _callbacks[callbackID]; 640 | }; 641 | }); 642 | } 643 | 644 | function recieveMessage(messageType, success, resultString, callbackID) { 645 | console.log("-->", messageType, success, resultString, callbackID); 646 | 647 | switch (messageType) { 648 | case "response": 649 | console.log("result:", resultString) 650 | _callbacks[callbackID](success, resultString); 651 | break; 652 | default: 653 | console.log("Unrecognised message from native:" + message); 654 | } 655 | } 656 | 657 | function NamedError(name, message) { 658 | var e = new Error(message || ''); 659 | e.name = name; 660 | return e; 661 | }; 662 | 663 | //Safari 9 doesn't have TextDecoder API 664 | function ab2str(buf) { 665 | return String.fromCharCode.apply(null, new Uint16Array(buf)); 666 | } 667 | 668 | function str2ab(str) { 669 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 670 | var bufView = new Uint16Array(buf); 671 | for (var i = 0, strLen = str.length; i < strLen; i++) { 672 | bufView[i] = str.charCodeAt(i); 673 | } 674 | return buf; 675 | } 676 | 677 | 678 | //Exposed interfaces 679 | window.BluetoothDevice = BluetoothDevice; 680 | window.BluetoothUUID = BluetoothUUID; 681 | window.recieveMessage = recieveMessage; 682 | navigator.bluetooth = bluetooth; 683 | window.BluetoothUUID = BluetoothUUID; 684 | 685 | })(); --------------------------------------------------------------------------------