Skip to content

Characteristic Aggregate Format Descriptor (0x2905) support for Peripheral device#1054

Open
srgg wants to merge 1 commit into
h2zero:masterfrom
srgg:feat/aggregate-format-2905
Open

Characteristic Aggregate Format Descriptor (0x2905) support for Peripheral device#1054
srgg wants to merge 1 commit into
h2zero:masterfrom
srgg:feat/aggregate-format-2905

Conversation

@srgg

@srgg srgg commented Oct 24, 2025

Copy link
Copy Markdown
Contributor

Summary

This PR adds full support for the Characteristic Aggregate Format Descriptor (UUID 0x2905) when operating as a BLE peripheral. Correct implementation requires reliable handle mapping for multiple Presentation Format descriptors (0x2904), which previously failed due to duplicate handle assignment.

This PR also introduces PlatformIO build support for the existing examples and includes a usage demonstration of this new capability in NimBLE_Server.ino.

Problem

Descriptor handle resolution used UUID-only lookup (ble_gatts_find_dsc()), which meant:

  • Multiple same-UUID descriptors were assigned the same handle
  • The Aggregate Format descriptor (0x2905) could not assemble a correct referenced handle list
  • Aggregated read/write behavior was broken for peripheral services that include multiple presentation formats

The BLE specification requires each referenced descriptor to have a unique handle for valid aggregation.

Fix

  • Reworked descriptor handle assignment to follow descriptor registration order from ble_gatt_dsc_def
  • Each descriptor instance now retains the unique handle assigned by the NimBLE stack
  • Aggregate Format descriptor correctly lists all Presentation Format descriptors associated with a characteristic

Additional Enhancements

  • PlatformIO support added for all example projects
    They still function as regular Arduino sketches
    and can now also be built and flashed using PlatformIO:
 pio run -e example_server -t upload 
  • Updated the NimBLE_Server.ino example to include a practical usage example of the Aggregate Format descriptor feature:
  /**
     *  2905 “Aggregate Format” descriptor is a special case. When create2905() is called,
     *  it creates an instance of NimBLE2905 with the correct properties and size.
     *  We must then explicitly add the constituent 2904 descriptors that define the
     *  aggregate format (e.g., name, fat percent, weight) using add2904Descriptor().
     *  This ensures the aggregate descriptor correctly references all its component descriptors.
     */
    NimBLE2905* pFormatAggregate = pBurgerIngredientsCharacteristic->create2905();
    pFormatAggregate->add2904Descriptor(pFormatName);
    pFormatAggregate->add2904Descriptor(pFormatFatPercent);
    pFormatAggregate->add2904Descriptor(pFormatWeight);
  • Added a configuration macro for controlling maximum aggregated descriptors:
    /** Uncomment to change the maximum number of aggregated presentation format descriptors; 5 by default. */
    // #define NIMBLE_MAX_AGGREGATE_FORMAT_DESCRIPTORS 5

Impact

  • Standards-compliant support for 0x2905 on NimBLE peripherals
  • Reliable operation for multiple Presentation Format descriptors (0x2904)
  • Backwards compatible with existing applications and development workflows
  • Easier testing, debugging, and example execution with PlatformIO

Summary by CodeRabbit

  • New Features
    • Added support for the Characteristic Aggregate Format descriptor (UUID 0x2905).
    • Applications can create aggregate descriptors and reference multiple presentation format descriptors.
    • Aggregate values are populated automatically when the server starts.
    • Added configurable limits for the number of referenced descriptors.

@srgg
srgg force-pushed the feat/aggregate-format-2905 branch from ccf45e6 to ec85ea8 Compare October 24, 2025 13:25
@h2zero

h2zero commented Oct 24, 2025

Copy link
Copy Markdown
Owner

This looks really good, only issue I have is the changes to the core files. This would make the api incompatible with the esp-idf version of NimBLE, maybe there's another way.

@srgg

srgg commented Oct 24, 2025

Copy link
Copy Markdown
Contributor Author

