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 | })();
--------------------------------------------------------------------------------