Skip to content

Class B beacon search / ping slots not disabled, and Class C not properly restored, after last multicast session stops #156

@aelbretonactility

Description

@aelbretonactility

Summary

When the last multicast Class B session stops (by timer expiry), Class B beacon search and ping
slots are not disabled — they keep running indefinitely. For Class C, two related issues exist
in the original code: a Class A device does not revert to Class A after the session ends; and a
native Class C device would be incorrectly switched off Class C entirely rather than returning
to its unicast RX2 frequency and data rate.

Affected components

  • lbm_lib/smtc_modem_core/lorawan_packages/remote_multicast_setup/v2.0.0/lorawan_remote_multicast_setup_package_v2.0.0.c
  • lbm_lib/smtc_modem_core/lorawan_packages/remote_multicast_setup/v1.0.0/lorawan_remote_multicast_setup_package_v1.0.0.c

Root cause

The Remote Multicast Setup package correctly manages session start and stop using a
timer-based scheduler, but there is an asymmetry between session start and session stop.

Class B

Session startlorawan_class_b_management_enable called at line 615 in the scheduling
section of service_on_update; lorawan_api_multicast_b_start_session called at line 458
inside the LAUNCH_CLASS_B_TASK handler in v2.0.0:

lorawan_class_b_management_enable( stack_id, true, delay_class_b_s, true );  // enables beacon search
lorawan_api_multicast_b_start_session( ... );

Session stopSTOP_CLASS_B_TASK handler in original code, lines 490–502 in v2.0.0:

lorawan_api_multicast_b_stop_session( mc_grp_id, stack_id );
// ... check if all sessions done ...
if( tmp == 0 )
{
    // ← lorawan_class_b_management_enable( false ) MISSING in original code
    increment_asynchronous_msgnumber( SMTC_MODEM_EVENT_NO_MORE_MULTICAST_SESSION_CLASS_B, ... );
}

lorawan_class_b_management_enable( stack_id, false, 0, true ) is only called in one place in
the entire packages directory: when switching from Class B to Class C multicast (line 399 in
v2.0.0). There is no call to disable Class B when all multicast B sessions end normally.

Class C

Session startLAUNCH_CLASS_C_TASK handler in original code (line 400 in v2.0.0):

lorawan_class_b_management_enable( stack_id, false, 0, true );
lorawan_api_class_c_enabled( true, stack_id );
lorawan_api_multicast_c_start_session( ... );

Session stopSTOP_CLASS_C_TASK handler in original code: calls
lorawan_api_multicast_c_stop_session() but never calls lorawan_api_class_c_enabled( false ).
This means a Class A device remains in continuous Class C receive mode after the session ends.

A naive fix of adding lorawan_api_class_c_enabled( false ) unconditionally would break native
Class C devices in the opposite direction: it would completely shut down Class C — including the
unicast RX2 window — rather than restoring the device to unicast Class C on its original RX2
frequency and data rate. The multicast session may use a different frequency and DR than the
unicast RX2 channel, so the correct handling depends on whether Class C was already active
before the multicast session started.

The problem is present in both v1.0.0 and v2.0.0 of the package.

Impact

  • Class B: after all multicast Class B sessions expire, the device continues to search for
    beacons and open ping slot receive windows, wasting battery and RF resources indefinitely.
    This persists until the device is reset or Class B is explicitly disabled by the application.
  • Class C / Class A device: remains in continuous receive mode after the multicast session
    ends, causing permanently elevated current consumption.
  • Class C / native Class C device: if lorawan_api_class_c_enabled( false ) were called
    unconditionally as a naive fix, the device would lose its unicast Class C RX2 window entirely
    and revert to Class A behaviour.
  • In all cases the application receives the NO_MORE_MULTICAST_SESSION_CLASS_B/C event but has
    no documented obligation to manually repair the transport mode — the stack is expected to
    manage its own lifecycle.

Steps to reproduce

Class B

  1. Start a multicast Class B session via Remote Multicast Setup package commands.
  2. Wait for the session to run to its natural timeout (session_time + time_out).
  3. Observe: SMTC_MODEM_EVENT_NO_MORE_MULTICAST_SESSION_CLASS_B fires, but beacon search and
    ping slots remain active (visible in radio activity or power trace).

