Skip to content
Open
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
20 changes: 20 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,27 @@ void MyMesh::handleCmdFrame(size_t len) {
result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout);
expected_ack = 0; // no Ack expected
} else {
const uint32_t app_timestamp = msg_timestamp;
const bool is_room_message = recipient->type == ADV_TYPE_ROOM;
uint8_t message_fingerprint[MAX_HASH_SIZE];
if (is_room_message) {
mesh::Utils::sha256(message_fingerprint, sizeof(message_fingerprint),
recipient->id.pub_key, PUB_KEY_SIZE,
(const uint8_t*)text, strlen(text));
if (!room_message_timestamps.find(message_fingerprint, app_timestamp,
&msg_timestamp)) {
// Older room servers compare posts with login and keep-alive
// timestamps, which already come from this monotonic clock.
msg_timestamp = getRTCClock()->getCurrentTimeUnique();
}
}
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout);
if (result != MSG_SEND_FAILED && is_room_message) {
// Preserve the translated timestamp across application retries so
// the room can ACK the retry without storing a duplicate post.
room_message_timestamps.remember(message_fingerprint, app_timestamp,
msg_timestamp);
}
}
// TODO: add expected ACK to table
if (result == MSG_SEND_FAILED) {
Expand Down
6 changes: 6 additions & 0 deletions examples/companion_radio/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <helpers/ArduinoHelpers.h>
#include <helpers/BaseSerialInterface.h>
#include <helpers/IdentityStore.h>
#include <helpers/MessageTimestampCache.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <target.h>
Expand Down Expand Up @@ -63,6 +64,10 @@
#define OFFLINE_QUEUE_SIZE 16
#endif

#ifndef ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE
#define ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE 16
#endif

#ifndef BLE_NAME_PREFIX
#define BLE_NAME_PREFIX "MeshCore-"
#endif
Expand Down Expand Up @@ -253,6 +258,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
#define EXPECTED_ACK_TABLE_SIZE 8
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table
int next_ack_idx;
mesh::MessageTimestampCache<ROOM_MESSAGE_TIMESTAMP_CACHE_SIZE> room_message_timestamps;

#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
Expand Down
76 changes: 76 additions & 0 deletions src/helpers/MessageTimestampCache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#pragma once

#include <MeshCore.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>

namespace mesh {

// Maps an application message identity to the timestamp used on air. This
// lets retransmissions keep the same timestamp even when the sender needs to
// translate timestamps between clock sources.
template <size_t ENTRY_COUNT>
class MessageTimestampCache {
public:
MessageTimestampCache() { clear(); }

bool find(const uint8_t fingerprint[MAX_HASH_SIZE], uint32_t source_timestamp,
uint32_t* mapped_timestamp = NULL) const {
if (fingerprint == NULL) return false;

for (size_t i = 0; i < ENTRY_COUNT; i++) {
const Entry& entry = entries_[i];
if (entry.valid && entry.source_timestamp == source_timestamp
&& memcmp(entry.fingerprint, fingerprint, MAX_HASH_SIZE) == 0) {
if (mapped_timestamp != NULL) {
*mapped_timestamp = entry.mapped_timestamp;
}
return true;
}
}
return false;
}

bool remember(const uint8_t fingerprint[MAX_HASH_SIZE], uint32_t source_timestamp,
uint32_t mapped_timestamp) {
if (fingerprint == NULL) return false;

for (size_t i = 0; i < ENTRY_COUNT; i++) {
Entry& entry = entries_[i];
if (entry.valid && entry.source_timestamp == source_timestamp
&& memcmp(entry.fingerprint, fingerprint, MAX_HASH_SIZE) == 0) {
entry.mapped_timestamp = mapped_timestamp;
return true;
}
}

Entry& entry = entries_[next_entry_];
memcpy(entry.fingerprint, fingerprint, MAX_HASH_SIZE);
entry.source_timestamp = source_timestamp;
entry.mapped_timestamp = mapped_timestamp;
entry.valid = true;
next_entry_ = (next_entry_ + 1) % ENTRY_COUNT;
return true;
}

void clear() {
memset(entries_, 0, sizeof(entries_));
next_entry_ = 0;
}

private:
static_assert(ENTRY_COUNT > 0, "MessageTimestampCache needs at least one entry");

struct Entry {
uint8_t fingerprint[MAX_HASH_SIZE];
uint32_t source_timestamp;
uint32_t mapped_timestamp;
bool valid;
};

Entry entries_[ENTRY_COUNT];
size_t next_entry_;
};

} // namespace mesh
75 changes: 75 additions & 0 deletions test/test_message_timestamp_cache/test_message_timestamp_cache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include <gtest/gtest.h>

#include <helpers/MessageTimestampCache.h>

static void makeFingerprint(uint8_t fingerprint[MAX_HASH_SIZE], uint8_t value) {
memset(fingerprint, value, MAX_HASH_SIZE);
}

TEST(MessageTimestampCache, ReturnsMappedTimestampForRetry) {
mesh::MessageTimestampCache<4> cache;
uint8_t fingerprint[MAX_HASH_SIZE];
makeFingerprint(fingerprint, 0x11);

ASSERT_TRUE(cache.remember(fingerprint, 100U, 500U));

uint32_t mapped = 0;
EXPECT_TRUE(cache.find(fingerprint, 100U, &mapped));
EXPECT_EQ(500U, mapped);
}

TEST(MessageTimestampCache, DistinguishesLogicalMessages) {
mesh::MessageTimestampCache<4> cache;
uint8_t first[MAX_HASH_SIZE];
uint8_t second[MAX_HASH_SIZE];
makeFingerprint(first, 0x21);
makeFingerprint(second, 0x22);

ASSERT_TRUE(cache.remember(first, 100U, 500U));

EXPECT_FALSE(cache.find(first, 101U));
EXPECT_FALSE(cache.find(second, 100U));
}

TEST(MessageTimestampCache, UpdatesAnExistingMapping) {
mesh::MessageTimestampCache<2> cache;
uint8_t fingerprint[MAX_HASH_SIZE];
makeFingerprint(fingerprint, 0x33);

ASSERT_TRUE(cache.remember(fingerprint, 7U, 70U));
ASSERT_TRUE(cache.remember(fingerprint, 7U, 71U));

uint32_t mapped = 0;
EXPECT_TRUE(cache.find(fingerprint, 7U, &mapped));
EXPECT_EQ(71U, mapped);
}

TEST(MessageTimestampCache, ReplacesOldestEntryWhenFull) {
mesh::MessageTimestampCache<2> cache;
uint8_t first[MAX_HASH_SIZE];
uint8_t second[MAX_HASH_SIZE];
uint8_t third[MAX_HASH_SIZE];
makeFingerprint(first, 0x41);
makeFingerprint(second, 0x42);
makeFingerprint(third, 0x43);

ASSERT_TRUE(cache.remember(first, 1U, 101U));
ASSERT_TRUE(cache.remember(second, 2U, 102U));
ASSERT_TRUE(cache.remember(third, 3U, 103U));

EXPECT_FALSE(cache.find(first, 1U));
EXPECT_TRUE(cache.find(second, 2U));
EXPECT_TRUE(cache.find(third, 3U));
}

TEST(MessageTimestampCache, RejectsNullFingerprint) {
mesh::MessageTimestampCache<2> cache;

EXPECT_FALSE(cache.remember(NULL, 1U, 2U));
EXPECT_FALSE(cache.find(NULL, 1U));
}

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}