Skip to content

Commit d41a968

Browse files
authored
Merge pull request meshcore-dev#1379 from oltaco/improved-contact-mgmt
Companion: Improved Contact Management
2 parents 56eb5b0 + df66870 commit d41a968

File tree

6 files changed

+155
-36
lines changed

6 files changed

+155
-36
lines changed

examples/companion_radio/DataStore.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
227227
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
228228
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
229229
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
230+
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
230231

231232
file.close();
232233
}
@@ -261,6 +262,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
261262
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
262263
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
263264
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
265+
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
264266

265267
file.close();
266268
}

examples/companion_radio/MyMesh.cpp

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
#define CMD_SEND_CONTROL_DATA 55 // v8+
5555
#define CMD_GET_STATS 56 // v8+, second byte is stats type
5656
#define CMD_SEND_ANON_REQ 57
57+
#define CMD_SET_AUTOADD_CONFIG 58
58+
#define CMD_GET_AUTOADD_CONFIG 59
5759

5860
// Stats sub-types for CMD_GET_STATS
5961
#define STATS_TYPE_CORE 0
@@ -85,6 +87,7 @@
8587
#define RESP_CODE_ADVERT_PATH 22
8688
#define RESP_CODE_TUNING_PARAMS 23
8789
#define RESP_CODE_STATS 24 // v8+, second byte is stats type
90+
#define RESP_CODE_AUTOADD_CONFIG 25
8891

8992
#define SEND_TIMEOUT_BASE_MILLIS 500
9093
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
@@ -110,6 +113,8 @@
110113
#define PUSH_CODE_BINARY_RESPONSE 0x8C
111114
#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D
112115
#define PUSH_CODE_CONTROL_DATA 0x8E // v8+
116+
#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest
117+
#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full
113118

114119
#define ERR_CODE_UNSUPPORTED_CMD 1
115120
#define ERR_CODE_NOT_FOUND 2
@@ -120,6 +125,15 @@
120125

121126
#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K
122127

128+
// Auto-add config bitmask
129+
// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full
130+
// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01
131+
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full
132+
#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
133+
#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
134+
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
135+
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
136+
123137
void MyMesh::writeOKFrame() {
124138
uint8_t buf[1];
125139
buf[0] = RESP_CODE_OK;
@@ -262,9 +276,54 @@ bool MyMesh::isAutoAddEnabled() const {
262276
return (_prefs.manual_add_contacts & 1) == 0;
263277
}
264278

279+
bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const {
280+
if ((_prefs.manual_add_contacts & 1) == 0) {
281+
return true;
282+
}
283+
284+
uint8_t type_bit = 0;
285+
switch (contact_type) {
286+
case ADV_TYPE_CHAT:
287+
type_bit = AUTO_ADD_CHAT;
288+
break;
289+
case ADV_TYPE_REPEATER:
290+
type_bit = AUTO_ADD_REPEATER;
291+
break;
292+
case ADV_TYPE_ROOM:
293+
type_bit = AUTO_ADD_ROOM_SERVER;
294+
break;
295+
case ADV_TYPE_SENSOR:
296+
type_bit = AUTO_ADD_SENSOR;
297+
break;
298+
default:
299+
return false; // Unknown type, don't auto-add
300+
}
301+
302+
return (_prefs.autoadd_config & type_bit) != 0;
303+
}
304+
305+
bool MyMesh::shouldOverwriteWhenFull() const {
306+
return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
307+
}
308+
309+
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
310+
if (_serial->isConnected()) {
311+
out_frame[0] = PUSH_CODE_CONTACT_DELETED;
312+
memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE);
313+
_serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE);
314+
}
315+
}
316+
317+
void MyMesh::onContactsFull() {
318+
if (_serial->isConnected()) {
319+
out_frame[0] = PUSH_CODE_CONTACTS_FULL;
320+
_serial->writeFrame(out_frame, 1);
321+
}
322+
}
323+
265324
void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) {
266325
if (_serial->isConnected()) {
267-
if (!isAutoAddEnabled() && is_new) {
326+
if (!shouldAutoAddContactType(contact.type) && is_new) {
268327
writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact);
269328
} else {
270329
out_frame[0] = PUSH_CODE_ADVERT;
@@ -803,6 +862,7 @@ void MyMesh::begin(bool has_display) {
803862

804863
resetContacts();
805864
_store->loadContacts(this);
865+
bootstrapRTCfromContacts();
806866
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
807867
_store->loadChannels(this);
808868

@@ -1663,6 +1723,15 @@ void MyMesh::handleCmdFrame(size_t len) {
16631723
} else {
16641724
writeErrFrame(ERR_CODE_TABLE_FULL);
16651725
}
1726+
} else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) {
1727+
_prefs.autoadd_config = cmd_frame[1];
1728+
savePrefs();
1729+
writeOKFrame();
1730+
} else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) {
1731+
int i = 0;
1732+
out_frame[i++] = RESP_CODE_AUTOADD_CONFIG;
1733+
out_frame[i++] = _prefs.autoadd_config;
1734+
_serial->writeFrame(out_frame, i);
16661735
} else {
16671736
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
16681737
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);

examples/companion_radio/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
114114

115115
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
116116
bool isAutoAddEnabled() const override;
117+
bool shouldAutoAddContactType(uint8_t type) const override;
118+
bool shouldOverwriteWhenFull() const override;
119+
void onContactsFull() override;
120+
void onContactOverwrite(const uint8_t* pub_key) override;
117121
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
118122
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
119123
void onContactPathUpdated(const ContactInfo &contact) override;

examples/companion_radio/NodePrefs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ struct NodePrefs { // persisted to file
2727
uint8_t buzzer_quiet;
2828
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
2929
uint32_t gps_interval; // GPS read interval in seconds
30+
uint8_t autoadd_config; // bitmask for auto-add contacts config
3031
};

src/helpers/BaseChatMesh.cpp

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,54 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
5555
}
5656
}
5757

