BLE event handlers, why so many! Plz explain structure!

Afternoon folks,

I'm new to nordic, previously a TI developer. I'm still getting my head around the SDK. I've unfortunately been left with some poorly organised, very-un-annotated firmware to try and decipher. I am currently trying to get my head around the system for handling BLE events:

I'm new to BLE terminology and subsequently i have tried to find best terms to express my understanding, please bare with me:

The code i have. Runs 3 event handlers every time a device is connected (for example). In no particular order, these are: a generic "ble_evt_handler", the register observer for this is set up during BLE stack initialisation. The application also runs a "ble_cus" handler, the register observer for this is set up during a separate initialisation function, before the main superloop. This second handler, contains a function that sets another event, triggering a third event handler "ble_cus_evt_handler" this handler is seemingly associated with the individual service during the initialisation of the service. 

I have seen a similar structure implemented on a few examples, but these examples seemed to split events across the handlers, not pile all events blindly through all 3 handlers. 

1: What is the difference between all of the "smaller", more specific handlers, such as "gatt_evt_handler", "hrs_evt_handler" etc. and the seemingly more global "ble_evt_handler"?

2: Why do these "smaller" handlers need to contain functions that trigger a secondary handler, can the first handler not just service the event itself?

3: Do i really need 3 handlers for a simple event?

4: I come from a background of "simplicity leads to functioning reliable code" - If i were to rationalise this, knowing the application will likely not have a great number of different events to process, is there some way i can do without one or two of these handlers? 

Many thanks,

Sean Howson

-nRf52840 DK, SDK17.0.2, S140, segger embedded studio, windows 10.

