From e2aa44ec5440f2b8b455acdaf748561371f10f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 19 May 2026 09:31:04 +0200 Subject: [PATCH] T-Echo-Card support (#10267) # Conflicts: # src/graphics/draw/UIRenderer.cpp --- src/graphics/Screen.cpp | 11 +- src/graphics/ScreenFonts.h | 2 +- src/graphics/SharedUIDisplay.cpp | 2 +- src/graphics/draw/DebugRenderer.cpp | 4 +- src/graphics/draw/MenuHandler.cpp | 2 +- src/graphics/draw/NodeListRenderer.cpp | 8 +- src/graphics/draw/NotificationRenderer.cpp | 2 +- src/graphics/draw/UIRenderer.cpp | 12 +- src/graphics/images.h | 2 +- src/main.cpp | 5 + src/mesh/NodeDB.cpp | 5 +- src/modules/ExternalNotificationModule.cpp | 10 + src/modules/ExternalNotificationModule.h | 17 ++ src/modules/StatusLEDModule.cpp | 33 +++ src/modules/StatusLEDModule.h | 26 +++ src/nimble/NimbleBluetooth.cpp | 2 +- variants/esp32c6/m5stack_unitc6l/variant.h | 3 + variants/nrf52840/t-echo-card/platformio.ini | 12 ++ variants/nrf52840/t-echo-card/variant.cpp | 66 ++++++ variants/nrf52840/t-echo-card/variant.h | 202 +++++++++++++++++++ 20 files changed, 403 insertions(+), 23 deletions(-) create mode 100644 variants/nrf52840/t-echo-card/platformio.ini create mode 100644 variants/nrf52840/t-echo-card/variant.cpp create mode 100644 variants/nrf52840/t-echo-card/variant.h diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 60e1c43a6..f51a6ee9e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -353,6 +353,11 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); +#if defined(OLED_Y_OFFSET_PAGES) + // Panels whose active window does not start at GDDRAM row 0 (e.g. 72x40 + // modules on pages 3..7) need a fixed vertical page shift on every write. + static_cast(dispdev)->setYOffset(OLED_Y_OFFSET_PAGES); +#endif #elif defined(USE_SPISSD1306) dispdev = new SSD1306Spi(SSD1306_RESET, SSD1306_RS, SSD1306_NSS, GEOMETRY_64_48); if (!dispdev->init()) { @@ -834,7 +839,7 @@ int32_t Screen::runOnce() #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) menuHandler::LoraRegionPicker(); #else menuHandler::OnboardMessage(); @@ -1058,7 +1063,7 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) if (!hiddenFrames.clock) { fsi.positions.clock = numframes; -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame @@ -1511,7 +1516,7 @@ void Screen::showFrame(FrameDirection direction) void Screen::setFastFramerate() { -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) dispdev->clear(); dispdev->display(); #endif diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 26276edb2..c6689d0d1 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -96,7 +96,7 @@ #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_MEDIUM FONT_LARGE_LOCAL // Height: 28 #define FONT_LARGE FONT_LARGE_LOCAL // Height: 28 -#elif defined(M5STACK_UNITC6L) +#elif defined(OLED_TINY) #define FONT_SMALL FONT_SMALL_LOCAL // Height: 13 #define FONT_MEDIUM FONT_SMALL_LOCAL // Height: 13 #define FONT_LARGE FONT_SMALL_LOCAL // Height: 13 diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 032b14dfa..7ad0b93bb 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -161,7 +161,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) // === Battery Icons === if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging batteryX += 1; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 6b26abe7f..6472f3e5e 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -449,7 +449,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) // === Fifth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; @@ -569,7 +569,7 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x // Label display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(labelX, getTextPositions(display)[line], label); -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) // Bar int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index a1d49946f..24302c1db 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -491,7 +491,7 @@ void menuHandler::TZPicker() void menuHandler::clockMenu() { -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) static const char *optionsArray[] = {"Back", "Time Format", "Timezone"}; #else static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 98644ee3b..d7f0a1483 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -21,7 +21,7 @@ extern bool haveGlyphs(const char *str); // Global screen instance extern graphics::Screen *screen; -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) static uint32_t lastSwitchTime = 0; #endif namespace graphics @@ -670,7 +670,7 @@ void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state unsigned long now = millis(); -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) display->clear(); if (now - lastSwitchTime >= 3000) { display->display(); @@ -706,7 +706,7 @@ void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *st unsigned long now = millis(); -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) display->clear(); if (now - lastSwitchTime >= 3000) { display->display(); @@ -771,7 +771,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, double lat = DegD(ourNode->position.latitude_i); double lon = DegD(ourNode->position.longitude_i); -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) display->clear(); uint32_t now = millis(); if (now - lastSwitchTime >= 2000) { diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 31eb2c3c8..3704dcf79 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -580,7 +580,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay } int16_t boxTop = (display->height() / 2) - (boxHeight / 2); boxHeight += (currentResolution == ScreenResolution::High) ? 2 : 1; -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) if (visibleTotalLines == 1) { boxTop += 25; } diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e3a4d13a2..92cc59a9a 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -20,7 +20,7 @@ // External variables extern graphics::Screen *screen; -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) static uint32_t lastSwitchTime = 0; #endif namespace graphics @@ -304,7 +304,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; display->clear(); -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) uint32_t now = millis(); if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 { @@ -518,7 +518,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i if (seenStr[0]) { display->drawString(x, getTextPositions(display)[line++], seenStr); } -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) // === 4. Uptime (only show if metric is present) === char uptimeStr[32] = ""; if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { @@ -795,7 +795,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } #endif -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) line += 1; // === Node Identity === @@ -1092,7 +1092,7 @@ void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLED // needs to be drawn relative to x and y // draw centered icon left to right and centered above the one line of app text -#if defined(M5STACK_UNITC6L) +#if defined(OLED_TINY) display->drawXbm(x + (SCREEN_WIDTH - 50) / 2, y + (SCREEN_HEIGHT - 28) / 2, icon_width, icon_height, icon_bits); display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1243,7 +1243,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } display->drawString(x, getTextPositions(display)[line++], altitudeLine); } -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) // === Draw Compass if heading is valid === if (validHeading) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- diff --git a/src/graphics/images.h b/src/graphics/images.h index 66fcbc79c..f11ad5686 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -318,7 +318,7 @@ const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; #define connection_icon_height 5 const uint8_t connection_icon[] = {0x36, 0x41, 0x5D, 0x41, 0x36}; -#ifdef M5STACK_UNITC6L +#ifdef OLED_TINY #include "img/icon_small.xbm" #else #include "img/icon.xbm" diff --git a/src/main.cpp b/src/main.cpp index 2f4b12437..dab965c4c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -741,6 +741,11 @@ void setup() } } #endif +#ifdef OLED_GEOMETRY_OVERRIDE + // Per-variant geometry (e.g. 72x40 micro-OLEDs). Takes precedence over the + // default GEOMETRY_128_64 set at the top of setup(). + screen_geometry = OLED_GEOMETRY_OVERRIDE; +#endif #endif #if !MESHTASTIC_EXCLUDE_I2C diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d35e0a38a..3ce78513c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -836,7 +836,8 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.has_store_forward = true; moduleConfig.has_telemetry = true; moduleConfig.has_external_notification = true; -#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) +#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) || \ + defined(NEOPIXEL_STATUS_NOTIFICATION_PIN) moduleConfig.external_notification.enabled = true; #endif #if defined(PIN_BUZZER) @@ -857,7 +858,7 @@ void NodeDB::installDefaultModuleConfig() #endif #if defined(PIN_VIBRATION) moduleConfig.external_notification.nag_timeout = 2; -#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) +#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) || defined(NEOPIXEL_STATUS_NOTIFICATION_PIN) moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; #endif diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 16ccdd744..0a1c4a6dd 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -206,6 +206,10 @@ void ExternalNotificationModule::setExternalState(uint8_t index, bool on) #ifdef PCA_LED_NOTIFICATION io.digitalWrite(PCA_LED_NOTIFICATION, on); +#endif +#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN + notificationPixel.setPixelColor(0, on ? NEOPIXEL_STATUS_NOTIFICATION_COLOR : 0); + notificationPixel.show(); #endif break; } @@ -324,6 +328,12 @@ ExternalNotificationModule::ExternalNotificationModule() LOG_INFO("Use Pin %i in digital mode", output); pinMode(output, OUTPUT); } +#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN + LOG_INFO("Use WS2812 on GPIO %d as notification LED", NEOPIXEL_STATUS_NOTIFICATION_PIN); + notificationPixel.begin(); + notificationPixel.clear(); + notificationPixel.show(); +#endif setExternalState(0, false); externalTurnedOn[0] = 0; if (moduleConfig.external_notification.output_vibra) { diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 94b021360..8781c1ca8 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -10,6 +10,19 @@ extern AmbientLightingThread *ambientLightingThread; #endif +// Drive a single WS2812 as the notification LED (M1/M2-style LED_NOTIFICATION +// but addressable). A variant defines NEOPIXEL_STATUS_NOTIFICATION_PIN to +// enable. Colour defaults to green but can be overridden. +#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN +#include +#ifndef NEOPIXEL_STATUS_TYPE +#define NEOPIXEL_STATUS_TYPE (NEO_GRB + NEO_KHZ800) +#endif +#ifndef NEOPIXEL_STATUS_NOTIFICATION_COLOR +#define NEOPIXEL_STATUS_NOTIFICATION_COLOR 0x00FF00 // green +#endif +#endif + #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) #include #else @@ -38,6 +51,10 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); uint32_t output = 0; +#ifdef NEOPIXEL_STATUS_NOTIFICATION_PIN + Adafruit_NeoPixel notificationPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_NOTIFICATION_PIN, NEOPIXEL_STATUS_TYPE); +#endif + public: ExternalNotificationModule(); diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index 4ea34fb52..f3a0e7a03 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -17,8 +17,29 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") if (inputBroker) inputObserver.observe(inputBroker); #endif +#ifdef NEOPIXEL_STATUS_POWER_PIN + powerPixel.begin(); + powerPixel.clear(); + powerPixel.show(); +#endif +#ifdef NEOPIXEL_STATUS_PAIRING_PIN + pairingPixel.begin(); + pairingPixel.clear(); + pairingPixel.show(); +#endif } +// Helper: write a 1-pixel NeoPixel strand to `color` when stateOn, else clear. +// Kept as a static inline here (rather than a member) so it compiles out +// completely when no NeoPixel status pins are defined. +#if defined(NEOPIXEL_STATUS_POWER_PIN) || defined(NEOPIXEL_STATUS_PAIRING_PIN) +static inline void writeStatusPixel(Adafruit_NeoPixel &pixel, uint32_t color, bool stateOn) +{ + pixel.setPixelColor(0, stateOn ? color : 0); + pixel.show(); +} +#endif + int StatusLEDModule::handleStatusUpdate(const meshtastic::Status *arg) { switch (arg->getStatusType()) { @@ -176,6 +197,12 @@ int32_t StatusLEDModule::runOnce() #ifdef LED_PAIRING digitalWrite(LED_PAIRING, PAIRING_LED_state); #endif +#ifdef NEOPIXEL_STATUS_POWER_PIN + writeStatusPixel(powerPixel, NEOPIXEL_STATUS_POWER_COLOR, CHARGE_LED_state == LED_STATE_ON); +#endif +#ifdef NEOPIXEL_STATUS_PAIRING_PIN + writeStatusPixel(pairingPixel, NEOPIXEL_STATUS_PAIRING_COLOR, PAIRING_LED_state == LED_STATE_ON); +#endif #ifdef RGB_LED_POWER if (!config.device.led_heartbeat_disabled) { @@ -225,6 +252,12 @@ void StatusLEDModule::setPowerLED(bool LEDon) #ifdef LED_PAIRING digitalWrite(LED_PAIRING, ledState); #endif +#ifdef NEOPIXEL_STATUS_POWER_PIN + writeStatusPixel(powerPixel, NEOPIXEL_STATUS_POWER_COLOR, LEDon); +#endif +#ifdef NEOPIXEL_STATUS_PAIRING_PIN + writeStatusPixel(pairingPixel, NEOPIXEL_STATUS_PAIRING_COLOR, LEDon); +#endif #ifdef Battery_LED_1 digitalWrite(Battery_LED_1, ledState); diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index f66a536f6..f20198e39 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -13,6 +13,25 @@ #include "input/InputBroker.h" #endif +// WS2812/NeoPixel status-LED support. A variant may define +// NEOPIXEL_STATUS_POWER_PIN (required to enable the power/charge pixel) +// NEOPIXEL_STATUS_POWER_COLOR (optional, default red 0xFF0000) +// NEOPIXEL_STATUS_PAIRING_PIN / _COLOR (default blue 0x0000FF) +// Each pixel is a standalone 1-LED strand on its own GPIO — this mirrors how +// boards like the LilyGo T-Echo-Card expose three independent WS2812s. +#if defined(NEOPIXEL_STATUS_POWER_PIN) || defined(NEOPIXEL_STATUS_PAIRING_PIN) +#include +#ifndef NEOPIXEL_STATUS_TYPE +#define NEOPIXEL_STATUS_TYPE (NEO_GRB + NEO_KHZ800) +#endif +#ifndef NEOPIXEL_STATUS_POWER_COLOR +#define NEOPIXEL_STATUS_POWER_COLOR 0xFF0000 // red +#endif +#ifndef NEOPIXEL_STATUS_PAIRING_COLOR +#define NEOPIXEL_STATUS_PAIRING_COLOR 0x0000FF // blue +#endif +#endif + class StatusLEDModule : private concurrency::OSThread { bool slowTrack = false; @@ -27,6 +46,13 @@ class StatusLEDModule : private concurrency::OSThread void setPowerLED(bool); +#ifdef NEOPIXEL_STATUS_POWER_PIN + Adafruit_NeoPixel powerPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_POWER_PIN, NEOPIXEL_STATUS_TYPE); +#endif +#ifdef NEOPIXEL_STATUS_PAIRING_PIN + Adafruit_NeoPixel pairingPixel = Adafruit_NeoPixel(1, NEOPIXEL_STATUS_PAIRING_PIN, NEOPIXEL_STATUS_TYPE); +#endif + protected: unsigned int my_interval = 1000; // interval in millisconds virtual int32_t runOnce() override; diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 3bb4ce817..d4cb1d9ef 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -610,7 +610,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, y_offset + y, "Bluetooth"); -#if !defined(M5STACK_UNITC6L) +#if !defined(OLED_TINY) display->setFont(FONT_SMALL); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; display->drawString(x_offset + x, y_offset + y, "Enter this code"); diff --git a/variants/esp32c6/m5stack_unitc6l/variant.h b/variants/esp32c6/m5stack_unitc6l/variant.h index 1654ee590..576d4e114 100644 --- a/variants/esp32c6/m5stack_unitc6l/variant.h +++ b/variants/esp32c6/m5stack_unitc6l/variant.h @@ -48,6 +48,9 @@ void c6l_init(); #define SSD1306_RESET 15 // #define OLED_DG 1 #endif +// Tiny OLED panel — opts into compile-time layout/font/feature substitutions +// gated on OLED_TINY across the graphics stack. +#define OLED_TINY #define SCREEN_TRANSITION_FRAMERATE 10 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness diff --git a/variants/nrf52840/t-echo-card/platformio.ini b/variants/nrf52840/t-echo-card/platformio.ini new file mode 100644 index 000000000..bc012d6e1 --- /dev/null +++ b/variants/nrf52840/t-echo-card/platformio.ini @@ -0,0 +1,12 @@ +[env:t-echo-card] +extends = nrf52840_base +board = t-echo +board_level = extra +debug_tool = jlink + +build_flags = ${nrf52840_base.build_flags} + -I variants/nrf52840/t-echo-card + -D PRIVATE_HW + -D T_ECHO_CARD + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/t-echo-card> diff --git a/variants/nrf52840/t-echo-card/variant.cpp b/variants/nrf52840/t-echo-card/variant.cpp new file mode 100644 index 000000000..e82a63f8e --- /dev/null +++ b/variants/nrf52840/t-echo-card/variant.cpp @@ -0,0 +1,66 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "Arduino.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // No plain GPIO LEDs on this board (only WS2812 addressable LEDs, not driven here). +} + +// Reproduces the vendor firmware's boot sequence from +// examples/original_test/original_test.ino. Runs before Meshtastic touches +// PIN_POWER_EN, so the RT9080 LDO gets a clean reset pulse and peripherals +// whose EN pins must be LOW at boot (GPS_EN, GPS_RF_EN, BUZZER) aren't left +// floating while the 3V3 rail is ramping. +void earlyInitVariant() +{ + // 3.3V rail: toggle RT9080_EN HIGH → LOW → HIGH with 100 ms dwell so the + // LDO enters enable from a known state. The single-shot HIGH in main.cpp + // is not enough on this hardware — if the chip was in a half-enabled + // state from a previous reset, the rail brown-outs once LoRa TX fires. + pinMode(PIN_POWER_EN, OUTPUT); + digitalWrite(PIN_POWER_EN, HIGH); + delay(100); + digitalWrite(PIN_POWER_EN, LOW); + delay(100); + digitalWrite(PIN_POWER_EN, HIGH); + delay(100); + + // Park peripherals with active-high enables LOW so they don't sink + // current while the rest of setup() runs. + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, LOW); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, LOW); + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); +} diff --git a/variants/nrf52840/t-echo-card/variant.h b/variants/nrf52840/t-echo-card/variant.h new file mode 100644 index 000000000..37fa28d89 --- /dev/null +++ b/variants/nrf52840/t-echo-card/variant.h @@ -0,0 +1,202 @@ +// Variant definition for LilyGo T-Echo-Card (nRF52840) + +#ifndef _VARIANT_T_ECHO_CARD_ +#define _VARIANT_T_ECHO_CARD_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32kHz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs - board only exposes 3x WS2812 addressable LEDs. No plain GPIO LEDs. +// Intentionally do not define PIN_LED1 on this variant, so nRF52 platform +// code does not auto-enable a nonexistent GPIO power/status LED. +#define LED_STATE_ON 1 + +// Three independent WS2812 data lines (one LED per line, not a daisy chain). +// Each is driven as a 1-pixel NeoPixel by StatusLEDModule / ExternalNotification, +// assigns LED_POWER (red) and LED_NOTIFICATION (green). +#define WS2812_DATA_1 (32 + 7) // P1.7 - charge/heartbeat (red) +#define WS2812_DATA_2 (32 + 12) // P1.12 - external notification (green) +#define WS2812_DATA_3 (0 + 28) // P0.28 - BLE pairing (blue) + +// Wire each WS2812 to a status role. Colour defaults are scaled to 25% +// brightness (0x40) — the bare-die WS2812s on this board are very bright at +// full intensity in a close-range enclosure. +#define NEOPIXEL_STATUS_POWER_PIN WS2812_DATA_1 +#define NEOPIXEL_STATUS_NOTIFICATION_PIN WS2812_DATA_2 +#define NEOPIXEL_STATUS_PAIRING_PIN WS2812_DATA_3 +#define NEOPIXEL_STATUS_POWER_COLOR 0x400000 // red @ 25% +#define NEOPIXEL_STATUS_NOTIFICATION_COLOR 0x004000 // green @ 25% +#define NEOPIXEL_STATUS_PAIRING_COLOR 0x000040 // blue @ 25% + +// The charger IC does not blink on its own; let StatusLEDModule do the +// software blink while charging +// If left defined: hardware would be expected to handle the charging pulse. +// #define POWER_LED_HARDWARE_BLINKS_WHILE_CHARGING + +// Buttons +#define PIN_BUTTON1 (32 + 10) // KEY_1: P1.10 + +#define BUTTON_CLICK_MS 400 + +// Analog pins +#define PIN_A0 (0 + 2) // Battery ADC (BATTERY_ADC_DATA) + +#define BATTERY_PIN PIN_A0 + +static const uint8_t A0 = PIN_A0; + +#define ADC_RESOLUTION 14 + +// BATTERY_MEASUREMENT_CONTROL - enable divider for battery reading +#define ADC_CTRL (0 + 31) +#define ADC_CTRL_ENABLED HIGH + +// NFC placeholders, not used +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +// Wire Interfaces (IIC_1 on the vendor header) +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (32 + 4) // IIC_1_SDA: P1.4 +#define PIN_WIRE_SCL (32 + 2) // IIC_1_SCL: P1.2 + +// External serial flash ZD25WQ32CEIGR +// QSPI Pins +#define PIN_QSPI_SCK (0 + 4) +#define PIN_QSPI_CS (0 + 12) +#define PIN_QSPI_IO0 (0 + 6) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (0 + 8) // MISO if using two bit interface +#define PIN_QSPI_IO2 (32 + 9) // WP +#define PIN_QSPI_IO3 (0 + 26) // HOLD + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR +#define EXTERNAL_FLASH_USE_QSPI + +// Lora S62F (SX1262) +#define USE_SX1262 +#define SX126X_CS (0 + 11) +#define SX126X_DIO1 (32 + 8) +#define SX126X_DIO2 (0 + 5) +#define SX126X_BUSY (0 + 14) +#define SX126X_RESET (0 + 7) +#define SX126X_RXEN (32 + 1) // SX1262_RF_VC2 +#define SX126X_TXEN (0 + 27) // SX1262_RF_VC1 +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// ─────────────────────────────────────────────────────────────────────────── +// OLED display: SSD1315 on I2C @ 0x3C (IIC_1). SSD1315 is register-compatible +// with SSD1306, so USE_SSD1306 initializes the controller correctly. +// +// Viewport: the physical panel is 72×40, mapped into the SSD1315's 128×64 +// GDDRAM at columns 28..99, pages 3..7 (rows 24..63). The firmware handles +// this by: +// * asking the library for GEOMETRY_72_40, which sets the framebuffer to +// 72×40 and emits the right SETMULTIPLEX (39) / SETCOMPINS at init; +// * relying on SSD1306Wire's built-in horizontal auto-centering +// ((128 - width) / 2 = 28), so no horizontal shim is needed; +// * calling SSD1306Wire::setYOffset(3) in Screen.cpp when +// OLED_Y_OFFSET_PAGES is defined — this shifts every PAGEADDR write by +// three pages (24 rows) so data lands on the visible rows. +// ─────────────────────────────────────────────────────────────────────────── +#define HAS_SCREEN 1 +#define USE_SSD1306 +#define OLED_GEOMETRY_OVERRIDE GEOMETRY_72_40 +#define OLED_Y_OFFSET_PAGES 3 +#define OLED_TINY + +// Controls power 3V3 for all peripherals (GPS + LoRa + Sensor) +#define PIN_POWER_EN (0 + 30) // RT9080_EN + +// SPI1 is unused (no external SPI display). Keep declarations for the core. +#define PIN_SPI1_MISO (-1) +#define PIN_SPI1_MOSI (-1) +#define PIN_SPI1_SCK (-1) + +// GPS pins +#define GPS_L76K +#define GPS_BAUDRATE 9600 +#define HAS_GPS 1 + +#define PIN_GPS_EN (32 + 15) // GPS_EN: P1.15 - GPS power enable +#define GPS_EN_ACTIVE 1 +#define PIN_GPS_STANDBY (0 + 25) // GPS_WAKE_UP: P0.25 - wakeup pin +#define PIN_GPS_PPS (0 + 23) // GPS_1PPS: P0.23 +#define GPS_RX_PIN (0 + 19) // MCU RX ← GPS's TX (vendor GPS_UART_TX / P0.19) +#define GPS_TX_PIN (0 + 21) // MCU TX → GPS's RX (vendor GPS_UART_RX / P0.21) +#define PIN_GPS_RESET (0 + 29) // GPS_RF_EN: GPS RF enable / reset + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_RX_PIN +#define PIN_SERIAL1_TX GPS_TX_PIN + +// SPI Interfaces (LoRa on SPI0) +#define SPI_INTERFACES_COUNT 2 + +// For LORA, SPI 0 +#define PIN_SPI_MISO (0 + 17) +#define PIN_SPI_MOSI (0 + 15) +#define PIN_SPI_SCK (0 + 13) + +// Battery +// The battery sense is hooked to PIN_A0 (P0.2) via a divider controlled by ADC_CTRL. +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (2.0F) + +// Buzzer (PWM output, passive piezo) +#define PIN_BUZZER (32 + 6) // BUZZER_DATA: P1.6 + +// ─────────────────────────────────────────────────────────────────────────── +// I²S speaker (MAX98357 Class-D amp). Stereo I²S data path. +// Not supported on nrf52. These defines exist for out-of-tree code only. +// ─────────────────────────────────────────────────────────────────────────── +#define SPEAKER_EN (32 + 11) // P1.11 - amp main enable +#define SPEAKER_EN_2 (0 + 3) // P0.3 - secondary enable (vendor firmware toggles both) +#define SPEAKER_BCLK (0 + 16) // P0.16 - I2S bit clock +#define SPEAKER_DATA (0 + 20) // P0.20 - I2S data (SDOUT) +#define SPEAKER_WS_LRCK (0 + 22) // P0.22 - I2S word select / LRCK + +// ─────────────────────────────────────────────────────────────────────────── +// PDM microphone (ST MP34DT05). +// TODO to enable a mic path: +// Use Adafruit nRF52 core's built-in PDM.h wrapper (Arduino-compatible +// API exists on nRF52840). Clock on MIC_SCLK, data on MIC_DATA. +// ─────────────────────────────────────────────────────────────────────────── +#define MIC_SCLK (32 + 3) // P1.3 - PDM clock (MIC_SCLK on vendor header) +#define MIC_DATA (32 + 5) // P1.5 - PDM data (MIC_DATA on vendor header) + +#define SERIAL_PRINT_PORT 0 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif