Skip to main content
Glama
dncore

efferent-ble-simulator

by dncore

Efferent simulates Bluetooth Low Energy (BLE) peripheral devices over a real radio (Linux BlueZ): FTMS smart trainers, cycling power meters, speed/cadence sensors and heart rate monitors. It exposes an MCP server (stdio or HTTP) so AI agents — and the bundled Web dashboard — can start, configure and monitor simulations as if controlling a real trainer.


Features

  • 4 device types over real BLE radio: FTMS smart trainer (0x1826), Cycling Power (0x1818), Speed/Cadence (0x1816), Heart Rate (0x180D)

  • Full FTMS control point support: Request Control / Start / Stop / Reset / Set Target Power / Set Resistance / Set Indoor Bike Simulation Params

  • Dynamic simulation: human-like power/cadence drift + physics-based speed model (weight, grade, Crr, CdA), 5 riding scenarios including the scripted ride_script

  • Hot parameter update: change params while running — the connected phone keeps its link and receives new values on the next notify (no re-pair)

  • Named config presets: save / load / rename / delete up to 20 named configs, shared between MCP and Dashboard

  • 17 MCP tools for agents: control, configure, config presets, interaction rules, sessions, logs, live device state, version

  • Web dashboard (React + shadcn/ui): status, forms, one-click ride templates, interaction rules, session/config management, live log side-panel

  • Docker Compose one-command deployment (MCP + Dashboard + Skill file over HTTP), long-running with health checks

  • Versioned: MCP serverInfo.version + ble_get_version let agents detect updates


Related MCP server: pyBTMCP

Runtime Environment

Requirement

Detail

OS

Linux only — the simulator drives the host BlueZ over D-Bus

BlueZ

≥ 5.87 (5.86 has an advertisement-registration bug)

Hardware

A BLE-capable adapter (built-in or USB dongle) is required to actually broadcast

D-Bus policy

/etc/dbus-1/system.d/ble-simulator.conf (installed by npm run setup) — required for non-root GATT registration

Docker

The mcp container mounts the host D-Bus socket; the BLE radio always stays on the host

Not supported

macOS / Windows natively (would need a different BLE stack); without any BLE adapter the MCP service runs but nothing is transmitted

Why Linux only

The fundamental reason: Efferent does not "pretend" to be a Bluetooth device at the application layer — it runs as a D-Bus client of BlueZ, the Linux Bluetooth stack. The BLE peripheral role (GATT server + advertising) is a first-class citizen on Linux: the kernel's net/bluetooth subsystem handles the radio protocol, and the userspace bluetoothd daemon exposes the full org.bluez API over the system D-Bus — any process can register custom GATT services via GattManager1 and control advertising precisely via LEAdvertisingManager1. All of Efferent's peripheral logic (FTMS characteristics, notify push, hot parameter updates, stable serial numbers) is calls against these interfaces, backed by a system-level D-Bus policy file (/etc/dbus-1/system.d/ble-simulator.conf) that grants non-root GATT registration. No other desktop OS offers this.

Both the Microsoft Bluetooth Stack (Windows) and CoreBluetooth (macOS) are closed high-level APIs: no BlueZ D-Bus interface, no HCI-level control. Efferent's code cannot run on top of them — details below.

Windows

Windows' WinRT API (Windows.Devices.Bluetooth) is centered on the central role (GATT client). A peripheral-role (GATT server) API exists, but it is hardware-dependent — per Microsoft's official FAQ, not all Windows devices support the Peripheral Role; whether emulation works at all depends on the adapter's firmware. Even when it does, Windows' GATT server is far weaker than BlueZ: advertising parameters and custom advertisement data are restricted, so you cannot control how the device is discovered the way LEAdvertisingManager1 allows. And since the codebase is written against BlueZ D-Bus, it cannot run on Windows as-is; an equivalent rewrite is both hardware-limited and loses core capabilities.

macOS

macOS uses the proprietary CoreBluetooth stack. It does offer a peripheral mode (CBPeripheralManager), but as a closed high-level wrapper it is not on par with BlueZ: advertising is limited to the local name and service UUIDs — custom manufacturer data cannot actually be broadcast; advertising parameters and the HCI layer are not controllable; GATT behavior is subject to system policy. Efferent's core features — multiple device types, runtime hot updates, fine-grained advertising/notification control — depend on BlueZ-level openness that CoreBluetooth cannot express. There is also no D-Bus or BlueZ on macOS, so the code cannot run there.

WSL2

