Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ compile_commands.json
.venv/
venv/
platformio.local.ini
.DS_Store
50 changes: 50 additions & 0 deletions boards/lilygo_techo_card.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"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"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"openocd_target": "nrf52840"
},
"frameworks": ["arduino"],
"name": "LilyGo T-Echo Card (nRF52840, SX1262, 4MB Flash)",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["nrfutil", "jlink", "cmsis-dap"],
"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",
"vendor": "LILYGO"
}
11 changes: 11 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim
if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
{ float t = board.getMCUTemperature(); if (!isnan(t)) telemetry.addTemperature(TELEM_CHANNEL_SELF, t); }
// query other sensors -- target specific
sensors.querySensors(permissions, telemetry);

Expand Down Expand Up @@ -1613,6 +1614,7 @@ void MyMesh::handleCmdFrame(size_t len) {
} else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request
telemetry.reset();
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
{ float t = board.getMCUTemperature(); if (!isnan(t)) telemetry.addTemperature(TELEM_CHANNEL_SELF, t); }
// query other sensors -- target specific
sensors.querySensors(0xFF, telemetry);

Expand Down Expand Up @@ -2191,3 +2193,12 @@ bool MyMesh::advert() {
return false;
}
}

// To check if there is pending work
bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep
#endif
// If getOutboundTotal() is not available, use: _mgr->getOutboundCount(0xFFFFFFFF) > 0
return _mgr->getOutboundTotal() > 0;
}
12 changes: 11 additions & 1 deletion examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,19 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
sprintf(interval_str, "%u", _prefs.gps_interval);
sensors.setSettingValue("gps_interval", interval_str);
}
#if defined(LILYGO_TECHO_CARD)
// Power the L76K down at boot if GPS is persisted as off.
// Without this, board.begin() drives PIN_GPS_EN HIGH unconditionally
// and the chip stays powered until the user manually toggles GPS
// twice through the menu.
board.enableGPS(_prefs.gps_enabled);
#endif
}
#endif

// To check if there is pending work
bool hasPendingWork() const;

private:
void writeOKFrame();
void writeErrFrame(uint8_t err_code);
Expand Down Expand Up @@ -249,4 +259,4 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};

extern MyMesh the_mesh;
extern MyMesh the_mesh;
8 changes: 7 additions & 1 deletion examples/companion_radio/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,10 @@ void loop() {
ui_task.loop();
#endif
rtc_clock.tick();
}

if (!the_mesh.hasPendingWork()) {
#if defined(NRF52_PLATFORM)
board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#endif
}
}
71 changes: 69 additions & 2 deletions examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
#if defined(LILYGO_TECHO_CARD)
#include "TechoCardHomeScreen.h"
#endif

#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 15000 // 15 seconds
Expand Down Expand Up @@ -34,7 +37,7 @@
class SplashScreen : public UIScreen {
UITask* _task;
unsigned long dismiss_after;
char _version_info[12];
char _version_info[24];

public:
SplashScreen(UITask* task) : _task(task) {
Expand All @@ -52,6 +55,15 @@ class SplashScreen : public UIScreen {
}

int render(DisplayDriver& display) override {
#if defined(LILYGO_TECHO_CARD)
// Text-only splash for 72×40 OLED -- no room for 128px logo
display.setColor(DisplayDriver::GREEN);
display.setTextSize(1);
display.drawTextCentered(display.width()/2, 2, "MeshCore");
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width()/2, 14, _version_info);
display.drawTextCentered(display.width()/2, 26, FIRMWARE_BUILD_DATE);
#else
// meshcore logo
display.setColor(DisplayDriver::BLUE);
int logoWidth = 128;
Expand All @@ -72,6 +84,7 @@ class SplashScreen : public UIScreen {

display.setTextSize(1);
display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE);
#endif

return 1000;
}
Expand Down Expand Up @@ -585,8 +598,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
_alert_expiry = 0;

splash = new SplashScreen(this);
#if defined(LILYGO_TECHO_CARD)
home = new TechoCardHomeScreen(this, &rtc_clock, node_prefs);
#else
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
#endif
#if !defined(LILYGO_TECHO_CARD)
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
#endif
setCurrScreen(splash);
}

