OThreadScan Class

About

The OThreadScan class discovers nearby Thread networks using MLE discover (otThreadDiscover() / OpenThread CLI discover). Each result is an OThreadNetworkInfo structure with Thread identity fields (network name, Extended PAN ID, joinable flag) and IEEE 802.15.4 link fields (extended address, PAN ID, channel, RSSI, LQI).

This is the same primitive Matter uses to list Thread networks during commissioning. The global instance is OThreadScan (same pattern as OThreadUDP and OThreadCoAPServer).

Key Features:

  • Single entry point: discoverNetworks() — no separate 802.15.4 or energy scan modes in the public API.

  • Blocking, async poll (scanComplete()), and streaming callbacks (onResult() / onComplete()).

  • Optional discover filters (PAN ID, joiner-only, EUI-64) matching otThreadDiscover() parameters.

  • Wi-Fi scan style return codes: OT_DISCOVER_RUNNING (-1), OT_DISCOVER_FAILED (-2).

  • Result storage is pre-reserved (OT_DISCOVER_MAX_RESULTS, default 16) before each scan so the OpenThread callback path does not heap-allocate while the OT API lock is held.

Use Cases:

  • List Thread networks before joining or commissioning.

  • Build a network-picker UI on a joiner device.

  • Debug RF environment when a Leader is expected nearby.

Prerequisites:

  • OThread.begin() and OThread.networkInterfaceUp() — Thread does not need to be started (matches Matter pre-commission discovery).

  • For meaningful results, at least one Leader or Router should be active nearby.

Include the header:

#include <OThreadScan.h>

OThreadNetworkInfo

Each discovery response is stored as OThreadNetworkInfo:

Field

Description

networkName

Thread network name (null-terminated)

extendedPanId

Extended PAN ID (8 bytes)

panId

IEEE 802.15.4 PAN ID

extAddress

Responder extended address (8 bytes)

channel

IEEE 802.15.4 channel (11..26)

rssi

Received signal strength (dBm)

lqi

Link Quality Indicator

threadVersion

Thread version (4-bit MLE value)

joinable

Joining permitted (non-empty Steering Data for MLE discover)

nativeCommissioner

Native Commissioner flag when present

Helper methods: networkNameStr(), extendedPanIdStr(), extAddressStr().

OThreadDiscoverFilters

Optional filters for setDiscoverFilters() (defaults match CLI discover with no extra arguments):

OThreadDiscoverFilters filters;
filters.panIdFilter = OT_PANID_BROADCAST;  // 0xffff = no PAN filter
filters.joinerOnly = false;
filters.eui64Filter = false;
OThreadScan.setDiscoverFilters(filters);

API Reference

Configuration

setScanTimeout

void setScanTimeout(uint32_t ms);

Maximum time to wait for blocking discovery or async completion. Default: OT_DISCOVER_DEFAULT_TIMEOUT_MS (30000 ms).

OT_DISCOVER_MAX_RESULTS

Maximum unique networks stored per scan (default 16). Vectors are pre-reserved to this size before each scan. Define the macro before including OThreadScan.h to override:

#define OT_DISCOVER_MAX_RESULTS 32
#include <OThreadScan.h>

Or pass -DOT_DISCOVER_MAX_RESULTS=32 at compile time. See ThreadScan_Discover for a sketch example.

setChannel

void setChannel(uint8_t channel);

Limit discovery to one IEEE 802.15.4 channel (11..26). Pass 0 to scan all supported channels (channel mask 0 in otThreadDiscover()).

setDiscoverFilters

void setDiscoverFilters(const OThreadDiscoverFilters &filters);

Apply PAN / joiner / EUI-64 filters before the next discoverNetworks() call.

Callbacks

onResult

void onResult(OThreadDiscoverResultCallback callback, void *context = nullptr);

Called for each network as OpenThread receives a Discovery Response. Matches the OpenThread / Matter streaming model.

onComplete

void onComplete(OThreadDiscoverCompleteCallback callback, void *context = nullptr);

Called when discovery finishes. resultCount is the number of networks collected, or OT_DISCOVER_FAILED on failure. error is the OpenThread error from the start request (OT_ERROR_NONE on success).

Discovery

discoverNetworks

int16_t discoverNetworks(bool async = false);

Start MLE Thread network discovery.

  • async == false — blocks until complete or setScanTimeout() expires; returns network count on success.

  • async == true — returns OT_DISCOVER_RUNNING immediately on success.

Return values:

  • >= 0 — number of networks found (blocking mode, or scanComplete() when done).

  • OT_DISCOVER_RUNNING (-1) — discovery in progress.

  • OT_DISCOVER_FAILED (-2) — failed, timed out, or not started.

