<repo_name>tungedison/EddystoneBeaconScanner<file_sep>/src/beacon/index.ts
export * from './Beacon';
export * from './BeaconService';
<file_sep>/src/index.ts
export * from './beacon';
export * from './Eddystone';
<file_sep>/demo/src/logic/try-it-out.js
import { Eddystone } from 'eddystone-web-bluetooth';

export default function tryItOut() {
  var eddystone = new Eddystone();
  var beacon, service;
  eddystone.request() // Scan for Eddystone beacons.
    .then((newBeacon) => {
      beacon = newBeacon;
      return beacon.connect(); // Connect to the Beacon's GATT service.
    })
    .then((newService) => {
      service = newService;
      return service.isLocked(); // Check if the beacon is locked.
    })
    .then((isLocked) => {
      if (isLocked) {
        return Promise.reject('The beacon is locked. Can\'t write new URL');
      }
      // Beacon's not locked. We can proceed with the recording of the new URL.
      // Keep in mind that the encoded URL must NOT be longer than 18 characters.
      return service.writeUrl('https://goo.gl/XXw2hi');
    })
    .then(() => {
      beacon.disconnect();
      alert('Beacon has been written!');
    });
}<file_sep>/README.md
# Eddystone Web Bluetooth

> Web Bluetooth Eddystone made easier