@h2zero I don’t have much experience with ESP-IDF yet. Could you provide more details about the issue so I can explore alternative ways to address it?

Honestly, I was hesitant to modify ble_gatts directly, since changes are introduced there from time to time. At the moment, the approach I took felt like the most straightforward way to fix the problem.

@h2zero

h2zero commented Oct 24, 2025

Copy link
Copy Markdown
Owner

I try to avoid modifying the stack code unless it's to address something that won't break the API with the esp-idf repo. I maintain a separate cpp wrapper for esp-idf: https://github.com/h2zero/esp-nimble-cpp since the NimBLE stack is included already in esp-idf, whereas in this repo it is provided.

So in this case the 2905 class would not work with esp-idf as it would not have those core changes that are needed and the API would no longer be the same for both repos.

@srgg
srgg force-pushed the feat/aggregate-format-2905 branch from 1ab9dfd to dc01636 Compare November 30, 2025 14:00
@h2zero

h2zero commented Mar 9, 2026

Copy link
Copy Markdown
Owner

@srgg I think I have a solution to the stack change requirements for this.

Please see #1107

That should simplify this PR and class quite a bit.
POC:

    NimBLEDevice::init("NimBLE_Aggregate_Demo");
    NimBLEServer *pServer = NimBLEDevice::createServer();
    pServer->setCallbacks(&serverCallbacks);
    NimBLEService *pService = pServer->createService("ABCD");

    NimBLECharacteristic *pChar = pService->createCharacteristic(
        "1234",
        NIMBLE_PROPERTY::READ);

    // 1. Create First Presentation Format (e.g., Temperature)
    NimBLE2904 *pFormat1 = pChar->create2904();
    pFormat1->setFormat(NimBLE2904::FORMAT_SINT16);
    pFormat1->setUnit(0x272F); // Celsius

    // 2. Create Second Presentation Format (e.g., Humidity)
    NimBLE2904 *pFormat2 = pChar->create2904();
    pFormat2->setFormat(NimBLE2904::FORMAT_UINT8);
    pFormat2->setUnit(0x27AD); // Percentage

    // 3. Create the Aggregate Format Descriptor (0x2905)
    // UUID 0x2905 requires a list of 16-bit handles of the referenced 2904 descriptors.
    NimBLEDescriptor *pAggregate = pChar->createDescriptor((uint16_t)0x2905);

    pService->start();
    pServer->start();

    // 4. Update the Aggregate Value with actual handles
    // Handles are only guaranteed to be valid AFTER pService->start()
    uint16_t h1 = pFormat1->getHandle();
    uint16_t h2 = pFormat2->getHandle();

    uint8_t aggregateValue[4];
    aggregateValue[0] = h1 & 0xFF;
    aggregateValue[1] = (h1 >> 8) & 0xFF;
    aggregateValue[2] = h2 & 0xFF;
    aggregateValue[3] = (h2 >> 8) & 0xFF;

    pAggregate->setValue(aggregateValue, 4);
    auto value = pAggregate->getValue();
    printf("Aggregate Descriptor Value: %02X%02X%02X%02X\n", value[0], value[1], value[2], value[3]);
    NimBLEDevice::startAdvertising();
    ```

@h2zero

h2zero commented Mar 16, 2026

Copy link
Copy Markdown
Owner

@srgg #1107 has been merged if you want to refactor and rebase this PR

@srgg
srgg force-pushed the feat/aggregate-format-2905 branch from dc01636 to 5e4ce61 Compare July 17, 2026 22:04
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds pirate-ready support for the Bluetooth Characteristic Aggregate Format descriptor (UUID 0x2905), including descriptor creation, 2904 handle aggregation, configurable capacity, and post-startup value initialization.

Changes

Aggregate Format descriptor

Layer / File(s) Summary
Descriptor contract and aggregation
src/NimBLE2905.h, src/NimBLE2905.cpp, src/nimconfig.h
Defines NimBLE2905, aggregates 2904 descriptor handles, initializes its value after handles exist, and documents the configurable capacity.
Characteristic descriptor creation
src/NimBLECharacteristic.h, src/NimBLECharacteristic.cpp
Adds the public factory API and creates UUID 0x2905 descriptors through NimBLECharacteristic.
Startup handle initialization
src/NimBLEServer.cpp
Scans descriptors after GATT startup and initializes each Aggregate Format descriptor value.ka

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant NimBLECharacteristic
  participant NimBLEServer
  participant NimBLE2905
  Application->>NimBLECharacteristic: create2905()
  NimBLECharacteristic-->>Application: NimBLE2905*
  NimBLECharacteristic->>NimBLE2905: add2904Descriptor()
  NimBLEServer->>NimBLEServer: ble_gatts_start()
  NimBLEServer->>NimBLE2905: initValue()
  NimBLE2905-->>NimBLEServer: aggregate 2904 handles
Loading

Possibly related PRs

Poem

Arrr, 2905 sets sail,
With 2904 handles in its trail.
GATT assigns the map,
The descriptor fills its cap,
And aggregate bytes won’t fail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly names the new 0x2905 peripheral support added in this changeset, matey.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Ports the 0x2905 Characteristic Aggregate Format feature (from feat/aggregate-format-2905 @ dc01636, 2.3.6-based) onto NimBLE-Arduino 2.5.0.

Only the feature is carried: NimBLE2905 descriptor class, NimBLECharacteristic::create2905(), the 0x2905 branch in createDescriptor(), the NIMBLE_MAX_AGGREGATE_FORMAT_DESCRIPTORS knob, and the post-start initValue() pass in NimBLEServer::start().

The fork's descriptor-handle-assignment fix (Phase-1 + the ble_gatts_find_dsc dsc_arg change) is intentionally dropped: 2.5.0 already contains upstream h2zero#1107 ("Properly set attribute handles"), which owns duplicate-safe handle assignment via the GATT register callback. So the host-C is left untouched.
@srgg
srgg force-pushed the feat/aggregate-format-2905 branch from 5e4ce61 to 1ff1c68 Compare July 17, 2026 22:07
@srgg

srgg commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@h2zero, excuse me for the long pause; it has been rebased

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/NimBLE2905.cpp`:
- Around line 40-61: Update NimBLE2905::initValue to replace the VLA
aggregatedHandles with a standard fixed-capacity array, and size the value
passed to setValue for the number of valid entries. When a descriptor handle is
zero, skip it with continue rather than returning, while preserving the cleanup
that clears and shrinks m_vAggregatedDescriptors on every path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4860e599-d3ad-442a-8e16-e41e6c3dcf15