Expand Down Expand Up @@ -635,8 +654,10 @@ void UITask::msgRead(int msgcount) {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;

#if !defined(LILYGO_TECHO_CARD)
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
setCurrScreen(msg_preview);
#endif

if (_display != NULL) {
if (!_display->isOn() && !hasConnection()) {
Expand Down Expand Up @@ -742,9 +763,49 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_ENTER);
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
#if defined(LILYGO_TECHO_CARD)
// Toggle screen on/off for battery saving
if (_display != NULL) {
if (_display->isOn()) {
_display->turnOff();
} else {
_display->turnOn();
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 0;
}
}
c = 0; // consume event
#else
c = handleDoubleClick(KEY_PREV);
#endif
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
#if defined(LILYGO_TECHO_CARD)
toggleBuzzer();
c = 0; // consume event
#else
c = handleTripleClick(KEY_SELECT);
#endif
}
#endif
#if defined(PIN_BOOT_BTN)
// Second navigation button (C / Boot on T-Echo Card) + torch
{
static MomentaryButton boot_btn(PIN_BOOT_BTN, LONG_PRESS_MILLIS, true);
static bool _boot_btn_ready = false;
if (!_boot_btn_ready) { boot_btn.begin(); _boot_btn_ready = true; }
static bool torch_on = false;
int ev2 = boot_btn.check();
if (ev2 == BUTTON_EVENT_CLICK && c == 0) {
c = checkDisplayOn(KEY_PREV);
} else if (ev2 == BUTTON_EVENT_DOUBLE_CLICK) {
torch_on = !torch_on;
if (torch_on) {
// Single LED only -- driving all three white exceeds RT9080 current budget and reboots
board.setStatusLED(0, 0xFFFFFF);
} else {
board.setStatusLED(0, 0);
}
}
}
#endif
#if defined(PIN_USER_BTN_ANA)
Expand Down Expand Up @@ -905,6 +966,12 @@ void UITask::toggleGPS() {
_node_prefs->gps_enabled = 1;
notify(UIEventType::ack);
}
#if defined(LILYGO_TECHO_CARD)
// Actually power the L76K down/up at the hardware level.
// Without this, toggling GPS off only stops reading position data
// while the chip itself stays powered (~25mA draw).
board.enableGPS(_node_prefs->gps_enabled);
#endif
the_mesh.savePrefs();
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
_next_refresh = 0;
Expand All @@ -928,4 +995,4 @@ void UITask::toggleBuzzer() {
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
_next_refresh = 0; // trigger refresh
#endif
}
}
127 changes: 127 additions & 0 deletions src/helpers/ui/U8g2Display.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#pragma once

#include "DisplayDriver.h"
#include <U8g2lib.h>
#include <Wire.h>

#ifndef DISPLAY_ADDRESS
#define DISPLAY_ADDRESS 0x3C
#endif

#ifndef OLED_WIDTH
#define OLED_WIDTH 72
#endif

#ifndef OLED_HEIGHT
#define OLED_HEIGHT 40
#endif

class U8g2Display : public DisplayDriver {
// U8g2 constructor for SSD1306/SSD1315 72×40 panel — handles all
// GDDRAM column/page offsets, SETMULTIPLEX, SETDISPLAYOFFSET internally
U8G2_SSD1306_72X40_ER_F_HW_I2C _u8g2;
bool _isOn;
uint8_t _drawColor;

// Font metrics for current font (cached on setTextSize)
uint8_t _fontAscent;
uint8_t _fontHeight;

void applyFont(int sz) {
if (sz >= 2) {
_u8g2.setFont(u8g2_font_5x7_mr); // 5×7 — "large" for this display
} else {
_u8g2.setFont(u8g2_font_5x7_mr);
}
_fontAscent = _u8g2.getAscent();
_fontHeight = _u8g2.getAscent() - _u8g2.getDescent();
}

public:
U8g2Display() : DisplayDriver(OLED_WIDTH, OLED_HEIGHT),
_u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE),
_isOn(false), _drawColor(1), _fontAscent(5), _fontHeight(6) {}

bool begin() {
// Wire must already be initialised by board.begin() before this is called
bool ok = _u8g2.begin();
if (ok) {
_u8g2.setI2CAddress(DISPLAY_ADDRESS * 2); // U8g2 uses 8-bit address
_u8g2.setFontPosTop(); // y coordinate = top of text, not baseline
_u8g2.setFontMode(1); // transparent background
applyFont(1); // default to compact font
_isOn = true;
}
return ok;
}

bool isOn() override { return _isOn; }

void turnOn() override {
_u8g2.setPowerSave(0);
_isOn = true;
}

void turnOff() override {
_u8g2.setPowerSave(1);
_isOn = false;
}

void clear() override {
_u8g2.clearBuffer();
_u8g2.sendBuffer();
}

void startFrame(Color bkg = DARK) override {
_u8g2.clearBuffer();
_drawColor = 1;
_u8g2.setDrawColor(1);
applyFont(1);
}

void setTextSize(int sz) override {
applyFont(sz);
}

void setColor(Color c) override {
_drawColor = (c != DARK) ? 1 : 0;
_u8g2.setDrawColor(_drawColor);
}

void setCursor(int x, int y) override {
_cursorX = x;
_cursorY = y;
}

void print(const char* str) override {
_u8g2.setDrawColor(_drawColor);
_u8g2.drawStr(_cursorX, _cursorY, str);
}

void fillRect(int x, int y, int w, int h) override {
_u8g2.setDrawColor(_drawColor);
_u8g2.drawBox(x, y, w, h);
}

void drawRect(int x, int y, int w, int h) override {
_u8g2.setDrawColor(_drawColor);
_u8g2.drawFrame(x, y, w, h);
}

void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override {
_u8g2.setDrawColor(1);
_u8g2.drawXBM(x, y, w, h, bits);
}

uint16_t getTextWidth(const char* str) override {
return _u8g2.getStrWidth(str);
}

void endFrame() override {
_u8g2.sendBuffer();
}

private:
int _cursorX = 0;
int _cursorY = 0;
};
Loading