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
189 changes: 148 additions & 41 deletions examples/simple_repeater/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@

#define AUTO_OFF_MILLIS 20000 // 20 seconds
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
#define STATUS_SCREEN_MILLIS 5000 // 5 seconds

// Electric plug icon 16x7px (side profile, cord left, prongs right)
static const uint8_t charging_icon [] PROGMEM = {
0x01, 0xE0, // .......####.....
0x03, 0xE0, // ......#####.....
0x03, 0xFE, // ......#########.
0xFF, 0xE0, // ###########.....
0x03, 0xFE, // ......#########.
0x03, 0xE0, // ......#####.....
0x01, 0xE0, // .......####.....
};

// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
Expand All @@ -26,61 +38,156 @@ static const uint8_t meshcore_logo [] PROGMEM = {
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};

// Draw a horizontal rule
static void drawHRule(DisplayDriver* d, int x, int y, int w) {
d->setColor(DisplayDriver::LIGHT);
d->fillRect(x, y, w, 1);
}

void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_node_prefs = node_prefs;
_display->turnOn();

// strip off dash and commit hash by changing dash to null terminator
// e.g: v1.2.3-abcdef -> v1.2.3
char *version = strdup(firmware_version);
char *dash = strchr(version, '-');
if(dash){
*dash = 0;
}
if(dash) *dash = 0;

// v1.2.3 (1 Jan 2025)
sprintf(_version_info, "%s (%s)", version, build_date);
}

void UITask::showStatus(uint16_t batt_mv, unsigned long uptime_ms, bool charging) {
_status_batt_mv = batt_mv;
_status_uptime_ms = uptime_ms;
_status_charging = charging;
_status_until = millis() + STATUS_SCREEN_MILLIS;
_auto_off = _status_until + AUTO_OFF_MILLIS;
_next_refresh = 0;
_display->turnOn();
}

void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
// meshcore logo
_display->setColor(DisplayDriver::BLUE);
int logoWidth = 128;
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
int W = _display->width();

// version info
if (millis() < BOOT_SCREEN_MILLIS) {
// ── BOOT SCREEN ──

// logo centered
_display->setColor(DisplayDriver::LIGHT);
_display->drawXbm((W - 128) / 2, 6, meshcore_logo, 128, 13);

// thin rule under logo
drawHRule(_display, 10, 22, W - 20);

// version centered
_display->setTextSize(1);
uint16_t versionWidth = _display->getTextWidth(_version_info);
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);

