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
52 changes: 34 additions & 18 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,31 +208,47 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
return 0;
}

void MyMesh::getStats(RepeaterStats& stats) {
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundTotal();
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
stats.total_air_time_secs = getTotalAirTime() / 1000;
stats.total_up_time_secs = uptime_millis / 1000;
stats.n_sent_flood = getNumSentFlood();
stats.n_sent_direct = getNumSentDirect();
stats.n_recv_flood = getNumRecvFlood();
stats.n_recv_direct = getNumRecvDirect();
stats.err_events = _err_flags;
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
}

uint32_t MyMesh::getNumNeighbours() const {
#if MAX_NEIGHBOURS
uint32_t n = 0;
for (int i = 0; i < MAX_NEIGHBOURS; i++) {
if (neighbours[i].heard_timestamp > 0) n++;
}
return n;
#else
return 0;
#endif
}

int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) {
// uint32_t now = getRTCClock()->getCurrentTimeUnique();
// memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')

if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now
RepeaterStats stats;
stats.batt_milli_volts = board.getBattMilliVolts();
stats.curr_tx_queue_len = _mgr->getOutboundTotal();
stats.noise_floor = (int16_t)_radio->getNoiseFloor();
stats.last_rssi = (int16_t)radio_driver.getLastRSSI();
stats.n_packets_recv = radio_driver.getPacketsRecv();
stats.n_packets_sent = radio_driver.getPacketsSent();
stats.total_air_time_secs = getTotalAirTime() / 1000;
stats.total_up_time_secs = uptime_millis / 1000;
stats.n_sent_flood = getNumSentFlood();
stats.n_sent_direct = getNumSentDirect();
stats.n_recv_flood = getNumRecvFlood();
stats.n_recv_direct = getNumRecvDirect();
stats.err_events = _err_flags;
stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4);
stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups();
stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups();
stats.total_rx_air_time_secs = getReceiveAirTime() / 1000;
stats.n_recv_errors = radio_driver.getPacketsRecvErrors();
getStats(stats);
memcpy(&reply_data[4], &stats, sizeof(stats));

return 4 + sizeof(stats); // reply_len
Expand Down
5 changes: 5 additions & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
return &_prefs;
}

// Snapshot of live repeater stats (used by both the client status request and the UI)
void getStats(RepeaterStats& stats);
// Number of currently-known neighbours (non-empty entries in the neighbours table)
uint32_t getNumNeighbours() const;

void savePrefs() override {
_cli.savePrefs(_fs);
}
Expand Down
181 changes: 157 additions & 24 deletions examples/simple_repeater/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,72 @@
#include "target.h"
#include <Arduino.h>
#include <helpers/CommonCLI.h>
#include "MyMesh.h"

#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif

#define AUTO_OFF_MILLIS 20000 // 20 seconds
#ifndef AUTO_OFF_MILLIS
#define AUTO_OFF_MILLIS 20000 // 20 seconds (0 = never auto-off, e.g. mains/solar repeaters)
#endif
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds

#define POWEROFF_DELAY 3000

#define EINK_REFRESH_MILLIS 30000 // e-ink: slow + wears with use, and CRC-gated in the driver
#define NUM_EINK_PAGES 3 // Status / Radio / Traffic

// battery voltage -> percent range (override per-variant if needed)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif

// 'meshcore', 128x13px
static const uint8_t meshcore_logo [] PROGMEM = {
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
};

void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
static int battPercent(uint16_t mv) {
int pct = ((int)mv - BATT_MIN_MILLIVOLTS) * 100 / (BATT_MAX_MILLIVOLTS - BATT_MIN_MILLIVOLTS);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
return pct;
}

// human-readable uptime at minute granularity (keeps e-ink physical updates to ~1/min)
static void fmtUptime(char* out, size_t n, uint32_t secs) {
uint32_t d = secs / 86400; secs %= 86400;
uint32_t h = secs / 3600; secs %= 3600;
uint32_t m = secs / 60;
if (d) snprintf(out, n, "%lud %luh %lum", (unsigned long)d, (unsigned long)h, (unsigned long)m);
else if (h) snprintf(out, n, "%luh %lum", (unsigned long)h, (unsigned long)m);
else snprintf(out, n, "%lum", (unsigned long)m);
}