58+
void BaseChatMesh::bootstrapRTCfromContacts() {
59+
uint32_t latest = 0;
60+
for (int i = 0; i < num_contacts; i++) {
61+
if (contacts[i].lastmod > latest) {
62+
latest = contacts[i].lastmod;
63+
}
64+
}
65+
if (latest != 0) {
66+
getRTCClock()->setCurrentTime(latest + 1);
67+
}
68+
}
69+
70+
ContactInfo* BaseChatMesh::allocateContactSlot() {
71+
if (num_contacts < MAX_CONTACTS) {
72+
return &contacts[num_contacts++];
73+
} else if (shouldOverwriteWhenFull()) {
74+
// Find oldest non-favourite contact by oldest lastmod timestamp
75+
int oldest_idx = -1;
76+
uint32_t oldest_lastmod = 0xFFFFFFFF;
77+
for (int i = 0; i < num_contacts; i++) {
78+
bool is_favourite = (contacts[i].flags & 0x01) != 0;
79+
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
80+
oldest_lastmod = contacts[i].lastmod;
81+
oldest_idx = i;
82+
}
83+
}
84+
if (oldest_idx >= 0) {
85+
onContactOverwrite(contacts[oldest_idx].id.pub_key);
86+
return &contacts[oldest_idx];
87+
}
88+
}
89+
return NULL; // no space, no overwrite or all contacts are all favourites
90+
}
91+
92+
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
93+
memset(&ci, 0, sizeof(ci));
94+
ci.id = id;
95+
ci.out_path_len = -1; // initially out_path is unknown
96+
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
97+
ci.type = parser.getType();
98+
if (parser.hasLatLon()) {
99+
ci.gps_lat = parser.getIntLat();
100+
ci.gps_lon = parser.getIntLon();
101+
}
102+
ci.last_advert_timestamp = timestamp;
103+
ci.lastmod = getRTCClock()->getCurrentTime();
104+
}
105+
58106
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
59107
AdvertDataParser parser(app_data, app_data_len);
60108
if (!(parser.isValid() && parser.hasName())) {
@@ -87,48 +135,37 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
87135

88136
bool is_new = false;
89137
if (from == NULL) {
90-
if (!isAutoAddEnabled()) {
138+
if (!shouldAutoAddContactType(parser.getType())) {
91139
ContactInfo ci;
92-
memset(&ci, 0, sizeof(ci));
93-
ci.id = id;
94-
ci.out_path_len = -1; // initially out_path is unknown
95-
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
96-
ci.type = parser.getType();
97-
if (parser.hasLatLon()) {
98-
ci.gps_lat = parser.getIntLat();
99-
ci.gps_lon = parser.getIntLon();
100-
}
101-
ci.last_advert_timestamp = timestamp;
102-
ci.lastmod = getRTCClock()->getCurrentTime();
140+
populateContactFromAdvert(ci, id, parser, timestamp);
103141
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
104142
return;
105143
}
106144

107145
is_new = true;
108-
if (num_contacts < MAX_CONTACTS) {
109-
from = &contacts[num_contacts++];
110-
from->id = id;
111-
from->out_path_len = -1; // initially out_path is unknown
112-
from->gps_lat = 0; // initially unknown GPS loc
113-
from->gps_lon = 0;
114-
from->sync_since = 0;
115-
116-
from->shared_secret_valid = false; // ecdh shared_secret will be calculated later on demand
117-
} else {
118-
MESH_DEBUG_PRINTLN("onAdvertRecv: contacts table is full!");
146+
from = allocateContactSlot();
147+
if (from == NULL) {
148+
ContactInfo ci;
149+
populateContactFromAdvert(ci, id, parser, timestamp);
150+
onDiscoveredContact(ci, true, packet->path_len, packet->path);
151+
onContactsFull();
152+
MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
119153
return;
120154
}
155+
156+
populateContactFromAdvert(*from, id, parser, timestamp);
157+
from->sync_since = 0;
158+
from->shared_secret_valid = false;
121159
}
122-
123160
// update
124-
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
125-
from->type = parser.getType();
126-
if (parser.hasLatLon()) {
127-
from->gps_lat = parser.getIntLat();
128-
from->gps_lon = parser.getIntLon();
129-
}
130-
from->last_advert_timestamp = timestamp;
131-
from->lastmod = getRTCClock()->getCurrentTime();
161+
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
162+
from->type = parser.getType();
163+
if (parser.hasLatLon()) {
164+
from->gps_lat = parser.getIntLat();
165+
from->gps_lon = parser.getIntLon();
166+
}
167+
from->last_advert_timestamp = timestamp;
168+
from->lastmod = getRTCClock()->getCurrentTime();
132169

133170
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
134171
}
@@ -722,10 +759,9 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre
722759
}
723760

724761
bool BaseChatMesh::addContact(const ContactInfo& contact) {
725-
if (num_contacts < MAX_CONTACTS) {
726-
auto dest = &contacts[num_contacts++];
762+
ContactInfo* dest = allocateContactSlot();
763+
if (dest) {
727764
*dest = contact;
728-
729765
dest->shared_secret_valid = false; // mark shared_secret as needing calculation
730766
return true; // success
731767
}

src/helpers/BaseChatMesh.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,17 @@ class BaseChatMesh : public mesh::Mesh {
8888
memset(connections, 0, sizeof(connections));
8989
}
9090

91+
void bootstrapRTCfromContacts();
9192
void resetContacts() { num_contacts = 0; }
93+
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
94+
ContactInfo* allocateContactSlot(); // helper to find slot for new contact
9295

9396
// 'UI' concepts, for sub-classes to implement
9497
virtual bool isAutoAddEnabled() const { return true; }
98+
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
99+
virtual void onContactsFull() {};
100+
virtual bool shouldOverwriteWhenFull() const { return false; }
101+
virtual void onContactOverwrite(const uint8_t* pub_key) {};
95102
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
96103
virtual ContactInfo* processAck(const uint8_t *data) = 0;
97104
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;

0 commit comments

Comments
 (0)