diff --git a/.github/workflows/platformio-examples.yml b/.github/workflows/platformio-examples.yml new file mode 100644 index 000000000..a2fbe0a3a --- /dev/null +++ b/.github/workflows/platformio-examples.yml @@ -0,0 +1,63 @@ +name: Build Arduino examples + +on: + push: + paths: + - 'src/**' + - 'examples/**' + - 'library.properties' + - '.github/workflows/platformio-examples.yml' + pull_request: + paths: + - 'src/**' + - 'examples/**' + - 'library.properties' + - '.github/workflows/platformio-examples.yml' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: platformio-${{ runner.os }}-${{ hashFiles('library.properties') }} + restore-keys: | + platformio-${{ runner.os }}- + + - name: Install PlatformIO Core + run: pip install --upgrade platformio + + - name: Build examples + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + sketches=(examples/*/*.ino) + + if [ ${#sketches[@]} -eq 0 ]; then + echo "No examples found" + exit 1 + fi + + for sketch in "${sketches[@]}"; do + sketch_dir=$(dirname "$sketch") + sketch_name=$(basename "$sketch_dir") + echo "::group::Building ${sketch_name}" + pio ci \ + --board esp32dev \ + --lib="." \ + "$sketch" + echo "::endgroup::" + done diff --git a/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino b/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino index b8d24d264..6660b28c7 100644 --- a/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino +++ b/examples/NimBLE_Scan_Continuous/NimBLE_Scan_Continuous.ino @@ -2,6 +2,10 @@ * This example will scan forever while consuming as few resources as possible * and report all advertisments on the serial monitor. * + * The scan callback prints the primary advertising channel for each + * advertisement when available, demonstrating the use of + * NimBLEAdvertisedDevice::getChannel(). + * * Created: on January 31 2021 * Author: H2zero * @@ -13,7 +17,14 @@ NimBLEScan* pBLEScan; class MyAdvertisedDeviceCallbacks: public NimBLEAdvertisedDeviceCallbacks { void onResult(NimBLEAdvertisedDevice* advertisedDevice) { - Serial.printf("Advertised Device: %s \n", advertisedDevice->toString().c_str()); + uint8_t channel = advertisedDevice->getChannel(); + if (channel != 0xFF) { + Serial.printf("Advertised Device on channel %u: %s \n", + channel, advertisedDevice->toString().c_str()); + } else { + Serial.printf("Advertised Device on unknown channel: %s \n", + advertisedDevice->toString().c_str()); + } } }; diff --git a/src/NimBLEAdvertisedDevice.cpp b/src/NimBLEAdvertisedDevice.cpp index 29c9532de..0916528b5 100644 --- a/src/NimBLEAdvertisedDevice.cpp +++ b/src/NimBLEAdvertisedDevice.cpp @@ -36,6 +36,7 @@ NimBLEAdvertisedDevice::NimBLEAdvertisedDevice() : m_callbackSent = false; m_timestamp = 0; m_advLength = 0; + m_channelIndex = 0xFF; } // NimBLEAdvertisedDevice @@ -823,6 +824,19 @@ time_t NimBLEAdvertisedDevice::getTimestamp() { } // getTimestamp +/** + * @brief Get the primary advertising channel. + * @return The advertising channel (37, 38, or 39) when available, or 0xFF when unknown. + */ +uint8_t NimBLEAdvertisedDevice::getChannel() { + // Map controller channel index (0-2) to BLE advertising channels (37-39) + if (m_channelIndex <= 2) { + return 37 + m_channelIndex; + } + return 0xFF; +} // getChannel + + /** * @brief Get the length of the payload advertised by the device. * @return The size of the payload in bytes. diff --git a/src/NimBLEAdvertisedDevice.h b/src/NimBLEAdvertisedDevice.h index 772bab914..468988c89 100644 --- a/src/NimBLEAdvertisedDevice.h +++ b/src/NimBLEAdvertisedDevice.h @@ -38,6 +38,9 @@ class NimBLEScan; * * When we perform a %BLE scan, the result will be a set of devices that are advertising. This * class provides a model of a detected device. + * + * The getChannel() method returns the primary advertising channel (37, 38, or 39) on which the + * advertisement was received, or 0xFF if the channel information is not available from the controller. */ class NimBLEAdvertisedDevice { public: @@ -120,6 +123,7 @@ class NimBLEAdvertisedDevice { size_t getPayloadLength(); uint8_t getAddressType(); time_t getTimestamp(); + uint8_t getChannel(); bool isAdvertisingService(const NimBLEUUID &uuid); bool haveAppearance(); bool haveManufacturerData(); @@ -149,6 +153,7 @@ class NimBLEAdvertisedDevice { void setAdvType(uint8_t advType, bool isLegacyAdv); void setPayload(const uint8_t *payload, uint8_t length, bool append); void setRSSI(int rssi); + void setChannelIndex(uint8_t channel) { m_channelIndex = channel; } #if CONFIG_BT_NIMBLE_EXT_ADV void setSetId(uint8_t sid) { m_sid = sid; } void setPrimaryPhy(uint8_t phy) { m_primPhy = phy; } @@ -164,6 +169,7 @@ class NimBLEAdvertisedDevice { time_t m_timestamp; bool m_callbackSent; uint8_t m_advLength; + uint8_t m_channelIndex; #if CONFIG_BT_NIMBLE_EXT_ADV bool m_isLegacyAdv; uint8_t m_sid; diff --git a/src/NimBLEScan.cpp b/src/NimBLEScan.cpp index e45da4481..eb2e24a5b 100644 --- a/src/NimBLEScan.cpp +++ b/src/NimBLEScan.cpp @@ -130,6 +130,7 @@ NimBLEScan::~NimBLEScan() { advertisedDevice->m_timestamp = time(nullptr); advertisedDevice->setRSSI(disc.rssi); + advertisedDevice->setChannelIndex(disc.channel_index); advertisedDevice->setPayload(disc.data, disc.length_data, (isLegacyAdv && event_type == BLE_HCI_ADV_RPT_EVTYPE_SCAN_RSP)); diff --git a/src/nimble/nimble/host/include/host/ble_gap.h b/src/nimble/nimble/host/include/host/ble_gap.h index 9f4b1ee47..1289ad468 100644 --- a/src/nimble/nimble/host/include/host/ble_gap.h +++ b/src/nimble/nimble/host/include/host/ble_gap.h @@ -403,6 +403,9 @@ struct ble_gap_ext_disc_desc { * set (BLE_ADDR_ANY otherwise). */ ble_addr_t direct_addr; + + /** Primary advertising channel index (0xFF if unavailable) */ + uint8_t channel_index; }; #endif @@ -433,6 +436,9 @@ struct ble_gap_disc_desc { * event type (BLE_ADDR_ANY otherwise). */ ble_addr_t direct_addr; + + /** Primary advertising channel index (0xFF if unavailable) */ + uint8_t channel_index; }; struct ble_gap_repeat_pairing { diff --git a/src/nimble/nimble/host/src/ble_hs_hci_evt.c b/src/nimble/nimble/host/src/ble_hs_hci_evt.c index f8d52d583..6af9ed977 100644 --- a/src/nimble/nimble/host/src/ble_hs_hci_evt.c +++ b/src/nimble/nimble/host/src/ble_hs_hci_evt.c @@ -465,7 +465,7 @@ ble_hs_hci_evt_le_adv_rpt_first_pass(const void *data, unsigned int len) rpt = data; len -= sizeof(*rpt) + 1; - data += sizeof(rpt) + 1; + data += sizeof(*rpt) + 1; if (rpt->data_len > len) { return BLE_HS_ECONTROLLER; @@ -501,11 +501,12 @@ ble_hs_hci_evt_le_adv_rpt(uint8_t subevent, const void *data, unsigned int len) data += sizeof(*ev); desc.direct_addr = *BLE_ADDR_ANY; + desc.channel_index = 0xFF; for (i = 0; i < ev->num_reports; i++) { rpt = data; - data += sizeof(rpt) + rpt->data_len + 1; + data += sizeof(*rpt) + rpt->data_len + 1; desc.event_type = rpt->type; desc.addr.type = rpt->addr_type; @@ -513,6 +514,10 @@ ble_hs_hci_evt_le_adv_rpt(uint8_t subevent, const void *data, unsigned int len) desc.length_data = rpt->data_len; desc.data = rpt->data; desc.rssi = rpt->data[rpt->data_len]; + /* Channel index follows RSSI if available */ + if ((const uint8_t*)data - (const uint8_t*)rpt > sizeof(*rpt) + rpt->data_len + 1) { + desc.channel_index = rpt->data[rpt->data_len + 1]; + } ble_gap_rx_adv_report(&desc); } @@ -535,6 +540,7 @@ ble_hs_hci_evt_le_dir_adv_rpt(uint8_t subevent, const void *data, unsigned int l /* Data fields not present in a direct advertising report. */ desc.data = NULL; desc.length_data = 0; + desc.channel_index = 0xFF; for (i = 0; i < ev->num_reports; i++) { desc.event_type = ev->reports[i].type; @@ -612,6 +618,7 @@ ble_hs_hci_evt_le_ext_adv_rpt(uint8_t subevent, const void *data, report = &ev->reports[0]; for (i = 0; i < ev->num_reports; i++) { memset(&desc, 0, sizeof(desc)); + desc.channel_index = 0xFF; desc.props = (report->evt_type) & 0x1F; if (desc.props & BLE_HCI_ADV_LEGACY_MASK) { @@ -649,6 +656,14 @@ ble_hs_hci_evt_le_ext_adv_rpt(uint8_t subevent, const void *data, desc.prim_phy = report->pri_phy; desc.sec_phy = report->sec_phy; desc.periodic_adv_itvl = report->periodic_itvl; + /* Channel index may follow the data if available */ + if (report->data_len < 255) { + const uint8_t *channel_ptr = &report->data[report->data_len]; + /* Verify we're not reading past the event data */ + if ((const uint8_t*)channel_ptr < (const uint8_t*)data + len) { + desc.channel_index = *channel_ptr; + } + } ble_gap_rx_ext_adv_report(&desc);