mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-09 11:27:29 +00:00
Added adaptive coding rate support and unit tests
This commit is contained in:
43
Dockerfile.test
Normal file
43
Dockerfile.test
Normal file
@@ -0,0 +1,43 @@
|
||||
# Minimal container to run PlatformIO native/portduino tests
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
python3 \
|
||||
python3-pip \
|
||||
git \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libncurses5 \
|
||||
libsdl2-dev \
|
||||
libx11-dev \
|
||||
libxext-dev \
|
||||
libusb-1.0-0-dev \
|
||||
libyaml-cpp-dev \
|
||||
libuv1-dev \
|
||||
libgpiod-dev \
|
||||
libbluetooth-dev \
|
||||
libulfius-dev \
|
||||
liborcania-dev \
|
||||
libmicrohttpd-dev \
|
||||
libjansson-dev \
|
||||
libgnutls28-dev \
|
||||
libcurl4-gnutls-dev \
|
||||
libi2c-dev \
|
||||
openssl \
|
||||
lsb-release \
|
||||
cppcheck \
|
||||
uuid-dev \
|
||||
zlib1g-dev \
|
||||
libbsd-dev \
|
||||
gdb \
|
||||
&& pip3 install --no-cache-dir platformio \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["bash"]
|
||||
@@ -238,6 +238,12 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
|
||||
// call to startRetransmission.
|
||||
packetPool.release(p);
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
if (iface) {
|
||||
iface->clearAdaptiveCodingRateState(getFrom(p), p->id);
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
|
||||
@@ -618,6 +618,13 @@ void RadioInterface::applyModemConfig()
|
||||
slotTimeMsec = computeSlotTimeMsec();
|
||||
preambleTimeMsec = preambleLength * (pow_of_2(sf) / bw);
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
if (adaptiveCrOverride >= 5 && adaptiveCrOverride <= 8 && cr != adaptiveCrOverride) {
|
||||
cr = adaptiveCrOverride;
|
||||
LOG_DEBUG("Adaptive coding rate override set to %u", cr);
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("Radio freq=%.3f, config.lora.frequency_offset=%.3f", freq, loraConfig.frequency_offset);
|
||||
LOG_INFO("Set radio: region=%s, name=%s, config=%u, ch=%d, power=%d", myRegion->name, channelName, loraConfig.modem_preset,
|
||||
channel_num, power);
|
||||
@@ -730,3 +737,70 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p)
|
||||
sendingPacket = p;
|
||||
return p->encrypted.size + sizeof(PacketHeader);
|
||||
}
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
uint8_t RadioInterface::computeAdaptiveCodingRate(uint8_t attempt) const
|
||||
{
|
||||
if (attempt <= 1) {
|
||||
return 5; // Attempt 1: 4/5
|
||||
}
|
||||
if (attempt == 2) {
|
||||
return 7; // Attempt 2: 4/7
|
||||
}
|
||||
return 8; // Attempt 3+: 4/8
|
||||
}
|
||||
|
||||
uint64_t RadioInterface::adaptiveKey(NodeNum from, PacketId id) const
|
||||
{
|
||||
return (static_cast<uint64_t>(from) << 32) | id;
|
||||
}
|
||||
|
||||
void RadioInterface::pruneAdaptiveAttempts(uint32_t now)
|
||||
{
|
||||
const uint32_t expiryMsec = 5 * 60 * 1000UL; // drop state after 5 minutes
|
||||
for (auto it = adaptiveAttempts.begin(); it != adaptiveAttempts.end();) {
|
||||
if (now - it->second.lastUseMsec > expiryMsec) {
|
||||
it = adaptiveAttempts.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t RadioInterface::recordAdaptiveAttempt(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
pruneAdaptiveAttempts(now);
|
||||
|
||||
const uint64_t key = adaptiveKey(getFrom(p), p->id);
|
||||
auto &state = adaptiveAttempts[key];
|
||||
if (state.attempts < UINT8_MAX) {
|
||||
state.attempts++;
|
||||
}
|
||||
state.lastUseMsec = now;
|
||||
return state.attempts;
|
||||
}
|
||||
|
||||
bool RadioInterface::applyAdaptiveCodingRate(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
const uint8_t attempt = recordAdaptiveAttempt(p);
|
||||
const uint8_t desiredCr = computeAdaptiveCodingRate(attempt);
|
||||
if (desiredCr < 5 || desiredCr > 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
adaptiveCrOverride = desiredCr;
|
||||
if (cr != desiredCr) {
|
||||
cr = desiredCr;
|
||||
reconfigure();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RadioInterface::clearAdaptiveCodingRateState(NodeNum from, PacketId id)
|
||||
{
|
||||
adaptiveAttempts.erase(adaptiveKey(from, id));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "PointerQueue.h"
|
||||
#include "airtime.h"
|
||||
#include "error.h"
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
#include <unordered_map>
|
||||
#endif
|
||||
|
||||
#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission
|
||||
|
||||
@@ -220,6 +223,11 @@ class RadioInterface
|
||||
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
|
||||
static bool uses_default_frequency_slot;
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
/** Clear adaptive coding rate tracking for a completed packet id */
|
||||
void clearAdaptiveCodingRateState(NodeNum from, PacketId id);
|
||||
#endif
|
||||
|
||||
protected:
|
||||
int8_t power = 17; // Set by applyModemConfig()
|
||||
|
||||
@@ -250,6 +258,20 @@ class RadioInterface
|
||||
*/
|
||||
virtual void saveChannelNum(uint32_t savedChannelNum);
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
bool applyAdaptiveCodingRate(const meshtastic_MeshPacket *p);
|
||||
struct AdaptiveAttemptState {
|
||||
uint8_t attempts = 0;
|
||||
uint32_t lastUseMsec = 0;
|
||||
};
|
||||
std::unordered_map<uint64_t, AdaptiveAttemptState> adaptiveAttempts;
|
||||
uint8_t adaptiveCrOverride = 0;
|
||||
uint8_t recordAdaptiveAttempt(const meshtastic_MeshPacket *p);
|
||||
uint8_t computeAdaptiveCodingRate(uint8_t attempt) const;
|
||||
void pruneAdaptiveAttempts(uint32_t now);
|
||||
uint64_t adaptiveKey(NodeNum from, PacketId id) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
/**
|
||||
* Convert our modemConfig enum into wf, sf, etc...
|
||||
|
||||
@@ -540,6 +540,9 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
|
||||
packetPool.release(txp);
|
||||
return false;
|
||||
} else {
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
applyAdaptiveCodingRate(txp);
|
||||
#endif
|
||||
configHardwareForSend(); // must be after setStandby
|
||||
|
||||
size_t numbytes = beginSending(txp);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "SerialConsole.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "mesh/MeshRadio.h"
|
||||
#include "mesh/NodeDB.h"
|
||||
|
||||
#include "TestUtil.h"
|
||||
|
||||
@@ -14,5 +16,19 @@ void initializeTestEnvironment()
|
||||
tv.tv_usec = 0;
|
||||
perhapsSetRTC(RTCQualityNTP, &tv);
|
||||
#endif
|
||||
concurrency::OSThread::setup();
|
||||
}
|
||||
|
||||
void initializeTestEnvironmentMinimal()
|
||||
{
|
||||
// Only satisfy OSThread assertions; skip SerialConsole and platform-specific setup
|
||||
concurrency::hasBeenSetup = true;
|
||||
|
||||
// Ensure region/config globals are sane before any RadioInterface instances compute slot timing
|
||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
config.lora.use_preset = true;
|
||||
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
||||
initRegion();
|
||||
|
||||
concurrency::OSThread::setup();
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
// Initialize testing environment.
|
||||
void initializeTestEnvironment();
|
||||
void initializeTestEnvironment();
|
||||
|
||||
// Minimal init without creating SerialConsole or portduino peripherals (useful for lightweight logic tests)
|
||||
void initializeTestEnvironmentMinimal();
|
||||
164
test/test_adaptive_coding_rate/AdaptiveCodingRate.cpp
Normal file
164
test/test_adaptive_coding_rate/AdaptiveCodingRate.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include <Arduino.h>
|
||||
#include <unity.h>
|
||||
|
||||
#include "TestUtil.h"
|
||||
// Ensure adaptive coding rate logic is available during tests
|
||||
#ifndef USE_ADAPTIVE_CODING_RATE
|
||||
#define USE_ADAPTIVE_CODING_RATE 1
|
||||
#endif
|
||||
#include "mesh/RadioInterface.h"
|
||||
|
||||
class TestRadio : public RadioInterface
|
||||
{
|
||||
public:
|
||||
bool applyForTest(const meshtastic_MeshPacket *p) { return applyAdaptiveCodingRate(p); }
|
||||
|
||||
uint8_t getAttempts(NodeNum from, PacketId id)
|
||||
{
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
auto it = adaptiveAttempts.find(adaptiveKey(from, id));
|
||||
return (it == adaptiveAttempts.end()) ? 0 : it->second.attempts;
|
||||
#else
|
||||
(void)from;
|
||||
(void)id;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
void setAdaptiveState(NodeNum from, PacketId id, uint8_t attempts, uint32_t lastUse)
|
||||
{
|
||||
adaptiveAttempts[adaptiveKey(from, id)] = {attempts, lastUse};
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t currentCr() const { return cr; }
|
||||
void setCrForTest(uint8_t value) { cr = value; }
|
||||
|
||||
ErrorCode send(meshtastic_MeshPacket *p) override
|
||||
{
|
||||
packetPool.release(p);
|
||||
return ERRNO_OK;
|
||||
}
|
||||
|
||||
uint32_t getPacketTime(uint32_t /*totalPacketLen*/, bool /*received*/ = false) override { return 0; }
|
||||
|
||||
bool reconfigure() override
|
||||
{
|
||||
reconfigureCount++;
|
||||
lastCr = cr;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t reconfigureCount = 0;
|
||||
uint8_t lastCr = 0;
|
||||
};
|
||||
|
||||
void test_attempt_progression()
|
||||
{
|
||||
TestRadio radio;
|
||||
meshtastic_MeshPacket packet = {};
|
||||
packet.from = 0xABCDEF01;
|
||||
packet.id = 0x1;
|
||||
|
||||
TEST_ASSERT_FALSE(radio.applyForTest(&packet));
|
||||
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(5, radio.currentCr());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, radio.reconfigureCount);
|
||||
|
||||
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
|
||||
TEST_ASSERT_EQUAL_UINT8(2, radio.getAttempts(packet.from, packet.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(7, radio.currentCr());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, radio.reconfigureCount);
|
||||
TEST_ASSERT_EQUAL_UINT8(7, radio.lastCr);
|
||||
|
||||
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
|
||||
TEST_ASSERT_EQUAL_UINT8(3, radio.getAttempts(packet.from, packet.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(8, radio.currentCr());
|
||||
TEST_ASSERT_EQUAL_UINT32(2, radio.reconfigureCount);
|
||||
TEST_ASSERT_EQUAL_UINT8(8, radio.lastCr);
|
||||
}
|
||||
|
||||
void test_attempts_are_per_packet()
|
||||
{
|
||||
TestRadio radio;
|
||||
meshtastic_MeshPacket first = {};
|
||||
first.from = 0x1001;
|
||||
first.id = 0xA;
|
||||
|
||||
meshtastic_MeshPacket second = {};
|
||||
second.from = 0x1001;
|
||||
second.id = 0xB;
|
||||
|
||||
radio.applyForTest(&first);
|
||||
radio.applyForTest(&second);
|
||||
radio.applyForTest(&first);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT8(2, radio.getAttempts(first.from, first.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(second.from, second.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(7, radio.currentCr());
|
||||
}
|
||||
|
||||
void test_clear_resets_attempts_and_rate()
|
||||
{
|
||||
TestRadio radio;
|
||||
meshtastic_MeshPacket packet = {};
|
||||
packet.from = 0xCAFE;
|
||||
packet.id = 0x55;
|
||||
|
||||
radio.applyForTest(&packet);
|
||||
radio.applyForTest(&packet);
|
||||
radio.applyForTest(&packet);
|
||||
|
||||
radio.reconfigureCount = 0;
|
||||
radio.setCrForTest(8);
|
||||
radio.clearAdaptiveCodingRateState(packet.from, packet.id);
|
||||
|
||||
TEST_ASSERT_TRUE(radio.applyForTest(&packet));
|
||||
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
|
||||
TEST_ASSERT_EQUAL_UINT8(5, radio.currentCr());
|
||||
TEST_ASSERT_EQUAL_UINT32(1, radio.reconfigureCount);
|
||||
}
|
||||
|
||||
void test_prunes_expired_state()
|
||||
{
|
||||
TestRadio radio;
|
||||
meshtastic_MeshPacket packet = {};
|
||||
packet.from = 0xBEEF;
|
||||
packet.id = 0x99;
|
||||
|
||||
radio.applyForTest(&packet);
|
||||
#ifdef USE_ADAPTIVE_CODING_RATE
|
||||
const uint32_t now = millis();
|
||||
radio.setAdaptiveState(packet.from, packet.id, 3, now - (5 * 60 * 1000UL + 50));
|
||||
#endif
|
||||
radio.reconfigureCount = 0;
|
||||
radio.setCrForTest(5);
|
||||
|
||||
TEST_ASSERT_FALSE(radio.applyForTest(&packet));
|
||||
TEST_ASSERT_EQUAL_UINT8(1, radio.getAttempts(packet.from, packet.id));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, radio.reconfigureCount);
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
printf("AdaptiveCodingRate test setup start\n");
|
||||
fflush(stdout);
|
||||
// Use minimal init to avoid pulling in SerialConsole/portduino peripherals for these logic-only tests
|
||||
initializeTestEnvironmentMinimal();
|
||||
|
||||
printf("AdaptiveCodingRate test init done\n");
|
||||
fflush(stdout);
|
||||
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_attempt_progression);
|
||||
RUN_TEST(test_attempts_are_per_packet);
|
||||
RUN_TEST(test_clear_resets_attempts_and_rate);
|
||||
RUN_TEST(test_prunes_expired_state);
|
||||
UNITY_END();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
delay(1000);
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
extends = portduino_base
|
||||
build_flags = ${portduino_base.build_flags} -I variants/native/portduino
|
||||
-I /usr/include
|
||||
-I /opt/homebrew/include
|
||||
-L /opt/homebrew/lib -largp
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
board = cross_platform
|
||||
board_level = extra
|
||||
lib_deps =
|
||||
@@ -9,6 +12,11 @@ lib_deps =
|
||||
# renovate: datasource=custom.pio depName=Melopero RV3028 packageName=melopero/library/Melopero RV3028
|
||||
melopero/Melopero RV3028@1.2.0
|
||||
|
||||
; Disable LovyanGFX for native test builds to avoid missing macOS system headers
|
||||
lib_ignore =
|
||||
${portduino_base.lib_ignore}
|
||||
LovyanGFX
|
||||
|
||||
build_src_filter = ${portduino_base.build_src_filter}
|
||||
|
||||
[env:native]
|
||||
|
||||
@@ -7,6 +7,7 @@ build_flags =
|
||||
${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/ELECROW-ThinkNode-M3
|
||||
-DELECROW_ThinkNode_M3
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
-DGPS_POWER_TOGGLE
|
||||
-D CONFIG_NFCT_PINS_AS_GPIOS=1
|
||||
-L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard"
|
||||
|
||||
@@ -9,6 +9,7 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/heltec_mesh_pocket
|
||||
-DHELTEC_MESH_POCKET
|
||||
-DHELTEC_MESH_POCKET_BATTERY_5000
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
-DUSE_EINK
|
||||
-DEINK_DISPLAY_MODEL=GxEPD2_213_B74
|
||||
-DEINK_WIDTH=250
|
||||
@@ -38,6 +39,7 @@ build_flags =
|
||||
-I variants/nrf52840/heltec_mesh_pocket
|
||||
-D HELTEC_MESH_POCKET
|
||||
-D HELTEC_MESH_POCKET_BATTERY_5000
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
lib_deps =
|
||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||
${nrf52840_base.lib_deps}
|
||||
@@ -54,6 +56,7 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
-Ivariants/nrf52840/heltec_mesh_pocket
|
||||
-DHELTEC_MESH_POCKET
|
||||
-DHELTEC_MESH_POCKET_BATTERY_10000
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
-DUSE_EINK
|
||||
-DEINK_DISPLAY_MODEL=GxEPD2_213_B74
|
||||
-DEINK_WIDTH=250
|
||||
@@ -83,6 +86,7 @@ build_flags =
|
||||
-I variants/nrf52840/heltec_mesh_pocket
|
||||
-D HELTEC_MESH_POCKET
|
||||
-D HELTEC_MESH_POCKET_BATTERY_10000
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
lib_deps =
|
||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||
${nrf52840_base.lib_deps}
|
||||
|
||||
@@ -7,6 +7,7 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
-I variants/nrf52840/rak_wismeshtag
|
||||
-D WISMESH_TAG
|
||||
-D RAK_4631
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
-DRADIOLIB_EXCLUDE_SX128X=1
|
||||
-DRADIOLIB_EXCLUDE_SX127X=1
|
||||
-DRADIOLIB_EXCLUDE_LR11X0=1
|
||||
|
||||
@@ -7,6 +7,7 @@ build_flags = ${nrf52840_base.build_flags}
|
||||
-Isrc/platform/nrf52/softdevice
|
||||
-Isrc/platform/nrf52/softdevice/nrf52
|
||||
-DTRACKER_T1000_E
|
||||
-DUSE_ADAPTIVE_CODING_RATE
|
||||
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1
|
||||
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
||||
-DMESHTASTIC_EXCLUDE_SCREEN=1
|
||||
|
||||
Reference in New Issue
Block a user