Skip to content

# Discovering and Connecting to Nix Devices

Regardless of device type, each unique Nix device can be represented by an object that conforms to the `IDeviceCompat` interface (see Kotlin or Java APIs). To obtain instances of the device object, they need to first be discovered using the `DeviceScanner` (see Kotlin or Java APIs). They can also be recalled at a later time directly using their `id` property (for Bluetooth connected devices only).

## Device discovery

The `DeviceScanner` implements the `IDeviceScanner` interface (see Kotlin or Java APIs) and searches for nearby Nix devices using Bluetooth. The `DeviceScanner` will also report devices connected via USB. Scanner events are reported via:

- `OnScannerStateChangeListener`:
  - Optional callback interface which reports when the scanner starts and stops searching for devices.
  - API reference: Kotlin or Java.
  - This is set using `setOnScannerStateChangeListener()`.
- `OnDeviceFoundListener`:
  - Callback interface which is called by the scanner each time that an `IDeviceCompat` instance has been found.
  - API reference: Kotlin or Java.
  - Note that this will be called both when a new device is found and when the RSSI/signal strength of a previously discovered device has changed. It is possible to use the `id` parameter as a means to uniquely identify discovered devices.

Note

Devices can only be discovered or recalled while your SDK licence is in an `LicenseManagerState/ACTIVE` state; see `LicenseManager.Shared.getState()` for Kotlin or Java. Only device types supported by your license can be discovered and connected; see `LicenseManager.Shared.getAllowedDeviceTypes()` for Kotlin or Java.

To use the `DeviceScanner`:

- Initialize a scanner instance
- Optionally set a `OnScannerStateChangeListener`, if it is desired to track when the scanner starts and stops a device search.
- Check the scanner state, which will be `DeviceScannerState/IDLE` if properly initialized.
  - The scanner state is available via `IDeviceScanner.getState()` (see Kotlin or Java APIs).
  - See a list possible states for Kotlin or Java.
  - If the state is `DeviceScannerState/ERROR_LICENSE`, check `LicenseManager.Shared.getState()` for further details.
- Start the scanner using `start()` (see Kotlin or Java APIs).
  - By default, the scanner will search for a period `DEFAULT_GENERAL_SCAN_PERIOD_MS`, but this interval can be overridden by providing an argument for `scanPeriodMs`.
- Observe discovered devices via the `onScanResult()` in the `OnDeviceFoundListener` interface. At this stage, it is valid to check a limited number of parameters on the device object:
  - `getId()`: Device identifier string (see Kotlin or Java APIs).
  - `getRssi()`: Device signal strength (-127 to 0). Will be 0 for USB connected devices (see Kotlin or Java APIs).
  - `getInterfaceType()`: Indicates whether the device was found via Bluetooth or USB connection (see Kotlin or Java APIs).
  - `getType()`: Indicates the specific device type (see Kotlin or Java APIs).
  - `getName()`: Full name of the device (e.g. 'Nix Spectro 2', see Kotlin or Java APIs).
- The device search will run for the specified duration, after which `onScannerStopped()` is called. The device search can also be manually stopped using `stop()`.

