mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
BIN
docs/solo_features/tools_screen/tls_scr_2_ping_oled.png
Normal file
BIN
docs/solo_features/tools_screen/tls_scr_2_ping_oled.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -31,7 +31,15 @@ Browse nodes that have recently advertised on the mesh. Filter by category with
|
||||
|
||||
Select a node to see its coordinates, distance, bearing with cardinal direction, type, and last-heard time.
|
||||
|
||||
**Hold Enter** → context menu → **Discover nearby** to send a live `NODE_DISCOVER_REQ` ping.
|
||||
From the list view, **Hold Enter** opens the context menu, where **Discover nearby** sends a live `NODE_DISCOVER_REQ` scan.
|
||||
|
||||
From either node detail view, **Hold Enter** opens the Ping popup:
|
||||
|
||||
| OLED |
|
||||
| :-----------------------: |
|
||||
|  |
|
||||
|
||||
Use **Enter** on the popup’s `Ping` row to send a direct mesh ping to that node. The popup then shows the RTT and SNR values on the next lines, and can be used again immediately for another ping.
|
||||
|
||||
> [!TIP]
|
||||
> Combined with **Auto-Advert** on the other device, Nearby Nodes becomes a passive location tracker — as long as the tracked device periodically broadcasts its GPS position, you can see its current distance and bearing without any manual interaction on either end.
|
||||
@@ -51,6 +59,7 @@ Results show as 2-line cards: node name + type, then RSSI / SNR / remote SNR.
|
||||
- **UP/DOWN** — navigate results
|
||||
- **Enter** — open full-screen detail (public key, signal data, contact status)
|
||||
- **Hold Enter** — rescan
|
||||
- **Hold Enter** in full-screen detail — open the Ping popup for the selected node
|
||||
- **Cancel / Back** — return to nearby list
|
||||
|
||||
---
|
||||
|
||||
@@ -799,6 +799,79 @@ int MyMesh::getDiscoverResults(DiscoverResult dest[], int max_count) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── Ping/Trace functionality ─────────────────────────────────────────────────
|
||||
|
||||
uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width) {
|
||||
if (hash_width == 0 || hash_width > 2) return 0;
|
||||
|
||||
// Generate random tag and auth code
|
||||
uint32_t tag, auth;
|
||||
getRNG()->random((uint8_t*)&tag, 4);
|
||||
getRNG()->random((uint8_t*)&auth, 4);
|
||||
|
||||
// Find a free slot in ping results
|
||||
int slot = -1;
|
||||
for (int i = 0; i < PING_RESULT_MAX; i++) {
|
||||
if (!_ping_results[i].received && _ping_results[i].tag == 0) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Initialize ping result tracking
|
||||
PingResult& result = _ping_results[slot];
|
||||
result.tag = tag;
|
||||
result.auth_code = auth;
|
||||
result.snr_out_x4 = 0;
|
||||
result.snr_back_x4 = 0;
|
||||
result.rtt_ms = 0;
|
||||
result.received = false;
|
||||
result.sent_ms = millis();
|
||||
|
||||
// Create path hash from destination public key
|
||||
uint8_t path_len = hash_width;
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
memcpy(path, dest_pubkey, path_len);
|
||||
|
||||
// Create and send trace packet
|
||||
auto pkt = createTrace(tag, auth, hash_width - 1); // flags = hash_width - 1
|
||||
if (pkt) {
|
||||
sendDirect(pkt, path, path_len);
|
||||
return tag;
|
||||
}
|
||||
|
||||
// Failed to create packet
|
||||
memset(&result, 0, sizeof(result));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void MyMesh::setPingCallback(PingCallback cb, void* arg) {
|
||||
_ping_callback = cb;
|
||||
_ping_callback_arg = arg;
|
||||
}
|
||||
|
||||
void MyMesh::clearPingResult(uint32_t tag) {
|
||||
if (tag == 0) return;
|
||||
for (int i = 0; i < PING_RESULT_MAX; i++) {
|
||||
if (_ping_results[i].tag == tag) {
|
||||
memset(&_ping_results[i], 0, sizeof(_ping_results[i]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MyMesh::PingResult* MyMesh::getPingResult(uint32_t tag) {
|
||||
for (int i = 0; i < PING_RESULT_MAX; i++) {
|
||||
if (_ping_results[i].tag == tag) {
|
||||
return &_ping_results[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void MyMesh::onControlDataRecv(mesh::Packet *packet) {
|
||||
// If we have an active standalone discover, check if this is a matching response.
|
||||
// Tag matching provides isolation — no isBLEConnected check needed.
|
||||
@@ -879,6 +952,49 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) {
|
||||
void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags,
|
||||
const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) {
|
||||
uint8_t path_sz = flags & 0x03; // NEW v1.11+
|
||||
|
||||
// Check if this is a response to our local ping
|
||||
for (int i = 0; i < PING_RESULT_MAX; i++) {
|
||||
if (_ping_results[i].tag == tag && _ping_results[i].auth_code == auth_code && !_ping_results[i].received) {
|
||||
PingResult& result = _ping_results[i];
|
||||
|
||||
// Extract SNR values
|
||||
// path_snrs contains signed SNR values in dB×4 for each hop (in order).
|
||||
// The last value is the SNR at the destination hearing our request (snr_out).
|
||||
// The final SNR from packet is the SNR at us hearing the response (snr_back).
|
||||
uint8_t snr_count = path_len >> path_sz;
|
||||
|
||||
if (snr_count > 0) {
|
||||
// Keep the encoded quarter-dB value; the UI converts it back to dB.
|
||||
result.snr_out_x4 = (int16_t)(int8_t)path_snrs[0];
|
||||
} else {
|
||||
result.snr_out_x4 = 0;
|
||||
}
|
||||
|
||||
// SNR back is from the final SNR in the packet (SNR at us receiving response).
|
||||
result.snr_back_x4 = (int16_t)(packet->getSNR() * 4);
|
||||
|
||||
// Calculate RTT
|
||||
unsigned long now = millis();
|
||||
if (now >= result.sent_ms) {
|
||||
result.rtt_ms = now - result.sent_ms;
|
||||
} else {
|
||||
result.rtt_ms = 0xFFFFFFFF; // Overflow
|
||||
}
|
||||
|
||||
result.received = true;
|
||||
|
||||
// Notify callback if set
|
||||
if (_ping_callback) {
|
||||
_ping_callback(tag, result.snr_out_x4, result.snr_back_x4, result.rtt_ms);
|
||||
}
|
||||
// The UI copies the result out of the callback, so reuse the slot immediately.
|
||||
memset(&result, 0, sizeof(result));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to serial app regardless (for compatibility)
|
||||
if (12 + path_len + (path_len >> path_sz) + 1 > sizeof(out_frame)) {
|
||||
MESH_DEBUG_PRINTLN("onTraceRecv(), path_len is too long: %d", (uint32_t)path_len);
|
||||
return;
|
||||
@@ -938,6 +1054,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_pending_node_discover_tag = 0;
|
||||
_pending_node_discover_until = 0;
|
||||
|
||||
// Ping state
|
||||
_ping_callback = NULL;
|
||||
_ping_callback_arg = NULL;
|
||||
memset(_ping_results, 0, sizeof(_ping_results));
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
@@ -2070,7 +2191,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
#ifdef ENABLE_SCREENSHOT
|
||||
void MyMesh::handleScreenshotRequest() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
UITask* ui_task = getUITask();
|
||||
UITask* ui_task = static_cast<UITask*>(getUITask());
|
||||
if (!ui_task || !ui_task->hasDisplay()) {
|
||||
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
|
||||
return;
|
||||
|
||||
@@ -122,6 +122,27 @@ public:
|
||||
int getRecentlyHeard(AdvertPath dest[], int max_num);
|
||||
int getDiscoverResults(DiscoverResult dest[], int max_count);
|
||||
|
||||
// Ping/Trace functionality
|
||||
#define PING_RESULT_MAX 4
|
||||
typedef void (*PingCallback)(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms);
|
||||
|
||||
struct PingResult {
|
||||
uint32_t tag;
|
||||
uint32_t auth_code;
|
||||
int16_t snr_out_x4; // SNR out (to first hop) × 4
|
||||
int16_t snr_back_x4; // SNR back (from last hop to us) × 4
|
||||
uint32_t rtt_ms; // Round-trip time in milliseconds
|
||||
bool received;
|
||||
unsigned long sent_ms;
|
||||
};
|
||||
|
||||
uint32_t sendPing(const uint8_t* dest_pubkey, uint8_t hash_width = 1);
|
||||
void setPingCallback(PingCallback cb, void* arg);
|
||||
void clearPingResult(uint32_t tag);
|
||||
PingResult* getPingResult(uint32_t tag);
|
||||
PingCallback getPingCallback() const { return _ping_callback; }
|
||||
AbstractUITask* getUITask() const { return _ui; }
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
int getInterferenceThreshold() const override;
|
||||
@@ -232,8 +253,6 @@ private:
|
||||
void sendScreenshotResponse(DisplayDriver* display, const uint8_t* buffer, uint16_t bufferSize);
|
||||
#endif
|
||||
|
||||
UITask* getUITask() { return (UITask*)_ui; }
|
||||
|
||||
// helpers, short-cuts
|
||||
void saveChannels() { _store->saveChannels(this); }
|
||||
void saveContacts() { _store->saveContacts(this); }
|
||||
@@ -293,6 +312,11 @@ private:
|
||||
int _discover_count;
|
||||
uint32_t _pending_node_discover_tag;
|
||||
unsigned long _pending_node_discover_until;
|
||||
|
||||
// ── Ping/Trace state ──────────────────────────────────────────────────────
|
||||
PingResult _ping_results[PING_RESULT_MAX];
|
||||
PingCallback _ping_callback;
|
||||
void* _ping_callback_arg;
|
||||
};
|
||||
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
@@ -38,6 +38,15 @@ class NearbyScreen : public UIScreen {
|
||||
static const unsigned long DETAIL_REFRESH_MS = 10000UL;
|
||||
|
||||
PopupMenu _ctx_menu;
|
||||
PopupMenu _ping_menu;
|
||||
char _ping_time_str[24]; // For storing ping RTT result
|
||||
char _ping_snr_out_str[24]; // For storing ping SNR out result
|
||||
char _ping_snr_back_str[24];// For storing ping SNR back result
|
||||
|
||||
// ── ping state ─────────────────────────────────────────────────────────────
|
||||
bool _pinging;
|
||||
unsigned long _ping_started_ms;
|
||||
static const unsigned long PING_TIMEOUT_MS = 3000UL;
|
||||
|
||||
// ── discover sub-screen state ────────────────────────────────────────────────
|
||||
bool _discover_mode;
|
||||
@@ -186,6 +195,204 @@ class NearbyScreen : public UIScreen {
|
||||
the_mesh.sendNodeDiscoverReq();
|
||||
}
|
||||
|
||||
void resetPingLines() {
|
||||
_ping_time_str[0] = '\0';
|
||||
_ping_snr_out_str[0] = '\0';
|
||||
_ping_snr_back_str[0] = '\0';
|
||||
}
|
||||
|
||||
void openPingMenu() {
|
||||
_ping_menu.begin("Ping", 4);
|
||||
_ping_menu.addItem("Ping");
|
||||
_ping_menu.addItem(_ping_time_str);
|
||||
_ping_menu.addItem(_ping_snr_out_str);
|
||||
_ping_menu.addItem(_ping_snr_back_str);
|
||||
}
|
||||
|
||||
bool renderPingMenuIfActive(DisplayDriver& display) {
|
||||
if (!_ping_menu.active) return false;
|
||||
_ping_menu.render(display);
|
||||
return true;
|
||||
}
|
||||
|
||||
void closePingMenu(bool clear_task = true) {
|
||||
_ping_menu.active = false;
|
||||
_pinging = false;
|
||||
if (clear_task && _task) _task->clearPing();
|
||||
resetPingLines();
|
||||
}
|
||||
|
||||
void startPingForKey(const uint8_t* pub_key) {
|
||||
resetPingLines();
|
||||
snprintf(_ping_time_str, sizeof(_ping_time_str), "Ping: ...");
|
||||
if (_task && _task->startPing(pub_key)) {
|
||||
_pinging = true;
|
||||
_ping_started_ms = millis();
|
||||
} else {
|
||||
_ping_time_str[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
void updatePingMenuState() {
|
||||
if (!_ping_menu.active || !_task || (!_pinging && !_task->isPingActive())) return;
|
||||
|
||||
int16_t snr_out = 0, snr_back = 0;
|
||||
uint32_t rtt = 0;
|
||||
_task->getPingResult(snr_out, snr_back, rtt);
|
||||
|
||||
if (_pinging && (snr_out != 0 || snr_back != 0 || rtt != 0)) {
|
||||
if (rtt > 0 && rtt < 10000) {
|
||||
snprintf(_ping_time_str, sizeof(_ping_time_str), "Ping: %lumS", rtt);
|
||||
} else {
|
||||
snprintf(_ping_time_str, sizeof(_ping_time_str), "Ping: timeout");
|
||||
}
|
||||
if (snr_out != 0) {
|
||||
snprintf(_ping_snr_out_str, sizeof(_ping_snr_out_str), "SNR out: %.1f", snr_out / 4.0f);
|
||||
}
|
||||
if (snr_back != 0) {
|
||||
snprintf(_ping_snr_back_str, sizeof(_ping_snr_back_str), "SNR back: %.1f", snr_back / 4.0f);
|
||||
}
|
||||
_pinging = false;
|
||||
} else if (_pinging && millis() - _ping_started_ms >= PING_TIMEOUT_MS) {
|
||||
snprintf(_ping_time_str, sizeof(_ping_time_str), "Ping: timeout");
|
||||
_ping_snr_out_str[0] = '\0';
|
||||
_ping_snr_back_str[0] = '\0';
|
||||
_pinging = false;
|
||||
if (_task) _task->clearPing();
|
||||
}
|
||||
}
|
||||
|
||||
bool handlePingMenuInput(char c, const uint8_t* pub_key, bool allow_enter_to_open = false) {
|
||||
if (!_ping_menu.active) {
|
||||
if (c == KEY_CONTEXT_MENU || (allow_enter_to_open && c == KEY_ENTER)) {
|
||||
openPingMenu();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto res = _ping_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
if (!_pinging && pub_key) {
|
||||
startPingForKey(pub_key);
|
||||
}
|
||||
// Keep the popup open so Ping stays available after completion.
|
||||
_ping_menu.active = true;
|
||||
} else if (res == PopupMenu::CANCELLED) {
|
||||
closePingMenu();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool selectedStoredPubKey(uint8_t* out_pub_key) const {
|
||||
ContactInfo ci;
|
||||
if (_task && _sel < _count && _entries[_sel].contact_idx >= 0 &&
|
||||
the_mesh.getContactByIdx(_entries[_sel].contact_idx, ci)) {
|
||||
memcpy(out_pub_key, ci.id.pub_key, PUB_KEY_SIZE);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t* selectedDiscoverPubKey() const {
|
||||
return (_dsel < _dresult_count) ? _dresults[_dsel].pub_key : nullptr;
|
||||
}
|
||||
|
||||
bool renderDiscoverDetail(DisplayDriver& display) {
|
||||
const DiscoverResult& r = _dresults[_dsel];
|
||||
const int hdr = display.headerH();
|
||||
const char* fullType = (r.type == ADV_TYPE_REPEATER) ? "Repeater" :
|
||||
(r.type == ADV_TYPE_SENSOR) ? "Sensor" :
|
||||
(r.type == ADV_TYPE_ROOM) ? "Room" : "Node";
|
||||
|
||||
char label[32];
|
||||
if (r.name[0]) { strncpy(label, r.name, 31); label[31] = '\0'; }
|
||||
else { snprintf(label, sizeof(label), "[%s]", fullType); }
|
||||
char filtered[32];
|
||||
display.translateUTF8ToBlocks(filtered, label, sizeof(filtered));
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, 0, display.width(), hdr - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, hdr - 1, display.width(), 1);
|
||||
|
||||
char b64[48];
|
||||
pubKeyToBase64(r.pub_key, b64, sizeof(b64));
|
||||
{
|
||||
int max_chars = (display.width() - 4) / display.getCharWidth();
|
||||
int b64_len = strlen(b64);
|
||||
char b64_line[48];
|
||||
if (b64_len > max_chars) {
|
||||
strncpy(b64_line, b64, max_chars - 3);
|
||||
b64_line[max_chars - 3] = '\0';
|
||||
strcat(b64_line, "...");
|
||||
} else {
|
||||
strncpy(b64_line, b64, sizeof(b64_line) - 1);
|
||||
b64_line[sizeof(b64_line) - 1] = '\0';
|
||||
}
|
||||
display.setCursor(2, hdr);
|
||||
display.print(b64_line);
|
||||
}
|
||||
|
||||
int step = display.lineStep();
|
||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)r.rssi);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "SNR: %d dB", (int)(r.snr_x4 / 4));
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Rem: %d dB", (int)(r.remote_snr_x4 / 4));
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
display.setCursor(2, hdr + step * 4);
|
||||
display.print(r.is_known ? "Status: known" : "Status: new");
|
||||
|
||||
updatePingMenuState();
|
||||
if (renderPingMenuIfActive(display)) return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool renderStoredDetail(DisplayDriver& display) {
|
||||
const Entry& e = _entries[_sel];
|
||||
const int hdr = display.headerH();
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, 0, display.width(), hdr - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
char filtered[32];
|
||||
display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered));
|
||||
display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
int step = display.lineStep();
|
||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||
display.setCursor(2, hdr); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
|
||||
if (e.dist_km >= 0.0f) {
|
||||
char dist[12];
|
||||
fmtDist(dist, sizeof(dist), e.dist_km);
|
||||
int az = bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6);
|
||||
snprintf(buf, sizeof(buf), "Dist: %s %s", dist, bearingCardinal(az));
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Dist: no GPS");
|
||||
}
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "Type: %s", typeName(e.type));
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
char age[16];
|
||||
fmtAge(age, sizeof(age), e.lastmod);
|
||||
snprintf(buf, sizeof(buf), "Seen: %s", age);
|
||||
display.drawTextEllipsized(2, hdr + step * 4, display.width() - 4, buf);
|
||||
|
||||
updatePingMenuState();
|
||||
if (renderPingMenuIfActive(display)) return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int renderDiscover(DisplayDriver& display) {
|
||||
int lh = display.getLineHeight();
|
||||
int hdr = display.headerH();
|
||||
@@ -196,61 +403,8 @@ class NearbyScreen : public UIScreen {
|
||||
if (_d_visible < 1) _d_visible = 1;
|
||||
|
||||
if (_ddetail) {
|
||||
// ── full-screen detail for selected node ──────────────────────────────
|
||||
const DiscoverResult& r = _dresults[_dsel];
|
||||
const char* fullType = (r.type == ADV_TYPE_REPEATER) ? "Repeater" :
|
||||
(r.type == ADV_TYPE_SENSOR) ? "Sensor" :
|
||||
(r.type == ADV_TYPE_ROOM) ? "Room" : "Node";
|
||||
|
||||
// title bar: node name inverted
|
||||
char label[32];
|
||||
if (r.name[0]) { strncpy(label, r.name, 31); label[31] = '\0'; }
|
||||
else { snprintf(label, sizeof(label), "[%s]", fullType); }
|
||||
char filtered[32];
|
||||
display.translateUTF8ToBlocks(filtered, label, sizeof(filtered));
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, 0, display.width(), hdr - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, hdr - 1, display.width(), 1);
|
||||
|
||||
// public key as base64 — truncate to one line via charWidth
|
||||
char b64[48];
|
||||
pubKeyToBase64(r.pub_key, b64, sizeof(b64));
|
||||
{
|
||||
int max_chars = (display.width() - 4) / display.getCharWidth();
|
||||
int b64_len = strlen(b64);
|
||||
char b64_line[48];
|
||||
if (b64_len > max_chars) {
|
||||
strncpy(b64_line, b64, max_chars - 3);
|
||||
b64_line[max_chars - 3] = '\0';
|
||||
strcat(b64_line, "...");
|
||||
} else {
|
||||
strncpy(b64_line, b64, sizeof(b64_line) - 1);
|
||||
b64_line[sizeof(b64_line) - 1] = '\0';
|
||||
}
|
||||
display.setCursor(2, hdr);
|
||||
display.print(b64_line);
|
||||
}
|
||||
|
||||
// distribute 4 remaining lines below b64 — compact step, shrink only if needed
|
||||
int step = display.lineStep();
|
||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)r.rssi);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "SNR: %d dB", (int)(r.snr_x4 / 4));
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Rem: %d dB", (int)(r.remote_snr_x4 / 4));
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
|
||||
display.setCursor(2, hdr + step * 4);
|
||||
display.print(r.is_known ? "Status: known" : "Status: new");
|
||||
|
||||
return 5000;
|
||||
renderDiscoverDetail(display);
|
||||
return _ping_menu.active ? 50 : 5000;
|
||||
}
|
||||
|
||||
// ── list view ─────────────────────────────────────────────────────────────
|
||||
@@ -330,16 +484,26 @@ class NearbyScreen : public UIScreen {
|
||||
{ display.setCursor(display.width() - cw, d_start_y + (_d_visible - 1) * d_item_h); display.print("v"); }
|
||||
}
|
||||
|
||||
updatePingMenuState();
|
||||
|
||||
// Show ping popup menu if active
|
||||
if (_ping_menu.active) {
|
||||
_ping_menu.render(display);
|
||||
return 50;
|
||||
}
|
||||
|
||||
return _discovering ? 200 : 2000;
|
||||
}
|
||||
|
||||
bool handleInputDiscover(char c) {
|
||||
if (_ddetail) {
|
||||
if (c == KEY_CANCEL) { _ddetail = false; return true; }
|
||||
if (c == KEY_CONTEXT_MENU || c == KEY_ENTER) {
|
||||
enterDiscoverMode(); // re-scan; clears _ddetail
|
||||
return true;
|
||||
if (c == KEY_CANCEL) {
|
||||
_ddetail = false;
|
||||
closePingMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (handlePingMenuInput(c, selectedDiscoverPubKey())) return true;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL) {
|
||||
@@ -374,7 +538,12 @@ public:
|
||||
_own_lat(0), _own_lon(0), _own_gps(false), _filter(0),
|
||||
_detail_refresh_ms(0),
|
||||
_discover_mode(false), _discovering(false), _discover_started_ms(0),
|
||||
_dresult_count(0), _dscroll(0), _dsel(0), _ddetail(false) {}
|
||||
_dresult_count(0), _dscroll(0), _dsel(0), _ddetail(false),
|
||||
_pinging(false), _ping_started_ms(0) {
|
||||
_ping_time_str[0] = '\0';
|
||||
_ping_snr_out_str[0] = '\0';
|
||||
_ping_snr_back_str[0] = '\0';
|
||||
}
|
||||
|
||||
void enter() {
|
||||
_sel = _scroll = 0;
|
||||
@@ -384,6 +553,12 @@ public:
|
||||
_ddetail = false;
|
||||
_dsel = 0;
|
||||
_ctx_menu.active = false;
|
||||
_ping_menu.active = false;
|
||||
_pinging = false;
|
||||
_ping_time_str[0] = '\0';
|
||||
_ping_snr_out_str[0] = '\0';
|
||||
_ping_snr_back_str[0] = '\0';
|
||||
_task->clearPing();
|
||||
refresh();
|
||||
}
|
||||
|
||||
@@ -409,46 +584,8 @@ public:
|
||||
|
||||
// ── detail view ──────────────────────────────────────────────────────────
|
||||
if (_detail && _sel < _count) {
|
||||
const Entry& e = _entries[_sel];
|
||||
int hdr = display.headerH();
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, 0, display.width(), hdr - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
char filtered[32];
|
||||
display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered));
|
||||
display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// 5 lines: lat, lon, dist+bearing, type, seen — compact step, shrink only if needed
|
||||
int step = display.lineStep();
|
||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||
display.setCursor(2, hdr); display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
|
||||
if (e.dist_km >= 0.0f) {
|
||||
char dist[12];
|
||||
fmtDist(dist, sizeof(dist), e.dist_km);
|
||||
int az = bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6);
|
||||
snprintf(buf, sizeof(buf), "Dist: %s %s", dist, bearingCardinal(az));
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), "Dist: no GPS");
|
||||
}
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Type: %s", typeName(e.type));
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
|
||||
char age[16];
|
||||
fmtAge(age, sizeof(age), e.lastmod);
|
||||
snprintf(buf, sizeof(buf), "Seen: %s", age);
|
||||
display.setCursor(2, hdr + step * 4); display.print(buf);
|
||||
|
||||
return 2000;
|
||||
renderStoredDetail(display);
|
||||
return _ping_menu.active ? 50 : 2000;
|
||||
}
|
||||
|
||||
// ── list view ────────────────────────────────────────────────────────────
|
||||
@@ -509,7 +646,14 @@ public:
|
||||
|
||||
// ── detail view ─────────────────────────────────────────────────────────
|
||||
if (_detail) {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _detail = false; return true; }
|
||||
if (c == KEY_CANCEL) {
|
||||
_detail = false;
|
||||
closePingMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t pub_key[PUB_KEY_SIZE];
|
||||
if (selectedStoredPubKey(pub_key) && handlePingMenuInput(c, pub_key)) return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1113,6 +1113,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
ui_started_at = millis();
|
||||
_alert_expiry = 0;
|
||||
_batt_mv = AbstractUITask::getBattMilliVolts(); // seed EMA with first reading
|
||||
|
||||
// Initialize ping state
|
||||
_ping_active = false;
|
||||
_ping_tag = 0;
|
||||
_ping_sent_ms = 0;
|
||||
_ping_snr_out_x4 = 0;
|
||||
_ping_snr_back_x4 = 0;
|
||||
_ping_rtt_ms = 0;
|
||||
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
@@ -1171,6 +1179,58 @@ void UITask::gotoAutoAdvertScreen() {
|
||||
setCurrScreen(auto_advert_screen);
|
||||
}
|
||||
|
||||
// Public method to handle ping result callback
|
||||
void UITask::handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) {
|
||||
if (_ping_active && _ping_tag == tag) {
|
||||
_ping_snr_out_x4 = snr_out_x4;
|
||||
_ping_snr_back_x4 = snr_back_x4;
|
||||
_ping_rtt_ms = rtt_ms;
|
||||
// Release the in-flight slot immediately; the UI keeps the result values.
|
||||
clearPing();
|
||||
}
|
||||
}
|
||||
|
||||
// Static ping callback (for MyMesh)
|
||||
static void onPingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) {
|
||||
AbstractUITask* ui = the_mesh.getUITask();
|
||||
if (ui) {
|
||||
UITask* task = static_cast<UITask*>(ui);
|
||||
task->handlePingResult(tag, snr_out_x4, snr_back_x4, rtt_ms);
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::clearPing() {
|
||||
if (_ping_tag != 0) {
|
||||
the_mesh.clearPingResult(_ping_tag);
|
||||
}
|
||||
_ping_active = false;
|
||||
_ping_tag = 0;
|
||||
}
|
||||
|
||||
bool UITask::startPing(const uint8_t* pub_key) {
|
||||
if (_ping_active || !pub_key) return false;
|
||||
if (_node_prefs && _node_prefs->path_hash_mode > 1) {
|
||||
showAlert("Ping not supported with 3-byte path hashes", 3000);
|
||||
return false;
|
||||
}
|
||||
|
||||
_ping_active = true;
|
||||
_ping_tag = 0;
|
||||
_ping_sent_ms = millis();
|
||||
_ping_snr_out_x4 = 0;
|
||||
_ping_snr_back_x4 = 0;
|
||||
_ping_rtt_ms = 0;
|
||||
|
||||
// Always install the callback before sending so the response cannot race it.
|
||||
the_mesh.setPingCallback(onPingResult, NULL);
|
||||
_ping_tag = the_mesh.sendPing(pub_key, _node_prefs ? _node_prefs->path_hash_mode + 1 : 1);
|
||||
if (_ping_tag == 0) {
|
||||
clearPing();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UITask::playMelody(const char* melody) {
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.playForced(melody);
|
||||
@@ -1849,13 +1909,11 @@ char UITask::handleLongPress(char c) {
|
||||
}
|
||||
|
||||
char UITask::handleDoubleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: double click triggered");
|
||||
checkDisplayOn(c);
|
||||
return c;
|
||||
}
|
||||
|
||||
char UITask::handleTripleClick(char c) {
|
||||
MESH_DEBUG_PRINTLN("UITask: triple click triggered");
|
||||
checkDisplayOn(c);
|
||||
toggleBuzzer();
|
||||
return 0;
|
||||
|
||||
@@ -80,6 +80,14 @@ class UITask : public AbstractUITask {
|
||||
TrailStore _trail;
|
||||
uint32_t _next_trail_sample_ms = 0;
|
||||
|
||||
// Ping state
|
||||
bool _ping_active = false;
|
||||
uint32_t _ping_tag = 0;
|
||||
unsigned long _ping_sent_ms = 0;
|
||||
int16_t _ping_snr_out_x4 = 0;
|
||||
int16_t _ping_snr_back_x4 = 0;
|
||||
uint32_t _ping_rtt_ms = 0;
|
||||
|
||||
void userLedHandler();
|
||||
|
||||
// Button action handlers
|
||||
@@ -149,6 +157,17 @@ public:
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
DisplayDriver* getDisplay() const { return _display; }
|
||||
|
||||
// Ping helpers
|
||||
bool startPing(const uint8_t* pub_key);
|
||||
bool isPingActive() const { return _ping_active; }
|
||||
void getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const {
|
||||
snr_out_x4 = _ping_snr_out_x4;
|
||||
snr_back_x4 = _ping_snr_back_x4;
|
||||
rtt_ms = _ping_rtt_ms;
|
||||
}
|
||||
void clearPing();
|
||||
void handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms);
|
||||
|
||||
// Favourites dial helpers. Slot index 0..FAVOURITES_COUNT-1.
|
||||
int findFavouriteSlot(const uint8_t* pub_key) const {
|
||||
if (!_node_prefs || !pub_key) return -1;
|
||||
|
||||
Reference in New Issue
Block a user