📥 Commits

Reviewing files that changed from the base of the PR and between a9eab2f and 1ff1c68.

📒 Files selected for processing (6)
  • src/NimBLE2905.cpp
  • src/NimBLE2905.h
  • src/NimBLECharacteristic.cpp
  • src/NimBLECharacteristic.h
  • src/NimBLEServer.cpp
  • src/nimconfig.h

Comment thread src/NimBLE2905.cpp
Comment on lines +40 to +61
void NimBLE2905::initValue() {
const size_t count = m_vAggregatedDescriptors.size();
uint16_t aggregatedHandles[count];

for (size_t i = 0; i < count; ++i) {
auto* desc = m_vAggregatedDescriptors[i];
uint16_t handle = desc->getHandle();

if (handle == 0) {
NIMBLE_LOGE(LOG_TAG, "Failed to initialize value: presentation format descriptor handle is not initialized");
return;
}

aggregatedHandles[i] = desc->getHandle();
} // initValue

setValue(reinterpret_cast<const uint8_t*>(aggregatedHandles), sizeof(aggregatedHandles));

// Presentation formats no longer needed, let's free some memory
m_vAggregatedDescriptors.clear();
m_vAggregatedDescriptors.shrink_to_fit();
} // initValue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Refactor to remove VLA and prevent memory hoarding on early return.

