diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 8fac33e84..4c9697114 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -1201,19 +1201,49 @@ void PhoneAPI::prefetchNodeInfos() onNowHasData(0); } +namespace +{ +/// Derive a stable id for a replayed satellite-DB record. Unchanged history replayed on +/// every reconnect must not look like a new packet to the phone's history/dedup - the id +/// only changes when the underlying data (its timestamp) actually changes. +// `kind` only needs to distinguish the record types that can otherwise collide (e.g. device +// vs. environment metrics both replay as TELEMETRY_APP with the same node/last_heard) - pass +// the payload's own variant/port constant. +uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t kind) +{ + uint32_t h = num; + h = h * 2654435761u + timestamp; + h = h * 2654435761u + kind; + return h ? h : 1; // some clients treat id 0 as "unset" +} + +/// Populate hop_start/hop_limit from the node's real last-known hop count (if any) so a +/// replayed packet doesn't read as "heard directly" when it wasn't. +void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header) +{ + uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); + uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0; + pkt.hop_start = hopLimit; + pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0; +} +} // namespace + meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos) { // Shape this exactly like a fresh live broadcast Position from the peer so the // phone runs it through its normal "live position broadcast" handler path. // to=ourNum would read as a DM-from-peer and never lands in node detail UI. meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; + const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); pkt.from = num; pkt.to = NODENUM_BROADCAST; - pkt.id = generatePacketId(); pkt.rx_time = pos.time; + // Stable per-node/per-fix id: replaying the same unchanged history on every + // reconnect must not look like a brand new packet to the phone's history/dedup. + pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_POSITION_APP); pkt.channel = 0; - pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - pkt.hop_start = pkt.hop_limit; + pkt.rx_snr = header ? header->snr : 0; + setReplayHopFields(pkt, header); pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND; // Mark as if heard over the air, not internally generated pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA; @@ -1231,13 +1261,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; pkt.from = num; pkt.to = NODENUM_BROADCAST; - pkt.id = generatePacketId(); // No native timestamp on telemetry packets here; use last_heard. const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); pkt.rx_time = header ? header->last_heard : 0; + pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_device_metrics_tag); pkt.channel = 0; - pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - pkt.hop_start = pkt.hop_limit; + pkt.rx_snr = header ? header->snr : 0; + setReplayHopFields(pkt, header); pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND; // Mark as if heard over the air, not internally generated - iOS client filters // TRANSPORT_INTERNAL packets out of broadcast peer state updates. @@ -1337,12 +1367,12 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; pkt.from = num; pkt.to = NODENUM_BROADCAST; - pkt.id = generatePacketId(); const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); pkt.rx_time = header ? header->last_heard : 0; + pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_Telemetry_environment_metrics_tag); pkt.channel = 0; - pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - pkt.hop_start = pkt.hop_limit; + pkt.rx_snr = header ? header->snr : 0; + setReplayHopFields(pkt, header); pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND; // Mark as if heard over the air, not internally generated - iOS client filters // TRANSPORT_INTERNAL packets out of broadcast peer state updates. @@ -1400,13 +1430,13 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_default; pkt.from = num; pkt.to = NODENUM_BROADCAST; - pkt.id = generatePacketId(); // StatusMessage has no native timestamp; use last_heard. const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num); pkt.rx_time = header ? header->last_heard : 0; + pkt.id = makeReplayPacketId(num, pkt.rx_time, meshtastic_PortNum_NODE_STATUS_APP); pkt.channel = 0; - pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); - pkt.hop_start = pkt.hop_limit; + pkt.rx_snr = header ? header->snr : 0; + setReplayHopFields(pkt, header); pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND; // Mark as if heard over the air, not internally generated - client filters pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;