npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.config from 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 sync

Android setup

  1. Credentials (ALL REQUIRED)
    You must provide all Lefu credentials when calling init(). 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.

  2. Config file (REQUIRED)
    Place your lefu.config from 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 sync to 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_FOUND error.

  3. 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.

  1. Credentials (ALL REQUIRED)
    You must provide all Lefu credentials when calling init(). 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.

  2. Config file (REQUIRED)
    The lefu.config file should be in your Capacitor app's shared assets folder (public/lefu.config). After running npx 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.config in your Capacitor app root
    • Run npx cap sync to 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_FOUND error.

  3. Lefu SDK pods
    The plugin Podspec depends on PPBaseKit, PPBluetoothKit, PPCalculateKit, and PPBasicCalculateKit. 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.

  4. Bluetooth usage
    Add NSBluetoothAlwaysUsageDescription (and if needed NSBluetoothPeripheralUsageDescription) to your app's Info.plist for Bluetooth access.

  5. Plugin is a static framework
    The plugin is built as a static framework (s.static_framework = true in the podspec). Its code is linked into your app binary (no separate LefuScalePlugin.framework). This is required so the Lefu SDK's Objective-C category (PPBodyFatModel+Calculate, which provides initWithInputModel:) 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:

  1. Use CocoaPods for iOS
    The plugin is CocoaPods-only. In the host app's ios/App/ add a Podfile that includes Capacitor and pod 'LefuScalePlugin', :path => '...' (path to the plugin), then pod install and open App.xcworkspace.

  2. 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) {}
    end
  3. App must get -ObjC when linking
    So that the PPCalculateKit category is loaded, the App target must link with -ObjC. The example app's Podfile adds this in post_install (inject into Pods-App.debug.xcconfig / Pods-App.release.xcconfig, or set OTHER_LDFLAGS on the App target). If you don't add -ObjC for the App, body composition can crash with "initWithInputModel: unrecognized selector".

  4. 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.

  5. Copy from the example app
    Reuse the example app's ios/App/Podfile (including pre_install and post_install that add CAPACITOR_DEBUG and -ObjC, and framework search paths). Adjust the plugin :path and any paths for Capacitor.

  6. Config, keys, Bluetooth

    • lefu.config in shared assets folder (public/lefu.config) - Capacitor syncs to both platforms (required)
    • appKey, appSecret, calculationSecret1, calculationSecret2, configFileName via init() method (all required)
    • NSBluetoothAlwaysUsageDescription in 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