// node type
const char* node_type = "< Repeater >";
uint16_t typeWidth = _display->getTextWidth(node_type);
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else { // home screen
// node name
_display->setCursor(0, 0);
_display->setColor(DisplayDriver::LIGHT);
_display->drawTextCentered(W / 2, 27, _version_info);

// role label in brackets
drawHRule(_display, 10, 39, W - 20);
_display->drawTextCentered(W / 2, 44, "[ REPEATER ]");

} else if (_status_until > 0 && millis() < _status_until) {
// ── STATUS SCREEN ──

// header bar: inverted "STATUS" label
_display->setColor(DisplayDriver::LIGHT);
_display->fillRect(0, 0, W, 11);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
_display->print(_node_prefs->node_name);

// freq / sf
_display->setCursor(0, 20);
_display->setColor(DisplayDriver::YELLOW);
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
_display->print(tmp);

// bw / cr
_display->setCursor(0, 30);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
_display->setColor(DisplayDriver::DARK);
_display->drawTextCentered(W / 2, 2, "STATUS");

int pct = 0;
if (_status_batt_mv >= 4200) pct = 100;
else if (_status_batt_mv > 3000) pct = (_status_batt_mv - 3000) * 100 / 1200;

// uptime row
_display->setColor(DisplayDriver::LIGHT);
unsigned long secs = _status_uptime_ms / 1000;
unsigned long mins = secs / 60;
unsigned long hrs = mins / 60;
unsigned long days = hrs / 24;

_display->setCursor(4, 15);
_display->print("UPTIME");
if (days > 0) {
sprintf(tmp, "%lud %luh %lum", days, hrs % 24, mins % 60);
} else if (hrs > 0) {
sprintf(tmp, "%luh %lum %lus", hrs, mins % 60, secs % 60);
} else {
sprintf(tmp, "%lum %lus", mins, secs % 60);
}
_display->drawTextRightAlign(W - 4, 15, tmp);

drawHRule(_display, 4, 25, W - 8);

// battery voltage row
_display->setCursor(4, 29);
_display->print("BATT");
sprintf(tmp, "%u.%02uV %d%%", _status_batt_mv / 1000, (_status_batt_mv % 1000) / 10, pct);
_display->drawTextRightAlign(W - 4, 29, tmp);

drawHRule(_display, 4, 39, W - 8);

// battery bar (full width gauge)
int barX = 4, barY = 43, barW = W - 8, barH = 8;
_display->drawRect(barX, barY, barW, barH);
int fillW = (pct * (barW - 4)) / 100;
if (fillW > 0) _display->fillRect(barX + 2, barY + 2, fillW, barH - 4);

// percentage label centered under bar, with charging icon if applicable
sprintf(tmp, "%d%%", pct);
if (_status_charging) {
int txtW = _display->getTextWidth(tmp);
int totalW = 18 + 2 + txtW + 4 + _display->getTextWidth("CHG");
int startX = (W - totalW) / 2;
_display->drawXbm(startX, 55, charging_icon, 16, 7);
_display->setCursor(startX + 20, 54);
_display->print(tmp);
_display->print(" CHG");
} else {
_display->drawTextCentered(W / 2, 54, tmp);
}

} else {
// ── HOME SCREEN ──

// header bar: inverted node name
_display->setColor(DisplayDriver::LIGHT);
_display->fillRect(0, 0, W, 11);
_display->setTextSize(1);
_display->setColor(DisplayDriver::DARK);
_display->drawTextCentered(W / 2, 2, _node_prefs->node_name);

// radio params
_display->setColor(DisplayDriver::LIGHT);

_display->setCursor(4, 16);
_display->print("FREQ");
sprintf(tmp, "%06.3f MHz", _node_prefs->freq);
_display->drawTextRightAlign(W - 4, 16, tmp);

drawHRule(_display, 4, 26, W - 8);

_display->setCursor(4, 30);
_display->print("SF");
sprintf(tmp, "%d", _node_prefs->sf);
_display->drawTextRightAlign(W / 2 - 4, 30, tmp);

_display->setCursor(W / 2 + 4, 30);
_display->print("CR");
sprintf(tmp, "%d", _node_prefs->cr);
_display->drawTextRightAlign(W - 4, 30, tmp);

drawHRule(_display, 4, 40, W - 8);

_display->setCursor(4, 44);
_display->print("BW");
sprintf(tmp, "%03.1f kHz", _node_prefs->bw);
_display->drawTextRightAlign(W - 4, 44, tmp);
}
}

