{"version":3,"sources":["../src/profiles/heart-rate.ts"],"names":["HEART_RATE_SERVICES","resolveUUID","HeartRateProfile","BaseProfile","callback","dv","parseHeartRate","readUint8","flags","offset","is16bit","bpm","readUint16LE","contact","energyExpended","rrIntervals"],"mappings":"+HAWO,IAAMA,CAAAA,CAAyC,CAACC,GAAAA,CAAY,YAAY,CAAC,CAAA,CAyDnEC,CAAAA,CAAN,cAA+BC,GAAY,CAA3C,WAAA,EAAA,CAAA,KAAA,CAAA,GAAA,SAAA,CAAA,CAIL,IAAA,CAAmB,OAAA,CAAU,cAG7B,WAAA,CAAYC,CAAAA,CAAqD,CAC/D,OAAO,IAAA,CAAK,SAAA,CAAU,wBAAA,CAA2BC,CAAAA,EAAO,CACtDD,CAAAA,CAASE,CAAAA,CAAeD,CAAE,CAAC,EAC7B,CAAC,CACH,CAGA,MAAM,oBAAsC,CAC1C,IAAMA,CAAAA,CAAK,MAAM,IAAA,CAAK,IAAA,CAAK,sBAAsB,CAAA,CACjD,OAAOE,CAAAA,CAAUF,CAAE,CACrB,CAGA,MAAM,mBAAA,EAAqC,CACzC,MAAM,IAAA,CAAK,MAAM,0BAAA,CAA4B,IAAI,UAAA,CAAW,CAAC,CAAC,CAAC,CAAC,EAClE,CACF,EAvBaH,CAAAA,CAEK,QAAA,CAAWF,CAAAA,CA2CtB,SAASM,CAAAA,CAAeD,CAAAA,CAA6B,CAC1D,IAAMG,EAAQD,CAAAA,CAAUF,CAAAA,CAAI,CAAC,CAAA,CACzBI,GAAAA,CAAS,CAAA,CAGPC,CAAAA,CAAAA,CAAWF,CAAAA,CAAQ,KAAU,CAAA,CAC7BG,CAAAA,CAAMD,CAAAA,CAAUE,CAAAA,CAAaP,EAAII,GAAM,CAAA,CAAIF,CAAAA,CAAUF,CAAAA,CAAII,GAAM,CAAA,CACrEA,GAAAA,EAAUC,CAAAA,CAAU,CAAA,CAAI,CAAA,CAIxB,IAAMG,GAAAA,CAAAA,CADoBL,CAAAA,CAAQ,KAAU,CAAA,CAAA,CACRA,CAAAA,CAAQ,CAAA,IAAU,CAAA,CAAI,KAGtDM,CAAAA,CAAgC,IAAA,CAChCN,CAAAA,CAAQ,CAAA,GACVM,EAAiBF,CAAAA,CAAaP,CAAAA,CAAII,GAAM,CAAA,CACxCA,GAAAA,EAAU,CAAA,CAAA,CAIZ,IAAMM,CAAAA,CAAwB,EAAC,CAC/B,GAAIP,CAAAA,CAAQ,EAAA,CAGV,KAAOC,GAAAA,CAAS,CAAA,EAAKJ,CAAAA,CAAG,UAAA,EAEtBU,EAAY,IAAA,CAAKH,CAAAA,CAAaP,CAAAA,CAAII,GAAM,CAAA,CAAI,IAAI,CAAA,CAChDA,GAAAA,EAAU,EAId,OAAO,CAAE,GAAA,CAAAE,CAAAA,CAAK,QAAAE,GAAAA,CAAS,cAAA,CAAAC,CAAAA,CAAgB,WAAA,CAAAC,CAAY,CACrD","file":"chunk-PZASPF25.mjs","sourcesContent":["import { readUint8, readUint16LE } from '../dataview-helpers';\nimport { resolveUUID } from '../uuid';\nimport { BaseProfile } from './base';\n\n/**\n * Service UUIDs a Heart Rate device may reach after connection (the SIG Heart\n * Rate Service, 0x180D). Canonical 128-bit form (resolved from the SIG alias via\n * the core registry — single source of truth). Use with `optionalServices` /\n * `Beacio.registerServices`, or via {@link deriveOptionalServices} given\n * {@link HeartRateProfile}.\n */\nexport const HEART_RATE_SERVICES: readonly string[] = [resolveUUID('heart_rate')];\n\n/**\n * Parsed heart rate measurement data from the Heart Rate Measurement\n * characteristic (UUID 0x2A37).\n *\n * Fields are populated based on the flags byte in the BLE payload.\n * Optional fields are `null` when the corresponding flag bit is unset.\n */\nexport interface HeartRateData {\n  /** Heart rate value in beats per minute (BPM). May be 8-bit or 16-bit depending on the flags byte. */\n  bpm: number;\n  /** Whether the sensor has skin contact. `null` if the sensor does not support contact detection. */\n  contact: boolean | null;\n  /** Cumulative energy expended in kilojoules since the last reset. `null` if not present in this measurement. */\n  energyExpended: number | null;\n  /** RR-interval values in seconds (1/1024 s resolution). Empty array if not present in this measurement. */\n  rrIntervals: number[];\n}\n\n/**\n * BLE Heart Rate Service profile (UUID 0x180D).\n *\n * Provides access to heart rate measurements, body sensor location,\n * and the energy-expended reset control point as defined by the\n * Bluetooth SIG Heart Rate Service specification.\n *\n * The measurement characteristic (0x2A37) uses a flags byte:\n * bit 0 = HR format (0 = UINT8, 1 = UINT16), bits 1-2 = sensor contact,\n * bit 3 = energy expended present, bit 4 = RR-interval present.\n *\n * @example\n * ```ts\n * import { HeartRateProfile } from '@beacio/core/profiles';\n *\n * const hr = new HeartRateProfile(device);\n * await hr.connect();\n *\n * // Subscribe to real-time heart rate data\n * const unsubscribe = hr.onHeartRate((data) => {\n *   console.log(`BPM: ${data.bpm}`);\n *   if (data.contact === false) {\n *     console.warn('No skin contact detected');\n *   }\n *   if (data.rrIntervals.length > 0) {\n *     console.log('RR intervals (s):', data.rrIntervals);\n *   }\n * });\n *\n * // Read sensor location (e.g. 1 = Chest, 2 = Wrist)\n * const location = await hr.readSensorLocation();\n *\n * // Clean up\n * unsubscribe();\n * hr.stop();\n * ```\n */\nexport class HeartRateProfile extends BaseProfile {\n  /** Services this profile's device may reach after connection (Heart Rate, 0x180D). Read by {@link deriveOptionalServices}. */\n  static readonly services = HEART_RATE_SERVICES;\n\n  protected readonly service = 'heart_rate';\n\n  /** Subscribe to heart rate measurements. Returns unsubscribe function. */\n  onHeartRate(callback: (data: HeartRateData) => void): () => void {\n    return this.subscribe('heart_rate_measurement', (dv) => {\n      callback(parseHeartRate(dv));\n    });\n  }\n\n  /** Read body sensor location (0=Other, 1=Chest, 2=Wrist, ...) */\n  async readSensorLocation(): Promise<number> {\n    const dv = await this.read('body_sensor_location');\n    return readUint8(dv);\n  }\n\n  /** Reset energy expended counter */\n  async resetEnergyExpended(): Promise<void> {\n    await this.write('heart_rate_control_point', new Uint8Array([1]));\n  }\n}\n\n/**\n * Parse a raw Heart Rate Measurement characteristic value (UUID 0x2A37)\n * into a structured {@link HeartRateData} object.\n *\n * The first byte is a flags field that determines the format and which\n * optional fields are present. This function handles all flag combinations\n * defined by the Bluetooth SIG specification.\n *\n * @param dv - Raw characteristic value as a {@link DataView}.\n * @returns Parsed heart rate data with BPM, contact status, energy, and RR intervals.\n *\n * @example\n * ```ts\n * import { parseHeartRate } from '@beacio/core/profiles';\n *\n * // Manually parse a DataView from a notification\n * const data = parseHeartRate(characteristicValue);\n * console.log(`Heart rate: ${data.bpm} BPM`);\n * ```\n */\nexport function parseHeartRate(dv: DataView): HeartRateData {\n  const flags = readUint8(dv, 0);\n  let offset = 1;\n\n  // Bit 0: Heart Rate Format — 0=UINT8, 1=UINT16\n  const is16bit = (flags & 0x01) !== 0;\n  const bpm = is16bit ? readUint16LE(dv, offset) : readUint8(dv, offset);\n  offset += is16bit ? 2 : 1;\n\n  // Bits 1-2: Sensor Contact\n  const contactSupported = (flags & 0x04) !== 0;\n  const contact = contactSupported ? (flags & 0x02) !== 0 : null;\n\n  // Bit 3: Energy Expended present\n  let energyExpended: number | null = null;\n  if (flags & 0x08) {\n    energyExpended = readUint16LE(dv, offset);\n    offset += 2;\n  }\n\n  // Bit 4: RR-Interval present\n  const rrIntervals: number[] = [];\n  if (flags & 0x10) {\n    // Consume only complete 2-byte RR pairs; a stray trailing byte (malformed\n    // frame) is dropped rather than triggering an out-of-bounds read.\n    while (offset + 2 <= dv.byteLength) {\n      // RR intervals are in 1/1024 seconds units, convert to seconds\n      rrIntervals.push(readUint16LE(dv, offset) / 1024);\n      offset += 2;\n    }\n  }\n\n  return { bpm, contact, energyExpended, rrIntervals };\n}\n"]}