diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c67bb68c9e..b96020eab1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -399,6 +399,30 @@ int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { return max_num; } +int MyMesh::startPing(const ContactInfo& recipient, uint32_t& est_timeout) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) return MSG_SEND_FAILED; // direct path required + uint32_t tag = 0; + int result = sendRequest(recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); + if (result == MSG_SEND_FAILED) return MSG_SEND_FAILED; + clearPendingReqs(); + memcpy(&pending_ping, recipient.id.pub_key, sizeof(pending_ping)); + pending_ping_at_ms = millis(); + _ping_result.ready = false; + return result; +} + +bool MyMesh::consumePingResult(uint8_t* pub_key_prefix, float& snr, int8_t& rssi, float& out_snr, int16_t& out_rssi, uint16_t& rtt_ms) { + if (!_ping_result.ready) return false; + memcpy(pub_key_prefix, _ping_result.pubkey_prefix, sizeof(_ping_result.pubkey_prefix)); + snr = _ping_result.snr; + rssi = _ping_result.rssi; + out_snr = _ping_result.out_snr; + out_rssi = _ping_result.out_rssi; + rtt_ms = _ping_result.rtt_ms; + _ping_result.ready = false; + return true; +} + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); @@ -698,6 +722,22 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, i += 6; // pub_key_prefix } _serial->writeFrame(out_frame, i); + } else if (pending_ping && memcmp(&pending_ping, contact.id.pub_key, sizeof(pending_ping)) == 0) { + pending_ping = 0; + _ping_result.snr = _last_resp_snr; + _ping_result.rssi = _last_resp_rssi; + _ping_result.out_snr = 0; + _ping_result.out_rssi = 0; + if (len >= 48) { // tag(4) + RepeaterStats up through last_snr at offset 42 + int16_t their_rssi, their_snr_x4; + memcpy(&their_rssi, &data[10], sizeof(their_rssi)); + memcpy(&their_snr_x4, &data[46], sizeof(their_snr_x4)); + _ping_result.out_snr = their_snr_x4 / 4.0f; + _ping_result.out_rssi = their_rssi; + } + _ping_result.rtt_ms = (uint16_t)(millis() - pending_ping_at_ms); + memcpy(_ping_result.pubkey_prefix, contact.id.pub_key, sizeof(_ping_result.pubkey_prefix)); + _ping_result.ready = true; } else if (len > 4 && // check for status response pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f6a4ce40e9..9547a3d413 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -101,6 +101,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { void enterCLIRescue(); int getRecentlyHeard(AdvertPath dest[], int max_num); + int startPing(const ContactInfo& recipient, uint32_t& est_timeout); + bool consumePingResult(uint8_t* pub_key_prefix, float& snr, int8_t& rssi, float& out_snr, int16_t& out_rssi, uint16_t& rtt_ms); protected: float getAirtimeBudgetFactor() const override; @@ -160,7 +162,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); } void clearPendingReqs() { - pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0; + pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = pending_ping = 0; } public: @@ -206,6 +208,19 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { uint32_t pending_status; uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ + + uint32_t pending_ping; + unsigned long pending_ping_at_ms; + struct { + uint8_t pubkey_prefix[6]; + bool ready; + float snr; + int8_t rssi; + float out_snr; + int16_t out_rssi; + uint16_t rtt_ms; + } _ping_result; + BaseSerialInterface *_serial; AbstractUITask* _ui; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6f363d7f96..f572aa70fa 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -107,7 +107,11 @@ class HomeScreen : public UIScreen { uint8_t _page; bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - + bool _select_mode = false; + int _select_idx = 0; + unsigned long _select_at_ms = 0; + bool _ping_in_flight = false; + unsigned long _ping_deadline = 0; void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -185,6 +189,26 @@ class HomeScreen : public UIScreen { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); } + if (_select_mode && millis() - _select_at_ms > 10000) { + _select_mode = false; + } + if (_ping_in_flight) { + uint8_t pubkey[6]; + float snr, out_snr; + int8_t rssi; + int16_t out_rssi; + uint16_t rtt_ms; + if (the_mesh.consumePingResult(pubkey, snr, rssi, out_snr, out_rssi, rtt_ms)) { + char buf[80]; + sprintf(buf, "rx SNR %.1f RSSI %d\ntx SNR %.1f RSSI %d %dms", + snr, rssi, out_snr, out_rssi, rtt_ms); + _task->showAlert(buf, 5000); + _ping_in_flight = false; + } else if (millis() > _ping_deadline) { + _task->showAlert("no response", 3000); + _ping_in_flight = false; + } + } } int render(DisplayDriver& display) override { @@ -241,6 +265,12 @@ class HomeScreen : public UIScreen { for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; if (a->name[0] == 0) continue; // empty slot + display.setColor(DisplayDriver::GREEN); + if (_select_mode && i == _select_idx) { + display.setColor(DisplayDriver::YELLOW); + display.drawRect(0, y - 1, display.width(), 11); + display.setColor(DisplayDriver::GREEN); // restore for text + } int secs = _rtc->getCurrentTime() - a->recv_timestamp; if (secs < 60) { sprintf(tmp, "%ds", secs); @@ -414,6 +444,35 @@ class HomeScreen : public UIScreen { } bool handleInput(char c) override { + if (_page == HomePage::RECENT && _select_mode) { + if (c == KEY_NEXT || c == KEY_RIGHT) { + for (int i = 1; i <= UI_RECENT_LIST_SIZE; i++) { + int idx = (_select_idx + i) % UI_RECENT_LIST_SIZE; + if (recent[idx].name[0] != 0) { _select_idx = idx; break; } + } + _select_at_ms = millis(); + return true; + } + if (c == KEY_ENTER) { + auto* contact = the_mesh.lookupContactByPubKey(recent[_select_idx].pubkey_prefix, 6); + if (contact == NULL) { + _task->showAlert("not in contacts", 2000); + } else { + uint32_t est_timeout = 0; + int result = the_mesh.startPing(*contact, est_timeout); + if (result == MSG_SEND_FAILED) { + _task->showAlert("no direct path", 2000); + } else { + _task->showAlert("Pinging...", 800); + _ping_in_flight = true; + _ping_deadline = millis() + 7000; + } + } + _select_mode = false; + return true; + } + return false; + } if (c == KEY_LEFT || c == KEY_PREV) { _page = (_page + HomePage::Count - 1) % HomePage::Count; return true; @@ -433,6 +492,16 @@ class HomeScreen : public UIScreen { } return true; } + if (c == KEY_ENTER && _page == HomePage::RECENT) { + for (int i = 0; i < UI_RECENT_LIST_SIZE; i++) { + if (recent[i].name[0] != 0) { + _select_idx = i; + _select_mode = true; + _select_at_ms = millis(); + return true; + } + } + } if (c == KEY_ENTER && _page == HomePage::ADVERT) { _task->notify(UIEventType::ack); if (the_mesh.advert()) { @@ -592,8 +661,21 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no } void UITask::showAlert(const char* text, int duration_millis) { - strcpy(_alert, text); + strncpy(_alert, text, sizeof(_alert) - 1); + _alert[sizeof(_alert) - 1] = '\0'; + _alert_line_count = 1; + for (char* p = _alert; *p; p++) { + if (*p == '\n') { + *p = '\0'; + _alert_line_count++; + } + } _alert_expiry = millis() + duration_millis; + if (_display != NULL && !_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; } void UITask::notify(UIEventType t) { @@ -797,11 +879,19 @@ void UITask::loop() { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; + const int line_h = 12; + int box_h = y; + int needed = p*6 + line_h * _alert_line_count; + if (needed > box_h) box_h = needed; _display->setColor(DisplayDriver::DARK); - _display->fillRect(p, y, _display->width() - p*2, y); + _display->fillRect(p, y, _display->width() - p*2, box_h); _display->setColor(DisplayDriver::LIGHT); // draw box border - _display->drawRect(p, y, _display->width() - p*2, y); - _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); + _display->drawRect(p, y, _display->width() - p*2, box_h); + const char* line = _alert; + for (uint8_t i = 0; i < _alert_line_count; i++) { + _display->drawTextCentered(_display->width() / 2, y + p*3 + i*line_h, line); + line += strlen(line) + 1; // step past the '\0' separator + } _next_refresh = _alert_expiry; // will need refresh when alert is dismissed } else { _next_refresh = millis() + delay_millis; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7ec..f30c7aaeb7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -35,6 +35,7 @@ class UITask : public AbstractUITask { NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; + uint8_t _alert_line_count; int _msgcount; unsigned long ui_started_at, next_batt_chck; int next_backlight_btn_check = 0; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 9d7a11131d..a9bbabd0c2 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -203,6 +203,7 @@ void Dispatcher::checkRecv() { } else { if (tryParsePacket(pkt, raw, len)) { pkt->_snr = _radio->getLastSNR() * 4.0f; + pkt->_rssi = _radio->getLastRSSI(); score = _radio->packetScore(_radio->getLastSNR(), len); air_time = _radio->getEstAirtimeFor(len); rx_air_time += air_time; @@ -359,6 +360,7 @@ Packet* Dispatcher::obtainNewPacket() { } else { pkt->payload_len = pkt->path_len = 0; pkt->_snr = 0; + pkt->_rssi = 0; } return pkt; } diff --git a/src/Packet.h b/src/Packet.h index 0886a06c4e..20bf61a5e6 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -49,6 +49,7 @@ class Packet { uint8_t path[MAX_PATH_SIZE]; uint8_t payload[MAX_PACKET_PAYLOAD]; int8_t _snr; + int8_t _rssi; /** * \brief calculate the hash of payload + type @@ -90,6 +91,7 @@ class Packet { bool isMarkedDoNotRetransmit() const { return header == 0xFF; } float getSNR() const { return ((float)_snr) / 4.0f; } + int8_t getRSSI() const { return _rssi; } /** * \returns the encoded/wire format length of this packet diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 7ddc461d29..8da7837c5c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -281,6 +281,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { + _last_resp_snr = packet->getSNR(); + _last_resp_rssi = packet->getRSSI(); onContactResponse(from, data, len); if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) { // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) @@ -298,6 +300,8 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui ContactInfo& from = contacts[i]; + _last_resp_snr = packet->getSNR(); + _last_resp_rssi = packet->getRSSI(); return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len); } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index b39e736388..a781ca7628 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -75,6 +75,9 @@ class BaseChatMesh : public mesh::Mesh { void sendAckTo(const ContactInfo& dest, uint32_t ack_hash); protected: + float _last_resp_snr = 0; + int8_t _last_resp_rssi = 0; + BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) : mesh::Mesh(radio, ms, rng, rtc, mgr, tables) {