Skip to content

Instantly share code, notes, and snippets.

@exadeci
Last active July 3, 2025 00:59
Show Gist options
  • Save exadeci/9ce0dd6e35407e6edda33cf699fcfb0e to your computer and use it in GitHub Desktop.
Save exadeci/9ce0dd6e35407e6edda33cf699fcfb0e to your computer and use it in GitHub Desktop.
LilyGo T-Echo Meshtastic patches for burning man street addresses (2.7 re-enables touch button as backlight button)
diff --git a/src/graphics/BRC.h b/src/graphics/BRC.h
new file mode 100644
index 000000000..9b13ea653
--- /dev/null
+++ b/src/graphics/BRC.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "GPSStatus.h"
+#include "gps/GeoCoord.h"
+#include "graphics/Screen.h"
+#include "configuration.h"
+
+using namespace meshtastic;
+
+const int32_t BRC_LATI= (40.786958 * 1e7);
+const int32_t BRC_LONI = (-119.202994 * 1e7);
+const double BRC_LATF = 40.786958;
+const double BRC_LONF = -119.202994;
+const double BRC_NOON = 1.5;
+const double RAD_TO_HOUR = (6.0/3.14159);
+const double METER_TO_FEET = 3.28084;
+
+// Pre-calculated street data for performance
+struct StreetInfo {
+ float center;
+ float width;
+ const char* name;
+};
+
+static const StreetInfo streets[] = {
+ {2500, 50, "Esp"},
+ {2940, 220, "A"},
+ {2940+290, 145, "B"},
+ {2940+290*2, 145, "C"},
+ {2940+290*3, 145, "D"},
+ {2940+290*4, 145, "E"},
+ {2940+290*4+490, 245, "F"},
+ {2940+290*5+490, 145, "G"},
+ {2940+290*6+490, 145, "H"},
+ {2940+290*7+490, 145, "I"},
+ {2940+290*7+490+190, 95, "J"},
+ {2940+290*7+490+190*2, 95, "K"},
+ {2940+290*7+490+190*2+75, 0, nullptr}
+};
+
+static char* BRCAddress(int32_t lat, int32_t lon)
+{
+ thread_local static char addrStr[20];
+
+ // Cache previous calculations to avoid expensive trigonometric operations
+ // when position hasn't changed significantly
+ thread_local static int32_t cachedLat = 0;
+ thread_local static int32_t cachedLon = 0;
+ thread_local static float cachedBearing = 0.0f;
+ thread_local static float cachedDistance = 0.0f;
+ thread_local static bool cacheValid = false;
+
+ // Check if we can use cached values
+ // GPS int32_t format: 1e-7 degrees, so 1 unit ≈ 1.11 cm at equator
+ // 25 units ≈ 28 cm, 90 units ≈ 1 meter, 900 units ≈ 10 meters
+ const int32_t CACHE_THRESHOLD = 90; // ~1 meter - good balance of precision vs performance
+ bool positionChanged = !cacheValid ||
+ abs(lat - cachedLat) > CACHE_THRESHOLD ||
+ abs(lon - cachedLon) > CACHE_THRESHOLD;
+
+ if (positionChanged) {
+ // Update cache with new calculations
+ double latD = DegD(lat);
+ double lonD = DegD(lon);
+
+ cachedBearing = GeoCoord::bearing(BRC_LATF, BRC_LONF, latD, lonD) * RAD_TO_HOUR;
+ cachedDistance = GeoCoord::latLongToMeter(BRC_LATF, BRC_LONF, latD, lonD);
+
+ cachedLat = lat;
+ cachedLon = lon;
+ cacheValid = true;
+ }
+
+ // Use cached values for calculations
+ float bearingToMan = cachedBearing;
+ bearingToMan += 12.0 - BRC_NOON;
+ while (bearingToMan > 12.0) {bearingToMan -= 12.0;}
+ uint8_t hour = (uint8_t)(bearingToMan);
+ uint8_t minute = (uint8_t)((bearingToMan - hour) * 60.0);
+ hour %= 12;
+ if (hour == 0) {hour = 12;}
+
+ float d = cachedDistance;
+
+ // Check unit preference once and set conversion factor and label
+ const bool useImperial = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL);
+ const float conversionFactor = useImperial ? METER_TO_FEET : 1.0f;
+ const char* unitLabel = useImperial ? "ft" : "m";
+
+ if (bearingToMan > 1.75 && bearingToMan < 10.25) {
+ const char* street = nullptr;
+ float dist = 0;
+ // Find the appropriate street based on distance (using original meter values for street matching)
+ for (const auto& s : streets) {
+ if (d > s.center - s.width) {
+ street = s.name;
+ dist = d - s.center;
+ } else {
+ break;
+ }
+ }
+ if (street) {
+ snprintf(addrStr, sizeof(addrStr), "%d:%02d & %s %d%s", hour, minute, street, int(dist * conversionFactor), unitLabel);
+ return addrStr;
+ }
+
+ }
+
+ snprintf(addrStr, sizeof(addrStr), "%d:%02d & %d%s", hour, minute, (uint32_t)(d * conversionFactor), unitLabel);
+ return addrStr;
+}
+
+
+static void drawBRCAddress(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps)
+{
+ // Only draw if we have a valid GPS position
+ if ((!gps->getIsConnected() || !gps->getHasLock()) && !config.position.fixed_position) {
+ return; // Skip drawing - no valid position available
+ }
+
+ auto displayLine = BRCAddress(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()));
+ display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
+}
diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp
index 4ce073c9b..18498f404 100644
--- a/src/graphics/draw/UIRenderer.cpp
+++ b/src/graphics/draw/UIRenderer.cpp
@@ -12,6 +12,7 @@
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
+#include "graphics/BRC.h"
#include "main.h"
#include "target_specific.h"
#include <OLEDDisplay.h>
@@ -641,6 +642,16 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
line += 1;
+ // === BRC Address (Burning Man) ===
+#if HAS_GPS
+ if ((gpsStatus->getIsConnected() && gpsStatus->getHasLock()) || config.position.fixed_position) {
+ auto brcDisplayLine = BRCAddress(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()));
+ int brcTextWidth = display->getStringWidth(brcDisplayLine);
+ int brcX = (SCREEN_WIDTH - brcTextWidth) / 2;
+ display->drawString(brcX, getTextPositions(display)[line++], brcDisplayLine);
+ }
+#endif
+
// === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0;
int nameX = 0;
@@ -841,7 +852,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED
display->setFont(FONT_MEDIUM);
display->setTextAlignment(TEXT_ALIGN_LEFT);
- const char *title = "meshtastic.org";
+ const char *title = "Peachy 7:30 & B";
display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title);
display->setFont(FONT_SMALL);
diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp
index ad667f003..a873cd41f 100644
--- a/src/input/ButtonThread.cpp
+++ b/src/input/ButtonThread.cpp
@@ -14,6 +14,7 @@
#include "modules/ExternalNotificationModule.h"
#include "power.h"
#include "sleep.h"
+#include "PowerFSM.h"
#ifdef ARCH_PORTDUINO
#include "platform/portduino/PortduinoGlue.h"
#endif
@@ -161,6 +162,19 @@ int32_t ButtonThread::runOnce()
leadUpPlayed = false;
leadUpSequenceActive = false;
resetLeadUpSequence();
+
+ #ifdef TTGO_T_ECHO
+ // Special handling for T-Echo touch button: turn off backlight when released after short press
+ if (_touchQuirk && _pinNum == PIN_BUTTON_TOUCH) {
+ // Only turn off if it was a short press (not a long press that's being handled elsewhere)
+ uint32_t pressDuration = millis() - buttonPressStartTime;
+ if (pressDuration < _longPressTime) {
+ #ifdef PIN_EINK_EN
+ digitalWrite(PIN_EINK_EN, LOW); // Turn off backlight
+ #endif
+ }
+ }
+ #endif
}
buttonWasPressed = buttonCurrentlyPressed;
@@ -174,6 +188,19 @@ int32_t ButtonThread::runOnce()
evt.touchY = 0;
switch (btnEvent) {
case BUTTON_EVENT_PRESSED: {
+ #ifdef TTGO_T_ECHO
+ // Special handling for T-Echo touch button: turn on backlight immediately when touched
+ if (_touchQuirk && _pinNum == PIN_BUTTON_TOUCH) {
+ #ifdef PIN_EINK_EN
+ digitalWrite(PIN_EINK_EN, HIGH); // Turn on backlight immediately
+ // Wake screen when backlight turns on
+ if (powerFSM.getState() == &stateDARK) {
+ powerFSM.trigger(EVENT_INPUT);
+ }
+ #endif
+ }
+ #endif
+
// Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event)
evt.inputEvent = _singlePress;
// evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types
@@ -191,6 +218,22 @@ int32_t ButtonThread::runOnce()
if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending())
break;
+ #ifdef TTGO_T_ECHO
+ // Special handling for T-Echo touch button: turn on backlight while held
+ if (_touchQuirk && _pinNum == PIN_BUTTON_TOUCH) {
+ #ifdef PIN_EINK_EN
+ digitalWrite(PIN_EINK_EN, HIGH); // Turn on backlight
+ // Wake screen when backlight turns on
+ if (powerFSM.getState() == &stateDARK) {
+ powerFSM.trigger(EVENT_INPUT);
+ }
+ #endif
+ // Don't send the normal long press event for touch button backlight control
+ waitingForLongPress = false;
+ break;
+ }
+ #endif
+
// Check if this is part of a short-press + long-press combination
if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress &&
(millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) {
@@ -254,6 +297,19 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_LONG_RELEASED: {
LOG_INFO("LONG PRESS RELEASE");
+
+ #ifdef TTGO_T_ECHO
+ // Special handling for T-Echo touch button: turn off backlight when released
+ if (_touchQuirk && _pinNum == PIN_BUTTON_TOUCH) {
+ #ifdef PIN_EINK_EN
+ digitalWrite(PIN_EINK_EN, LOW); // Turn off backlight
+ #endif
+ // Reset combination tracking
+ waitingForLongPress = false;
+ break;
+ }
+ #endif
+
if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE &&
(millis() - buttonPressStartTime) >= _longLongPressTime) {
evt.inputEvent = _longLongPress;
diff --git a/src/main.cpp b/src/main.cpp
index ddc1df28c..b78bcebf0 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -961,6 +961,7 @@ void setup()
};
touchConfig.singlePress = INPUT_BROKER_NONE;
touchConfig.longPress = INPUT_BROKER_BACK;
+ touchConfig.touchQuirk = true; // Enable T-Echo touch quirk protection
TouchButtonThread->initButton(touchConfig);
#endif
diff --git a/variants/t-echo/variant.h b/variants/t-echo/variant.h
index 4f3a53ebf..d39390dc0 100644
--- a/variants/t-echo/variant.h
+++ b/variants/t-echo/variant.h
@@ -68,6 +68,10 @@ extern "C" {
#define BUTTON_TOUCH_ACTIVE_LOW true
#define BUTTON_TOUCH_ACTIVE_PULLUP true
+// Main user button for BaseUI (use the physical side button)
+#define BUTTON_PIN PIN_BUTTON1
+#define BUTTON_NEED_PULLUP
+
#define BUTTON_CLICK_MS 400
#define BUTTON_TOUCH_MS 200
diff --git a/src/graphics/BRC.h b/src/graphics/BRC.h
new file mode 100644
index 000000000..9b13ea653
--- /dev/null
+++ b/src/graphics/BRC.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include "GPSStatus.h"
+#include "gps/GeoCoord.h"
+#include "graphics/Screen.h"
+#include "configuration.h"
+
+using namespace meshtastic;
+
+const int32_t BRC_LATI= (40.786958 * 1e7);
+const int32_t BRC_LONI = (-119.202994 * 1e7);
+const double BRC_LATF = 40.786958;
+const double BRC_LONF = -119.202994;
+const double BRC_NOON = 1.5;
+const double RAD_TO_HOUR = (6.0/3.14159);
+const double METER_TO_FEET = 3.28084;
+
+// Pre-calculated street data for performance
+struct StreetInfo {
+ float center;
+ float width;
+ const char* name;
+};
+
+static const StreetInfo streets[] = {
+ {2500, 50, "Esp"},
+ {2940, 220, "A"},
+ {2940+290, 145, "B"},
+ {2940+290*2, 145, "C"},
+ {2940+290*3, 145, "D"},
+ {2940+290*4, 145, "E"},
+ {2940+290*4+490, 245, "F"},
+ {2940+290*5+490, 145, "G"},
+ {2940+290*6+490, 145, "H"},
+ {2940+290*7+490, 145, "I"},
+ {2940+290*7+490+190, 95, "J"},
+ {2940+290*7+490+190*2, 95, "K"},
+ {2940+290*7+490+190*2+75, 0, nullptr}
+};
+
+static char* BRCAddress(int32_t lat, int32_t lon)
+{
+ thread_local static char addrStr[20];
+
+ // Cache previous calculations to avoid expensive trigonometric operations
+ // when position hasn't changed significantly
+ thread_local static int32_t cachedLat = 0;
+ thread_local static int32_t cachedLon = 0;
+ thread_local static float cachedBearing = 0.0f;
+ thread_local static float cachedDistance = 0.0f;
+ thread_local static bool cacheValid = false;
+
+ // Check if we can use cached values
+ // GPS int32_t format: 1e-7 degrees, so 1 unit ≈ 1.11 cm at equator
+ // 25 units ≈ 28 cm, 90 units ≈ 1 meter, 900 units ≈ 10 meters
+ const int32_t CACHE_THRESHOLD = 90; // ~1 meter - good balance of precision vs performance
+ bool positionChanged = !cacheValid ||
+ abs(lat - cachedLat) > CACHE_THRESHOLD ||
+ abs(lon - cachedLon) > CACHE_THRESHOLD;
+
+ if (positionChanged) {
+ // Update cache with new calculations
+ double latD = DegD(lat);
+ double lonD = DegD(lon);
+
+ cachedBearing = GeoCoord::bearing(BRC_LATF, BRC_LONF, latD, lonD) * RAD_TO_HOUR;
+ cachedDistance = GeoCoord::latLongToMeter(BRC_LATF, BRC_LONF, latD, lonD);
+
+ cachedLat = lat;
+ cachedLon = lon;
+ cacheValid = true;
+ }
+
+ // Use cached values for calculations
+ float bearingToMan = cachedBearing;
+ bearingToMan += 12.0 - BRC_NOON;
+ while (bearingToMan > 12.0) {bearingToMan -= 12.0;}
+ uint8_t hour = (uint8_t)(bearingToMan);
+ uint8_t minute = (uint8_t)((bearingToMan - hour) * 60.0);
+ hour %= 12;
+ if (hour == 0) {hour = 12;}
+
+ float d = cachedDistance;
+
+ // Check unit preference once and set conversion factor and label
+ const bool useImperial = (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL);
+ const float conversionFactor = useImperial ? METER_TO_FEET : 1.0f;
+ const char* unitLabel = useImperial ? "ft" : "m";
+
+ if (bearingToMan > 1.75 && bearingToMan < 10.25) {
+ const char* street = nullptr;
+ float dist = 0;
+ // Find the appropriate street based on distance (using original meter values for street matching)
+ for (const auto& s : streets) {
+ if (d > s.center - s.width) {
+ street = s.name;
+ dist = d - s.center;
+ } else {
+ break;
+ }
+ }
+ if (street) {
+ snprintf(addrStr, sizeof(addrStr), "%d:%02d & %s %d%s", hour, minute, street, int(dist * conversionFactor), unitLabel);
+ return addrStr;
+ }
+
+ }
+
+ snprintf(addrStr, sizeof(addrStr), "%d:%02d & %d%s", hour, minute, (uint32_t)(d * conversionFactor), unitLabel);
+ return addrStr;
+}
+
+
+static void drawBRCAddress(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps)
+{
+ // Only draw if we have a valid GPS position
+ if ((!gps->getIsConnected() || !gps->getHasLock()) && !config.position.fixed_position) {
+ return; // Skip drawing - no valid position available
+ }
+
+ auto displayLine = BRCAddress(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()));
+ display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine);
+}
diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index 0c18f3287..9e330b12f 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -50,6 +50,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "modules/WaypointModule.h"
#include "sleep.h"
#include "target_specific.h"
+#include "graphics/BRC.h"
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
#include "mesh/wifi/WiFiAPClient.h"
@@ -1343,8 +1344,8 @@ void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength)
// -- if using time delta instead --
else if (agoSecs < 120 * 60) // last 2 hrs
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60);
- // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data.
- else if ((agoSecs / 60 / 60) < (hours_in_month * 6))
+ // Only show hours ago if it's been less than half a month. Otherwise, we may have bad data.
+ else if ((agoSecs / 60 / 60) < (hours_in_month / 2))
snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60);
else
snprintf(timeStr, maxLength, "unknown age");
@@ -1451,7 +1452,20 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
strncpy(distStr, "? km ?°", sizeof(distStr));
}
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
- const char *fields[] = {username, lastStr, signalStr, distStr, NULL};
+ const char *fields[] = {username, lastStr, signalStr, distStr, NULL, NULL};
+
+
+ if (node && nodeDB->hasValidPosition(node)) {
+ meshtastic_PositionLite &p = node->position;
+ // Overwrite the signal strength with the BRC position if screen is small
+ if ((SCREEN_HEIGHT / FONT_HEIGHT_SMALL) < 6) {
+ fields[2] = BRCAddress(p.latitude_i, p.longitude_i);
+ } else {
+ fields[4] = fields[3];
+ fields[3] = BRCAddress(p.latitude_i, p.longitude_i);
+ }
+ }
+
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT);
@@ -2678,6 +2692,7 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat
// hms += tz.tz_dsttime * SEC_PER_HOUR;
// hms -= tz.tz_minuteswest * SEC_PER_MIN;
// mod `hms` to ensure in positive range of [0...SEC_PER_DAY)
+ hms += SEC_PER_DAY - 7 * SEC_PER_HOUR; // pacific time
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
// Tear apart hms into h:m:s
@@ -2718,10 +2733,7 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat
#if HAS_GPS
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
// Line 3
- if (config.display.gps_format !=
- meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
- drawGPSAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
-
+ drawBRCAddress(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
// Line 4
drawGPScoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus);
} else {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment