IOTPractices

This publication covers the practical knowledge and experience of software development practices such as TDD, CICD, Automated Testing, Agile for IoT development. It is an open community initiative for and by the IoT enthusiasts

OpenThread Commissioner Implementation in Java

5 min readMar 10, 2025

--

In 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:

  1. Discovering the Border Router
  2. Connecting to the Border Router
  3. Adding Joiner Rules
  4. 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 whenver 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.

  • 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 -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-soure 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

--

--

IOTPractices
IOTPractices

Published in IOTPractices

This publication covers the practical knowledge and experience of software development practices such as TDD, CICD, Automated Testing, Agile for IoT development. It is an open community initiative for and by the IoT enthusiasts

Kuldeep Singh
Kuldeep Singh

Written by Kuldeep Singh

Engineering Director and Head of XR Practice @ ThoughtWorks India.

No responses yet