fitbit_air 0.1.1 copy "fitbit_air: ^0.1.1" to clipboard
fitbit_air: ^0.1.1 copied to clipboard

Experimental Android Bluetooth access to Fitbit Air, including haptics, live heart rate, device information, and diagnostics.

fitbit_air #

Experimental Flutter access to a Fitbit Air over its direct Android Bluetooth transport: haptic controls, live heart rate, device information, and connection diagnostics. Haptics are one use case, using this communication path:

Flutter → Android BLE → Gattlink → DTLS bootstrap session → CoAP /settings → Air

This is not a Fitbit Web API wrapper. It does not require OAuth, root, unpairing, or access to Google Health's private data. The Air must already be paired to the same Android phone in the installed Google Health app.

Warning

This is independent reverse-engineering research, not an official Google or Fitbit SDK. It relies on private, version-sensitive device protocols and on a compatible, authentic Google Health installation. Android arm64 is the only configuration tested so far. Reliable wrist-tap detection has not been decoded.

Install #

Install the published package:

flutter pub add fitbit_air

Or add it to pubspec.yaml:

dependencies:
  fitbit_air: ^0.1.0

For unreleased development changes only, use the Git repository instead:

dependencies:
  fitbit_air:
    git:
      url: https://github.com/ThoreKoritzius/fitbit-air.git

The plugin contributes the Android Bluetooth permissions. Android 12+ asks the user for Nearby devices access on first use.

Use #

import 'package:fitbit_air/fitbit_air.dart';

if (await FitbitAir.isAirPaired()) {
  // Exactly one pulse. This alternates and retains low/high strength.
  final result = await FitbitAir.pulse();
  print('${result.previousIntensity} → ${result.currentIntensity}');
}

The public haptic operations have deliberately different semantics:

  • pulse() produces one pulse by alternating low/high strength and leaves the new strength active from the plugin's perspective. Google Health may later reconcile its own stored setting after the plugin disconnects.
  • setHapticIntensity() persistently selects low or high and produces one pulse only when the value changes.
  • getHapticIntensity() is read-only.
  • vibrate() is the restore-safe preview: it changes the strength, waits, then restores it. That necessarily produces two pulses.

Every write is read back and verified. The restore-safe operation also makes a best-effort restoration if its main sequence fails.

Run the example #

Connect an Android phone with a Fitbit Air already paired in Google Health, then:

cd example
flutter run

Current scope #

Capability Status
Detect an Android-bonded Fitbit Air Implemented
Trigger a short Air haptic Physically verified from this package on Pixel 7 Pro (2026-10-14)
Exactly one pulse Implemented and protocol-verified; alternates the persistent strength
Read/set low or high vibration strength Implemented and verified
Device model, firmware, manufacturer GATT read verified on Pixel 7 Pro / Air firmware 67.20001.253.2
BLE services and connection status Read-only discovery and link status decoder implemented
Scan advertised heart-rate sensors Experimental; requires Nearby devices scan permission
Restore/verify prior vibration intensity Physically verified
Root/OAuth/private PSK required No, for the bootstrap haptic path
Detect taps on the Air Not implemented
Headless haptics Release-mode background and one controlled screen-off command verified; long-term/idle reliability unresolved
Raw diagnostic events Opt-in research stream; no confirmed tap decoder
Battery resource Bootstrap read access verified; selected raw fields available in diagnostics, units/scale unverified
Heart rate Standard BLE readings and continuous updates verified; requires an active Google Health sharing session
Google Health coexistence Plugin releases GATT; Google Health remains connected. Concurrent Goldengate sessions produced packet-sequence warnings, so uninterrupted sync is not yet proven
iOS Not implemented
Non-arm64 Android Untested; requires a compatible library in the installed Google Health APK

The Google Health implementation contains a GET liveactivity resource with steps, activity, and heart-rate fields, but the Air rejects it with CoAP 4.01 in the bootstrap session available to this plugin. It is therefore documented as research rather than exposed as a misleading public API. See doc/research.md.

readDeviceInfo() returns available standard fields, the actual GATT service inventory, and the read-only connection configuration/status. Battery is nullable: the tested Air does not expose the standard Battery Service.