Class C — Class A device

  1. Configure a Class A device and start a multicast Class C session.
  2. Wait for the session timeout.
  3. Observe: SMTC_MODEM_EVENT_NO_MORE_MULTICAST_SESSION_CLASS_C fires, but the device stays in
    Class C (continuous RX window open) instead of reverting to Class A.

Class C — native Class C device

  1. Configure a native Class C device and start a multicast Class C session on a different
    frequency / DR than the unicast RX2 channel.
  2. Wait for the session timeout.
  3. Observe: device continues listening on the multicast frequency instead of the RX2 frequency
    and DR (unicast Class C window is never restored).

Fix

Class B

In the STOP_CLASS_B_TASK handler, when tmp == 0, add lorawan_class_b_management_enable
disable call before the event, symmetric with what was enabled at session start:

if( tmp == 0 )
{
    lorawan_class_b_management_enable( stack_id, false, 0, true );  // ← added
    increment_asynchronous_msgnumber( SMTC_MODEM_EVENT_NO_MORE_MULTICAST_SESSION_CLASS_B, 0, stack_id );
}

Class C

The fix requires distinguishing between a Class A device and a native Class C device at session
stop time.

Step 1 — add bool class_c_running_before_session to
lorawan_remote_multicast_setup_package_ctx_t. It is zeroed by the memset at init.

Step 2 — in LAUNCH_CLASS_C_TASK, capture whether Class C was already running before
calling lorawan_api_class_c_enabled( true ):

lorawan_class_b_management_enable( stack_id, false, 0, true );
if( lorawan_api_class_c_is_running( stack_id ) )
{
    ctx->class_c_running_before_session = true;  // ← added: native Class C device
}
lorawan_api_class_c_enabled( true, stack_id );
lorawan_api_multicast_c_start_session( ... );

Step 3 — in STOP_CLASS_C_TASK when tmp == 0, replace the missing disable call with
conditional teardown:

if( tmp == 0 )
{
    if( ctx->class_c_running_before_session == false )
    {
        // Device was Class A: fully disable Class C
        lorawan_api_class_c_enabled( false, stack_id );
    }
    else
    {
        // Device was natively Class C: unicast RX2 is already restored automatically.
        // lorawan_api_multicast_c_stop_session() above re-enabled the unicast session
        // and issued rp_task_abort(), which triggers lr1mac_class_c_launch() to reload
        // lr1_mac->rx2_data_rate / rx2_frequency. No further action needed.
        ctx->class_c_running_before_session = false;
    }
    increment_asynchronous_msgnumber( SMTC_MODEM_EVENT_NO_MORE_MULTICAST_SESSION_CLASS_C, 0, stack_id );
}

The restore of unicast Class C frequency and DR for native Class C devices is automatic. When
lorawan_api_multicast_c_stop_session() stops the last multicast session,
lr1mac_class_c_multicast_stop_session() re-enables rx_session_param[RX_SESSION_UNICAST] and
calls rp_task_abort(). The RP abort callback re-enqueues via lr1mac_class_c_launch(), which
unconditionally reloads unicast parameters from lr1_mac->rx2_data_rate and
lr1_mac->rx2_frequency — the original unicast RX2 values that multicast sessions never
modify. This works correctly as long as class_c_obj->enabled remains true, which it does
because we no longer call lorawan_api_class_c_enabled( false ) for native Class C devices.

Both changes are applied in lorawan_remote_multicast_setup_package_v2.0.0.c and
lorawan_remote_multicast_setup_package_v1.0.0.c.

Notes

  • This issue exists independently of this issue (multicast sessions not stopped on FUOTA done). Even
    on the normal timer-expiry path (without FUOTA), Class B/C is not correctly handled after the
    last session stops. Both issues should be fixed together.
  • The force_stop_class_b and force_stop_class_c functions introduced as part of this issue
    fix reuse the same tmp == 0 logic and include the same conditional teardown, so the FUOTA
    completion path and the timer-expiry path behave consistently.
  • The class_c_running_before_session flag is only ever set to true and never cleared during
    an active session, so multiple concurrent multicast groups on a native Class C device are
    handled correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions