Catalog · How the catalog works
LazBle
Cross-platform Bluetooth Low Energy library for Free Pascal and Lazarus.
- ★ 2 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
LazBle
LazBle is an asynchronous BLE Central and GATT client library for Free Pascal and Lazarus. It provides device discovery, connections, GATT and descriptor operations, notifications and indications, typed profiles, optional reconnection, and LCL controls.
LazBle uses SimpleDelphiBLE to communicate with Bluetooth devices. API access and callbacks run on the main thread.
Packages and components
| Package | Purpose |
|---|---|
lazble.lpk |
Core library, SimpleDelphiBLE backend, NUS, Battery, and Device Information profiles; no LCL dependency |
lazblelcl.lpk |
LCL components, device picker, and translations |
lazblelcldesign.lpk |
Design-time registration on the Lazarus component palette |
The main types are:
TLazBle: device discovery, availability checks, client ownership, and shutdown.TBleClient: one device, its connection, profiles, and reconnect policy.TBleGattSession: shared GATT access for a client's operations and profiles.TBleGattProfile: base class for typed profiles.TBleByteChannel: reusable Write/Notify byte channel.TNusProfile: Nordic UART Service, with TX notifications and RX writes without response when supported, otherwise with response.TBleBatteryProfile: typed Battery Service 1.1 data, reads, and updates.TBleDeviceInformationProfile: device identity, revisions, System ID, PnP ID, and UDI.TLazBleSync: blocking facade for console applications.TLazBleComponent: LCL facade owner, availability checks, and scan controller.TLazBleLclClient: persistent LCL wrapper for a selected device.TBleDeviceSelectForm: modal device picker using an existing scan controller.
Dependencies and native libraries
Use Free Pascal 3.2.2 or later and, for LCL components, Lazarus. LazBle requires SimpleDelphiBLE 1.4.0, supplied separately from LazBle. Use its sources and native runtime from the same distribution.
Extract it to a convenient directory. The commands below use this layout:
workspace/
lazble/
lazble.lpk
simpledelphible/ # official distribution
SimpleDelphiBLEPackage.lpk
LICENSE.md
source/
lib/
linux-x64/
win64/
macos-x64/
macos-arm64/
...Run commands from lazble/ unless another directory is specified.
| Supported target | Distribution directory | Runtime file |
|---|---|---|
| Linux x86_64 | lib/linux-x64 |
libsimplecble.so |
| Windows x86_64 | lib/win64 |
simplecble.dll |
| macOS 15.0+ Intel x86_64 | lib/macos-x64 |
libsimplecble.dylib |
| macOS 15.0+ Apple Silicon (aarch64) | lib/macos-arm64 |
libsimplecble.dylib |
Deploy the matching runtime file beside the executable. For a macOS app bundle,
use MyApp.app/Contents/MacOS. With no explicit path, SimpleDelphiBLE first
looks in the executable's directory, then uses the system library search.
On Linux, the runtime can instead be installed in a directory visible to the
system dynamic loader, or supplied through LD_LIBRARY_PATH.
To select a runtime file or existing directory, set TLazBle.LibraryPath
from the LazBleFacade unit on the main thread, before starting BLE operations:
TLazBle.LibraryPath := '/path/to/native';The setting applies to the whole process. An empty string enables the executable/system search. Reading or setting the property does not load the runtime; changing it after loading does not switch the loaded library.
The runtime loads on the first native call. Building, starting the application,
creating the facade or LCL components, and reading package information or
diagnostics do not require it. A missing runtime fails the BLE operation with
LazBleErrorBackendUnavailable; an availability check also stores the reason in
DiagnosticInfo.AvailabilityErrorMessage. Checking availability includes adapter
selection and power status, not just library loading.
The application needs Bluetooth permission.
Lazarus
LazBle depends on the official SimpleDelphiBLEPackage runtime package.
Register that package before the LazBle packages:
lazbuild --ws=qt6 --add-package-link ../simpledelphible/SimpleDelphiBLEPackage.lpk
lazbuild --ws=qt6 --add-package-link lazble.lpk
lazbuild --ws=qt6 --add-package-link lazblelcl.lpk
lazbuild --ws=qt6 lazble.lpk
lazbuild --ws=qt6 lazblelcl.lpkAdd LazBle to your project's required packages, and LazBleLCL when using
components or controls. Deploy the matching runtime file to the executable's
output directory; lazbuild does not copy it.
To install the components on the Lazarus palette, open lazblelcldesign.lpk
and choose Use > Install. Lazarus rebuilds its IDE executable with the design
package. Opening the IDE and using the component palette do not require the
native runtime; it is needed when the IDE process itself performs BLE work.
Direct FPC use
Add src/core, src/backends, and src/profiles to -Fu. Add the official
../simpledelphible/source directory to both -Fu and -Fi. Native linker paths
and rpath options are not required. Keep compiled units for different targets
in separate -FU directories. Unix console programs must include cthreads
first in their uses clause.
Console usage
TBleByteChannel.SendAsync splits byte buffers into sequential writes
that fit the connection's payload limit. Each send returns one operation, and
consecutive sends do not interleave. Failure or cancellation stops the remaining
bytes; already transmitted bytes cannot be recalled. Message framing belongs to
the application protocol.
This program scans, connects to a NUS device, waits for profile readiness, sends one byte, and completes shutdown before releasing the facade. Pass the device ID as its command-line argument.
program NusExample;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}cthreads,{$ENDIF}
Classes, SysUtils,
LazBleTypes, LazBleOperation, LazBleClient, LazBleFacade, LazBleNus;
procedure Await(const Operation: IBleOperation);
begin
while Operation.State = lbopPending do
CheckSynchronize(10);
if Operation.State <> lbopSucceeded then
raise Exception.CreateFmt('BLE operation failed (%d): %s',
[Operation.ErrorCode, Operation.ErrorMessage]);
end;
var
Ble: TLazBle;
Client: TBleClient;
Nus: TNusProfile;
Operation: IBleOperation;
Value: TBytes;
begin
if ParamCount <> 1 then
raise Exception.Create('Usage: NusExample DEVICE_ID');
Ble := TLazBle.Create;
try
Await(Ble.ScanAsync('', 5000));
Client := Ble.CreateClient(ParamStr(1));
Nus := TNusProfile.Create;
Client.AddProfile(Nus);
Await(Client.ConnectAsync);
SetLength(Value, 1);
Value[0] := 1;
Await(Nus.SendAsync(Value));
finally
Operation := Ble.ShutdownAsync;
while Operation.State = lbopPending do
CheckSynchronize(10);
if Operation.State = lbopFailed then
WriteLn(StdErr, 'BLE shutdown failed: ', Operation.ErrorMessage);
Ble.Free;
end;
end.Use asynchronous operations in GUI applications and callbacks. TLazBleSync
provides console methods for Scan, Connect, Disconnect, and Shutdown. It pumps
CheckSynchronize while waiting. A wait deadline returns False with an error
message; the BLE operation can still be running and keeps its eventual result.
Operations, discovery, and ownership
Asynchronous requests return reference-counted operation interfaces. A request
that cannot start returns an operation in lbopFailed. Keep the interface while
observing its state, result, or OnCompleted handler. Assigning OnCompleted
after completion invokes it immediately. Do not call Free on operation or
subscription interfaces.
Repeated ScanAsync or CheckAvailabilityAsync calls return the current
operation while it is pending. A scan and an availability check cannot run
at the same time; the competing request fails. GATT requests can still be
queued during a scan. Completion handlers see updated diagnostics and can
start the next scan or availability check.
CheckAvailabilityAsync(AdapterId) explicitly checks Bluetooth availability.
On success, IBleAvailabilityOperation.Availability is lbaAvailable or
lbaUnavailable; a failed check carries an error code and message. Creating a
facade or loading an LCL component does not enumerate adapters or start a scan.
ScanAsync(AdapterId, DurationMs) returns IBleScanOperation. OnResult reports
new and updated discoveries; Results contains deduplicated device snapshots
in discovery order. Installing a result handler does not replay earlier results.
Reaching the scan duration normally completes the scan successfully. Duration 0
keeps scanning until cancellation or shutdown. Use the scan operation's Cancel
method to stop it and await lbopCancelled before starting another scan.
ConnectAsync uses a device known from an earlier scan, an existing
connection, or the paired-device list. An unknown DeviceId fails with
LazBleErrorDeviceNotKnown; Connect does not start a scan. Creating a client
only creates its local state. Scan explicitly before connecting an unknown
device, including one whose ID was restored from settings.
Use CheckAvailabilityAsync for adapter and power checks. GATT requests do not
perform a separate availability check.
TLazBle owns clients created by CreateClient. Device IDs must be unique
within a facade. After successful AddProfile, the client owns that profile.
Required profiles must be Ready before the client enters lbcstReady.
Client.ConnectAsync completes successfully only after connection, service
discovery, and readiness of those profiles.
Use RemoveClient to release a client in Disconnected or Error state.
Accepted GATT requests retain their results even after client removal.
Completed operations, scan snapshots, and inactive subscription tokens remain
readable after their client/session is released.
GATT operations
TBleClient and TBleGattSession expose the same UUID-based requests:
| Request | Result |
|---|---|
ReadAsync(Service, Characteristic) |
IBleGattOperation, with bytes in Value |
WriteAsync(Service, Characteristic, Value, WriteMode) |
IBleGattOperation; lbwmRequest or lbwmCommand |
ReadDescriptorAsync(Service, Characteristic, Descriptor) |
IBleGattOperation, with bytes in Value |
WriteDescriptorAsync(Service, Characteristic, Descriptor, Value) |
IBleGattOperation |
SubscribeAsync(Service, Characteristic) |
IBleSubscription using Notify |
SubscribeAsync(Service, Characteristic, lbsmIndicate) |
IBleSubscription using Indicate |
Descriptor requests require a connected session and a descriptor present in the
discovered service tree. Subscriptions require a connected session, a discovered
characteristic, and CanNotify or CanIndicate for the selected mode. Missing
targets fail with LazBleErrorGattNotFound; unsupported subscription modes fail
with LazBleErrorGattPropertyNotSupported. UUID lookup is case-insensitive.
Notify is the default TLazBleSubscriptionMode (lbsmNotify); Indicate uses
lbsmIndicate. Both modes use the same OnData, readiness, and Unsubscribe
contracts. NUS uses Notify; Battery selects Notify or Indicate for each characteristic.
Reconnection
Automatic reconnection after unexpected connection loss is optional and disabled by default:
Client.ReconnectOptions := TLazBleReconnectOptions.Create(1000, 30000, 5);
Client.AutoReconnect := True;The retry delay doubles up to the configured maximum, with a limit on attempts.
An initial connection failure is not retried. Manual Disconnect, Shutdown, or
setting AutoReconnect to False stops a pending retry. Profiles detach on loss
and attach again after service discovery.
TLazBleLclClient exposes this policy through AutoReconnect and the nested
ReconnectOptions properties. Loading these properties from an .lfm does not
create a core client or start a connection.
Threads and lifecycle
Create, use, and destroy LazBle objects on the main thread. This includes state reads, handler registration, operation submission, cancellation, and profile access. Callbacks also run on the main thread.
Console applications must service CheckSynchronize; LCL does this through
the GUI event loop. BLE operations run asynchronously.
Callbacks may start the next asynchronous operation. Do not run nested BLE waits
or free the owner inside its callback. Defer destruction with
TThread.ForceQueue or an owned LCL async call; TThread.Queue may execute
immediately when called from the main thread.
Complete ShutdownAsync before freeing an active facade or LCL owner. Shutdown
stops new application work, reconnect attempts, and subscription data delivery
and detaches profiles. It cancels requests that have not started, waits for
started operations, and cleans up subscriptions and connections.
The shutdown operation remains pending until accepted requests and cleanup
finish and all client sessions are closed. Clients and sessions remain readable
until their owner is freed. Repeated ShutdownAsync calls return the same
operation, including after failure. Its terminal result preserves the first
cleanup failure. A native call can delay shutdown until it returns; there is
no guaranteed cleanup deadline. Destruction before completed shutdown can block and abandons
pending logical results.
Use one active TLazBle instance at a time. Complete its shutdown and release
it before starting BLE work through another instance. Shutdown does not unload
the native library.
Cancellation and subscriptions
CancelRequested records a request, not an operation outcome. Requests that
have not started can be skipped. A started native Connect/GATT call returns its
actual success or failure and retains its effects. Cancelling profile attachment does not undo
the connection. If discovery is cancelled after physical connection, the client
enters Error and explicit Disconnect remains available.
Notifications and indications are delivered only after successful subscription readiness; early values are dropped. A characteristic can have one active subscription per facade. Notifications queued before a reconnect cannot always be distinguished from values received after it.
Unsubscribe immediately closes local delivery. With a pending Notify or Indicate, cleanup waits for its outcome and unsubscribes if registration succeeds. A failed Unsubscribe preserves the error, leaves the token inactive, and disconnects the affected session; configured reconnection may then run. Unsubscribe and explicit Disconnect cleanup continue after a cancellation request.
Accepted GATT commands keep their actual results across connection loss and orderly shutdown. Their results do not reactivate closed subscriptions or apply an old unsubscribe failure to a new session.
Data and errors
GATT data uses TLazBleGattService and TLazBleGattCharacteristic from
LazBleTypes. Use CanRead, CanWriteRequest, CanWriteCommand, CanNotify,
and CanIndicate; descriptors are UnicodeString UUIDs.
Services, nested arrays, read values, and subscription payloads are managed snapshots treated as read-only. Explicitly copy arrays you need to change. Write requests copy the supplied buffer when submitted.
Inspect operation State to determine success or failure. Native error code 0
and empty error text can represent a failure. Named negative error codes in
LazBleTypes identify LazBle errors.
TLazBle.Info provides static package Name, Version, and About text.
DiagnosticInfo reports the backend, native version, adapter, availability,
and latest availability-check error. Reading it starts no BLE work.
LCL integration
Assign LazBle and DeviceId on TLazBleLclClient, then configure profiles in
OnConfigureClient. The underlying core client is created on demand.
OnUnconfigureClient runs while that client and its profiles are still alive,
before replacement, root shutdown, or detachment. Clear profile callbacks and
release borrowed references there; do not start BLE work or free the owner in
that handler.
TLazBleComponent.RefreshAvailability publishes availability changes through
OnAvailabilityChanged. Scanning uses the component's AdapterId and
ScanTimeoutMs, with state/result/completion events delivered directly on the
main thread.
TLazBleComponent.ShutdownAsync returns the same operation on repeated calls.
Shutdown starts that cleanup without returning its operation. In a form's
OnCloseQuery, start shutdown, retain the operation, and defer closure while
it is pending. Its completion handler schedules Close using
Application.QueueAsyncCall. Remove owned queued calls in the destructor.
See the LCL demo for the complete pattern.
Device selection
TBleDeviceSelectForm uses an existing scan controller and returns a selected
device snapshot:
Dialog := TBleDeviceSelectForm.Create(nil, LazBleComponent.ScanController);
try
Dialog.AdapterId := LazBleComponent.AdapterId;
Dialog.ScanTimeoutMs := LazBleComponent.ScanTimeoutMs;
if Dialog.Execute(Device) then
LazBleClient.SelectDevice(Device);
finally
Dialog.Free;
end;The picker keeps discovery order and updates matching rows.
The LCL demo shows how to build device
selection, connection actions, and status display using TLazBleLclClient.
Translations
The languages directory contains catalogues for the device picker. Load them
after choosing the application language and before creating controls:
TranslateLazBleLclResourceStrings('languages', Language, FallbackLanguage);Deploy the .po files with the application. Building lazblelcl.lpk updates the
.pot templates through Lazarus package i18n support.
Battery profile
TBleBatteryProfile supports BAS 1.1
for one battery. Add it to TBleClient before connecting.
Use LevelPercent / OnLevelChanged for charge percentage and Data / OnUpdated
for status, energy, time, health, and identity data. Check HasValue, Has...,
and numeric State before using optional values.
Read-only devices are supported. RefreshAsync returns an IBleOperation for
explicit updates; there is no automatic polling.
See batterymonitor for usage.
Device Information profile
TBleDeviceInformationProfile reads DIS 1.2
on attachment. Use Data.PresentFields to check which optional fields were read;
an empty service is valid. RefreshAsync reads a new complete snapshot, and
OnUpdated fires once after each successful snapshot. The IEEE 11073-20601
certification list is returned as encoded bytes; its contents are not decoded.
One addressable service instance is supported. See deviceinformation for usage.
Custom profiles
Derive a profile from TBleGattProfile. Use its protected Session for GATT
operations; start work in DoAttach, report readiness with
MarkReady or failure with MarkError, and release callbacks/subscriptions in
DoDetach. Register profiles through TBleClient.AddProfile before connecting.
Examples
| Example | Purpose |
|---|---|
| blescan | Discover devices and display scan results |
| gattinspect | Inspect services and characteristics |
| nusterminal | Send and receive NUS bytes with interactive input |
| batterymonitor | Read Battery Service 1.1 data, monitor updates, and refresh with r |
| deviceinformation | Read device identity, revisions, System ID, PnP ID, and UDI |
| LCL demo | Select a device, connect, and close asynchronously |
Build examples with their .lpi projects, for example:
lazbuild --ws=qt6 examples/nusterminal.lpi
lazbuild --ws=qt6 examples/lcl/lazblelcldemo.lpiConsole and LCL example executables are written to examples/bin; deploy the
native runtime there or use the loader search described above. Interactive
console input continues servicing callbacks while waiting for a line, including
partial input and EOF.
For test build and run instructions, see tests/README.md.
License
LazBle uses the MIT License. SimpleDelphiBLE and its native libraries retain their own licenses; include the distribution's license with deployed runtime files.

