Compare commits

..
Author SHA1 Message Date
Ricardo Guzman f2f0d1725e date and update version 2026-10-18 17:00:52 -03:00
Ricardo Guzman ef7d38e940 is2 added for testing 2026-10-18 17:00:24 -03:00
Ricardo Guzman 1591d9f7fe all updated for V4.0.4 2026-10-14 10:00:17 -03:00
Ricardo Guzman 1747922036 date update 2026-10-14 09:32:51 -03:00
Ricardo Guzman 37afd9a659 lora settings fix 2026-10-14 09:32:07 -03:00
Ricardo Guzman f9439704f7 testing Tbeam1W 2026-10-09 13:02:48 -03:00
Ricardo Guzman 9314bf10ba VEXT fix for Heltec Wireless Tracker 2026-10-07 15:09:16 -03:00
Ricardo Guzman a045599df1 gps fix for supreme 2026-10-07 14:08:56 -03:00
Ricardo Guzman 9930603e99 ALDO1 2 update 2026-10-07 13:56:44 -03:00
18 changed files with 393 additions and 31 deletions

No files matched your search

+2
View File
@@ -71,6 +71,8 @@ jobs:
chip: esp32s3
- name: ttgo_t_beam_s3_SUPREME_v3
chip: esp32s3
- name: ttgo-t-beam-1W
chip: esp32s3
- name: ESP32_DIY_LoRa_A7670_SX1278
chip: esp32
- name: ESP32_DIY_LoRa_A7670_915_SX1276
+3 -1
View File
@@ -37,7 +37,7 @@ ____________________________________________________
- TTGO Lilygo LoRa32 LoRa32 V2.1 (V1.6 is the same) and T3S3 V1.2.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules) and Supreme V3.
- TTGO T-Beam V1.0 , V1.1, V1.2 (also variations with SX1262 and SX1268 LoRa Modules), Supreme V3 and T-Beam 1W.
- T-Deck Plus (and also regular T-Deck with/without GPS).
@@ -64,6 +64,8 @@ ____________________________________________________
<br />
# Timeline (Versions):
- 2026-10-14 T-Beam 1W board added (with fan control and Tx over-temperature protection).
- 2026-10-05 Fix for stored XSS in received packets, Wifi warning with bad password, date on Rx packets, mDNS kiss-tnc for phone apps, APRSPacketLib V1.0.10 update.
- 2026-09-30 GPS baud rate auto-detection, CAD timeout (packet dropped after 10s of busy channel)
- 2026-09-23 APRSPacketLib V1.0.7 update and many code cleaning with this.
- 2026-09-17 Real APRS Symbols added to map and TNC2 output as a posibility in TNC mode.
+3 -2
View File
@@ -27,6 +27,7 @@
"active": false,
"messagesToRF": false,
"objectsToRF": false,
"is2Metadata": false,
"server": "rotate.aprs2.net",
"passcode": "XYZVW",
"port": 14580,
@@ -47,8 +48,8 @@
"rxSignalBandwidth": 125000,
"txActive": false,
"txFreq": 433775000,
"txSpreadingFactor": 9,
"txCodingRate4": 7,
"txSpreadingFactor": 12,
"txCodingRate4": 5,
"txSignalBandwidth": 125000,
"power": 20,
"cadActive": true
+24 -8
View File
@@ -937,6 +937,22 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
>
</div>
</div>
<div class="col-6">
<div class="form-check form-switch">
<input
type="checkbox"
name="aprs_is.is2Metadata"
id="aprs_is.is2Metadata"
class="form-check-input"
/>
<label
for="aprs_is.is2Metadata"
class="form-label"
>Send IS2 Metadata
(RSSI/SNR) to APRS-IS</label
>
</div>
</div>
<div class="col-8">
<label
for="aprs_is.server"
@@ -1313,10 +1329,10 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
<option value="6">SF6</option>
<option value="7">SF7</option>
<option value="8">SF8</option>
<option value="9" selected>SF9</option>
<option value="9">SF9</option>
<option value="10">SF10</option>
<option value="11">SF11</option>
<option value="12">SF12 - Highest sensitivity</option>
<option value="12" selected>SF12 - Highest sensitivity</option>
</select>
</div>
<div class="col-6 mt-3">
@@ -1335,10 +1351,10 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
<option value="6">SF6</option>
<option value="7">SF7</option>
<option value="8">SF8</option>
<option value="9" selected>SF9</option>
<option value="9">SF9</option>
<option value="10">SF10</option>
<option value="11">SF11</option>
<option value="12">SF12 - Highest sensitivity</option>
<option value="12" selected>SF12 - Highest sensitivity</option>
</select>
</div>
<div class="col-6 mt-3">
@@ -1353,9 +1369,9 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
id="lora.rxCodingRate4"
required=""
>
<option value="5">5</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7" selected>7</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
</div>
@@ -1371,9 +1387,9 @@ body{background:var(--msh-bg);color:var(--msh-text);font-family:"Inter",system-u
id="lora.txCodingRate4"
required=""
>
<option value="5">5</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7" selected>7</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
</div>
+4
View File
@@ -103,6 +103,7 @@ function loadSettings(settings) {
document.getElementById("aprs_is.active").checked = settings.aprs_is.active;
document.getElementById("aprs_is.messagesToRF").checked = settings.aprs_is.messagesToRF;
document.getElementById("aprs_is.objectsToRF").checked = settings.aprs_is.objectsToRF;
document.getElementById("aprs_is.is2Metadata").checked = settings.aprs_is.is2Metadata;
document.getElementById("aprs_is.server").value = settings.aprs_is.server;
document.getElementById("aprs_is.port").value = settings.aprs_is.port;
document.getElementById("aprs_is.filter").value = settings.aprs_is.filter;
@@ -110,6 +111,7 @@ function loadSettings(settings) {
APRSISCheckbox.checked = settings.aprs_is.active;
APRSISGateMessages.disabled = !APRSISCheckbox.checked;
APRSISGateObjects.disabled = !APRSISCheckbox.checked;
APRSISMetadata.disabled = !APRSISCheckbox.checked;
APRSISServer.disabled = !APRSISCheckbox.checked;
APRSISPort.disabled = !APRSISCheckbox.checked;
APRSISPasscode.disabled = !APRSISCheckbox.checked;
@@ -350,6 +352,7 @@ StatusCheckbox.addEventListener("change", function() {
const APRSISCheckbox = document.querySelector('input[name="aprs_is.active"]');
const APRSISGateMessages = document.querySelector('input[name="aprs_is.messagesToRF"]');
const APRSISGateObjects = document.querySelector('input[name="aprs_is.objectsToRF"]');
const APRSISMetadata = document.querySelector('input[name="aprs_is.is2Metadata"]');
const APRSISServer = document.querySelector('input[name="aprs_is.server"]');
const APRSISPort = document.querySelector('input[name="aprs_is.port"]');
const APRSISPasscode = document.querySelector('input[name="aprs_is.passcode"]');
@@ -357,6 +360,7 @@ const APRSISFilter = document.querySelector('input[name="aprs_is.fi
APRSISCheckbox.addEventListener("change", function() {
APRSISGateMessages.disabled = !this.checked;
APRSISGateObjects.disabled = !this.checked;
APRSISMetadata.disabled = !this.checked;
APRSISServer.disabled = !this.checked;
APRSISPort.disabled = !this.checked;
APRSISPasscode.disabled = !this.checked;
+1
View File
@@ -65,6 +65,7 @@ public:
String filter;
bool messagesToRF;
bool objectsToRF;
bool is2Metadata;
};
class DIGI {
+41
View File
@@ -0,0 +1,41 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
// Fan / thermal control adapted from KJ7NYE's LoRa_FieldOps_APRS_Tracker (GPLv3)
#ifndef THERMAL_UTILS_H_
#define THERMAL_UTILS_H_
#include "board_pinout.h"
#ifdef FAN_CTRL_PIN
namespace THERMAL_Utils {
void setup();
void monitor();
void onTxStart();
void onTxEnd();
void turnOffFan();
bool isTxBlocked();
}
#endif
#endif
+12 -2
View File
@@ -52,6 +52,7 @@ ___________________________________________________________________*/
#include "syslog_utils.h"
#include "power_utils.h"
#include "sleep_utils.h"
#include "thermal_utils.h"
#include "mqtt_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
@@ -68,8 +69,8 @@ ___________________________________________________________________*/
#endif
String versionDate = "2026-10-05";
String versionNumber = "4.0.3";
String versionDate = "2026-10-18";
String versionNumber = "4.0.5";
Configuration Config;
WiFiClient aprsIsClient;
WiFiClient mqttClient;
@@ -108,6 +109,9 @@ void setup() {
}
networkManager->setHostName("iGATE-" + Config.callsign);
POWER_Utils::setup();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::setup();
#endif
Utils::setupDisplay();
LoRa_Utils::setup();
Utils::validateFreqs();
@@ -135,6 +139,9 @@ void loop() {
SLEEP_Utils::checkWakeUpFlag();
Utils::checkBeaconInterval();
STATION_Utils::processOutputPacketBufferUltraEcoMode();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::monitor();
#endif
Utils::checkSleepByLowBatteryVoltage(1);
SLEEP_Utils::startSleeping();
} else {
@@ -222,6 +229,9 @@ void loop() {
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
#endif
#ifdef FAN_CTRL_PIN
THERMAL_Utils::monitor();
#endif
Utils::checkRebootTime();
Utils::checkSleepByLowBatteryVoltage(1);
}
+17 -1
View File
@@ -152,6 +152,18 @@ namespace APRS_IS_Utils {
return packetToUpload;
}
String buildIS2Meta() {
static String fixedTail; // rx_modem + rx_bitrate + rx_frequency: RX config doesn't change at runtime
if (fixedTail == "") {
// LoRa bit rate = SF * (BW / 2^SF) * 4/CR (CR = denominator: 5..8)
uint32_t bitrate = (uint32_t)(Config.loramodule.rxSpreadingFactor
* ((float)Config.loramodule.rxSignalBandwidth / (1 << Config.loramodule.rxSpreadingFactor))
* (4.0 / Config.loramodule.rxCodingRate4));
fixedTail = " rx_modem=2 rx_bitrate=" + String(bitrate) + " rx_frequency=" + String(Config.loramodule.rxFreq);
}
return "#is2-meta rx_rssi_dbm=" + String(lastAprsPacket.rssi) + " rx_snr_db=" + String(lastAprsPacket.snr, 1) + fixedTail;
}
bool processReceivedLoRaMessage(const String& sender, const String& packet) {
String receivedMessage;
int leftCurlyBraceIndex = packet.indexOf("{");
@@ -202,7 +214,11 @@ namespace APRS_IS_Utils {
A7670_Utils::uploadToAPRSIS(aprsPacketToUpload);
stationBeacon = false;
#else
upload(aprsPacketToUpload);
if (Config.aprs_is.is2Metadata && aprsPacketToUpload != "") {
upload(buildIS2Meta() + "\r\n" + aprsPacketToUpload); // IS2PLAIN: meta line right before the packet, same write
} else {
upload(aprsPacketToUpload);
}
#endif
Utils::println("(Uploaded to APRS-IS)");
displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
+5 -1
View File
@@ -63,6 +63,7 @@ bool Configuration::writeFile() {
data["aprs_is"]["filter"] = aprs_is.filter;
data["aprs_is"]["messagesToRF"] = aprs_is.messagesToRF;
data["aprs_is"]["objectsToRF"] = aprs_is.objectsToRF;
data["aprs_is"]["is2Metadata"] = aprs_is.is2Metadata;
data["beacon"]["comment"] = beacon.comment;
data["beacon"]["rfCommentEveryXBeacons"] = beacon.rfCommentEveryXBeacons;
@@ -238,7 +239,8 @@ bool Configuration::readFile() {
data["aprs_is"]["port"].isNull() ||
data["aprs_is"]["filter"].isNull() ||
data["aprs_is"]["messagesToRF"].isNull() ||
data["aprs_is"]["objectsToRF"].isNull()) needsRewrite = true;
data["aprs_is"]["objectsToRF"].isNull() ||
data["aprs_is"]["is2Metadata"].isNull()) needsRewrite = true;
aprs_is.active = data["aprs_is"]["active"] | false;
aprs_is.passcode = data["aprs_is"]["passcode"] | "XYZWV";
aprs_is.server = data["aprs_is"]["server"] | "rotate.aprs2.net";
@@ -246,6 +248,7 @@ bool Configuration::readFile() {
aprs_is.filter = data["aprs_is"]["filter"] | "m/10";
aprs_is.messagesToRF = data["aprs_is"]["messagesToRF"] | false;
aprs_is.objectsToRF = data["aprs_is"]["objectsToRF"] | false;
aprs_is.is2Metadata = data["aprs_is"]["is2Metadata"] | false;
if (data["beacon"]["latitude"].isNull() ||
data["beacon"]["longitude"].isNull() ||
@@ -473,6 +476,7 @@ void Configuration::setDefaultValues() {
aprs_is.filter = "m/10";
aprs_is.messagesToRF = false;
aprs_is.objectsToRF = false;
aprs_is.is2Metadata = false;
beacon.comment = "LoRa APRS";
beacon.rfCommentEveryXBeacons = 1;
+23 -7
View File
@@ -28,6 +28,7 @@
#include "ntp_utils.h"
#include "display.h"
#include "utils.h"
#include "thermal_utils.h"
extern Configuration Config;
@@ -107,9 +108,11 @@ namespace LoRa_Utils {
}
void setup() {
#ifdef RADIO_VCC_PIN // LoRa module power switch (QRP Labs LightGateway, T-Beam 1W)
pinMode(RADIO_VCC_PIN, OUTPUT);
digitalWrite(RADIO_VCC_PIN, HIGH);
#endif
#if defined (LIGHTGATEWAY_1_0) || defined(LIGHTGATEWAY_PLUS_1_0)
pinMode(RADIO_VCC_PIN,OUTPUT);
digitalWrite(RADIO_VCC_PIN,HIGH);
loraSPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN, RADIO_CS_PIN);
#else
SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
@@ -124,6 +127,8 @@ namespace LoRa_Utils {
#endif
#if (defined(RADIO_RXEN) && defined(RADIO_TXEN)) // before begin() so RF switch is driven from start (Ebyte E22/E32 1W, QRP Labs LightGateway)
radio.setRfSwitchPins(RADIO_RXEN, RADIO_TXEN);
#elif defined(RADIO_RXEN) // T-Beam 1W: RXEN = LNA only, DIO2 drives the PA
radio.setRfSwitchPins(RADIO_RXEN, RADIOLIB_NC);
#endif
int state = radio.begin(freq);
if (state != RADIOLIB_ERR_NONE) {
@@ -152,11 +157,6 @@ namespace LoRa_Utils {
radio.setBandwidth(signalBandwidth);
radio.setCRC(true);
/*#ifdef SX126X_DIO2_AS_RF_SWITCH
radio.setRfSwitchPins(RADIO_RXEN, RADIOLIB_NC);
radio.setDio2AsRfSwitch(true);
#endif*/
int power = validPower(Config.loramodule.power);
if (power != Config.loramodule.power) {
Utils::println("LoRa power adjusted: " + String(Config.loramodule.power) + " -> " + String(power));
@@ -178,6 +178,10 @@ namespace LoRa_Utils {
#if defined(HAS_TCXO) && !defined(HAS_1W_LORA)
radio.setDio2AsRfSwitch();
#endif
#if defined(TTGO_T_BEAM_1W)
radio.setDio2AsRfSwitch(true); // DIO2 drives the 1W PA
radio.setPaRampTime(RADIOLIB_SX126X_PA_RAMP_800U); // PA needs >800us to settle (default 200us)
#endif
#ifdef HAS_TCXO
radio.setTCXO(1.8);
#endif
@@ -239,6 +243,12 @@ namespace LoRa_Utils {
void sendNewPacket(const String& newPacket) {
if (!Config.loramodule.txActive) return;
#ifdef FAN_CTRL_PIN
if (THERMAL_Utils::isTxBlocked()) {
Utils::println("Thermal: Tx blocked (over-temperature), packet dropped: " + newPacket);
return;
}
#endif
if (Config.loramodule.txFreq != Config.loramodule.rxFreq) {
if (!packetIsBeacon || (packetIsBeacon && Config.beacon.beaconFreq == 1)) {
@@ -262,7 +272,13 @@ namespace LoRa_Utils {
}
if (!cadDropped) {
#ifdef FAN_CTRL_PIN
THERMAL_Utils::onTxStart();
#endif
int state = radio.transmit("\x3c\xff\x01" + newPacket);
#ifdef FAN_CTRL_PIN
THERMAL_Utils::onTxEnd();
#endif
transmitFlag = true;
if (state == RADIOLIB_ERR_NONE) {
if (Config.syslog.active && networkManager->isConnected()) {
+7 -8
View File
@@ -66,7 +66,7 @@ namespace POWER_Utils {
}
void vext_ctrl_OFF() {
digitalWrite(VEXT_CTRL_PIN, Config.digi.ecoMode == 1 ? VEXT_CTRL_ON_STATE : !VEXT_CTRL_ON_STATE);
digitalWrite(VEXT_CTRL_PIN, !VEXT_CTRL_ON_STATE); // always off, regardless of ecoMode
}
#endif
@@ -111,9 +111,7 @@ namespace POWER_Utils {
PMU.enableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_ON();
#endif
// HELTEC_WIRELESS_TRACKER: GPS shares Vext (GPIO3) with the TFT, so it follows the display power (not controlled here)
//gpsIsActive = true;
}
@@ -129,9 +127,7 @@ namespace POWER_Utils {
PMU.disableALDO3();
#endif
#endif
#ifdef HELTEC_WIRELESS_TRACKER
adc_ctrl_OFF();
#endif
// HELTEC_WIRELESS_TRACKER: GPS shares Vext with the TFT, turned off with it (see activateGPS)
//gpsIsActive = false;
}
@@ -199,7 +195,10 @@ namespace POWER_Utils {
PMU.setDC1Voltage(3300);
PMU.enableDC1();
#ifdef TTGO_T_Beam_S3_SUPREME_V3
PMU.setALDO1Voltage(3300);
PMU.setALDO1Voltage(3300); // BME280, QMC6310, QMI8658, OLED
PMU.enableALDO1();
PMU.setALDO2Voltage(3300); // sensors/OLED I2C + RTC PCF8563 (must stay on)
PMU.enableALDO2();
#endif
PMU.setButtonBatteryChargeVoltage(3300);
PMU.enableButtonBatteryCharge();
+4
View File
@@ -21,6 +21,7 @@
#include "sleep_utils.h"
#include "digi_utils.h"
#include "lora_utils.h"
#include "thermal_utils.h"
extern Configuration Config;
@@ -75,6 +76,9 @@ namespace SLEEP_Utils {
Serial.print("(Sleeping : "); Serial.print(timeToSleep); Serial.println("seconds)");
delay(100);
LoRa_Utils::wakeRadio();
#ifdef FAN_CTRL_PIN
THERMAL_Utils::turnOffFan(); // GPIOs keep their level in light sleep
#endif
esp_light_sleep_start();
}
}
+154
View File
@@ -0,0 +1,154 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
// Fan / thermal control adapted from KJ7NYE's LoRa_FieldOps_APRS_Tracker (GPLv3)
// NTC circuit (T-Beam 1W V1.0 schematic): 3V3 -> NCP18XH103F03RB -> TEMP_PIN -> 10K -> GND
#include "board_pinout.h"
#ifdef FAN_CTRL_PIN
#include <Arduino.h>
#include <math.h>
#include "thermal_utils.h"
#include "utils.h"
#define NTC_B 3380.0f // B25/50 (K)
#define NTC_R25 10000.0f // NTC resistance at 25C (ohm)
#define NTC_T0 298.15f // 25C in Kelvin
#define NTC_R_FIXED 10000.0f // pull-down resistor (ohm)
#define NTC_VCC_MV 3300.0f
#define NTC_MIN_VALID_MV 100 // below this the sample is a failed ADC2 read (100mV = ~-45C)
#define FAN_ON_TEMP 50.0f // fan on at or above
#define FAN_OFF_TEMP 42.0f // fan off below (hysteresis)
#define TEMP_WARNING 75.0f
#define TEMP_TX_BLOCK 85.0f // Tx blocked at or above
#define TEMP_TX_UNBLOCK 75.0f // Tx allowed again below (hysteresis)
#define TX_COOLDOWN_MS 30000 // fan kept on after TX
#define TEMP_SAMPLE_MS 30000
static float currentTemperature = 25.0f;
static bool fanOn = false;
static bool txBlocked = false;
static bool txCooldownActive = false;
static uint32_t txEndTime = 0;
static uint32_t lastTempSample = 0;
static bool firstTempSample = true;
namespace THERMAL_Utils {
float readTemperature() { // TEMP_PIN is on ADC2: reads fail (return 0) while Wi-Fi holds the ADC2 arbiter
analogReadMilliVolts(TEMP_PIN); // dummy read to settle the ADC
delay(1);
uint32_t sum = 0;
int validSamples = 0;
for (int i = 0; i < 5; i++) {
uint32_t sample = analogReadMilliVolts(TEMP_PIN);
if (sample > NTC_MIN_VALID_MV) {
sum += sample;
validSamples++;
}
delay(3);
}
if (validSamples == 0) return NAN;
float mv = (float)sum / validSamples;
if (mv >= NTC_VCC_MV) return NAN;
float ntcResistance = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv;
float kelvin = 1.0f / (1.0f / NTC_T0 + (1.0f / NTC_B) * logf(ntcResistance / NTC_R25));
return kelvin - 273.15f;
}
void setFan(bool on, const String& reason) {
if (fanOn == on) return;
fanOn = on;
digitalWrite(FAN_CTRL_PIN, on ? HIGH : LOW);
Utils::println("Thermal: Fan " + String(on ? "ON" : "OFF") + " (" + reason + ", " + String(currentTemperature, 1) + " C)");
}
void updateFan() {
if (!fanOn) {
if (txCooldownActive) {
setFan(true, "TX cooldown");
} else if (currentTemperature >= FAN_ON_TEMP) {
setFan(true, "temperature");
}
} else if (!txCooldownActive && currentTemperature < FAN_OFF_TEMP) {
setFan(false, "cooled down");
}
}
void setup() {
pinMode(TEMP_PIN, INPUT);
analogSetPinAttenuation(TEMP_PIN, ADC_11db);
pinMode(FAN_CTRL_PIN, OUTPUT);
digitalWrite(FAN_CTRL_PIN, LOW);
fanOn = false;
firstTempSample = true; // first monitor() call samples immediately
}
void monitor() {
uint32_t now = millis();
if (txCooldownActive && (now - txEndTime >= TX_COOLDOWN_MS)) {
txCooldownActive = false;
updateFan();
}
if (!firstTempSample && (now - lastTempSample < TEMP_SAMPLE_MS)) return;
firstTempSample = false;
lastTempSample = now;
float temperature = readTemperature();
if (!isnan(temperature)) currentTemperature = temperature;
if (!txBlocked && currentTemperature >= TEMP_TX_BLOCK) {
txBlocked = true;
Utils::println("Thermal: Over-temperature at " + String(currentTemperature, 1) + " C, Tx blocked");
} else if (txBlocked && currentTemperature < TEMP_TX_UNBLOCK) {
txBlocked = false;
Utils::println("Thermal: Temperature back to " + String(currentTemperature, 1) + " C, Tx allowed");
} else if (!txBlocked && currentTemperature >= TEMP_WARNING) {
Utils::println("Thermal: High temperature: " + String(currentTemperature, 1) + " C");
}
updateFan();
}
void onTxStart() {
txCooldownActive = false;
setFan(true, "TX");
}
void onTxEnd() {
txCooldownActive = true;
txEndTime = millis();
}
void turnOffFan() { // before sleeping: no Tx while asleep, so the PA doesn't heat up
txCooldownActive = false;
setFan(false, "sleep");
}
bool isTxBlocked() {
return txBlocked;
}
}
#endif
+6 -1
View File
@@ -27,6 +27,7 @@
#include "board_pinout.h"
#include "power_utils.h"
#include "syslog_utils.h"
#include "thermal_utils.h"
#include "A7670_utils.h"
#include "lora_utils.h"
#include "wifi_utils.h"
@@ -433,9 +434,13 @@ namespace Utils {
displayToggle(false);
}
#ifdef VEXT_CTRL_PIN
digitalWrite(VEXT_CTRL_PIN, !VEXT_CTRL_ON_STATE); // VEXT off before deep sleep (always, regardless of ecoMode)
POWER_Utils::vext_ctrl_OFF(); // VEXT off before deep sleep (always, regardless of ecoMode)
#endif
#ifdef FAN_CTRL_PIN
THERMAL_Utils::turnOffFan();
#endif
LoRa_Utils::sleepRadio();
POWER_Utils::deactivateGPS(); // GPS rail off (AXP boards), otherwise it keeps draining the low battery while sleeping
#ifdef RADIO_VCC_PIN
digitalWrite(RADIO_VCC_PIN, LOW); // cut LoRa module power (QRP Labs LightGateway) so it draws nothing while sleeping
#endif
+1
View File
@@ -234,6 +234,7 @@ namespace WEB_Utils {
if (Config.aprs_is.active) {
Config.aprs_is.messagesToRF = request->hasParam("aprs_is.messagesToRF", true);
Config.aprs_is.objectsToRF = request->hasParam("aprs_is.objectsToRF", true);
Config.aprs_is.is2Metadata = request->hasParam("aprs_is.is2Metadata", true);
Config.aprs_is.server = getParamStringSafe("aprs_is.server", Config.aprs_is.server);
Config.aprs_is.passcode = getParamStringSafe("aprs_is.passcode", Config.aprs_is.passcode);
Config.aprs_is.port = getParamIntSafe("aprs_is.port", Config.aprs_is.port);
+65
View File
@@ -0,0 +1,65 @@
/* Copyright (C) 2026 Ricardo Guzman - CA2RXU
*
* This file is part of LoRa APRS iGate.
*
* LoRa APRS iGate is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LoRa APRS iGate is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LoRa APRS iGate. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef BOARD_PINOUT_H_
#define BOARD_PINOUT_H_
// LoRa Radio
#define HAS_SX1262
#define HAS_1W_LORA
#define HAS_TCXO
#define RADIO_VCC_PIN 40 // (RADIO_LDO_EN_PIN) HIGH = turn on the radio, LOW = turns off the radio
#define RADIO_SCLK_PIN 13
#define RADIO_MISO_PIN 12
#define RADIO_MOSI_PIN 11
#define RADIO_CS_PIN 15
#define RADIO_RST_PIN 3
#define RADIO_DIO1_PIN 1
#define RADIO_BUSY_PIN 38
#define RADIO_RXEN 21 // (RADIO_CTRL_PIN) HIGH = LNA on (Rx), LOW = LNA off (Tx)
#define RADIO_WAKEUP_PIN RADIO_DIO1_PIN
#define GPIO_WAKEUP_PIN GPIO_SEL_1
// I2C
#define USE_WIRE_WITH_OLED_PINS
// Display
#define HAS_DISPLAY
#define HAS_SH1106
#undef OLED_SDA
#undef OLED_SCL
#undef OLED_RST
#define OLED_SDA 8
#define OLED_SCL 9
#define OLED_RST -1 // Reset pin # (or -1 if sharing Arduino reset pin)
// Aditional Config
#define BATTERY_PIN 4
#define BATTERY_CELLS 2 // 7.4V 2S battery
#define BUTTON_PIN 0
#define TEMP_PIN 14 // NTC Temperature sensor
#define FAN_CTRL_PIN 41
// GPS
#define HAS_GPS
#define GPS_RX 6
#define GPS_TX 5
#endif
+21
View File
@@ -0,0 +1,21 @@
[env:ttgo-t-beam-1W]
board = esp32-s3-devkitm-1
board_build.mcu = esp32s3
board_build.flash_size = 16MB
board_build.flash_mode = qio
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.psram_type = opi
build_flags =
${common.build_flags}
${common.usb_flags}
-D RADIOLIB_EXCLUDE_LR11X0=1
-D RADIOLIB_EXCLUDE_SX127X=1
-D RADIOLIB_EXCLUDE_SX128X=1
-D TTGO_T_BEAM_1W
-D BOARD_HAS_PSRAM
lib_deps =
${common.lib_deps}
${common.display_libs}
adafruit/Adafruit SH110X @ 2.1.15