diff --git a/zephcore/Kconfig b/zephcore/Kconfig index bfa4cea..fc4a3a1 100644 --- a/zephcore/Kconfig +++ b/zephcore/Kconfig @@ -256,6 +256,24 @@ config ZEPHCORE_RADIO_LR1110 endchoice +config ZEPHCORE_DEFAULT_TX_POWER_DBM + int "Default TX power (dBm)" + default 22 + range -9 22 + help + Default SX1262 TX power in dBm for fresh devices (no saved prefs). + Boards with external PA should set this lower to avoid overdriving. + Example: Station G2 with 20dB PA gain uses 15 dBm (→ 35 dBm output). + +config ZEPHCORE_MAX_TX_POWER_DBM + int "Maximum TX power (dBm)" + default 22 + range -9 22 + help + Hard cap on SX1262 TX power. The radio adapter clamps any value + above this, protecting external PAs from damage. + Example: Station G2 caps at 19 dBm to stay below PA P1dB (35 dBm). + endmenu # Radio Configuration menu "Board Configuration" diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 2acfc79..ff4def2 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -186,6 +186,11 @@ void LoRaRadioBase::buildModemConfig(struct lora_modem_config &cfg, bool tx) cfg.preamble_len = LoRaConfig::PREAMBLE_LEN; cfg.tx_power = _prefs ? (int8_t)_prefs->tx_power_dbm : LoRaConfig::TX_POWER_DBM; +#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM + if (cfg.tx_power > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) { + cfg.tx_power = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM; + } +#endif cfg.tx = tx; cfg.iq_inverted = false; cfg.public_network = false; diff --git a/zephcore/boards/esp32/station_g2/board.conf b/zephcore/boards/esp32/station_g2/board.conf index 871741f..3c1731d 100644 --- a/zephcore/boards/esp32/station_g2/board.conf +++ b/zephcore/boards/esp32/station_g2/board.conf @@ -17,5 +17,11 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="BQ Station G2" # Radio type — SX1262 via Zephyr native LoRa driver CONFIG_ZEPHCORE_RADIO_NATIVE=y +# TX power limits — Station G2 has ~20dB external PA. +# Default 15 dBm → ~35 dBm output (P1dB point). +# Cap at 19 dBm → ~39 dBm absolute max (matches Arduino MAX_LORA_TX_POWER). +CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=15 +CONFIG_ZEPHCORE_MAX_TX_POWER_DBM=19 + # Flash size — 16MB (override Zephyr default) CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index d7b9f5f..297e584 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -111,6 +111,11 @@ void CommonCLI::loadPrefs(const char* path) { _prefs->sf = constrain(_prefs->sf, (uint8_t)5, (uint8_t)12); _prefs->cr = constrain(_prefs->cr, (uint8_t)5, (uint8_t)8); _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, (uint8_t)1, (uint8_t)30); +#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM + if (_prefs->tx_power_dbm > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) { + _prefs->tx_power_dbm = (uint8_t)CONFIG_ZEPHCORE_MAX_TX_POWER_DBM; + } +#endif _prefs->multi_acks = constrain(_prefs->multi_acks, (uint8_t)0, (uint8_t)1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); _prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, (uint8_t)0, (uint8_t)1); @@ -317,7 +322,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "get ", 4) == 0) { const char* config = &command[4]; if (memcmp(config, "af", 2) == 0) { - snprintf(reply, CLI_REPLY_SIZE, "> %.0f%%", (double)_prefs->airtime_factor); + int dc = (_prefs->airtime_factor == 0.0f) ? 100 : (int)_prefs->airtime_factor; + snprintf(reply, CLI_REPLY_SIZE, "> Duty Cycle: %d%%", dc); } else if (memcmp(config, "int.thresh", 10) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->interference_threshold); } else if (memcmp(config, "agc.reset.interval", 18) == 0) { @@ -391,9 +397,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(command, "set ", 4) == 0) { const char* config = &command[4]; if (memcmp(config, "af ", 3) == 0) { - _prefs->airtime_factor = atof(&config[3]); + int val = atoi(&config[3]); + if (val <= 0 || val >= 100) { + _prefs->airtime_factor = 0.0f; + } else { + _prefs->airtime_factor = (float)val; + } savePrefs(); - strcpy(reply, "OK"); + int dc = (_prefs->airtime_factor == 0.0f) ? 100 : (int)_prefs->airtime_factor; + snprintf(reply, CLI_REPLY_SIZE, "Duty Cycle set: %d%%", dc); } else if (memcmp(config, "int.thresh ", 11) == 0) { _prefs->interference_threshold = atoi(&config[11]); savePrefs(); @@ -532,10 +544,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch savePrefs(); strcpy(reply, "OK"); } else if (memcmp(config, "tx ", 3) == 0) { - _prefs->tx_power_dbm = atoi(&config[3]); + int val = atoi(&config[3]); +#ifdef CONFIG_ZEPHCORE_MAX_TX_POWER_DBM + if (val > CONFIG_ZEPHCORE_MAX_TX_POWER_DBM) { + val = CONFIG_ZEPHCORE_MAX_TX_POWER_DBM; + } +#endif + _prefs->tx_power_dbm = (uint8_t)val; savePrefs(); _callbacks->setTxPower(_prefs->tx_power_dbm); - strcpy(reply, "OK"); + snprintf(reply, CLI_REPLY_SIZE, "OK - tx power=%u dBm", (uint32_t)_prefs->tx_power_dbm); } else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) { _prefs->freq = atof(&config[5]); savePrefs(); diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index e9b660e..78dee72 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -90,7 +90,11 @@ static inline void initNodePrefs(NodePrefs* prefs) { prefs->bw = 62.5f; // LoRaConfig::BANDWIDTH prefs->sf = 8; // LoRaConfig::SPREADING_FACTOR prefs->cr = 8; // CR 4/8 (MeshCore uses 5-8 for CR 4/5 through 4/8) +#ifdef CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM + prefs->tx_power_dbm = CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM; +#else prefs->tx_power_dbm = 22; // LoRaConfig::TX_POWER_DBM +#endif prefs->disable_fwd = 0; prefs->advert_interval = 60; // 2 minutes (value / 2) prefs->flood_advert_interval = 12; // 12 hours