294 lines
14 KiB
C++
294 lines
14 KiB
C++
// Share hardware/pool/old-parser fixtures with the independent heartbeat suite.
|
|
// All diagnostics below exercise the compiled production RS232Bridge.cpp.
|
|
#define main heartbeat_fixture_main
|
|
#include "rs232_heartbeat/main.cpp"
|
|
#undef main
|
|
|
|
namespace diag = mesh::bridge::rs232diag;
|
|
|
|
std::vector<uint8_t> probe(uint32_t nonce) {
|
|
std::vector<uint8_t> bytes(diag::FrameSize);
|
|
bytes[0] = 0xC0; bytes[1] = 0x3E;
|
|
assert(diag::encodeProbe(bytes.data() + 4, bytes.size() - 4, nonce));
|
|
return bytes;
|
|
}
|
|
|
|
void probe_codec_test() {
|
|
assert(diag::FrameSize == 52 && sizeof(diag::Stats) <= 128);
|
|
for (uint32_t nonce : {0U, 1U, 0xA1B2C3D4U, UINT32_MAX}) {
|
|
const auto frame = probe(nonce);
|
|
assert(frame[2] == 0 && frame[3] == 0);
|
|
assert(memcmp(frame.data() + 4, "DG1T", 4) == 0);
|
|
for (size_t i = 4; i < frame.size(); ++i) assert(frame[i] < 0x80);
|
|
uint32_t parsed = 0x12345678;
|
|
assert(diag::decodeProbe(frame.data() + 4, 48, parsed) && parsed == nonce);
|
|
hb::Control heartbeat;
|
|
assert(!hb::decode(frame.data() + 4, heartbeat));
|
|
for (size_t i = 4; i < frame.size(); ++i) for (unsigned bit = 0; bit < 8; ++bit) {
|
|
auto bad = frame; bad[i] ^= 1U << bit;
|
|
parsed = 0x12345678;
|
|
assert(!diag::decodeProbe(bad.data() + 4, 48, parsed));
|
|
assert(parsed == 0x12345678);
|
|
}
|
|
for (size_t size = 0; size < 80; ++size) if (size != 48) {
|
|
parsed = 0x12345678;
|
|
assert(!diag::decodeProbe(frame.data() + 4, size, parsed));
|
|
assert(parsed == 0x12345678);
|
|
}
|
|
}
|
|
std::array<uint8_t, 60> untouched; untouched.fill(0x55);
|
|
assert(!diag::encodeProbe(untouched.data(), 47, 42));
|
|
for (auto value : untouched) assert(value == 0x55);
|
|
uint32_t nonce = 42;
|
|
assert(!diag::encodeProbe(nullptr, 48, 1));
|
|
assert(!diag::decodeProbe(nullptr, 48, nonce) && nonce == 42);
|
|
auto reserved = probe(42); reserved[16] = '1'; rechecksum(reserved);
|
|
assert(!diag::decodeProbe(reserved.data() + 4, 48, nonce));
|
|
}
|
|
|
|
void probe_isolation_test() {
|
|
now_ms = 100; Peer sender(A_SESSION), receiver(B_SESSION);
|
|
sender.driver.setHeartbeatEnabled(false);
|
|
receiver.driver.setHeartbeatEnabled(false);
|
|
sender.uart.delivered = [&](const uint8_t* data, size_t size) { receiver.uart.feed(data, size); };
|
|
receiver.manager.starved = true;
|
|
const auto last_peer = receiver.driver._power.peerAge(now_ms);
|
|
assert(sender.driver.sendTest(0xA1B2C3D4));
|
|
assert(sender.uart.tx.empty()); // Queueing is not serial or peer acceptance.
|
|
sender.driver.loop(); receiver.driver.loop();
|
|
assert(sender.uart.tx == probe(0xA1B2C3D4));
|
|
assert(sender.driver._stats.probe.sent == 1);
|
|
assert(receiver.driver._stats.probe.received == 1);
|
|
assert(receiver.driver._stats.probe.last_rx_nonce == 0xA1B2C3D4);
|
|
assert(sender.manager.allocations == 0 && receiver.manager.allocations == 0);
|
|
assert(sender.manager.received.empty() && receiver.manager.received.empty());
|
|
assert(sender.driver.seenCount() == 0 && receiver.driver.seenCount() == 0);
|
|
assert(receiver.uart.tx.empty() && !receiver.driver._have_ack && !receiver.driver._legacy_rx);
|
|
assert(receiver.driver._pong_target == 0 && !receiver.driver._power.hasPeer());
|
|
assert(receiver.driver._power.peerAge(now_ms) == last_peer);
|
|
receiver.driver.setHeartbeatEnabled(true);
|
|
assert(receiver.driver.servicePowerPolicy(true, 1, now_ms + 1, true) == Action::Suspend);
|
|
receiver.driver.setHeartbeatEnabled(false);
|
|
for (unsigned i = 0; i < 5; ++i) { sender.driver.loop(); receiver.driver.loop(); }
|
|
assert(sender.uart.tx.size() == diag::FrameSize && receiver.uart.tx.empty());
|
|
auto bad = probe(42); bad[49] ^= 1;
|
|
receiver.uart.feed(bad); receiver.driver.loop();
|
|
assert(receiver.driver._stats.probe.bad == 1 && receiver.manager.allocations == 0);
|
|
assert(receiver.driver._stats.probe.last_rx_nonce == 0xA1B2C3D4);
|
|
}
|
|
|
|
void probe_legacy_test() {
|
|
for (unsigned parser = 0; parser < 2; ++parser) for (size_t prefix = 0; prefix <= diag::FrameSize; ++prefix) {
|
|
NodePrefs prefs; HardwareSerial uart; uart.begin(115200);
|
|
mesh::PacketManager manager;
|
|
StockBridge old(&prefs, uart, &manager);
|
|
UpstreamStockBridge upstream(&prefs, uart, &manager);
|
|
BridgeBase* bridge = parser ? static_cast<BridgeBase*>(&upstream) : &old;
|
|
const auto frame = probe(0xA1B2C3D4);
|
|
uart.feed(frame.data(), prefix); bridge->loop();
|
|
assert(manager.allocations == 0 && manager.received.empty());
|
|
const auto data = data_frame({5, 6, 7, 8});
|
|
uart.feed(data); bridge->loop();
|
|
// A legacy parser cannot resynchronize a truncated header (1..3 bytes),
|
|
// just as for any interrupted data frame. A whole probe or body prefix is
|
|
// ignored and does not hide the following ordinary frame.
|
|
if (prefix == 0 || prefix >= 6) {
|
|
assert(manager.received == std::vector<std::vector<uint8_t>>({{5, 6, 7, 8}}));
|
|
assert(manager.allocations == 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void probe_backpressure_test() {
|
|
// All possible partial writes retain their exact prefix. A timeout cancels
|
|
// only an unsent frame; a partial header keeps its unsent suffix owned until
|
|
// UART recovery. This protects both legacy parsers from a later mesh header.
|
|
for (size_t prefix = 0; prefix < diag::FrameSize; ++prefix) {
|
|
now_ms = 100; Peer sender(A_SESSION), receiver(B_SESSION);
|
|
sender.driver.setHeartbeatEnabled(false); receiver.driver.setHeartbeatEnabled(false);
|
|
sender.uart.write_limit = prefix;
|
|
assert(sender.driver.sendTest(0x12345678));
|
|
sender.driver.drainTransmit(now_ms);
|
|
assert(sender.uart.tx.size() == prefix);
|
|
const auto accepted = sender.uart.tx;
|
|
sender.uart.write_limit = 0;
|
|
sender.driver.loop();
|
|
assert(sender.uart.tx == accepted && sender.driver._stats.probe.sent == 0);
|
|
now_ms += sender.driver.frameAssemblyMs();
|
|
sender.driver.loop();
|
|
assert(sender.driver._stats.probe.failed == 1);
|
|
assert(sender.driver._tx_size == (prefix ? diag::FrameSize : 0));
|
|
assert(sender.driver._stats.error == diag::Error::TestTimeout);
|
|
if (prefix) assert(sender.driver._tx_wire_pending); // Prefix wire delay is not erased.
|
|
for (unsigned n = 0; n < 3; ++n) sender.driver.loop();
|
|
assert(sender.uart.tx == accepted && sender.driver._stats.probe.failed == 1);
|
|
sender.uart.write_limit = SIZE_MAX;
|
|
now_ms += 100;
|
|
if (prefix) {
|
|
assert(!sender.driver.sendTest(0xFEDCBA98));
|
|
sender.driver.loop();
|
|
assert(sender.uart.tx == probe(0x12345678));
|
|
assert(sender.driver._stats.probe.sent == 1);
|
|
now_ms += 100;
|
|
}
|
|
assert(sender.driver.sendTest(0xFEDCBA98)); sender.driver.loop();
|
|
receiver.uart.feed(sender.uart.tx); receiver.driver.loop();
|
|
assert(receiver.driver._stats.probe.received == (prefix ? 2U : 1U));
|
|
assert(receiver.driver._stats.probe.last_rx_nonce == 0xFEDCBA98);
|
|
for (unsigned parser = 0; parser < 2; ++parser) {
|
|
NodePrefs prefs; HardwareSerial uart; uart.begin(115200);
|
|
mesh::PacketManager manager;
|
|
StockBridge old(&prefs, uart, &manager);
|
|
UpstreamStockBridge upstream(&prefs, uart, &manager);
|
|
BridgeBase* bridge = parser ? static_cast<BridgeBase*>(&upstream) : &old;
|
|
uart.feed(sender.uart.tx); bridge->loop();
|
|
assert(manager.allocations == 0);
|
|
uart.feed(data_frame({5, 6, 7, 8})); bridge->loop();
|
|
assert(manager.received == std::vector<std::vector<uint8_t>>({{5, 6, 7, 8}}));
|
|
}
|
|
assert(receiver.manager.allocations == 0 && !receiver.driver._power.hasPeer());
|
|
}
|
|
now_ms = 100; Peer completed(A_SESSION); completed.driver.setHeartbeatEnabled(false);
|
|
completed.uart.write_limit = 1;
|
|
assert(completed.driver.sendTest(0));
|
|
for (unsigned i = 0; i < 26; ++i) completed.driver.loop();
|
|
assert(completed.uart.tx == probe(0) && completed.driver._stats.probe.sent == 1);
|
|
assert(completed.driver._stats.probe.failed == 0);
|
|
}
|
|
|
|
void probe_state_test() {
|
|
now_ms = 100; Peer p(A_SESSION);
|
|
const auto before = p.driver._power.peerAge(now_ms);
|
|
assert(!p.driver.sendTest(1)); // Initial heartbeat must go first.
|
|
assert(p.driver._stats.error == diag::Error::TestBusy && p.uart.tx.empty());
|
|
p.driver.setHeartbeatEnabled(false);
|
|
assert(p.driver.sendTest(2));
|
|
assert(!p.driver.sendTest(3));
|
|
assert(p.driver._stats.probe.last_tx_nonce == 2 && p.driver._power.peerAge(now_ms) == before);
|
|
p.driver.end();
|
|
assert(p.driver._stats.probe.failed == 3); // 2 rejected + 1 canceled.
|
|
assert(!p.driver.sendTest(4));
|
|
assert(p.driver._stats.error == diag::Error::TestStopped && !p.uart.installed);
|
|
p.driver.begin();
|
|
assert(p.driver._stats.probe.queued == 1 && p.driver._stats.probe.failed == 4);
|
|
p.driver.setHeartbeatEnabled(true);
|
|
p.enter_standby(now_ms + 1);
|
|
assert(!p.driver.sendTest(5) && !p.uart.installed);
|
|
assert(p.driver._power.suspended());
|
|
}
|
|
|
|
void diagnostic_counters_test() {
|
|
now_ms = 100; Peer p(A_SESSION); p.driver.setHeartbeatEnabled(false);
|
|
p.uart.feed({0x01, 0xC0, 0x11, 0xC0, 0x3E, 0xFF, 0xFF}); p.driver.loop();
|
|
assert(p.driver._stats.rx.bytes == 7 && p.driver._stats.rx.noise == 2);
|
|
assert(p.driver._stats.rx.length_bad == 1);
|
|
auto bad = data_frame({4, 5, 6}); bad.back() ^= 1;
|
|
p.uart.feed(bad); p.driver.loop();
|
|
assert(p.driver._stats.rx.checksum_bad == 1 && p.driver._stats.rx.frames == 0);
|
|
p.uart.feed({0xC0, 0x3E, 0, 10, 1}); p.driver.loop();
|
|
now_ms += p.driver.frameAssemblyMs(); p.driver.loop();
|
|
assert(p.driver._stats.rx.timeout == 1);
|
|
p.manager.starved = true;
|
|
p.uart.feed(data_frame({8, 9, 10})); p.driver.loop();
|
|
for (unsigned n = 0; n < 10; ++n) p.driver.loop();
|
|
assert(p.driver._stats.rx.frames == 1 && p.driver._stats.rx.pool_wait == 1);
|
|
p.manager.starved = false; p.driver.loop();
|
|
assert(p.driver._stats.rx.delivered == 1 && !p.driver._rx_packet_ready);
|
|
mesh::Packet packet; packet.payload = {12, 13, 14};
|
|
p.driver.sendPacket(&packet); p.driver.sendPacket(&packet);
|
|
assert(p.driver._stats.tx.requested == 2 && p.driver._stats.tx.queued == 1);
|
|
assert(p.driver._stats.tx.sent == 1 && p.driver._stats.tx.duplicate == 1);
|
|
p.uart.feed(data_frame(packet.payload)); p.driver.loop();
|
|
assert(p.driver._stats.rx.duplicate == 1 && p.driver._stats.rx.delivered == 1);
|
|
packet.allowed = false; p.driver.sendPacket(&packet);
|
|
assert(p.driver._stats.tx.filtered == 1);
|
|
const auto old_rx = p.driver._stats.rx.bytes;
|
|
p.driver.end(); p.driver.begin();
|
|
assert(p.driver._stats.rx.bytes == old_rx && p.driver._stats.tx.sent == 1);
|
|
p.driver._outstanding_challenge = 1234; p.driver._last_ack = 12;
|
|
p.driver._pong_target = B_SESSION;
|
|
p.uart.feed({0xC0}); p.driver.loop();
|
|
const auto parser_pos = p.driver._rx_buffer_pos;
|
|
p.driver.resetStats();
|
|
assert(p.driver._stats.rx.bytes == 0 && p.driver._stats.tx.sent == 0);
|
|
assert(p.driver._outstanding_challenge == 1234 && p.driver._last_ack == 12);
|
|
assert(p.driver._rx_buffer_pos == parser_pos);
|
|
}
|
|
|
|
void diagnostic_bounds_test() {
|
|
now_ms = 100; Peer p(A_SESSION); p.driver.setHeartbeatEnabled(false);
|
|
for (uint32_t start : {0U, UINT32_MAX - 1, UINT32_MAX}) {
|
|
uint32_t counter = start;
|
|
diag::increment(counter);
|
|
assert(counter == (start == UINT32_MAX ? UINT32_MAX : start + 1));
|
|
diag::add(counter, SIZE_MAX); assert(counter == UINT32_MAX);
|
|
}
|
|
p.driver._stats.rx = {UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX,
|
|
UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX};
|
|
p.driver._stats.tx = {UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX,
|
|
UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX};
|
|
p.driver._stats.probe = {UINT32_MAX, UINT32_MAX, UINT32_MAX, UINT32_MAX,
|
|
UINT32_MAX, UINT32_MAX, UINT32_MAX};
|
|
for (const char* page : {"", "rx", "tx", "drops", "test", "invalid"}) {
|
|
std::array<char, 200> output; output.fill('!');
|
|
p.driver.formatStats(page, output.data(), 157);
|
|
assert(strlen(output.data()) < 157 && output[157] == '!');
|
|
if (strcmp(page, "drops") == 0) assert(strstr(output.data(), "big=4294967295"));
|
|
p.driver.formatStats(page, output.data(), 1); assert(output[0] == 0);
|
|
p.driver.formatStats(page, nullptr, 0);
|
|
}
|
|
p.prefs.bridge_baud = UINT32_MAX; p.driver._rx_pin = INT16_MIN; p.driver._tx_pin = INT16_MAX;
|
|
p.driver._stats.error = diag::Error::TestStopped;
|
|
char status[158]; memset(status, '!', sizeof(status));
|
|
p.driver.formatStatus(status, 157);
|
|
assert(strlen(status) < 157 && status[157] == '!');
|
|
assert(strstr(status, "last=test.stopped"));
|
|
p.driver.formatStatus(status, 1); assert(status[0] == 0);
|
|
p.driver.formatStatus(nullptr, 0);
|
|
}
|
|
|
|
void diagnostic_debug_test() {
|
|
now_ms = 1000; Peer p(A_SESSION); p.driver.setHeartbeatEnabled(false);
|
|
mesh::logging_enabled = mesh::debug_enabled = false;
|
|
p.driver.loop(); assert(mesh::debug_stream.writes == 0);
|
|
mesh::debug_enabled = true; p.driver.loop(); assert(mesh::debug_stream.writes == 0);
|
|
mesh::debug_enabled = false; mesh::logging_enabled = true;
|
|
p.driver.loop(); assert(mesh::debug_stream.writes == 0);
|
|
mesh::debug_enabled = true;
|
|
p.driver.loop(); assert(mesh::debug_stream.writes == 1);
|
|
const std::string text(mesh::debug_stream.bytes.begin(), mesh::debug_stream.bytes.end());
|
|
assert(text.find("RS232: ready baud=115200 pins=5/6") != std::string::npos);
|
|
assert(p.uart.tx.empty()); // Debug output never enters the bridge wire.
|
|
now_ms += 10000; p.driver.loop(); assert(mesh::debug_stream.writes == 1);
|
|
for (unsigned i = 0; i < 20; ++i) {
|
|
p.uart.feed({1}); p.driver.loop();
|
|
}
|
|
assert(mesh::debug_stream.writes == 2); // At most one dirty summary/second.
|
|
mesh::debug_stream.capacity = 0; now_ms += 1000;
|
|
p.uart.feed({1}); p.driver.loop();
|
|
const auto total = mesh::debug_stream.bytes.size();
|
|
now_ms += 1000; p.uart.feed({1}); p.driver.loop();
|
|
assert(mesh::debug_stream.bytes.size() == total);
|
|
mesh::debug_stream.capacity = 4096;
|
|
mesh::debug_enabled = false; now_ms += 1000; p.uart.feed({1}); p.driver.loop();
|
|
assert(mesh::debug_stream.bytes.size() == total);
|
|
mesh::debug_enabled = true; p.driver.loop();
|
|
assert(mesh::debug_stream.bytes.size() > total && p.uart.tx.empty());
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2);
|
|
const std::string name = argv[1];
|
|
if (name == "codec") probe_codec_test();
|
|
else if (name == "isolation") probe_isolation_test();
|
|
else if (name == "legacy") probe_legacy_test();
|
|
else if (name == "backpressure") probe_backpressure_test();
|
|
else if (name == "state") probe_state_test();
|
|
else if (name == "counters") diagnostic_counters_test();
|
|
else if (name == "bounds") diagnostic_bounds_test();
|
|
else if (name == "debug") diagnostic_debug_test();
|
|
else assert(false);
|
|
printf("%s passed\n", name.c_str());
|
|
return 0;
|
|
}
|