Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion protobufs
1 change: 1 addition & 0 deletions src/mesh/LR11x0Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ template <typename T> void LR11x0Interface<T>::addReceiveMetadata(meshtastic_Mes
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
mp->rx_snr = lora.getSNR();
mp->rx_rssi = lround(lora.getRSSI());
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
}

Expand Down
1 change: 1 addition & 0 deletions src/mesh/LR20x0Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ template <typename T> void LR20x0Interface<T>::addReceiveMetadata(meshtastic_Mes
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
mp->rx_snr = lora.getSNR();
mp->rx_rssi = lround(lora.getRSSI());
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
// LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError()); // not implemented for LR20x0, but noop for LR11x0
// too(!)
}
Expand Down
4 changes: 3 additions & 1 deletion src/mesh/MeshService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,10 @@ void MeshService::injectAsReceived(meshtastic_MeshPacket &p)
return;
if (mp->rx_snr == 0) // plausible synthetic link metadata unless the caller set it
mp->rx_snr = 8;
if (mp->rx_rssi == 0)
if (!mp->has_rx_rssi) { // rx_rssi has explicit presence; only fabricate if the caller didn't supply a real one
mp->rx_rssi = -40;
mp->has_rx_rssi = true;
}
mp->rx_time = getValidTime(RTCQualityFromNet);
LOG_INFO("inject: RX from=0x%08x to=0x%08x id=0x%08x ch=%d %s", mp->from, mp->to, mp->id, mp->channel,
mp->which_payload_variant == meshtastic_MeshPacket_encrypted_tag ? "encrypted" : "decoded");
Expand Down
31 changes: 28 additions & 3 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
if (ostream) {
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
for (auto item : *vec) {
item.snr_q4 = (int32_t)(item.snr * 4.0f);
// Round rather than truncate: truncation wiped any |SNR| < 0.25 dB to exactly
// 0, which collided with the "never stored" sentinel below.
item.snr_q4 = (int32_t)lroundf(item.snr * 4.0f);
item.snr = 0.0f;
if (!pb_encode_tag_for_field(ostream, iter))
return false;
Expand All @@ -214,8 +216,14 @@ bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostre
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
if (pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) {
if (node.snr_q4)
// snr_q4 = 0 is byte-identical to "field never written" but 0 dB is valid.
// NODEINFO_BITFIELD_HAS_SNR_MASK disambiguates going forward; legacy
// records (bit clear) treat this as unknown.
if (nodeInfoLiteHasSnr(&node)) {
node.snr = node.snr_q4 / 4.0f;
} else if (node.snr_q4) {
node.snr = node.snr_q4 / 4.0f;
}
node.snr_q4 = 0;
vec->push_back(node);
}
Expand Down Expand Up @@ -3624,8 +3632,20 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard
info->last_heard = mp.rx_time;

if (mp.rx_snr)
// Gate on the packet actually having been received over our own radio, not on rx_snr being
// truthy, because 0 dB is valid. TRANSPORT_LORA is set only on the real over-the-air RX path
// (RadioInterface.cpp); it excludes TRANSPORT_INTERNAL and TRANSPORT_MQTT, while still accepting
// an MQTT-origin packet that a gateway rebroadcasts onto LoRa - we genuinely measured that one
// ourselves. Mirrors hop histogram below.
// Belt-and-braces: also require has_rx_rssi, which every genuine RF-reception site sets
// unconditionally alongside rx_snr - unlike PhoneAPI's replay packets, which set TRANSPORT_LORA
// too (so the client treats restored history as if heard over the air) but never has_rx_rssi.
// Replay packets don't reach updateFrom() today; this check guards against a future change that
// routes them back through this path silently recording a replayed rx_snr as a fresh measurement.
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && mp.has_rx_rssi) {
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_HAS_SNR_MASK, true);
}

nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
mp.via_mqtt); // Store if we received this packet via MQTT
Expand All @@ -3637,6 +3657,11 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
// exclude via_mqtt too.
//
// The std::max clamp below is deliberate, not a bug: Counting an unproven-but-real neighbor as 0 hops is the
// conservative direction. This intentionally does not agree with the `hopsAway >= 0`
// gate below, which rejects the same -1 rather than storing it as a fabricated 0 -
// the histogram and the stored hops_away serve different purposes.
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && !mp.via_mqtt &&
hopScalingModule) {
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
Expand Down
13 changes: 12 additions & 1 deletion src/mesh/NodeDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,12 @@ extern uint32_t error_address;
#define NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK (1u << NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_SHIFT)
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT 9
#define NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK (1u << NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_SHIFT)
// Bits 10..31 reserved for future single-bit flags.
// snr_q4 (persisted, sint32) is proto3 singular, so 0 == "never written", but 0 dB is valid.
// This bit disambiguates: whenever snr_q4 is written from a genuine RF measurement.
// Use this instead of `if (snr_q4)`. Legacy records (bit clear) are unambiguously "unknown".
#define NODEINFO_BITFIELD_HAS_SNR_SHIFT 10
#define NODEINFO_BITFIELD_HAS_SNR_MASK (1u << NODEINFO_BITFIELD_HAS_SNR_SHIFT)
// Bits 11..31 reserved for future single-bit flags.

// Convenience accessors so call sites read like the old struct fields.
inline bool nodeInfoLiteHasUser(const meshtastic_NodeInfoLite *n)
Expand Down Expand Up @@ -783,6 +788,12 @@ inline bool nodeInfoLiteHasXeddsaSigned(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK);
}
/// True if this node's snr_q4 was written from a genuine RF measurement (including a real
/// 0 dB reading). False means "never measured" - do not treat 0 as data.
inline bool nodeInfoLiteHasSnr(const meshtastic_NodeInfoLite *n)
{
return n && (n->bitfield & NODEINFO_BITFIELD_HAS_SNR_MASK);
}
/// A node that the eviction/migration paths must not drop: a favourite, an
/// ignored (blocked) node, or a manually-verified key.
inline bool nodeInfoLiteIsProtected(const meshtastic_NodeInfoLite *n)
Expand Down
35 changes: 27 additions & 8 deletions src/mesh/PhoneAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1222,17 +1222,29 @@ uint32_t makeReplayPacketId(NodeNum num, uint32_t timestamp, uint32_t 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.
/// Populate hop_start/hop_limit from the node's last-known hop count - never fabricate one.
/// hop_start == 0 with no decoded bitfield means unknown, not a direct neighbor; clients must
/// treat it that way too (see hop_start in mesh.proto).
void setReplayHopFields(meshtastic_MeshPacket &pkt, const meshtastic_NodeInfoLite *header)
{
if (!header || !header->has_hops_away) {
pkt.hop_start = 0; // unknown - do not fabricate a direct-neighbor reading
pkt.hop_limit = 0;
return;
}
uint8_t hopLimit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
uint8_t hopsAway = (header && header->has_hops_away) ? header->hops_away : 0;
uint8_t hopsAway = header->hops_away;
pkt.hop_start = hopLimit;
pkt.hop_limit = hopsAway < hopLimit ? (uint8_t)(hopLimit - hopsAway) : 0;
}

} // namespace