Expand All @@ -89,17 +196,17 @@ void UITask::loop() {
if (millis() >= _next_read) {
int btnState = digitalRead(PIN_USER_BTN);
if (btnState != _prevBtnState) {
if (btnState == USER_BTN_PRESSED) { // pressed?
if (btnState == USER_BTN_PRESSED) {
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_auto_off = millis() + AUTO_OFF_MILLIS;
}
_prevBtnState = btnState;
}
_next_read = millis() + 200; // 5 reads per second
_next_read = millis() + 200;
}
#endif

Expand All @@ -109,7 +216,7 @@ void UITask::loop() {
renderCurrScreen();
_display->endFrame();

_next_refresh = millis() + 1000; // refresh every second
_next_refresh = millis() + 1000;
}
if (millis() > _auto_off) {
_display->turnOff();
Expand Down
8 changes: 7 additions & 1 deletion examples/simple_repeater/UITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ class UITask {
NodePrefs* _node_prefs;
char _version_info[32];

unsigned long _status_until; // millis when status screen expires
uint16_t _status_batt_mv;
unsigned long _status_uptime_ms;
bool _status_charging;

void renderCurrScreen();
public:
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; }
UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; _status_until = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void showStatus(uint16_t batt_mv, unsigned long uptime_ms, bool charging);

void loop();
};
38 changes: 38 additions & 0 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ static unsigned long userBtnDownAt = 0;
#define USER_BTN_HOLD_OFF_MILLIS 1500
#endif

#if defined(PIN_USER_BTN_ANA) && !defined(_SEEED_SENSECAP_SOLAR_H_)
static unsigned long anaBtnDownAt = 0;
#define ANA_BTN_HOLD_OFF_MILLIS 5000
#define ANA_BTN_SHORT_MILLIS 500
static bool anaBtnShortHandled = false;
#endif

void setup() {
Serial.begin(115200);
delay(1000);
Expand Down Expand Up @@ -147,6 +154,37 @@ void loop() {
}
#endif

#if defined(PIN_USER_BTN_ANA) && !defined(_SEEED_SENSECAP_SOLAR_H_)
bool anaPressed = (digitalRead(PIN_USER_BTN_ANA) == LOW);
if (anaPressed) {
if (anaBtnDownAt == 0) {
anaBtnDownAt = millis();
anaBtnShortHandled = false;
}
unsigned long held = (unsigned long)(millis() - anaBtnDownAt);
if (held >= ANA_BTN_HOLD_OFF_MILLIS) {
Serial.println("AIN1 shutdown triggered");
#ifdef DISPLAY_CLASS
display.turnOn();
display.startFrame();
display.setCursor(0, 20);
display.print("Shutting down...");
display.endFrame();
delay(1500);
display.turnOff();
#endif
board.powerOff();
} else if (!anaBtnShortHandled && held >= ANA_BTN_SHORT_MILLIS) {
anaBtnShortHandled = true;
#ifdef DISPLAY_CLASS
ui_task.showStatus(board.getBattMilliVolts(), millis(), board.isExternalPowered());
#endif
}
} else {
anaBtnDownAt = 0;
}
#endif

the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
Expand Down
14 changes: 14 additions & 0 deletions variants/rak3401/RAK3401Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ void RAK3401Board::initiateShutdown(uint8_t reason) {
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
}

#ifdef PIN_USER_BTN_ANA
// Configure AIN1 button as GPIO SENSE wake source (active LOW)
// Wait for button release first β€” SENSE is level-triggered
while (digitalRead(PIN_USER_BTN_ANA) == LOW) delay(10);
delay(50); // debounce

// Configure pin for SENSE LOW wake via nRF52 GPIO registers
uint32_t pin = (uint32_t)PIN_USER_BTN_ANA;
NRF_GPIO->PIN_CNF[pin] = (GPIO_PIN_CNF_DIR_Input << GPIO_PIN_CNF_DIR_Pos)
| (GPIO_PIN_CNF_INPUT_Connect << GPIO_PIN_CNF_INPUT_Pos)
| (GPIO_PIN_CNF_PULL_Pullup << GPIO_PIN_CNF_PULL_Pos)
| (GPIO_PIN_CNF_SENSE_Low << GPIO_PIN_CNF_SENSE_Pos);
#endif

enterSystemOff(reason);
}
#endif
Expand Down
4 changes: 4 additions & 0 deletions variants/rak3401/RAK3401Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class RAK3401Board : public NRF52BoardDCDC {
RAK3401Board() : NRF52Board("RAK3401_OTA") {}
void begin();

#ifdef NRF52_POWER_MANAGEMENT
void powerOff() override { initiateShutdown(SHUTDOWN_REASON_USER); }
#endif

#define BATTERY_SAMPLES 8

uint16_t getBattMilliVolts() override {
Expand Down
1 change: 1 addition & 0 deletions variants/rak3401/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extends = rak3401
build_flags =
${rak3401.build_flags}
-D DISPLAY_CLASS=SSD1306Display
-D PIN_USER_BTN_ANA=31
-D ADVERT_NAME='"RAK3401 1W Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
Expand Down