This Gist was generated by Contrived.
Do not modify the metadata file if you want to open in Contrived again. Otherwise, it is safe to delete.
Happy Hacking!
{"user":"5f0c542a4a2ce5e528e01fdf","templateVersion":"1","templateId":"vanillajs","resources":["<meta charset=\"UTF-8\" />","<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">","<link href=\"https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css\" rel=\"stylesheet\" />"],"dependencies":[],"files":[{"id":1,"parentId":0,"name":"src","path":"/src","type":"folder","isRoot":true,"selected":false,"expanded":true,"children":[{"id":3,"name":"index.js"},{"id":4,"name":"styles.css"}]},{"id":2,"parentId":0,"name":"index.html","path":"/index.html","type":"file","mimeType":"html","isRoot":true,"open":true,"selected":true,"content":"<div class=\"flex flex-col max-w-xl m-auto\">\n <h1 class=\"text-3xl\">AirPods WebBluetooth Demo</h1>\n <aside>\n <a\n class=\"text-blue-600\"\n href=\"https://codesandbox.io/s/airpods-webbluetooth-demo-c4jfh\"\n >Source</a\n >\n </aside>\n <div class=\"my-4\">\n Example usage of the\n <a\n class=\"text-blue-600\"\n href=\"https://webbluetoothcg.github.io/web-bluetooth/scanning.html\"\n >WebBluetooth Scanning API</a\n >; using <code class=\"tracking-tight\">requestLEScan</code> to listen for\n Apple AirPods Manufacture Data. <br /><br />\n <strong\n >If the AirPods are not paird with this device the battery percentage\n is only report in 10% increments.</strong\n >\n\n <blockquote class=\"bg-green-200 p-4 mt-4 -mx-4\">\n <h4 class=\"font-bold\">Note:</h4>\n Scanning is still under development. You must be using Chrome 79+ with\n the\n <code class=\"tracking-tighter\">\n chrome://flags/#enable-experimental-web-platform-features\n </code>\n flag enabled. <br />\n <a href=\"chrome://flags/\">open chrome flags: chrome://flags/</a><br />\n Bluetooth scanning is currently (as of 25.04.2020) only supported on\n Android and Mac see\n <a\n class=\"text-blue-500\"\n href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=897312&desc=2\"\n >Chrome issue</a\n >\n </blockquote>\n </div>\n <div class=\"hidden\" id=\"🎧\">\n <div\n class=\"rounded flex-grow border-solid border-4 border-gray-600 m-1\"\n >\n <div class=\"px-6 py-2\">\n <div class=\"font-bold text-xl text-center mb-2\">\n <span id=\"name\"></span>\n <span class=\"invisible\" id=\"airpods-connected\">🔊</span>\n </div>\n </div>\n </div>\n <div class=\"flex flex-grow\">\n <div\n class=\"rounded flex-grow border-solid border-4 border-gray-600 m-1\"\n >\n <div class=\"px-6 py-2\">\n <div class=\"font-bold text-xl text-center mb-2\">Left</div>\n <div class=\"text-gray-700 text-base text-center\">\n <div class=\"inline-block w-13\">\n <span id=\"left-charging\" class=\"invisible\">⚡</span>\n <span id=\"left-battery\">-</span>\n <span>%</span>\n </div>\n </div>\n </div>\n </div>\n <div\n class=\"rounded flex-grow border-solid border-4 border-gray-600 m-1\"\n >\n <div class=\"px-6 py-2\">\n <div class=\"font-bold text-xl text-center mb-2\">Right</div>\n <div class=\"text-gray-700 text-base text-center\">\n <div class=\"inline-block w-13\">\n <span id=\"right-charging\" class=\"invisible\">⚡</span>\n <span id=\"right-battery\">-</span>\n <span>%</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div\n class=\"rounded flex-grow border-solid border-4 border-gray-600 m-1\"\n >\n <div class=\"px-6 py-2\">\n <div class=\"font-bold text-xl text-center mb-2\">Case</div>\n <div class=\"text-gray-700 text-base text-center\">\n <div class=\"inline-block w-13\">\n <span id=\"case-charging\" class=\"invisible\">⚡</span>\n <span id=\"case-battery\">-</span>\n <span>%</span>\n <span id=\"case-lidOpen\"> (Open)</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"hidden font-bold text-center text-green-700\" id=\"connecting\">\n Please allow the scaning request and open your AirPods next to this\n device.\n </div>\n <div class=\"mb-3 hidden font-bold text-center text-red-700\" id=\"error\">\n Filled by JS\n </div>\n <button\n id=\"scan\"\n class=\"bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded\"\n >\n Start Scanning for AirPods\n </button>\n </div>"},{"id":3,"parentId":1,"name":"index.js","path":"/src/index.js","type":"file","mimeType":"js","isRoot":false,"open":true,"selected":false,"isEntry":true,"content":"class AirPodsBatteryScanner {\n constructor() {\n this.listener = new Set();\n this.deviceMap = {\n \"8194\": \"AirPods\",\n \"8195\": \"Powerbeats\",\n \"8197\": \"BeatsX\",\n \"8198\": \"Beats Solo³\",\n \"8199\": \"Beats Studio³\",\n \"8201\": \"Beats Studio³\",\n \"8203\": \"Powerbeats Pro\",\n \"8204\": \"Beats Solo Pro\",\n \"8205\": \"Powerbeats\",\n \"8206\": \"AirPods Pro\",\n \"8207\": \"AirPods (2nd gen.)\"\n };\n }\n\n // Musst be called from a user interaction\n async startScanning() {\n let options = {\n // Filter isn't working in chrome yet\n /*filters: [\n {\n manufacturerData: {\n 0x004c: {\n dataPrefix: new Uint8Array([0x07])\n }\n }\n }\n ],*/\n keepRepeatedDevices: true,\n acceptAllAdvertisements: true\n };\n\n const scan = await navigator.bluetooth.requestLEScan(options);\n if (scan.active) {\n navigator.bluetooth.addEventListener(\"advertisementreceived\", event => {\n // Filter for APPL AirPod data...\n // chrome has no support for it in the request yet.\n const applData = event.manufacturerData.get(0x004c); // APPL Key;\n if (applData) {\n const data = new Uint8Array(applData.buffer);\n if (data[0] === 0x07 && data[1] === 0x19) {\n const deviceId = \"\" + ((data[4] << 8) + data[3]);\n const dataSpec = data[5];\n\n const leftFirst = (dataSpec & 0b00100000) !== 0;\n const hasCaseData = (dataSpec & 0b00010000) !== 0;\n const hasOther = (dataSpec & 0b00000001) !== 0;\n const connected = (dataSpec & 0b00000010) !== 0;\n const lidOpen = (data[8] & 0b00001000) === 0;\n const lidOpenCounter = data[8] & 0b00000111;\n\n // data[21] && data[22] where always 0 on decrypted deviced.\n // No idea if this is always true\n const decrypted = data[21] === 0 && data[22] === 0;\n\n let status = {\n device: {\n id: deviceId,\n name: this._getDeviceName(deviceId),\n decrypted,\n connected\n }\n };\n\n let firstCharging, firstBattery;\n let secondCharging, secondBattery;\n let batteryCaseCharging, batteryCase;\n\n // if the AirPods where paird\n if (decrypted) {\n firstCharging = (data[12] & 0b10000000) !== 0;\n firstBattery = data[12] & 0b01111111;\n\n secondCharging = (data[13] & 0b10000000) !== 0;\n secondBattery = data[13] & 0b01111111;\n\n batteryCaseCharging = (data[14] & 0b10000000) !== 0;\n batteryCase = data[14] & 0b01111111;\n // Not paird only low resolution data avaiable\n } else {\n firstCharging = (data[7] & 0b00010000) !== 0;\n firstBattery = ((data[6] >> 4) & 0xf) * 10;\n\n secondCharging = (data[7] & 0b00100000) !== 0;\n secondBattery = (data[6] & 0xf) * 10;\n\n batteryCaseCharging = (data[7] & 0b01000000) !== 0;\n batteryCase = (data[7] & 0xf) * 10;\n }\n\n if (leftFirst) {\n status.left = {\n charging: firstCharging,\n battery: firstBattery\n };\n if (hasOther) {\n status.right = {\n charging: secondCharging,\n battery: secondBattery\n };\n }\n } else {\n status.right = {\n charging: firstCharging,\n battery: firstBattery\n };\n if (hasOther) {\n status.left = {\n charging: secondCharging,\n battery: secondBattery\n };\n }\n }\n if (hasCaseData) {\n status.case = {\n charging: batteryCaseCharging,\n battery: batteryCase,\n lidOpen,\n lidOpenCounter\n };\n }\n\n this._fireStatusEvent(status);\n }\n }\n });\n }\n }\n\n onBatteryChange(callback) {\n this.listener.add(callback);\n }\n\n removeBatteryChange(callback) {\n this.listener.delete(callback);\n }\n\n _fireStatusEvent(event) {\n for (let callback of this.listener) {\n callback(event);\n }\n }\n _getDeviceName(deviceId) {\n return this.deviceMap[deviceId] || `Unkown (${deviceId})`;\n }\n}\n\nconst toHex = valueDataView => {\n return [...new Uint8Array(valueDataView.buffer)]\n .map(b => {\n return b.toString(16).padStart(2, \"0\");\n })\n .join(\" \");\n};\n\nconst toBin = valueDataView => {\n return [...new Uint8Array(valueDataView.buffer)]\n .map(b => {\n return b.toString(2).padStart(8, \"0\");\n })\n .join(\" \");\n};\n\nconst airPods = new AirPodsBatteryScanner();\n\nconst $scanBtn = document.querySelector(\"#scan\");\nconst $name = document.querySelector(\"#name\");\nconst $airpodsStatus = document.querySelector(\"#🎧\");\nconst $airpodsConected = document.querySelector(\"#airpods-connected\");\nconst $connecting = document.querySelector(\"#connecting\");\nconst $error = document.querySelector(\"#error\");\nconst leftSetter = createValueSetters(\"left\");\nconst rightSetter = createValueSetters(\"right\");\nconst caseSetter = createValueSetters(\"case\", name => {\n const $lidOpen = document.querySelector(`#${name}-lidOpen`);\n return value => {\n if (value) {\n $lidOpen.textContent = value.lidOpen ? \"🔓\" : \"🔒\";\n }\n };\n});\n\n$scanBtn.addEventListener(\"click\", onButtonClick);\n\nasync function onButtonClick() {\n airPods.onBatteryChange(status => {\n $airpodsStatus.classList.remove(\"hidden\");\n $connecting.classList.add(\"hidden\");\n\n $name.textContent = status.device.name;\n $airpodsConected.style.visibility = status.device.connected\n ? \"visible\"\n : \"hidden\";\n\n leftSetter(status.left);\n rightSetter(status.right);\n caseSetter(status.case);\n });\n try {\n $connecting.classList.remove(\"hidden\");\n $error.classList.add(\"hidden\");\n $scanBtn.classList.add(\"hidden\");\n await airPods.startScanning();\n } catch (e) {\n $connecting.classList.add(\"hidden\");\n $scanBtn.classList.remove(\"hidden\");\n $error.classList.remove(\"hidden\");\n let message = e.message;\n if (message.includes(\"blocked by user\")) {\n message +=\n \"<br /> You can re-enable request for BLE Scanning by clicking the lock icon next to the URL in the address bar.\";\n }\n $error.innerHTML = message;\n }\n}\n\nfunction createValueSetters(name, setup) {\n const $battery = document.querySelector(`#${name}-battery`);\n const $charging = document.querySelector(`#${name}-charging`);\n const addition = setup && setup(name);\n return function(value) {\n if (value) {\n $battery.textContent = value.battery;\n $charging.style.visibility = value.charging ? \"visible\" : \"hidden\";\n }\n addition && addition(value);\n };\n}\n"},{"id":4,"parentId":1,"name":"styles.css","path":"/src/styles.css","type":"file","mimeType":"css","isRoot":false,"open":false,"selected":false,"content":"body {\n font-family: sans-serif;\n}\n"}],"experimentId":"5f7df5a9f4bb6c001798e54b"} |