Skip to content

Partnered control functions are evicted by address-claim roll-calls, and restored CFs are immediately re-evicted #717

Description

@Arjan-Woltjer

Describe the bug

Two related defects in the address-claim roll-call path cause a PartneredControlFunction to be evicted from controlFunctionTable while a VT or TC client is actively connected to it, and then to be re-evicted on every subsequent roll-call. The practical result is that the client goes permanently deaf to a partner that never stopped transmitting.

Both are present on current main (checked today against isobus/src/can_network_manager.cpp).

This is adjacent to #584 but distinct. #584 covers the initial adoption of an already-active partner in update_new_partners() (I have a fix for that one in AgIsoStack-Arduino#16). Even with that fixed, the two defects below still bite, because they recur for the lifetime of the connection rather than only at bind time.


1. prune_inactive_control_functions() prunes Partnered CFs

Only Internal control functions are exempt:

auto controlFunction = tableEntry;
if (ControlFunction::Type::Internal != controlFunction->get_type())
{
    inactiveControlFunctions.push_back(controlFunction);
    LOG_INFO("[NM]: Control function with address %u and NAME %016llx is now offline on channel %u.", ...);
    tableEntry = nullptr;
    controlFunction->address = NULL_CAN_ADDRESS;
}

Any node broadcasting a PGN 60928 (Address Claimed) request clears claimedAddressSinceLastAddressClaimRequest on every tracked CF and starts the 755 ms MAX_ADDRESS_CLAIM_RESOLUTION_TIME timer. A partner that does not re-announce inside that window is evicted — out from under a live, working client session.

Whether it re-announces in time is largely luck. In our captures the same terminal survived one roll-call and not the next.

Why eviction is fatal rather than cosmetic. Once tableEntry is nulled, message.get_source_control_function() no longer resolves for that address, and process_can_message_for_global_and_partner_callbacks() gates global dispatch on exactly that:

if ((nullptr == messageDestination) &&
    ((nullptr != messageSource) || ...))

So every subsequent broadcast from that address is silently dropped before reaching any global PGN callback. VirtualTerminalClient::process_rx_message is registered as a global callback, so the VT client stops seeing VT Status messages and times out on VT_STATUS_TIMEOUT_MS. TaskControllerClient sits in WaitForServerStatusMessage, which has no timeout at all and therefore cannot recover under any circumstances.

The failure looks exactly like the terminal going quiet. It isn't — the frames are still on the wire.

2. update_address_table() restores a pruned CF without restoring its liveness flag

if (targetControlFunction != nullptr)
{
    targetControlFunction->claimedAddressSinceLastAddressClaimRequest = true;
}
else
{
    // Look through all inactive CFs, maybe one of them has freshly claimed the address
    ...
    if (result != inactiveControlFunctions.end())
    {
        targetControlFunction = *result;
        LOG_DEBUG("[NM]: %s CF '%016llx' is now active at address '%d' on channel '%d'.", ...);
        process_control_function_state_change_callback(targetControlFunction, ControlFunctionState::Online);
    }
}

The restore branch never sets claimedAddressSinceLastAddressClaimRequest, so a CF that has been pruned once returns to the table already eligible for pruning again on the next roll-call. That is what turns a transient eviction into a self-sustaining cycle: we observed unbroken is now offline → has claimed address → is now offline churn every 1–2 seconds, running for 300+ seconds, with the VT and TC dead throughout.


Supporting Documentation

Found on a real bus: New Holland tractor, Ag Leader InCommand 1200 as VT/TC, plus the tractor's own terminal and the usual ECU population. Our own guidance-PGN traffic counter climbed steadily throughout every failure, so this was not a receive stall.

Counting evictions per window, before and after exempting Partnered CFs:

Window Evictions Partner lost?
single VT, quiet bus 0 — no prunes ran
single VT, prunes running 23 no (survived on timing)
two VTs 62 yes
one VT, busy bus 6 yes — died ~3 s after connect
after the fix, same config, 800 s 235 no — zero losses

Before the fix, the record for a held VT connection on that bus was roughly three seconds. After it, VT and TC stayed up together for 21+ minutes, including through a second terminal being powered on mid-session and enumerating the bus — an event that had previously killed a healthy session within about four seconds.

Environment

  • OS: bare metal, Teensy 4.1 (i.MX RT1062)
  • Compiler: arm-none-eabi-gcc (PlatformIO teensy platform), C++17
  • CAN driver: FlexCAN_T4 via FlexCANT4Plugin
  • Library: AgIsoStack-Arduino 0.1.5; both defects re-verified against AgIsoStack-plus-plus main before filing

Suggested fix

For (1), exempt Partnered alongside Internal. A partner is a CF the application explicitly bound to and is actively conversing with; a bus roll-call should not be able to evict it out from under a live session, and genuine partner loss is already detected at the right layer by each client's own status timeout. For (2), set the flag when restoring — by the time a CF is being restored it has just announced, so the flag is true by definition.

Happy to open a PR with both, plus a regression test if you can point me at where you would want it. I have deliberately kept these two changes separate from #584's so each stays attributable.

Activity

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

Metadata

Metadata

Assignees

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