diff --git a/apps/linux_sim_shell/CMakeLists.txt b/apps/linux_sim_shell/CMakeLists.txt index 395a9cf3..a90ee660 100644 --- a/apps/linux_sim_shell/CMakeLists.txt +++ b/apps/linux_sim_shell/CMakeLists.txt @@ -285,6 +285,7 @@ if(BUILD_TESTING) "${TRAIL_MATE_REPO_ROOT}/modules/core_phone/src/meshcore/meshcore_phone_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_phone/src/meshtastic/meshtastic_phone_core.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_phone/src/meshtastic/meshtastic_phone_session.cpp" + "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_node_payload.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_packet_wire.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_protocol_helpers.cpp" "${TRAIL_MATE_REPO_ROOT}/modules/core_chat/src/infra/meshtastic/mt_radio_config.cpp" diff --git a/modules/core_phone/include/phone/meshtastic/meshtastic_phone_core.h b/modules/core_phone/include/phone/meshtastic/meshtastic_phone_core.h index 2dd70a34..0fb9c551 100644 --- a/modules/core_phone/include/phone/meshtastic/meshtastic_phone_core.h +++ b/modules/core_phone/include/phone/meshtastic/meshtastic_phone_core.h @@ -1,6 +1,7 @@ #pragma once #include "chat/domain/chat_types.h" +#include "chat/infra/meshtastic/mt_node_payload.h" #include "meshtastic/admin.pb.h" #include "meshtastic/channel.pb.h" #include "meshtastic/config.pb.h" @@ -256,18 +257,30 @@ class MeshtasticPhoneCore }; static constexpr size_t kPhoneQueueDepth = 4; + static constexpr size_t kNodeProjectionCacheDepth = 8; + + struct NodeProjectionCacheEntry + { + uint32_t node_id = 0; + uint32_t signature = 0; + }; bool handleToRadioPacket(meshtastic_MeshPacket& packet); bool handleAdmin(meshtastic_MeshPacket& packet); bool handleLocalSelfPacket(meshtastic_MeshPacket& packet); bool encodeFromRadio(meshtastic_FromRadio& from, uint32_t from_num, MeshtasticBleFrame* out); bool popConfigSnapshotFrame(MeshtasticBleFrame* out); + void enqueueKnownNodeInfoProjection(chat::NodeId node_id); + bool enqueueMetadataNodeInfoProjection(const chat::MeshIncomingData& msg); + bool shouldProjectNodeInfo(chat::NodeId node_id, uint32_t signature); void enqueueQueueStatus(uint32_t packet_id, bool ok); void enqueueConfigSnapshot(uint32_t config_nonce); void notifyFromNum(uint32_t from_num); void fillMyInfo(meshtastic_MyNodeInfo* out) const; void fillSelfNodeInfo(meshtastic_NodeInfo* out) const; void fillNodeInfoFromEntry(const PhoneNodeView& entry, meshtastic_NodeInfo* out) const; + void fillNodeInfoFromDecodedPayload(const chat::meshtastic::DecodedNodePayload& node, + meshtastic_NodeInfo* out) const; void fillMetadata(meshtastic_DeviceMetadata* out) const; void fillDeviceUi(meshtastic_DeviceUIConfig* out) const; void fillChannel(uint8_t idx, meshtastic_Channel* out) const; @@ -315,7 +328,10 @@ class MeshtasticPhoneCore bool admin_edit_transaction_restart_pending_ = false; bool restart_pending_ = false; FixedRingQueue queue_status_queue_; + FixedRingQueue node_info_queue_; FixedRingQueue packet_queue_; + std::array node_projection_cache_{}; + size_t node_projection_cache_next_ = 0; meshtastic_Config_BluetoothConfig bluetooth_config_ = meshtastic_Config_BluetoothConfig_init_zero; meshtastic_LocalModuleConfig module_config_ = meshtastic_LocalModuleConfig_init_zero; char admin_canned_messages_[160] = {}; @@ -325,6 +341,7 @@ class MeshtasticPhoneCore meshtastic_AdminMessage admin_resp_scratch_ = meshtastic_AdminMessage_init_zero; meshtastic_MeshPacket reply_packet_scratch_ = meshtastic_MeshPacket_init_zero; meshtastic_MqttClientProxyMessage mqtt_proxy_scratch_ = meshtastic_MqttClientProxyMessage_init_zero; + meshtastic_Data node_metadata_decode_scratch_ = meshtastic_Data_init_zero; meshtastic_FromRadio from_radio_scratch_ = meshtastic_FromRadio_init_zero; }; diff --git a/modules/core_phone/src/meshtastic/meshtastic_phone_core.cpp b/modules/core_phone/src/meshtastic/meshtastic_phone_core.cpp index 2056b74c..5f44c2bb 100644 --- a/modules/core_phone/src/meshtastic/meshtastic_phone_core.cpp +++ b/modules/core_phone/src/meshtastic/meshtastic_phone_core.cpp @@ -92,6 +92,89 @@ bool hasAndroidVisibleNodeName(const PhoneNodeView& entry) hasBoundedText(entry.long_name, sizeof(entry.long_name)); } +void mixProjectionByte(uint32_t& hash, uint8_t value) +{ + hash ^= value; + hash *= 16777619UL; +} + +void mixProjectionString(uint32_t& hash, const char* text, size_t max_len) +{ + if (!text) + { + mixProjectionByte(hash, 0); + return; + } + size_t index = 0; + while (index < max_len && text[index] != '\0') + { + mixProjectionByte(hash, static_cast(text[index])); + ++index; + } + mixProjectionByte(hash, 0); +} + +void mixProjectionU32(uint32_t& hash, uint32_t value) +{ + mixProjectionByte(hash, static_cast(value & 0xFFU)); + mixProjectionByte(hash, static_cast((value >> 8U) & 0xFFU)); + mixProjectionByte(hash, static_cast((value >> 16U) & 0xFFU)); + mixProjectionByte(hash, static_cast((value >> 24U) & 0xFFU)); +} + +void mixProjectionBytes(uint32_t& hash, const uint8_t* data, size_t len) +{ + if (!data) + { + mixProjectionByte(hash, 0); + return; + } + for (size_t index = 0; index < len; ++index) + { + mixProjectionByte(hash, data[index]); + } + mixProjectionByte(hash, 0); +} + +uint32_t nodeProjectionSignature(const PhoneNodeView& entry) +{ + uint32_t hash = 2166136261UL; + mixProjectionU32(hash, entry.node_id); + mixProjectionString(hash, entry.short_name, sizeof(entry.short_name)); + mixProjectionString(hash, entry.long_name, sizeof(entry.long_name)); + mixProjectionByte(hash, entry.protocol); + mixProjectionByte(hash, entry.role); + mixProjectionByte(hash, entry.hw_model); + mixProjectionByte(hash, entry.channel); + mixProjectionByte(hash, entry.via_mqtt ? 1U : 0U); + mixProjectionByte(hash, entry.is_ignored ? 1U : 0U); + mixProjectionByte(hash, entry.has_public_key ? 1U : 0U); + mixProjectionByte(hash, entry.key_manually_verified ? 1U : 0U); + return hash == 0 ? 1U : hash; +} + +uint32_t nodeProjectionSignature(const chat::meshtastic::DecodedNodePayload& node) +{ + uint32_t hash = 2166136261UL; + mixProjectionU32(hash, node.node_id); + mixProjectionString(hash, node.short_name.c_str(), node.short_name.size() + 1U); + mixProjectionString(hash, node.long_name.c_str(), node.long_name.size() + 1U); + mixProjectionByte(hash, node.protocol); + mixProjectionByte(hash, node.role); + mixProjectionByte(hash, node.hw_model); + mixProjectionByte(hash, node.channel); + mixProjectionByte(hash, node.via_mqtt ? 1U : 0U); + mixProjectionByte(hash, node.is_ignored ? 1U : 0U); + mixProjectionByte(hash, node.has_public_key_state ? 1U : 0U); + mixProjectionByte(hash, node.has_public_key ? 1U : 0U); + if (node.has_public_key) + { + mixProjectionBytes(hash, node.public_key.data(), node.public_key.size()); + } + mixProjectionByte(hash, node.key_manually_verified ? 1U : 0U); + return hash == 0 ? 1U : hash; +} + void applyChannelPsk(uint8_t* dst, size_t dst_len, uint8_t* dst_key_len, @@ -522,11 +605,16 @@ void MeshtasticPhoneCore::reset() admin_edit_transaction_restart_pending_ = false; restart_pending_ = false; queue_status_queue_.clear(); + node_info_queue_.clear(); packet_queue_.clear(); + node_projection_cache_ = {}; + node_projection_cache_next_ = 0; } void MeshtasticPhoneCore::onIncomingText(const chat::MeshIncomingText& msg) { + enqueueKnownNodeInfoProjection(msg.from); + bool dropped = false; auto& packet = packet_queue_.pushSlotDropOldest(&dropped); fillPacketFromText(msg, &packet); @@ -544,6 +632,11 @@ void MeshtasticPhoneCore::onIncomingText(const chat::MeshIncomingText& msg) void MeshtasticPhoneCore::onIncomingData(const chat::MeshIncomingData& msg) { + if (!enqueueMetadataNodeInfoProjection(msg)) + { + enqueueKnownNodeInfoProjection(msg.from); + } + bool dropped = false; auto& packet = packet_queue_.pushSlotDropOldest(&dropped); fillPacketFromData(msg, &packet); @@ -571,6 +664,128 @@ bool MeshtasticPhoneCore::isConfigFlowActive() const return config_flow_active_; } +bool MeshtasticPhoneCore::shouldProjectNodeInfo(chat::NodeId node_id, uint32_t signature) +{ + if (node_id == 0 || signature == 0) + { + return false; + } + + for (const auto& cached : node_projection_cache_) + { + if (cached.node_id == node_id && cached.signature == signature) + { + return false; + } + } + + node_projection_cache_[node_projection_cache_next_] = {node_id, signature}; + node_projection_cache_next_ = (node_projection_cache_next_ + 1U) % node_projection_cache_.size(); + return true; +} + +void MeshtasticPhoneCore::enqueueKnownNodeInfoProjection(chat::NodeId node_id) +{ + if (node_id == 0 || node_id == app_.getSelfNodeId()) + { + return; + } + + PhoneNodeView entry{}; + if (!app_.findPhoneNode(node_id, entry) || !hasAndroidVisibleNodeName(entry)) + { + return; + } + + const uint32_t signature = nodeProjectionSignature(entry); + if (!shouldProjectNodeInfo(entry.node_id, signature)) + { + return; + } + + bool dropped = false; + auto& info = node_info_queue_.pushSlotDropOldest(&dropped); + fillNodeInfoFromEntry(entry, &info); + if (dropped) + { + logDual("[BLE][mtcore] node_info queue dropped oldest before realtime projection\n"); + } + logDual("[BLE][mtcore] enqueue node_info projection node=%08lX short=%s long=%s via_mqtt=%u\n", + static_cast(entry.node_id), + entry.short_name, + entry.long_name, + entry.via_mqtt ? 1U : 0U); + notifyFromNum(entry.node_id); +} + +bool MeshtasticPhoneCore::enqueueMetadataNodeInfoProjection(const chat::MeshIncomingData& msg) +{ + if (msg.from == 0 || msg.from == app_.getSelfNodeId() || + !chat::meshtastic::isNodeMetadataPayload(static_cast(msg.portnum)) || + msg.payload.empty()) + { + return false; + } + + auto& data = node_metadata_decode_scratch_; + data = meshtastic_Data_init_zero; + if (msg.payload.size() > sizeof(data.payload.bytes)) + { + return false; + } + data.portnum = static_cast(msg.portnum); + data.dest = msg.to; + data.source = msg.from; + data.request_id = msg.request_id; + data.want_response = msg.want_response; + data.has_bitfield = true; + data.bitfield = 0; + data.payload.size = static_cast(msg.payload.size()); + if (data.payload.size > 0) + { + std::memcpy(data.payload.bytes, msg.payload.data(), data.payload.size); + } + + chat::meshtastic::NodePayloadDecodeContext context{}; + context.fallback_node_id = msg.from; + context.snr = msg.rx_meta.snr_db_x10 / 10.0f; + context.rssi = msg.rx_meta.rssi_dbm_x10 / 10.0f; + context.timestamp = msg.rx_meta.rx_timestamp_s; + context.hops_away = msg.rx_meta.hop_count; + context.channel = channelIndexFromId(msg.channel); + context.via_mqtt = msg.rx_meta.from_is; + + chat::meshtastic::DecodedNodePayload node{}; + if (!chat::meshtastic::decodeNodeMetadataPayload(data, context, &node) || + node.node_id == 0 || node.node_id == app_.getSelfNodeId() || + (node.short_name.empty() && node.long_name.empty())) + { + return false; + } + + const uint32_t signature = nodeProjectionSignature(node); + if (!shouldProjectNodeInfo(node.node_id, signature)) + { + return false; + } + + bool dropped = false; + auto& info = node_info_queue_.pushSlotDropOldest(&dropped); + fillNodeInfoFromDecodedPayload(node, &info); + if (dropped) + { + logDual("[BLE][mtcore] node_info queue dropped oldest before metadata projection\n"); + } + logDual("[BLE][mtcore] enqueue metadata node_info node=%08lX short=%s long=%s via_mqtt=%u port=%u\n", + static_cast(node.node_id), + node.short_name.c_str(), + node.long_name.c_str(), + node.via_mqtt ? 1U : 0U, + static_cast(msg.portnum)); + notifyFromNum(node.node_id); + return true; +} + bool MeshtasticPhoneCore::handleToRadio(const uint8_t* data, size_t len) { if (!data || len == 0 || len > sizeof(last_to_radio_)) @@ -1435,6 +1650,15 @@ bool MeshtasticPhoneCore::popToPhone(MeshtasticBleFrame* out) return encodeFromRadio(from, mesh_packet_id, out); } + if (const meshtastic_NodeInfo* node_info = node_info_queue_.front()) + { + from.which_payload_variant = meshtastic_FromRadio_node_info_tag; + from.node_info = *node_info; + const uint32_t node_id = node_info->num; + node_info_queue_.pop(); + return encodeFromRadio(from, node_id, out); + } + if (const meshtastic_MeshPacket* packet = packet_queue_.front()) { from.which_payload_variant = meshtastic_FromRadio_packet_tag; @@ -1852,6 +2076,82 @@ void MeshtasticPhoneCore::fillNodeInfoFromEntry(const PhoneNodeView& entry, mesh } } +void MeshtasticPhoneCore::fillNodeInfoFromDecodedPayload( + const chat::meshtastic::DecodedNodePayload& node, meshtastic_NodeInfo* out) const +{ + if (!out) + { + return; + } + + meshtastic_NodeInfo& info = *out; + std::memset(&info, 0, sizeof(info)); + info.num = node.node_id; + info.has_user = node.has_user || !node.short_name.empty() || !node.long_name.empty(); + + if (info.has_user) + { + char user_id[16] = {}; + std::snprintf(user_id, sizeof(user_id), "!%08lX", static_cast(node.node_id)); + copyBounded(info.user.id, sizeof(info.user.id), user_id); + copyBounded(info.user.long_name, sizeof(info.user.long_name), node.long_name.c_str()); + copyBounded(info.user.short_name, sizeof(info.user.short_name), node.short_name.c_str()); + if (node.has_macaddr) + { + std::memcpy(info.user.macaddr, node.macaddr.data(), sizeof(info.user.macaddr)); + } + info.user.hw_model = static_cast(node.hw_model); + info.user.role = roleFromEntry(node.role); + if (node.has_public_key) + { + const size_t copy_len = std::min(node.public_key.size(), sizeof(info.user.public_key.bytes)); + std::memcpy(info.user.public_key.bytes, node.public_key.data(), copy_len); + info.user.public_key.size = static_cast(copy_len); + } + } + + info.channel = node.channel; + info.last_heard = node.timestamp; + info.snr = node.snr; + info.has_hops_away = node.hops_away != 0xFFU; + info.hops_away = node.hops_away; + info.via_mqtt = node.via_mqtt; + info.is_ignored = node.is_ignored; + info.is_key_manually_verified = node.key_manually_verified; + if (node.has_device_metrics) + { + info.has_device_metrics = true; + info.device_metrics.has_battery_level = node.device_metrics.has_battery_level; + info.device_metrics.battery_level = node.device_metrics.battery_level; + info.device_metrics.has_voltage = node.device_metrics.has_voltage; + info.device_metrics.voltage = node.device_metrics.voltage; + info.device_metrics.has_channel_utilization = node.device_metrics.has_channel_utilization; + info.device_metrics.channel_utilization = node.device_metrics.channel_utilization; + info.device_metrics.has_air_util_tx = node.device_metrics.has_air_util_tx; + info.device_metrics.air_util_tx = node.device_metrics.air_util_tx; + info.device_metrics.has_uptime_seconds = node.device_metrics.has_uptime_seconds; + info.device_metrics.uptime_seconds = node.device_metrics.uptime_seconds; + } + + if (node.has_position && node.position.valid) + { + info.has_position = true; + info.position = meshtastic_Position_init_zero; + info.position.has_latitude_i = true; + info.position.latitude_i = node.position.latitude_i; + info.position.has_longitude_i = true; + info.position.longitude_i = node.position.longitude_i; + info.position.timestamp = node.position.timestamp; + info.position.has_altitude = node.position.has_altitude; + info.position.altitude = node.position.altitude; + info.position.precision_bits = node.position.precision_bits; + info.position.PDOP = node.position.pdop; + info.position.HDOP = node.position.hdop; + info.position.VDOP = node.position.vdop; + info.position.gps_accuracy = node.position.gps_accuracy_mm; + } +} + meshtastic_NodeInfo MeshtasticPhoneCore::buildNodeInfoFromEntry(const PhoneNodeView& entry) const { meshtastic_NodeInfo info = meshtastic_NodeInfo_init_zero; diff --git a/modules/core_phone/tests/test_phone_core_smoke.cpp b/modules/core_phone/tests/test_phone_core_smoke.cpp index 30358934..53af9efe 100644 --- a/modules/core_phone/tests/test_phone_core_smoke.cpp +++ b/modules/core_phone/tests/test_phone_core_smoke.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "meshtastic/admin.pb.h" #include "meshtastic/portnums.pb.h" @@ -308,6 +309,19 @@ bool decodeFromRadio(const phone::meshtastic::MeshtasticBleFrame& frame, meshtas return pb_decode(&stream, meshtastic_FromRadio_fields, &out); } +bool encodeNodeInfoPayload(const meshtastic_NodeInfo& node, std::vector& out) +{ + uint8_t buffer[meshtastic_NodeInfo_size] = {}; + pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer)); + if (!pb_encode(&stream, meshtastic_NodeInfo_fields, &node)) + { + return false; + } + + out.assign(buffer, buffer + stream.bytes_written); + return true; +} + } // namespace int main() @@ -389,6 +403,121 @@ int main() assert(text_from.packet.decoded.has_bitfield); assert(!text_session.popToPhone(&text_frame)); + phone::tests::FakePhoneRuntimeContext known_node_runtime; + FakeMeshtasticTransport known_node_transport; + phone::PhoneNodeView cd8c_node{}; + cd8c_node.node_id = incoming_text.from; + copyBounded(cd8c_node.short_name, sizeof(cd8c_node.short_name), "cd8c"); + copyBounded(cd8c_node.long_name, sizeof(cd8c_node.long_name), "Haibara.Ai"); + cd8c_node.last_seen = incoming_text.timestamp; + cd8c_node.role = 0; + cd8c_node.hw_model = static_cast(meshtastic_HardwareModel_PRIVATE_HW); + cd8c_node.channel = 0; + cd8c_node.via_mqtt = true; + known_node_runtime.nodes.push_back(cd8c_node); + phone::meshtastic::MeshtasticPhoneSession known_node_session( + known_node_runtime, known_node_transport, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + + incoming_text.msg_id = 0x9DD4E0EA; + known_node_session.onIncomingText(incoming_text); + assert(known_node_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_frame.from_num == cd8c_node.node_id); + assert(text_from.which_payload_variant == meshtastic_FromRadio_node_info_tag); + assert(text_from.node_info.num == cd8c_node.node_id); + assert(text_from.node_info.has_user); + assert(std::strcmp(text_from.node_info.user.short_name, cd8c_node.short_name) == 0); + assert(std::strcmp(text_from.node_info.user.long_name, cd8c_node.long_name) == 0); + assert(text_from.node_info.via_mqtt); + assert(known_node_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_frame.from_num == incoming_text.msg_id); + assert(text_from.which_payload_variant == meshtastic_FromRadio_packet_tag); + + incoming_text.msg_id = 0x9DD4E0EB; + known_node_session.onIncomingText(incoming_text); + assert(known_node_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_from.which_payload_variant == meshtastic_FromRadio_packet_tag); + assert(!known_node_session.popToPhone(&text_frame)); + + copyBounded(known_node_runtime.nodes[0].long_name, + sizeof(known_node_runtime.nodes[0].long_name), + "Haibara.Ai (MQTT)"); + incoming_text.msg_id = 0x9DD4E0EC; + known_node_session.onIncomingText(incoming_text); + assert(known_node_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_from.which_payload_variant == meshtastic_FromRadio_node_info_tag); + assert(std::strcmp(text_from.node_info.user.long_name, "Haibara.Ai (MQTT)") == 0); + assert(known_node_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_from.which_payload_variant == meshtastic_FromRadio_packet_tag); + assert(!known_node_session.popToPhone(&text_frame)); + + phone::tests::FakePhoneRuntimeContext metadata_runtime; + FakeMeshtasticTransport metadata_transport; + phone::meshtastic::MeshtasticPhoneSession metadata_session( + metadata_runtime, metadata_transport, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + + meshtastic_NodeInfo metadata_node = meshtastic_NodeInfo_init_zero; + metadata_node.num = 0x0C16AAEC; + metadata_node.has_user = true; + copyBounded(metadata_node.user.id, sizeof(metadata_node.user.id), "!0C16AAEC"); + copyBounded(metadata_node.user.short_name, sizeof(metadata_node.user.short_name), "AAEC"); + copyBounded(metadata_node.user.long_name, sizeof(metadata_node.user.long_name), "lilygo-AAEC"); + metadata_node.user.hw_model = meshtastic_HardwareModel_T_LORA_PAGER; + metadata_node.user.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + metadata_node.user.public_key.size = 32; + for (uint8_t index = 0; index < metadata_node.user.public_key.size; ++index) + { + metadata_node.user.public_key.bytes[index] = static_cast(index + 1U); + } + metadata_node.snr = 5.2F; + metadata_node.last_heard = 123456790; + metadata_node.via_mqtt = true; + metadata_node.has_hops_away = true; + metadata_node.hops_away = 2; + + chat::MeshIncomingData metadata_data{}; + metadata_data.portnum = meshtastic_PortNum_NODEINFO_APP; + metadata_data.from = metadata_node.num; + metadata_data.to = 0xFFFFFFFF; + metadata_data.packet_id = 0x30FF92E3; + metadata_data.channel = chat::ChannelId::PRIMARY; + metadata_data.rx_meta.from_is = true; + metadata_data.rx_meta.snr_db_x10 = 52; + metadata_data.rx_meta.rssi_dbm_x10 = -590; + metadata_data.rx_meta.rx_timestamp_s = metadata_node.last_heard; + metadata_data.rx_meta.hop_count = metadata_node.hops_away; + assert(encodeNodeInfoPayload(metadata_node, metadata_data.payload)); + + metadata_session.onIncomingData(metadata_data); + assert(metadata_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_frame.from_num == metadata_node.num); + assert(text_from.which_payload_variant == meshtastic_FromRadio_node_info_tag); + assert(text_from.node_info.num == metadata_node.num); + assert(text_from.node_info.has_user); + assert(std::strcmp(text_from.node_info.user.short_name, "AAEC") == 0); + assert(std::strcmp(text_from.node_info.user.long_name, "lilygo-AAEC") == 0); + assert(text_from.node_info.user.public_key.size == 32); + assert(text_from.node_info.via_mqtt); + assert(text_from.node_info.has_hops_away); + assert(text_from.node_info.hops_away == 2); + assert(metadata_session.popToPhone(&text_frame)); + text_from = meshtastic_FromRadio_init_zero; + assert(decodeFromRadio(text_frame, text_from)); + assert(text_frame.from_num == metadata_data.packet_id); + assert(text_from.which_payload_variant == meshtastic_FromRadio_packet_tag); + assert(!metadata_session.popToPhone(&text_frame)); + phone::tests::FakePhoneRuntimeContext admin_runtime; FakeMeshtasticTransport admin_transport; phone::meshtastic::MeshtasticPhoneSession admin_session(