void UITask::begin(MyMesh* mesh, NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
_mesh = mesh;
_prevBtnState = HIGH;
_auto_off = millis() + AUTO_OFF_MILLIS;
_started_at = millis();
_node_prefs = node_prefs;
_is_eink = _display->isEink();
_page = 0;
_display->turnOn();

#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
Expand All @@ -53,6 +87,97 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
free(version);
}

// Rich, multi-page dashboard for large e-ink panels (e.g. E290 / E213).
void UITask::renderRichScreen() {
static const char* PAGE_NAMES[NUM_EINK_PAGES] = { "Status", "Radio", "Traffic" };
char line[64];
RepeaterStats st;
_mesh->getStats(st);

const int w = _display->width();
const int dy = 13;
int y;

int batt_pct = battPercent(st.batt_milli_volts);

// header: node name (large) + battery percent (top-right)
_display->setColor(UIColor::primary_txt);
_display->setTextSize(2);
_display->drawTextEllipsized(2, 0, w - 52, _node_prefs->node_name);

_display->setTextSize(1);
snprintf(line, sizeof(line), "%d%%", batt_pct);
_display->drawTextRightAlign(w - 2, 2, line);

// subtitle: role + page indicator
_display->setColor(UIColor::secondary_txt);
snprintf(line, sizeof(line), "Repeater [%d/%d] %s", _page + 1, (int)NUM_EINK_PAGES, PAGE_NAMES[_page]);
_display->drawTextLeftAlign(2, 20, line);

// divider
_display->fillRect(0, 31, w, 1);

_display->setColor(UIColor::primary_txt);
y = 38;
if (_page == 0) { // ---------- Status ----------
snprintf(line, sizeof(line), "FW: %s", _version_info);
_display->drawTextLeftAlign(4, y, line); y += dy;

char up[24];
fmtUptime(up, sizeof(up), st.total_up_time_secs);
snprintf(line, sizeof(line), "Uptime: %s", up);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Battery: %u mV (%d%%)", st.batt_milli_volts, batt_pct);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Freq: %.3f MHz SF%u", _node_prefs->freq, _node_prefs->sf);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "BW: %.2f kHz CR: 4/%u", _node_prefs->bw, _node_prefs->cr);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "TX: %d dBm Neighbours: %lu",
(int)_node_prefs->tx_power_dbm, (unsigned long)_mesh->getNumNeighbours());
_display->drawTextLeftAlign(4, y, line); y += dy;

} else if (_page == 1) { // ---------- Radio ----------
snprintf(line, sizeof(line), "Last RSSI: %d dBm", st.last_rssi);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Last SNR: %.2f dB", st.last_snr / 4.0f);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Noise floor: %d dBm", st.noise_floor);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "TX queue: %u", st.curr_tx_queue_len);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Recv errors: %lu", (unsigned long)st.n_recv_errors);
_display->drawTextLeftAlign(4, y, line); y += dy;

} else { // ---------- Traffic ----------
snprintf(line, sizeof(line), "Rx: %lu (F:%lu D:%lu)",
(unsigned long)st.n_packets_recv,
(unsigned long)st.n_recv_flood, (unsigned long)st.n_recv_direct);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Tx: %lu (F:%lu D:%lu)",
(unsigned long)st.n_packets_sent,
(unsigned long)st.n_sent_flood, (unsigned long)st.n_sent_direct);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Airtime tx:%lus rx:%lus",
(unsigned long)st.total_air_time_secs, (unsigned long)st.total_rx_air_time_secs);
_display->drawTextLeftAlign(4, y, line); y += dy;

snprintf(line, sizeof(line), "Dups F:%u D:%u Err:%u",
st.n_flood_dups, st.n_direct_dups, st.err_events);
_display->drawTextLeftAlign(4, y, line); y += dy;
}
}