Kotlin Java

    // Define the OnScannerStateChangeListener
    val scannerStateListener = object : OnScannerStateChangeListener {
        override fun onScannerStarted(sender: IDeviceScanner) {
            // Scanner has started ...
        }

        override fun onScannerStopped(sender: IDeviceScanner) {
            // Scanner has stopped ...
        }
    }

    // Define the OnDeviceFoundListener
    val deviceFoundListener = object : OnDeviceFoundListener {
        override fun onScanResult(sender: IDeviceScanner, device: IDeviceCompat) {
            // Nearby device found
            // Handle discovery here ...

            // Valid to query some parameters now:
            Log.d(TAG, String.format(
                "Found %s (%s) at RSSI %d",
                device.id,
                device.name,
                device.rssi)
            )
        }
    }

    // Application context, in a Fragment use getContext()
    val context: Context = applicationContext

    // Initialize the scanner
    val scanner = DeviceScanner(context)
    scanner.setOnScannerStateChangeListener(scannerStateListener)

    // Start the scanner
    if (scanner.state == IDeviceScanner.DeviceScannerState.IDLE) {
        scanner.start(listener = deviceFoundListener)
    } else {
        // Check the error state and handle accordingly ...
    }

    // Define the OnScannerStateChangeListener
    IDeviceScanner.OnScannerStateChangeListener stateChangeListener =
        new IDeviceScanner.OnScannerStateChangeListener()
    {
        @Override
        public void onScannerStarted(@NonNull IDeviceScanner sender) {
            // Scanner has started
            // ...
        }

        @Override
        public void onScannerStopped(@NonNull IDeviceScanner sender) {
            // Scanner has stopped
            // ...
        }
    };

    // Define the OnDeviceFoundListener
    IDeviceScanner.OnDeviceFoundListener deviceFoundListener =
        new IDeviceScanner.OnDeviceFoundListener()
    {
        @Override
        public void onScanResult(
            @NonNull IDeviceScanner sender,
            @NonNull IDeviceCompat device
        ) {
            // Nearby device found
            // Handle discovery here ...

            // Valid to query some parameters now:
            Log.d(TAG, String.format(
                "Found %s (%s) with RSSI %d",
                device.getId(),
                device.getName(),
                device.getRssi()));
        }
    };

    // Application context, in a Fragment use getContext()
    Context context = getApplicationContext();

    // Initialize the scanner
    IDeviceScanner scanner = new DeviceScanner(context);
    scanner.setOnScannerStateChangeListener(scannerStateListener);

    // Start the scanner
    if (scanner.getState() == IDeviceScanner.DeviceScannerState.IDLE) {
        scanner.start(
            deviceFoundListener,
            IDeviceScanner.DEFAULT_GENERAL_SCAN_PERIOD_MS);
    } else {
        // Check the error state and handle accordingly ...
    }

## Recalling a known Nix device

For Bluetooth connected devices only, it is possible to construct an `IDeviceCompat` instance directly if the hardware address and device type is known. The constructor takes the hardware address and device names as arguments.

- The device address must match the original value queried from `getId()`. Note that this corresponds to the hardware address of the advertising Bluetooth device
- The device name must match the original value queried from `getName()`. Note that this corresponds to the device name as advertised on Bluetooth. If the `DeviceType` enum is known, this also corresponds to `getFullName()` (see Kotlin or Java APIs for `DeviceType`).

Kotlin Java

    // Application context, in a Fragment use getContext()
    val context: Context = applicationContext

    // Hardware address of known device
    val exampleAddress: String = "00:AA:11:BB:22:CC"

    // Name of known device / type (must match advertised name)
    val exampleName: String = "Nix Spectro 2"

    // Create IDeviceCompat instance
    val recalledDevice: IDeviceCompat = DeviceCompat(
        context = context,
        address = exampleAddress,
        name = exampleName
    )

    // Application context, in a Fragment use getContext()
    Context context = getApplicationContext();

    // Hardware address of known device
    String exampleAddress = "00:AA:11:BB:22:CC";

    // Name of known device / type (must match advertised name)
    String exampleName = "Nix Spectro 2";

    // Create IDeviceCompat instance
    IDeviceCompat recalledDevice = new DeviceCompat(
        context,
        exampleAddress,
        exampleName,
        Integer.MIN_VALUE);

## Opening a connection

Once an `IDeviceCompat` instance has been obtained, a connection can be opened by calling `connect()` (see Kotlin or Java APIs). Connection state changes are provided via an `OnDeviceStateChangeListener` interface. The connection can be cancelled or closed normally by later calling `disconnect()`.

The steps necessary for connecting include:

1. Implement the `OnDeviceStateChangeListener` interface in your class
2. Stop the `DeviceScanner` before opening a connection, if still running.
   - It is safe to call `stop()` on the `DeviceScanner` at any time, even if already stopped.
3. Start the connection process by calling `connect()`.
   - The connection process is asynchronous and `connect()` will return immediately. If a connection activity indicator is shown, it can be shown immediately before calling this method.
   - A call to `connect()` will always result in either the `onConnected()` or `onDisconnected()` callback being triggered.
   - Device operations are not possible until the `onConnected()` callback is received (successful connection)
   - If the connection process fails, the `onDisconnected()` callback will be invoked with an appropriate status code indicating the cause of the error.
   - `onDisconnected()` will also be called later whenever the device is disconnected. This can occur normally (i.e. - if the host application calls `disconnect()` on the `IDeviceCompat` instance), or abnormally (i.e. - if the connection is dropped due to an error or low Bluetooth signal strength).
   - Your license is linked to a specific allocation of Nix devices and will not operate with devices from another allocation.
     - At connection time, the SDK will read an allocation code stored on the Nix device and compare to the license information.
     - If this check does not pass, the SDK will contact a Nix authentication server to check if that device serial number is authorized.
     - If the device cannot be authenticated (i.e. – an unknown allocation code was found and an internet connection is unavailable), the device will be disconnected with the status `ERROR_UNAUTHORIZED`.
     - The internet connection is required only once every 30 days – once authorized, this status is saved, and connections can be made offline for this time period.
