The Beacons App is a Gluon code sample. For a full list of Gluon code samples, refer to the Gluon website.
In this tutorial, we’ll explain how to create the Beacons application that can be deployed on desktop, Android and iOS devices. It makes use of the Bluetooth Low Energy API from Gluon Attach, to either scan for beacons or broadcast beacons.
Before you start, make sure to check the list of prerequisites for each platform.
Note: This tutorial will use the plugin for IntelliJ, but it works as well on NetBeans and Eclipse.
Code: The code for this project can be found in the samples repository at GitHub.
The sample is located under the directory beacons
. The reader can clone this repository or create the entire project from scratch, based on the following steps.
BLE and Beacons
Bluetooth Low Energy (BLE), a wireless personal area network technology, while providing the same range of communications as classic bluetooth, considerably reduces the power consumption and cost.
Based on this principle, beacons are small wireless devices designed to transmit a simple radio signal continuously, which can be received by nearby smartphones. They transmit only their ID and their signal power, and the smartphone can read those values and estimate the distance to that device.
Among other uses, one of the possible applications for it is for proximity sensing: physical proximity to certain devices equipped with this technology can be estimated based on their radio receiver’s rssi value. This allows for indoor positioning, indoor behavior tracking, in-store interaction between customers and retailers, indoor navigation, and so on.
iBeacons and other beacon protocols
iBeacon is a brand name created by Apple in 2014, a new location technology that lets any phone using iOS7 or newer constantly scan the environment for BLE devices such as beacons. It is a proprietary, closed standard. Other standards like AltBeacon are open. Google launched Eddystone, its open source beacon, in 2015.
Hardware
If you want to use this technology in production, you will need real beacons equipped with BLE, and implementing one of the mentioned protocols. Those are small devices that can be purchased online from different vendors like Estimote, Kontakt or Gimbal. You can find an extensive list of vendors here.
Gluon Attach Implementation Details
Under the hood, Attach creates the implementation for accessing native bluetooth services on Android and iOS, so developers don’t need to worry about this.
Beacon Protocol
Beacons transmit small amounts of data. Beacon IDs consists of a maximum of three values: A universally unique identifier (UUID), a major value and a minor value. Every beacon also transmits information about its signal power.
According to the Bluetooth core specification, a beacon advertises a data package called the Scan Response Data. The scan response is divided into what are called AD structures.
Usually, these will be the 31 bytes sent by the beacon on every transmission, corresponding to two AD structures:
Android
Android API provides a Bluetooth adapter, and a BLE scanner:
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
scanner.startScan(new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
ScanRecord scanRecord = result.getScanRecord();
// process scanRecord
}
});
The resulting ScanRecord
object can be processed following the mentioned bytes structure to obtain a ScanDetection
instance that can be passed finally to the service callback.
And it also provides a BLE advertiser to broadcast a beacon:
BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
advertiser.startAdvertising(parameters, advertiseData, advertiseCallback);
where AdvertiseData
is based on the UUID, and major and minor significant values of a given beacon.
ScanDetection detection = processAD(scanRecord.getBytes());
callback.accept(detection);
iOS
iBeacon is closed protocol, but iOS7+ provides a Location framework for dealing with different location services like GPS and beacons. A CLBeaconRegion
object defines a type of region that is based on the device’s proximity to a Bluetooth beacon.
While a beacon is in range, the startRangingBeaconsInRegion
method is used to begin receiving notifications when the relative distance to the beacon changes.
These notifications will be received through the locationManager:didRangeBeacons:inRegion
method, returning an array of CLBeacon
with the beacons in range, sorted by proximity. Selecting the first one on that array allows generating a ScanDetection
instance that can be passed to the service callback.
It also provides startBroadcast
method that takes the UUID, and major and minor significant values of a given beacon to start advertising a beacon.
Creating the project
Please check this tutorial to learn how to create a new Gluon project in IntelliJ.
Select Gluon Mobile - Multiple View Project with Glisten-Afterburner
from the list of available Projects.
Alternatively, you can also use the
Gluon Mobile Maven Archetype to create a new project.
Add the package name (com.gluonhq.samples.beacons
) and change the main class name to Beacons
.
Add the Beacon
and Beacons
views, select a JDK 11+ and a valid location for the project.
Finally, the project is created, and the main class is shown:
Modifying the project
Let’s start modifying the default project to create our Beacons application.
First let’s add the Gluon Attach BLE service, to the pom.
<dependencies>
...
<dependency>
<groupId>com.gluonhq.attach</groupId>
<artifactId>ble</artifactId>
<version>${attach.version}</version>
</dependency>
</dependencies>
...
<plugin>
<groupId>com.gluonhq</groupId>
...
<configuration>
...
<target>${gluonfx.target}</target>
<attachList>
<list>ble</list>
...
</plugins>
The FXML files can be edited with Scene Builder, that includes the Gluon Charm dependencies, so you can easily design Gluon Mobile views.
Beacons view
First of all, edit the beacons.fxml
file with Scene Builder, remove the content of the VBox
, and add a CharmListView
control.
When a beacon is detected, the BleService
will return an instance of ScanDetection
that provides the UUID, major and minor id, rssi, and proximity.
Back to the BeaconsPresenter
, we will add the CharmListView<ScanDetection, String>
that will show all the discovered beacons.
public class BeaconsPresenter {
...
@FXML
private CharmListView<ScanDetection, String> beaconsList;
}
We need to define a callback that will add each new item found to the list when the service returns a new ScanDetection
. Also, if a beacon is already selected, it will update its proximity value whenever it changes.
public class BeaconsPresenter {
...
public void initialize() {
Consumer<ScanDetection> callback = (ScanDetection t) -> {
if (t.getUuid().equalsIgnoreCase(settings.getUuid()) && beaconsList.itemsProperty().stream().noneMatch(s ->
s.getMajor() == t.getMajor() && s.getMinor() == t.getMinor())) {
javafx.application.Platform.runLater(() ->
beaconsList.itemsProperty().add(t));
}
AppViewManager.BEACON_VIEW.getPresenter().ifPresent(p -> {
BeaconPresenter presenter = (BeaconPresenter) p;
ScanDetection currentBeacon = presenter.getCurrentBeacon();
if (currentBeacon != null && currentBeacon.getUuid().equals(t.getUuid()) &&
currentBeacon.getMajor() == t.getMajor() &&
currentBeacon.getMinor() == t.getMinor()) {
presenter.setBeacon(t);
}
});
};
...
}
}
Note that the callback handler is called on a non-JavaFX Thread. Hence, to update user interface components we have to call Platform.runLater()
.
Then we will define a cell factory that shows, for each beacon found, its UUID, major and minor values, and when selected switches to the Beacon
view, passing down the ScanDetection
related object.
public class BeaconsPresenter {
...
public void initialize() {
...
beaconsList.setPlaceholder(new Label("No beacons found"));
beaconsList.setCellFactory(p -> new CharmListCell<>() {
private ScanDetection scan;
private final ListTile tile;
{
tile = new ListTile();
tile.setPrimaryGraphic(MaterialDesignIcon.BLUETOOTH.graphic());
tile.setSecondaryGraphic(MaterialDesignIcon.CHEVRON_RIGHT.graphic());
tile.setOnMouseClicked(e ->
AppViewManager.BEACON_VIEW.switchView().ifPresent(presenter ->
((BeaconPresenter) presenter).setBeacon(scan)));
}
@Override
public void updateItem(ScanDetection item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
scan = item;
tile.setTextLine(0, "UUID: " + item.getUuid());
tile.setTextLine(1, "Major: " + item.getMajor());
tile.setTextLine(2, "Minor: " + item.getMinor());
setGraphic(tile);
} else {
setGraphic(null);
}
}
});
}
}
Finally, we’ll add two buttons to start and stop beacon discovery.
We will use Configuration
to set the unique UUID of the group of devices with that same ID that will be scanned.
public class BeaconsPresenter {
private static final String UUID = "74278BDA-B644-4520-8F0C-720EAF059935";
...
public void initialize() {
...
ObservableList<Button> actions =
BleService.create()
.map(bleService -> {
final Button buttonScan = MaterialDesignIcon.BLUETOOTH_SEARCHING.button();
final Button buttonStop = MaterialDesignIcon.STOP.button();
buttonScan.setOnAction(e -> {
bleService.stopScanning();
Configuration conf = new Configuration(UUID);
bleService.startScanning(conf, callback);
buttonStop.setDisable(false);
});
buttonStop.setOnAction(e -> {
bleService.stopScanning();
buttonStop.setDisable(true);
beaconsList.itemsProperty().clear();
});
buttonScan.disableProperty().bind(buttonStop.disableProperty().not());
buttonStop.setDisable(true);
return FXCollections.observableArrayList(buttonScan, buttonStop);
})
.orElseGet(FXCollections::emptyObservableList);
beacons.showingProperty().addListener((obs, oldValue, newValue) -> {
if (newValue) {
...
appBar.getActionItems().addAll(actions);
}
});
}
}
Beacon view
Let’s edit beacon.fxml
, removing the content of the VBox
, and adding a GridPane
, populated with some labels to show the beacon values. Also three circles will inform about its proximity (unknown, far, near or immediate):
Once-the fx:id
tags have been added to each control, select View→Show sample Controller Skeleton
from Scene Builder, copy the content and paste it in the BeaconPresenter
.
And we will add the required methods:
public class BeaconPresenter {
...
private ScanDetection scanDetection;
public void initialize() {
...
}
void setBeacon(ScanDetection scan) {
scanDetection = scan;
labelUUID.setText(scan.getUuid());
labelMajor.setText(String.valueOf(scan.getMajor()));
labelMinor.setText(String.valueOf(scan.getMinor()));
labelRSSI.setText(String.valueOf(scan.getRssi()));
labelDistance.setText(scan.getProximity().name());
}
ScanDetection getCurrentBeacon() {
return beacon.isShowing() ? scanDetection : null;
}
}
Now let’s bind now the circles' stroke property to the proximity of the detection, adding also a DropShadow
effect to the related circle.
public class BeaconPresenter {
...
public void initialize() {
circleFar.setFill(null);
circleFar.setStroke(Color.TRANSPARENT);
circleFar.strokeProperty().bind(Bindings.createObjectBinding(() -> {
if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.FAR)) {
circleFar.setEffect(new DropShadow(10, Color.GREEN));
return Color.GREEN;
}
circleFar.setEffect(null);
return Color.GRAY;
}, labelDistance.textProperty()));
circleNear.setFill(null);
circleNear.setStroke(Color.TRANSPARENT);
circleNear.strokeProperty().bind(Bindings.createObjectBinding(() -> {
if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.NEAR)) {
circleNear.setEffect(new DropShadow(15, Color.GREEN));
return Color.GREEN;
}
circleNear.setEffect(null);
return Color.GRAY;
}, labelDistance.textProperty()));
circleImmediate.setFill(null);
circleImmediate.setStroke(Color.TRANSPARENT);
circleImmediate.strokeProperty().bind(Bindings.createObjectBinding(() -> {
if (scanDetection.get() != null && scanDetection.get().getProximity().equals(Proximity.IMMEDIATE)) {
circleImmediate.setEffect(new DropShadow(20, Color.GREEN));
return Color.GREEN;
}
circleImmediate.setEffect(null);
return Color.GRAY;
}, labelDistance.textProperty()));
...
}
}
Setting the UUID
Let’s create a Settings
class, that will contain the basic settings for this application. For now, just the uuid string that will be scanned. By default we will use this one: "74278BDA-B644-4520-8F0C-720EAF059935"
.
public class Settings {
public static final String UUID = "74278BDA-B644-4520-8F0C-720EAF059935";
private final StringProperty uuid = new SimpleStringProperty(UUID);
public final StringProperty uuidProperty() {
return this.uuid;
}
public final String getUuid() {
return this.uuidProperty().get();
}
public final void setUuid(final String uuid) {
this.uuidProperty().set(uuid);
}
}
The class will be injected in the presenters, so it needs to be added to the reflection list:
<reflectionList>
<list>com.gluonhq.samples.beacons.views.BeaconPresenter</list>
<list>com.gluonhq.samples.beacons.views.BeaconsPresenter</list>
<list>com.gluonhq.samples.beacons.settings.Settings</list>
</reflectionList>
Now we can inject it and replace the UUID
string:
public class BeaconsPresenter {
@Inject
Settings settings;
public void initialize() {
...
Configuration conf = new Configuration(settings.getUuid());
...
}
}
Testing the App
Build, run and test the application on desktop, both on HotSpot and creating a native image.
HotSpot
Before creating a native image, let’s run first the app on HotSpot with the regular JDK 11+, as this will be faster faster for spotting and fixing any possible issue.
Select Plugins → gluonfx → gluonfx:run
from the Maven tool window, or open a terminal and run: mvn gluonfx:run
.
Native image
Open a terminal and run: mvn gluonfx:build gluonfx:nativerun
If everything works as it should, it is time to target the application for mobile platforms like Android and iOS.
These steps are already documented in the client docs for each IDE. We will refer to the IntelliJ section.
-
For Android: Create & install an Android native image
-
For iOS: Create & install an iOS native image
Android
From a Linux machine, run:
mvn -Pandroid gluonfx:build gluonfx:package
When the build finishes, the file target/gluonfx/aarch64-android/gvm/android_project/app/build/intermediates/merged_manifest/debug/out/AndroidManifest.xml
already includes the bluetooth required permissions:
...
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Now, plug an Android device and run:
mvn -Pandroid gluonfx:package gluonfx:install gluonfx:nativerun
When running the application, notice that if bluetooth is not enabled, a popup will ask you to enable it.
Providing there is a beacon transmitter nearby, it will be discovered:
Selecting that beacon, it will show its proximity:
iOS
On iOS, the Default-Info.plist
file has to include the some related Bluetooth keys to require location services. You can change its description.
On MacOS run:
mvn -Pios gluonfx:build
When the build finishes, the file target/gluonfx/arm64-ios/gensrc/ios/Default-Info.plist
already contains the required permissions:
...
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location services</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location services</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs bluetooth services</string>
</dict>
</plist>
Now plug an iOS device and run:
mvn -Pios gluonfx:link gluonfx:nativerun
When running the application, notice that if bluetooth is not enabled, a popup will ask you to enable it, and the first time you click on the start scanning button, a popup will ask you to enable location services. This can be changed in Settings → Security → Location
.
Providing there is a beacon transmitter nearby, it will be discovered:
Selecting that beacon, it will show its proximity:
Beacon advertising and Settings
Adding the Beacon advertising functionality and a settings view to set different UUID values for scanning or advertising, or different minor, major values for advertising, are left out of this tutorial.
The reader can have a look at the full code for this project in the samples repository at GitHub.
The project includes a home view, to select either scanning or advertising:
The advertising view:
And the settings view:
Conclusion
Throughout this post we’ve covered in detail the basic steps to use the Bluetooth Low Energy API within a Multi View project, and we have tested the application on desktop and mobile devices, by building a native image.
If you have made it this far, congratulations, we hope this sample, and the documentation was helpful to you! In case you have any questions, your first stop should be the Gluon support page, where you can access the latest documentation. Gluon also recommends the community to support each other over at the Gluon StackOverflow page. Finally, Gluon offers commercial support as well, to kick start your projects.