[![Build Status](https://travis-ci.org/zurfyx/eddystone-web-bluetooth.svg?branch=master)](https://travis-ci.org/zurfyx/eddystone-web-bluetooth)
[![David](https://david-dm.org/zurfyx/eddystone-web-bluetooth.svg)](https://david-dm.org/zurfyx/eddystone-web-bluetooth)
[![David](https://david-dm.org/zurfyx/eddystone-web-bluetooth/dev-status.svg)](https://david-dm.org/zurfyx/eddystone-web-bluetooth#info=devDependencies)
[![Code Climate](https://codeclimate.com/github/zurfyx/eddystone-web-bluetooth/badges/gpa.svg)](https://codeclimate.com/github/zurfyx/eddystone-web-bluetooth)

<p align="center">
  <img src="./assets/demo.gif" width="500" /><br />
  <a href="#getting-started">Getting started source-code</a> using <a href="https://twitter.com/ThePhysicalWeb/status/770262699766755329">Physical Web beacons</a>
</p>

## Features

- [x] Scan Eddystone beacons
- [x] Connect / Disconnect
- [x] Monitor connection status
- [ ] Read Capabilities
- [ ] Read / Write Active Slot
- [x] Read / Write Advertising Interval
- [x] Read / Write Radio Tx Power
- [x] Read / Write Advertised Tx Power
- [x] Read Lock State
- [ ] Write Lock State
- [ ] Read / Write Unlock
- [ ] Read Public ECDH Key
- [ ] Read EID Identity Key
- [x] Read / Write ADV Slot Data
- [x] Write Factory reset
- [ ] Read / Write Remain Connectable

## Getting started

```
npm install --save eddystone-web-bluetooth
```

```javascript
var eddystone = new Eddystone();
var beacon, service;
eddystone.request() // Scan for Eddystone beacons.
  .then((newBeacon) => {
    beacon = newBeacon;
    return beacon.connect(); // Connect to the Beacon's GATT service.
  })
  .then((newService) => {
    service = newService;
    return service.isLocked(); // Check if the beacon is locked.
  })
  .then((isLocked) => {
    if (isLocked) {
      return Promise.reject('The beacon is locked. Can\'t write new URL');
    }
    // Beacon's not locked. We can proceed with the recording of the new URL.
    // Keep in mind that the encoded URL must NOT be longer than 18 characters.
    return service.writeUrl('https://www.google.com');
  })
  .then(() => {
    beacon.disconnect();
    alert('OK!');
  });
```

See the rest of the services [here](https://github.com/zurfyx/eddystone-web-bluetooth/blob/master/src/beacon/BeaconService.ts).

## Development

Eddystone Web Bluetooth implementation is based on the official specifications:

[https://github.com/google/eddystone/tree/master/configuration-service](https://github.com/google/eddystone/tree/master/configuration-service)

## Contributions

Contributions are very welcome.

## License

MIT © [<NAME>](//zurfyx.com)

----

Special thanks to @beaufortfrancois for providing https://github.com/beaufortfrancois/sandbox/blob/gh-pages/web-bluetooth/eddystone-url-config/app.js magnificent example source code.
<file_sep>/src/constants.ts
export default Object.freeze({
  EDDYSTONE_CONFIG_SERVICE_UUID:                    'a3c87500-8ed3-4bdf-8a39-a01bebede295',
  CAPABILITIES_CHARACTERISTIC_UUID:                 'a3c87501-8ed3-4bdf-8a39-a01bebede295',
  ACTIVE_SLOT_CHARACTERISTIC_UUID:                  'a3c87502-8ed3-4bdf-8a39-a01bebede295',
  ADVERTISING_INTERVAL_CHARACTERISTIC_UUID:         'a3c87503-8ed3-4bdf-8a39-a01bebede295',
  RADIO_TX_POWER_CHARACTERISTIC_UUID:               'a3c87504-8ed3-4bdf-8a39-a01bebede295',
  ADVANCED_ADVERTISED_TX_POWER_CHARACTERISTIC_UUID: 'a3c87505-8ed3-4bdf-8a39-a01bebede295',
  EDDYSTONE_LOCK_STATE_CHARACTERISTIC_UUID:         'a3c87506-8ed3-4bdf-8a39-a01bebede295',
  EDDYSTONE_UNLOCK_CHARACTERISTIC_UUID:             'a3c87507-8ed3-4bdf-8a39-a01bebede295',
  PUBLIC_ECDH_KEY_CHARACTERISTIC_UUID:              'a3c87508-8ed3-4bdf-8a39-a01bebede295',
  EID_IDENTITY_KEY_CHARACTERISTIC_UUID:             'a3c87509-8ed3-4bdf-8a39-a01bebede295',
  ADV_SLOT_DATA_CHARACTERISTIC_UUID:                'a3c8750a-8ed3-4bdf-8a39-a01bebede295',
  ADVANCED_FACTORY_RESET_CHARACTERISTIC_UUID:       'a3c8750b-8ed3-4bdf-8a39-a01bebede295',
  ADVANCED_REMAIN_CONNECTABLE_CHARACTERISTIC_UUID:  'a3c8750c-8ed3-4bdf-8a39-a01bebede295',
});
<file_sep>/demo/src/App.js
import React, { Component } from 'react';

import tryItOut from './logic/try-it-out';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>Eddystone Web Bluetooth</h1>
        <button className="big" onClick={() => tryItOut()}>Try it out!</button>
      </div>
    );
  }
}

export default App;
<file_sep>/demo/README.md
# Demo

This folder contains working examples of `eddystone-web-bluetooth`, running on a very simple React setup.

The relevant generic bits of code are located into `src/logic`.<file_sep>/src/beacon/enums.ts
enum LOCK_VALUES {
  LOCKED = 0x00,
  UNLOCKED = 0x01,
  UNLOCKED_AND_AUTOMATIC_RELOCK_DISABLED = 0x02,
}

enum DATA_VALUES {
  UID = 0x00,
  URL = 0x10,
  TLM = 0x20,
  EID = 0x40,
}

export {
  LOCK_VALUES,
  DATA_VALUES,
};
<file_sep>/src/beacon/__tests__/url.test.ts
import { decodeUrl, encodeUrl } from '../url';

const URL1 = 'https://example.com';
const URL2 = 'https://www.npmjs.com/features';

it('should decode the same value it has encoded', () => {
  const result1 = decodeUrl(encodeUrl(URL1));
  expect(result1).toBe(URL1);

  const result2 = decodeUrl(encodeUrl(URL2));
  expect(result2).toBe(URL2);
});

it('the encoded value should be as short as possible (by using prefixed codes)', () => {
  const result1 = encodeUrl(URL1);
  expect(result1.byteLength).toBe(9);

  const result2 = encodeUrl(URL2);
  expect(result2.byteLength).toBe(15);
});
<file_sep>/src/Eddystone.ts
import constants from './constants';
import { Beacon } from './beacon';

export class Eddystone {

  async request(): Promise<Beacon> {
    const bluetooth: Bluetooth = navigator.bluetooth;
    if (!bluetooth) {
      return Promise.reject('Your browser does not support Web Bluetooth.');
    }
    const requestOptions = { filters: [{ services: [constants.EDDYSTONE_CONFIG_SERVICE_UUID] }] };
    const device = await bluetooth.requestDevice(requestOptions);
    return new Beacon(device);
  }
}
<file_sep>/src/beacon/Beacon.ts
import constants from '../constants';
import { BeaconService } from './BeaconService';

export class Beacon {

  constructor(public device: BluetoothDevice) {}

  onDisconnect(listener: (this: this, ev: Event) => any) {
    this.device.addEventListener('gattserverdisconnected', listener);
  }

  async connect(): Promise<BeaconService> {
    if (!this.device.gatt) {
      return Promise.reject('Bluetooth device is probably not a beacon - it does not support GATT');
    }
    const bluetoothGattServer = await this.device.gatt.connect();
    const service = await bluetoothGattServer
      .getPrimaryService(constants.EDDYSTONE_CONFIG_SERVICE_UUID);
    return new BeaconService(service);
  }

  disconnect() : void {
    const gatt: BluetoothRemoteGATTServer | undefined = this.device.gatt;
    if (!(gatt && gatt.connected)) {
      console.warn('Ignored disconnection request. You are not connected!');
      return;
    }
    gatt.disconnect();
  }
}
<file_sep>/src/beacon/BeaconService.ts
import constants from '../constants';
import { LOCK_VALUES, DATA_VALUES } from './enums';
import { decodeUrl, encodeUrl } from './url';

export class BeaconService {

  constructor(public service: BluetoothRemoteGATTService) {}

  private async readCharacteristic(uuid: string): Promise<DataView> {
    const characteristic = await this.service.getCharacteristic(uuid);
    return characteristic.readValue();
  }

  private async writeCharacteristic(uuid: string, value: BufferSource): Promise<void> {
    const characteristic = await this.service.getCharacteristic(uuid);
    return characteristic.writeValue(value);
  }

  /**
   * Interval.
   */
  async readInterval(): Promise<number> {
    const uuid = constants.ADVERTISING_INTERVAL_CHARACTERISTIC_UUID;
    const rawVal = await this.readCharacteristic(uuid);
    const val = rawVal.getUint16(0, false); // Big-Endian.
    return val;
  }

  async writeInterval(ms: number): Promise<void> {
    const uuid = constants.ADVERTISING_INTERVAL_CHARACTERISTIC_UUID;
    const rawMs = new DataView(new ArrayBuffer(2)); // 2 * 8bit
    rawMs.setUint16(0, ms, false);
    return this.writeCharacteristic(uuid, rawMs);
  }

  /**
   * LOCK
   */

  async isLocked(): Promise<boolean> {
    const uuid = constants.EDDYSTONE_LOCK_STATE_CHARACTERISTIC_UUID;
    const rawVal = await this.readCharacteristic(uuid);
    const val = rawVal.getUint8(0);
    return val === LOCK_VALUES.LOCKED;
  }

  /**
   * RADIO
   */

  async readRadioTxPower(): Promise<number> {
    const uuid = constants.RADIO_TX_POWER_CHARACTERISTIC_UUID;
    const rawVal = await this.readCharacteristic(uuid);
    const val = rawVal.getInt8(0);
    return val;
  }

  /**
   * Writes Radio Tx Power.
   * @param dbm Tx power. Values should range between -100 and +20 dBm.
   * If a power is selected that is not supported by the radio, the beacon should select
   * the next highest power supported, or else the maximum power.
   * @see https://github.com/google/eddystone/blob/master/eddystone-url/README.md#tx-power-level
   */
  async writeRadioTxPower(dbm: number): Promise<void> {
    const uuid = constants.RADIO_TX_POWER_CHARACTERISTIC_UUID;
    const dbmByte = new Int8Array([dbm]);
    return this.writeCharacteristic(uuid, dbmByte);
  }

  async readAdvertisedTxPower(): Promise<number> {
    const uuid = constants.ADVANCED_ADVERTISED_TX_POWER_CHARACTERISTIC_UUID;
    const rawVal = await this.readCharacteristic(uuid);
    const val = rawVal.getInt8(0);
    return val;
  }

  async writeAdvertisedTxPower(dbm: number): Promise<void> {
    const uuid = constants.ADVANCED_ADVERTISED_TX_POWER_CHARACTERISTIC_UUID;
    const dbmByte = new Int8Array([dbm]);
    return this.writeCharacteristic(uuid, dbmByte);
  }

  /**
   * URL
   */
  async readUrl(): Promise<string> {
    const uuid = constants.ADV_SLOT_DATA_CHARACTERISTIC_UUID;
    const rawVal = await this.readCharacteristic(uuid);
    const type = rawVal.getUint8(0);
    if (type !== DATA_VALUES.URL) {
      return Promise.reject('Advertised data is not a URL');
    }
    const rawUrl = new DataView(rawVal.buffer, 2); // w/o type.
    return decodeUrl(rawUrl);
  }

  async writeUrl(url: string): Promise<void> {
    const uuid = constants.ADV_SLOT_DATA_CHARACTERISTIC_UUID;
    const raw = encodeUrl(url);
    if (raw.byteLength > 18) {
      return Promise.reject('Encoded URL is longer than 18 bytes');
    }
    const urlBytes = Array.from(Array(raw.byteLength).keys()).map((bytePos) => {
      return raw.getUint8(bytePos);
    });
    const fullBytes = new Uint8Array([DATA_VALUES.URL, ...urlBytes]); // With URL type preceding.
    return this.writeCharacteristic(uuid, fullBytes);
  }

  async clearUrl(): Promise<void> {
    const uuid = constants.ADV_SLOT_DATA_CHARACTERISTIC_UUID;
    const clearByte = new Uint8Array([0x00]);
    return this.writeCharacteristic(uuid, clearByte);
  }

  /**
   * MISC
   */

  async factoryReset(): Promise<void> {
    const uuid = constants.ADVANCED_FACTORY_RESET_CHARACTERISTIC_UUID;
    const factoryResetByte = new Uint8Array([0x0B]);
    return this.writeCharacteristic(uuid, factoryResetByte);
  }
}
<file_sep>/src/beacon/url.ts
/**
 * https://github.com/google/eddystone/tree/master/eddystone-url
 */

import { TextEncoder } from 'text-encoding';

type HexTypes = { [code: number]: string };

const URL_SCHEMES: HexTypes = {
  0x00: 'http://www.',
  0x01: 'https://www.',
  0x02: 'http://',
  0x03: 'https://',
};

const URL_CODES: HexTypes = {
  0x00: '.com/',
  0x01: '.org/',
  0x02: '.edu/',
  0x03: '.net/',
  0x04: '.info/',
  0x05: '.biz/',
  0x06: '.gov/',
  0x07: '.com',
  0x08: '.org',
  0x09: '.edu',
  0x0a: '.net',
  0x0b: '.info',
  0x0c: '.biz',
  0x0d: '.gov',
};

function decodeUrl(raw: DataView): string {
  const scheme: string = URL_SCHEMES[raw.getUint8(0)];
  const url = Array.from(Array(raw.byteLength).keys())
    .slice(1)
    .map((bytePos) => {
      const byteVal: number = raw.getUint8(bytePos);
      return URL_CODES[byteVal] || String.fromCharCode(byteVal);
    })
    .join('');
  return `${scheme}${url}`;
}

function encodeUrl(val: string): DataView {
  const encoder = new TextEncoder('utf-8');
  const encoded: number[] = [];
  
  for (let i = 0; i < val.length; i += 1) {
    // Try shorten the result as much as possible by using the above references.
    const shortEncoded = shortEncode(val.slice(i));
    if (shortEncoded) {
      encoded.push(shortEncoded.code);
      i += shortEncoded.jump - 1;
      continue;
    }
    // If it can't be shortened, simply encode the character.
    encoded.push(encoder.encode(val[i])[0]);
  }

  const buffer = new ArrayBuffer(encoded.length);
  const raw = new DataView(buffer);
  encoded.forEach((character, i) => raw.setUint8(i, character));
  return raw;
}

function shortEncode(val: string): { code: number, jump: number } | undefined {
  return shortEncodeWithDict(val, URL_SCHEMES) 
      || shortEncodeWithDict(val, URL_CODES);
}

function shortEncodeWithDict(val: string, hexTypes: HexTypes)
    : { code: number, jump: number } | undefined {
  const matching: string[] = Object.keys(hexTypes).filter((codeIndex: string) => {
    const code = Number(codeIndex);
    return val.startsWith(hexTypes[code]);
  });
  if (matching.length === 0) {
    return undefined;
  }

  matching.sort();
  const bestMatch = Number(matching[0]);
  return {
    code: bestMatch,
    jump: hexTypes[bestMatch].length,
  };
}

export {
  encodeUrl,
  decodeUrl,
};