Parents
  • Hi Guys,

    Thank you both for your replies and thanks for answering all my questions.

    After posting i did find the app_uart project and actually found some annotations in the code i have that had clearly been copied and pasted from the uart project.

    Thanks for breaking down the different types of handler.

    So to add clarity to my previous question:

    3: Do i really need 3 handlers for a simple event?

    What i should have said was:

    Do i really need to run a handler, constaining an enormous switch statment, the each of the cases for which, runs a function which converts the event to a custom event type (yet still identical in it's raw value) and then redirects the execution, to the next handler.... the next handler also containing an equally large and identical switch statement, the cases for this statement ACTUALLY handle the event. ALSO the event is handled completely separately by the ble_event_handler.

    For now, i have removed most of the gubbins from the handler registered as an observer and simply passed the event to the secondary handler (without any form of sorting/processing)

    I now understand the origins of the re-direct and the way each handler is aimed at a specific subgroup of events (illustrated by their names i suppose).

    I can understand why there would be several of the "smaller" handlers running for different categories of event. I just can't see a reason to process, for example, the "on connect" event, in both the "big" ble_evt_handler AND the "small" ble_gap_evt_handler, seems to lack organisation/clarity?

    General opinion question here:

    Some of my issues are currently stemming from the multiple file types present in the project. A number of the main handler files have been written in Cpp,  with a "be careful not to make more than one instance of each class" - notice issued against each of them. For me this set off red flags.

    The previous author came from a non industrial background and used cpp because it was the language that made most sense to him, but the more i read through the code, the more it seems to be cramming a round peg into a square hole. All sorts of things have been modified, to try to accomadate the cpp code.

    Is this normal practice within the Nordic world? It's certainly not been normal for me in previous roles.

    Thanks,

    Sean

    P.S. for now i think you've clarified enough for me that i will avoid posting any code, but i appreciate the offer.

  • Hi Sean

    Thanks to Markku for the clarification. I thought "cus" sounded familiar, but when I didn't find it in the SDK I stopped searching for it. 

    SeanHowsonTB said:
    Do i really need to run a handler, constaining an enormous switch statment, the each of the cases for which, runs a function which converts the event to a custom event type (yet still identical in it's raw value) and then redirects the execution, to the next handler.... the next handler also containing an equally large and identical switch statement, the cases for this statement ACTUALLY handle the event. ALSO the event is handled completely separately by the ble_event_handler.

    No, running the switch statement twice in a nested fashion makes little sense to me, and to my knowledge this is not done by any of the standard examples. The normal approach is for the top level handler to do the filtering, and cast the event to the specific event type (which as you indicate is not a conversion per se, just a type cast), and then forward the event to a more specific handler that is only used for a single type of event, or a small subset of events. 

    The code snippet below is from the ble_nus.c implementation, which does it in this way for events relevant to the NUS service: 

    void ble_nus_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
    {
        if ((p_context == NULL) || (p_ble_evt == NULL))
        {
            return;
        }
    
        ble_nus_t * p_nus = (ble_nus_t *)p_context;
    
        switch (p_ble_evt->header.evt_id)
        {
            case BLE_GAP_EVT_CONNECTED:
                on_connect(p_nus, p_ble_evt);
                break;
    
            case BLE_GATTS_EVT_WRITE:
                on_write(p_nus, p_ble_evt);
                break;
    
            case BLE_GATTS_EVT_HVN_TX_COMPLETE:
                on_hvx_tx_complete(p_nus, p_ble_evt);
                break;
    
            default:
                // No implementation needed.
                break;
        }
    }
    

    In this case the casting of the event is handled by the on_write(), on_connect() and on_hvx_tx_complete() functions, but since the event type is checked by the top level handler it is not checked again in the event specific handler. 

    SeanHowsonTB said:
    I just can't see a reason to process, for example, the "on connect" event, in both the "big" ble_evt_handler AND the "small" ble_gap_evt_handler, seems to lack organisation/clarity?

    I agree. Within one file or module each event should only be processed once, but you could have multiple files that all have to process the connected event, which is normal. 

    SeanHowsonTB said:
    Is this normal practice within the Nordic world? It's certainly not been normal for me in previous roles.

    Using C++ is very rare for Nordic projects, but it happens from time to time. It is definitely not something I recommend unless you have previous experience using C++ in embedded systems, and have a good reason for doing so. 

    Many of the drivers and modules in the SDK are instance based, which would make them well suited to be re-implemented in an object oriented language like C++, but all the time the entire SDK is written in C it is quite a bit of work to adapt it to C++. 

    Also, many of the modules in the SDK won't build in C++ because they use syntax structures that are only supported in C (those do exist unforunately), and in this case the best workaround is to only call these libraries from C files, or rewriting the library files to be C++ compliant, which I would strongly discourage. 

    Best regards
    Torbjørn

Reply
  • Hi Sean

    Thanks to Markku for the clarification. I thought "cus" sounded familiar, but when I didn't find it in the SDK I stopped searching for it. 

    SeanHowsonTB said:
    Do i really need to run a handler, constaining an enormous switch statment, the each of the cases for which, runs a function which converts the event to a custom event type (yet still identical in it's raw value) and then redirects the execution, to the next handler.... the next handler also containing an equally large and identical switch statement, the cases for this statement ACTUALLY handle the event. ALSO the event is handled completely separately by the ble_event_handler.

    No, running the switch statement twice in a nested fashion makes little sense to me, and to my knowledge this is not done by any of the standard examples. The normal approach is for the top level handler to do the filtering, and cast the event to the specific event type (which as you indicate is not a conversion per se, just a type cast), and then forward the event to a more specific handler that is only used for a single type of event, or a small subset of events. 

    The code snippet below is from the ble_nus.c implementation, which does it in this way for events relevant to the NUS service: 

    void ble_nus_on_ble_evt(ble_evt_t const * p_ble_evt, void * p_context)
    {
        if ((p_context == NULL) || (p_ble_evt == NULL))
        {
            return;
        }
    
        ble_nus_t * p_nus = (ble_nus_t *)p_context;
    
        switch (p_ble_evt->header.evt_id)
        {
            case BLE_GAP_EVT_CONNECTED:
                on_connect(p_nus, p_ble_evt);
                break;
    
            case BLE_GATTS_EVT_WRITE:
                on_write(p_nus, p_ble_evt);
                break;
    
            case BLE_GATTS_EVT_HVN_TX_COMPLETE:
                on_hvx_tx_complete(p_nus, p_ble_evt);
                break;
    
            default:
                // No implementation needed.
                break;
        }
    }
    

    In this case the casting of the event is handled by the on_write(), on_connect() and on_hvx_tx_complete() functions, but since the event type is checked by the top level handler it is not checked again in the event specific handler. 

    SeanHowsonTB said:
    I just can't see a reason to process, for example, the "on connect" event, in both the "big" ble_evt_handler AND the "small" ble_gap_evt_handler, seems to lack organisation/clarity?

    I agree. Within one file or module each event should only be processed once, but you could have multiple files that all have to process the connected event, which is normal. 

    SeanHowsonTB said:
    Is this normal practice within the Nordic world? It's certainly not been normal for me in previous roles.

    Using C++ is very rare for Nordic projects, but it happens from time to time. It is definitely not something I recommend unless you have previous experience using C++ in embedded systems, and have a good reason for doing so. 

    Many of the drivers and modules in the SDK are instance based, which would make them well suited to be re-implemented in an object oriented language like C++, but all the time the entire SDK is written in C it is quite a bit of work to adapt it to C++. 

    Also, many of the modules in the SDK won't build in C++ because they use syntax structures that are only supported in C (those do exist unforunately), and in this case the best workaround is to only call these libraries from C files, or rewriting the library files to be C++ compliant, which I would strongly discourage. 

    Best regards
    Torbjørn

Children
No Data
Related