Heart rate uses the separate standard Bluetooth Heart Rate Service. Start the sharing flow in Google Health (Connections → Air → Share heart rate). Keep that page open during initial discovery. scanHeartRateDevices() finds advertised sensors, and readHeartRate(deviceId: ...) reads one measurement. With sharing already active for this phone, readHeartRate() uses the paired Air directly.

watchHeartRate() keeps one Bluetooth client open and streams measurements; cancel its subscription to release that client before sending a haptic:

final subscription = FitbitAir.watchHeartRate().listen((sample) {
  print('${sample.beatsPerMinute} bpm');
});
// Later:
await subscription.cancel();

An accepted subscription alone does not guarantee data: the Air accepted it but sent nothing before sharing was active. cancelOperation() cancels reads and scans; haptic writes finish their verification before returning.

Experimental device events #

Import package:fitbit_air/fitbit_air_diagnostics.dart to use FitbitAirDiagnostics.watchDeviceEvents(). This monitors a limited set of link-status and unclassified BLE characteristics, not the encrypted tracker channel. It enables notification subscriptions but sends no haptic/control commands. The demo keeps a bounded event log in memory only. Payloads may contain sensitive device data; do not upload them without user consent.

These events are not wrist-tap detection. The first physical tap test was inconclusive: subscriptions succeeded, but the connection ended before any tap-related payload could be established. Cancel the stream before using other SDK operations. Health Connect and historical stats are deferred.

FitbitAirDiagnostics.probeBatteryAccess() opens a separate Golden Gate session and performs a read-only GET of the device-info resource. Access was verified on the tested Air. It returns CoAP status and selected raw battery fields, not a verified percentage or voltage unit. This is an explicit research operation with the same Google Health coexistence caveat as other secure sessions; it is never run automatically by standard GATT reads.

Background and notification integration #

For notification-triggered commands and the headless Android test, see doc/background.md. A release-mode headless haptic was physically verified with the demo backgrounded. A controlled screen-off command also completed with duplicate suppression. An earlier trial timed out during session establishment; long-term and deep-idle reliability remain unproven. The host app must provide message reception and lifecycle management; a phone notification alone does not trigger the Air.

Native compatibility and provenance #

The package and its APKs do not redistribute Google's libxp.so. At runtime, the Android plugin verifies the installed com.fitbit.FitbitMobile signing certificate, reads the matching library from that app's publicly installed APK split, and copies it to this app's private code cache for loading. The cached copy is keyed by Google Health's version and update timestamp.

The JNI-facing Kotlin bindings are adapted from the Apache-2.0 Golden Gate Android bindings at commit b8a6f2552e12aaf92c1549dc88a41c9b7e73f1aa. See THIRD_PARTY_NOTICES.md for provenance.

This arrangement avoids distributing a proprietary native binary or reading Google Health's private files, but it creates a strict compatibility dependency: a Google Health update can change the JNI ABI and require corresponding binding updates in this package.

Safety and compatibility #

  • The operation is serialized; concurrent calls return BUSY.
  • Starting with 0.1.1, that transport guard also covers separate Flutter engines in the same Android process. It cannot lock Google Health's separate process.
  • A 45-second deadline closes the GATT connection.
  • Google Health remains the pairing owner. This plugin opens a short-lived connection and does not unregister, factory-reset, or alter pairing keys.
  • The plugin accepts the native implementation only from the expected signed Google Health package; a look-alike package is rejected.
  • Firmware or Google Health updates may change the private protocol.
  • Do not use this package for medical or safety-critical behavior.

Name and trademarks #

The Dart package name fitbit_air was unclaimed on pub.dev when checked on 2026-10-14. Availability is not a reservation until publication. Fitbit and Google are trademarks of their respective owners; this project is unaffiliated.

License #

Plugin code is MIT licensed. Adapted upstream source retains its Apache-2.0 license. The runtime-loaded Google Health library remains subject to its owner's terms and is not part of this distribution.

0
likes
0
points
149
downloads

Publisher

unverified uploader

Weekly Downloads

Experimental Android Bluetooth access to Fitbit Air, including haptics, live heart rate, device information, and diagnostics.

Repository (GitHub)
View/report issues

Topics

#bluetooth #wearable #fitbit #haptics

License

unknown (license)

Dependencies

clock, flutter

More

Packages that depend on fitbit_air

Packages that implement fitbit_air