BMES Tools (bmes) is a cross-platform Python library and command-line interface for managing
BMES100 environmental sensor devices over Bluetooth Low Energy. The CLI front-end is built with
Click and Rich.
It brings scanning, live readings, the device clock, history offload, firmware update and the BLE shell under one command.
Tip
The CLI is self-documenting. Use --help on any command for what it does and examples of how to
use it.
- Scan for devices, decoding the BTHome advertisement live into a table — temperature, humidity, battery and device time, all without connecting.
- Read live temperature and humidity from the Environmental Sensing Service, alongside the Device Information and battery characteristics.
- Get and set the device clock over the Current Time Service.
- Offload buffered readings into a CSV, resuming from where the last offload stopped.
- Update firmware over MCUmgr/SMP, with a progress bar, alongside the rest of MCUboot's image handling: listing the slots, confirming an image and resetting the device.
- Shell into the device over the Nordic UART Service, with keystroke-level forwarding and automatic reconnection across a device reset.
Requires Python 3.11 or newer.
# Clone the repository
$ git clone <bmes-tools-repository-url>
$ cd bmes-tools
# Create a virtual environment. Use `python3` if `python` is not found.
$ python -m venv .venv
# Activate it in the current shell
$ source .venv/bin/activate
# Install. Use `pip install -e .` for an editable install while developing.
(.venv) $ pip install .
# Check it starts
(.venv) $ bmes --helpOn macOS the first run prompts for Bluetooth permission for your terminal. Grant it under System Settings → Privacy & Security → Bluetooth, or nothing will ever be discovered.
Watch what is advertising. The table updates as advertisements arrive, and the readings come straight out of the BTHome payload, so nothing needs to connect.
$ bmes scan # every BMES100 in range, until Ctrl-C
$ bmes scan --all --details # every BLE device, with raw advertising dataConnect and watch the live readings, after printing what the device says about itself.
$ bmes info # live, until Ctrl-C
$ bmes info --once # a single snapshot$ bmes time get # read the clock and compare it with the host
$ bmes time set # synchronise it with this machineThe device keeps Unix time in UTC and loses it across a reset that stops its counter, so a freshly
programmed device reads 1970 until something sets it. time get says so when it sees that.
Pull the buffered readings into a CSV.
$ bmes offload readings.csv # append everything new
$ bmes offload --all fresh.csv # take the whole buffer again
$ bmes offload --discard readings.csv # offload, then free the space on the deviceReadings live in a RAM ring buffer. Reading does not consume them, so an interrupted offload can
simply be run again; only --discard frees anything. Pointed at an existing CSV, the offload
resumes from the last sequence number in the file.
Every row carries the device serial from the Device Information Service, so readings can be traced back to the device that took them. Since sequence numbers are per device, offloading into a CSV another device wrote is called out rather than silently resumed.
Sequence numbers restart from zero when the device resets, since the buffer does not survive one. That is detected and reported, but across a long enough gap the timestamp column is what to trust.
Everything that talks to the device's MCUmgr/SMP server: firmware update and MCUboot's images.
$ bmes dfu update build/dfu_application.zip # upload, mark, reset
$ bmes dfu list # what the device is holding
$ bmes dfu confirm # keep the image it is running
$ bmes dfu reset # reboot itSend a firmware image over MCUmgr's Simple Management Protocol.
$ bmes dfu update build/dfu_application.zip
$ bmes dfu update --no-confirm build/dfu_application.zipAccepts a signed binary, an Intel HEX file, or the dfu_application.zip.
The uploaded image is marked permanent by default. --no-confirm marks it for test
instead: MCUboot swaps it in, and reverts to the previous one on the next reset unless the new
firmware confirms itself, or bmes dfu confirm is run once it is up.
Open the device's shell over the BLE Nordic UART Service.
$ bmes shell # interactiveOn a terminal, keystrokes are forwarded one at a time, so the device's own tab completion, history
and line editing all work. That means Ctrl-] ends the session, leaving Ctrl-C free to
interrupt whatever the device is running.
The session survives the device going away: resetting the board is the usual way to exercise
firmware from here, so a dropped link is waited out and picked back up. --no-reconnect turns that
off.
Every command that talks to a device takes the same three options:
| Option | Meaning |
|---|---|
-n, --name |
Advertised name prefix, BMES100 by default. |
-a, --address |
Address prefix, for pinning one device when several are in range. |
-t, --timeout |
How long to scan before giving up. |
Both name and address match on a prefix, so the short address bmes scan prints can be pasted
straight back into --address. With several devices matching and nothing to choose between them,
the tool asks.
The CLI is a front-end over an importable library. Everything a command does can be done directly:
import asyncio
from bmes import connect, ess, find_device
async def main():
discovered = await find_device() # scans for BMES100*
async with connect(discovered) as client:
reading = await ess.read(client)
print(f"{reading.temperature_c} C, {reading.humidity_pct} %")
asyncio.run(main())| Module | What it covers |
|---|---|
bmes.device |
Discovery, connection, Device Information, Battery Service. |
bmes.bthome |
BTHome v2 advertisement decoding. |
bmes.ess |
Environmental Sensing Service reads and notifications. |
bmes.clock |
Current Time Service. |
bmes.history |
Sensor History Service and its CSV format. |
bmes.dfu |
MCUboot image loading and the SMP update sequence. |
bmes.shell |
Nordic UART Service shell sessions. |
bmes.uuids |
Every GATT UUID the device exposes. |
What the tool talks to:
| Service | UUID |
|---|---|
| Environmental Sensing | 0x181A |
| Current Time | 0x1805 |
| Device Information | 0x180A |
| Battery | 0x180F |
| Sensor History | 8e7c1000-4a3b-4d5e-9f21-6b0d2c8a5e40 |
| Nordic UART shell | 6e400001-b5a3-f393-e0a9-e50e24dcca9e |
| SMP (MCUmgr) | 8d53dc1d-1db7-4cd3-868b-8a527460aa84 |
Not every device exposes all of them. When one is missing, the command that needs it says so.
Run with -v for warnings, up to -vvvv to log everything including the Bluetooth stack and the
SMP client, and -o log.txt to write it to a file as well:
$ bmes -vvvv -o log.txt offload readings.csv| Symptom | Likely cause |
|---|---|
| Nothing found | Bluetooth permission not granted, or the device is not advertising. |
| Found, but connect fails | Another host is already connected; the firmware accepts one. |
info shows - for readings |
The device has not taken its first reading yet. |
Offload gets data notifications not enabled |
Something else is mid-transfer on that device. |
dfu cannot open an SMP session |
The device does not expose the SMP service. |
| Shell connects but stays silent | The device does not have a shell on the Nordic UART Service. |
(.venv) $ pip install -e ".[dev]"
(.venv) $ ruff check src/
(.venv) $ black src/