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.
Describe the bug
Two related defects in the address-claim roll-call path cause a
PartneredControlFunctionto be evicted fromcontrolFunctionTablewhile 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 againstisobus/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 CFsOnly
Internalcontrol functions are exempt:Any node broadcasting a PGN 60928 (Address Claimed) request clears
claimedAddressSinceLastAddressClaimRequeston every tracked CF and starts the 755 msMAX_ADDRESS_CLAIM_RESOLUTION_TIMEtimer. 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
tableEntryis nulled,message.get_source_control_function()no longer resolves for that address, andprocess_can_message_for_global_and_partner_callbacks()gates global dispatch on exactly that:So every subsequent broadcast from that address is silently dropped before reaching any global PGN callback.
VirtualTerminalClient::process_rx_messageis registered as a global callback, so the VT client stops seeing VT Status messages and times out onVT_STATUS_TIMEOUT_MS.TaskControllerClientsits inWaitForServerStatusMessage, 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 flagThe 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 unbrokenis now offline→has claimed address→is now offlinechurn 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:
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
FlexCANT4PluginAgIsoStack-plus-plusmainbefore filingSuggested fix
For (1), exempt
PartneredalongsideInternal. 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.