{"version":3,"sources":["../../../src/experimental/profiles/storz-bickel.ts"],"names":["STORZ_BICKEL_SERVICE","STORZ_BICKEL_CHARACTERISTICS","STORZ_BICKEL_SERVICE_2","STORZ_BICKEL_SERVICE_3","STORZ_BICKEL_SERVICE_2_CHARACTERISTICS","STORZ_BICKEL_SERVICE_3_CHARACTERISTICS","STORZ_BICKEL_AUTH_GATE","STORZ_BICKEL_SERVICES","STORZ_BICKEL_VOLCANO_SERVICES","STORZ_BICKEL_VEAZY_VENTY_SERVICES","STORZ_BICKEL_FAMILY_SERVICES","decodeTemperatureDeciCelsius","dv","readUint16LE","encodeTemperatureDeciCelsius","celsius","raw","buffer","decodeBatteryPercent","clampPercent","readUint8","StorzBickelProfile","BaseProfile","callback","StorzBickel","deriveOptionalServices"],"mappings":"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","file":"storz-bickel.mjs","sourcesContent":["import { readUint8, readUint16LE, clampPercent, type Percentage, type NativeOverflowEvent } from '../../index';\nimport { BaseProfile } from '../../profiles/base';\nimport { deriveOptionalServices } from '../../profiles/services';\n\n/**\n * Storz & Bickel Crafty / Crafty+ / Mighty / Mighty+ vaporizer profile.\n *\n * @experimental UUIDs from PUBLIC reverse-engineering — standard-validated,\n * on-device deferred to operator. Storz & Bickel publishes no official GATT\n * specification; every UUID and decode below is derived from the vendor's own\n * (minified) Web Bluetooth bundle plus independent community re-implementations,\n * then cross-checked. The accessors exposed here cover only the HIGH-confidence\n * characteristics (corroborated by the official bundle AND >= 1 independent\n * source).\n *\n * Standard-grounded validation (the present trust basis): conformance to the\n * Web Bluetooth Living Standard (https://webbluetoothcg.github.io/web-bluetooth/)\n * is the authority for the runtime GATT contracts each accessor exercises —\n *   - §4 Device Discovery — `requestDevice({ filters, optionalServices })`\n *     (GAP-1 fix: optionalServices must be declared so iOS discover resolves),\n *   - §6 GATT Interaction — `getPrimaryService(uuid)` / `getCharacteristic(uuid)`\n *     (BluetoothRemoteGATTService uuid per §6.3),\n *   - §6.4 — `characteristic.readValue()` returns `Promise<DataView>`; the\n *     profile's `decodeTemperatureDeciCelsius` consumes `DataView.getInt16`\n *     little-endian and `decodeBatteryPercent` consumes `DataView.getUint8` —\n *     spec-conformant reads,\n *   - §6.4 — `characteristic.writeValue(value: BufferSource)` /\n *     `writeValueWithResponse` / `writeValueWithoutResponse` (BufferSource input;\n *     the profile's `encodeTemperatureDeciCelsius` produces a `DataView` — a\n *     valid BufferSource),\n *   - §6.4 — `characteristic.startNotifications()` => `characteristicvaluechanged`\n *     event with `event.target.value: DataView`; the profile subscribes\n *     exclusively via `BaseProfile.subscribe` (per the \"Notifications\" note above)\n *     and never writes a CCCD/SCCD descriptor itself — strictly W3C GATT,\n *   - §7.1 Standardized UUIDs — `BluetoothUUID.canonicalUUID` resolves to the\n *     lowercase 128-bit form (e.g. `00001818-0000-1000-8000-00805f9b34fb`);\n *     this profile stores UUIDs uppercase internally (beacio convention — see\n *     `NormalizedUUID`) and emits them lowercase to web-facing payloads (matches\n *     the spec's external canonical form).\n * The S&B-derived UUID/opcode VALUES themselves are interface-only\n * interoperability facts (see the 5+ source corroboration block immediately\n * below) and have NOT been exercised on physical hardware through this library.\n * On-device confirmation against a real Volcano/Crafty/Venty is an\n * operator-supplied gate, tracked separately at\n * `outreach/storz-bickel/onboarding/reviews/PR178-fixes/IP-02.md`. Treat\n * reads/writes as standard-conformant in SHAPE but provisional in VALUE until\n * device-confirmed.\n *\n * Device family & encoding\n * -------------------------\n * The Crafty/Mighty line shares a single GATT tree. The 96-bit vendor base is\n * ASCII `STORZ&BICKEL` written **byte-reversed** (`…-4c45-4b43-4942-265a524f5453`,\n * which decodes to `LEKCIB&ZROTS`). The sibling Volcano Hybrid line uses the\n * *big-endian* form of the same base (`…-5354-4f52-5a26-4249434b454c`) and a\n * different characteristic map; it is intentionally out of scope for this\n * profile. Mighty/Mighty+ reuse the identical Crafty tree and are\n * disambiguated at runtime via the model characteristic (`0x22`).\n *\n * Temperatures are little-endian uint16 in tenths of a degree Celsius\n * (deciCelsius): raw `1822` == `182.2 °C`. Battery level is a little-endian\n * uint16 percentage (0–100; only the low byte is used).\n *\n * Notifications (current temperature, battery) are enabled exclusively through\n * {@link BaseProfile.subscribe} (`startNotifications()`); this profile never\n * reads or writes a CCCD/SCCD descriptor itself — strictly W3C\n * `navigator.bluetooth` GATT.\n *\n * Sources (verified 2026-06-14):\n * - Official S&B Web Bluetooth app bundle (app.storz-bickel.com, js/main.js +\n *   crafty.js): `serviceUuidCrafty1`, `charactersiticCurrTemperatureChanged`,\n *   `characteristicWriteTemp`, `characteristicWriteBoostTemp`,\n *   `characteristicPowerChanged`.\n * - J-Cat/crafty-control craftyUuids.ts — https://github.com/J-Cat/crafty-control\n *   (ServiceUuid, TemperatureUuid 0x11, SetPointUuid 0x21, BoostUuid 0x31,\n *   BatteryUuid 0x41; verified from source).\n * - ligi/VaporizerControl CRAFTY_UUIDS.java — https://github.com/ligi/VaporizerControl\n *   (DATA_SERVICE craft(1), TEMPERATURE craft(0x11), SETPOINT craft(0x21),\n *   BOOST craft(0x31), BATTERY craft(0x41); verified from source).\n * - gsasouza/sb-crafty-watch-os — https://github.com/gsasouza/sb-crafty-watch-os\n *   (battery + current-temperature handling; verified from source).\n * - firsttris/reactive-volcano-app — https://github.com/firsttris/reactive-volcano-app\n *   (currTemperatureChanged, writeTemp, writeBoostTemp).\n * - 0022111/sbtracker — https://github.com/0022111/sbtracker\n *   (BleConstants.kt: \"// Crafty/Mighty+ (older or traditional protocol)\").\n *\n * @example\n * ```ts\n * import { StorzBickelProfile } from '@beacio/core/experimental/profiles/storz-bickel';\n *\n * // requestDevice({ filters: [{ namePrefix: 'S&B' }], optionalServices: [\n * //   '00000001-4c45-4b43-4942-265a524f5453',\n * // ] })\n * const vape = new StorzBickelProfile(device);\n * await vape.connect();\n *\n * await vape.setTargetTemperature(182.2);\n *\n * const off = vape.onCurrentTemperature((c) => console.log(`now ${c} °C`));\n * console.log('battery', await vape.batteryLevel(), '%');\n *\n * off();\n * vape.stop();\n * ```\n */\n\n/** Crafty/Mighty PRIMARY DATA SERVICE (live temp, setpoint, boost, battery). HIGH confidence. */\nexport const STORZ_BICKEL_SERVICE = '00000001-4c45-4b43-4942-265a524f5453';\n\n/**\n * HIGH-confidence Crafty/Mighty characteristic UUIDs surfaced by this profile.\n *\n * Only characteristics rated HIGH in the consolidated reverse-engineering data\n * (official bundle + >= 1 independent corroborator) are included. Lower-\n * confidence and diagnostic characteristics are intentionally omitted until\n * device-confirmed.\n */\nexport const STORZ_BICKEL_CHARACTERISTICS = {\n  /** Current/live temperature. read/notify; deciCelsius LE. HIGH. */\n  currentTemperature: '00000011-4c45-4b43-4942-265a524f5453',\n  /** Target/setpoint temperature. read/write; deciCelsius LE (e.g. 1822 = 182.2 °C). HIGH. */\n  targetTemperature: '00000021-4c45-4b43-4942-265a524f5453',\n  /** Boost temperature offset. read/write; deciCelsius LE. (NOT heater on/off.) HIGH. */\n  boostTemperature: '00000031-4c45-4b43-4942-265a524f5453',\n  /** Battery level percent. read/notify; uint16 LE (low byte used). HIGH. */\n  batteryLevel: '00000041-4c45-4b43-4942-265a524f5453',\n} as const;\n\n/**\n * Crafty/Mighty SECONDARY service (device-info: serial number, model/firmware).\n * The official bundle opens this via `getPrimaryService(serviceUuidCrafty2)`\n * (`main.js:119`, `crafty.js`). Read-only metadata — not surfaced by the\n * temperature/battery accessors. MEDIUM confidence (official bundle only).\n */\nexport const STORZ_BICKEL_SERVICE_2 = '00000002-4c45-4b43-4942-265a524f5453';\n\n/**\n * Crafty/Mighty TERTIARY service (project/status registers, model, hour-meter).\n * Opened via `getPrimaryService(serviceUuidCrafty3)` (`main.js:120`, `crafty.js`).\n * MEDIUM confidence (official bundle only).\n */\nexport const STORZ_BICKEL_SERVICE_3 = '00000003-4c45-4b43-4942-265a524f5453';\n\n/**\n * Read-only metadata characteristics on {@link STORZ_BICKEL_SERVICE_2}, pinned\n * with provenance (`crafty.js` `primaryServiceCraftyUuid2.getCharacteristic`).\n * Surfaced for callers that want device-info; not used by this profile's\n * temperature/battery accessors. MEDIUM confidence.\n */\nexport const STORZ_BICKEL_SERVICE_2_CHARACTERISTICS = {\n  /** Serial number. read; UTF-8 (first 8 chars). crafty.js:224. */\n  serialNumber: '00000052-4c45-4b43-4942-265a524f5453',\n  /** Model identifier. read. crafty.js:261. */\n  model: '00000032-4c45-4b43-4942-265a524f5453',\n} as const;\n\n/**\n * Read-only metadata characteristics on {@link STORZ_BICKEL_SERVICE_3}, pinned\n * with provenance (`crafty.js` `primaryServiceCraftyUuid3.getCharacteristic`).\n * MEDIUM confidence.\n */\nexport const STORZ_BICKEL_SERVICE_3_CHARACTERISTICS = {\n  /** Firmware/BLE version string. read. crafty.js:323. */\n  firmwareVersion: '000001c3-4c45-4b43-4942-265a524f5453',\n  /** Project-status register (model/state flags). read/notify. crafty.js:338. */\n  projectStatus: '00000023-4c45-4b43-4942-265a524f5453',\n} as const;\n\n/**\n * The S&B Crafty/Mighty GATT is NOT auth-gated: every documented characteristic\n * is reachable after a plain `getPrimaryService` + `getCharacteristic` with no\n * pairing/bonding or write-to-unlock handshake (confirmed across the official\n * bundle and the independent community re-implementations). This sentinel\n * records that fact for integrators / @beacio/detect rather than leaving the\n * absence of an auth gate implicit — there is no characteristic to write first.\n */\nexport const STORZ_BICKEL_AUTH_GATE = null;\n\n/**\n * Crafty/Mighty (Family A) data services, in `getPrimaryService` order: the\n * primary data service ({@link STORZ_BICKEL_SERVICE}) plus the device-info and\n * project-register services. Canonical lowercase. This is the per-profile\n * `services` array read by {@link deriveOptionalServices} (and exposed as the\n * static `StorzBickelProfile.services`).\n *\n * Provenance: `captured/beautified/main.js:118-120` (serviceUuidCrafty1/2/3).\n */\nexport const STORZ_BICKEL_SERVICES = [\n  STORZ_BICKEL_SERVICE,\n  STORZ_BICKEL_SERVICE_2,\n  STORZ_BICKEL_SERVICE_3,\n] as const;\n\n/**\n * Volcano HYBRID (Family B) services actually opened by the vendor bundle, in\n * `getPrimaryService` order (`volcano.js:550/554/558/562`). This family is out\n * of scope for {@link StorzBickelProfile}'s accessors but is included in the\n * connect-time `optionalServices` bundle so the picker can reach a Volcano.\n *\n * NOTE — these four UUIDs do NOT share a single base; the bundle mixes two:\n * - volcano1/volcano2 use a generic-vendor base `…-1989-0108-1234-123456789abc`\n *   (NOT an S&B base at all).\n * - volcano3/volcano4 use the *big-endian* S&B base `…-5354-4f52-5a26-4249434b454c`\n *   (ASCII `STORZ&BICKEL`), the same form used by the Veazy/Venty (QVAP) family,\n *   and the byte-reverse of Crafty's `…-4c45-4b43-4942-265a524f5453` base.\n * Each line is individually source-cited to the vendor bundle; the values are\n * pinned by the SB-SDK-02 regression below. PENDING on-device confirmation.\n *\n * `serviceUuidVolcano5` (`10130000-…`, `main.js:125`) is DELIBERATELY excluded:\n * it is declared in the bundle but never `getPrimaryService`'d.\n */\nexport const STORZ_BICKEL_VOLCANO_SERVICES = [\n  '00000001-1989-0108-1234-123456789abc', // volcano.js:550 serviceUuidVolcano1 — generic-vendor base (1989-0108)\n  '01000002-1989-0108-1234-123456789abc', // volcano.js:554 serviceUuidVolcano2 — generic-vendor base (1989-0108)\n  '10100000-5354-4f52-5a26-4249434b454c', // volcano.js:558 serviceUuidVolcano3 — big-endian S&B base\n  '10110000-5354-4f52-5a26-4249434b454c', // volcano.js:562 serviceUuidVolcano4 — big-endian S&B base\n] as const;\n\n/**\n * Veazy / Venty (Family C, the \"QVAP\" bundle) services opened by the vendor\n * bundle (`qvap.js:556/582`): the vendor data service plus SIG `generic_access`.\n * Out of scope for {@link StorzBickelProfile}'s accessors; included in the\n * connect-time `optionalServices` bundle.\n */\nexport const STORZ_BICKEL_VEAZY_VENTY_SERVICES = [\n  '00000000-5354-4f52-5a26-4249434b454c', // qvap.js:556 serviceUuidQvap\n  '00001800-0000-1000-8000-00805f9b34fb', // qvap.js:582 serviceUuidQvap1 (generic_access)\n] as const;\n\n/**\n * The three S&B device families' service arrays, keyed by family. Consumed by\n * {@link StorzBickel.allServices} to build the de-duped multi-family\n * `optionalServices` bundle for a picker that should reach ANY S&B device.\n */\nexport const STORZ_BICKEL_FAMILY_SERVICES = {\n  crafty: STORZ_BICKEL_SERVICES,\n  volcano: STORZ_BICKEL_VOLCANO_SERVICES,\n  veazyVenty: STORZ_BICKEL_VEAZY_VENTY_SERVICES,\n} as const;\n\n/**\n * Decode a Storz & Bickel temperature characteristic value.\n *\n * Wire format: little-endian uint16 in tenths of a degree Celsius\n * (deciCelsius). Raw `1822` decodes to `182.2`.\n *\n * @param dv - Raw characteristic value (current, target, or boost temperature).\n * @returns Temperature in degrees Celsius.\n * @throws {BeacioError} INVALID_PARAMETER if the value is shorter than 2 bytes.\n */\nexport function decodeTemperatureDeciCelsius(dv: DataView): number {\n  return readUint16LE(dv) / 10;\n}\n\n/**\n * Encode a degrees-Celsius temperature into the Storz & Bickel wire format:\n * little-endian uint16 deciCelsius, rounded to the nearest 0.1 °C.\n *\n * @param celsius - Temperature in degrees Celsius (e.g. `182.2`).\n * @returns A 2-byte little-endian payload (raw deciCelsius).\n */\nexport function encodeTemperatureDeciCelsius(celsius: number): Uint8Array<ArrayBuffer> {\n  const raw = Math.round(celsius * 10);\n  const buffer = new ArrayBuffer(2);\n  new DataView(buffer).setUint16(0, raw, true);\n  return new Uint8Array(buffer);\n}\n\n/**\n * Decode the Storz & Bickel battery-level characteristic value.\n *\n * Wire format: little-endian uint16 percentage (0–100); only the low byte is\n * populated in practice. The official bundle labels this `power`; community\n * sources label it `battery` — both are the same battery-level read path.\n *\n * @param dv - Raw battery characteristic value.\n * @returns {Percentage} Battery level as an integer percentage (0–100).\n * @throws {BeacioError} INVALID_PARAMETER if the value is shorter than 1 byte.\n */\nexport function decodeBatteryPercent(dv: DataView): Percentage {\n  // Wire format documents a uint16 LE where ONLY the low byte carries 0..100;\n  // the high byte is unused/garbage. For a little-endian uint16 the byte at\n  // offset 0 IS the low byte, so readUint8(dv) returns exactly the documented\n  // payload and structurally discards the high byte (strictly better than\n  // `readUint16LE(dv) & 0xff`). clampPercent saturates any malformed frame into\n  // 0..100 so a notification decode is total and never throws on bad data.\n  return clampPercent(readUint8(dv));\n}\n\nexport class StorzBickelProfile extends BaseProfile {\n  /**\n   * Crafty/Mighty (Family A) services this profile's device may reach after\n   * connection. Read by {@link deriveOptionalServices} so a caller can pass the\n   * profile class itself instead of hand-copying {@link STORZ_BICKEL_SERVICES}.\n   */\n  static readonly services = STORZ_BICKEL_SERVICES;\n\n  protected readonly service = STORZ_BICKEL_SERVICE;\n\n  /**\n   * Read the current/live heater temperature (°C).\n   * Characteristic `00000011-…` (read/notify), deciCelsius LE.\n   */\n  async currentTemperature(): Promise<number> {\n    return decodeTemperatureDeciCelsius(await this.read(STORZ_BICKEL_CHARACTERISTICS.currentTemperature));\n  }\n\n  /**\n   * Subscribe to live temperature updates (°C). Returns an unsubscribe\n   * function. Notifications are enabled via {@link BaseProfile.subscribe}\n   * only — no CCCD write.\n   */\n  onCurrentTemperature(callback: (celsius: number) => void): () => void {\n    return this.subscribe(STORZ_BICKEL_CHARACTERISTICS.currentTemperature, (dv) => {\n      callback(decodeTemperatureDeciCelsius(dv));\n    });\n  }\n\n  /**\n   * Observe NATIVE notification-queue overflows on the live-temperature stream.\n   * Returns an unsubscribe function (also cleaned up by {@link BaseProfile.stop}).\n   *\n   * Under sustained high-frequency notifications Safari's bounded Swift\n   * `EventQueue` evicts samples rather than dropping them silently, and the\n   * polyfill surfaces each eviction as `beacio:overflow`. When this fires, the\n   * temperature shown from the last notified value is potentially stale: the\n   * recommended response is to issue a fresh {@link currentTemperature} read and\n   * repaint the gauge from that value rather than trusting the last\n   * `onCurrentTemperature` sample.\n   *\n   * Thin-JS: this only surfaces the existing native signal; it changes no flow\n   * control. The `event` carries the eviction metadata\n   * ({@link NativeOverflowEvent}: `evictedCount`, `queueCapacity`, `seq`,\n   * `timestamp`).\n   *\n   * @example\n   * ```ts\n   * vape.onCurrentTemperatureStale(async () => {\n   *   // notifications were evicted — resync the gauge from a fresh read\n   *   updateGauge(await vape.currentTemperature());\n   * });\n   * ```\n   */\n  onCurrentTemperatureStale(callback: (event: NativeOverflowEvent) => void): () => void {\n    return this.onOverflow(STORZ_BICKEL_CHARACTERISTICS.currentTemperature, callback);\n  }\n\n  /**\n   * Read the target/setpoint temperature (°C).\n   * Characteristic `00000021-…` (read/write), deciCelsius LE.\n   */\n  async targetTemperature(): Promise<number> {\n    return decodeTemperatureDeciCelsius(await this.read(STORZ_BICKEL_CHARACTERISTICS.targetTemperature));\n  }\n\n  /**\n   * Write the target/setpoint temperature (°C) using write-with-response.\n   * Characteristic `00000021-…`, deciCelsius LE.\n   *\n   * @param celsius - Desired setpoint in degrees Celsius (e.g. `182.2`).\n   */\n  async setTargetTemperature(celsius: number): Promise<void> {\n    await this.write(STORZ_BICKEL_CHARACTERISTICS.targetTemperature, encodeTemperatureDeciCelsius(celsius));\n  }\n\n  /**\n   * Read the boost temperature **offset** (°C). This is added on top of the\n   * setpoint while boost is engaged — it is NOT a heater on/off control.\n   * Characteristic `00000031-…` (read/write), deciCelsius LE.\n   */\n  async boost(): Promise<number> {\n    return decodeTemperatureDeciCelsius(await this.read(STORZ_BICKEL_CHARACTERISTICS.boostTemperature));\n  }\n\n  /**\n   * Write the boost temperature **offset** (°C) using write-with-response.\n   * Characteristic `00000031-…`, deciCelsius LE.\n   *\n   * @param celsius - Boost offset in degrees Celsius (e.g. `15`).\n   */\n  async setBoost(celsius: number): Promise<void> {\n    await this.write(STORZ_BICKEL_CHARACTERISTICS.boostTemperature, encodeTemperatureDeciCelsius(celsius));\n  }\n\n  /**\n   * Read the battery level (0–100 %).\n   * Characteristic `00000041-…` (read/notify), uint16 LE.\n   */\n  async batteryLevel(): Promise<Percentage> {\n    return decodeBatteryPercent(await this.read(STORZ_BICKEL_CHARACTERISTICS.batteryLevel));\n  }\n\n  /**\n   * Subscribe to battery-level updates (0–100 %). Returns an unsubscribe\n   * function. Notifications are enabled via {@link BaseProfile.subscribe}\n   * only — no CCCD write.\n   */\n  onBatteryLevel(callback: (percent: Percentage) => void): () => void {\n    return this.subscribe(STORZ_BICKEL_CHARACTERISTICS.batteryLevel, (dv) => {\n      callback(decodeBatteryPercent(dv));\n    });\n  }\n}\n\n/**\n * Storz & Bickel vendor-level helpers spanning ALL device families (Crafty/\n * Mighty, Volcano HYBRID, Veazy/Venty), as distinct from the single-family\n * {@link StorzBickelProfile}.\n */\nexport const StorzBickel = {\n  /**\n   * The de-duped, canonical-lowercase union of every `getPrimaryService`-opened\n   * service across all three S&B families ({@link STORZ_BICKEL_FAMILY_SERVICES}).\n   * Pass this as `optionalServices` to a single `requestDevice` so the picker can\n   * reach ANY Storz & Bickel device regardless of family.\n   *\n   * @returns De-duped canonical service UUIDs (first-seen order).\n   *\n   * @example\n   * ```ts\n   * const device = await ble.requestDevice({\n   *   filters: [{ namePrefix: 'S&B' }, { namePrefix: 'STORZ' }],\n   *   optionalServices: StorzBickel.allServices(),\n   * });\n   * ```\n   */\n  allServices(): string[] {\n    return deriveOptionalServices(\n      STORZ_BICKEL_FAMILY_SERVICES.crafty,\n      STORZ_BICKEL_FAMILY_SERVICES.volcano,\n      STORZ_BICKEL_FAMILY_SERVICES.veazyVenty,\n    );\n  },\n} as const;\n"]}