Files
MeshCore-Solo/src/helpers/radiolib/CustomSX1262Wrapper.h
T
JakubandClaude Sonnet 5 95e3cf2618 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 <noreply@anthropic.com>
2026-09-16 12:54:48 +02:00

109 lines
4.7 KiB
C++

#pragma once
#include "CustomSX1262.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
#ifndef USE_SX1262
#define USE_SX1262
#endif
class CustomSX1262Wrapper : public RadioLibWrapper {
// Cached runtime radio params, only used to recover from a hard reset (see
// radioHardReset()): std_init() re-applies the compiled LORA_FREQ/BW/SF/CR
// firmware defaults, not whatever the user has actually configured, so
// these are needed to restore real state afterwards.
float _wd_freq = 0, _wd_bw = 0;
uint8_t _wd_sf = 0, _wd_cr = 0;
bool _wd_params_valid = false;
bool _wd_rx_boosted_gain = false;
public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
_wd_freq = freq; _wd_bw = bw; _wd_sf = sf; _wd_cr = cr; _wd_params_valid = true;
((CustomSX1262 *)_radio)->setFrequency(freq);
((CustomSX1262 *)_radio)->setSpreadingFactor(sf);
((CustomSX1262 *)_radio)->setBandwidth(bw);
((CustomSX1262 *)_radio)->setCodingRate(cr);
updatePreamble(sf);
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf));
((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis);
((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis);
}
// From RadioLib's SX1262::setFrequency(): RADIOLIB_CHECK_RANGE(freq, 150.0f, 960.0f, ...).
void getFreqBounds(float& min_mhz, float& max_mhz) const override { min_mhz = 150.0f; max_mhz = 960.0f; }
bool isReceivingPacket() override {
return ((CustomSX1262 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1262 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
uint8_t getSpreadingFactor() const override { return ((CustomSX1262 *)_radio)->spreadingFactor; }
virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false);
}
// Power-save RX = hardware RX duty-cycle (SX126x SetRxDutyCycle, datasheet
// 13.1.7). The chip's sequencer cycles RX↔sleep on its own, latches a preamble
// of the configured length and then stays in RX to receive the packet, raising
// RX_DONE on DIO1 — handled by the normal recvRaw() path, no MCU polling.
// minSymbols=8 is the reliable preamble-latch count for SF7-12. If the
// configured preamble is too short for a real duty-cycle (senderPreamble <
// 2*minSymbols+1), RadioLib transparently falls back to a continuous receive.
int16_t startPowerSaveRecv() override {
return ((SX126x *)_radio)->startReceiveDutyCycleAuto(preambleLengthForSF(_preamble_sf), 8);
}
bool setRxBoostedGainMode(bool en) override {
_wd_rx_boosted_gain = en;
return ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE;
}
bool getRxBoostedGainMode() const override {
return ((CustomSX1262 *)_radio)->getRxBoostedGainMode();
}
bool supportsRxPsWatchdog() const override { return true; }
// BUSY is high whenever the chip can't service SPI -- including the sleep
// window of an armed RX duty-cycle. Same access pattern already used by
// sx126xResetAGC() in SX126xReset.h.
bool isChipBusy() override {
SX126x* radio = (SX126x *)_radio;
return radio->mod->hal->digitalRead(radio->mod->getGpio());
}
// Full chip reset + re-init after a stuck RX duty-cycle that a soft re-arm
// didn't clear. std_init() re-applies compiled firmware defaults, not the
// user's runtime settings, so reapply the cached params and re-attach the
// packet-received action once it returns.
bool radioHardReset() override {
if (!((CustomSX1262 *)_radio)->std_init(&SPI)) return false;
reattachRecvAction();
if (_wd_params_valid) {
((CustomSX1262 *)_radio)->setFrequency(_wd_freq);
((CustomSX1262 *)_radio)->setSpreadingFactor(_wd_sf);
((CustomSX1262 *)_radio)->setBandwidth(_wd_bw);
((CustomSX1262 *)_radio)->setCodingRate(_wd_cr);
updatePreamble(_wd_sf);
}
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.
((CustomSX1262 *)_radio)->setRxBoostedGainMode(_wd_rx_boosted_gain);
return true;
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio, getRxBoostedGainMode()); }
};