WSL2 is a lightweight Hyper-V virtual machine, not "Linux built into Windows", so it inherits three fundamental VM limitations:

  • No radio of its own. The Bluetooth adapter is owned exclusively by the Windows Bluetooth stack; the WSL2 VM sees no HCI device. A USB dongle can be shared in via the official usbipd-win route, but that requires detaching the adapter from Windows first — the host's Bluetooth dies while WSL uses the hardware, a fragile trade-off.

  • No Bluetooth in the default kernel. Microsoft's official WSL kernel config does not enable Bluetooth (CONFIG_BT); you must recompile the kernel and enable the HCI drivers. This is a long-standing community request (microsoft/WSL#242) with no official built-in support.

  • Missing userspace components. Even with a custom kernel, WSL2 ships without bluez/bluetoothd; you would install and run them manually with no official support path.

In short: running Efferent in WSL2 means recompiling the kernel + USB passthrough + hand-rolling the Bluetooth userspace while the host gives up its own Bluetooth — replicating an entire Bluetooth stack inside a VM and fighting Windows for the same hardware. WSL1 shares the Windows kernel and has no Linux Bluetooth stack at all. Therefore: native Linux only.

Architecture

┌──────────────┐   MCP protocol    ┌───────────────────────────┐
│ AI Agent /   │ ◄───────────────► │ MCP Server (HTTP :3300)   │
│ MCP Client   │  stdio / :3300    │ Web Dashboard (:3330)     │
│ (Claude,     │                   └────────────┬──────────────┘
│  Codex, pi)  │                                │ BlueZ D-Bus (system bus)
└──────────────┘                                ▼
                                     ┌───────────────────────────┐
                                     │ Host Linux BlueZ          │
                                     │ bluetoothd + BLE adapter  │
                                     │ (D-Bus policy, ≥ 5.87)    │
                                     └────────────┬──────────────┘
                                                  │ HCI / radio
                                                  ▼
                                     ┌───────────────────────────┐
                                     │ BLE advertisement + GATT  │
                                     │ FTMS / Power / CSC / HR   │
                                     └────────────┬──────────────┘
                                                  │
                                     ┌────────────▼──────────────┐
                                     │ Phone / Cycling App       │
                                     │ (Zwift, Garmin, Strava...)│
                                     └───────────────────────────┘

Device Types

Type

BLE Service

Characteristics

FTMS smart trainer

0x1826

FM Feature, Indoor Bike Data (notify), Control Point (write/indicate), Status, Resistance/Power ranges

Cycling Power

0x1818

Power/Cadence measurement, wheel & crank data

Speed/Cadence (CSC)

0x1816

Wheel & crank revolution counters

Heart Rate

0x180D

HR measurement, body sensor location, battery


Quick Start

Prerequisites

  • Linux with BlueZ ≥ 5.87 (bluetoothd) and a BLE-capable adapter

    BlueZ 5.86 has an advertisement-registration bug; on kernels with strict MGMT validation every RegisterAdvertisement fails. Upgrade first: sudo pacman -S bluez && sudo systemctl restart bluetooth (Arch) / sudo apt install bluez (Debian).

  • Node.js ≥ 18

1. Native install

npm install
npm run setup     # sudo — installs D-Bus policy, enables bluetoothd, verifies GATT
npm run build
npm run start:http   # HTTP mode on :3300 (or npm start for stdio)

npm run setup writes /etc/dbus-1/system.d/ble-simulator.confrequired for non-root GATT registration (without it the device advertises but cannot be connected).

bash scripts/docker-up.sh     # host prep (sudo, once) + build + start

URL

Purpose

http://<host>:3300/mcp

MCP endpoint

http://<host>:3330/

Web dashboard

http://<host>:3330/skill/SKILL.md

Operation skill (for agents)

http://<host>:3330/help

Dashboard help page

Zero-compile build (better-sqlite3 prebuilt binary); on restricted networks use a mirror:

NPM_REGISTRY=https://registry.npmmirror.com \
BETTER_SQLITE3_BINARY_HOST_MIRROR=https://registry.npmmirror.com/-/binary/better-sqlite3 \
bash scripts/docker-up.sh

Management: bash scripts/docker-up.sh status|logs|down


MCP Tools (17)

Tool

Purpose

ble_start / ble_stop / ble_restart

Start / stop / restart a simulation

ble_status

Controller state + active config + live device state (phase/power/cadence/HR)

ble_configure

Update config — hot-updates in place if running with the same device type

ble_get_config

Current/latest config as JSON (form backfill)

ble_save_config

Save current params as a named preset (max 20)

ble_list_configs / ble_get_config_detail

List / view saved presets

ble_rename_config / ble_delete_config

Rename / delete a preset

ble_set_interaction / ble_clear_interactions

Custom BLE write-response rules

