Unify Heltec V4 OLED, TFT and headless display selection

Detect OLED or the original stock TFT touch assembly in ordinary V4.2/V4.3
images. Add saved live display.type auto/oled/tft/off selection and report
the active selection in board output. Keep sensors on primary I2C, preserve
shared power, and isolate TFT FSPI from radio HSPI through sleep and changes.

Consolidate future picker/build choices only with application capability
proof. Retain exact legacy OTA identities and separate R8 hardware images.
Document controls and physical qualification limits.

Validation: five serialized Full builds with capability/size/RAM checks;
focused host tests, existing R8/T-Deck/OLED regressions, build-profile suite,
and picker/provider tests. V4.3 OLED live changes and off/auto reboot
persistence passed on Mercerwood. Physical TFT qualification remains pending.
This commit is contained in:
mikecarper committed 2026-10-26 15:19:17 -07:00
1 parent 5b5708283d
commit b00d346df7
43 files changed
+2123 -76

No files matched your search

+18
View File
@@ -162,6 +162,15 @@ def requirements(platform, defines, target):
if platform != "ESP32_PLATFORM":
raise ValueError("Wio Tracker L2 display budget requires ESP32")
display_heap = wio_tracker_l2_display_heap(defines)
elif display == "HeltecV4Display":
if platform != "ESP32_PLATFORM" or not integer(defines, "HELTEC_V4_DISPLAY_UNIFIED", 0):
raise ValueError("unified Heltec V4 display budget requires ESP32 and its unified recipe")
# Both backend objects and the private TwoWire instance are linked
# globals. The OLED nevertheless retains its one 1024-byte framebuffer
# after a live switch to TFT. The source assertion conservatively bounds
# the complete wrapper plus that allocation and allocator metadata to
# 2 KiB. TFT draws directly; it allocates no RGB framebuffer/canvas.
display_heap = 2048
elif display == "ST7789Display":
display_heap = st7789_display_heap(defines)
elif display == "ST7735Display":
@@ -187,6 +196,14 @@ def requirements(platform, defines, target):
# (up to 320 rows, including overlap while growing) and SDK bus/touch
# driver metadata. These allocations coexist with the UI sprite.
parts["display_dma_and_bus"] = 16384
if display == "HeltecV4Display":
# Pinned Arduino-ESP32 2.x: the private I2C controller owns two bounded
# 128-byte buffers, HAL/IDF locks, master state and transient command
# links. Switching to TFT can retain its SPI/parameter mutexes while
# the OLED framebuffer remains allocated. Reserve those SDK/bus owners
# together, even on a headless/off boot: a saved runtime choice can
# activate either panel later. Keep core/transient margins unchanged.
parts["display_i2c_spi_and_probe"] = 4096
mqtt = None
combined_rak_ethernet = "RAK4631_COMBINED_ETHERNET" in defines
cooperative_rak_ethernet = not companion and (combined_rak_ethernet or (
@@ -411,6 +428,7 @@ def requirements(platform, defines, target):
# including worst-case 8-byte Entry alignment.
small_display = display in {
"SSD1306Display", "SH1106Display", "ST7735Display", "U8g2Display",
"HeltecV4Display",
}
small_font = integer(defines, "UI_SMALL_MESSAGE_FONT", int(small_display))
default_preview = 161 if small_font else 78
+32 -3
View File
@@ -6,6 +6,13 @@ import sys
from optional_display_profiles import resolved_defines, spec_for_defines
SPECS = (
dict(key="unifiedV4Display", capability="display.heltec_v4.unified",
marker="meshcore.capability.display.heltec_v4.unified.v1",
platform="ESP32_PLATFORM", board="heltec_v4",
target=(r"heltec_v4(?:_2_v4_3|(?:_3)?(?:_expansionkit)?(?:_tft)?)_"
r"(?:(?:repeater(?:_bridge_espnow)?|room_server)(?:_observer_mqtt)?|sensor|"
r"companion_radio_(?:usb|ble|wifi|full)(?:_mqtt)?(?:_ps)?(?:_fem(?:on|off))?)"),
defines={"HELTEC_V4_DISPLAY_UNIFIED": "1", "DISPLAY_CLASS": "HeltecV4Display"}),
dict(key="combinedCompanion", capability="companion.picow.combined",
marker="meshcore.capability.companion.picow.combined.v1",
platform="RP2040_PLATFORM", board="rpipicow", target=r"PicoW_companion_radio",
@@ -62,12 +69,34 @@ def runtime_metadata(manifest, env, release_source):
return result
def unified_v4_replacements(target):
"""Ordinary V4 display successors; R8 and deployed mOTA IDs stay separate."""
match = re.fullmatch(
r"heltec_v4(?:_3)?(?:_expansionkit)?(?:_tft)?_"
r"(repeater(?:_bridge_espnow)?|room_server|sensor)(_observer_mqtt)?", target, re.I)
if match:
role, observer = match.groups()
base = "heltec_v4_" + role.lower()
choices = [base + (observer or ""), base]
if role.lower() == "repeater_bridge_espnow":
choices.append("heltec_v4_repeater")
return [(name, "unifiedV4Display") for name in dict.fromkeys(choices)
if name.lower() != target.lower()]
if re.fullmatch(
r"heltec_v4(?:_3)?(?:_expansionkit)?(?:_tft)?_companion_radio_"
r"(?:usb|ble|wifi|full)(?:_ps)?(?:_fem(?:on|off))?", target, re.I):
canonical = "heltec_v4_2_v4_3_companion_radio_full_femon"
return [(canonical, "unifiedV4Display")]
return []
def replacements(target):
"""Known functionality successors, never aliases for deployed update IDs."""
unified = unified_v4_replacements(target)
if re.fullmatch(r"PicoW_companion_radio_(?:usb|ble|wifi)", target, re.I):
return [("PicoW_companion_radio", "combinedCompanion")]
if re.fullmatch(r"heltec_v4(?:_3)?_expansionkit_tft_companion_radio_(?:usb|ble|wifi|full)(?:_ps)?(?:_fem(?:on|off))?", target, re.I):
return [("heltec_v4_tft_companion_radio_full_femon", "tftExpansionCompatible")]
return unified + [("heltec_v4_tft_companion_radio_full_femon", "tftExpansionCompatible")]
match = re.fullmatch(r"muzi_base_(uno|duo)_superio_companion_radio_(?:usb|ble|full)", target, re.I)
if match:
return [("muzi_base_" + match[1].lower() + "_companion_radio_full", "muziAccessories")]
@@ -76,7 +105,7 @@ def replacements(target):
match = re.fullmatch(r"heltec_v4_expansionkit_(repeater|room_server)(_observer_mqtt)?", target, re.I)
if match:
base = "heltec_v4_" + match[1].lower()
return [(base + (match[2] or ""), "oledExpansionCompatible"), (base, "oledExpansionCompatible")]
return unified + [(base + (match[2] or ""), "oledExpansionCompatible"), (base, "oledExpansionCompatible")]
match = re.fullmatch(r"heltec_v4_r8_tft_portrait_(repeater|room_server)_observer_mqtt", target, re.I)
if match:
base = "heltec_v4_r8_tft_" + match[1].lower()
@@ -84,7 +113,7 @@ def replacements(target):
match = re.fullmatch(r"heltec_rc32_without_display_(repeater|room_server|sensor|companion_radio_(?:usb|ble|wifi|full))(?:_observer_mqtt)?", target, re.I)
if match:
return [(target.lower().replace("_without_display_", "_", 1), "displayAuto")]
return []
return unified
def configuration_replacement(target, envs):