Discovering Devices

In the simple example in the previous section, the device id and product id needed to connect to a device were hard coded in the client implementation. In an actual product, some way of obtaining the ids is needed. Any way of obtaining the device id and product id that makes sense in a given scenario are fine, for instance:

  • Discover local devices through mDNS (BonJour) - this is supported directly by the Nabto Edge SDKs as described below
  • Cache known device id and product ids (typically through a list of bookmarks)
  • Enter ids directly through the app UI - often supplied as part of a compact pairing string
  • Or use some side channel to obtain a pairing string:
    • scan a QR code
    • enter a short-code
    • use BlueTooth
    • use MQTT

In the following the first bullet (device discovery through the Nabto Edge Client SDK) is described. All the other approaches listed above are basically just about obtaining the ids in “some way”. The application then provides these to the SDK when connecting.

Nabto Edge mDNS Discovery

When using the Nabto Edge iOS and Android SDK wrappers, device discovery is done using the MdnsScanner class. When using the C API, an mDNS resolver listener is used. Please note the app configuration necessary on iOS for discovery to work!

func discover() {
    self.devices = []
    let client = Client()
    let scanner = client.createMdnsScanner(subType: "thermostat")
    scanner.addMdnsResultReceiver(self)
    do {
        try scanner.start()
        DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
            scanner.stop()
        }
    } catch {
        print("Could not start scan: \(error)")
    }
}

func onResultReady(result: MdnsResult) {
    if (result.action == .ADD) {
        let name: String? = result.txtItems["fn"]
        let device = Device(deviceId: result.deviceId, productId: result.productId, name: name)
        self.devices.append(device)
    }
}
public class Device {
    public String productId;
    public String deviceId;
    public Device(String productId, String deviceId) {
        this.productId = productId;
        this.deviceId = deviceId;
    }
}

public class MyScanner implements MdnsResultListener {
    private List<Device> devices;
    private MdnsScanner scanner;

    public MyScanner(NabtoClient client) {
        this.devices = new ArrayList<>();
        this.scanner = client.createMdnsScanner("thermostat");
        scanner.addMdnsResultReceiver(this);
        scanner.start(); // use scanner.stop() to stop scanning later on.
    }

    @Override
    public void onChange(MdnsResult result) {
        if (result.getAction() == MdnsResult.Action.ADD) {
            Device dev = new Device(result.getProductId(), result.getDeviceId());
            devices.add(dev);
        }
    }

    public List<Device> getDevices() {
        return Collections.unmodifiableList(devices);
    }
}

data class Device(
    val productId: String,
    val deviceId: String
)

class MyScanner(val nabtoClient: NabtoClient) : MdnsResultListener {
    private val scanner ´client.createMdnsScanner("thermostat")
    private val _devices = mutableListOf<Device>()
    val devices: List<Device>
        get() = _devices
    
    init {
        scanner.addMdnsResultReceiver(this)
        scanner.start()
    }

    override fun onChange(result: MdnsResult) {
        if (result.getAction() == MdnsResult.Action.ADD) {
            val dev = Device(result.getProductId(), result.getDeviceId());
            _devices.add(dev);
        }
    }
}

using Nabto.Edge.Client;

public record Device(String? ServiceInstanceName, String? ProductID, String? DeviceID);

public class MyScanner : IDisposable, IAsyncDisposable
{
    public List<Device> Devices { get; private set; } = new List<Device>{};
    private IMdnsScanner _scanner; 
    private bool _disposed;

    public MyScanner(INabtoClient client)
    {
        _scanner = client.CreateMdnsScanner();
        _scanner.Handlers += (MdnsResult r) => OnChange(r);
        _scanner.Start();
    }

    public void OnChange(MdnsResult result)
    {
        if (result.Action == MdnsResult.MdnsAction.ADD)
        {
            Console.WriteLine("Found new device: {0}.{1} ({2})", result.ProductId, result.DeviceId, result.ServiceInstanceName);  
            Devices.Add(new Device(result.ServiceInstanceName, result.ProductId, result.DeviceId));
        }
        else if (result.Action == MdnsResult.MdnsAction.REMOVE)
        {
            Console.WriteLine("Device removed: {0}", result.ServiceInstanceName, result.ProductId, result.DeviceId);
            Devices.RemoveAll(d => d.ServiceInstanceName == result.ServiceInstanceName);
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public ValueTask DisposeAsync()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
        return ValueTask.CompletedTask;
    }

    ~MyScanner()
    {
        Dispose(false);
    }

    protected void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Console.WriteLine("Disposing scanner");
                _scanner.Dispose();
            }
            _disposed = true;
        }
    }


    
}

This and all examples in this section use the synchronous APIs for simplicity. So if re-using snippets in your own app, make sure to run all potentially slow operations on a different thread than the main thead, e.g. using a global dispatch queue on iOS. Alternatively, use the asynchronous variants of the functions in iOS and the Kotlin extenion’s await variants in Android.