4. When the user has finished using the device, the connection can be closed by calling `disconnect()`.

Kotlin Java

    // Define the OnDeviceStateChangeListener
    val deviceStateListener = object : OnDeviceStateChangeListener {
        override fun onConnected(
            sender: IDeviceCompat
        ) {
            // Device has connected successfully
            // Device properties can now be queried
            // Device commands/operations can now run
            // ...
        }

        override fun onDisconnected(
            sender: IDeviceCompat,
            status: DeviceStatus
        ) {
            // Handle status codes here, if desired in your application
            // At minimum, should check for ERROR_UNAUTHORIZED and
            // ERROR_LICENSE cases
            when (status) {
                // Device not authorized for this license UUID
                DeviceStatus.ERROR_UNAUTHORIZED -> {
                    // ...
                }

                // There is an issue with the LicenseManager
                // Check LicenseManager.state
                DeviceStatus.ERROR_LICENSE -> {
                    // ...
                }

                // Normal disconnect, triggered by device.disconnect()
                DeviceStatus.SUCCESS -> {
                    // ...
                }

                // Nix device dropped the connection
                DeviceStatus.ERROR_DROPPED_CONNECTION -> {
                    // ...
                }

                // Connection to Nix device timed out
                DeviceStatus.ERROR_TIMEOUT -> {
                    // ...
                }

                // Other internal errors
                DeviceStatus.ERROR_MAX_ATTEMPTS,
                DeviceStatus.ERROR_UNSUPPORTED_DEVICE,
                DeviceStatus.ERROR_INTERNAL -> {
                    // ...
                }
            }
        }

        override fun onBatteryStateChanged(
            sender: IDeviceCompat,
            newState: Int
        ) {
            super.onBatteryStateChanged(sender, newState)
            // Battery level has updated
            // ...
        }

        override fun onExtPowerStateChanged(
            sender: IDeviceCompat,
            newState: Boolean
        ) {
            super.onExtPowerStateChanged(sender, newState)
            // External power has been connected or disconnected
            // ...
        }
    }

    // Device instance that has already been found by the DeviceScanner
    var device: IDeviceCompat

    // Initiate the connection
    device.connect(deviceStateListener)

    // Define the OnDeviceStateChangeListener
    IDeviceCompat.OnDeviceStateChangeListener deviceStateListener =
        new IDeviceCompat.OnDeviceStateChangeListener()
    {
        @Override
        public void onConnected(@NonNull IDeviceCompat sender) {
            // Device has connected successfully
            // Device properties can now be queried
            // Device commands/operations can now run
            // ...
        }

        @Override
        public void onDisconnected(
            @NonNull IDeviceCompat sender,
            @NonNull DeviceStatus status)
        {
            // Handle status codes here, if desired in your application
            // At minimum, should check for ERROR_UNAUTHORIZED and
            // ERROR_LICENSE cases
            switch (status) {
                case ERROR_UNAUTHORIZED:
                    // Device not authorized for this license UUID
                    // ...
                    break;
                case ERROR_LICENSE:
                    // There is an issue with the LicenseManager
                    // Check LicenseManager.Shared.getState()
                    break;
                case SUCCESS:
                    // Normal disconnect, triggered by device.disconnect()
                    //...
                    break;
                case ERROR_DROPPED_CONNECTION:
                    // Nix device dropped the connection
                    // ...
                    break;
                case ERROR_TIMEOUT:
                    // Connection to Nix device timed out
                    // ...
                    break;
                case ERROR_MAX_ATTEMPTS:
                case ERROR_UNSUPPORTED_DEVICE:
                case ERROR_INTERNAL:
                    // Other internal errors
                    // ...
                    break;
            }
        }

        @Override
        public void onBatteryStateChanged(
            @NonNull IDeviceCompat sender,
            int newState
        ) {
            // Battery level has updated
            // ...
        }

        @Override
        public void onExtPowerStateChanged(
            @NonNull IDeviceCompat sender,
            boolean newState
        ) {
            // External power has been connected or disconnected
            // ...
        }
    };

    // Device instance that has already been found by the DeviceScanner
    IDeviceCompat device;

    // Initiate the connection
    device.connect(deviceStateListener);

## Next steps

- Device Operations