Escalate coding rate on hop-zero flood retries
This commit is contained in:
1 parent
b8ecd7fcac
commit
499e5bc94a
6 files changed
+415
-3
No files matched your search
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute preset application, role retry budgets, and the flood-count setter."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RetryCrPresetsTest(unittest.TestCase):
|
||||
def test_presets_and_disabling_flood_leave_direct_settings_unchanged(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++17 compiler is required")
|
||||
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
|
||||
core = (ROOT / "src/Mesh.cpp").read_text()
|
||||
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
|
||||
definitions = "\n".join(re.findall(
|
||||
r"^#define (?:RETRY_PRESET_|DIRECT_RETRY_|FLOOD_RETRY_)[A-Z0-9_]+[^\n]*",
|
||||
header, re.M))
|
||||
definitions += "\n" + re.search(
|
||||
r"#define FLOOD_RETRY_PATH_GATE_DISABLED[^\n]*",
|
||||
(ROOT / "src/Mesh.h").read_text()).group()
|
||||
generated = definitions + r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#define constrain(x, low, high) ((x) < (low) ? (low) : (x) > (high) ? (high) : (x))
|
||||
struct NodePrefs {
|
||||
uint8_t retry_preset, direct_retry_attempts, direct_retry_enabled;
|
||||
uint8_t direct_retry_cr_enabled, direct_retry_recent_enabled;
|
||||
uint8_t direct_retry_prefs_magic[2], flood_retry_attempts;
|
||||
uint8_t flood_retry_max_path, flood_retry_group_max_path, disable_fwd;
|
||||
uint16_t direct_retry_base_ms, direct_retry_step_ms, direct_retry_snr_margin_x4;
|
||||
int8_t direct_retry_cr4_snr_x4, direct_retry_cr5_snr_x4;
|
||||
int8_t direct_retry_cr7_snr_x4, direct_retry_cr8_snr_x4;
|
||||
};
|
||||
namespace mesh {
|
||||
struct Packet { uint8_t hops = 0; uint8_t getPathHashCount() const { return hops; } };
|
||||
class Mesh { public:
|
||||
static uint8_t getDirectRetryCodingRateForAttempt(uint8_t, uint8_t);
|
||||
};
|
||||
}
|
||||
class MyMesh : public mesh::Mesh {
|
||||
public:
|
||||
NodePrefs _prefs{};
|
||||
unsigned direct_cancellations = 0, flood_cancellations = 0;
|
||||
uint8_t getFloodRetryMaxAttempts(const mesh::Packet*) const;
|
||||
uint8_t roomFloodAttempts(const mesh::Packet*) const;
|
||||
uint8_t sensorFloodAttempts(const mesh::Packet*) const;
|
||||
uint8_t getDirectRetryCodingRateForSNR(int8_t) const;
|
||||
void onRetryConfigChanged();
|
||||
void cancelAllDirectRetries() { ++direct_cancellations; }
|
||||
void cancelAllFloodRetries() { ++flood_cancellations; }
|
||||
};
|
||||
struct CLI {
|
||||
NodePrefs* _prefs;
|
||||
MyMesh* _callbacks;
|
||||
unsigned saves = 0;
|
||||
void savePrefs() { ++saves; }
|
||||
int _atoi(const char* value) { return atoi(value); }
|
||||
void set(const char* config, char* reply);
|
||||
};
|
||||
'''
|
||||
for signature in ("static void markDirectRetryPrefsValid(",
|
||||
"static void applyFloodRetryPreset(",
|
||||
"static void applyDirectRetryPreset(",
|
||||
"static void setDefaultDirectRetryPrefs(",
|
||||
"static bool looksUnsignedInteger("):
|
||||
generated += extract_braced(common, signature) + "\n"
|
||||
generated += "namespace mesh {\n" + extract_braced(
|
||||
core, "uint8_t Mesh::getDirectRetryCodingRateForAttempt(") + "\n}\n"
|
||||
for signature in ("uint8_t MyMesh::getFloodRetryMaxAttempts(",
|
||||
"uint8_t MyMesh::getDirectRetryCodingRateForSNR(",
|
||||
"void MyMesh::onRetryConfigChanged("):
|
||||
generated += extract_braced(repeater, signature) + "\n"
|
||||
for role, owner, method in (("simple_room_server", "MyMesh", "roomFloodAttempts"),
|
||||
("simple_sensor", "SensorMesh", "sensorFloodAttempts")):
|
||||
source = (ROOT / f"examples/{role}/{owner}.cpp").read_text()
|
||||
budget = extract_braced(source, f"uint8_t {owner}::getFloodRetryMaxAttempts(")
|
||||
generated += budget.replace(f"{owner}::getFloodRetryMaxAttempts", f"MyMesh::{method}") + "\n"
|
||||
setter = extract_braced(common, 'if (memcmp(config, "flood.retry.count ", 18) == 0)')
|
||||
generated += "void CLI::set(const char* config, char* reply) { " + setter + " }\n"
|
||||
generated += r'''
|
||||
static std::vector<int> directSettings(const NodePrefs& p) {
|
||||
return {p.direct_retry_attempts, p.direct_retry_enabled, p.direct_retry_cr_enabled,
|
||||
p.direct_retry_recent_enabled, p.direct_retry_base_ms, p.direct_retry_step_ms,
|
||||
p.direct_retry_snr_margin_x4, p.direct_retry_cr4_snr_x4, p.direct_retry_cr5_snr_x4,
|
||||
p.direct_retry_cr7_snr_x4, p.direct_retry_cr8_snr_x4};
|
||||
}
|
||||
static std::vector<uint8_t> directSchedule(const MyMesh& node, int8_t snr) {
|
||||
std::vector<uint8_t> rates;
|
||||
for (uint8_t attempt = 1; attempt <= node._prefs.direct_retry_attempts; ++attempt) {
|
||||
rates.push_back(node.getDirectRetryCodingRateForAttempt(
|
||||
node.getDirectRetryCodingRateForSNR(snr), attempt));
|
||||
}
|
||||
return rates;
|
||||
}
|
||||
int main() {
|
||||
const uint8_t presets[] = {RETRY_PRESET_INFRA, RETRY_PRESET_ROOFTOP, RETRY_PRESET_MOBILE};
|
||||
const uint8_t direct_counts[] = {4, 15, 15};
|
||||
const uint8_t origin_counts[] = {2, 6, 15};
|
||||
const uint8_t expected_cr5[] = {5, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8};
|
||||
for (unsigned preset = 0; preset < 3; ++preset) {
|
||||
for (int8_t snr : {-40, 10, 12, 30, 40}) {
|
||||
MyMesh node;
|
||||
setDefaultDirectRetryPrefs(&node._prefs);
|
||||
applyDirectRetryPreset(&node._prefs, presets[preset]);
|
||||
assert(node._prefs.direct_retry_attempts == direct_counts[preset]);
|
||||
mesh::Packet packet;
|
||||
assert(node.getFloodRetryMaxAttempts(&packet) == origin_counts[preset]);
|
||||
assert(node.roomFloodAttempts(&packet) == origin_counts[preset]);
|
||||
assert(node.sensorFloodAttempts(&packet) == origin_counts[preset]);
|
||||
for (uint8_t i = 0; i < origin_counts[preset]; ++i) {
|
||||
assert(node.getDirectRetryCodingRateForAttempt(5, i + 1) == expected_cr5[i]);
|
||||
}
|
||||
const auto settings = directSettings(node._prefs);
|
||||
const auto schedule = directSchedule(node, snr);
|
||||
CLI cli{&node._prefs, &node};
|
||||
char reply[160] = {};
|
||||
cli.set("flood.retry.count 0", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && cli.saves == 1);
|
||||
assert(node._prefs.retry_preset == RETRY_PRESET_CUSTOM);
|
||||
assert(node.getFloodRetryMaxAttempts(&packet) == 0);
|
||||
assert(node.roomFloodAttempts(&packet) == 0);
|
||||
assert(node.sensorFloodAttempts(&packet) == 0);
|
||||
assert(node.flood_cancellations == 1 && node.direct_cancellations == 0);
|
||||
assert(directSettings(node._prefs) == settings);
|
||||
assert(directSchedule(node, snr) == schedule);
|
||||
}
|
||||
}
|
||||
puts("15 preset/disabled-flood schedule checks passed");
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix=".tmp-retry-cr-", dir=ROOT) as directory:
|
||||
work = Path(directory)
|
||||
source = work / "retry-cr.cpp"
|
||||
source.write_text(generated, encoding="ascii")
|
||||
binary = work / "retry-cr"
|
||||
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
str(source), "-o", str(binary)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn("15 preset/disabled-flood schedule checks passed", checked.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <helpers/FloodAdvertCLI.h>
|
||||
#include <helpers/StaticPoolPacketManager.h>
|
||||
#include <helpers/ota/OtaFormat.h>
|
||||
#include <vector>
|
||||
|
||||
class TraceTestClock : public mesh::MillisecondClock {
|
||||
public:
|
||||
@@ -165,6 +166,186 @@ static mesh::Packet makeFloodPacket(uint8_t payload_type) {
|
||||
return packet;
|
||||
}
|
||||
|
||||
class RetryCodingRateRadio : public TraceTestRadio {
|
||||
public:
|
||||
uint8_t cr = 5;
|
||||
std::vector<uint8_t> transmitted_crs;
|
||||
|
||||
bool setCodingRate(uint8_t value) override { cr = value; return true; }
|
||||
bool startSendRaw(const uint8_t* bytes, int length) override {
|
||||
transmitted_crs.push_back(cr);
|
||||
return TraceTestRadio::startSendRaw(bytes, length);
|
||||
}
|
||||
};
|
||||
|
||||
class RetryCodingRateMesh : public TraceTestMesh {
|
||||
public:
|
||||
using TraceTestMesh::TraceTestMesh;
|
||||
uint8_t base_cr = 5;
|
||||
uint8_t flood_attempts = 15;
|
||||
uint8_t direct_attempts = 15;
|
||||
|
||||
uint8_t getDefaultTxCodingRate() const override { return base_cr; }
|
||||
uint8_t getFloodRetryMaxAttempts(const mesh::Packet*) const override { return flood_attempts; }
|
||||
uint8_t getDirectRetryMaxAttempts(const mesh::Packet*) const override { return direct_attempts; }
|
||||
bool allowDirectRetry(const mesh::Packet*, const uint8_t*, uint8_t) const override { return true; }
|
||||
|
||||
uint8_t floodCR(const mesh::Packet& packet, uint8_t attempt) {
|
||||
mesh::Packet retry = packet;
|
||||
configureFloodRetryPacket(&retry, &packet, attempt);
|
||||
return retry.tx_cr;
|
||||
}
|
||||
uint8_t directCR(uint8_t attempt) {
|
||||
mesh::Packet original, retry;
|
||||
configureDirectRetryPacket(&retry, &original, attempt);
|
||||
return retry.tx_cr;
|
||||
}
|
||||
void disableFloodRetries() {
|
||||
flood_attempts = 0;
|
||||
floodRetriesAllowed = false;
|
||||
cancelAllFloodRetries();
|
||||
}
|
||||
};
|
||||
|
||||
class RetryCodingRateTest : public testing::Test {
|
||||
protected:
|
||||
TraceTestClock clock;
|
||||
TraceTestRTC rtc;
|
||||
TraceTestRNG rng;
|
||||
RetryCodingRateRadio radio;
|
||||
TraceTestTables tables;
|
||||
StaticPoolPacketManager manager{12};
|
||||
RetryCodingRateMesh node{radio, clock, rng, rtc, manager, tables};
|
||||
|
||||
void SetUp() override { node.begin(); }
|
||||
|
||||
void transmitNext() {
|
||||
clock.now += 10000;
|
||||
node.loop();
|
||||
ASSERT_TRUE(radio.sending);
|
||||
radio.complete = true;
|
||||
++clock.now;
|
||||
node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(node.base_cr, radio.cr); // the next packet and RX use the normal CR
|
||||
}
|
||||
|
||||
void queueFlood(bool scoped = false) {
|
||||
auto* packet = manager.allocNew();
|
||||
ASSERT_NE(nullptr, packet);
|
||||
*packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT);
|
||||
if (scoped) {
|
||||
uint16_t codes[] = {0x1234, 0x5678};
|
||||
ASSERT_TRUE(node.sendFlood(packet, codes, 0, 3));
|
||||
} else {
|
||||
ASSERT_TRUE(node.sendFlood(packet));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(RetryCodingRateTest, HopZeroMatchesDirectLadderFromEveryRadioCR) {
|
||||
for (uint8_t cr : {4, 5, 6, 7, 8}) {
|
||||
node.base_cr = cr;
|
||||
for (uint8_t route : {ROUTE_TYPE_FLOOD, ROUTE_TYPE_TRANSPORT_FLOOD}) {
|
||||
auto packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT);
|
||||
packet.header = route | (PAYLOAD_TYPE_GRP_TXT << PH_TYPE_SHIFT);
|
||||
packet.tx_cr = 8; // a previous retry must not advance the ladder twice
|
||||
for (uint8_t hash_size : {1, 2, 3}) {
|
||||
packet.setPathHashSizeAndCount(hash_size, 0);
|
||||
for (uint8_t attempt = 1; attempt <= 15; ++attempt) {
|
||||
SCOPED_TRACE(testing::Message() << "CR" << int(cr) << " retry " << int(attempt));
|
||||
EXPECT_EQ(node.directCR(attempt), node.floodCR(packet, attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RetryCodingRateTest, ForwardedFloodsKeepActiveCRAtEveryAttempt) {
|
||||
auto packet = makeFloodPacket(PAYLOAD_TYPE_GRP_TXT);
|
||||
packet.tx_cr = 8;
|
||||
for (uint8_t cr : {5, 6, 7, 8}) {
|
||||
node.base_cr = cr;
|
||||
for (uint8_t hash_size : {1, 2, 3}) {
|
||||
for (uint8_t hops : {1, 2, 8}) {
|
||||
packet.setPathHashSizeAndCount(hash_size, hops);
|
||||
for (uint8_t attempt = 1; attempt <= 15; ++attempt) {
|
||||
EXPECT_EQ(cr, node.floodCR(packet, attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RetryCodingRateTest, HopZeroCR5ScheduleReachesTheRadioForEachPresetBudget) {
|
||||
// infra, rooftop, mobile hop-zero budgets; the production preset/role
|
||||
// calculations are exercised separately by test_retry_cr_presets.py.
|
||||
for (uint8_t attempts : {2, 6, 15}) {
|
||||
for (bool scoped : {false, true}) {
|
||||
node.begin();
|
||||
node.flood_attempts = attempts;
|
||||
radio.transmitted_crs.clear();
|
||||
queueFlood(scoped);
|
||||
std::vector<uint8_t> expected{5}; // initial transmission
|
||||
for (uint8_t i = 0; i <= attempts; ++i) {
|
||||
if (i > 0) expected.push_back(i == 1 ? 5 : i <= 3 ? 7 : 8);
|
||||
transmitNext();
|
||||
}
|
||||
EXPECT_EQ(expected, radio.transmitted_crs);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(RetryCodingRateTest, DisabledFloodRetrySendsOnlyTheInitialPacket) {
|
||||
node.disableFloodRetries();
|
||||
queueFlood();
|
||||
transmitNext();
|
||||
EXPECT_EQ((std::vector<uint8_t>{5}), radio.transmitted_crs);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
}
|
||||
|
||||
TEST_F(RetryCodingRateTest, DisablingFloodRetryMidSequenceCancelsEscalatedCopies) {
|
||||
queueFlood();
|
||||
transmitNext();
|
||||
transmitNext();
|
||||
transmitNext();
|
||||
ASSERT_EQ(1, manager.getOutboundTotal());
|
||||
EXPECT_EQ(7, manager.getOutboundByIdx(0)->tx_cr);
|
||||
node.disableFloodRetries();
|
||||
clock.now += 10000;
|
||||
node.loop();
|
||||
EXPECT_FALSE(radio.sending);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
EXPECT_EQ((std::vector<uint8_t>{5, 5, 7}), radio.transmitted_crs);
|
||||
}
|
||||
|
||||
TEST_F(RetryCodingRateTest, DirectTransmissionsKeepTheirScheduleWithFloodRetryOff) {
|
||||
for (uint8_t attempts : {4, 15}) {
|
||||
for (bool flood_enabled : {true, false}) {
|
||||
node.begin();
|
||||
node.direct_attempts = attempts;
|
||||
node.floodRetriesAllowed = flood_enabled;
|
||||
node.flood_attempts = flood_enabled ? 15 : 0;
|
||||
radio.transmitted_crs.clear();
|
||||
auto* packet = node.obtainNewPacket();
|
||||
ASSERT_NE(nullptr, packet);
|
||||
packet->header = PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT;
|
||||
packet->payload_len = 4;
|
||||
memset(packet->payload, 0x34, packet->payload_len);
|
||||
const uint8_t path[] = {0x12, 0x34};
|
||||
ASSERT_TRUE(node.sendDirect(packet, path, sizeof(path)));
|
||||
std::vector<uint8_t> expected{5};
|
||||
for (uint8_t i = 0; i <= attempts; ++i) {
|
||||
if (i > 0) expected.push_back(i == 1 ? 5 : i <= 3 ? 7 : 8);
|
||||
transmitNext();
|
||||
}
|
||||
EXPECT_EQ(expected, radio.transmitted_crs);
|
||||
EXPECT_EQ(0, manager.getOutboundTotal());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mesh::Packet makeOtaManifestFragment(uint8_t format_version) {
|
||||
mesh::Packet packet = makeFloodPacket(PAYLOAD_TYPE_OTA);
|
||||
packet.payload_len = 12;
|
||||
|
||||
Reference in new issue
Block a user