void UITask::renderCurrScreen() {
char tmp[80];
if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen
Expand Down Expand Up @@ -91,7 +216,9 @@ void UITask::renderCurrScreen() {
uint16_t poffWidth = _display->getTextWidth(poweroff_string);
_display->setCursor((_display->width() - poffWidth) / 2, 48);
_display->drawTextCentered(_display->width()/2, 48, poweroff_string);
} else {
} else if (_is_eink) { // large e-ink: rich, multi-page dashboard
renderRichScreen();
} else { // small OLED: compact legacy layout
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(UIColor::primary_txt);
Expand All @@ -113,16 +240,17 @@ void UITask::loop() {
#if defined(PIN_USER_BTN) && defined(DISPLAY_CLASS)
int ev = user_btn.check();
if (ev == BUTTON_EVENT_CLICK) {
if (_display->isOn()) {
// TODO: any action ?
} else {
_display->turnOn();
if (!_display->isOn()) {
_display->turnOn(); // wake a sleeping display
} else if (_is_eink) {
_page = (_page + 1) % NUM_EINK_PAGES; // cycle info pages (Status/Radio/Traffic)
_next_refresh = 0; // redraw immediately
}
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
_display->turnOn();
Serial.println("Powering Off");
_powering_off_at = millis() + POWEROFF_DELAY;
_powering_off_at = millis() + POWEROFF_DELAY;
}
#endif

Expand All @@ -132,9 +260,14 @@ void UITask::loop() {
renderCurrScreen();
_display->endFrame();

_next_refresh = millis() + 1000; // refresh every second
// e-ink content changes slowly (minute granularity) and the driver CRC-gates
// physical updates, so a gentle cadence avoids needless flashing/wear...
unsigned long interval = _is_eink ? EINK_REFRESH_MILLIS : 1000;
// ...but keep the boot splash / power-off countdown responsive.
if (millis() < _started_at + BOOT_SCREEN_MILLIS || _powering_off_at > 0) interval = 500;
_next_refresh = millis() + interval;
}
if (millis() > _auto_off) {
if (AUTO_OFF_MILLIS > 0 && millis() > _auto_off) {
_display->turnOff();
}
}
Expand Down
10 changes: 8 additions & 2 deletions examples/simple_repeater/UITask.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@
#include <helpers/ui/DisplayDriver.h>
#include <helpers/CommonCLI.h>

class MyMesh; // fwd decl (defined in MyMesh.h) - keeps this header light

class UITask {
mesh::MainBoard* _board;
DisplayDriver* _display;
MyMesh* _mesh = nullptr;
unsigned long _next_read, _next_refresh, _auto_off;
int _prevBtnState;
NodePrefs* _node_prefs;
char _version_info[32];
unsigned long _powering_off_at = 0;
unsigned long _started_at = 0;
bool _is_eink = false; // richer, multi-page layout is used on large e-ink panels
uint8_t _page = 0; // currently displayed page (e-ink only)

void renderCurrScreen();
void renderRichScreen(); // large e-ink multi-page dashboard
public:
UITask(mesh::MainBoard& board, DisplayDriver& display) : _board(&board), _display(&display) { _next_read = _next_refresh = 0; }
void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version);
void begin(MyMesh* mesh, NodePrefs* node_prefs, const char* build_date, const char* firmware_version);

void loop();
};
};
2 changes: 1 addition & 1 deletion examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ void setup() {
the_mesh.begin(fs);

#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
ui_task.begin(&the_mesh, the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif

#ifdef ETHERNET_ENABLED
Expand Down
3 changes: 2 additions & 1 deletion variants/heltec_e290/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ build_flags =
-D P_LORA_MISO=11
-D P_LORA_MOSI=10
-D P_LORA_TX_LED=45
-D PIN_USER_BTN=0
-D PIN_USER_BTN=21 ; dedicated "Custom" button (GPIO21); Boot=0, Reset are separate
-D PIN_VEXT_EN=18
-D PIN_VEXT_EN_ACTIVE=HIGH
-D PIN_VBAT_READ=7
Expand Down Expand Up @@ -88,6 +88,7 @@ build_flags =
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D AUTO_OFF_MILLIS=0 ; keep e-ink display live (image persists w/o power anyway)
build_src_filter = ${Heltec_E290_base.build_src_filter}
+<helpers/ui/E290Display.cpp>
+<../examples/simple_repeater>
Expand Down
4 changes: 3 additions & 1 deletion variants/heltec_e290/target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ SensorManager sensors;

#ifdef DISPLAY_CLASS
DISPLAY_CLASS display(&board.periph_power);
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
// pulldownup=true -> INPUT_PULLUP (active-low). The Custom button on GPIO21 has no
// external pull, so it floats and won't register presses without the internal pull-up.
MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true);
#endif

bool radio_init() {
Expand Down