OpenThread Commissioner Implementation in Java
- 6 minutes read - 1156 wordsIn a previous article, we explored how to build and use an Android app as an external commissioner to onboard Thread devices into a network. This article takes it a step further by demonstrating how to implement a Java-based external commissioner.
The thread commissioning process involves the following key steps:
- Discovering the Border Router
- Connecting to the Border Router
- Adding Joiner Rules
- Joining the Thread Network
In this guide, we will implement these steps in a Java application, providing a clear and structured approach to external commissioning in OpenThread.
1. Discovering the Border Router
We can use JmDNS for DNS resolution of border router. Border router service is registered at “_meshcop._udp.local.
” and we can resolve it in the network where mdns is enabled.
DNS Service Listener
DNS Service Listener listens to the callbacks whenever it finds a service added/remove and resolved.
public class OTBRDiscoverer implements ServiceListener{
public void serviceAdded(ServiceEvent serviceEvent) {
logger.debug("Service added: {}", serviceEvent.getInfo());
}
@Override
public void serviceRemoved(ServiceEvent serviceEvent) {
logger.debug("Service removed: {}", serviceEvent.getInfo());
}
@Override
public void serviceResolved(ServiceEvent serviceEvent) {
logger.debug("Service resolved: {}", serviceEvent.getInfo());
try {
String networkName = serviceEvent.getInfo().getPropertyString(KEY_NETWORK_NAME);
int otbrPort = serviceEvent.getInfo().getPort();
String extPanId = Utils.getHexString(serviceEvent.getInfo().getPropertyBytes(KEY_EXTENDED_PAN_ID));
if(serviceEvent.getInfo().getInet4Addresses().length>0){
String otbrAddress = serviceEvent.getInfo().getInet4Addresses()[0].getHostAddress();
otbrInfo = new OTBRInfo(networkName, extPanId, otbrAddress, otbrPort);
logger.info("Service resolved: {}:{} {} {}", otbrAddress, otbrPort, networkName, extPanId);
}
} catch (Exception e) {
logger.error("Failed to resolve service: {}", e.getMessage());
} finally {
try {
jmdns.close();
} catch (IOException e) {
logger.error("Failed to close JNS service: {}", e.getMessage());
}
}
}
}
In serviceResolved
we can get border router details like IP Address, Port and other network information that we would need later to connect with it.
Add DNS Service Listener
Add listener for ‘_meshcop._udp.local.
” service type.
try {
jmdns = JmDNS.create(getLocalHostIP());
jmdns.addServiceListener(MDNS_SERVICE_TYPE, new OTBRDiscoverer());
logger.info("Discovering Border Router at {}", MDNS_SERVICE_TYPE);
} catch (IOException e) {
logger.warn("Failed to create JmDNS {}", e.getMessage());
}
Note:- Get local IP address instead of localhost, sometime “localhost”/127.0.0.1 does not work.
2. Connecting with Border Router
Once a border router is discovered, we can connect with it with PSKc.
Pre-Shared Key for Commissioner(PSKc)
We can get pskc from Border Router’s CLI
$ sudo ot-ctl pskc
445f2b5ca6f2a93a55ce570a70efeecb
done
$
If we don’t have access to Border Router’s CLI then we can generate it programming
pskc = commissioner.computePskc(passphrase, otbrInfo.getNetworkName(), new ByteArray(Utils.getByteArray(otbrInfo.getExtendedPanId())));
Where passphrase
, network name
, extended pan id
are defined in OpenThread Border Router.
Connecting Commissioner with Border Router
Connecting commissioner with border router at discovered address, port and pckc.
commissioner.connect(pskc, otbrInfo.getOtbrAddress(), otbrInfo.getOtbrPort())
.thenRun(() -> {
logger.info("Commissioner connected successfully!");
})
.exceptionally(
ex -> {
logger.error("Commissioner failed to connect : {}", String.valueOf(ex));
return null;
});
It would start petitioning in native commissioner. At a time only one commissioner can be active in thread network.
private Error connect(String borderAgentAddress, int borderAgentPort) {
// Petition to be the active commissioner in the Thread Network.
return nativeCommissioner.petition(new String[1], borderAgentAddress, borderAgentPort);
}
3. Add Joiner Rules
Once commissioner is connected to Border Router, we can now add joiner rules. Here we are enabling all joiners with Pre-Shared Key for Device (PSKd) or also called joiner key.
commissioner.enableAllJoiners(pskd)
.thenRun(() -> {
logger.info("All Joiners are accepted at PSKD:{}", pskd);
})
.exceptionally(
ex -> {
logger.error("Failed to add Joiner :{}", ex.getMessage());
return null;
});
This would add joiner with ‘0xFF
’ steering data and ‘0x00
’ as joiner id.
Joiner ID is generally a sha256 hash of the EUI64 of device trying to join.
Commissioner.addJoiner(steeringData, joinerId);
CommissionerDataset commDataset = new CommissionerDataset();
commDataset.setPresentFlags(commDataset.getPresentFlags() & ~CommissionerDataset.kSessionIdBit);
commDataset.setPresentFlags(commDataset.getPresentFlags() & ~CommissionerDataset.kBorderAgentLocatorBit);
commDataset.setPresentFlags(commDataset.getPresentFlags() | CommissionerDataset.kSteeringDataBit);
commDataset.setSteeringData(steeringData);
nativeCommissioner.setCommissionerDataset(commDataset);
We are also maintaining a local map for Joiner id and PSKd in CommissionerHandler, where we put any new joiner into it with a registered PSKd.
public class ThreadCommissioner extends CommissionerHandler {
private final Map<String, String> joiners = new HashMap<>();
public CompletableFuture<Void> enableAllJoiners(String pskd) {
return CompletableFuture.runAsync(
() -> {
throwIfFail(this.enableAllJoiners());
joiners.put(Utils.getHexString(computeJoinerIdAll()), pskd);
});
}
}
4. Join Thread Network
When a Thread End Device initiate the joining process, CommissionerHandler defined in Java application would receive callbacks. On onJoinerRequest
, where we return pskd for the joiner trying to join. This would allow joining the thread network.
public String onJoinerRequest(ByteArray joinerId) {
String joinerIdStr = Utils.getHexString(joinerId);
logger.info("A joiner (ID={}) is requesting commissioning", joinerIdStr);
String pskd = joiners.get(joinerIdStr);
if (pskd == null) {
// Check is JOINER ID All is registered
pskd = joiners.get(Utils.getHexString(computeJoinerIdAll()));
}
return pskd;
}
public void onJoinerConnected(ByteArray joinerId, Error error) {
logger.info("A joiner (ID={}) is connected with {}", Utils.getHexString(joinerId), error);
}
public boolean onJoinerFinalize(ByteArray joinerId,String vendorName,String vendorModel,String vendorSwVersion,ByteArray vendorStackVersion,String provisioningUrl,ByteArray vendorData) {
logger.info("A joiner (ID={}) is finalizing", Utils.getHexString(joinerId));
//Allow all joiner.
return true;
}
OpenThread Commissioner Java Project
We have open sourced complete java application here, follow the read me to build the java application.
GitHub - iotpractice/openthread-commissioner-java: Java Implementation of Open Thread Commissioner
We can use pre-build libraries from the project or build them from scratch for your platform. and then compile and package the Java Commissioner using Maven:
mvn clean package
Run OpenThread Commissioner Java Application
Navigate to the target/ directory and execute the Commissioner.
cd target
java -Djava.library.path=. -jar openthread-commissioner-java-1.0-SNAPSHOT-jar-with-dependencies.jar
Expected output:
INFO c.thread.commissioner.OTBRDiscoverer - Discovering Border Router at _meshcop._udp.local.
INFO com.thread.commissioner.Runner - Discovering Border Router...1
INFO com.thread.commissioner.Runner - Discovering Border Router...2
INFO com.thread.commissioner.Runner - Discovering Border Router...3
INFO c.thread.commissioner.OTBRDiscoverer - Service resolved: 172.20.10.20:49154 OpenThreadDemo 1111111122222222
>>> Enter PSKc (leave blank to compute): 445f2b5ca6f2a93a55ce570a70efeecb
Commands:
1. Check State
2. Enable All Joiners
3. Exit
Enter command number: 1
INFO com.thread.commissioner.Runner - Commissioner connected successfully!
INFO com.thread.commissioner.Runner - State: kActive
If PSKc is not provided, the application will prompt for the network name, extended PAN ID, and passphrase to generate it.
Enable Joiners to Join the Network
Add Joiner Rule by choosing the command 2, it will enable all joiner for a Pre-Shared Key for Device (PSKD):
Commands:
1. Check State
2. Enable All Joiners
3. Exit
Enter command number: 2
Enter PSKd For All Joiner:JO1NME
INFO c.t.commissioner.ThreadCommissioner - enableAllJoiners - steeringData=ffffffffffffffffffffffffffffffff A joiner (ID=af5570f5a1810b7a)
2025-03-09 22:00:30 INFO com.thread.commissioner.Runner - All Joiners are accepted at PSKD:JO1NME
Joining from a Device
To test device joining, build and flash a Thread end-device firmware with commissioning support. The device should discover the network and attempt to join using the Pre-Shared Key for Device (PSKD).
Initiate the joining process with the following command:
> joiner start J01NME
joiner success
Done
>
On a successful join attempt, the CommissionerHandler in java application will print :
INFO c.t.commissioner.ThreadCommissioner - A joiner (ID=ca666d7873988c66) is requesting commissioning
INFO c.t.commissioner.ThreadCommissioner - A joiner (ID=ca666d7873988c66) is connected with OK
INFO c.t.commissioner.ThreadCommissioner - A joiner (ID=ca666d7873988c66) is finalizing
Contribute
We welcome contributions from the community to improve and expand the open-source project . Our goal is to refine it to a level where it can be submitted as a PR to the official OpenThread repository. Please refer to OpenThread’s contribution guidelines to get started.
References
- Connecting Thread devices to Internet
- Understanding Commissioner and Joiner
- Understanding External Commissioning — Commissioner CLI
- Understanding External Commissioning — Android App
Find the IoT Practices Publication for more details.
#IOT #network #cloud #getting started #learning #technology #fundamentals #thread #openthread #commissioner #android #java #nRF #SDK #open source