Shiver me timbers! Ye be usin' a Variable-Length Array (VLA) for aggregatedHandles, which be a compiler extension and not standard C++. Strict compilers will throw ye overboard! Worse yet, if ye hit a handle that's zero, yer early return skips the memory cleanup below, leavin' ye hoarding memory like a greedy pirate.

Let's patch both leaks by using a standard array sized to the maximum capacity and skippin' invalid handles with a continue instead of abandonin' ship!

🛠️ Proposed refactor for `initValue`
 void NimBLE2905::initValue() {
-    const size_t count = m_vAggregatedDescriptors.size();
-    uint16_t aggregatedHandles[count];
-
-    for (size_t i = 0; i < count; ++i) {
-        auto* desc = m_vAggregatedDescriptors[i];
-        uint16_t handle = desc->getHandle();
-
-        if (handle == 0) {
-            NIMBLE_LOGE(LOG_TAG, "Failed to initialize value: presentation format descriptor handle is not initialized");
-            return;
-        }
-
-        aggregatedHandles[i] = desc->getHandle();
-    } // initValue
-
-    setValue(reinterpret_cast<const uint8_t*>(aggregatedHandles), sizeof(aggregatedHandles));
+    uint16_t aggregatedHandles[NIMBLE_MAX_AGGREGATE_FORMAT_DESCRIPTORS];
+    size_t validCount = 0;
+
+    for (auto* desc : m_vAggregatedDescriptors) {
+        uint16_t handle = desc->getHandle();
+
+        if (handle == 0) {
+            NIMBLE_LOGE(LOG_TAG, "Failed to initialize value: presentation format descriptor handle is not initialized");
+            continue;
+        }
+
+        aggregatedHandles[validCount++] = handle;
+    }
+
+    if (validCount > 0) {
+        setValue(reinterpret_cast<const uint8_t*>(aggregatedHandles), validCount * sizeof(uint16_t));
+    }
 
     // Presentation formats no longer needed, let's free some memory
     m_vAggregatedDescriptors.clear();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void NimBLE2905::initValue() {
const size_t count = m_vAggregatedDescriptors.size();
uint16_t aggregatedHandles[count];
for (size_t i = 0; i < count; ++i) {
auto* desc = m_vAggregatedDescriptors[i];
uint16_t handle = desc->getHandle();
if (handle == 0) {
NIMBLE_LOGE(LOG_TAG, "Failed to initialize value: presentation format descriptor handle is not initialized");
return;
}
aggregatedHandles[i] = desc->getHandle();
} // initValue
setValue(reinterpret_cast<const uint8_t*>(aggregatedHandles), sizeof(aggregatedHandles));
// Presentation formats no longer needed, let's free some memory
m_vAggregatedDescriptors.clear();
m_vAggregatedDescriptors.shrink_to_fit();
} // initValue
void NimBLE2905::initValue() {
uint16_t aggregatedHandles[NIMBLE_MAX_AGGREGATE_FORMAT_DESCRIPTORS];
size_t validCount = 0;
for (auto* desc : m_vAggregatedDescriptors) {
uint16_t handle = desc->getHandle();
if (handle == 0) {
NIMBLE_LOGE(LOG_TAG, "Failed to initialize value: presentation format descriptor handle is not initialized");
continue;
}
aggregatedHandles[validCount++] = handle;
}
if (validCount > 0) {
setValue(reinterpret_cast<const uint8_t*>(aggregatedHandles), validCount * sizeof(uint16_t));
}
// Presentation formats no longer needed, let's free some memory
m_vAggregatedDescriptors.clear();
m_vAggregatedDescriptors.shrink_to_fit();
} // initValue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/NimBLE2905.cpp` around lines 40 - 61, Update NimBLE2905::initValue to
replace the VLA aggregatedHandles with a standard fixed-capacity array, and size
the value passed to setValue for the number of valid entries. When a descriptor
handle is zero, skip it with continue rather than returning, while preserving
the cleanup that clears and shrinks m_vAggregatedDescriptors on every path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants