[modbus] Rename send_pdu() to queue_pdu() (#18196)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Bonne Eggleston
2026-08-09 17:00:44 -05:00
committed by GitHub
co-authored by Claude J. Nick Koston
parent e9f428983e
commit ab12e5490f
8 changed files with 260 additions and 195 deletions
+4 -4
View File
@@ -134,7 +134,7 @@ TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
size_t total = 0;
for (int i = 0; i != n; i++) {
req[2] = static_cast<uint8_t>(i); // distinct start addresses: identical frames would dedup, not enqueue
total += sample([&] { device.send_pdu(req); }).count;
total += sample([&] { device.queue_pdu(req); }).count;
}
printf("HEAPPROBE queue_%d_typical_commands total_allocs=%zu\n", n, total);
EXPECT_EQ(total, 0u);
@@ -151,11 +151,11 @@ TEST(HeapProbe, WriteBehindQueuedReadsAppendsAllocationFree) {
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
for (int i = 0; i != 3; i++) {
req[2] = static_cast<uint8_t>(i); // distinct start addresses: identical frames would dedup, not enqueue
device.send_pdu(req);
device.queue_pdu(req);
}
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
Sample append = sample([&] { device.send_pdu(write_pdu); });
Sample append = sample([&] { device.queue_pdu(write_pdu); });
printf("HEAPPROBE write_append count=%zu bytes=%zu\n", append.count, append.bytes);
EXPECT_EQ(append.count, 0u);
}
@@ -180,7 +180,7 @@ TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) {
const uint8_t small_resp[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
auto round_trip = [&](std::span<const uint8_t> response_pdu) {
device.send_pdu(req);
device.queue_pdu(req);
hub.loop(); // transmit; the tx queue is empty during the measured receive below
uart.inject_frame(0x02, response_pdu);
return sample([&] { hub.loop(); }); // receive + parse + match + dispatch
+161 -125
View File
@@ -116,7 +116,7 @@ TEST(ModbusClientHubNoResponse, RetryRequeuesWaitingFrame) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/true);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
ASSERT_EQ(hub.queued_frames(), 1u);
hub.force_send_next();
ASSERT_EQ(hub.queued_frames(), 0u);
@@ -141,7 +141,7 @@ TEST(ModbusClientHubNoResponse, NoRetryDropsWaitingFrame) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/false);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.timeout_waiting();
@@ -157,7 +157,7 @@ TEST(ModbusClientHubNoResponse, DetachedDeviceIsNotNotified) {
NoResponseProbeHub hub;
{
RetryingDevice device(&hub, 0x02, /*retry=*/true);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
// device destructor clears its queue entries, including the waiting frame's device pointer
}
@@ -177,7 +177,7 @@ TEST(ModbusClientHubNoResponse, RetryBehindInterruptedShell) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/true);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
// A frame from the wrong address (0x07, expected 0x02) hits the unexpected-frame branch.
@@ -204,7 +204,7 @@ TEST(ModbusClientHubNoResponse, InterruptedShellDeclinedRetryRetiresOnRelease) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/false);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
@@ -229,7 +229,7 @@ TEST(ModbusClientHubNoResponse, MidCallbackClearCancelsRetry) {
NoResponseProbeHub hub;
ClearingRetryDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.timeout_waiting();
@@ -246,9 +246,9 @@ TEST(ModbusClientHubPriority, WritesSendBeforeQueuedReads) {
const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02};
const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02};
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
device.send_pdu(read_a);
device.send_pdu(read_b);
device.send_pdu(write_pdu);
device.queue_pdu(read_a);
device.queue_pdu(read_b);
device.queue_pdu(write_pdu);
ASSERT_EQ(hub.queued_frames(), 3u);
hub.force_send_next();
@@ -266,8 +266,8 @@ TEST(ModbusClientHubPriority, DuplicateQueuedFrameAbsorbedNotDuplicated) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/false);
device.send_pdu(read_pdu());
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
device.queue_pdu(read_pdu());
ASSERT_EQ(hub.queued_frames(), 1u);
EXPECT_EQ(hub.queued(0).pending, 2u); // one entry standing for two accepted requests
@@ -280,9 +280,9 @@ TEST(ModbusClientHubPriority, InFlightDuplicateRunsOnceMore) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/false);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
device.send_pdu(read_pdu()); // duplicate of the waiting frame
device.queue_pdu(read_pdu()); // duplicate of the waiting frame
EXPECT_EQ(hub.queued_frames(), 0u); // not queued twice
EXPECT_EQ(hub.waiting_command().pending, 2u);
@@ -303,9 +303,9 @@ TEST(ModbusClientHubPriority, AbsorbedRequestSurvivesDeviceRetry) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/true);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
device.send_pdu(read_pdu()); // duplicate of the waiting frame -> absorbed
device.queue_pdu(read_pdu()); // duplicate of the waiting frame -> absorbed
ASSERT_EQ(hub.waiting_command().pending, 2u);
hub.timeout_waiting(); // no response; the device requests a retry
@@ -459,8 +459,8 @@ TEST(ModbusClientHubPriority, WritesThenOneShotReadsThenContinuousPolls) {
device.read_holding_registers(0x100, 2, {.continuous = true});
const uint8_t one_shot[] = {0x03, 0x02, 0x00, 0x00, 0x01};
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
device.send_pdu(one_shot);
device.send_pdu(write_pdu);
device.queue_pdu(one_shot);
device.queue_pdu(write_pdu);
ASSERT_EQ(hub.queued_frames(), 3u);
EXPECT_EQ(hub.queued(0).priority(), CommandPriority::CONTINUOUS);
EXPECT_EQ(hub.queued(1).priority(), CommandPriority::READ);
@@ -482,7 +482,7 @@ TEST(ModbusClientHubPriority, ContinuousIgnoredForWrites) {
RetryingDevice device(&hub, 0x02, /*retry=*/false);
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
device.send_pdu(write_pdu, {.continuous = true});
device.queue_pdu(write_pdu, {.continuous = true});
ASSERT_EQ(hub.queued_frames(), 1u);
EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE);
EXPECT_FALSE(hub.queued(0).continuous);
@@ -530,8 +530,8 @@ TEST(ModbusClientHubPriority, DuplicateQueuedWriteRefused) {
SentCountingDevice device(&hub, 0x02);
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
EXPECT_TRUE(device.send_pdu(write_pdu));
EXPECT_FALSE(device.send_pdu(write_pdu)); // duplicate write: refused
EXPECT_TRUE(device.queue_pdu(write_pdu));
EXPECT_FALSE(device.queue_pdu(write_pdu)); // duplicate write: refused
hub.sweep_for_test();
ASSERT_EQ(hub.queued_frames(), 1u);
@@ -547,8 +547,8 @@ TEST(ModbusClientHubPriority, DuplicateCustomFunctionCodeRefused) {
SentCountingDevice device(&hub, 0x02);
const uint8_t custom_pdu[] = {0x41, 0x01, 0x02}; // user-defined function code
EXPECT_TRUE(device.send_pdu(custom_pdu));
EXPECT_FALSE(device.send_pdu(custom_pdu)); // duplicate custom command: refused
EXPECT_TRUE(device.queue_pdu(custom_pdu));
EXPECT_FALSE(device.queue_pdu(custom_pdu)); // duplicate custom command: refused
hub.sweep_for_test();
ASSERT_EQ(hub.queued_frames(), 1u);
@@ -562,8 +562,8 @@ TEST(ModbusClientHubPriority, AnonymousDuplicateDroppedNotPromoted) {
NoResponseProbeHub hub;
const uint8_t read[] = {0x03, 0x01, 0x00, 0x00, 0x02};
hub.send_pdu(0x02, read);
hub.send_pdu(0x02, read); // anonymous duplicate: dropped
hub.queue_pdu(0x02, read);
hub.queue_pdu(0x02, read); // anonymous duplicate: dropped
ASSERT_EQ(hub.queued_frames(), 1u);
EXPECT_EQ(hub.queued(0).pending, 1u); // never absorbed for a null owner
@@ -575,13 +575,13 @@ TEST(ModbusClientHubPriority, RetriedReadGoesBehindFreshReads) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/true);
device.send_pdu(read_pdu());
hub.force_send_next(); // the frame that will time out and retry
device.send_pdu(read_pdu()); // waiting duplicate: absorbed into the waiting entry
device.queue_pdu(read_pdu());
hub.force_send_next(); // the frame that will time out and retry
device.queue_pdu(read_pdu()); // waiting duplicate: absorbed into the waiting entry
const uint8_t fresh_a[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t fresh_b[] = {0x03, 0x00, 0x20, 0x00, 0x01};
device.send_pdu(fresh_a);
device.send_pdu(fresh_b);
device.queue_pdu(fresh_a);
device.queue_pdu(fresh_b);
ASSERT_EQ(hub.queued_frames(), 2u);
hub.timeout_waiting(); // device retries; the entry returns to READY behind the fresh reads
@@ -608,9 +608,9 @@ TEST(ModbusClientHubPriority, AbsorbedDuplicateKeepsPlaceInLine) {
const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01};
device.send_pdu(read_a);
device.send_pdu(read_b);
device.send_pdu(read_a); // duplicate of the older entry: absorbed, place unchanged
device.queue_pdu(read_a);
device.queue_pdu(read_b);
device.queue_pdu(read_a); // duplicate of the older entry: absorbed, place unchanged
ASSERT_EQ(hub.queued_frames(), 2u);
const ModbusDeviceCommand *next = hub.next_ready();
@@ -626,14 +626,14 @@ TEST(ModbusClientHubPriority, RetriedWriteKeepsWritePriorityAndStaysNonRequeueab
RetryingDevice device(&hub, 0x02, /*retry=*/true);
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
device.send_pdu(write_pdu);
device.queue_pdu(write_pdu);
hub.force_send_next();
hub.timeout_waiting(); // no response -> device requests retry -> back to READY
ASSERT_EQ(hub.queued_frames(), 1u);
EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE); // retry preserves the WRITE class
device.send_pdu(write_pdu); // duplicate of the retried write
device.queue_pdu(write_pdu); // duplicate of the retried write
ASSERT_EQ(hub.queued_frames(), 1u); // still not queued twice...
EXPECT_EQ(hub.queued(0).priority(), CommandPriority::WRITE);
hub.sweep_for_test();
@@ -655,7 +655,7 @@ TEST(ModbusClientHubSent, BlockedHubDefersInsteadOfFailing) {
AlwaysBlockedHub hub;
SentCountingDevice device(&hub, 0x02);
EXPECT_TRUE(device.send_pdu(read_pdu()));
EXPECT_TRUE(device.queue_pdu(read_pdu()));
hub.send_next_for_test();
EXPECT_EQ(device.sent_count_, 0);
@@ -673,7 +673,7 @@ TEST(ModbusClientHubSent, FiresOnWireNotOnQueue) {
hub.setup(); // frame timing derives from the baud rate
SentCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
EXPECT_EQ(device.sent_count_, 0); // queued only - nothing sent yet
hub.send_next_for_test();
@@ -703,7 +703,7 @@ TEST(ModbusClientHubSent, SendRejectedAfterDelayLeavesFrameReady) {
hub.setup();
SentCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.send_next_for_test(); // gate passes, send_frame_ rejects on the post-delay re-check
EXPECT_EQ(device.sent_count_, 0); // nothing transmitted
@@ -766,7 +766,7 @@ TEST(ModbusClientHubCallbackCount, SingleReadSingleCallback) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
drain_with_responses(hub, OK_RESPONSE);
EXPECT_EQ(device.data_count_, 1);
@@ -781,8 +781,8 @@ TEST(ModbusClientHubCallbackCount, DuplicateReadExactlyTwoCallbacks) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
device.queue_pdu(read_pdu());
int cycles = drain_with_responses(hub, OK_RESPONSE);
EXPECT_EQ(cycles, 2);
@@ -812,8 +812,8 @@ TEST(ModbusClientHubCallbackCount, ClearFromResponseResolvesDuplicateWithNotSent
NoResponseProbeHub hub;
ClearOnFirstResponseDevice device(&hub, 0x02);
EXPECT_TRUE(device.send_pdu(read_pdu()));
EXPECT_TRUE(device.send_pdu(read_pdu())); // absorbed: one entry, pending 2
EXPECT_TRUE(device.queue_pdu(read_pdu()));
EXPECT_TRUE(device.queue_pdu(read_pdu())); // absorbed: one entry, pending 2
hub.force_send_next();
hub.receive_frame_for_test(0x02, OK_RESPONSE); // response -> on_response -> clear, then sweep
@@ -829,9 +829,9 @@ TEST(ModbusClientHubCallbackCount, TripleReadRefusesTheThird) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
EXPECT_TRUE(device.send_pdu(read_pdu()));
EXPECT_TRUE(device.send_pdu(read_pdu()));
EXPECT_FALSE(device.send_pdu(read_pdu())); // the entry is already at its cap
EXPECT_TRUE(device.queue_pdu(read_pdu()));
EXPECT_TRUE(device.queue_pdu(read_pdu()));
EXPECT_FALSE(device.queue_pdu(read_pdu())); // the entry is already at its cap
hub.sweep_for_test();
EXPECT_EQ(device.not_sent_count_, 0); // refused synchronously, nothing owed
int cycles = drain_with_responses(hub, OK_RESPONSE);
@@ -849,8 +849,8 @@ TEST(ModbusClientHubCallbackCount, DuplicateWriteRefusedWithoutLifecycle) {
DataCountingDevice device(&hub, 0x02);
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
EXPECT_TRUE(device.send_pdu(write_pdu));
EXPECT_FALSE(device.send_pdu(write_pdu));
EXPECT_TRUE(device.queue_pdu(write_pdu));
EXPECT_FALSE(device.queue_pdu(write_pdu));
hub.sweep_for_test();
EXPECT_EQ(device.terminals(), 0); // the accepted write has not resolved; the other never existed
@@ -867,7 +867,7 @@ TEST(ModbusClientHubCallbackCount, ErrorResponseIsSoleTerminal) {
hub.setup();
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.send_next_for_test();
const uint8_t exception_response[] = {0x83, 0x02};
hub.receive_frame_for_test(0x02, exception_response);
@@ -886,7 +886,7 @@ TEST(ModbusClientHubCallbackCount, NoResponseIsSoleTerminalAndNotSentHasNoSent)
hub.setup();
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.send_next_for_test();
hub.timeout_waiting();
EXPECT_EQ(device.no_response_count_, 1);
@@ -896,8 +896,8 @@ TEST(ModbusClientHubCallbackCount, NoResponseIsSoleTerminalAndNotSentHasNoSent)
// Unabsorbable duplicate: the second identical write is refused at the door - no lifecycle, no
// terminal, nothing sent.
const uint8_t write_pdu[] = {0x06, 0x00, 0x10, 0xBE, 0xEF};
EXPECT_TRUE(device.send_pdu(write_pdu));
EXPECT_FALSE(device.send_pdu(write_pdu));
EXPECT_TRUE(device.queue_pdu(write_pdu));
EXPECT_FALSE(device.queue_pdu(write_pdu));
hub.sweep_for_test();
EXPECT_EQ(device.not_sent_count_, 0);
EXPECT_EQ(device.terminals(), 1); // still just the read's timeout
@@ -920,7 +920,7 @@ TEST(ModbusClientHubCallbackCount, RetryLifecyclesEachGetSentAndTerminal) {
DataCountingDevice device(&hub, 0x02);
device.retries_ = 1; // ask for exactly one retry
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.send_next_for_test();
hub.timeout_waiting(); // lifecycle 1: sent + no_response (retry requested -> re-queued)
ASSERT_EQ(hub.queued_frames(), 1u);
@@ -946,13 +946,13 @@ TEST(ModbusClientHubCallbackCount, RetryIsNeverRefusedByFullQueue) {
device.retries_ = 1;
SentCountingDevice filler(&hub, 0x05);
device.send_pdu(read_pdu());
hub.force_send_next(); // waiting
device.send_pdu(read_pdu()); // absorbed: two requests pending
device.queue_pdu(read_pdu());
hub.force_send_next(); // waiting
device.queue_pdu(read_pdu()); // absorbed: two requests pending
// Fill the remaining live capacity with distinct frames.
for (uint16_t i = 0; hub.entries() < MODBUS_TX_BUFFER_SIZE; i++) {
const uint8_t fill[] = {0x03, static_cast<uint8_t>(i >> 8), static_cast<uint8_t>(i & 0xFF), 0x00, 0x01};
filler.send_pdu(fill);
filler.queue_pdu(fill);
}
hub.timeout_waiting(); // retry requested; the entry flips back to READY regardless of capacity
@@ -991,10 +991,10 @@ TEST(ModbusClientHubQueue, SendRawTooShortIsRefusedAtTheDoor) {
NotSentCountingRawDevice device(&hub, 0x02);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
EXPECT_FALSE(device.send_raw({})); // too short to contain a PDU
device.send_raw({}); // too short to contain a PDU; the deprecated void spelling cannot report it
#pragma GCC diagnostic pop
EXPECT_EQ(device.not_sent_count_, 0); // refusals are returned, never delivered
EXPECT_TRUE(hub.tx_buffer_empty());
EXPECT_EQ(device.not_sent_count_, 0); // refused at the door: no callback delivered
EXPECT_TRUE(hub.tx_buffer_empty()); // the only evidence of the refusal is that nothing queued
}
// A continuous read: every wire transmission pairs one sent with one terminal, ending on the error.
@@ -1040,7 +1040,7 @@ class ChainOnSentDevice : public ModbusClientDevice {
if (!this->chained_) {
this->chained_ = true;
const uint8_t follow[] = {0x03, 0x00, 0x09, 0x00, 0x01}; // read holding 0x0009 x1
this->send_pdu(follow);
this->queue_pdu(follow);
}
}
bool chained_{false};
@@ -1059,9 +1059,9 @@ TEST(ModbusClientHubQueue, ClearAddressQueueNotifiesEveryOwner) {
const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02};
const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02};
const uint8_t read_c[] = {0x03, 0x03, 0x00, 0x00, 0x02};
controller_like.send_pdu(read_a);
bystander_same.send_pdu(read_b);
bystander_other.send_pdu(read_c);
controller_like.queue_pdu(read_a);
bystander_same.queue_pdu(read_b);
bystander_other.queue_pdu(read_c);
ASSERT_EQ(hub.queued_frames(), 3u);
controller_like.clear_tx_queue_for_address();
@@ -1083,8 +1083,8 @@ TEST(ModbusClientHubQueue, ClearAddressDeliversOneTerminalPerAcceptedRequest) {
SentCountingDevice device(&hub, 0x02);
const uint8_t read[] = {0x03, 0x01, 0x00, 0x00, 0x02};
device.send_pdu(read);
device.send_pdu(read); // duplicate: absorbed into the queued entry
device.queue_pdu(read);
device.queue_pdu(read); // duplicate: absorbed into the queued entry
ASSERT_EQ(hub.queued_frames(), 1u);
ASSERT_EQ(hub.queued(0).pending, 2u);
@@ -1103,8 +1103,8 @@ TEST(ModbusClientHubQueue, ClearSentOnInFlightDuplicateStillNotifiesTheDuplicate
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.send_pdu(read_pdu()); // absorbed: one entry, pending 2
device.queue_pdu(read_pdu());
device.queue_pdu(read_pdu()); // absorbed: one entry, pending 2
ASSERT_EQ(hub.queued(0).pending, 2u);
hub.force_send_next(); // the frame is sent (WAITING); pending still 2
ASSERT_TRUE(hub.waiting());
@@ -1122,7 +1122,7 @@ TEST(ModbusClientHubQueue, ClearWhileInFlightStillDeliversTheResponse) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next(); // sent, now WAITING
ASSERT_TRUE(hub.waiting());
@@ -1151,7 +1151,7 @@ class ResendOnNotSentDevice : public ModbusClientDevice {
this->not_sent_count_++;
if (this->not_sent_count_ == 1) {
const uint8_t again[] = {0x06, 0x00, 0x40, 0x00, 0x01}; // a write: ranked first at selection, not by position
this->send_pdu(again);
this->queue_pdu(again);
}
}
int not_sent_count_{0};
@@ -1165,7 +1165,7 @@ TEST(ModbusClientHubQueue, ClearAddressReentrantResendSurvives) {
ResendOnNotSentDevice device(&hub, 0x02);
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
device.send_pdu(read);
device.queue_pdu(read);
ASSERT_EQ(hub.queued_frames(), 1u);
hub.clear_tx_queue_for_address(0x02);
@@ -1187,8 +1187,8 @@ TEST(ModbusClientHubQueue, ClearAddressReentrantResendNotSwept) {
const uint8_t read_victim[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t read_other[] = {0x03, 0x00, 0x20, 0x00, 0x01};
resender.send_pdu(read_victim);
bystander_other.send_pdu(read_other);
resender.queue_pdu(read_victim);
bystander_other.queue_pdu(read_other);
ASSERT_EQ(hub.queued_frames(), 2u);
hub.clear_tx_queue_for_address(0x02);
@@ -1212,13 +1212,14 @@ class AlwaysResendDevice : public ModbusClientDevice {
void on_not_sent(std::span<const uint8_t> request_pdu) override {
this->not_sent_count_++;
const uint8_t again[] = {0x03, 0x00, 0x50, 0x00, 0x01};
this->send_pdu(again);
this->queue_pdu(again);
}
int not_sent_count_{0};
};
// From inside on_not_sent, clears ANOTHER address - those victims must still be notified (the per-device
// guard suppresses deliveries only to a device already inside its own on_not_sent()).
// From inside on_not_sent, clears ANOTHER address - those victims must still be notified. Nothing
// suppresses that: a re-entrant clear only flips states, retire() is a no-op on an already-retired
// entry, and each entry still owes one notification per un-run request until pending reaches zero.
class ClearOtherOnNotSentDevice : public ModbusClientDevice {
public:
ClearOtherOnNotSentDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {}
@@ -1238,9 +1239,9 @@ TEST(ModbusClientHubQueue, PendingNeverExceedsTheServableCap) {
AlwaysResendDevice device(&hub, 0x02);
const uint8_t read[] = {0x03, 0x00, 0x50, 0x00, 0x01};
EXPECT_TRUE(device.send_pdu(read));
EXPECT_TRUE(device.send_pdu(read));
EXPECT_FALSE(device.send_pdu(read)); // at the cap: refused
EXPECT_TRUE(device.queue_pdu(read));
EXPECT_TRUE(device.queue_pdu(read));
EXPECT_FALSE(device.queue_pdu(read)); // at the cap: refused
ASSERT_EQ(hub.queued_frames(), 1u);
EXPECT_EQ(hub.queued(0).pending, 2u);
@@ -1260,12 +1261,12 @@ TEST(ModbusClientHubQueue, FullQueueRefusesWithoutCallbacks) {
// Fill the queue with distinct frames (distinct start addresses keep the dedup from absorbing them).
for (uint16_t i = 0; i < MODBUS_TX_BUFFER_SIZE; i++) {
const uint8_t fill[] = {0x03, static_cast<uint8_t>(i >> 8), static_cast<uint8_t>(i & 0xFF), 0x00, 0x01};
filler.send_pdu(fill);
filler.queue_pdu(fill);
}
ASSERT_EQ(hub.queued_frames(), MODBUS_TX_BUFFER_SIZE);
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
EXPECT_FALSE(device.send_pdu(read)); // refused synchronously
EXPECT_FALSE(device.queue_pdu(read)); // refused synchronously
hub.sweep_for_test();
EXPECT_EQ(device.not_sent_count_, 0); // nothing was accepted, so nothing is owed
@@ -1298,13 +1299,13 @@ TEST(ModbusClientHubQueue, SelfClearFromNotSentResolvesEveryRequest) {
const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01};
const uint8_t read_c[] = {0x03, 0x00, 0x30, 0x00, 0x01};
clearer.send_pdu(read_a);
clearer.send_pdu(read_b);
bystander.send_pdu(read_c);
clearer.queue_pdu(read_a);
clearer.queue_pdu(read_b);
bystander.queue_pdu(read_c);
ASSERT_EQ(hub.queued_frames(), 3u);
EXPECT_FALSE(clearer.send_pdu(std::span<const uint8_t>{})); // empty: refused, no callback
clearer.clear_tx_queue_for_address(); // the clear the handler used to make
EXPECT_FALSE(clearer.queue_pdu(std::span<const uint8_t>{})); // empty: refused, no callback
clearer.clear_tx_queue_for_address(); // the clear the handler used to make
hub.sweep_for_test();
@@ -1323,8 +1324,8 @@ TEST(ModbusClientHubQueue, NestedClearFromNotSentStillNotifiesVictims) {
const uint8_t read_a[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t read_b[] = {0x03, 0x00, 0x20, 0x00, 0x01};
clearer.send_pdu(read_a);
victim.send_pdu(read_b);
clearer.queue_pdu(read_a);
victim.queue_pdu(read_b);
ASSERT_EQ(hub.queued_frames(), 2u);
hub.clear_tx_queue_for_address(0x02); // clearer's on_not_sent clears address 0x03 in turn
@@ -1345,7 +1346,7 @@ class ResendSecondFrameDevice : public ModbusClientDevice {
this->not_sent_count_++;
if (this->not_sent_count_ == 1) {
const uint8_t same_as_r2[] = {0x03, 0x00, 0x22, 0x00, 0x01};
this->send_pdu(same_as_r2);
this->queue_pdu(same_as_r2);
}
}
int not_sent_count_{0};
@@ -1360,8 +1361,8 @@ TEST(ModbusClientHubQueue, SweepDedupSkipsDeletedFrames) {
const uint8_t r1[] = {0x03, 0x00, 0x21, 0x00, 0x01};
const uint8_t r2[] = {0x03, 0x00, 0x22, 0x00, 0x01};
device.send_pdu(r1);
device.send_pdu(r2);
device.queue_pdu(r1);
device.queue_pdu(r2);
ASSERT_EQ(hub.queued_frames(), 2u);
hub.clear_tx_queue_for_address(0x02);
@@ -1383,7 +1384,7 @@ class ResendAndClearOnNotSentDevice : public ModbusClientDevice {
void on_not_sent(std::span<const uint8_t> request_pdu) override {
this->not_sent_count_++;
const uint8_t again[] = {0x03, 0x00, 0x70, 0x00, 0x01};
this->send_pdu(again);
this->queue_pdu(again);
this->clear_tx_queue_for_address();
}
int not_sent_count_{0};
@@ -1397,7 +1398,7 @@ TEST(ModbusClientHubQueue, ResendAndClearFromNotSentCannotExtendTheSweep) {
ResendAndClearOnNotSentDevice device(&hub, 0x02);
const uint8_t read[] = {0x03, 0x00, 0x70, 0x00, 0x01};
device.send_pdu(read);
device.queue_pdu(read);
hub.clear_tx_queue_for_address(0x02);
hub.sweep_for_test();
@@ -1421,8 +1422,8 @@ TEST(ModbusClientHubQueue, ClearDeviceQueueDropsSilently) {
const uint8_t read_a[] = {0x03, 0x01, 0x00, 0x00, 0x02};
const uint8_t read_b[] = {0x03, 0x02, 0x00, 0x00, 0x02};
device.send_pdu(read_a);
device.send_pdu(read_b);
device.queue_pdu(read_a);
device.queue_pdu(read_b);
ASSERT_EQ(hub.queued_frames(), 2u);
device.clear_tx_queue_for_device();
@@ -1431,7 +1432,7 @@ TEST(ModbusClientHubQueue, ClearDeviceQueueDropsSilently) {
EXPECT_EQ(device.not_sent_count_, 0); // silent drop: no terminal callback
}
// A send_pdu() from inside on_sent() enqueues behind the waiting frame rather than sending
// A queue_pdu() from inside on_sent() enqueues behind the waiting frame rather than sending
// immediately or corrupting the waiting transaction.
TEST(ModbusClientHubSent, ReentrantSendFromOnSentQueues) {
NullUART uart;
@@ -1440,7 +1441,7 @@ TEST(ModbusClientHubSent, ReentrantSendFromOnSentQueues) {
hub.setup();
ChainOnSentDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.send_next_for_test(); // first frame is sent -> on_sent chains a follow-up
EXPECT_TRUE(hub.waiting()); // first frame is waiting
@@ -1507,34 +1508,69 @@ class LegacyNameDevice : public ModbusClientDevice {
#pragma GCC diagnostic pop
} // namespace
// send_pdu() was renamed queue_pdu() because the call queues a request rather than transmitting one.
// The old spelling stays for the deprecation window with the signature 2026.7.4 shipped - void, no
// CommandOptions - so a component built against a real release still compiles and still queues.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
TEST(ModbusClientHubCompat, DeprecatedSendPduStillQueues) {
NoResponseProbeHub hub;
RetryingDevice device(&hub, 0x02, /*retry=*/false);
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
device.send_pdu(read); // deprecated device spelling: void, as 2026.7.4 shipped it
EXPECT_EQ(hub.queued_frames(), 1u);
// A refusal is invisible to this spelling - no return value and no callback - so the only evidence
// is that nothing was queued. Reporting the refusal is exactly what moving to queue_pdu() buys.
device.send_pdu(std::span<const uint8_t>());
EXPECT_EQ(hub.queued_frames(), 1u);
// The deprecated hub spelling queues the same way, addressed explicitly.
const uint8_t other[] = {0x03, 0x00, 0x20, 0x00, 0x01};
hub.send_pdu(0x03, other, &device);
EXPECT_EQ(hub.queued_frames(), 2u);
// Both frames resolve to the same owner. Drain them in turn: the device-spelling frame first (FIFO),
// then the hub-spelling frame - addressed to 0x03 yet owned by &device, so reaching device's
// on_no_response proves the request routes by owner pointer, not by address.
hub.force_send_next();
hub.timeout_waiting();
EXPECT_EQ(device.no_response_count_, 1); // device-spelling frame (address 0x02)
hub.force_send_next();
hub.timeout_waiting();
EXPECT_EQ(device.no_response_count_, 2); // hub-spelling frame (address 0x03, &device routing)
}
#pragma GCC diagnostic pop
TEST(ModbusClientHubCompat, LegacyCallbackNamesStillForward) {
NoResponseProbeHub hub;
LegacyNameDevice device(&hub, 0x02);
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
device.send_pdu(read);
device.queue_pdu(read);
hub.force_send_next();
hub.timeout_waiting(); // no reply -> on_no_response -> forwards to on_modbus_no_response
EXPECT_EQ(device.legacy_no_response_, 1);
// A refused send returns false with no callback, so exercise the forward through an accepted
// request instead: a cleared queue entry delivers on_not_sent(), which forwards to the old name.
EXPECT_FALSE(device.send_pdu(std::span<const uint8_t>())); // empty PDU: refused at the door
EXPECT_FALSE(device.queue_pdu(std::span<const uint8_t>())); // empty PDU: refused at the door
EXPECT_EQ(device.legacy_not_sent_, 0);
const uint8_t queued[] = {0x03, 0x00, 0x11, 0x00, 0x01};
EXPECT_TRUE(device.send_pdu(queued));
EXPECT_TRUE(device.queue_pdu(queued));
hub.clear_tx_queue_for_address(0x02);
hub.sweep_for_test();
EXPECT_EQ(device.legacy_not_sent_, 1);
}
// The send_pdu() capacity bound: a PDU larger than MAX_PDU_SIZE would build a frame past the RTU
// The queue_pdu() capacity bound: a PDU larger than MAX_PDU_SIZE would build a frame past the RTU
// 256-byte limit, so it is refused up front - false at the call site, no entry, no callback.
TEST(ModbusClientHub, OversizedPduIsRefusedAtTheDoor) {
NoResponseProbeHub hub;
LegacyNameDevice device(&hub, 0x02);
std::vector<uint8_t> big(MAX_PDU_SIZE + 1, 0x41);
EXPECT_FALSE(device.send_pdu(big));
EXPECT_FALSE(device.queue_pdu(big));
EXPECT_EQ(device.legacy_not_sent_, 0); // refusals are returned, never delivered
EXPECT_TRUE(hub.tx_buffer_empty());
EXPECT_EQ(hub.entries(), 0u);
@@ -1568,7 +1604,7 @@ TEST(ModbusDeviceShim, LegacyCallbacksReceiveTheOldShapes) {
// Read response: on_modbus_data() historically received the payload after the function code and
// the byte-count byte, as an owning vector.
const uint8_t read_req[] = {0x03, 0x00, 0x10, 0x00, 0x02};
device.send_pdu(read_req);
device.queue_pdu(read_req);
hub.force_send_next();
const uint8_t response[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
hub.receive_frame_for_test(0x02, response);
@@ -1577,14 +1613,14 @@ TEST(ModbusDeviceShim, LegacyCallbacksReceiveTheOldShapes) {
// Write echo: no byte-count byte, so the payload is everything after the function code.
const uint8_t write_req[] = {0x06, 0x00, 0x10, 0x00, 0x2A};
device.send_pdu(write_req);
device.queue_pdu(write_req);
hub.force_send_next();
hub.receive_frame_for_test(0x02, write_req); // single-write responses echo the request
const std::vector<uint8_t> expected_echo{0x00, 0x10, 0x00, 0x2A};
EXPECT_EQ(device.last_data_, expected_echo);
// Exception response: on_modbus_error() received the masked function code and the exception code.
device.send_pdu(read_req);
device.queue_pdu(read_req);
hub.force_send_next();
const uint8_t error[] = {0x83, 0x02};
hub.receive_frame_for_test(0x02, error);
@@ -1675,9 +1711,9 @@ class ResendOnDataDevice : public ModbusClientDevice {
public:
ResendOnDataDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {}
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
this->send_pdu(std::vector<uint8_t>(request_pdu.begin(), request_pdu.end()));
this->queue_pdu(std::vector<uint8_t>(request_pdu.begin(), request_pdu.end()));
}
void send_pdu(const std::vector<uint8_t> &pdu) { ModbusClientDevice::send_pdu(pdu); }
void queue_pdu(const std::vector<uint8_t> &pdu) { ModbusClientDevice::queue_pdu(pdu); }
};
} // namespace
@@ -1704,8 +1740,8 @@ TEST(ModbusClientHubPriority, ExceptionFlaggedDuplicateDroppedNotPromoted) {
SentCountingDevice device(&hub, 0x02);
const uint8_t weird[] = {0x83, 0x01, 0x00, 0x00, 0x02}; // read-shaped but exception-flagged
EXPECT_TRUE(device.send_pdu(weird));
EXPECT_FALSE(device.send_pdu(weird)); // non-requeueable: cap of one, so the duplicate is refused
EXPECT_TRUE(device.queue_pdu(weird));
EXPECT_FALSE(device.queue_pdu(weird)); // non-requeueable: cap of one, so the duplicate is refused
hub.sweep_for_test();
ASSERT_EQ(hub.queued_frames(), 1u);
@@ -1715,7 +1751,7 @@ TEST(ModbusClientHubPriority, ExceptionFlaggedDuplicateDroppedNotPromoted) {
// The write-shaped twin (0x86 masks to WRITE_SINGLE_REGISTER) must not take WRITE-class
// ordering either: exception-flagged codes are excluded from the mutates classification.
const uint8_t weird_write[] = {0x86, 0x00, 0x10, 0xBE, 0xEF};
device.send_pdu(weird_write);
device.queue_pdu(weird_write);
ASSERT_EQ(hub.queued_frames(), 2u);
EXPECT_EQ(hub.queued(1).priority(), CommandPriority::READ); // not WRITE
const ModbusDeviceCommand *next = hub.next_ready();
@@ -1732,7 +1768,7 @@ class ResendInFlightOnNotSentDevice : public ModbusClientDevice {
this->not_sent_count_++;
if (this->not_sent_count_ == 1) {
const uint8_t same_as_waiting[] = {0x03, 0x01, 0x00, 0x00, 0x02}; // == READ_PDU
this->send_pdu(same_as_waiting);
this->queue_pdu(same_as_waiting);
}
}
int not_sent_count_{0};
@@ -1746,10 +1782,10 @@ TEST(ModbusClientHubQueue, SweepResendAfterClearQueuesFreshNotAbsorbedIntoShell)
NoResponseProbeHub hub;
ResendInFlightOnNotSentDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next(); // READ_PDU now waiting
const uint8_t queued_read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
device.send_pdu(queued_read); // a queued frame for the sweep to notify
device.queue_pdu(queued_read); // a queued frame for the sweep to notify
ASSERT_EQ(hub.queued_frames(), 1u);
hub.clear_tx_queue_for_address(0x02);
@@ -1787,7 +1823,7 @@ TEST(ModbusClientHubNoResponse, SelfClearFromNoResponseDoesNotDoubleResolve) {
NoResponseProbeHub hub;
ClearAddressOnNoResponseDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.timeout_waiting();
@@ -1802,8 +1838,8 @@ TEST(ModbusClientHubNoResponse, SelfClearFromNoResponseResolvesTheAbsorbedReques
NoResponseProbeHub hub;
ClearAddressOnNoResponseDevice device(&hub, 0x02);
EXPECT_TRUE(device.send_pdu(read_pdu()));
EXPECT_TRUE(device.send_pdu(read_pdu())); // absorbed: one entry, two requests
EXPECT_TRUE(device.queue_pdu(read_pdu()));
EXPECT_TRUE(device.queue_pdu(read_pdu())); // absorbed: one entry, two requests
hub.force_send_next();
hub.timeout_waiting();
@@ -1818,7 +1854,7 @@ TEST(ModbusClientHubQueue, ClearedShellReleasesTheBusOnLateResponse) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.clear_tx_queue_for_address(0x02);
ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED);
@@ -1836,7 +1872,7 @@ TEST(ModbusClientHubQueue, ClearedShellReleasesTheBusOnTimeout) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.clear_tx_queue_for_address(0x02);
ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED);
@@ -1856,7 +1892,7 @@ TEST(ModbusClientHubQueue, ClearInterruptedFrameGetsNoResponseAtTimeout) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02); // declines the retry (retries_ == 0)
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
hub.receive_frame_for_test(0x07, stray_pdu); // wrong address: interrupts the transaction
@@ -1887,7 +1923,7 @@ TEST(ModbusClientHubQueue, InterruptAfterClearStillDistrusts) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
hub.clear_tx_queue_for_address(0x02);
ASSERT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED);
@@ -1912,8 +1948,8 @@ TEST(ModbusClientHubQueue, ClearedInFlightDuplicateTimesOutWithoutRerunning) {
NoResponseProbeHub hub;
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.send_pdu(read_pdu()); // absorbed: one entry, pending 2
device.queue_pdu(read_pdu());
device.queue_pdu(read_pdu()); // absorbed: one entry, pending 2
ASSERT_EQ(hub.queued(0).pending, 2u);
hub.force_send_next(); // sent, pending still 2
hub.clear_tx_queue_for_address(0x02);
@@ -1936,9 +1972,9 @@ TEST(ModbusClientHubCallbackCount, AbsorbedRequestRunsAfterErrorResponse) {
hub.setup();
DataCountingDevice device(&hub, 0x02);
device.send_pdu(read_pdu());
device.queue_pdu(read_pdu());
hub.force_send_next();
device.send_pdu(read_pdu()); // waiting duplicate: absorbed
device.queue_pdu(read_pdu()); // waiting duplicate: absorbed
const uint8_t exception_response[] = {0x83, 0x02};
hub.receive_frame_for_test(0x02, exception_response); // error terminal for request 1
@@ -1954,8 +1990,8 @@ TEST(ModbusClientHubPriority, ReadModifyWritesRankAsWrites) {
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
const uint8_t mask_write[] = {0x16, 0x00, 0x10, 0x00, 0xFF, 0x00, 0x01};
device.send_pdu(read);
device.send_pdu(mask_write);
device.queue_pdu(read);
device.queue_pdu(mask_write);
ASSERT_EQ(hub.queued_frames(), 2u);
const ModbusDeviceCommand *next = hub.next_ready();