diff --git a/boards/lilygo_techo_card.json b/boards/lilygo_techo_card.json index d7c39bd8..a57f88b4 100644 --- a/boards/lilygo_techo_card.json +++ b/boards/lilygo_techo_card.json @@ -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", diff --git a/variants/lilygo_techo_card_WIP/Cpupowermanager.h b/variants/lilygo_techo_card/Cpupowermanager.h similarity index 100% rename from variants/lilygo_techo_card_WIP/Cpupowermanager.h rename to variants/lilygo_techo_card/Cpupowermanager.h diff --git a/variants/lilygo_techo_card/GPSStreamCounter.h b/variants/lilygo_techo_card/GPSStreamCounter.h new file mode 100644 index 00000000..4013e9fc --- /dev/null +++ b/variants/lilygo_techo_card/GPSStreamCounter.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +// 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; +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/Radiopresets.h b/variants/lilygo_techo_card/Radiopresets.h similarity index 100% rename from variants/lilygo_techo_card_WIP/Radiopresets.h rename to variants/lilygo_techo_card/Radiopresets.h diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp new file mode 100644 index 00000000..cf9c601f --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -0,0 +1,339 @@ +#include "TechoCardBoard.h" +#include "variant.h" +#include +#include +#include +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; +} \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h new file mode 100644 index 00000000..ae432682 --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include "variant.h" + +#if defined(HAS_RGB_LED) + #include +#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 }; +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h new file mode 100644 index 00000000..78c789eb --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -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 +#include +#include +#include +#include +#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(); + } + } + } +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/pins_arduino.h b/variants/lilygo_techo_card/pins_arduino.h similarity index 100% rename from variants/lilygo_techo_card_WIP/pins_arduino.h rename to variants/lilygo_techo_card/pins_arduino.h diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini new file mode 100644 index 00000000..1c106c2b --- /dev/null +++ b/variants/lilygo_techo_card/platformio.ini @@ -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} + + + + + + + +<../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} + + + +<../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} + + + +<../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> \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp new file mode 100644 index 00000000..0fb27d22 --- /dev/null +++ b/variants/lilygo_techo_card/target.cpp @@ -0,0 +1,52 @@ +#include +#include "target.h" +#include +#include + +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); +} \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h new file mode 100644 index 00000000..19e41434 --- /dev/null +++ b/variants/lilygo_techo_card/target.h @@ -0,0 +1,44 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include "TechoCardBoard.h" + +#if ENV_INCLUDE_GPS +#include "GPSStreamCounter.h" +#endif + +#ifdef DISPLAY_CLASS + #if defined(USE_U8G2_DISPLAY) + #include + #else + #include + #endif + #include +#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(); \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.cpp b/variants/lilygo_techo_card/variant.cpp new file mode 100644 index 00000000..4cba928f --- /dev/null +++ b/variants/lilygo_techo_card/variant.cpp @@ -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}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h new file mode 100644 index 00000000..c60c7684 --- /dev/null +++ b/variants/lilygo_techo_card/variant.h @@ -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 \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/TechoCardBoard.cpp b/variants/lilygo_techo_card_WIP/TechoCardBoard.cpp deleted file mode 100644 index 3fff0503..00000000 --- a/variants/lilygo_techo_card_WIP/TechoCardBoard.cpp +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/TechoCardBoard.h b/variants/lilygo_techo_card_WIP/TechoCardBoard.h deleted file mode 100644 index 68b73326..00000000 --- a/variants/lilygo_techo_card_WIP/TechoCardBoard.h +++ /dev/null @@ -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 -#include -#include "variant.h" - -// WS2812 NeoPixel support — 3 independent LEDs on separate GPIOs -#if defined(HAS_RGB_LED) - #include -#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 -}; \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/platformio.ini b/variants/lilygo_techo_card_WIP/platformio.ini deleted file mode 100644 index 249f22d0..00000000 --- a/variants/lilygo_techo_card_WIP/platformio.ini +++ /dev/null @@ -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> - + -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} - + - + - +<../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> \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/target.cpp b/variants/lilygo_techo_card_WIP/target.cpp deleted file mode 100644 index b183b983..00000000 --- a/variants/lilygo_techo_card_WIP/target.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// ============================================================================= -// MeshCore target implementation for LilyGo T-Echo Card -// -// nRF52840 + SX1262 (HPB16B3 / S62F module) + SSD1315 OLED + L76K GPS -// ============================================================================= - -#include -#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 - 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); -} \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/target.h b/variants/lilygo_techo_card_WIP/target.h deleted file mode 100644 index f76ef3e4..00000000 --- a/variants/lilygo_techo_card_WIP/target.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -// ============================================================================= -// MeshCore target declarations for LilyGo T-Echo Card -// ============================================================================= - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef DISPLAY_CLASS -#include -#endif - -#include - -#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(); \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/variant.cpp b/variants/lilygo_techo_card_WIP/variant.cpp deleted file mode 100644 index a47af379..00000000 --- a/variants/lilygo_techo_card_WIP/variant.cpp +++ /dev/null @@ -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 - -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 -}; \ No newline at end of file diff --git a/variants/lilygo_techo_card_WIP/variant.h b/variants/lilygo_techo_card_WIP/variant.h deleted file mode 100644 index 6ccc8370..00000000 --- a/variants/lilygo_techo_card_WIP/variant.h +++ /dev/null @@ -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