mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-08 14:24:51 +02:00
updating t-echo card folder
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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};
|
||||
@@ -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 28–99, pages 3–7 (rows 24–63).
|
||||
// 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 0–3.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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.00–P0.31 (0–31) + P1.00–P1.15 (32–47)
|
||||
// =============================================================================
|
||||
|
||||
#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
|
||||
};
|
||||
@@ -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 28–99, pages 3–7 (rows 24–63).
|
||||
// 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 0–4 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 3–7 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 → 0–3.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
|
||||
Reference in New Issue
Block a user