capacitor-lefu-weight-scale-plugin
v1.0.4
Published
Capacitor plugin for Lefu PeripheralApple scale – scan, connect, weigh, body fat (personal details from app)
Readme
capacitor-lefu-weight-scale-plugin
Capacitor 8 plugin for Lefu scales: Bluetooth scan, connect, weigh, body composition (body fat, muscle, BMR, etc.). Supports PeripheralApple and PeripheralBorre device types. The app passes personal details (height, age, gender); the plugin handles permissions, BT, scan, connect, measurement, and returns a full PhysicalDataResult with metrics and optional status labels. Progress events are emitted during the flow for UI feedback (scanning, connecting, weighing, heart rate, calculating).
Requirements
- Capacitor 8 (
@capacitor/core^8.0.0) - Android: minSdk 24, targetSdk 36
- Lefu config and keys: App key, app secret, calculation secrets (4-electrode and 8-electrode), and
lefu.configfrom Lefu Open Platform. All must be provided by your app - the plugin contains no hardcoded secrets or defaults. - Bluetooth & location: Android may require location enabled for BLE scan (depending on OS version).
Installation
In your Capacitor app:
npm install capacitor-lefu-weight-scale-plugin
npx cap syncAndroid setup
Credentials (ALL REQUIRED)
You must provide all Lefu credentials when callinginit(). The plugin contains no hardcoded secrets or defaults - everything must come from your app:await LefuScale.init({ appKey: 'YOUR_APP_KEY', // Required - from Lefu Open Platform appSecret: 'YOUR_APP_SECRET', // Required - from Lefu Open Platform calculationSecret1: 'YOUR_CALC_SECRET_1', // Required - 4-electrode secret (from Lefu) calculationSecret2: 'YOUR_CALC_SECRET_2', // Required - 8-electrode secret (from Lefu) configFileName: 'lefu.config' // Required - config file name });The plugin will reject initialization immediately if any of these are missing.
Config file (REQUIRED)
Place yourlefu.configfrom Lefu Open Platform in your Capacitor app's shared assets folder:public/lefu.config(recommended - Capacitor syncs this to both platforms)- Or
src/assets/lefu.config(if using Vite/other bundlers)
After placing the file, run
npx cap syncto copy it to both Android and iOS:- Android:
android/app/src/main/assets/lefu.config - iOS: App bundle (via Copy Bundle Resources)
The plugin ONLY looks in your app's assets/bundle. No fallback to plugin assets.
If not found, initialization will fail with
CONFIG_FILE_NOT_FOUNDerror.Permissions
The plugin requests Bluetooth permissions at runtime; no extra manifest entries are required beyond what Capacitor and the plugin add.
iOS setup
CocoaPods required. This plugin ships a CocoaPods podspec only (no SPM Package.swift). If your Capacitor iOS app was created with Swift Package Manager (Capacitor 8 default), you will see "LefuScale plugin is not implemented on ios" because the native plugin is not linked. Fix it by switching the app to CocoaPods: add a Podfile in ios/App/ that includes Capacitor and pod 'LefuScalePlugin', :path => '../../lefu-plugin' (adjust path if your plugin lives elsewhere), remove the CapApp-SPM package dependency from the Xcode project, then run pod install and open App.xcworkspace (not the .xcodeproj). The plugin repo has a root-level LefuScalePlugin.podspec so the :path can point at the plugin root.
Credentials (ALL REQUIRED)
You must provide all Lefu credentials when callinginit(). The plugin contains no hardcoded secrets or defaults - everything must come from your app:await LefuScale.init({ appKey: 'YOUR_APP_KEY', // Required - from Lefu Open Platform appSecret: 'YOUR_APP_SECRET', // Required - from Lefu Open Platform calculationSecret1: 'YOUR_CALC_SECRET_1', // Required - 4-electrode secret (from Lefu) calculationSecret2: 'YOUR_CALC_SECRET_2', // Required - 8-electrode secret (from Lefu) configFileName: 'lefu.config' // Required - config file name });The plugin will reject initialization immediately if any of these are missing.
Config file (REQUIRED)
Thelefu.configfile should be in your Capacitor app's shared assets folder (public/lefu.config). After runningnpx cap sync, Capacitor automatically copies it to your iOS app bundle.The plugin ONLY looks in your app bundle (
Bundle.main). No fallback to plugin bundle.If you need to add it manually:
- Ensure it's in
public/lefu.configin your Capacitor app root - Run
npx cap syncto copy it to iOS - Or manually add it to your app's "Copy Bundle Resources" build phase in Xcode
If not found, initialization will fail with
CONFIG_FILE_NOT_FOUNDerror.- Ensure it's in
Lefu SDK pods
The plugin Podspec depends onPPBaseKit,PPBluetoothKit,PPCalculateKit, andPPBasicCalculateKit. Ensure your app or the plugin can resolve these (same versions as BluetoothKit-iOSDemo: 1.2.17, 1.2.33, 1.2.24, 1.0.5). If they are in a private CocoaPods source, add it to your app's Podfile.Bluetooth usage
AddNSBluetoothAlwaysUsageDescription(and if neededNSBluetoothPeripheralUsageDescription) to your app'sInfo.plistfor Bluetooth access.Plugin is a static framework
The plugin is built as a static framework (s.static_framework = truein the podspec). Its code is linked into your app binary (no separateLefuScalePlugin.framework). This is required so the Lefu SDK's Objective-C category (PPBodyFatModel+Calculate, which providesinitWithInputModel:) is loaded when your app links PPCalculateKit with-ObjC. Do not change the plugin to a dynamic framework or body composition will crash with "unrecognized selector initWithInputModel:".
Integrating this plugin in other apps
When you add this plugin to a different Capacitor app:
Use CocoaPods for iOS
The plugin is CocoaPods-only. In the host app'sios/App/add aPodfilethat includes Capacitor andpod 'LefuScalePlugin', :path => '...'(path to the plugin), thenpod installand open App.xcworkspace.Allow static framework in the app's Podfile
The plugin depends on PP* pods that ship as static XCFrameworks. Add this so CocoaPods doesn't reject the setup:pre_install do |installer| Pod::Installer::Xcode::TargetValidator.send(:define_method, :verify_no_static_framework_transitive_dependencies) {} endApp must get
-ObjCwhen linking
So that the PPCalculateKit category is loaded, the App target must link with-ObjC. The example app's Podfile adds this inpost_install(inject intoPods-App.debug.xcconfig/Pods-App.release.xcconfig, or setOTHER_LDFLAGSon the App target). If you don't add-ObjCfor the App, body composition can crash with "initWithInputModel: unrecognized selector".Do not add PP pods to the App target*
The plugin already depends on PPBaseKit, PPBluetoothKit, PPCalculateKit, PPBasicCalculateKit. Do not add these pods to the App target; that would create duplicate classes (e.g. "PPBodyFatModel is implemented in both …") and spurious crashes. The plugin as a static framework is the only place that should pull them in.Copy from the example app
Reuse the example app'sios/App/Podfile(includingpre_installandpost_installthat add CAPACITOR_DEBUG and-ObjC, and framework search paths). Adjust the plugin:pathand any paths for Capacitor.Config, keys, Bluetooth
lefu.configin shared assets folder (public/lefu.config) - Capacitor syncs to both platforms (required)appKey,appSecret,calculationSecret1,calculationSecret2,configFileNameviainit()method (all required)NSBluetoothAlwaysUsageDescriptionin Info.plist (required)
API reference
init(options: InitOptions): Promise<void>
Initialize the Lefu SDK once (e.g. at app startup). You must provide ALL Lefu credentials and ensure lefu.config is in your app's shared assets folder (public/lefu.config). After running npx cap sync, Capacitor copies it to both Android and iOS. The plugin contains no hardcoded secrets or defaults - everything must come from your app.
| Option | Type | Required | Description |
|---------------------|--------|----------|--------------------------------|
| appKey | string | Yes | App Key from Lefu Open Platform. Plugin will reject if missing. |
| appSecret | string | Yes | App Secret from Lefu Open Platform. Plugin will reject if missing. |
| calculationSecret1| string | Yes | Calculation secret for 4-electrode devices (TwoLegs140, TwoArms140, TwoLegs240). From Lefu (e.g., secret.xlsx). Plugin will reject if missing. |
| calculationSecret2| string | Yes | Calculation secret for 8-electrode devices (Body270). From Lefu (e.g., secret.xlsx). Plugin will reject if missing. |
| configFileName | string | Yes | Config file name (e.g., "lefu.config"). The file must be in your app's shared assets folder (public/lefu.config). Capacitor syncs it to both platforms. Plugin will reject if missing. |
ensureBluetoothReady(): Promise<void>
Ensures Bluetooth permissions are granted and Bluetooth is on. Resolves when ready. Rejects if permission is denied or the user refuses to turn Bluetooth on. May prompt the system "Turn on Bluetooth?" dialog.
startPhysicalDataFlow(options: { personalDetails: PersonalDetails; deviceType?: 'apple' \| 'borre' \| 'auto' }): Promise<PhysicalDataResult>
Runs the full flow: permissions + BT on → scan for scale devices → connect to the first device found (or after timeout) → weigh → body fat calculation → disconnect. Returns the full PhysicalDataResult.
Auto-detect (default): Omit deviceType or set deviceType: 'auto' to scan for both PeripheralApple and PeripheralBorre; the plugin picks the first device found and uses the correct controller/calculation for that type. You do not need to specify the device type.
Restrict scan: Set deviceType: 'apple' or deviceType: 'borre' to scan only that type. If multiple devices are found, the plugin connects to the first; the app can use connectAndMeasure with a selected device.
| Option | Type | Required | Description |
|---------------------|------------------|----------|--------------------|
| personalDetails | PersonalDetails | Yes | Height, age, gender (see below) |
| deviceType | 'apple' \| 'borre' \| 'auto' | No | Default 'auto': scan both types and auto-detect. Use 'apple' or 'borre' to scan only that type. |
connectAndMeasure(options: { device: ScannedDevice; personalDetails: PersonalDetails }): Promise<PhysicalDataResult>
When multiple devices are found, call this with the user-selected device and the same personal details to connect and run weigh + body fat, then get PhysicalDataResult. The plugin auto-detects whether the device is Apple or Borre from the stored device info; you do not need to pass deviceType.
| Option | Type | Required | Description |
|---------------------|------------------|----------|--------------------|
| device | ScannedDevice | Yes | deviceName, deviceMac, rssi (from the scan / progress event) |
| personalDetails | PersonalDetails | Yes | Height, age, gender |
disconnect(): Promise<void>
Disconnects the current scale connection. Safe to call when not connected.
Personal details
Pass these when starting the flow or calling connectAndMeasure:
| Field | Type | Required | Range / values |
|-----------------|---------|----------|-----------------------------|
| heightCm | number | Yes | 90–220 (cm) |
| age | number | Yes | 6–99 (years) |
| gender | string | Yes | 'male' | 'female' |
| isAthleteMode| boolean | No | Default false; 4-electrode dual-frequency only |
Measurement progress events
The plugin emits measurementProgress events during the flow so the app can show step-by-step messages (e.g. "Searching for scale…", "Connecting…", "Weighing…", "Measuring heart rate…", "Calculating body composition…").
Listen for events
import { LefuScale } from 'capacitor-lefu-weight-scale-plugin';
import type { MeasurementProgressEvent } from 'capacitor-lefu-weight-scale-plugin';
const handle = await LefuScale.addListener('measurementProgress', (event: MeasurementProgressEvent) => {
console.log(event.stage, event.message);
// Update UI: event.stage, event.message, event.weightKg, event.errorCode, etc.
});
// Later: remove listener
handle.remove();Note: addListener is provided by the Capacitor runtime on native. It is not implemented on web (plugin throws on web for scale methods).
Event payload: MeasurementProgressEvent
| Field | Type | Description |
|-------------|----------|-------------|
| stage | string | One of the stages below |
| message | string? | Human-readable message for UI |
| weightKg | number? | Live weight when stage === 'weighing' |
| unit | string? | "kg" or "lb" when weighing |
| deviceCount | number? | Number of devices when stage === 'device_found' |
| errorCode | string? | Error code when stage === 'error' |
Progress stages (MeasurementProgressStage)
| Stage | When it is emitted |
|------------------------|--------------------|
| scan_started | BLE scan started |
| scan_stopped | Scan stopped (device found or timeout) |
| device_found | First PeripheralApple device found |
| connecting | Connecting to the scale |
| connected | BLE connected; user can step on scale |
| weighing | Live weight updates (includes weightKg, unit) |
| weight_stable | Weight and impedance stable |
| heart_rate_measuring | Heart rate measurement in progress |
| impedance_measuring | Impedance measurement (device-dependent) |
| measurement_complete | Scale measurement finished; plugin will calculate body fat |
| calculating | Building body composition result |
| complete | Result ready; promise will resolve |
| error | Error (connection lost, no devices, over weight, etc.); includes message and optional errorCode |
Result: PhysicalDataResult
Returned by startPhysicalDataFlow and connectAndMeasure.
interface PhysicalDataResult {
metrics: PhysicalDataMetrics;
errorType: string; // e.g. "PP_ERROR_TYPE_NONE"
}PhysicalDataMetrics
All values are MetricEntry: { value, display, status? }.
- value – raw number or string
- display – formatted for UI (e.g.
"72.50 kg","18.2%") - status – optional label from SDK (e.g.
"Standard","Slightly low","Overweight") when the metric has a standard range
| Metric key | Description |
|--------------------------|----------------------------|
| weightKg | Weight (kg) |
| bmi | Body mass index |
| bodyFatPercent | Body fat % |
| fatMassKg | Fat mass (kg) |
| musclePercent | Muscle % |
| muscleMassKg | Muscle mass (kg) |
| bmrKcal | Basal metabolic rate (kcal/day) |
| bodyAge | Body age (years) |
| healthScore | Health score |
| proteinPercent | Protein % |
| waterPercent | Water % |
| subcutaneousFatPercent | Subcutaneous fat % |
| skeletalMusclePercent | Skeletal muscle % |
| skeletalMuscleMassKg | Skeletal muscle mass (kg) |
| visceralFatLevel | Visceral fat level |
| boneMassKg | Bone mass (kg) |
| standardWeightKg | Standard weight (kg) |
| idealWeightKg | Ideal weight (kg) |
| controlWeightKg | Control weight (kg) |
| fatControlKg | Fat control (kg) |
| muscleControlKg | Muscle control (kg) |
| heartRateBpm | Heart rate (bpm) |
| footLengthCm | Foot length (cm) |
| bodyType | Body type (string) |
| obesityLevel | Obesity level (string) |
| healthEvaluation | Health evaluation (string) |
Usage example
import { LefuScale } from 'capacitor-lefu-weight-scale-plugin';
import type {
PersonalDetails,
PhysicalDataResult,
MeasurementProgressEvent,
} from 'capacitor-lefu-weight-scale-plugin';
// 1. Initialize with all required credentials (must be called before any other method)
await LefuScale.init({
appKey: process.env.LEFU_APP_KEY || 'YOUR_APP_KEY',
appSecret: process.env.LEFU_APP_SECRET || 'YOUR_APP_SECRET',
calculationSecret1: process.env.LEFU_CALC_SECRET_1 || 'YOUR_CALC_SECRET_1',
calculationSecret2: process.env.LEFU_CALC_SECRET_2 || 'YOUR_CALC_SECRET_2',
configFileName: 'lefu.config',
});
// 2. Listen for progress (e.g. in React useEffect)
const handle = await LefuScale.addListener('measurementProgress', (event: MeasurementProgressEvent) => {
console.log(event.stage, event.message);
if (event.stage === 'weighing' && event.weightKg != null) {
console.log('Live weight:', event.weightKg, event.unit);
}
if (event.stage === 'error') {
console.error(event.message, event.errorCode);
}
});
// 3. Ensure Bluetooth ready (or skip and let startPhysicalDataFlow do it)
await LefuScale.ensureBluetoothReady();
// 4. Run full flow
const personalDetails: PersonalDetails = {
heightCm: 175,
age: 32,
gender: 'male',
isAthleteMode: false,
};
const result: PhysicalDataResult = await LefuScale.startPhysicalDataFlow({ personalDetails });
console.log('Weight:', result.metrics.weightKg.display, result.metrics.weightKg.status);
console.log('BMI:', result.metrics.bmi.display);
console.log('Body fat:', result.metrics.bodyFatPercent.display);
// 5. Cleanup listener
handle.remove();
// 6. Optional: disconnect explicitly
await LefuScale.disconnect();TypeScript types (exported)
- PersonalDetails – height, age, gender, isAthleteMode
- ScannedDevice – deviceName, deviceMac, rssi
- PhysicalDataResult – metrics, errorType
- PhysicalDataMetrics – all metric keys → MetricEntry
- MetricEntry – value, display, status?
- MeasurementProgressEvent – stage, message?, weightKg?, unit?, deviceCount?, errorCode?
- MeasurementProgressStage – union of all stage strings
- InitOptions – appKey, appSecret, calculationSecret1, calculationSecret2, configFileName (all required)
Implementation status
| Platform | Status | |----------|--------| | TypeScript | Definitions, web stub (throws "not implemented on web") | | Android | Full flow: init, permissions, scan, connect, weigh, body fat, result, progress events | | iOS | Full flow: init, ensureBluetoothReady, scan, connect, weigh, body fat, result, progress events (see iOS setup below) |
Reference
- Lefu Open Platform – app key, secret, config, calculation secret
- CAPACITOR_PLUGIN_GUIDE (in Lefu-SDK-IntervalHealth repo) – SDK details, PPDataChangeListener, body fat calculation