ble_list_sessions / ble_get_session

Session history / detail

ble_get_logs

Persistent communication logs (connect/write/notify/error)

ble_get_version

MCP + Skill versions, changelog, Skill URL (for update detection)

Example

{
  "method": "tools/call",
  "params": {
    "name": "ble_start",
    "arguments": {
      "deviceType": "ftms",
      "ftms": { "simulation": { "enabled": true, "scenario": { "type": "ride_script" } } }
    }
  }
}

Dynamic Simulation

Enable via simulation on FTMS / CyclingPower / CSC:

"simulation": {
  "enabled": true,
  "riderWeightKg": 75, "bikeWeightKg": 8, "crr": 0.004, "cdA": 0.35,
  "fatigueFactor": 0.0005, "cadenceCoupling": "proportional", "microPauseProbability": 0.008,
  "autoStart": true,
  "scenario": { "type": "ride_script" }
}

Param

Default

Description

enabled

false

Master switch

riderWeightKg / bikeWeightKg

75 / 8

Speed physics (rider+bike mass)

crr / cdA

0.004 / 0.35

Rolling resistance / drag area

fatigueFactor

0

Power decay per minute (0 = none)

cadenceCoupling

proportional

proportional / inverse / independent

microPauseProbability

0.005

Per-tick coasting pauses

autoStart

false (ride_script defaults on)

Start riding without an app sending Start

scenario

freeride

Riding scenario (below)

Scenarios

  • steady — natural micro-variation only

  • freeride (default) — base power drifts ±20% every 60–180s

  • intervals — high/low power alternation

  • warmup_main_cooldown — warmup → main → cooldown

  • ride_script — scripted ride: a sequence of riding actions (start / cruise / climb / sprint / coast / stop), each with target power, grade and optional cadence; auto-starts, loops with repeat: true, stop zeroes power/cadence/speed. Omit phases to use the built-in default script.


Hot Update & Stable Serial

  • Hot update: ble_configure while running + same device type applies params in place — the connected phone keeps its link and receives updated values on the next notify. Device-type changes still restart (new serial forces re-discovery).

  • Stable serial: SIM_SERIAL_STABLE=1 keeps the serial stable across same-type sessions (SIM001-FTMS), so a phone can reconnect after stop/start without forgetting the device.


Environment Variables

Var

Default

Description

MCP_TRANSPORT

stdio

stdio or http

MCP_PORT / MCP_HOST

3300 / 0.0.0.0

HTTP listen address

SIM_DEVICE_PREFIX

OPEN_RIDE

Advertised name prefix

SIM_INSTANCE_ID_MODE

Instance ID mode for the device name

SIM_SERIAL_STABLE

unset

1 = stable serial for same-type sessions

SKILL_PUBLIC_URL

/skill/SKILL.md

Public Skill URL (returned by ble_get_version)


Source Layout

src/
├── index.ts             # entry, lifecycle
├── mcp-server.ts        # 17 MCP tool definitions & handlers
├── ble-controller.ts    # BlueZ D-Bus lifecycle, advertisement, hot-update
├── database.ts          # SQLite layer (sessions / saved_configs / logs)
├── config.ts            # config merge & defaults
├── version.ts           # version single-source
├── simulator.ts         # HeartRate / Battery simulators
├── cycling-simulator.ts # power/cadence simulators + physics + ride_script engine
└── devices/             # heart-rate / cycling-power / csc / ftms GATT devices

BLE peripheral access runs over the host BlueZ D-Bus (the container mounts the host D-Bus socket). See docs/design/*.md for design records.


License

MIT © BLE Simulator Contributors

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Bluetooth Low Energy (BLE) MCP server that allows AI agents to scan, connect to and communicated with BLE devices, as well as simulate BLE perhipherals.
    16
    BSD 2-Clause "Simplified"
  • F
    license
    Not graded
    quality
    Not graded
    maintenance
    A BLE fitness device simulator that enables AI agents to control simulated heart rate monitors, treadmills, and cycling trainers via the Model Context Protocol. It integrates ESP32 firmware, an MQTT broker, and a FastAPI backend for comprehensive device management and real-time interaction.
    -
  • A
    license
    A
    quality
    C
    maintenance
    A stateful Bluetooth Low Energy (BLE) MCP server that enables AI agents to scan, connect, read/write characteristics, and subscribe to notifications on BLE devices.
    35
    17
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    A lightweight Bluetooth Low Energy debug MCP server for AI agents. It provides 21 tools for BLE scanning, connection, GATT read/write, descriptor access, MTU query, notification subscription, device recon, and engine diagnostics.
    21
    Apache 2.0