T-Echo-Card support (#10267)

# Conflicts:
#	src/graphics/draw/UIRenderer.cpp
This commit is contained in:
Thomas Göttgens
2026-05-19 09:31:04 +02:00
parent 0832330327
commit e2aa44ec54
20 changed files with 403 additions and 23 deletions

View File

@@ -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<SSD1306Wire *>(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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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"};

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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 ---

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 <Adafruit_NeoPixel.h>
#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 <NonBlockingRtttl.h>
#else
@@ -38,6 +51,10 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency:
CallbackObserver<ExternalNotificationModule, const InputEvent *>(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();

View File

@@ -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);

View File

@@ -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 <Adafruit_NeoPixel.h>
#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;

View File

@@ -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");

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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