# BLE fitness sensors - JavaScript bridge API

> Heart rate, speed, cadence, power and battery from standard Bluetooth Low Energy profiles.

- **Applies to:** AppMint
- **Source:** extracted from the app runtime and its per-method example files; this page is generated from them.
- **HTML:** https://freewebtoapk.com/docs/api/ble

### `bleConnect`

```js
window.WebToApk.bleConnect(address: String)
```

Runtime for permBluetooth. No wizard switch sets it: the build arms it when the page's own code calls these methods (or Web Bluetooth). Before this runtime the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

**Example**

Connects to one fitness sensor found by `bleStartScan()` and starts streaming its readings.

**Returns:** nothing. Progress and data arrive as `appmint:ble` events (and `window.onAppMintBle`): `{kind:'state', state:'connecting', address}`, `{kind:'state', state:'connected', address}`, `{kind:'state', state:'ready', services:[...]}`, then `{kind:'data', type, data}` for every reading. **Needs:** Bluetooth (turned on by a ZIP or HTML build when your code uses the sensor bridge) and the Nearby devices permission, asked on first use.

Pass the `address` from a `device` event, from the user's tap:

```js
function connectSensor(address) {
  if (!window.WebToApk || typeof window.WebToApk.bleConnect !== 'function') return;
  window.WebToApk.bleConnect(address);
}

window.onAppMintBle = function (d) {
  if (d.kind === 'state') {
    // 'connecting' | 'connected' | 'ready' | 'disconnected' (also 'scanning' / 'scan_stopped')
    showStatus(d.state);
  } else if (d.kind === 'data') {
    if (d.type === 'heart_rate')    show('bpm', d.data.bpm);
    if (d.type === 'speed_cadence') {                   // either key can be missing in one packet
      if (d.data.speedKmh !== undefined)   show('speed', d.data.speedKmh.toFixed(1) + ' km/h');
      if (d.data.cadenceRpm !== undefined) show('cadence', Math.round(d.data.cadenceRpm) + ' rpm');
    }
    if (d.type === 'power')   show('power', d.data.watts + ' W');
    if (d.type === 'running') show('pace', d.data.speedKmh.toFixed(1) + ' km/h, ' + d.data.cadenceSpm + ' spm');
    if (d.type === 'battery') show('battery', d.data.percent + '%');
  } else if (d.kind === 'error') {
    showStatus('Could not connect: ' + d.error);
  }
};
```

**Notes:** One sensor at a time: connecting stops any scan and drops the previous sensor first (you get its `disconnected`). `ready.services` lists the full UUIDs of the supported services this sensor has. The first speed/cadence packet after connecting sends nothing - speed and cadence need two packets to compute. `heart_rate` also carries `rrIntervalsMs` (beat-to-beat intervals in ms, when the strap sends them), `energyKj` and `contact` when the strap reports them; `power` carries `cadenceRpm` from its second packet on when the meter sends crank data. Errors: `not_enabled`, `permission_denied`, `bluetooth_unavailable`, `invalid_address`. If the sensor walks out of range you get `{kind:'state', state:'disconnected', address}`.

### `bleDisconnect`

```js
window.WebToApk.bleDisconnect()
```

Runtime for permBluetooth. No wizard switch sets it: the build arms it when the page's own code calls these methods (or Web Bluetooth). Before this runtime the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

**Example**

Disconnects the fitness sensor connected with `bleConnect()`.

**Returns:** nothing. If a sensor was connected, an `appmint:ble` event `{kind:'state', state:'disconnected'}` follows. **Needs:** Bluetooth (turned on by a ZIP or HTML build when your code uses the sensor bridge); without it the call does nothing.

```js
document.getElementById('end-workout').addEventListener('click', function () {
  if (window.WebToApk && typeof window.WebToApk.bleDisconnect === 'function') {
    window.WebToApk.bleDisconnect();
  }
});

window.addEventListener('appmint:ble', function (e) {
  if (e.detail.kind === 'state' && e.detail.state === 'disconnected') {
    showStatus('Sensor disconnected');
  }
});
```

**Notes:** The `disconnected` event you cause yourself has no `address`; one caused by the sensor (out of range, switched off) has it. The app also disconnects when it closes.

### `bleSetWheelCircumference`

```js
window.WebToApk.bleSetWheelCircumference(mm: Int)
```

Runtime for permBluetooth. No wizard switch sets it: the build arms it when the page's own code calls these methods (or Web Bluetooth). Before this runtime the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

**Example**

Sets the bike wheel size (circumference in millimetres) used to turn a speed sensor's wheel turns into km/h.

**Returns:** nothing. **Needs:** nothing to call it; the speed readings themselves need a connected cycling speed sensor (see `bleConnect`).

