text: const crypto = require("crypto");
  
  const SERVICE_DATA_UUID = "fe95";
  
  const FrameControlFlags = {
    isFactoryNew: 1 << 0,
    isConnected: 1 << 1,
    isCentral: 1 << 2,
    isEncrypted: 1 << 3,
    hasMacAddress: 1 << 4,
    hasCapabilities: 1 << 5,
    hasEvent: 1 << 6,
    hasCustomData: 1 << 7,
    hasSubtitle: 1 << 8,
    hasBinding: 1 << 9
  };
  
  const CapabilityFlags = {
    connectable: 1 << 0,
    central: 1 << 1,
    secure: 1 << 2,
    io: (1 << 3) | (1 << 4)
  };
  
  const EventTypes = {
    temperature: 4100,
    humidity: 4102,
    illuminance: 4103,
    moisture: 4104,
    fertility: 4105,
    battery: 4106,
    temperatureAndHumidity: 4109
  };
  
  class Parser {
    constructor(buffer, bindKey = null) {
      this.baseByteLength = 5;
      if (buffer == null) {
        throw new Error("A buffer must be provided.");
      }
      this.buffer = buffer;
      if (buffer.length < this.baseByteLength) {
        throw new Error(
          `Service data length must be >= 5 bytes. ${this.toString()}`
        );
      }
      this.bindKey = bindKey;
    }
  
    parse() {
      this.frameControl = this.parseFrameControl();
      this.version = this.parseVersion();
      this.productId = this.parseProductId();
      this.frameCounter = this.parseFrameCounter();
      this.macAddress = this.parseMacAddress();
      this.capabilities = this.parseCapabilities();
  
      if (this.frameControl.isEncrypted) {
        this.decryptPayload();
      }
  
      this.eventType = this.parseEventType();
      this.eventLength = this.parseEventLength();
      this.event = this.parseEventData();
      return {
        frameControl: this.frameControl,
        event: this.event,
        productId: this.productId,
        frameCounter: this.frameCounter,
        macAddress: this.macAddress,
        eventType: this.eventType,
        capabilities: this.capabilities,
        eventLength: this.eventLength,
        version: this.version
      };
    }
  
    parseFrameControl() {
      const frameControl = this.buffer.readUInt16LE(0);
      return Object.keys(FrameControlFlags).reduce((map, flag) => {
        map[flag] = (frameControl & FrameControlFlags[flag]) !== 0;
        return map;
      }, {});
    }
  
    parseVersion() {
      return this.buffer.readUInt8(1) >> 4;
    }
  
    parseProductId() {
      return this.buffer.readUInt16LE(2);
    }
  
    parseFrameCounter() {
      return this.buffer.readUInt8(4);
    }
  
    parseMacAddress() {
      if (!this.frameControl.hasMacAddress) {
        return null;
      }
      const macBuffer = this.buffer.slice(
        this.baseByteLength,
        this.baseByteLength + 6
      );
      return Buffer.from(macBuffer)
        .reverse()
        .toString("hex");
    }
  
    get capabilityOffset() {
      if (!this.frameControl.hasMacAddress) {
        return this.baseByteLength;
      }
      return 11;
    }
  
    parseCapabilities() {
      if (!this.frameControl.hasCapabilities) {
        return null;
      }
      const capabilities = this.buffer.readUInt8(this.capabilityOffset);
      return Object.keys(CapabilityFlags).reduce((map, flag) => {
        map[flag] = (capabilities & CapabilityFlags[flag]) !== 0;
        return map;
      }, {});
    }
  
    get eventOffset() {
      let offset = this.baseByteLength;
      if (this.frameControl.hasMacAddress) {
        offset = 11;
      }
      if (this.frameControl.hasCapabilities) {
        offset += 1;
      }
  
      return offset;
    }
  
    parseEventType() {
      if (!this.frameControl.hasEvent) {
        return null;
      }
      return this.buffer.readUInt16LE(this.eventOffset);
    }
  
    parseEventLength() {
      if (!this.frameControl.hasEvent) {
        return null;
      }
      return this.buffer.readUInt8(this.eventOffset + 2);
    }
  
    decryptPayload() {
      const msgLength = this.buffer.length;
      const eventLength = msgLength - this.eventOffset;
  
      if (eventLength < 3) {
        return;
      }
      if (this.bindKey == null) {
        throw Error("Sensor data is encrypted. Please configure a bindKey.");
      }
  
      const encryptedPayload = this.buffer.slice(this.eventOffset, msgLength);
  
      const nonce = Buffer.concat([
        this.buffer.slice(5, 11), //mac_reversed
        this.buffer.slice(2, 4), //device_type
        this.buffer.slice(4, 5), //frame_cnt
        encryptedPayload.slice(-7, -4) //ext_cnt
      ]);
  
      const decipher = crypto.createDecipheriv(
        "aes-128-ccm",
        Buffer.from(this.bindKey, "hex"), //key
        nonce, //iv
        { authTagLength: 4 }
      );
  
      const ciphertext = encryptedPayload.slice(0, -7);
  
      decipher.setAuthTag(encryptedPayload.slice(-4));
      decipher.setAAD(Buffer.from("11", "hex"), {
        plaintextLength: ciphertext.length
      });
  
      const receivedPlaintext = decipher.update(ciphertext);
  
      decipher.final();
  
      this.buffer = Buffer.concat([
        this.buffer.slice(0, this.eventOffset),
        receivedPlaintext
      ]);
    }
  
    parseEventData() {
      if (!this.frameControl.hasEvent) {
        return null;
      }
      switch (this.eventType) {
        case EventTypes.temperature: {
          return this.parseTemperatureEvent();
        }
        case EventTypes.humidity: {
          return this.parseHumidityEvent();
        }
        case EventTypes.battery: {
          return this.parseBatteryEvent();
        }
        case EventTypes.temperatureAndHumidity: {
          return this.parseTemperatureAndHumidityEvent();
        }
        case EventTypes.illuminance: {
          return this.parseIlluminanceEvent();
        }
        case EventTypes.fertility: {
          return this.parseFertilityEvent();
        }
        case EventTypes.moisture: {
          return this.parseMoistureEvent();
        }
        default: {
          throw new Error(
            `Unknown event type: ${this.eventType}. ${this.toString()}`
          );
        }
      }
    }
  
    parseTemperatureEvent() {
      return {
        temperature: this.buffer.readInt16LE(this.eventOffset + 3) / 10
      };
    }
  
    parseHumidityEvent() {
      return {
        humidity: this.buffer.readUInt16LE(this.eventOffset + 3) / 10
      };
    }
  
    parseBatteryEvent() {
      return {
        battery: this.buffer.readUInt8(this.eventOffset + 3)
      };
    }
  
    parseTemperatureAndHumidityEvent() {
      const temperature = this.buffer.readInt16LE(this.eventOffset + 3) / 10;
      const humidity = this.buffer.readUInt16LE(this.eventOffset + 5) / 10;
      return { temperature, humidity };
    }
  
    parseIlluminanceEvent() {
      return {
        illuminance: this.buffer.readUIntLE(this.eventOffset + 3, 3)
      };
    }
  
    parseFertilityEvent() {
      return {
        fertility: this.buffer.readInt16LE(this.eventOffset + 3)
      };
    }
  
    parseMoistureEvent() {
      return {
        moisture: this.buffer.readInt8(this.eventOffset + 3)
      };
    }
  
    toString() {
      return this.buffer.toString("hex");
    }
  }
  
  module.exports = {
    Parser,
    EventTypes,
    SERVICE_DATA_UUID
  };
  