At most OT_DISCOVER_MAX_RESULTS unique networks (default 16) are stored. Vectors are pre-reserved before the scan starts so the OpenThread callback does not allocate while the API lock is held. Multiple discovery responses with the same Extended PAN ID are merged in storage (strongest RSSI kept). onResult() is still invoked for every Discovery Response. Additional unique networks beyond the cap are still delivered through onResult() but are omitted from getResult() / getResultCount().

scanComplete

int16_t scanComplete();

Poll async discovery state from loop(). Same return semantics as discoverNetworks() in blocking/async modes. Completion is reported only after the final discovery callback sets internal state; do not treat otThreadIsDiscoverInProgress() as a substitute for scanComplete().

scanDelete

void scanDelete();

Release stored results and free associated memory (internal vector capacity is released, not just cleared). Call after processing results. This is a no-op until the final discovery callback has completed, even if OpenThread already reports discover idle; call again after the scan finishes. After a timeout it likewise stays a no-op until OpenThread delivers that final callback — retry once isDiscoverInProgress() returns false.

isDiscoverInProgress

bool isDiscoverInProgress() const;

Returns true while an MLE discovery scan is in progress.

Results

Result access (WiFiScan-style)

Indexed result APIs (getResult(), getResultCount(), field getters, and getActiveScanResult()) are available only after discovery completes:

  • Blocking: discoverNetworks() return value >= 0

  • Async: scanComplete() return value >= 0

  • Callbacks: onComplete() has fired

While a scan is still running, these methods return empty values (count 0, false, nullptr). Use onResult() to handle networks as they are discovered. Do not call other OThreadScan methods from inside onResult() or onComplete() (they run on the OpenThread task with the API lock held).

getResultCount

uint16_t getResultCount() const;

Number of entries from the last completed discovery (saturated at UINT16_MAX if the internal list is larger). Returns 0 while discovery is still in progress.

getResult

const OThreadNetworkInfo &getResult(uint16_t index) const;
bool getResult(uint16_t index, OThreadNetworkInfo &info) const;

Access one result by index (0 .. count-1). Internal storage is valid until scanDelete(). Returns a static empty entry while discovery is still in progress or if the index is out of range.

Index-based convenience getters (networkName(), panId(), extendedPanIdStr(), extAddressStr(), channel(), rssi(), lqi(), isJoinable(), threadVersion(), isNativeCommissioner()) delegate to getResult(index).

getActiveScanResult

const otActiveScanResult *getActiveScanResult(uint16_t index) const;

Raw OpenThread result for advanced use. Valid until scanDelete(). Returns nullptr while discovery is still in progress or if the index is out of range.

Usage Patterns

Blocking (WiFiScan-style)

#include <OThread.h>
#include <OThreadScan.h>

void setup() {
    Serial.begin(115200);
    OThread.begin(false);
    OThread.networkInterfaceUp();

    int n = OThreadScan.discoverNetworks();
    if (n >= 0) {
        for (int i = 0; i < n; ++i) {
            const OThreadNetworkInfo &net = OThreadScan.getResult(i);
            Serial.println(net.networkNameStr());
        }
        OThreadScan.scanDelete();
    }
}

Async poll (WiFiScanAsync-style)

OThreadScan.discoverNetworks(true);

void loop() {
    int16_t status = OThreadScan.scanComplete();
    if (status >= 0) {
        // process OThreadScan.getResult(i) ...
        OThreadScan.scanDelete();
    }
}

Streaming callbacks

static void onNetwork(const OThreadNetworkInfo &info, void *) {
    Serial.println(info.networkNameStr());
}

static void onDone(int16_t resultCount, otError error, void *) {
    // Summary only — do not call scanDelete() here (OT API lock held).
    Serial.printf("discovery done: %d\n", resultCount);
}

void setup() {
    OThreadScan.onResult(onNetwork);
    OThreadScan.onComplete(onDone);
}

void loop() {
    OThreadScan.discoverNetworks(true);
    while (OThreadScan.scanComplete() == OT_DISCOVER_RUNNING) {
        delay(50);
    }
    OThreadScan.scanDelete();
}

Examples

Native API examples (libraries/OpenThread/examples/Native/ThreadScan/):

CLI equivalent: CLI ThreadScan (discover command via OThreadCLI).

Troubleshooting

No networks found
  • Start a Leader or Router on another board first (for example SimpleThreadNetwork LeaderNode).

  • Confirm OThread.networkInterfaceUp() was called before discoverNetworks().

``OT_DISCOVER_FAILED``
  • IPv6 interface down, lock/allocation failure, or timeout — confirm networkInterfaceUp() and increase setScanTimeout() if needed.

``OT_DISCOVER_RUNNING``
  • Discovery is already in progress — wait for scanComplete() to report completion before starting another scan or calling scanDelete().

Wrong network listed
  • Multiple partitions nearby — compare extendedPanId with the target Leader.

See also