OpenHPS/openhps-rf

View on GitHub
src/data/BLEiBeacon.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { SerializableMember, SerializableObject, UUID } from '@openhps/core';
import { BLEUUID } from './BLEUUID';
import { MACAddress } from './MACAddress';
import { BLEBeaconBuilder, BLEBeaconObject } from './BLEBeaconObject';
import { arrayBuffersAreEqual, concatBuffer, toHexString } from '../utils/BufferUtils';

/**
 * BLE iBeacon Data Object
 * @see {@link https://wiki.aprbrother.com/en/iBeacon_Packet.html}
 */
@SerializableObject()
export class BLEiBeacon extends BLEBeaconObject {
    @SerializableMember()
    major: number;

    @SerializableMember()
    minor: number;

    @SerializableMember()
    proximityUUID: BLEUUID;

    constructor(address?: MACAddress, scanData?: Uint8Array) {
        super(address);
        if (scanData) {
            this.parseAdvertisement(scanData);
        }
    }

    computeUID(): string {
        let uid = new TextDecoder().decode(this.proximityUUID.toBuffer());
        uid = toHexString(
            concatBuffer(this.proximityUUID.toBuffer(), Uint8Array.from([this.major]), Uint8Array.from([this.minor])),
        );
        return uid;
    }

    isValid(): boolean {
        return this.proximityUUID !== undefined && this.major !== undefined && this.minor !== undefined;
    }

    parseManufacturerData(manufacturer: number, manufacturerData: Uint8Array): this {
        super.parseManufacturerData(manufacturer, manufacturerData);
        if (!manufacturerData) {
            return this;
        }

        const view = new DataView(manufacturerData.buffer, 0);
        if (
            manufacturer !== 0x004c ||
            manufacturerData.byteLength < 23 ||
            !arrayBuffersAreEqual(manufacturerData.buffer.slice(0, 2), Uint8Array.from([0x02, 0x15]).buffer)
        ) {
            return this;
        }
        this.proximityUUID = BLEUUID.fromBuffer(manufacturerData.slice(2, 18));
        this.major = view.getUint16(18, false);
        this.minor = view.getUint16(20, false);
        this.calibratedRSSI = view.getInt8(22);
        if (this.uid === undefined) {
            this.uid = this.computeUID();
        }
        return this;
    }
}

/**
 * BLE iBeacon builder
 */
export class BLEiBeaconBuilder extends BLEBeaconBuilder<BLEiBeacon> {
    protected constructor() {
        super();
        this.beacon = new BLEiBeacon();
        this.beacon.proximityUUID = BLEUUID.fromString(UUID.generate().toString());
        this.beacon.major = 0;
        this.beacon.minor = 0;
    }

    static create(): BLEiBeaconBuilder {
        return new BLEiBeaconBuilder();
    }

    static fromBeacon(beacon: BLEiBeacon): BLEiBeaconBuilder {
        const builder = new BLEiBeaconBuilder();
        builder.beacon = beacon;
        return builder;
    }

    proximityUUID(uuid: BLEUUID): this {
        this.beacon.proximityUUID = uuid;
        return this;
    }

    major(major: number): this {
        this.beacon.major = major;
        return this;
    }

    minor(minor: number): this {
        this.beacon.minor = minor;
        return this;
    }

    build(): Promise<BLEiBeacon> {
        return new Promise((resolve) => {
            // Compute manufacturer data
            const manufacturerData = new DataView(new ArrayBuffer(23), 0);
            // Advertisement data
            manufacturerData.setUint8(0, 0x02); // Beacon code
            manufacturerData.setUint8(1, 0x15);
            // Proximity UUID
            const proximityUUID = new DataView(this.beacon.proximityUUID.toBuffer().buffer, 0);
            for (let i = 2; i < 2 + 16; i++) {
                manufacturerData.setUint8(i, proximityUUID.getUint8(i - 2));
            }
            manufacturerData.setUint16(18, this.beacon.major);
            manufacturerData.setUint16(20, this.beacon.minor);

            manufacturerData.setInt8(22, this.beacon.calibratedRSSI); // Calibrated RSSI

            this.beacon.manufacturerData.set(0x004c, new Uint8Array(manufacturerData.buffer));
            this.beacon.uid = this.beacon.computeUID();
            resolve(this.beacon);
        });
    }
}