updating t-echo card folder

This commit is contained in:
pelgraine
2026-05-05 12:41:24 +10:00
parent 8afe71510e
commit 20310018d3
20 changed files with 1490 additions and 967 deletions
+14 -33
View File
@@ -5,47 +5,29 @@
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": [
"-DARDUINO_NRF52840_TECHO_CARD",
"-DNRF52840_XXAA",
"-DNRF52_SERIES",
"-DUSE_LFXO",
"-DLED_BUILTIN=39",
"-DLED_BLUE=39",
"-DLED_RED=39",
"-DLED_STATE_ON=1",
"-DPIN_WIRE_SDA=36",
"-DPIN_WIRE_SCL=34",
"-DWIRE_INTERFACES_COUNT=1",
"-DPIN_SPI_MISO=17",
"-DPIN_SPI_SCK=13",
"-DPIN_SPI_MOSI=15",
"-DSPI_INTERFACES_COUNT=1",
"-DPIN_SERIAL1_RX=21",
"-DPIN_SERIAL1_TX=19",
"-DPIN_A0=2"
],
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [["0x239A", "0x8029"]],
"hwids": [
["0x239A", "0x8029"]
],
"usb_product": "T-Echo Card",
"mcu": "nrf52840",
"variant": "lilygo_techo_card",
"variants_dir": "variants_bsp",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"usb_product": "T-Echo Card"
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth", "lora"],
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"openocd_target": "nrf52840"
@@ -53,15 +35,14 @@
"frameworks": ["arduino"],
"name": "LilyGo T-Echo Card (nRF52840, SX1262, 4MB Flash)",
"upload": {
"flash_size": "796KB",
"maximum_ram_size": 248832,
"maximum_size": 815104,
"native_usb": true,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["nrfutil", "jlink", "cmsis-dap"],
"require_upload_port": true,
"speed": 115200,
"native_usb": true,
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://github.com/Xinyuan-LilyGO/T-Echo-Card",
@@ -0,0 +1,76 @@
#pragma once
#include <Arduino.h>
// Transparent Stream wrapper that counts NMEA sentences (newline-delimited)
// flowing from the GPS serial port to the MicroNMEA parser.
//
// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock);
// Use: GPSStreamCounter gpsStream(Serial2);
// MicroNMEALocationProvider gps(gpsStream, &rtc_clock);
//
// Every read() call passes through to the underlying stream; when a '\n'
// is seen the sentence counter increments. This lets the UI display a
// live "nmea" count so users can confirm the baud rate is correct and
// the GPS module is actually sending data.
class GPSStreamCounter : public Stream {
public:
GPSStreamCounter(Stream& inner)
: _inner(inner), _sentences(0), _sentences_snapshot(0),
_last_snapshot(0), _sentences_per_sec(0) {}
// --- Stream read interface (passes through) ---
int available() override { return _inner.available(); }
int peek() override { return _inner.peek(); }
int read() override {
int c = _inner.read();
if (c == '\n') {
_sentences++;
}
return c;
}
// --- Stream write interface (pass through for NMEA commands if needed) ---
size_t write(uint8_t b) override { return _inner.write(b); }
// Required override on Adafruit nRF52 BSP where Stream::flush() is pure virtual.
// No-op equivalent on ESP32 cores that provide a default implementation.
void flush() override { _inner.flush(); }
// --- Sentence counting API ---
// Total sentences received since boot (or last reset)
uint32_t getSentenceCount() const { return _sentences; }
// Sentences received per second (updated each time you call it,
// with a 1-second rolling window)
uint16_t getSentencesPerSec() {
unsigned long now = millis();
unsigned long elapsed = now - _last_snapshot;
if (elapsed >= 1000) {
uint32_t delta = _sentences - _sentences_snapshot;
// Scale to per-second if interval wasn't exactly 1000ms
_sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed);
_sentences_snapshot = _sentences;
_last_snapshot = now;
}
return _sentences_per_sec;
}
// Reset all counters (e.g. when GPS hardware power cycles)
void resetCounters() {
_sentences = 0;
_sentences_snapshot = 0;
_sentences_per_sec = 0;
_last_snapshot = millis();
}
private:
Stream& _inner;
volatile uint32_t _sentences;
uint32_t _sentences_snapshot;
unsigned long _last_snapshot;
uint16_t _sentences_per_sec;
};
@@ -0,0 +1,339 @@
#include "TechoCardBoard.h"
#include "variant.h"
#include <Wire.h>
#include <nrf_soc.h>
#include <InternalFileSystem.h>
using namespace Adafruit_LittleFS_Namespace;
void TechoCardBoard::begin() {
NRF52BoardDCDC::begin();
Serial.begin(115200);
// RT9080 3V3 rail: clean reset cycle (from Meshtastic PR #10267)
// Toggling EN HIGH→LOW→HIGH forces a clean power-on, preventing
// brown-out when LoRa TX fires at full power.
#if PIN_OLED_EN >= 0
pinMode(PIN_OLED_EN, OUTPUT);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
digitalWrite(PIN_OLED_EN, LOW);
delay(100);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
#endif
// Park peripheral enable pins LOW before setup runs
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
pinMode(PIN_GPS_RF_EN, OUTPUT);
digitalWrite(PIN_GPS_RF_EN, LOW);
#endif
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
#endif
#if defined(HAS_SPEAKER)
pinMode(PIN_SPK_EN, OUTPUT);
digitalWrite(PIN_SPK_EN, LOW);
#if PIN_SPK_EN2 >= 0
pinMode(PIN_SPK_EN2, OUTPUT);
digitalWrite(PIN_SPK_EN2, LOW);
#endif
#endif
// Enable GPS power after rail stabilises
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
delay(10);
digitalWrite(PIN_GPS_EN, HIGH);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
digitalWrite(PIN_GPS_RF_EN, HIGH);
#endif
// Initialise GPS UART
#if defined(HAS_GPS)
Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX);
Serial1.begin(GPS_BAUDRATE);
#endif
pinMode(PIN_VBAT_READ, INPUT);
pinMode(PIN_USER_BTN, INPUT);
// Initialise I2C -- must be done before display.begin() is called from main.cpp
Wire.begin();
Wire.setClock(400000);
// Initialise WS2812 NeoPixel chain (all off at boot)
// Force data line LOW before init to prevent stray HIGH latching green
#if defined(HAS_RGB_LED)
pinMode(PIN_RGB_LED_1, OUTPUT);
digitalWrite(PIN_RGB_LED_1, LOW);
delayMicroseconds(300); // WS2812 reset pulse is ~280µs
_pixels.begin();
_pixels.clear();
_pixels.show();
#endif
}
void TechoCardBoard::enableGPS(bool enable) {
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
digitalWrite(PIN_GPS_EN, enable ? HIGH : LOW);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
digitalWrite(PIN_GPS_RF_EN, enable ? HIGH : LOW);
#endif
}
float TechoCardBoard::getMCUTemperature() {
// SoftDevice owns the TEMP peripheral -- direct register access hard faults.
// Use sd_temp_get() when SoftDevice is enabled.
int32_t temp;
uint8_t sd_en = 0;
sd_softdevice_is_enabled(&sd_en);
if (sd_en) {
if (sd_temp_get(&temp) == NRF_SUCCESS) {
return temp * 0.25f;
}
return NAN;
}
// SoftDevice off -- fall back to parent's direct register access
return NRF52Board::getMCUTemperature();
}
void TechoCardBoard::enableSpeaker(bool enable) {
#if defined(HAS_SPEAKER)
digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW);
#if PIN_SPK_EN2 >= 0
digitalWrite(PIN_SPK_EN2, enable ? HIGH : LOW);
#endif
#endif
}
void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) {
#if defined(HAS_RGB_LED)
uint32_t color = Adafruit_NeoPixel::Color(r, g, b);
for (int i = 0; i < NUM_NEOPIXELS; i++) {
_pixels.setPixelColor(i, color);
}
_pixels.show();
#else
(void)r; (void)g; (void)b;
#endif
}
void TechoCardBoard::ledOff() {
setLED(0, 0, 0);
}
void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) {
#if defined(HAS_RGB_LED)
if (led_index < NUM_NEOPIXELS) {
_pixels.setPixelColor(led_index, color);
_pixels.show();
}
#else
(void)led_index; (void)color;
#endif
}
void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) {
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
if (freq_hz == 0 || duration_ms == 0) {
noTone(PIN_BUZZER);
return;
}
tone(PIN_BUZZER, freq_hz, duration_ms);
#else
(void)freq_hz; (void)duration_ms;
#endif
}
// =============================================================================
// BQ25896 Charger IC (I2C address 0x6B)
// =============================================================================
#define BQ25896_ADDR 0x6B
bool TechoCardBoard::probeCharger() {
if (!_chargerProbed) {
Wire.beginTransmission(BQ25896_ADDR);
_chargerPresent = (Wire.endTransmission() == 0);
_chargerProbed = true;
if (!_chargerPresent) {
Serial.println("BQ25896: not found at 0x6B");
}
}
return _chargerPresent;
}
uint8_t TechoCardBoard::readChargerReg(uint8_t reg) {
if (!probeCharger()) return 0;
Wire.beginTransmission(BQ25896_ADDR);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return 0;
Wire.requestFrom((uint8_t)BQ25896_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
void TechoCardBoard::writeChargerReg(uint8_t reg, uint8_t val) {
Wire.beginTransmission(BQ25896_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
void TechoCardBoard::enableChargerADC() {
uint8_t reg02 = readChargerReg(0x02);
reg02 |= 0xC0; // CONV_RATE=1 (continuous) + CONV_START=1
writeChargerReg(0x02, reg02);
}
uint8_t TechoCardBoard::getChargeStatus() {
return (readChargerReg(0x0B) >> 3) & 0x03;
}
uint16_t TechoCardBoard::getChargerBattMV() {
return 2304 + (readChargerReg(0x0E) & 0x7F) * 20;
}
uint8_t TechoCardBoard::getChargerTSPCT() {
return 21 + (readChargerReg(0x10) & 0x7F);
}
// =============================================================================
// ICM20948 / AK09916 Compass
//
// Enable I2C bypass on the ICM20948 so the AK09916 magnetometer at 0x0C
// appears directly on Wire. Then set continuous measurement mode.
// =============================================================================
#define ICM20948_ADDR 0x68
#define AK09916_ADDR 0x0C
static uint8_t _i2c_rd(uint8_t addr, uint8_t reg) {
Wire.beginTransmission(addr);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return 0;
Wire.requestFrom(addr, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
static void _i2c_wr(uint8_t addr, uint8_t reg, uint8_t val) {
Wire.beginTransmission(addr);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
bool TechoCardBoard::initCompass() {
if (_compassReady) return true;
// Bank 0
_i2c_wr(ICM20948_ADDR, 0x7F, 0x00);
// Check WHO_AM_I (expect 0xEA)
if (_i2c_rd(ICM20948_ADDR, 0x00) != 0xEA) return false;
// Wake up: auto clock, not sleep
_i2c_wr(ICM20948_ADDR, 0x06, 0x01);
delay(10);
// Enable I2C bypass so AK09916 is directly accessible
_i2c_wr(ICM20948_ADDR, 0x0F, 0x02);
delay(5);
// Check AK09916 WHO_AM_I (expect 0x09)
if (_i2c_rd(AK09916_ADDR, 0x01) != 0x09) return false;
// Leave in power-down -- readMag triggers single measurements on demand
_i2c_wr(AK09916_ADDR, 0x31, 0x00);
_compassReady = true;
return true;
}
bool TechoCardBoard::readMag(int16_t& mx, int16_t& my, int16_t& mz) {
if (!_compassReady) return false;
// Single-measurement mode: trigger one fresh measurement per call.
// Continuous mode gets disrupted by OLED I2C display writes sharing
// the bus through ICM20948 bypass, causing stale data.
_i2c_wr(AK09916_ADDR, 0x31, 0x01); // single measurement trigger
// Wait for data ready (measurement takes ~7.2ms)
for (int i = 0; i < 20; i++) {
if (_i2c_rd(AK09916_ADDR, 0x10) & 0x01) break;
delay(1);
}
// Burst read 6 data bytes + ST2 (must read ST2 to complete cycle)
Wire.beginTransmission(AK09916_ADDR);
Wire.write(0x11);
if (Wire.endTransmission(false) != 0) return false;
Wire.requestFrom((uint8_t)AK09916_ADDR, (uint8_t)7);
if (Wire.available() < 7) return false;
uint8_t buf[7];
for (int i = 0; i < 7; i++) buf[i] = Wire.read();
mx = (int16_t)(buf[1] << 8 | buf[0]);
my = (int16_t)(buf[3] << 8 | buf[2]);
mz = (int16_t)(buf[5] << 8 | buf[4]);
// buf[6] = ST2, read to unlatch
return true;
}
// Power down the AK09916 magnetometer and put the ICM20948 itself to sleep.
// Saves ~3-4mA when not actively viewing the compass page.
// Next call to initCompass() will fully re-initialise the chain.
void TechoCardBoard::sleepCompass() {
if (!_compassReady) return;
// Bank 0 (in case we drifted)
_i2c_wr(ICM20948_ADDR, 0x7F, 0x00);
// AK09916 CNTL2 = 0x00 -- power-down mode (stops continuous measurement)
_i2c_wr(AK09916_ADDR, 0x31, 0x00);
// ICM20948 PWR_MGMT_1 = 0x40 -- SLEEP bit set
_i2c_wr(ICM20948_ADDR, 0x06, 0x40);
_compassReady = false;
}
// =============================================================================
// Compass calibration persistence
// =============================================================================
#define COMPASS_CAL_FILE "/compass_cal"
bool TechoCardBoard::loadCalibration() {
// InternalFS must already be initialised (done in main.cpp setup)
File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_READ);
if (file) {
int n = file.read((uint8_t*)&_cal, sizeof(_cal));
file.close();
if (n == (int)sizeof(_cal) && _cal.magic == COMPASS_CAL_MAGIC) {
return true;
}
}
// No valid calibration -- reset to identity (no correction)
_cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 };
return false;
}
bool TechoCardBoard::saveCalibration(const CompassCalibration& cal) {
_cal = cal;
_cal.magic = COMPASS_CAL_MAGIC;
// Direct-write pattern: remove then create (nRF52 LittleFS compatible)
InternalFS.remove(COMPASS_CAL_FILE);
File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_WRITE);
if (!file) return false;
file.write((const uint8_t*)&_cal, sizeof(_cal));
file.close();
return true;
}
+100
View File
@@ -0,0 +1,100 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/NRF52Board.h>
#include "variant.h"
#if defined(HAS_RGB_LED)
#include <Adafruit_NeoPixel.h>
#endif
// Hard-iron offsets + soft-iron axis scaling.
// Computed by on-device calibration (rotate slowly for ~20 seconds).
// Persisted to /compass_cal on InternalFS.
#define COMPASS_CAL_MAGIC 0xCA1B0000
struct CompassCalibration {
int16_t off_x, off_y, off_z; // hard-iron offsets (raw ADC counts)
float scale_x, scale_y, scale_z; // soft-iron per-axis scale factors
uint32_t magic; // COMPASS_CAL_MAGIC when valid
};
class TechoCardBoard : public NRF52BoardDCDC {
private:
#if defined(HAS_RGB_LED)
Adafruit_NeoPixel _pixels = Adafruit_NeoPixel(NUM_NEOPIXELS, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800);
#endif
public:
TechoCardBoard() : NRF52Board("TECHO_CARD_OTA") {}
void begin();
uint16_t getBattMilliVolts() override {
int adcvalue = 0;
analogReadResolution(12);
analogReference(AR_INTERNAL_3_0);
pinMode(PIN_BAT_CTL, OUTPUT);
pinMode(PIN_VBAT_READ, INPUT);
digitalWrite(PIN_BAT_CTL, HIGH);
delay(10);
adcvalue = analogRead(PIN_VBAT_READ);
digitalWrite(PIN_BAT_CTL, LOW);
return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER);
}
const char* getManufacturerName() const override {
return "LilyGo T-Echo Card";
}
float getMCUTemperature() override;
void powerOff() override {
sd_power_system_off();
}
// GPS power control
void enableGPS(bool enable);
// Speaker power control
void enableSpeaker(bool enable);
// RGB LEDs -- all three to same colour
void setLED(uint8_t r, uint8_t g, uint8_t b);
void ledOff();
// Per-LED status control (0=power, 1=notify, 2=pairing)
void setStatusLED(uint8_t led_index, uint32_t color);
// Buzzer
void buzz(uint16_t freq_hz, uint16_t duration_ms);
// BQ25896 charger IC (0x6B)
bool probeCharger(); // check if BQ25896 responds on I2C
uint8_t readChargerReg(uint8_t reg);
void writeChargerReg(uint8_t reg, uint8_t val);
void enableChargerADC(); // start continuous ADC conversion
uint8_t getChargeStatus(); // 0=none, 1=pre, 2=fast, 3=done
uint16_t getChargerBattMV(); // battery voltage from charger ADC
uint8_t getChargerTSPCT(); // thermistor voltage as % of REGN
// ICM20948 / AK09916 compass (0x68 bypass to 0x0C)
bool initCompass();
bool readMag(int16_t& mx, int16_t& my, int16_t& mz);
void sleepCompass(); // power down magnetometer + put ICM20948 in sleep mode
// Compass calibration (persisted to InternalFS)
bool loadCalibration(); // call after InternalFS.begin()
bool saveCalibration(const CompassCalibration& cal);
bool isCalibrated() const { return _cal.magic == COMPASS_CAL_MAGIC; }
const CompassCalibration& getCalibration() const { return _cal; }
private:
bool _compassReady = false;
bool _chargerProbed = false;
bool _chargerPresent = false;
CompassCalibration _cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 };
};
@@ -0,0 +1,512 @@
// =============================================================================
// TechoCardHomeScreen -- 72x40 OLED home screen for LilyGo T-Echo Card
//
// Four-line layout using U8g2's 4x6 tom_thumb font (18 chars x 4 lines).
// U8g2's native SSD1306_72X40_ER support handles all GDDRAM offset mapping.
//
// Two-button navigation: A (pin 42) = next page / long-press activate
// C (pin 24) = previous page
//
// Pages: STATUS -> RADIO -> BLE -> ADVERT -> GPS -> COMPASS -> BATTERY -> HIBERNATE
// =============================================================================
#pragma once
#include <math.h>
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/sensors/LocationProvider.h>
#include <target.h>
#include "MyMesh.h"
#include "UITask.h"
class TechoCardHomeScreen : public UIScreen {
enum Page {
STATUS,
RADIO,
#ifdef BLE_PIN_CODE
BLE,
#endif
ADVERT,
#if ENV_INCLUDE_GPS == 1
GPS,
#endif
COMPASS,
BATTERY,
HIBERNATE,
PAGE_COUNT
};
UITask* _task;
mesh::RTCClock* _rtc;
NodePrefs* _prefs;
uint8_t _page;
bool _shutdown_init;
unsigned long _shutdown_at;
// Compass state
bool _compassInitDone;
bool _compassOK;
float _lastHeading;
int16_t _lastMx, _lastMy, _lastMz;
// Compass calibration state
bool _calMode;
unsigned long _calStart;
uint16_t _calCount;
int16_t _calMinX, _calMaxX;
int16_t _calMinY, _calMaxY;
int16_t _calMinZ, _calMaxZ;
// Diagnostic counters (temporary)
uint16_t _magOk;
uint16_t _magFail;
// Four lines at 9px spacing within 40px display.
// U8g2 handles panel offset natively -- y=0 is the true visible top.
static const int Y0 = 2;
static const int Y1 = 11;
static const int Y2 = 20;
static const int Y3 = 29;
int battPercent() {
uint16_t mv = _task->getBattMilliVolts();
if (mv == 0) return 0;
int pct = ((int)mv - 3000) * 100 / 1160;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
return pct;
}
const char* cardinal(float deg) {
if (deg >= 337.5f || deg < 22.5f) return "N";
if (deg < 67.5f) return "NE";
if (deg < 112.5f) return "E";
if (deg < 157.5f) return "SE";
if (deg < 202.5f) return "S";
if (deg < 247.5f) return "SW";
if (deg < 292.5f) return "W";
return "NW";
}
public:
TechoCardHomeScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs)
: _task(task), _rtc(rtc), _prefs(prefs),
_page(STATUS), _shutdown_init(false), _shutdown_at(0),
_compassInitDone(false), _compassOK(false),
_lastHeading(0), _lastMx(0), _lastMy(0), _lastMz(0),
_calMode(false), _calStart(0), _calCount(0),
_calMinX(0), _calMaxX(0),
_calMinY(0), _calMaxY(0),
_calMinZ(0), _calMaxZ(0),
_magOk(0), _magFail(0) {}
void cancelEditing() { _shutdown_init = false; }
int render(DisplayDriver& display) override {
char tmp[32];
display.setTextSize(1);
switch (_page) {
// ----- STATUS -----
case STATUS: {
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
char filtered_name[sizeof(_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _prefs->node_name,
sizeof(filtered_name));
display.print(filtered_name);
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
snprintf(tmp, sizeof(tmp), "MSG: %d", _task->getMsgCount());
display.print(tmp);
snprintf(tmp, sizeof(tmp), "%d%%", battPercent());
display.drawTextRightAlign(display.width() - 1, Y1, tmp);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
if (_task->hasConnection()) {
display.print("Connected");
} else if (_task->isSerialEnabled()) {
display.print("BLE: On");
} else {
display.print("BLE: Off");
}
break;
}
// ----- RADIO -----
case RADIO: {
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y0);
snprintf(tmp, sizeof(tmp), "%.1f MHz SF%d",
_prefs->freq, _prefs->sf);
display.print(tmp);
display.setCursor(0, Y1);
snprintf(tmp, sizeof(tmp), "BW %.0f CR %d",
_prefs->bw, _prefs->cr);
display.print(tmp);
display.setCursor(0, Y2);
snprintf(tmp, sizeof(tmp), "TX: %d dBm",
_prefs->tx_power_dbm);
display.print(tmp);
display.setCursor(0, Y3);
snprintf(tmp, sizeof(tmp), "NF: %d",
radio_driver.getNoiseFloor());
display.print(tmp);
break;
}
#ifdef BLE_PIN_CODE
// ----- BLE -----
case BLE: {
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
display.print(_task->isSerialEnabled() ? "BLE: ON" : "BLE: OFF");
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
snprintf(tmp, sizeof(tmp), "PIN: %lu",
(unsigned long)the_mesh.getBLEPin());
display.print(tmp);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y3);
display.print("Hold A: toggle");
break;
}
#endif
// ----- ADVERT -----
case ADVERT: {
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
display.print("Advert");
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
display.print("Hold A: send");
break;
}
#if ENV_INCLUDE_GPS == 1
// ----- GPS -----
case GPS: {
LocationProvider* loc = sensors.getLocationProvider();
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
if (!_prefs->gps_enabled) {
display.print("GPS: OFF");
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
display.print("Hold A: toggle");
break;
}
display.print("GPS: ON");
if (loc) {
snprintf(tmp, sizeof(tmp), "S: %d",
loc->satellitesCount());
display.drawTextRightAlign(display.width() - 1, Y0, tmp);
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
display.print(loc->isValid() ? "Fix: 3D" : "No fix");
if (loc->isValid()) {
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
snprintf(tmp, sizeof(tmp), "%.4f",
loc->getLatitude() / 1000000.0);
display.print(tmp);
display.setCursor(0, Y3);
snprintf(tmp, sizeof(tmp), "%.4f",
loc->getLongitude() / 1000000.0);
display.print(tmp);
} else {
// No fix yet -- show NMEA sentence rate to confirm the chip is talking.
// If this stays at 0, GPS is silent (baud rate wrong, RF off, etc).
display.setCursor(0, Y2);
snprintf(tmp, sizeof(tmp), "NMEA: %u/s",
(unsigned)gpsStream.getSentencesPerSec());
display.print(tmp);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y3);
display.print("Hold A: toggle");
}
}
break;
}
#endif
// ----- COMPASS -----
case COMPASS: {
if (!_compassInitDone) {
_compassOK = board.initCompass();
board.loadCalibration();
_compassInitDone = true;
}
// --- Calibration mode ---
if (_calMode) {
int16_t mx, my, mz;
if (_compassOK && board.readMag(mx, my, mz)) {
if (_calCount == 0) {
_calMinX = _calMaxX = mx;
_calMinY = _calMaxY = my;
_calMinZ = _calMaxZ = mz;
} else {
if (mx < _calMinX) _calMinX = mx;
if (mx > _calMaxX) _calMaxX = mx;
if (my < _calMinY) _calMinY = my;
if (my > _calMaxY) _calMaxY = my;
if (mz < _calMinZ) _calMinZ = mz;
if (mz > _calMaxZ) _calMaxZ = mz;
}
_calCount++;
}
int spreadX = _calMaxX - _calMinX;
int spreadY = _calMaxY - _calMinY;
int spreadZ = _calMaxZ - _calMinZ;
unsigned long elapsed = millis() - _calStart;
bool adequate = (spreadX >= 100 && spreadY >= 100 && _calCount >= 150);
bool timeout = (elapsed >= 30000);
if (adequate || (timeout && spreadX >= 50 && spreadY >= 50)) {
// Compute and save calibration
CompassCalibration cal;
cal.off_x = (_calMinX + _calMaxX) / 2;
cal.off_y = (_calMinY + _calMaxY) / 2;
cal.off_z = (_calMinZ + _calMaxZ) / 2;
float avgRange = ((float)spreadX + (float)spreadY) / 2.0f;
cal.scale_x = (spreadX > 0) ? avgRange / (float)spreadX : 1.0f;
cal.scale_y = (spreadY > 0) ? avgRange / (float)spreadY : 1.0f;
cal.scale_z = (spreadZ > 30) ? avgRange / (float)spreadZ : 1.0f;
cal.magic = COMPASS_CAL_MAGIC;
board.saveCalibration(cal);
_calMode = false;
_task->showAlert("Cal saved!", 800);
return 500;
}
if (timeout) {
_calMode = false;
_task->showAlert("Try again", 800);
return 500;
}
// Calibration progress display
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
display.print("Calibrate");
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
display.print("Rotate slowly...");
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
snprintf(tmp, sizeof(tmp), "Samples: %u", _calCount);
display.print(tmp);
display.setCursor(0, Y3);
snprintf(tmp, sizeof(tmp), "X:%d Y:%d", spreadX, spreadY);
display.print(tmp);
return 100; // fast sample collection
}
// --- Normal compass display ---
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
display.print("Compass");
if (board.isCalibrated()) {
display.drawTextRightAlign(display.width() - 1, Y0, "CAL");
}
if (!_compassOK) {
display.setColor(DisplayDriver::RED);
display.setCursor(0, Y2);
display.print("IMU not found");
break;
}
int16_t mx, my, mz;
if (board.readMag(mx, my, mz)) {
_magOk++;
// Exponential moving average: 7/8 old + 1/8 new (settles in ~2s)
if (_magOk == 1) {
_lastMx = mx; _lastMy = my; _lastMz = mz;
} else {
_lastMx = (_lastMx * 7 + mx + 4) >> 3;
_lastMy = (_lastMy * 7 + my + 4) >> 3;
_lastMz = (_lastMz * 7 + mz + 4) >> 3;
}
float cx = (float)_lastMx;
float cy = (float)_lastMy;
if (board.isCalibrated()) {
const CompassCalibration& cal = board.getCalibration();
cx = ((float)_lastMx - cal.off_x) * cal.scale_x;
cy = ((float)_lastMy - cal.off_y) * cal.scale_y;
}
// Y axis is inverted relative to compass convention on this PCB
_lastHeading = atan2f(-cy, cx) * 180.0f / (float)M_PI;
if (_lastHeading < 0) _lastHeading += 360.0f;
} else {
_magFail++;
}
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
snprintf(tmp, sizeof(tmp), "%.0f %s",
_lastHeading, cardinal(_lastHeading));
display.print(tmp);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
snprintf(tmp, sizeof(tmp), "X:%d Y:%d", _lastMx, _lastMy);
display.print(tmp);
display.setCursor(0, Y3);
snprintf(tmp, sizeof(tmp), "Z:%d", _lastMz);
display.print(tmp);
return 250; // smooth readable refresh
}
// ----- BATTERY -----
case BATTERY: {
display.setColor(DisplayDriver::GREEN);
display.setCursor(0, Y0);
display.print("Battery");
uint16_t mv = _task->getBattMilliVolts();
snprintf(tmp, sizeof(tmp), "%d%%", battPercent());
display.drawTextRightAlign(display.width() - 1, Y0, tmp);
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y1);
snprintf(tmp, sizeof(tmp), "%d.%02dV", mv / 1000, (mv % 1000) / 10);
display.print(tmp);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
{
float dieTemp = board.getMCUTemperature();
snprintf(tmp, sizeof(tmp), "Temp: %.0fC", dieTemp);
display.print(tmp);
}
break;
}
// ----- HIBERNATE -----
case HIBERNATE: {
if (_shutdown_init) {
display.setColor(DisplayDriver::RED);
display.setCursor(0, Y1);
display.print("Shutting down...");
return 200;
}
display.setColor(DisplayDriver::YELLOW);
display.setCursor(0, Y0);
display.print("Hibernate");
display.setColor(DisplayDriver::LIGHT);
display.setCursor(0, Y2);
display.print("Hold A: sleep");
break;
}
} // switch
return 5000;
}
bool handleInput(char c) override {
if (_shutdown_init) {
_shutdown_init = false;
return true;
}
// Any input during calibration cancels it
if (_calMode) {
_calMode = false;
_task->showAlert("Cancelled", 500);
return true;
}
if (c == KEY_NEXT || c == 'd') {
_page = (_page + 1) % PAGE_COUNT;
return true;
}
if (c == KEY_PREV || c == 'a') {
_page = (_page + PAGE_COUNT - 1) % PAGE_COUNT;
return true;
}
if (c == KEY_ENTER) {
switch (_page) {
#ifdef BLE_PIN_CODE
case BLE:
if (_task->isSerialEnabled()) {
_task->disableSerial();
_task->showAlert("BLE Off", 800);
} else {
_task->enableSerial();
_task->showAlert("BLE On", 800);
}
return true;
#endif
case ADVERT:
_task->notify(UIEventType::ack);
if (the_mesh.advert()) {
_task->showAlert("Sent!", 800);
} else {
_task->showAlert("Failed", 800);
}
return true;
#if ENV_INCLUDE_GPS == 1
case GPS:
_task->toggleGPS();
return true;
#endif
case COMPASS:
if (!_compassOK) return false;
_calMode = true;
_calStart = millis();
_calCount = 0;
return true;
case HIBERNATE:
_shutdown_init = true;
_shutdown_at = millis() + 500;
return true;
default:
return false;
}
}
return false;
}
void poll() override {
if (_shutdown_init && millis() >= _shutdown_at) {
if (!_task->isButtonPressed()) {
_task->shutdown();
}
}
}
};
+140
View File
@@ -0,0 +1,140 @@
; =============================================================================
; LilyGo T-Echo Card -- nRF52840 + SX1262 + SSD1306 OLED (72x40) + L76K GPS
; =============================================================================
[lilygo_techo_card]
extends = nrf52_base
board = lilygo_techo_card
platform_packages = framework-arduinoadafruitnrf52
board_build.ldscript = boards/nrf52840_s140_v6.ld
; Point FrameworkArduinoVariant at a directory containing ONLY variant.h/cpp.
; Without this, PlatformIO tries to compile TechoCardBoard.cpp and target.cpp
; as part of the framework variant, which fails because MeshCore.h and
; RadioLib.h aren't on the BSP include path.
board_build.variants_dir = variants_bsp
build_flags = ${nrf52_base.build_flags}
-I src/helpers/nrf52
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/lilygo_techo_card
-D LILYGO_TECHO_CARD
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D USE_U8G2_DISPLAY
-D DISPLAY_CLASS=U8g2Display
-D PIN_BUZZER=77
-D PIN_BOOT_BTN=24
-D ENV_INCLUDE_GPS=1
-D ENV_SKIP_GPS_DETECT
-D DISABLE_DIAGNOSTIC_OUTPUT
-D AUTO_SHUTDOWN_MILLIVOLTS=2980
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/ui/buzzer.cpp>
+<../variants/lilygo_techo_card>
lib_deps =
${nrf52_base.lib_deps}
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit NeoPixel @ ^1.12.3
adafruit/Adafruit SSD1306 @ ^2.5.12
adafruit/Adafruit GFX Library @ ^1.11.11
adafruit/Adafruit BusIO @ ^1.16.2
end2endzone/NonBlockingRtttl @ ^1.3.0
olikraus/U8g2 @ ^2.35.19
debug_tool = jlink
upload_protocol = nrfutil
[env:techo_card_companion_radio_ble]
extends = lilygo_techo_card
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_card.build_flags}
-I examples/companion_radio
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=500
-D MAX_GROUP_CHANNELS=8
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=64
-D AUTO_OFF_MILLIS=60000
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_card.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:techo_card_companion_radio_usb]
extends = lilygo_techo_card
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags =
${lilygo_techo_card.build_flags}
-I examples/companion_radio
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D AUTO_OFF_MILLIS=0
; -D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<helpers/nrf52/*.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${lilygo_techo_card.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:techo_card_repeater]
extends = lilygo_techo_card
build_flags =
${lilygo_techo_card.build_flags}
-D ADVERT_NAME='"T-Echo Card Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_repeater>
[env:techo_card_room_server]
extends = lilygo_techo_card
build_flags =
${lilygo_techo_card.build_flags}
-D ADVERT_NAME='"T-Echo Card Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_room_server>
[env:techo_card_sensor]
extends = lilygo_techo_card
build_flags =
${lilygo_techo_card.build_flags}
-D ADVERT_NAME='"T-Echo Card Sensor"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_sensor>
+52
View File
@@ -0,0 +1,52 @@
#include <Arduino.h>
#include "target.h"
#include <helpers/ArduinoHelpers.h>
#include <helpers/sensors/MicroNMEALocationProvider.h>
TechoCardBoard board;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
WRAPPER_CLASS radio_driver(radio, board);
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
GPSStreamCounter gpsStream(Serial1);
MicroNMEALocationProvider gps(gpsStream, &rtc_clock);
EnvironmentSensorManager sensors(gps);
#else
EnvironmentSensorManager sensors;
#endif
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#endif
bool radio_init() {
// board.begin() and display.begin() are called by main.cpp before this.
// radio_init() should ONLY initialise the radio -- matching Meshpocket pattern.
return radio.std_init(&SPI);
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#include <helpers/sensors/LocationProvider.h>
#include "TechoCardBoard.h"
#if ENV_INCLUDE_GPS
#include "GPSStreamCounter.h"
#endif
#ifdef DISPLAY_CLASS
#if defined(USE_U8G2_DISPLAY)
#include <helpers/ui/U8g2Display.h>
#else
#include <helpers/ui/SSD1306Display.h>
#endif
#include <helpers/ui/MomentaryButton.h>
#endif
extern TechoCardBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
extern EnvironmentSensorManager sensors;
#if ENV_INCLUDE_GPS
extern GPSStreamCounter gpsStream;
#endif
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
+11
View File
@@ -0,0 +1,11 @@
#include "variant.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 32.768 kHz crystal (LFXO)
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};
+202
View File
@@ -0,0 +1,202 @@
/*
* variant.h -- LilyGo T-Echo Card pin definitions
*
* nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72x40) + L76K GPS
* + MAX98357 Speaker + MP34DT05 PDM Mic + ICM20948 IMU + BQ25896 + Solar
*
* Cross-referenced against:
* - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h
* - Meshtastic PR #10267 (caveman99)
*/
#pragma once
#include "WVariant.h"
////////////////////////////////////////////////////////////////////////////////
// Low frequency clock source
#define USE_LFXO // 32.768 kHz crystal
#define VARIANT_MCK (64000000ul)
////////////////////////////////////////////////////////////////////////////////
// Power / Battery
#define PIN_VBAT_READ 2 // (0, 2) = AIN0
#define BATTERY_ADC_AIN 0 // nRF SAADC AIN channel number
// Gated voltage divider: drive HIGH before ADC read, LOW after
#define PIN_BAT_CTL 31 // (0, 31)
#define ADC_MULTIPLIER (2.0F)
#define MV_LSB (3000.0F / 4096.0F)
#define ADC_RESOLUTION (14)
#define BATTERY_SENSE_RES (12)
#define AREF_VOLTAGE (3.0)
////////////////////////////////////////////////////////////////////////////////
// Pin counts
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
////////////////////////////////////////////////////////////////////////////////
// UART -- GPS (L76K)
#define PIN_SERIAL1_RX 19 // (0, 19) -- GPS TX -> nRF RX
#define PIN_SERIAL1_TX 21 // (0, 21) -- nRF TX -> GPS RX
////////////////////////////////////////////////////////////////////////////////
// I2C (shared: OLED, IMU ICM20948)
#define WIRE_INTERFACES_COUNT (1)
#define PIN_WIRE_SDA 36 // (1, 4)
#define PIN_WIRE_SCL 34 // (1, 2)
////////////////////////////////////////////////////////////////////////////////
// LEDs -- WS2812 addressable (no plain GPIO LED)
// The BSP drives LED_BUILTIN via digitalWrite for BLE status -- if pointed at
// the WS2812 data pin (39), it holds the line HIGH and all LEDs glow green.
// Point at an unused GPIO (46 = P1.14) so the BSP toggles harmlessly.
#define LED_BUILTIN 46 // Unused GPIO -- keeps BSP happy
#define PIN_LED LED_BUILTIN
#define LED_RED LED_BUILTIN
#define LED_BLUE (-1) // Prevents Bluefruit flashing during advertising
#define PIN_STATUS_LED LED_BUILTIN
#define LED_STATE_ON 1
// WS2812 RGB LEDs -- 3 LEDs daisy-chained on a single data line (pin 39)
// Hardware verified: all three light when pin 39 is driven HIGH.
// Meshtastic PR #10267 mapped them as separate GPIOs (39, 44, 28) but
// testing confirms they're chained.
#define HAS_RGB_LED 1
#define PIN_RGB_LED_1 39 // (1, 7) -- chain data in
#define PIN_NEOPIXEL PIN_RGB_LED_1
#define NUM_NEOPIXELS 3
////////////////////////////////////////////////////////////////////////////////
// Buttons
#define PIN_BUTTON1 42 // (1, 10) -- orange front button
#define BUTTON_PIN PIN_BUTTON1
#define PIN_USER_BTN BUTTON_PIN
// Boot button: P0.24 (hardware only, used for DFU)
////////////////////////////////////////////////////////////////////////////////
// SPI -- LoRa
#define SPI_INTERFACES_COUNT (1)
#define PIN_SPI_MISO 17 // (0, 17)
#define PIN_SPI_MOSI 15 // (0, 15)
#define PIN_SPI_SCK 13 // (0, 13)
////////////////////////////////////////////////////////////////////////////////
// SX1262 LoRa Radio (HPB16B3 / S62F module)
#define USE_SX1262
#define SX126X_CS 11 // (0, 11)
#define SX126X_DIO1 40 // (1, 8)
#define SX126X_BUSY 14 // (0, 14)
#define SX126X_RESET 7 // (0, 7)
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE 1.8
#define P_LORA_NSS SX126X_CS
#define P_LORA_DIO_1 SX126X_DIO1
#define P_LORA_RESET SX126X_RESET
#define P_LORA_BUSY SX126X_BUSY
#define P_LORA_SCLK PIN_SPI_SCK
#define P_LORA_MISO PIN_SPI_MISO
#define P_LORA_MOSI PIN_SPI_MOSI
// RF switch control lines (may be needed in addition to DIO2)
#define LORA_RF_VC1 27 // (0, 27)
#define LORA_RF_VC2 33 // (1, 1)
////////////////////////////////////////////////////////////////////////////////
// OLED Display -- SSD1315 (SSD1306-compatible), 72x40, I2C
//
// Physical panel is 72x40 within 128x64 GDDRAM.
// Visible window: columns 2899, pages 37 (rows 2463).
// SETDISPLAYOFFSET = 24 maps page 0 writes to the visible area.
#define HAS_OLED 1
#define OLED_I2C_ADDR 0x3C
#define OLED_WIDTH 72
#define OLED_HEIGHT 40
#define OLED_DISPLAY_OFFSET 24
// RT9080 enable -- controls 3V3 rail (OLED, GPS, LoRa, sensors)
#define PIN_OLED_EN 30 // (0, 30)
#define PIN_OLED_RESET (-1)
////////////////////////////////////////////////////////////////////////////////
// GPS -- L76K Multi-GNSS
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
#define PIN_GPS_TX 21 // nRF TX -> GPS RX (vendor GPS_UART_RX / P0.21)
#define PIN_GPS_RX 19 // nRF RX <- GPS TX (vendor GPS_UART_TX / P0.19)
#define PIN_GPS_EN 47 // (1, 15)
#define PIN_GPS_WAKEUP 25 // (0, 25)
#define PIN_GPS_1PPS 23 // (0, 23)
#define PIN_GPS_RF_EN 29 // (0, 29)
////////////////////////////////////////////////////////////////////////////////
// Speaker -- MAX98357 I2S Class-D Mono Amp
#define HAS_SPEAKER 1
#define PIN_SPK_EN 43 // (1, 11)
#define PIN_SPK_EN2 3 // (0, 3)
#define PIN_SPK_BCLK 16 // (0, 16)
#define PIN_SPK_DATA 20 // (0, 20)
#define PIN_SPK_LRCK 22 // (0, 22)
////////////////////////////////////////////////////////////////////////////////
// Microphone -- MP34DT05 Digital MEMS PDM
#define HAS_MICROPHONE 1
#define PIN_MIC_CLK 35 // (1, 3)
#define PIN_MIC_DATA 37 // (1, 5)
////////////////////////////////////////////////////////////////////////////////
// Buzzer
#ifndef HAS_BUZZER
#define HAS_BUZZER 1
#endif
#ifndef PIN_BUZZER
#define PIN_BUZZER 38 // (1, 6)
#endif
////////////////////////////////////////////////////////////////////////////////
// IMU -- ICM20948
#define HAS_IMU 1
#define IMU_I2C_ADDR 0x68
////////////////////////////////////////////////////////////////////////////////
// NFC -- nRF52840 NFC-A (dedicated P0.09/P0.10)
#define HAS_NFC 1
////////////////////////////////////////////////////////////////////////////////
// External Flash -- ZD25WQ32CEIGR 4MB QSPI
#define HAS_EXT_FLASH 1
#define PIN_QSPI_SCK 4 // (0, 4)
#define PIN_QSPI_CS 12 // (0, 12)
#define PIN_QSPI_IO0 6 // (0, 6)
#define PIN_QSPI_IO1 8 // (0, 8)
#define PIN_QSPI_IO2 41 // (1, 9)
#define PIN_QSPI_IO3 26 // (0, 26)
////////////////////////////////////////////////////////////////////////////////
// No dedicated RTC chip -- time from GPS or BLE companion sync
#define HAS_RTC 0
@@ -1,261 +0,0 @@
// =============================================================================
// TechoCardBoard — Implementation for LilyGo T-Echo Card
//
// Patches applied from Meshtastic PR #10267 (caveman99):
// 1. RT9080 power rail reset cycle in begin() — prevents LoRa TX brown-out
// 2. Battery measurement control pin (P0.31) — enables voltage divider
// 3. WS2812 NeoPixel implementation via Adafruit_NeoPixel
// =============================================================================
#include "TechoCardBoard.h"
#include "variant.h"
// nRF52840 SAADC includes
#include "nrf.h"
void TechoCardBoard::begin() {
NRF52BoardDCDC::begin();
// -------------------------------------------------------------------------
// RT9080 3V3 rail: clean reset cycle
//
// From Meshtastic PR #10267 (earlyInitVariant): if the nRF52840 was in a
// half-enabled state from a previous soft reset, the RT9080 LDO can be in
// an indeterminate state. Toggling EN HIGH→LOW→HIGH with 100ms dwell
// forces a clean power-on. Without this, the 3V3 rail can brown-out when
// LoRa TX fires at full power (+22 dBm).
// -------------------------------------------------------------------------
#if PIN_OLED_EN >= 0
pinMode(PIN_OLED_EN, OUTPUT);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
digitalWrite(PIN_OLED_EN, LOW);
delay(100);
digitalWrite(PIN_OLED_EN, HIGH);
delay(100);
#endif
// -------------------------------------------------------------------------
// Park peripheral enable pins LOW before the rest of setup runs.
// Prevents peripherals from sinking current while the 3V3 rail is ramping.
// (Adapted from Meshtastic PR #10267 earlyInitVariant)
// -------------------------------------------------------------------------
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, LOW);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
pinMode(PIN_GPS_RF_EN, OUTPUT);
digitalWrite(PIN_GPS_RF_EN, LOW);
#endif
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW);
#endif
// Configure battery measurement control pin
#if defined(BATTERY_MEASUREMENT_CONTROL)
pinMode(BATTERY_MEASUREMENT_CONTROL, OUTPUT);
digitalWrite(BATTERY_MEASUREMENT_CONTROL, !BATTERY_MEASUREMENT_ACTIVE);
#endif
// Configure battery ADC pin as analog input
pinMode(PIN_VBAT_READ, INPUT);
// Configure button(s)
pinMode(PIN_BUTTON_A, INPUT_PULLUP);
pinMode(PIN_BUTTON_BOOT, INPUT_PULLUP);
// Initialise WS2812 NeoPixels (3 independent LEDs, all off at boot)
#if defined(HAS_RGB_LED)
_pixel_power.begin();
_pixel_power.clear();
_pixel_power.show();
_pixel_notify.begin();
_pixel_notify.clear();
_pixel_notify.show();
_pixel_pairing.begin();
_pixel_pairing.clear();
_pixel_pairing.show();
#endif
}
// -----------------------------------------------------------------------------
// Battery voltage reading via nRF52840 SAADC
//
// The T-Echo Card has a gated voltage divider on AIN0 (P0.02).
// BATTERY_MEASUREMENT_CONTROL (P0.31) must be driven HIGH to enable the
// divider before reading, and LOW after to avoid parasitic drain.
//
// nRF52840 SAADC: 12-bit, internal 0.6V reference, 1/6 gain.
// With 1/6 gain: input range 03.6V. Multiply by divider ratio (ADC_MULTIPLIER).
// -----------------------------------------------------------------------------
uint16_t TechoCardBoard::getBattMilliVolts() {
uint32_t now = millis();
// Cache battery reading — only read every 10 seconds
if (_cached_battery_mv > 0 && (now - _last_battery_read) < 10000) {
return _cached_battery_mv;
}
// Enable battery voltage divider
#if defined(BATTERY_MEASUREMENT_CONTROL)
digitalWrite(BATTERY_MEASUREMENT_CONTROL, BATTERY_MEASUREMENT_ACTIVE);
delay(5); // Allow divider to settle
#endif
// Configure SAADC for single-shot reading
NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit;
// Channel 0: AIN0 (P0.02), 1/6 gain, internal 0.6V reference
// Effective range: 0 3.6V
NRF_SAADC->CH[0].PSELP = SAADC_CH_PSELP_PSELP_AnalogInput0; // AIN0
NRF_SAADC->CH[0].PSELN = SAADC_CH_PSELN_PSELN_NC; // Single-ended
NRF_SAADC->CH[0].CONFIG =
(SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos) |
(SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos) |
(SAADC_CH_CONFIG_TACQ_40us << SAADC_CH_CONFIG_TACQ_Pos) |
(SAADC_CH_CONFIG_MODE_SE << SAADC_CH_CONFIG_MODE_Pos) |
(SAADC_CH_CONFIG_BURST_Disabled << SAADC_CH_CONFIG_BURST_Pos) |
(SAADC_CH_CONFIG_RESP_Bypass << SAADC_CH_CONFIG_RESP_Pos) |
(SAADC_CH_CONFIG_RESN_Bypass << SAADC_CH_CONFIG_RESN_Pos);
// Set up result buffer
volatile int16_t result = 0;
NRF_SAADC->RESULT.PTR = (uint32_t)&result;
NRF_SAADC->RESULT.MAXCNT = 1;
// Enable, calibrate on first use
NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled;
// Start and wait for sample
NRF_SAADC->EVENTS_END = 0;
NRF_SAADC->TASKS_START = 1;
while (!NRF_SAADC->EVENTS_STARTED);
NRF_SAADC->EVENTS_STARTED = 0;
NRF_SAADC->TASKS_SAMPLE = 1;
while (!NRF_SAADC->EVENTS_END);
NRF_SAADC->EVENTS_END = 0;
NRF_SAADC->TASKS_STOP = 1;
while (!NRF_SAADC->EVENTS_STOPPED);
NRF_SAADC->EVENTS_STOPPED = 0;
// Disable SAADC to save power
NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Disabled;
// Disable battery voltage divider to save power
#if defined(BATTERY_MEASUREMENT_CONTROL)
digitalWrite(BATTERY_MEASUREMENT_CONTROL, !BATTERY_MEASUREMENT_ACTIVE);
#endif
// Convert: voltage = (result / 4096) * 3.6V * ADC_MULTIPLIER * 1000 (mV)
if (result < 0) result = 0;
float voltage_mv = ((float)result / 4096.0f) * 3600.0f * _adc_multiplier;
_cached_battery_mv = voltage_mv;
_last_battery_read = now;
return (uint16_t)voltage_mv;
}
uint8_t TechoCardBoard::getBatteryPercent() {
uint16_t mv = getBattMilliVolts();
if (mv == 0) return 0;
// Simple linear approximation for single-cell LiPo
// 3200 mV = 0%, 4200 mV = 100%
if (mv >= 4200) return 100;
if (mv <= 3200) return 0;
return (uint8_t)(((uint32_t)(mv - 3200) * 100) / 1000);
}
// -----------------------------------------------------------------------------
// GPS power control
// -----------------------------------------------------------------------------
void TechoCardBoard::enableGPS(bool enable) {
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
digitalWrite(PIN_GPS_EN, enable ? HIGH : LOW);
#endif
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
digitalWrite(PIN_GPS_RF_EN, enable ? HIGH : LOW);
#endif
}
// -----------------------------------------------------------------------------
// Speaker power control
// -----------------------------------------------------------------------------
void TechoCardBoard::enableSpeaker(bool enable) {
#if defined(HAS_SPEAKER)
digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW);
#if PIN_SPK_EN2 >= 0
digitalWrite(PIN_SPK_EN2, enable ? HIGH : LOW);
#endif
#endif
}
// -----------------------------------------------------------------------------
// RGB LEDs — WS2812 via Adafruit_NeoPixel
//
// Three independent WS2812s on separate data pins (not a chain).
// Each is a 1-pixel NeoPixel strand.
//
// setLED() drives all three to the same colour (legacy interface).
// For per-LED control, use setStatusLED() with a role index.
// -----------------------------------------------------------------------------
void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) {
#if defined(HAS_RGB_LED)
uint32_t color = Adafruit_NeoPixel::Color(r, g, b);
_pixel_power.setPixelColor(0, color);
_pixel_power.show();
_pixel_notify.setPixelColor(0, color);
_pixel_notify.show();
_pixel_pairing.setPixelColor(0, color);
_pixel_pairing.show();
#else
(void)r; (void)g; (void)b;
#endif
}
void TechoCardBoard::ledOff() {
setLED(0, 0, 0);
}
void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) {
#if defined(HAS_RGB_LED)
switch (led_index) {
case 0: // Power / charge
_pixel_power.setPixelColor(0, color);
_pixel_power.show();
break;
case 1: // Notification / mesh activity
_pixel_notify.setPixelColor(0, color);
_pixel_notify.show();
break;
case 2: // BLE pairing
_pixel_pairing.setPixelColor(0, color);
_pixel_pairing.show();
break;
}
#else
(void)led_index; (void)color;
#endif
}
// -----------------------------------------------------------------------------
// Buzzer — PWM tone generation
// -----------------------------------------------------------------------------
void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) {
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
if (freq_hz == 0 || duration_ms == 0) {
noTone(PIN_BUZZER);
return;
}
tone(PIN_BUZZER, freq_hz, duration_ms);
#else
(void)freq_hz; (void)duration_ms;
#endif
}
@@ -1,88 +0,0 @@
#pragma once
// =============================================================================
// TechoCardBoard — Board class for LilyGo T-Echo Card
//
// Extends NRF52BoardDCDC with:
// - Battery ADC (AIN0, P0.02) with gated voltage divider (P0.31)
// - Solar charging via BQ25896
// - GPS power control (L76K)
// - Speaker/mic enable
// - WS2812 RGB LEDs (3 independent, via Adafruit_NeoPixel)
// - Buzzer
// - NFC NDEF contact sharing
// =============================================================================
#include <Arduino.h>
#include <helpers/NRF52Board.h>
#include "variant.h"
// WS2812 NeoPixel support — 3 independent LEDs on separate GPIOs
#if defined(HAS_RGB_LED)
#include <Adafruit_NeoPixel.h>
#endif
#ifdef NRF52_POWER_MANAGEMENT
// Power management config for T-Echo Card
// AIN0 (P0.02) for battery voltage sensing
// REFSEL=4 → 5/8 VDD ≈ 2.0625V threshold (with 2:1 divider → ~4.125V cell)
static const PowerMgtConfig TECHO_CARD_POWER_CONFIG = {
.lpcomp_ain_channel = BATTERY_ADC_AIN, // AIN0
.lpcomp_refsel = 4, // 5/8 VDD
.voltage_bootlock = 3100, // Don't boot below 3.1V
};
#endif
class TechoCardBoard : public NRF52BoardDCDC {
private:
float _adc_multiplier;
uint32_t _last_battery_read;
float _cached_battery_mv;
// Three independent WS2812 NeoPixels (1 LED per strand)
#if defined(HAS_RGB_LED)
Adafruit_NeoPixel _pixel_power = Adafruit_NeoPixel(1, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel _pixel_notify = Adafruit_NeoPixel(1, PIN_RGB_LED_2, NEO_GRB + NEO_KHZ800);
Adafruit_NeoPixel _pixel_pairing = Adafruit_NeoPixel(1, PIN_RGB_LED_3, NEO_GRB + NEO_KHZ800);
#endif
public:
TechoCardBoard()
: _adc_multiplier(ADC_MULTIPLIER),
_last_battery_read(0),
_cached_battery_mv(0) {}
void begin() override;
// Battery
uint16_t getBattMilliVolts() override;
uint8_t getBatteryPercent();
float getAdcMultiplier() const override { return _adc_multiplier; }
bool setAdcMultiplier(float mult) override { _adc_multiplier = mult; return true; }
// Board identity
const char* getManufacturerName() const override { return "LilyGo T-Echo Card"; }
// GPS power control
void enableGPS(bool enable);
// Speaker power control
void enableSpeaker(bool enable);
// RGB LEDs — all three to same colour (legacy interface)
void setLED(uint8_t r, uint8_t g, uint8_t b);
void ledOff();
// Per-LED status control (0=power, 1=notify, 2=pairing)
// colour is packed 0xRRGGBB — use NEOPIXEL_COLOR_* defines from variant.h
void setStatusLED(uint8_t led_index, uint32_t color);
// Buzzer
void buzz(uint16_t freq_hz, uint16_t duration_ms);
#ifdef NRF52_POWER_MANAGEMENT
const PowerMgtConfig* getPowerConfig() const {
return &TECHO_CARD_POWER_CONFIG;
}
#endif
};
@@ -1,140 +0,0 @@
; =============================================================================
; LilyGo T-Echo Card — Meck variant configuration
;
; nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72×40) + L76K GPS
; + MAX98357 Speaker + MP34DT05 PDM Mic + ICM20948 IMU + Solar + NFC
;
; Platform: nRF52 (Adafruit nRF52 Arduino)
; =============================================================================
; --- Base configuration for all T-Echo Card builds ---
[lilygo_techo_card]
extends = nrf52_base
platform = https://github.com/maxgerhardt/platform-nordicnrf52.git
board = lilygo_techo_card
board_check = false
extra_scripts =
create-uf2.py
build_flags = ${nrf52_base.build_flags}
-I variants/lilygo_techo_card
-I src/helpers/ui
-D LILYGO_TECHO_CARD
-D NRF52_POWER_MANAGEMENT
-D WIRE_INTERFACES_COUNT=1
-D SPI_INTERFACES_COUNT=1
-D ps_calloc=calloc
; I2C
-D PIN_BOARD_SDA=36
-D PIN_BOARD_SCL=34
-D PIN_WIRE_SDA=36
-D PIN_WIRE_SCL=34
-D PIN_SPI_MISO=17
-D PIN_SPI_SCK=13
-D PIN_SPI_MOSI=15
-D USE_LFXO
-D LED_BLUE=39
-D LED_BUILTIN=39
-D LED_STATE_ON=1
-D PINS_COUNT=48
-D NUM_DIGITAL_PINS=48
-D NUM_ANALOG_INPUTS=6
-D PIN_SERIAL1_RX=21
-D PIN_SERIAL1_TX=19
; LoRa SX1262 (HPB16B3 module)
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_NSS=11
-D P_LORA_DIO_1=40
-D P_LORA_RESET=7
-D P_LORA_BUSY=14
-D P_LORA_SCLK=13
-D P_LORA_MISO=17
-D P_LORA_MOSI=15
-D LORA_TX_POWER=22
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D SX126X_DIO2_AS_RF_SWITCH=1
; Display — not defined in base; added per-env when needed
; (companion BLE skips display — phone app is the UI)
-D PIN_OLED_RESET=-1
; GPS — L76K
-D HAS_GPS=1
-D PIN_GPS_TX=19
-D PIN_GPS_RX=21
-D PIN_GPS_EN=47
-D GPS_EN_ACTIVE=HIGH
-D GPS_BAUDRATE=9600
-D ENV_INCLUDE_GPS=1
; Battery ADC
-D PIN_VBAT_READ=2
; User button
-D PIN_USER_BTN=42
; Board class
-D BOARD_CLASS=TechoCardBoard
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/lilygo_techo_card/*.cpp>
+<helpers/sensors>
lib_deps = ${nrf52_base.lib_deps}
olikraus/U8g2 @ ^2.35.19
stevemarple/MicroNMEA @ ^2.0.6
adafruit/Adafruit NeoPixel @ ^1.12.3
end2endzone/NonBlockingRtttl @ ^1.3.0
; =============================================================================
; Build Environments
; =============================================================================
; --- BLE Companion Radio ---
; Pairs with MeshCore companion app (Android/iOS/Web) over Bluetooth.
; No DISPLAY_CLASS — phone app is the primary interface.
; OLED status screen will be added as a follow-up.
[env:meck_techo_card_companion_radio_ble]
extends = lilygo_techo_card
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_flags = ${lilygo_techo_card.build_flags}
-I examples/companion_radio
-D FIRMWARE_NAME='"Meck T-Echo Card BLE"'
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D OFFLINE_QUEUE_SIZE=256
; Debug (disable for release)
; -D MESH_DEBUG
; -D BLE_DEBUG_LOGGING
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
lib_deps = ${lilygo_techo_card.lib_deps}
densaugeo/base64 @ ~1.4.0
; --- Repeater ---
; Standalone LoRa repeater node. GPS for position adverts.
; OLED shows status. Solar + 800mAh battery for outdoor deployment.
[env:meck_techo_card_repeater]
extends = lilygo_techo_card
build_flags = ${lilygo_techo_card.build_flags}
-D FIRMWARE_NAME='"Meck T-Echo Card Repeater"'
; -D MESH_DEBUG
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_repeater/*.cpp>
; --- Room Server ---
; BBS-style message board node.
[env:meck_techo_card_room_server]
extends = lilygo_techo_card
build_flags = ${lilygo_techo_card.build_flags}
-D FIRMWARE_NAME='"Meck T-Echo Card Room"'
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_room_server/*.cpp>
; --- Sensor Node ---
; Telemetry node with GPS position reporting.
; IMU data (ICM20948) can be added as a custom sensor source.
[env:meck_techo_card_sensor]
extends = lilygo_techo_card
build_flags = ${lilygo_techo_card.build_flags}
-D FIRMWARE_NAME='"Meck T-Echo Card Sensor"'
build_src_filter = ${lilygo_techo_card.build_src_filter}
+<../examples/simple_sensor/*.cpp>
-127
View File
@@ -1,127 +0,0 @@
// =============================================================================
// MeshCore target implementation for LilyGo T-Echo Card
//
// nRF52840 + SX1262 (HPB16B3 / S62F module) + SSD1315 OLED + L76K GPS
// =============================================================================
#include <Arduino.h>
#include "target.h"
#include "variant.h"
// --- Board ---
TechoCardBoard board;
// --- Clock ---
// No hardware RTC on T-Echo Card — VolatileRTCClock tracks time via millis().
// Time gets set from GPS lock or BLE companion app sync.
// AutoDiscoverRTCClock probes I2C for hardware RTCs; if none found, uses fallback.
VolatileRTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
// --- Radio ---
// nRF52 Adafruit BSP SPIClass requires (peripheral, MISO, SCK, MOSI)
#if defined(P_LORA_SCLK)
static SPIClass spi(NRF_SPIM3, P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
#else
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
#endif
WRAPPER_CLASS radio_driver(radio, board);
// --- Display ---
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
#endif
// --- Sensor manager ---
#if ENV_INCLUDE_GPS
#include <helpers/sensors/MicroNMEALocationProvider.h>
static MicroNMEALocationProvider gps_provider(Serial1);
EnvironmentSensorManager sensors(gps_provider);
#else
EnvironmentSensorManager sensors;
#endif
// --- Target initialization ---
bool radio_init() {
// Board-level init — cycles RT9080 3V3 rail, parks peripheral pins LOW
board.begin();
// Enable GPS power (was parked LOW in board.begin())
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
digitalWrite(PIN_GPS_EN, HIGH);
delay(10);
#endif
// GPS RF/LNA enable
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
digitalWrite(PIN_GPS_RF_EN, HIGH);
#endif
// Initialise GPS UART
#if defined(HAS_GPS)
Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX);
Serial1.begin(GPS_BAUDRATE);
#endif
// Speaker off by default
#if defined(HAS_SPEAKER)
pinMode(PIN_SPK_EN, OUTPUT);
digitalWrite(PIN_SPK_EN, LOW);
#if PIN_SPK_EN2 >= 0
pinMode(PIN_SPK_EN2, OUTPUT);
digitalWrite(PIN_SPK_EN2, LOW);
#endif
#endif
// Initialise I2C
Wire.begin();
Wire.setClock(400000);
// Initialise clocks — probe I2C for hardware RTCs, fall back to VolatileRTCClock
rtc_clock.begin(Wire);
// Initialise display
#ifdef DISPLAY_CLASS
display.begin();
#endif
// SX1262 DIO2 as RF switch control
radio.setDio2AsRfSwitch(true);
// SX1262 TCXO via DIO3 (1.8V, from Meshtastic PR #10267)
// TODO: Verify on hardware — if module uses crystal, remove this call
radio.setTCXO(1.8f);
// Initialise radio
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}
uint32_t radio_get_rng_seed() {
return radio.random(0x7FFFFFFF);
}
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
radio.setFrequency(freq);
radio.setSpreadingFactor(sf);
radio.setBandwidth(bw);
radio.setCodingRate(cr);
}
void radio_set_tx_power(int8_t dbm) {
radio.setOutputPower(dbm);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng);
}
void radio_reset_agc() {
radio.setRxBoostedGainMode(true);
}
-43
View File
@@ -1,43 +0,0 @@
#pragma once
// =============================================================================
// MeshCore target declarations for LilyGo T-Echo Card
// =============================================================================
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <RadioLib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/NRF52Board.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#endif
#include <helpers/sensors/EnvironmentSensorManager.h>
#include "TechoCardBoard.h"
// Hardware object declarations (instantiated in target.cpp)
extern RADIO_CLASS radio;
extern WRAPPER_CLASS radio_driver;
extern TechoCardBoard board;
extern AutoDiscoverRTCClock rtc_clock;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
#endif
extern EnvironmentSensorManager sensors;
// Target functions — called from main.cpp and MyMesh.cpp
bool radio_init();
uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity();
void radio_reset_agc();
@@ -1,18 +0,0 @@
// =============================================================================
// g_ADigitalPinMap — nRF52840 Arduino pin to GPIO mapping
//
// Required by the Adafruit nRF52 BSP (SPI.cpp, Wire, NeoPixel, etc.).
// On nRF52840, Arduino pin numbers map 1:1 to nRF GPIO numbers.
// 48 entries: P0.00P0.31 (031) + P1.00P1.15 (3247)
// =============================================================================
#include <Arduino.h>
const uint32_t g_ADigitalPinMap[] = {
0, 1, 2, 3, 4, 5, 6, 7, // P0.00 P0.07
8, 9, 10, 11, 12, 13, 14, 15, // P0.08 P0.15
16, 17, 18, 19, 20, 21, 22, 23, // P0.16 P0.23
24, 25, 26, 27, 28, 29, 30, 31, // P0.24 P0.31
32, 33, 34, 35, 36, 37, 38, 39, // P1.00 P1.07
40, 41, 42, 43, 44, 45, 46, 47, // P1.08 P1.15
};
-257
View File
@@ -1,257 +0,0 @@
#pragma once
// =============================================================================
// LilyGo T-Echo Card — Pin Definitions for Meck Firmware
//
// nRF52840 + SX1262 + SSD1315 (72×40 OLED) + L76K GPS + MAX98357 Speaker
// + MP34DT05 PDM Microphone + ICM20948 IMU + BQ25896 Charger + Solar
//
// Pin notation from LilyGo pinmap: (port, pin) → nRF GPIO = port*32 + pin
//
// Cross-referenced against:
// - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h
// - Meshtastic PR #10267 (caveman99 T-Echo-Card support)
// =============================================================================
#define LILYGO_TECHO_CARD
// -----------------------------------------------------------------------------
// I2C Bus (shared: OLED, IMU ICM20948, fuel gauge BQ27220 if present)
// -----------------------------------------------------------------------------
#define I2C_SDA 36 // (1, 4)
#define I2C_SCL 34 // (1, 2)
#define PIN_BOARD_SDA I2C_SDA
#define PIN_BOARD_SCL I2C_SCL
// -----------------------------------------------------------------------------
// SX1262 LoRa Radio (SPI bit-bang on nRF52)
// -----------------------------------------------------------------------------
#define P_LORA_NSS 11 // (0, 11) — CS
#define P_LORA_RESET 7 // (0, 7) — RST
#define P_LORA_SCLK 13 // (0, 13) — SCK
#define P_LORA_MOSI 15 // (0, 15) — MOSI
#define P_LORA_MISO 17 // (0, 17) — MISO
#define P_LORA_BUSY 14 // (0, 14) — BUSY
#define P_LORA_DIO_1 40 // (1, 8) — DIO1 / interrupt
// RF switch control (HPB16B3 / S62F module)
// DIO2 is used internally by the SX1262 for RF switch control.
// RF_VC1 / RF_VC2 are external PA/LNA select lines.
// Meshtastic PR #10267 maps these as TXEN/RXEN — verify on hardware
// whether DIO2-as-RF-switch alone is sufficient, or if VC1/VC2 are also
// needed for full TX/RX performance.
#define LORA_DIO2 5 // (0, 5) — DIO2 (RF switch / TXCO)
#define LORA_RF_VC1 27 // (0, 27) — RF_VC1 (potential TXEN)
#define LORA_RF_VC2 33 // (1, 1) — RF_VC2 (potential RXEN)
// SX1262 TCXO voltage via DIO3
// Meshtastic PR #10267 sets this to 1.8V. Without it, the TCXO may not
// start and the radio will have frequency drift or fail to init.
// Confirm on hardware — if the module has a TCXO fed by DIO3, this is needed.
#define SX126X_DIO3_TCXO_VOLTAGE 1.8f
// LoRa radio power: RT9080 controls the 3V3 rail for all peripherals
// including LoRa. No dedicated LoRa power enable pin.
#define PIN_LORA_EN -1
// Default radio settings (Australia)
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 22
#endif
// -----------------------------------------------------------------------------
// 0.42" OLED Display — SSD1315 (SSD1306-compatible), 72×40, I2C
//
// The SSD1315 has a 128×64 GDDRAM, but the physical panel is only 72×40.
// The visible window is mapped at columns 2899, pages 37 (rows 2463).
// This means:
// - Horizontal: auto-centred by driver ((128 - 72) / 2 = 28)
// - Vertical: need SETDISPLAYOFFSET = 24 (3 pages × 8 rows) so that
// data written to pages 04 appears on the physical display.
//
// If the MeshCore OLEDDisplay driver's display() method sends PAGEADDR
// starting at page 0, the SETDISPLAYOFFSET command should handle the
// mapping. If content appears shifted or blank, the alternative is to
// modify the PAGEADDR commands to write to pages 37 directly.
//
// Ref: Meshtastic PR #10267 uses setYOffset(3) to shift every PAGEADDR
// write, plus GEOMETRY_72_40 which sets SETMULTIPLEX to 39.
// -----------------------------------------------------------------------------
#define HAS_OLED 1
#define OLED_I2C_ADDR 0x3C
#define OLED_WIDTH 72
#define OLED_HEIGHT 40
#define OLED_SDA I2C_SDA
#define OLED_SCL I2C_SCL
// SSD1315 display offset: 3 pages = 24 rows
// Applied via SETDISPLAYOFFSET (0xD3) after display.begin() in target.cpp
#define OLED_DISPLAY_OFFSET 24
// OLED / peripheral power control via RT9080 enable pin
// This controls the 3V3 rail for OLED, GPS, LoRa, and sensors.
#define PIN_OLED_EN 30 // (0, 30) — RT9080_EN
// No hardware reset pin for OLED on T-Echo Card
#define PIN_OLED_RESET -1
// -----------------------------------------------------------------------------
// GPS — L76K Multi-GNSS (GPS, GLONASS, BeiDou, QZSS)
// -----------------------------------------------------------------------------
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
#define PIN_GPS_TX 19 // (0, 19) — nRF TX → GPS RX
#define PIN_GPS_RX 21 // (0, 21) — nRF RX ← GPS TX
#define PIN_GPS_EN 47 // (1, 15) — GPS power enable
#define PIN_GPS_WAKEUP 25 // (0, 25) — GPS wakeup
#define PIN_GPS_1PPS 23 // (0, 23) — 1PPS time pulse
#define PIN_GPS_RF_EN 29 // (0, 29) — GPS RF / LNA enable
// -----------------------------------------------------------------------------
// Battery & Power
// -----------------------------------------------------------------------------
#define PIN_VBAT_READ 2 // (0, 2) = AIN0 — battery voltage ADC
#define BATTERY_ADC_AIN 0 // nRF SAADC AIN channel number
#define BATTERY_CAPACITY_MAH 800
// Battery voltage divider enable gate
// P0.31 controls a FET/switch that enables the resistive divider feeding
// AIN0. Must be driven HIGH before ADC read and LOW after to avoid
// parasitic drain through the divider.
// Source: LilyGo t_echo_card_config.h → BATTERY_MEASUREMENT_CONTROL
// Confirmed: Meshtastic PR #10267 → ADC_CTRL (0 + 31), ADC_CTRL_ENABLED HIGH
#define BATTERY_MEASUREMENT_CONTROL 31 // (0, 31)
#define BATTERY_MEASUREMENT_ACTIVE HIGH
// Battery voltage divider calibration
// nRF52840 SAADC: 0.6V internal ref, 1/6 gain → 03.6V range
// With 2:1 resistive divider, multiply by 2 to get actual cell voltage.
// Adjust ADC_MULTIPLIER after measuring real voltage vs ADC reading.
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER 2.0f
#endif
// BQ25896 charger is on I2C but managed by hardware — no software control needed
// Solar panel input: 0.25W 5V via VBUS
// Auto-shutdown threshold (millivolts)
#define AUTO_SHUTDOWN_MILLIVOLTS 3200
// -----------------------------------------------------------------------------
// Speaker — MAX98357 I2S Class-D Mono Amp (8Ω, 1W)
// -----------------------------------------------------------------------------
#define HAS_SPEAKER 1
#define PIN_SPK_EN 43 // (1, 11) — speaker amplifier enable
#define PIN_SPK_EN2 3 // (0, 3) — secondary enable
#define PIN_SPK_BCLK 16 // (0, 16) — I2S bit clock
#define PIN_SPK_DATA 20 // (0, 20) — I2S data out
#define PIN_SPK_LRCK 22 // (0, 22) — I2S word select / LRCK
// -----------------------------------------------------------------------------
// Microphone — MP34DT05 Digital MEMS PDM
// -----------------------------------------------------------------------------
#define HAS_MICROPHONE 1
#define PIN_MIC_CLK 35 // (1, 3) — PDM clock
#define PIN_MIC_DATA 37 // (1, 5) — PDM data
// -----------------------------------------------------------------------------
// Buttons
// -----------------------------------------------------------------------------
#define PIN_BUTTON_A 42 // (1, 10) — orange button (front, main user button)
#define PIN_BUTTON_BOOT 24 // (0, 24) — boot button (nRF52840 BOOT)
#define PIN_USER_BTN PIN_BUTTON_A
// Button_B is RESET — hardware only, no GPIO
// Active LOW for nRF52 buttons (internal pull-up, press = LOW)
#define BUTTON_ACTIVE_LOW 1
// -----------------------------------------------------------------------------
// Buzzer
// -----------------------------------------------------------------------------
#define HAS_BUZZER 1
#define PIN_BUZZER 38 // (1, 6) — piezo buzzer data / PWM
// -----------------------------------------------------------------------------
// WS2812 RGB LEDs — 3 independent LEDs on separate data lines (NOT a chain)
//
// Confirmed by Meshtastic PR #10267: each WS2812 is on its own GPIO,
// driven as a 1-pixel NeoPixel strand. The bare-die WS2812s are very
// bright at full intensity — scale to ~25% (0x40 max per channel).
//
// Role assignments (matching Meshtastic PR #10267):
// DATA_1 (P1.7) → power/charge status (red)
// DATA_2 (P1.12) → notification / mesh activity (green)
// DATA_3 (P0.28) → BLE pairing status (blue)
// -----------------------------------------------------------------------------
#define HAS_RGB_LED 1
#define PIN_RGB_LED_1 39 // (1, 7) — WS2812 data 1 (power/charge)
#define PIN_RGB_LED_2 44 // (1, 12) — WS2812 data 2 (notification)
#define PIN_RGB_LED_3 28 // (0, 28) — WS2812 data 3 (BLE pairing)
// Default NeoPixel colours at 25% brightness (0x40 max per channel)
#define NEOPIXEL_COLOR_POWER 0x400000 // red
#define NEOPIXEL_COLOR_NOTIFY 0x004000 // green
#define NEOPIXEL_COLOR_PAIRING 0x000040 // blue
// Legacy aliases (kept for any code referencing these)
#define PIN_NEOPIXEL PIN_RGB_LED_1
#define NUM_NEOPIXELS 3
// -----------------------------------------------------------------------------
// IMU — ICM20948 9-axis MotionTracking (accelerometer + gyro + compass)
// -----------------------------------------------------------------------------
#define HAS_IMU 1
#define IMU_I2C_ADDR 0x68
#define IMU_SDA I2C_SDA
#define IMU_SCL I2C_SCL
// -----------------------------------------------------------------------------
// NFC — nRF52840 NFC-A (Type 2 Tag)
// NFC uses dedicated NFC1/NFC2 pins on nRF52840 (P0.09 / P0.10)
// These are fixed by silicon — no GPIO config needed.
// NFC is handled by the nRF52 SDK nfc_t2t_lib.
// -----------------------------------------------------------------------------
#define HAS_NFC 1
// -----------------------------------------------------------------------------
// External Flash — ZD25WQ32CEIGR (4MB QSPI Flash)
//
// Pin mapping confirmed from LilyGo t_echo_card_config.h and Meshtastic
// PR #10267. These are on a separate SPI bus from LoRa.
// The Adafruit nRF52 core supports QSPI via Adafruit_SPIFlash + LittleFS.
// -----------------------------------------------------------------------------
#define HAS_EXT_FLASH 1
#define PIN_QSPI_SCK 4 // (0, 4)
#define PIN_QSPI_CS 12 // (0, 12)
#define PIN_QSPI_IO0 6 // (0, 6) — MOSI / D0
#define PIN_QSPI_IO1 8 // (0, 8) — MISO / D1
#define PIN_QSPI_IO2 41 // (1, 9) — WP / D2
#define PIN_QSPI_IO3 26 // (0, 26) — HOLD / D3
// -----------------------------------------------------------------------------
// No SD Card on T-Echo Card
// Settings stored in LittleFS on internal/external flash
// -----------------------------------------------------------------------------
// #define HAS_SDCARD
// -----------------------------------------------------------------------------
// No dedicated RTC chip — time from GPS or BLE companion sync
// nRF52840 has a 32.768 kHz RTC peripheral for timekeeping while running
// -----------------------------------------------------------------------------
// #define HAS_PCF85063_RTC
// -----------------------------------------------------------------------------
// Misc / Compatibility
// -----------------------------------------------------------------------------
// No e-ink display
// #define HAS_EINK
// This board has no physical keyboard
// #define HAS_PHYSICAL_KEYBOARD
// Fallback for code that references GPS_BAUDRATE without HAS_GPS guard
#ifndef GPS_BAUDRATE
#define GPS_BAUDRATE 9600
#endif