🐦 What is the Easy BLE library?
The Easy BLE library is an advanced BLE management tool for ZeppOS 3.0 watches that features an automated profile generator, a hybrid asynchronous and sequential queue for efficient handling of all operations including writing and reading, user-friendly string-based interactions, seamless auto-conversions of data and addresses, support for multiple data types, and simplified device management through MAC address-centric commands, all designed to enhance usability and streamline BLE communications.
Table of Contents
✨️ Easy BLE Interaction Flow
SCAN (OPTIONAL)
CONNECT
BUILD PROFILE
START LISTENER
MANIPULATE
QUIT (STOP)
💡#0: Install and Initialize the Library
You can install the Easy BLE from the NPM registry or download it directly from GitHub.
npm i @silver-zepp/easy-ble
git clone
https://github.com/silver-zepp/zeppos-easy-ble.git
// install -> npm i @silver-zepp/easy-ble
import BLEMaster from "@silver-zepp/easy-ble"
const ble = new BLEMaster();
💡#1: Scan for devices
Initially, you want to scan for all devices around and potentially find one or a couple of those that you are looking for.
const scan_success = ble.startScan((scan_result) => {
console.log(JSON.stringify(scan_result));
}
This will do two things: Print a found device on each scan tick, giving you an object that looks like this:
{
"1a:2b:3c:4d:5e:6f": {
"dev_name":"ESP32_BLE_PERIPHERAL",
"rssi":-58,
"service_data_array":[],
"vendor_data":""
}
}
If the found device includes vendor or service data it will be included and stringified.
The next thing this scan does is populate the dictionary of devices that you can access at any given time with ble.get.devices();
// returns a singular device. MAC = "1A:2B:..."
const device = ble.get.devices()[MAC];
// returns dictionary with all found devices
const devices = ble.get.devices();
{
"a1:a2:a3:a4:a5:a6": { // device #1
"dev_name": "my device",
"rssi": -92
},
"c1:c2:c3:c4:c5:c6": { // device #2
"dev_name": "another device",
"rssi": -69,
"service_uuid_array": [
"181D"
],
"service_data_array": [
{
"uuid": "181D",
"service_data": "ff5ad4" // unique info
}
]
}
}
As you can tell the second device contains service data that can be used to identify a specific type of device and make your App connect to it straight away if a particular string “ff5ad4” is found. This data can be further decoded.
You can also rely on the UUID but many devices might use the same ones, so it’s not the most reliable fingerprint.
The other method you can use here is ble.get.hasMAC(…). Using it, you can keep your scan running and connect to the device only when it comes in range to handle further interaction like ble.connect(…) but before that, make sure to stop the scan with ble.stopScan().
// the device you are searching for
const MAC = "1A:2B:3C:4D:5E:6F";
// check if dictionary contains a specific device
if (ble.get.hasMAC(MAC)){
// stop the scan
ble.stopScan();
// proceed with the connection
ble.connect(MAC, (connect_result) => {
// ...
}
}
💡#2: Connect to a Device
Now that you have found your device, it’s time to connect to it with ble.connect(…). It takes a mac address as a parameter and receives a connect_result callback, which is an object that looks like this:
{ "connected": false, "status": "disconnected" }
Based on the response you can either proceed to further interaction or retry the connection. Additionally, the connect_result object that is received, contains a [string] status that can better describe the connection failure with strings like “in progress“, “invalid mac“, etc. Leveraging statuses might come in handy when dealing with unstable connections or introducing a reconnection mechanism.
ble.connect(MAC, (connect_result) => {
// successfully connected
if (connect_result.connected) {
// 1. generate profile
// 2. start listener
// 3. communicate
} else {
// handle connection failure
console.log('Failed to connect. Status:',
connect_result.status);
}
});
💡#3: Build a Profile
Before communicating with the device you have to build a profile by preparing a simplified services object.
// simplified object that describes a profile
const services = {
// service #1
"FF00": { // service UUID
"FF02": [], // READ characteristic UUID
"FF03": ["2902"], // NOTIFY chara UUID
// ^--- here's a descriptor UUID
},
// ... add other services here if needed
}
And providing it to the generateProfileObject(…) method to do the rest of the job.
// generate a complex profile object just providing services
const profile_object = ble.generateProfileObject(services);
And that’s pretty much it. For the most part, you are ready to communicate with a device.
But we’ll have to look into one additional feature of the generate profile object method – it’s a third (optional) argument (object) that allows you to provide custom permissions per specific UUIDs. You only have to provide these for characteristics and descriptors that require them, and the rest will still be autogenerated.
Most of the time you won’t need to use it but when it comes to complex systems with encrypted communications they might have restricted rules for permissions and you will have to use permissions like READ_ENCRYPTED_MITM.
For this case, the BLE Master library contains a PERMISSIONS table that you can import and use like this:
const permissions = {
"char_uuid_1": PERMISSIONS.READ_ENCRYPTED,
"desc_uuid_1": PERMISSIONS.WRITE_DESCRIPTOR
};
const profile_object = generateProfileObject(services, permissions);
💡#4: Start the Listener
Now that you have crafted your profile_object, it’s time to feed it to the startListener(…) method
// start listening for the response
// from the watch's backend
ble.startListener(profile_object, (response) => {
if (response.success){
// #5: manipulate with
// characteristics and descriptors
}
});
💡#5: Manipulate
// >> inside the ble.startListener(...)
// manipulate with characteristics and descriptors
if (response.success){
// first subcribe to the events
ble.on.charaValueArrived((uuid, data, len) => {
console.log("Read result:", uuid, data, len);
});
// then manipulate - read/write/etc
ble.read.characteristic("FF02");
// As a result of this execution you should log
// Read result: 'FF02' 'BAT_LVL_77' '10'
}
💡#6: Quit
Whenever you’re done communicating with your device, you have to execute the ble.quit() command. It’s good practice to keep it in your onDestroy() lifecycle. This way everything gets stopped and destroyed when the app is closed.
The stop command does 4 different things at a time:
- it stops all the callbacks you are subscribed to
- it destroys a profile on the backend of your watch (which is very important)
- disconnects from the BLE peripheral.
- and deregisters user-created callbacks to save some memory
onDestroy(){
ble.quit();
}
✨️ Full Communications Example
Here’s a full basic working communications example. Compared to the raw BLE communication approach that uses hmBle.mst…() methods, it’s super simple and straightforward.
// install -> npm i @silver-zepp/easy-ble
import BLEMaster from "@silver-zepp/easy-ble"
const ble = new BLEMaster();
// the mac of a device you are connecting to
const MAC = "1A:2B:3C:4D:5E:6F";
// simplified object that describes a profile
const services = {
// service #1
"FF00": { // service UUID
"FF02": [], // READ chara UUID
"FF03": ["2902"], // NOTIFY chara UUID
// ^--- descriptor UUID
},
// ... add other services here if needed
}
// connect to a device
ble.connect(MAC, (connect_result)=>{
// proceed further if [bool] connected is true
if (connect_result.connected){
// generate a complex profile object
// providing description of its services
const profile_object
= ble.generateProfileObject(services);
// start listening for the response
// from watch's backend
ble.startListener(profile_object, (response)=> {
if (response.success){
// first subcribe to the events
ble.on.charaValueArrived((uuid, data, len)=> {
console.log("Read result:", uuid, data, len);
}
);
// then manipulate - read/write/etc
ble.read.characteristic("FF02");
// As a result you should log
// Read result: 'FF02' 'BAT_LVL_77' '10'
}
});
}
});
📝 Easy BLE (Master) API Reference
response_callback
: Callback function to handle scan results. This function is called with a modified scan result object for each BLE device found.options
: Optional parameters for scanning, provided as an object. The available properties within this object are:duration
: (Number) Specifies the duration of the scan in milliseconds. If set, the scan will automatically stop after this period.on_duration
: (Function) Callback function that is invoked when the scan stops after the specified duration. Useful for post-scan processing.throttle_interval
: (Number) Interval in milliseconds to throttle the processing of scan results.allow_duplicates
: (Boolean) Whether to include duplicate devices in each callback. Defaults to false.
dev_addr
: The MAC address of the device to connect to.response_callback
: Callback function receiving the result of the connection attempt. The callback is called with an object containing:connected
: A boolean indicating if the connection was successful.status
: A string indicating the connection status. Possible values are `connected`, `invalid mac`, `in progress`, `failed`, or `disconnected`.
Disconnects from a BLE device.
Returns: Success status of the disconnection.
Pairs with a BLE device.
Returns: Success status of the pairing.
profile_object
: The profile object describing how to interact with the BLE device. Should be generated using the `generateProfileObject` method.response_callback
: Callback function called with the result of the profile preparation. The callback receives an object containing `success`, `message`, and optionally `code` properties.
Generates a generic profile object for interacting with a BLE device.
services
: A list of services with their characteristics and descriptors. Each service is identified by its UUID and contains a map of its characteristics. Each characteristic, identified by its UUID, includes an array of its descriptor UUIDs.permissions
(Optional): An object specifying custom permissions for characteristics and descriptors. Defaults to a permission value of 32 (all permissions) for each entry if not provided.
Returns: A generic profile object for the device, or `null` if the device was not found. The profile object includes device connection information, services, characteristics, and their permissions.
Quit BLE communication with a connected device and clean up.
debug_level
: The debug level to set. Possible values:- `0`: No logs.
- `1`: Critical errors only (default).
- `2`: Errors and warnings.
- `3`: All logs, including debug information.
Writes data to a characteristic.
uuid
: UUID of the characteristic.data
: Data to be written.write_without_response
: If true, write without waiting for a response.
Writes data to a characteristic.
uuid
: UUID of the characteristic.data
: Data to be written.write_without_response
: If true, write without waiting for a response.
Enables or disables notifications for a characteristic.
chara
: UUID of the characteristic.enable
: Boolean to enable or disable notifications.
Reads data from a characteristic.
uuid
: UUID of the characteristic.
Reads data from a descriptor.
chara
: UUID of the characteristic.desc
: UUID of the descriptor.
callback
: Function to call when read operation completes.
callback
: Function to call with the characteristic value.
callback
: Function to call when write operation completes.
callback
: Function to call when read operation completes.
callback
: Function to call with the descriptor value.
callback
: Function to call when write operation completes.
callback
: Function to call with notification data.
callback
: Function to call when a service change starts.
callback
: Function to call when a service change ends.
- No parameters.
.off.charaReadComplete();
- No parameters.
.off.charaValueArrived();
- No parameters.
.off.charaWriteComplete();
- No parameters.
.off.descReadComplete();
- No parameters.
.off.descValueArrived();
- No parameters.
.off.descWriteComplete();
- No parameters.
.off.charaNotification();
- No parameters.
.off.serviceChangeBegin();
- No parameters.
.off.serviceChangeEnd();
Deregisters all callbacks associated with the current BLE connection. This method is crucial for ensuring that no event callbacks remain active, which could lead to memory leaks or unexpected behavior. This method is executed under the hood by the .stop()
method so usually you don’t have to use it.
- No parameters.
Example usage:.off.deregisterAll();
{
"MAC_ADDRESS": {
"dev_name": "DEVICE_NAME",
"rssi": RSSI_VALUE,
"service_data_array": [ARRAY_OF_SERVICE_DATA],
"vendor_data": "VENDOR_DATA"
"service_uuid_array": [ARRAY_OF_SERVICE_UUIDS],
"vendor_id": VENDOR_ID
},
// ... more devices
}
Check if a device is connected.
Returns: Boolean indicating connection status.
dev_addr
: The MAC address of the device.
dev_name
: The name of the device.
service_uuid
: The UUID of the service to check for.
service_data
: The service data to check for. This can be a string or a pattern that you are looking for in the service data of the devices.
uuid
: The service data UUID to check for. This should be a string representing the UUID of the service data.
vendor_data
: The vendor data to check for. This should be a string representing the specific data or identifier associated with the vendor.
vendor_id
: The vendor ID to check for. This should be a numerical value representing the vendor’s unique identifier.
Gets the profile pointer ID of a device.
Returns: Profile pointer ID.
Gets the connection ID of a device.
Returns: Connection ID.
Converts an ArrayBuffer to a string of hexadecimal numbers. Useful for representing binary data in a readable format, such as BLE device addresses or data.
- buffer {ArrayBuffer} – The ArrayBuffer to be converted.
Returns: The hexadecimal string representation of the ArrayBuffer. Each byte is represented as a two-character hex code.
Converts an ArrayBuffer into a string. Useful for converting binary data (ArrayBuffer) into a regular JavaScript string, especially for data received from BLE devices.
- buffer {ArrayBuffer} – The ArrayBuffer to be converted.
Returns: The resulting string. Note that the output is dependent on the encoding of the byte data in the ArrayBuffer.
Converts an ArrayBuffer to a number. This function is useful when you need to represent binary data in a readable number format. For example, it can be used to display BLE device battery levels or other data in a human-readable form.
buffer
: The ArrayBuffer to be converted.
A collection of constants representing different permissions for BLE characteristics and descriptors. Each permission type has a description and a corresponding value. Used alongside .generateProfileObject(…) method as a third optional parameter.
- READ: Allows reading the characteristic value.
- READ_ENCRYPTED: Allows reading the characteristic value with an encrypted link.
- READ_ENCRYPTED_MITM: Allows reading the characteristic value with an encrypted and authenticated link (MITM protection).
- WRITE: Allows writing the characteristic value.
- WRITE_ENCRYPTED: Allows writing the characteristic value with an encrypted link.
- WRITE_ENCRYPTED_MITM: Allows writing the characteristic value with an encrypted and authenticated link (MITM protection).
- WRITE_SIGNED: Allows writing the characteristic value with a signed write (without response).
- WRITE_SIGNED_MITM: Allows writing the characteristic value with a signed write (without response) and authenticated link (MITM protection).
- READ_DESCRIPTOR: Allows reading the descriptor value.
- WRITE_DESCRIPTOR: Allows writing the descriptor value.
- READ_WRITE_DESCRIPTOR: Allows both reading and writing the descriptor value.
- NONE: No permissions granted.
- ALL: All permissions granted.
📖 Known Bugs
- Sometimes the backend returns this random mac: 70:53:36:8e:0b:c0 in the (result) object when trying to hmBle.mstConnect
Looks like this MAC is returned when connected status is either 1 or 2 but not 0
SOLUTION: assigning a user provided MAC instead of one returned from the connect object - pair() method doesn’t work (?) and might crash the device
- Backend BUG (?) profile creation doesn’t always trigger the mstOnPrepare callback (?)
- Tertiary (3rd level) REQUIRES “len2: chara_len” despite docs stating otherwise
- Might not support non-standard UUID like 748ad00d-c286-49a7-992e-ccfcdbc12d35 or 1337
- After many connections, the watch suddenly doesn’t allow another one and just keeps failing
– Connection failed. Max attempts reached.
– Connect result: {“connected”:false,”status”:”disconnected”}Resetting/Rebooting everything doesn’t help. The connection gets latched on the backend. Does it write con_state into some file instead of keeping it in RAM (?)– EDIT: The watch doesn’t stop the scan mode at times. Spamming SCAN_RSP & SCAN_REQ. Disabling BLE or rebooting the watch doesn’t help, scan gets latched.
WORKAROUND:
– use scan instead of connect / before connect and it should work
– or connect to a different BLE device, then you should be able to connect to a previous one.
📖 Potential ToDo's
- Support of different CMD Write methods (encrypted comms are currently impossible)
- Read/Write Authorization
- required for complex systems and encrypted comms
- Bonding and Pairing Process
- using pairing PIN and other methods
- Handle special cases other than 2902
- 2900: Characteristic Extended Properties
- 2904: Characteristic Presentation Format Descriptor
- 2905: Characteristic Aggregate Format Descriptor
- 2A05: Service Changed Characteristic (should be handled)
- Reliable Write Operations
- Long Chara Values
- due to a relatively small MTU, it shouldn’t be possible to handle charas bigger than an MTU packet
- Queue
- add the ability to disable/bypass the queue with a static flag
- add a limiter to avoid queues that are too large
- Devices (object)
- make sure the object doesn’t grow large
- – add an ability to reduce it to only active connection
- – or timeout devices. but that requires a bit more stored data (date)
- – add a limiter to avoid RAM issues
- – On each read/write check if attribute is inside the profile
- handled by the backend BX_CORE_MISS_ATT. not much benefit adding it here (?)
📖 Change Log
1.6.8
– @fix Read -> descriptor jsdoc
– @add function ab2num
– @add stop the scan in case it was forgotten or the app crashed during it
– @add support for mac patterns “11:XX:33:44:55:66”. needed for semi randomized macs
– @fix abnormal mac checker TARGET_MAC -> dev_addr
– @add new methods to Get class -> hasService(), hasServiceData(), hasVendorData(), hasServiceDataUUID(), hasVendorID()
– @fix scan throttler was self resetting itself
– @add duplicates handling. (community contribution) 1.6.1
– @upd Get -> hasDevice() moved to hasMac()
– @add Get -> hasDeviceName(dev_name)
– @upd stop() moved to quit() to avoid confusion with stopScan()
– @fix visibility of On callbacks
– @fix profile_idp in the docs
– @fix get; property + npm = “SyntaxError: invalid property name” now cached. Link to the rescue!
– @fix handle unique devices immediately, otherwise duplicates pop
1.5.3
– @upd abnormal mac address detector log level increase 2 -> 1 (1.3.8)
– @upd ab2hexStr() method capitalizes hexadecimals
– @upd better err responses for hmBle.mst write/reads
– @add static getters inside QueueManager to manage timeouts and intervals consistently
– @upd err code expanded missing att -> missing attribute
– @upd (!) removed mac requirement from the most methods as concurrent connections are currently impossible
– @rem bugfix for mstDestroyProfileInstance, as the issue was on the peripheral device
– @rem (!) callback receiver for notification enabler (CCCD 2902) – static cb_notification_received + whole workaround logic as all desc writes are fixed now and using descWriteComplete.
– @add separate desc and chara read/write operations + extended error codes for logs
– @add (!) possibility to subscribe to either descReadComplete or descValueArrived for queue to work. same for charas. (1.4.7)
– @add getCurrentlyConnectedDevice() alongside the getDevices() getter to reduce memory consumption of the Write & Read sub-classes
– @upd Get subclass’ creation was moved into the constructor to avoid unnecessary new memory allocations
– @add more err codes
– @add explicitly dereference the queue operation, allowing garbage collection
– @add (!) Off subclass to handle On callbacks deregistration
– @upd err table with indicators for when the callbacks were deregistered while user tries to invoke them
1.3.7
– @add generateProfileObject method, providing a systematic way to create generic profile objects for devices
– @upd ENABLE_DEBUG_LOG changed to DEBUG_LOG_LEVEL; a level-based logging system for more control over logging; levels 1 -> 3
– @add error message constants with an ERR_PREFIX for standardized error messaging
– @add #connection_in_progress property for better management of connection states
– @add introduction of On class with methods for subscribing to supported BLE event callbacks
– @add isValidMacAddress function for validating MAC addresses
– @add more helper functions for versatile data handling
– @upd in Write class, introduction of a write queue and associated processing methods for managing write operations sequentially
– @upd improved error handling and logging throughout the library
– @add static SetDebugLevel(…) method in BLEMaster class for dynamic control of logging levels
– @upd characteristic method in Write class now supports fast writes and comms using write_without_response flag
– @add enableCharaNotifications method in Write class to enable or disable notifications for a characteristic
– @upd overhaul of the startListener method + startScan throttler
– @upd enhancements in error messaging, incorporating new error constants and improving the clarity of error logs
– @add additional static methods in On class to manage flags and statuses for write operations
– @upd revisions in the Read class methods (characteristic, descriptor) for improved error handling
– @upd modifications in the Get class for improved consistency and reliability
– @fix disconnect() method was missing a return type when the condition was false
– @upd stop() method now can stop all devices or a specific one
– @add clean jsdocs with samples
– @upd introduction of read queue inside the Read class, including the onValueArrived event
– @add possibility to connect to a device without a prior scan (1.2.2)
– @add isConnected internal check to avoid multiple connect ions to the same mac (1.2.3)
– @add simplified response_callback of the connect() method. returns only connected: true/false (1.2.4)
– @upd profile_idp was renamed to profile_pid (profile pointer ID)
– @add handle undefined dev_addr case
– @fix hexbuf. 1 hex = half a bytes not full byte (1.2.7)
– @fix CCCD 2902 write is now properly handled by the queue. waitForWriteCompletion additionally takes a uuid to decide if it’s a CCCD (1.2.8)
– @add QueManager: a unified queue – now read, write and CCCD requests can be chained together
– @upd Read/Write queues refactored to use the queue manager
– @add check for abnormal mac to see if this bug is reproduceable
– @add CCCD 2902 is now handled outside the enableCharaNotifications in case user writes directly into CCCD desc
– @upd err codes pushed into a dict for readability
– @add two new methods in the Get subclass – profilePID & connectionID. handy when the user wants to directly talk to hmBle.mst… (1.3.4)
– @add BX_CORE_CODES dictionary with human readable error messages
– @upd err codes expanded on timeout occurances to tell the user which events exactly they have to subscribe to (charaNotification | charaWriteComplete | charaValueArrived)
– @add additional “permissions” param for generateProfileObject(_, _, permissions) method, to allow the user specify custom persmission per each UUID
1.0.0
– initial release
❗ Raw BLE Communications
- SCAN (OPTIONAL)
- CONNECT
- START LISTENER
- BUILD PROFILE
- MANIPULATE
- STOP & DESTROY
If you would like to communicate with the BLE peripheral directly using the hmBle.mst…() here are the gifs that will help to understand how the connection works under the hood.