```js
// 2096 mm = 700x23c (the default). 2105 = 700x25c, 2136 = 700x28c, 2070 = 26x2.0 MTB.
function setWheel(mm) {
  if (window.WebToApk && typeof window.WebToApk.bleSetWheelCircumference === 'function') {
    window.WebToApk.bleSetWheelCircumference(mm);
  }
}

document.getElementById('wheel').addEventListener('change', function (e) {
  setWheel(parseInt(e.target.value, 10));
});
```

**Notes:** Values outside 500-4000 mm are ignored silently (the old value stays). The setting lasts until the app closes, so set it again after each launch, for example right before `bleConnect()`. It changes `speedKmh` in the `speed_cadence` data of `appmint:ble`; cadence is not affected.

### `bleStartScan`

```js
window.WebToApk.bleStartScan()
```

Runtime for permBluetooth. No wizard switch sets it: the build arms it when the page's own code calls these methods (or Web Bluetooth). Before this runtime the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

**Example**

Scans for nearby Bluetooth fitness sensors (heart rate, cycling speed/cadence, power, running footpods) for 15 seconds.

**Returns:** nothing. Results arrive as `appmint:ble` events (and `window.onAppMintBle(detail)`): one `{kind:'device', address, name, sensors, rssi}` per sensor found, plus `{kind:'state', state:'scanning'}` at the start and `{kind:'state', state:'scan_stopped'}` at the end. **Needs:** Bluetooth. There is no switch for it: when you build from a ZIP or an HTML page, the build turns Bluetooth on by itself if your code calls `bleStartScan` / `bleConnect` or uses `navigator.bluetooth`. A website (URL) build cannot turn it on. The first scan asks the user for the Nearby devices permission (Location on Android 11 and older).

Listen first, then scan from a button tap:

```js
var found = {};

window.addEventListener('appmint:ble', function (e) {
  var d = e.detail;
  if (d.kind === 'device') {
    found[d.address] = d;
    // d.sensors: any of 'heart_rate', 'speed_cadence', 'power', 'running'
    addRow(d.address, (d.name || 'Unnamed sensor') + ' ' + d.sensors.join(', ') + ' (' + d.rssi + ' dBm)');
  } else if (d.kind === 'state' && d.state === 'scanning') {
    showStatus('Looking for sensors...');
  } else if (d.kind === 'state' && d.state === 'scan_stopped') {
    showStatus(Object.keys(found).length ? 'Pick a sensor' : 'No sensors found. Wake the sensor and try again.');
  } else if (d.kind === 'error') {
    showStatus('Bluetooth error: ' + d.error);
  }
});

document.getElementById('scan').addEventListener('click', function () {
  if (window.WebToApk && typeof window.WebToApk.bleStartScan === 'function') {
    found = {};
    window.WebToApk.bleStartScan();
  } else {
    showStatus('Sensors work in the installed app only.');
  }
});
```

**Notes:** Error codes in `{kind:'error', error}`: `not_enabled` (the build has no Bluetooth), `permission_denied`, `bluetooth_unavailable` (no adapter), `bluetooth_off`, `scan_failed` (may carry the Android `code`). The scan only reports devices that advertise one of the four fitness services, and each device only once per scan. Starting a new scan stops the old one. Use either the `appmint:ble` listener or `window.onAppMintBle`, not both - both are called for every message.

### `bleStopScan`

```js
window.WebToApk.bleStopScan()
```

Runtime for permBluetooth. No wizard switch sets it: the build arms it when the page's own code calls these methods (or Web Bluetooth). Before this runtime the permissions were declared while nothing in the runtime touched a sensor, the exact declared-but-dead pattern SmsBridge documents. Results stream as `appmint:ble` events / window.onAppMintBle({kind, ...}).

_Described by its group, BLE fitness sensors (heart rate / speed / cadence / power / battery), rather than on its own._

**Example**

Stops a sensor scan started with `bleStartScan()` before its 15 seconds are up.

**Returns:** nothing. If a scan was running, an `appmint:ble` event `{kind:'state', state:'scan_stopped'}` follows. **Needs:** Bluetooth (turned on by a ZIP or HTML build when your code uses the sensor bridge); without it the call does nothing.

```js
window.addEventListener('appmint:ble', function (e) {
  if (e.detail.kind === 'state' && e.detail.state === 'scan_stopped') {
    document.getElementById('scan').disabled = false;
  }
});

document.getElementById('stop-scan').addEventListener('click', function () {
  if (window.WebToApk && typeof window.WebToApk.bleStopScan === 'function') {
    window.WebToApk.bleStopScan();
  }
});
```

**Notes:** You rarely need this: the scan stops by itself after 15 seconds, and `bleConnect()` stops it too. Calling it when no scan is running is safe and sends no event.

