Add Heltec RC32, RC52 and RCC6 boards, and LC760CA GNSS support (#11572)

* refactor(graphics): select Arduino_GFX panels with a capability flag

TFTDisplay tested `defined(HACKADAY_COMMUNICATOR)` in a dozen places to mean
"this panel is driven by Arduino_GFX rather than LovyanGFX". Every new
Arduino_GFX board had to be appended to all of them.

Move the decision into the variant as USE_ARDUINO_GFX so the display code
stops naming individual boards. No behaviour change: the Hackaday Communicator
is still the only board that sets it.

* feat(boards): add Heltec RC32, RC52 and RCC6

Three boards around the same 128x220 NV3001B panel: RC32 (ESP32-S3), RCC6
(ESP32-C6) and RC52 (nRF52840). They differ only in how the panel bus is
wired, so they share one branch in TFTDisplay behind TFT_NV3001B.

RC32 and RC52 also carry a rotary encoder on a TCA6408 I2C expander. That
lands as its own input source rather than as board conditionals inside
i2cButton, which is the M5Stack UnitC6L button driver and stays untouched.

On RC52 and RCC6 the panel is an add-on module, so probe it before reporting
a screen. The probe reuses the bit-banged SPI helper that already backs the
T114 ST7789 check.

Arduino_GFX is pinned to the upstream commit that added the NV3001B driver;
it has not shipped in a tagged release yet.

Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com>

* feat(gps): detect and configure the LC760CA GNSS module

The LC760CA is another Unicore part, so it joins the $PDTINFO probe family
and reuses the CM121 message-rate setup. It answers with CC1161W.

GNSS_MODEL_LC760CA goes immediately before GNSS_MODEL_GENERIC_NMEA: the
sentinel has to stay last because isValidGnssModel() uses it as the exclusive
upper bound on values the probe cache may hold. Placing the new model after
it would leave LC760CA permanently uncacheable.

Co-Authored-By: Quency-D <55523105+Quency-D@users.noreply.github.com>

* fix(graphics): re-init the NV3001B after the panel rail comes back

DISPLAYOFF de-asserts VTFT_CTRL, which cuts power to the panel, so the
controller loses MADCTL, COLMOD and gamma. displayOn() only sends sleep-out
and cannot restore them, leaving the panel dark or in the wrong format after
wake. Re-run begin() once the rail has settled, and repaint in full since the
re-init leaves display RAM undefined.

Also stop the TCA6408 rotary polling from two threads at once. Registering as
an InputPollable meant InputBroker's pollSoon task could call pollOnce() while
runOnce() was mid-transfer on the main thread, with nothing serialising Wire
or the decoder state. Drop InputPollable and have the interrupt wake the
thread instead, the way ButtonThread does, so the bus and the decode stay on
one thread.

* fix(graphics): skip the NV3001B wake when re-init fails

begin() reports whether the bus came up. Ignoring it meant a failed re-init
still lit the backlight and drove a full-screen repaint at a panel that was
never initialised.

* chore(boards): ship the Heltec RC boards at release level

release is the normal level for a variant; the matrix generator still builds
each of these in this PR because they add a new platformio.ini.

---------

Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com>
This commit is contained in:
Ben Meadors
2026-08-23 11:00:37 +00:00
committed by GitHub
co-authored by Quency-D
parent ac330e6a6b
commit bfd1e1a231
23 files changed
+1031 -17

No files matched your search

+50 -12
View File
@@ -205,7 +205,7 @@ static void rak14014_tpIntHandle(void)
_rak14014_touch_int = true;
}
#elif defined(HACKADAY_COMMUNICATOR)
#elif defined(USE_ARDUINO_GFX)
#include <Arduino_GFX_Library.h>
Arduino_GFX *tft = nullptr;
@@ -1397,7 +1397,7 @@ void TFTDisplay::display(bool fromBlank)
}
}
}
#if defined(HACKADAY_COMMUNICATOR)
#if defined(USE_ARDUINO_GFX)
tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk);
#else
tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer);
@@ -1564,7 +1564,7 @@ void TFTDisplay::display(bool fromBlank)
const uint8_t lines_updated = 1;
#endif
#if defined(HACKADAY_COMMUNICATOR)
#if defined(USE_ARDUINO_GFX)
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
#else
@@ -1639,12 +1639,24 @@ void TFTDisplay::sendCommand(uint8_t com)
switch (com) {
case DISPLAYON: {
LOG_DEBUG("Display on");
#if defined(TFT_NV3001B)
// DISPLAYOFF cuts the panel rail, so the controller loses its configuration and sleep-out
// alone cannot bring it back. Restore the rail, let it settle, then re-run the init sequence.
digitalWrite(VTFT_CTRL, TFT_EN_ON);
delay(10);
if (!tft->begin(SPI_FREQUENCY)) {
// Nothing below this point can reach the panel, so skip the wake instead of lighting
// the backlight and repainting over a bus that did not come up.
LOG_ERROR("NV3001B re-init failed on wake");
break;
}
#endif
backlightEnable->set(true);
#if ARCH_PORTDUINO
display(true);
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
#elif defined(USE_ARDUINO_GFX)
tft->displayOn();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
@@ -1652,7 +1664,13 @@ void TFTDisplay::sendCommand(uint8_t com)
tft->powerSaveOff();
#endif
#ifdef VTFT_CTRL
#if defined(TFT_NV3001B)
// Re-init left display RAM undefined, so repaint in full rather than diff against a
// buffer that no longer describes the panel.
display(true);
#endif
#if defined(VTFT_CTRL) && !defined(TFT_NV3001B) // NV3001B panels already powered the rail above
digitalWrite(VTFT_CTRL, LOW);
#endif
#ifdef UNPHONE
@@ -1660,7 +1678,7 @@ void TFTDisplay::sendCommand(uint8_t com)
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(ST7789_CS) && \
!defined(HACKADAY_COMMUNICATOR) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
!defined(USE_ARDUINO_GFX) // T-Deck gets brightness set in Screen.cpp in the handleSetOn function
tft->setBrightness(172);
#endif
break;
@@ -1672,7 +1690,7 @@ void TFTDisplay::sendCommand(uint8_t com)
tft->clear();
if (portduino_config.displayBacklight.pin > 0)
digitalWrite(portduino_config.displayBacklight.pin, !TFT_BACKLIGHT_ON);
#elif defined(HACKADAY_COMMUNICATOR)
#elif defined(USE_ARDUINO_GFX)
tft->displayOff();
#elif !defined(RAK14014) && !defined(M5STACK) && !defined(UNPHONE) && !defined(HELTEC_MESH_NODE_T096) && \
!defined(HELTEC_MESH_NODE_T1)
@@ -1687,7 +1705,7 @@ void TFTDisplay::sendCommand(uint8_t com)
unphone.backlight(false); // using unPhone library
#endif
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR)
#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX)
tft->setBrightness(0);
#endif
break;
@@ -1703,7 +1721,7 @@ void TFTDisplay::setDisplayBrightness(uint8_t _brightness)
{
#if defined(RAK14014) || defined(HELTEC_MESH_NODE_T096) || defined(HELTEC_MESH_NODE_T1)
// todo
#elif !defined(HACKADAY_COMMUNICATOR)
#elif !defined(USE_ARDUINO_GFX)
tft->setBrightness(_brightness);
LOG_DEBUG("Brightness is set to value: %i ", _brightness);
#endif
@@ -1721,7 +1739,7 @@ bool TFTDisplay::hasTouch(void)
{
#ifdef RAK14014
return true;
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
return tft->touch() != nullptr;
#else
return false;
@@ -1740,7 +1758,7 @@ bool TFTDisplay::getTouch(int16_t *x, int16_t *y)
} else {
return false;
}
#elif !defined(M5STACK) && !defined(HACKADAY_COMMUNICATOR) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
#elif !defined(M5STACK) && !defined(USE_ARDUINO_GFX) && !defined(HELTEC_MESH_NODE_T096) && !defined(HELTEC_MESH_NODE_T1)
return tft->getTouch(x, y);
#else
return false;
@@ -1768,6 +1786,21 @@ bool TFTDisplay::connect()
tft = new Arduino_NV3007(bus, 40, 0 /* rotation */, false /* IPS */, 142 /* width */, 428 /* height */,
12 /* col offset 1 */, 0 /* row offset 1 */, 14 /* col offset 2 */, 0 /* row offset 2 */,
nv3007_279_init_operations, sizeof(nv3007_279_init_operations));
#elif defined(TFT_NV3001B)
// The Heltec RC panels all use the same controller and differ only in how the bus is wired.
#if defined(HELTEC_RC52)
// nRF52840: the panel sits on SPI1, clear of the LoRa radio on SPI0.
Arduino_DataBus *bus = new Arduino_HWSPI(TFT_RS, TFT_CS, &SPI1, true /* is_shared_interface */);
#elif defined(HELTEC_RCC6)
// ESP32-C6: the panel shares pins with the LoRa host, so bit-bang it rather than claim the peripheral.
Arduino_DataBus *bus = new Arduino_SWSPI(TFT_RS, TFT_CS, TFT_SCL, TFT_SDA, GFX_NOT_DEFINED /* MISO */);
#else
// ESP32-S3: keep the panel off the LoRa FSPI host, since Arduino_GFX reconfigures whichever bus it is handed.
Arduino_DataBus *bus =
new Arduino_ESP32SPI(TFT_RS, TFT_CS, TFT_SCL, TFT_SDA, GFX_NOT_DEFINED /* MISO */, HSPI /* spi_num */);
#endif
tft = new Arduino_NV3001B(bus, TFT_RST, 3 /* rotation */, true /* IPS */, TFT_WIDTH, TFT_HEIGHT, 0 /* col offset 1 */,
0 /* row offset 1 */, 0 /* col offset 2 */, 0 /* row offset 2 */);
#else
tft = new LGFX;
#endif
@@ -1779,8 +1812,13 @@ bool TFTDisplay::connect()
#ifdef UNPHONE
unphone.backlight(true); // using unPhone library
#endif
#ifdef HACKADAY_COMMUNICATOR
#ifdef USE_ARDUINO_GFX
#if defined(TFT_NV3001B)
// Arduino_SWSPI ignores the clock argument, so this only bites on the hardware-SPI variants.
bool beginStatus = tft->begin(SPI_FREQUENCY);
#else
bool beginStatus = tft->begin();
#endif
if (beginStatus)
LOG_DEBUG("TFT Success");
else