From 95e3cf2618ad5bad1b1261d997718642b2d299a4 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:54:48 +0200 Subject: [PATCH] feat(radio): honest TX power for GAT562 30S external PA gat562_30s_mesh_kit has an always-on external PA rated at +30dBm, but the firmware wrote tx_power_dbm straight to the SX1262 register (capped at 22), so the app/CLI/UI never matched the real radiated power and couldn't ask for more than 22 even though the hardware supports it. Add a generic, macro-gated PA gain-curve resolution in RadioLibWrapper::setTxPower() (the single choke point every TX-power path already funnels through), reusable by any future board with a similar PA. For this board, reuse the vendor-measured 869MHz curve from the open upstream Meshtastic PR that added it for the same physical module (meshtastic/firmware#11212): pick the lowest SX1262 setting whose measured output reaches the requested dBm, clamped at the PA's saturation knee. Bump LORA_TX_POWER/MAX_LORA_TX_POWER to 30 for this variant only -- the other three GAT562 boards use the SX1262's own PA_BOOST (22dBm, no external PA) and are unaffected. Since a request below the PA's floor gain (14dBm) still radiates at that floor, also make CommonCLICallbacks::setTxPower() return what was actually applied, and have every setter (BLE CMD_SET_RADIO_TX_POWER, CLI "tx", simple_repeater/room_server/sensor) persist and report that value instead of the raw request -- so tx_power_dbm never silently disagrees with the physical output in either direction. Co-Authored-By: Claude Sonnet 5 --- examples/companion_radio/MyMesh.cpp | 6 ++++-- examples/simple_repeater/MyMesh.cpp | 3 ++- examples/simple_repeater/MyMesh.h | 2 +- examples/simple_room_server/MyMesh.cpp | 3 ++- examples/simple_room_server/MyMesh.h | 2 +- examples/simple_sensor/SensorMesh.cpp | 3 ++- examples/simple_sensor/SensorMesh.h | 2 +- src/helpers/CommonCLI.cpp | 5 +++-- src/helpers/CommonCLI.h | 6 +++++- src/helpers/radiolib/CustomSX1262Wrapper.h | 2 +- src/helpers/radiolib/RadioLibWrappers.cpp | 17 ++++++++++++++++- variants/gat562_30s_mesh_kit/platformio.ini | 11 ++++++++++- 12 files changed, 48 insertions(+), 14 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index feda3545..5b590379 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2488,9 +2488,11 @@ void MyMesh::handleCmdFrame(size_t len) { if (power < -9 || power > MAX_LORA_TX_POWER) { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } else { - _prefs.tx_power_dbm = power; + radio_driver.setTxPower(power); + // Store what the radio actually applied, not the raw request -- on a + // board with an external-PA gain curve those can differ (see setTxPower()). + _prefs.tx_power_dbm = radio_driver.getTxPower(); savePrefs(); - radio_driver.setTxPower(_prefs.tx_power_dbm); writeOKFrame(); } } else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ac0965bd..370ef549 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1095,8 +1095,9 @@ void MyMesh::dumpLogFile() { } } -void MyMesh::setTxPower(int8_t power_dbm) { +int8_t MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); + return radio_driver.getTxPower(); } bool MyMesh::setRxBoostedGain(bool enable) { diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 04bd4fb9..bd561a2d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -212,7 +212,7 @@ public: } void dumpLogFile() override; - void setTxPower(int8_t power_dbm) override; + int8_t setTxPower(int8_t power_dbm) override; void formatNeighborsReply(char *reply) override; void removeNeighbor(const uint8_t* pubkey, int key_len) override; void formatStatsReply(char *reply) override; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index f2588ec8..606110f2 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -839,8 +839,9 @@ void MyMesh::dumpLogFile() { } } -void MyMesh::setTxPower(int8_t power_dbm) { +int8_t MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); + return radio_driver.getTxPower(); } bool MyMesh::setRxBoostedGain(bool enable) { diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6..b2aeae58 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -208,7 +208,7 @@ public: } void dumpLogFile() override; - void setTxPower(int8_t power_dbm) override; + int8_t setTxPower(int8_t power_dbm) override; bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char *reply) override { diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 9bfa5ec6..9513b33f 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -848,8 +848,9 @@ void SensorMesh::updateFloodAdvertTimer() { } } -void SensorMesh::setTxPower(int8_t power_dbm) { +int8_t SensorMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); + return radio_driver.getTxPower(); } void SensorMesh::formatStatsReply(char *reply) { diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index b5e96d5c..3541951c 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -67,7 +67,7 @@ public: void setLoggingOn(bool enable) override { } void eraseLogFile() override { } void dumpLogFile() override { } - void setTxPower(int8_t power_dbm) override; + int8_t setTxPower(int8_t power_dbm) override; void formatNeighborsReply(char *reply) override { strcpy(reply, "not supported"); } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58..5e598ac2 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -706,9 +706,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); + // Store what the radio actually applied, not the raw request -- on a + // board with an external-PA gain curve those can differ (see setTxPower()). + _prefs->tx_power_dbm = _callbacks->setTxPower(atoi(&config[3])); savePrefs(); - _callbacks->setTxPower(_prefs->tx_power_dbm); strcpy(reply, "OK"); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 237c758e..db2d201b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -207,7 +207,11 @@ public: virtual void setLoggingOn(bool enable) = 0; virtual void eraseLogFile() = 0; virtual void dumpLogFile() = 0; - virtual void setTxPower(int8_t power_dbm) = 0; + // Returns the dBm the radio actually applied -- on a board with an + // external-PA gain curve this can differ from power_dbm (e.g. a request + // below the PA's floor gain still radiates at that floor), so callers + // should persist/report the return value, not the raw request. + virtual int8_t setTxPower(int8_t power_dbm) = 0; virtual void formatNeighborsReply(char *reply) = 0; virtual void removeNeighbor(const uint8_t* pubkey, int key_len) { // no op by default diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 06453868..82005ad6 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -97,7 +97,7 @@ public: ((CustomSX1262 *)_radio)->setCodingRate(_wd_cr); updatePreamble(_wd_sf); } - _radio->setOutputPower(getTxPower()); + setTxPower(getTxPower()); // re-run through the wrapper's own setter so any PA gain curve reapplies // Unconditional: std_init() may have just turned boosted gain back ON via // the board's SX126X_RX_BOOSTED_GAIN compile default, so the OFF case // needs reapplying just as much as ON. diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 485f743e..d17d5777 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -52,8 +52,23 @@ uint32_t RadioLibWrapper::getRngSeed() { } void RadioLibWrapper::setTxPower(int8_t dbm) { - _tx_dbm = dbm; + _tx_dbm = dbm; // logical/requested power -- what getTxPower(), the app and the CLI report +#if defined(NUM_PA_POINTS) && defined(TX_GAIN_LORA) + // Per-board external-PA gain curve (radio_dbm -> measured gain in dB, from + // the vendor/GAT562-30S data cited in the upstream Meshtastic PR that added + // it, meshtastic/firmware#11212). The PA is always-on (no enable pin), so + // the SX1262 register alone decides real output; pick the lowest register + // setting whose (setting + its measured gain) reaches the requested dBm, + // clamping to the last entry once the PA saturates. + static const int8_t pa_gain[NUM_PA_POINTS] = { TX_GAIN_LORA }; + int8_t radio_dbm = NUM_PA_POINTS - 1; + for (int i = 0; i < NUM_PA_POINTS; i++) { + if ((i + pa_gain[i]) >= dbm) { radio_dbm = i; break; } + } + _radio->setOutputPower(radio_dbm); +#else _radio->setOutputPower(dbm); +#endif } void RadioLibWrapper::idle() { diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index ef6e9f6d..bf4cfd25 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -14,7 +14,16 @@ build_flags = ${nrf52_base.build_flags} -D UI_HAS_JOYSTICK=1 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper - -D LORA_TX_POWER=22 + -D LORA_TX_POWER=30 + -D MAX_LORA_TX_POWER=30 + ; External PA (not the bare SX1262's own PA_BOOST), always on, no enable + ; pin -- gain curve measured by the vendor at 869MHz, from the upstream + ; Meshtastic PR that added it for this exact module (meshtastic/firmware#11212). + ; Picks the lowest SX1262 register setting whose (setting + gain) reaches + ; the requested dBm, so "30" in the app/CLI means the real ~30dBm the PA + ; saturates at, not a raw, unamplified chip register value. + -D NUM_PA_POINTS=16 + -D TX_GAIN_LORA=14,15,16,17,17,18,18,18,18,18,17,17,17,16,16,15 -D SX126X_CURRENT_LIMIT=140 -D PIN_BUZZER=33 -D SX126X_RX_BOOSTED_GAIN=1