diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 8b5e74e..dc01694 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -51,4 +51,5 @@ public: // Repeater admin callbacks (from MyMesh) virtual void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {} virtual void onAdminCliResponse(const char* from_name, const char* text) {} + virtual void onAdminTelemetryResult(const uint8_t* data, uint8_t len) {} }; \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c24f20f..359bc7e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -706,6 +706,29 @@ bool MyMesh::uiSendCliCommand(uint32_t contact_idx, const char* command) { return true; } +bool MyMesh::uiSendTelemetryRequest(uint32_t contact_idx) { + ContactInfo contact; + if (!getContactByIdx(contact_idx, contact)) return false; + + ContactInfo* recipient = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE); + if (!recipient) return false; + + uint32_t tag, est_timeout; + int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); + if (result == MSG_SEND_FAILED) { + MESH_DEBUG_PRINTLN("UI: Telemetry request send failed to %s", recipient->name); + return false; + } + + clearPendingReqs(); + pending_telemetry = tag; + + MESH_DEBUG_PRINTLN("UI: Telemetry request sent to %s (%s), timeout=%dms", + recipient->name, result == MSG_SEND_SENT_FLOOD ? "flood" : "direct", + est_timeout); + return true; +} + uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, uint8_t len, uint8_t *reply) { if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { @@ -816,6 +839,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, _serial->writeFrame(out_frame, i); } else if (len > 4 && tag == pending_telemetry) { // check for matching response tag pending_telemetry = 0; + MESH_DEBUG_PRINTLN("Telemetry response received from %s, len=%d", contact.name, len); int i = 0; out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; @@ -825,6 +849,11 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + + #ifdef DISPLAY_CLASS + // Route telemetry data to UI (LPP buffer after the 4-byte tag) + if (_ui) _ui->onAdminTelemetryResult(&data[4], len - 4); + #endif } else if (len > 4 && tag == pending_req) { // check for matching response tag pending_req = 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index cc0cb45..a165f9c 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -111,6 +111,7 @@ public: // Repeater admin - UI-initiated operations bool uiLoginToRepeater(uint32_t contact_idx, const char* password); bool uiSendCliCommand(uint32_t contact_idx, const char* command); + bool uiSendTelemetryRequest(uint32_t contact_idx); int getAdminContactIdx() const { return _admin_contact_idx; } diff --git a/examples/companion_radio/ui-new/Repeateradminscreen.h b/examples/companion_radio/ui-new/Repeateradminscreen.h index 694b9d1..184f500 100644 --- a/examples/companion_radio/ui-new/Repeateradminscreen.h +++ b/examples/companion_radio/ui-new/Repeateradminscreen.h @@ -208,6 +208,13 @@ private: PwdCacheEntry _pwdCache[PWD_CACHE_SIZE]; int _pwdCacheCount; + // Remote telemetry (from LPP response) + float _telemVoltage; // Battery voltage in volts + float _telemTempC; // Temperature in celsius + bool _telemHasVoltage; + bool _telemHasTemp; + bool _telemRequested; // Telemetry request already sent + const char* getCachedPassword(int contactIdx) { for (int i = 0; i < _pwdCacheCount; i++) { if (_pwdCache[i].contactIdx == contactIdx) return _pwdCache[i].password; @@ -236,6 +243,49 @@ private: } } + // --- LPP telemetry parser --- + // Walks a CayenneLPP buffer extracting voltage and temperature. + // Format per entry: [channel 1B][type 1B][data varies] + void parseLPPTelemetry(const uint8_t* data, uint8_t len) { + const uint8_t TELEM_TYPE_VOLTAGE = 0x74; // 2 bytes, uint16 / 100 = V + const uint8_t TELEM_TYPE_TEMPERATURE = 0x67; // 2 bytes, int16 / 10 = C + + int pos = 0; + while (pos + 2 < len) { + // uint8_t channel = data[pos]; // not needed + uint8_t type = data[pos + 1]; + pos += 2; // skip channel + type + + // Determine data size for this type + int dataSize = 0; + switch (type) { + case TELEM_TYPE_VOLTAGE: dataSize = 2; break; + case TELEM_TYPE_TEMPERATURE: dataSize = 2; break; + case 0x88: dataSize = 9; break; // GPS + case 0x75: dataSize = 2; break; // Current + case 0x68: dataSize = 1; break; // Humidity + case 0x73: dataSize = 2; break; // Pressure + case 0x76: dataSize = 2; break; // Altitude + case 0x80: dataSize = 2; break; // Power + default: return; // Unknown type, stop parsing + } + + if (pos + dataSize > len) break; + + if (type == TELEM_TYPE_VOLTAGE) { + uint16_t raw = ((uint16_t)data[pos] << 8) | data[pos + 1]; + _telemVoltage = raw / 100.0f; + _telemHasVoltage = true; + } else if (type == TELEM_TYPE_TEMPERATURE) { + int16_t raw = ((int16_t)data[pos] << 8) | data[pos + 1]; + _telemTempC = raw / 10.0f; + _telemHasTemp = true; + } + + pos += dataSize; + } + } + // --- Helpers --- static void formatTime(char* buf, size_t bufLen, uint32_t epoch) { @@ -378,7 +428,9 @@ public: _catSel(0), _cmdSel(0), _scrollOffset(0), _paramLen(0), _pendingCmd(nullptr), _responseLen(0), _responseScroll(0), _responseTotalLines(0), - _cmdSentAt(0), _waitingForLogin(false), _pwdCacheCount(0) { + _cmdSentAt(0), _waitingForLogin(false), _pwdCacheCount(0), + _telemVoltage(0), _telemTempC(0), + _telemHasVoltage(false), _telemHasTemp(false), _telemRequested(false) { _password[0] = '\0'; _repeaterName[0] = '\0'; _response[0] = '\0'; @@ -405,6 +457,9 @@ public: _pendingCmd = nullptr; _paramLen = 0; _paramBuf[0] = '\0'; + _telemHasVoltage = false; + _telemHasTemp = false; + _telemRequested = false; const char* cached = getCachedPassword(contactIdx); if (cached) { @@ -427,6 +482,14 @@ public: _serverTime = server_time; _state = STATE_CATEGORY_MENU; cachePassword(_contactIdx, _password); + + // Auto-request telemetry (battery & temperature) after login + if (!_telemRequested) { + _telemRequested = true; + bool sent = the_mesh.uiSendTelemetryRequest(_contactIdx); + Serial.printf("[Admin] Telemetry request %s for contact idx %d\n", + sent ? "sent" : "FAILED", _contactIdx); + } } else { snprintf(_response, sizeof(_response), "Login failed.\nCheck password."); _responseLen = strlen(_response); @@ -456,6 +519,15 @@ public: _state = STATE_RESPONSE_VIEW; } + void onTelemetryResult(const uint8_t* data, uint8_t len) { + Serial.printf("[Admin] Telemetry response received, %d bytes:", len); + for (int i = 0; i < len && i < 32; i++) Serial.printf(" %02X", data[i]); + Serial.println(); + parseLPPTelemetry(data, len); + Serial.printf("[Admin] Parsed: hasVoltage=%d (%.2fV) hasTemp=%d (%.1fC)\n", + _telemHasVoltage, _telemVoltage, _telemHasTemp, _telemTempC); + } + void poll() override { if ((_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) && _cmdSentAt > 0 && (millis() - _cmdSentAt) > ADMIN_TIMEOUT_MS) { @@ -469,6 +541,7 @@ public: _responseLen = strlen(_response); _state = STATE_ERROR; } + _task->forceRefresh(); // Immediate redraw on state change } } @@ -558,7 +631,7 @@ public: break; } - if (_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) return 1000; + if (_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) return 30000; // static text; poll()/callbacks force refresh on state change if (_state == STATE_PASSWORD_ENTRY && _lastCharAt > 0 && (millis() - _lastCharAt) < 800) { return _lastCharAt + 800 - millis() + 50; } @@ -682,6 +755,28 @@ private: y += lineHeight + 2; } + // Remote telemetry info line (battery & temperature) + if (_telemHasVoltage || _telemHasTemp) { + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, y); + char telem[48]; + int tpos = 0; + if (_telemHasVoltage) { + tpos += snprintf(telem + tpos, sizeof(telem) - tpos, "Batt:%.2fV", _telemVoltage); + } + if (_telemHasTemp) { + if (tpos > 0) tpos += snprintf(telem + tpos, sizeof(telem) - tpos, " "); + tpos += snprintf(telem + tpos, sizeof(telem) - tpos, "Temp:%.1fC", _telemTempC); + } + display.print(telem); + y += lineHeight + 2; + } else if (_telemRequested) { + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, y); + display.print("Telemetry: requesting..."); + y += lineHeight + 2; + } + // Render categories for (int i = 0; i < CAT_COUNT && y + lineHeight <= display.height() - 16; i++) { bool isSystem = (i == CAT_REBOOT_OTA); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 18e50e8..9d6f76b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1550,6 +1550,14 @@ void UITask::onAdminCliResponse(const char* from_name, const char* text) { } } +void UITask::onAdminTelemetryResult(const uint8_t* data, uint8_t len) { + Serial.printf("[UITask] onAdminTelemetryResult: %d bytes, onAdmin=%d\n", len, isOnRepeaterAdmin()); + if (repeater_admin && isOnRepeaterAdmin()) { + ((RepeaterAdminScreen*)repeater_admin)->onTelemetryResult(data, len); + _next_refresh = 100; // trigger re-render + } +} + #ifdef MECK_AUDIO_VARIANT bool UITask::isAudioPlayingInBackground() const { if (!audiobook_screen) return false; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index ded69d2..b0ff41b 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -158,6 +158,7 @@ public: // Repeater admin callbacks void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) override; void onAdminCliResponse(const char* from_name, const char* text) override; + void onAdminTelemetryResult(const uint8_t* data, uint8_t len) override; // Get current screen for checking state UIScreen* getCurrentScreen() const { return curr; }