// Replayed packets deliberately leave rx_rssi absent. NodeInfoLite stores no RSSI, and
// rx_rssi has explicit presence on the wire, indicating "unknown".
// Previously these packets carried a bare 0, which a client renders as a real reading.
// Note the asymmetry with rx_snr below: that field is still proto3 singular, so "unknown" and
// "0 dB" remain indistinguishable there.
meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const meshtastic_PositionLite &pos)
{
// Shape this exactly like a fresh live broadcast Position from the peer so the
Expand All @@ -1242,12 +1254,16 @@ meshtastic_MeshPacket PhoneAPI::makeReplayPositionPacket(NodeNum num, const mesh
const meshtastic_NodeInfoLite *header = nodeDB->getMeshNode(num);
pkt.from = num;
pkt.to = NODENUM_BROADCAST;
pkt.rx_time = pos.time;
// rx_time means "when *we* received this" - use last_heard, not the position's own GPS
// fix time (which is often 0 and, when present, already round-trips inside the payload
// via ConvertToPosition).
pkt.rx_time = header ? header->last_heard : 0;
// 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.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
setReplayHopFields(pkt, header);
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated
Expand All @@ -1270,8 +1286,9 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
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.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
setReplayHopFields(pkt, header);
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated - iOS client filters
Expand Down Expand Up @@ -1375,8 +1392,9 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
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.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
setReplayHopFields(pkt, header);
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated - iOS client filters
Expand Down Expand Up @@ -1439,8 +1457,9 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
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.channel = header ? header->channel : 0;
pkt.rx_snr = header ? header->snr : 0;
pkt.via_mqtt = nodeInfoLiteViaMqtt(header);
setReplayHopFields(pkt, header);
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated - client filters
Expand Down
1 change: 1 addition & 0 deletions src/mesh/RF95Interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ void RF95Interface::addReceiveMetadata(meshtastic_MeshPacket *mp)
{
mp->rx_snr = lora->getSNR();
mp->rx_rssi = lround(lora->getRSSI());
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
LOG_DEBUG("Corrected frequency offset: %f", lora->getFrequencyError());
}

Expand Down
2 changes: 1 addition & 1 deletion src/mesh/RadioInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ void printPacket(const char *prefix, const meshtastic_MeshPacket *p)
out += DEBUG_PORT.mt_sprintf(" rxtime=%u", p->rx_time);
if (p->rx_snr != 0.0)
out += DEBUG_PORT.mt_sprintf(" rxSNR=%g", p->rx_snr);
if (p->rx_rssi != 0)
if (p->has_rx_rssi) // rx_rssi has explicit presence; a != 0 check would hide a genuine 0 dBm reading
out += DEBUG_PORT.mt_sprintf(" rxRSSI=%i", p->rx_rssi);
if (p->via_mqtt != 0)
out += DEBUG_PORT.mt_sprintf(" via MQTT");
Expand Down
1 change: 1 addition & 0 deletions src/mesh/SX126xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ template <typename T> void SX126xInterface<T>::addReceiveMetadata(meshtastic_Mes
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
mp->rx_snr = lora.getSNR();
mp->rx_rssi = lround(lora.getRSSI());
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
}

Expand Down
1 change: 1 addition & 0 deletions src/mesh/SX128xInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ template <typename T> void SX128xInterface<T>::addReceiveMetadata(meshtastic_Mes
// LOG_DEBUG("PacketStatus %x", lora.getPacketStatus());
mp->rx_snr = lora.getSNR();
mp->rx_rssi = lround(lora.getRSSI());
mp->has_rx_rssi = true; // rx_rssi has explicit presence - a genuine reading must be marked present to survive encoding
LOG_DEBUG("Corrected frequency offset: %f", lora.getFrequencyError());
}

Expand Down
3 changes: 3 additions & 0 deletions src/mesh/TypeConversions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfo
meshtastic_NodeInfo info = meshtastic_NodeInfo_init_default;

info.num = lite->num;
// NodeInfo.snr (wire) is still proto3 singular float - unlike NodeInfoLite.snr_q4, it has no
// presence bit and cannot distinguish a genuine 0 dB from "unknown". nodeInfoLiteHasSnr(lite)
// is available if a future NodeInfo revision needs to carry that distinction to clients.
info.snr = lite->snr;
info.last_heard = lite->last_heard;
info.channel = lite->channel;
Expand Down
7 changes: 5 additions & 2 deletions src/mesh/generated/meshtastic/deviceonly.pb.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ typedef struct _meshtastic_NodeInfoLite {
/* The public key of the user's device, for PKI-based encrypted DMs. */
meshtastic_NodeInfoLite_public_key_t public_key;
/* Q4-encoded SNR: dB × 4, sint32 zigzag. Matches RouteDiscovery convention.
Encode: snr_q4 = (int32_t)(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR. */
Encode: snr_q4 = (int32_t)lroundf(snr * 4.0f). Decode: snr = snr_q4 / 4.0f.
float snr is always zeroed on disk; this field carries all persisted SNR.
A stored 0 does not by itself mean "unknown" here - see NODEINFO_BITFIELD_HAS_SNR in
src/mesh/NodeDB.h for the presence bit that disambiguates a genuine 0 dB reading from
"never measured". */
int32_t snr_q4;
} meshtastic_NodeInfoLite;

Expand Down
Loading
Loading