mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
* simple_secure_chat now with a proper CLI
* new: BaseChatMesh class, for abstract chat client
This commit is contained in:
@@ -1,13 +1,20 @@
|
|||||||
#include <Arduino.h> // needed for PlatformIO
|
#include <Arduino.h> // needed for PlatformIO
|
||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
|
|
||||||
|
#if defined(NRF52_PLATFORM)
|
||||||
|
#include <InternalFileSystem.h>
|
||||||
|
#elif defined(ESP32)
|
||||||
|
#include <SPIFFS.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#define RADIOLIB_STATIC_ONLY 1
|
#define RADIOLIB_STATIC_ONLY 1
|
||||||
#include <RadioLib.h>
|
#include <RadioLib.h>
|
||||||
#include <helpers/RadioLibWrappers.h>
|
#include <helpers/RadioLibWrappers.h>
|
||||||
#include <helpers/ArduinoHelpers.h>
|
#include <helpers/ArduinoHelpers.h>
|
||||||
#include <helpers/StaticPoolPacketManager.h>
|
#include <helpers/StaticPoolPacketManager.h>
|
||||||
#include <helpers/SimpleMeshTables.h>
|
#include <helpers/SimpleMeshTables.h>
|
||||||
#include <helpers/AdvertDataHelpers.h>
|
#include <helpers/IdentityStore.h>
|
||||||
|
#include <RTClib.h>
|
||||||
|
|
||||||
/* ---------------------------------- CONFIGURATION ------------------------------------- */
|
/* ---------------------------------- CONFIGURATION ------------------------------------- */
|
||||||
|
|
||||||
@@ -27,22 +34,31 @@
|
|||||||
#define LORA_TX_POWER 20
|
#define LORA_TX_POWER 20
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
//#define RUN_AS_ALICE true
|
#ifndef MAX_CONTACTS
|
||||||
|
#define MAX_CONTACTS 100
|
||||||
#if RUN_AS_ALICE
|
|
||||||
#define USER_NAME "Alice"
|
|
||||||
const char* alice_private = "B8830658388B2DDF22C3A508F4386975970CDE1E2A2A495C8F3B5727957A97629255A1392F8BA4C26A023A0DAB78BFC64D261C8E51507496DD39AFE3707E7B42";
|
|
||||||
#else
|
|
||||||
#define USER_NAME "Bob"
|
|
||||||
const char *bob_private = "30BAA23CCB825D8020A59C936D0AB7773B07356020360FC77192813640BAD375E43BBF9A9A7537E4B9614610F1F2EF874AAB390BA9B0C2F01006B01FDDFEFF0C";
|
|
||||||
#endif
|
#endif
|
||||||
const char *alice_public = "106A5136EC0DD797650AD204C065CF9B66095F6ED772B0822187785D65E11B1F";
|
|
||||||
const char *bob_public = "020BCEDAC07D709BD8507EC316EB5A7FF2F0939AF5057353DCE7E4436A1B9681";
|
|
||||||
|
|
||||||
#ifdef HELTEC_LORA_V3
|
#include <helpers/BaseChatMesh.h>
|
||||||
|
|
||||||
|
#define SEND_TIMEOUT_BASE_MILLIS 300
|
||||||
|
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
|
||||||
|
#define DIRECT_SEND_PERHOP_FACTOR 4.0f
|
||||||
|
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 200
|
||||||
|
|
||||||
|
|
||||||
|
#if defined(HELTEC_LORA_V3)
|
||||||
#include <helpers/HeltecV3Board.h>
|
#include <helpers/HeltecV3Board.h>
|
||||||
#include <helpers/CustomSX1262Wrapper.h>
|
#include <helpers/CustomSX1262Wrapper.h>
|
||||||
static HeltecV3Board board;
|
static HeltecV3Board board;
|
||||||
|
#elif defined(ARDUINO_XIAO_ESP32C3)
|
||||||
|
#include <helpers/XiaoC3Board.h>
|
||||||
|
#include <helpers/CustomSX1262Wrapper.h>
|
||||||
|
#include <helpers/CustomSX1268Wrapper.h>
|
||||||
|
static XiaoC3Board board;
|
||||||
|
#elif defined(SEEED_XIAO_S3)
|
||||||
|
#include <helpers/ESP32Board.h>
|
||||||
|
#include <helpers/CustomSX1262Wrapper.h>
|
||||||
|
static ESP32Board board;
|
||||||
#elif defined(RAK_4631)
|
#elif defined(RAK_4631)
|
||||||
#include <helpers/RAK4631Board.h>
|
#include <helpers/RAK4631Board.h>
|
||||||
#include <helpers/CustomSX1262Wrapper.h>
|
#include <helpers/CustomSX1262Wrapper.h>
|
||||||
@@ -51,234 +67,278 @@
|
|||||||
#error "need to provide a 'board' object"
|
#error "need to provide a 'board' object"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define SEND_TIMEOUT_BASE_MILLIS 300
|
|
||||||
#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f
|
|
||||||
#define DIRECT_SEND_PERHOP_FACTOR 4.0f
|
|
||||||
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 100
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------------------- */
|
||||||
|
|
||||||
static unsigned long txt_send_timeout;
|
|
||||||
static int curr_contact_idx = 0;
|
static int curr_contact_idx = 0;
|
||||||
|
|
||||||
#define MAX_CONTACTS 8
|
class MyMesh : public BaseChatMesh, ContactVisitor {
|
||||||
#define MAX_SEARCH_RESULTS 2
|
FILESYSTEM* _fs;
|
||||||
|
uint32_t expected_ack_crc;
|
||||||
|
unsigned long last_msg_sent;
|
||||||
|
ContactInfo* curr_recipient;
|
||||||
|
char command[MAX_TEXT_LEN+1];
|
||||||
|
|
||||||
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
|
const char* getTypeName(uint8_t type) const {
|
||||||
|
if (type == ADV_TYPE_CHAT) return "Chat";
|
||||||
|
if (type == ADV_TYPE_REPEATER) return "Repeater";
|
||||||
|
if (type == ADV_TYPE_ROOM) return "Room";
|
||||||
|
return "??"; // unknown
|
||||||
|
}
|
||||||
|
|
||||||
struct ContactInfo {
|
void loadContacts() {
|
||||||
mesh::Identity id;
|
if (_fs->exists("/contacts")) {
|
||||||
const char* name;
|
File file = _fs->open("/contacts");
|
||||||
int out_path_len;
|
if (file) {
|
||||||
uint8_t out_path[MAX_PATH_SIZE];
|
bool full = false;
|
||||||
uint32_t last_advert_timestamp;
|
while (!full) {
|
||||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
ContactInfo c;
|
||||||
};
|
uint8_t pub_key[32];
|
||||||
|
uint8_t unused;
|
||||||
|
uint32_t reserved;
|
||||||
|
|
||||||
class MyMesh : public mesh::Mesh {
|
bool success = (file.read(pub_key, 32) == 32);
|
||||||
public:
|
success = success && (file.read((uint8_t *) &c.name, 32) == 32);
|
||||||
ContactInfo contacts[MAX_CONTACTS];
|
success = success && (file.read(&c.type, 1) == 1);
|
||||||
int num_contacts;
|
success = success && (file.read(&c.flags, 1) == 1);
|
||||||
|
success = success && (file.read(&unused, 1) == 1);
|
||||||
|
success = success && (file.read((uint8_t *) &reserved, 4) == 4);
|
||||||
|
success = success && (file.read((uint8_t *) &c.out_path_len, 1) == 1);
|
||||||
|
success = success && (file.read((uint8_t *) &c.last_advert_timestamp, 4) == 4);
|
||||||
|
success = success && (file.read(c.out_path, 64) == 64);
|
||||||
|
|
||||||
void addContact(const char* name, const mesh::Identity& id) {
|
if (!success) break; // EOF
|
||||||
if (num_contacts < MAX_CONTACTS) {
|
|
||||||
curr_contact_idx = num_contacts; // auto-select this contact as current selection
|
|
||||||
|
|
||||||
contacts[num_contacts].id = id;
|
c.id = mesh::Identity(pub_key);
|
||||||
contacts[num_contacts].name = strdup(name);
|
if (!addContact(c)) full = true;
|
||||||
contacts[num_contacts].last_advert_timestamp = 0;
|
}
|
||||||
contacts[num_contacts].out_path_len = -1;
|
file.close();
|
||||||
// only need to calculate the shared_secret once, for better performance
|
}
|
||||||
self_id.calcSharedSecret(contacts[num_contacts].shared_secret, id);
|
}
|
||||||
num_contacts++;
|
}
|
||||||
|
|
||||||
|
void saveContacts() {
|
||||||
|
#if defined(NRF52_PLATFORM)
|
||||||
|
File file = _fs->open("/contacts", FILE_O_WRITE);
|
||||||
|
if (file) { file.seek(0); file.truncate(); }
|
||||||
|
#else
|
||||||
|
File file = _fs->open("/contacts", "w", true);
|
||||||
|
#endif
|
||||||
|
if (file) {
|
||||||
|
ContactsIterator iter;
|
||||||
|
ContactInfo c;
|
||||||
|
uint8_t unused = 0;
|
||||||
|
uint32_t reserved = 0;
|
||||||
|
|
||||||
|
while (iter.hasNext(this, c)) {
|
||||||
|
bool success = (file.write(c.id.pub_key, 32) == 32);
|
||||||
|
success = success && (file.write((uint8_t *) &c.name, 32) == 32);
|
||||||
|
success = success && (file.write(&c.type, 1) == 1);
|
||||||
|
success = success && (file.write(&c.flags, 1) == 1);
|
||||||
|
success = success && (file.write(&unused, 1) == 1);
|
||||||
|
success = success && (file.write((uint8_t *) &reserved, 4) == 4);
|
||||||
|
success = success && (file.write((uint8_t *) &c.out_path_len, 1) == 1);
|
||||||
|
success = success && (file.write((uint8_t *) &c.last_advert_timestamp, 4) == 4);
|
||||||
|
success = success && (file.write(c.out_path, 64) == 64);
|
||||||
|
|
||||||
|
if (!success) break; // write failed
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
void onDiscoveredContact(ContactInfo& contact, bool is_new) override {
|
||||||
|
// TODO: if not in favs, prompt to add as fav(?)
|
||||||
|
|
||||||
int searchPeersByHash(const uint8_t* hash) override {
|
Serial.printf("ADVERT from -> %s\n", contact.name);
|
||||||
int n = 0;
|
Serial.printf(" type: %s\n", getTypeName(contact.type));
|
||||||
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
|
Serial.print(" public key: "); mesh::Utils::printHex(Serial, contact.id.pub_key, PUB_KEY_SIZE); Serial.println();
|
||||||
if (contacts[i].id.isHashMatch(hash)) {
|
|
||||||
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
saveContacts();
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#define ADV_TYPE_NONE 0 // unknown
|
void onContactPathUpdated(const ContactInfo& contact) override {
|
||||||
#define ADV_TYPE_CHAT 1
|
Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (int32_t) contact.out_path_len);
|
||||||
#define ADV_TYPE_REPEATER 2
|
saveContacts();
|
||||||
//FUTURE: 3..15
|
|
||||||
|
|
||||||
#define ADV_LATLON_MASK 0x10
|
|
||||||
#define ADV_BATTERY_MASK 0x20
|
|
||||||
#define ADV_TEMPERATURE_MASK 0x40
|
|
||||||
#define ADV_NAME_MASK 0x80
|
|
||||||
|
|
||||||
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override {
|
|
||||||
Serial.print("Valid Advertisement -> ");
|
|
||||||
mesh::Utils::printHex(Serial, id.pub_key, PUB_KEY_SIZE);
|
|
||||||
Serial.println();
|
|
||||||
|
|
||||||
for (int i = 0; i < num_contacts; i++) {
|
|
||||||
ContactInfo& from = contacts[i];
|
|
||||||
if (id.matches(from.id)) { // is from one of our contacts
|
|
||||||
if (timestamp > from.last_advert_timestamp) { // check for replay attacks!!
|
|
||||||
from.last_advert_timestamp = timestamp;
|
|
||||||
Serial.printf(" From contact: %s\n", from.name);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// unknown node
|
|
||||||
AdvertDataParser parser(app_data, app_data_len);
|
|
||||||
if (parser.getType() == ADV_TYPE_CHAT && parser.hasName()) { // is it a 'Chat' node (with a name)?
|
|
||||||
// automatically add to our contacts
|
|
||||||
addContact(parser.getName(), id);
|
|
||||||
Serial.printf(" ADDED contact: %s\n", parser.getName());
|
|
||||||
} else {
|
|
||||||
Serial.printf(" Unknown app_data type: %02X, len=%d\n", app_data[0], app_data_len);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override {
|
bool processAck(const uint8_t *data) override {
|
||||||
int i = matching_peer_indexes[peer_idx];
|
|
||||||
if (i >= 0 && i < num_contacts) {
|
|
||||||
// lookup pre-calculated shared_secret
|
|
||||||
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
|
|
||||||
} else {
|
|
||||||
MESH_DEBUG_PRINTLN("getPeerSHharedSecret: Invalid peer idx: %d", i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override {
|
|
||||||
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
|
|
||||||
int i = matching_peer_indexes[sender_idx];
|
|
||||||
if (i < 0 || i >= num_contacts) {
|
|
||||||
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactInfo& from = contacts[i];
|
|
||||||
|
|
||||||
uint32_t timestamp;
|
|
||||||
memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
|
||||||
uint flags = data[4]; // message attempt number, and other flags
|
|
||||||
|
|
||||||
// len can be > original length, but 'text' will be padded with zeroes
|
|
||||||
data[len] = 0; // need to make a C string again, with null terminator
|
|
||||||
|
|
||||||
//if ( ! alreadyReceived timestamp ) {
|
|
||||||
Serial.printf("(%s) MSG -> from %s\n", packet->isRouteFlood() ? "FLOOD" : "DIRECT", from.name);
|
|
||||||
Serial.printf(" %s\n", (const char *) &data[5]);
|
|
||||||
//}
|
|
||||||
|
|
||||||
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
|
||||||
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
|
|
||||||
|
|
||||||
if (packet->isRouteFlood()) {
|
|
||||||
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
|
||||||
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
|
|
||||||
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
|
||||||
if (path) sendFlood(path);
|
|
||||||
} else {
|
|
||||||
mesh::Packet* ack = createAck(ack_hash);
|
|
||||||
if (ack) {
|
|
||||||
if (from.out_path_len < 0) {
|
|
||||||
sendFlood(ack);
|
|
||||||
} else {
|
|
||||||
sendDirect(ack, from.out_path, from.out_path_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override {
|
|
||||||
int i = matching_peer_indexes[sender_idx];
|
|
||||||
if (i < 0 || i >= num_contacts) {
|
|
||||||
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactInfo& from = contacts[i];
|
|
||||||
Serial.printf("PATH to: %s, path_len=%d\n", from.name, (uint32_t) path_len);
|
|
||||||
|
|
||||||
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
|
|
||||||
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
|
|
||||||
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
|
|
||||||
|
|
||||||
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
|
||||||
// also got an encoded ACK!
|
|
||||||
processAck(extra);
|
|
||||||
}
|
|
||||||
return true; // send reciprocal path if necessary
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override {
|
|
||||||
if (processAck((uint8_t *)&ack_crc)) {
|
|
||||||
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool processAck(const uint8_t *data) {
|
|
||||||
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
|
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
|
||||||
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
|
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
|
||||||
// NOTE: the same ACK can be received multiple times!
|
// NOTE: the same ACK can be received multiple times!
|
||||||
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
|
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
|
||||||
txt_send_timeout = 0;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t crc;
|
//uint32_t crc;
|
||||||
memcpy(&crc, data, 4);
|
//memcpy(&crc, data, 4);
|
||||||
MESH_DEBUG_PRINTLN(" unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
|
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onMessageRecv(const ContactInfo& from, bool was_flood, uint32_t sender_timestamp, const char *text) override {
|
||||||
|
Serial.printf("(%s) MSG -> from %s\n", was_flood ? "FLOOD" : "DIRECT", from.name);
|
||||||
|
Serial.printf(" %s\n", text);
|
||||||
|
|
||||||
|
if (strcmp(text, "clock sync") == 0) { // special text command
|
||||||
|
uint32_t curr = getRTCClock()->getCurrentTime();
|
||||||
|
if (sender_timestamp > curr) {
|
||||||
|
getRTCClock()->setCurrentTime(sender_timestamp + 1);
|
||||||
|
Serial.println(" (OK - clock set!)");
|
||||||
|
} else {
|
||||||
|
Serial.println(" (ERR: clock cannot go backwards)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const override {
|
||||||
|
return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis);
|
||||||
|
}
|
||||||
|
uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override {
|
||||||
|
return SEND_TIMEOUT_BASE_MILLIS +
|
||||||
|
( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSendTimeout() override {
|
||||||
|
Serial.println(" ERROR: timed out, no ACK.");
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uint32_t expected_ack_crc;
|
char self_name[sizeof(ContactInfo::name)];
|
||||||
unsigned long last_msg_sent;
|
|
||||||
|
|
||||||
MyMesh(RadioLibWrapper& radio, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables)
|
MyMesh(RadioLibWrapper& radio, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables)
|
||||||
: mesh::Mesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables)
|
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables)
|
||||||
{
|
{
|
||||||
num_contacts = 0;
|
command[0] = 0;
|
||||||
|
curr_recipient = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
mesh::Packet* composeMsgPacket(ContactInfo& recipient, uint8_t attempt, const char *text) {
|
void begin(FILESYSTEM& fs) {
|
||||||
int text_len = strlen(text);
|
_fs = &fs;
|
||||||
if (text_len > MAX_TEXT_LEN) return NULL;
|
|
||||||
|
|
||||||
uint8_t temp[5+MAX_TEXT_LEN+1];
|
BaseChatMesh::begin();
|
||||||
uint32_t timestamp = getRTCClock()->getCurrentTime();
|
|
||||||
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
|
||||||
temp[4] = attempt;
|
|
||||||
memcpy(&temp[5], text, text_len + 1);
|
|
||||||
|
|
||||||
// calc expected ACK reply
|
strcpy(self_name, "UNSET");
|
||||||
mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
|
IdentityStore store(fs, "/identity");
|
||||||
last_msg_sent = _ms->getMillis();
|
if (!store.load("_main", self_id, self_name, sizeof(self_name))) {
|
||||||
|
self_id = mesh::LocalIdentity(getRNG()); // create new random identity
|
||||||
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.shared_secret, temp, 5 + text_len);
|
store.save("_main", self_id);
|
||||||
}
|
|
||||||
|
|
||||||
void sendSelfAdvert() {
|
|
||||||
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
|
||||||
uint8_t app_data_len;
|
|
||||||
{
|
|
||||||
AdvertDataBuilder builder(ADV_TYPE_CHAT, USER_NAME);
|
|
||||||
app_data_len = builder.encodeTo(app_data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mesh::Packet* adv = createAdvert(self_id, app_data, app_data_len);
|
loadContacts();
|
||||||
if (adv) {
|
}
|
||||||
sendFlood(adv, 800); // add slight delay
|
|
||||||
Serial.println(" (advert sent).");
|
void showWelcome() {
|
||||||
|
Serial.println("===== MeshCore Chat Terminal =====");
|
||||||
|
Serial.println();
|
||||||
|
Serial.printf("WELCOME %s\n", self_name);
|
||||||
|
Serial.println(" (enter 'help' for basic commands)");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContactVisitor
|
||||||
|
void onContactVisit(const ContactInfo& contact) override {
|
||||||
|
Serial.printf(" %s - ", contact.name);
|
||||||
|
char tmp[40];
|
||||||
|
int32_t secs = contact.last_advert_timestamp - getRTCClock()->getCurrentTime();
|
||||||
|
AdvertTimeHelper::formatRelativeTimeDiff(tmp, secs, false);
|
||||||
|
Serial.println(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleCommand(const char* command) {
|
||||||
|
while (*command == ' ') command++; // skip leading spaces
|
||||||
|
|
||||||
|
if (memcmp(command, "send ", 5) == 0) {
|
||||||
|
if (curr_recipient) {
|
||||||
|
const char *text = &command[5];
|
||||||
|
int result = sendMessage(*curr_recipient, 0, text, expected_ack_crc);
|
||||||
|
if (result == MSG_SEND_FAILED) {
|
||||||
|
Serial.println(" ERROR: unable to send.");
|
||||||
|
} else {
|
||||||
|
last_msg_sent = _ms->getMillis();
|
||||||
|
Serial.printf(" (message sent - %s)\n", result == MSG_SEND_SENT_FLOOD ? "FLOOD" : "DIRECT");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println(" ERROR: no recipient selected (use 'to' cmd).");
|
||||||
|
}
|
||||||
|
} else if (memcmp(command, "list", 4) == 0) { // show Contact list, by most recent
|
||||||
|
int n = 0;
|
||||||
|
if (command[4] == ' ') { // optional param, last 'N'
|
||||||
|
n = atoi(&command[5]);
|
||||||
|
}
|
||||||
|
scanRecentContacts(n, this);
|
||||||
|
} else if (strcmp(command, "clock") == 0) { // show current time
|
||||||
|
uint32_t now = getRTCClock()->getCurrentTime();
|
||||||
|
DateTime dt = DateTime(now);
|
||||||
|
Serial.printf( "%02d:%02d - %d/%d/%d UTC\n", dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
|
||||||
|
} else if (memcmp(command, "name ", 5) == 0) { // set name
|
||||||
|
strncpy(self_name, &command[5], sizeof(self_name)-1);
|
||||||
|
self_name[sizeof(self_name)-1] = 0;
|
||||||
|
IdentityStore store(*_fs, "/identity"); // update IdentityStore
|
||||||
|
store.save("_main", self_id, self_name);
|
||||||
|
} else if (memcmp(command, "to ", 3) == 0) { // set current recipient
|
||||||
|
curr_recipient = searchContactsByPrefix(&command[3]);
|
||||||
|
if (curr_recipient) {
|
||||||
|
Serial.printf(" Recipient %s now selected.\n", curr_recipient->name);
|
||||||
|
} else {
|
||||||
|
Serial.println(" Error: Name prefix not found.");
|
||||||
|
}
|
||||||
|
} else if (strcmp(command, "to") == 0) { // show current recipient
|
||||||
|
if (curr_recipient) {
|
||||||
|
Serial.printf(" Current: %s\n", curr_recipient->name);
|
||||||
|
} else {
|
||||||
|
Serial.println(" Err: no recipient selected");
|
||||||
|
}
|
||||||
|
} else if (strcmp(command, "advert") == 0) {
|
||||||
|
auto pkt = createSelfAdvert(self_name);
|
||||||
|
if (pkt) {
|
||||||
|
sendZeroHop(pkt);
|
||||||
|
Serial.println(" (advert sent, zero hop).");
|
||||||
|
} else {
|
||||||
|
Serial.println(" ERR: unable to send");
|
||||||
|
}
|
||||||
|
} else if (strcmp(command, "reset path") == 0) {
|
||||||
|
if (curr_recipient) {
|
||||||
|
resetPathTo(*curr_recipient);
|
||||||
|
saveContacts();
|
||||||
|
Serial.println(" Done.");
|
||||||
|
}
|
||||||
|
} else if (memcmp(command, "help", 4) == 0) {
|
||||||
|
Serial.printf("Hello %s, Commands:\n", self_name);
|
||||||
|
Serial.println(" name <your name>");
|
||||||
|
Serial.println(" clock");
|
||||||
|
Serial.println(" list {n}");
|
||||||
|
Serial.println(" to <recipient name or prefix>");
|
||||||
|
Serial.println(" to");
|
||||||
|
Serial.println(" send <text>");
|
||||||
|
Serial.println(" advert");
|
||||||
|
Serial.println(" reset path");
|
||||||
} else {
|
} else {
|
||||||
Serial.println(" ERROR: unable to create packet.");
|
Serial.print(" ERROR: unknown command: "); Serial.println(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
BaseChatMesh::loop();
|
||||||
|
|
||||||
|
int len = strlen(command);
|
||||||
|
while (Serial.available() && len < sizeof(command)-1) {
|
||||||
|
char c = Serial.read();
|
||||||
|
if (c != '\n') {
|
||||||
|
command[len++] = c;
|
||||||
|
command[len] = 0;
|
||||||
|
}
|
||||||
|
Serial.print(c);
|
||||||
|
}
|
||||||
|
if (len == sizeof(command)-1) { // command buffer full
|
||||||
|
command[sizeof(command)-1] = '\r';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
||||||
|
command[len - 1] = 0; // replace newline with C string null terminator
|
||||||
|
|
||||||
|
handleCommand(command);
|
||||||
|
command[0] = 0; // reset command buffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -299,8 +359,6 @@ void halt() {
|
|||||||
while (1) ;
|
while (1) ;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char command[MAX_TEXT_LEN+1];
|
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
|
|
||||||
@@ -336,91 +394,25 @@ void setup() {
|
|||||||
|
|
||||||
fast_rng.begin(radio.random(0x7FFFFFFF));
|
fast_rng.begin(radio.random(0x7FFFFFFF));
|
||||||
|
|
||||||
#if RUN_AS_ALICE
|
#if defined(NRF52_PLATFORM)
|
||||||
Serial.println(" --- user: Alice ---");
|
InternalFS.begin();
|
||||||
the_mesh.self_id = mesh::LocalIdentity(alice_private, alice_public);
|
the_mesh.begin(InternalFS);
|
||||||
the_mesh.addContact("Bob", mesh::Identity(bob_public));
|
#elif defined(ESP32)
|
||||||
|
SPIFFS.begin(true);
|
||||||
|
the_mesh.begin(SPIFFS);
|
||||||
#else
|
#else
|
||||||
Serial.println(" --- user: Bob ---");
|
#error "need to define filesystem"
|
||||||
the_mesh.self_id = mesh::LocalIdentity(bob_private, bob_public);
|
|
||||||
the_mesh.addContact("Alice", mesh::Identity(alice_public));
|
|
||||||
#endif
|
#endif
|
||||||
Serial.println("Help:");
|
|
||||||
Serial.println(" enter 'adv' to advertise presence to mesh");
|
|
||||||
Serial.println(" enter 'send {message text}' to send a message");
|
|
||||||
|
|
||||||
the_mesh.begin();
|
the_mesh.showWelcome();
|
||||||
|
|
||||||
command[0] = 0;
|
|
||||||
txt_send_timeout = 0;
|
|
||||||
|
|
||||||
// send out initial Advertisement to the mesh
|
// send out initial Advertisement to the mesh
|
||||||
the_mesh.sendSelfAdvert();
|
auto pkt = the_mesh.createSelfAdvert(the_mesh.self_name);
|
||||||
|
if (pkt) {
|
||||||
|
the_mesh.sendFlood(pkt, 1200); // add slight delay
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
int len = strlen(command);
|
|
||||||
while (Serial.available() && len < sizeof(command)-1) {
|
|
||||||
char c = Serial.read();
|
|
||||||
if (c != '\n') {
|
|
||||||
command[len++] = c;
|
|
||||||
command[len] = 0;
|
|
||||||
}
|
|
||||||
Serial.print(c);
|
|
||||||
}
|
|
||||||
if (len == sizeof(command)-1) { // command buffer full
|
|
||||||
command[sizeof(command)-1] = '\r';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len > 0 && command[len - 1] == '\r') { // received complete line
|
|
||||||
command[len - 1] = 0; // replace newline with C string null terminator
|
|
||||||
|
|
||||||
if (memcmp(command, "send ", 5) == 0) {
|
|
||||||
// TODO: some way to select recipient??
|
|
||||||
ContactInfo& recipient = the_mesh.contacts[curr_contact_idx];
|
|
||||||
|
|
||||||
const char *text = &command[5];
|
|
||||||
mesh::Packet* pkt = the_mesh.composeMsgPacket(recipient, 0, text);
|
|
||||||
if (pkt) {
|
|
||||||
uint32_t t = radio.getTimeOnAir(pkt->payload_len + pkt->path_len + 2) / 1000;
|
|
||||||
|
|
||||||
if (recipient.out_path_len < 0) {
|
|
||||||
the_mesh.sendFlood(pkt);
|
|
||||||
txt_send_timeout = the_mesh.futureMillis(SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * t));
|
|
||||||
Serial.printf(" (message sent - FLOOD, t=%d)\n", t);
|
|
||||||
} else {
|
|
||||||
the_mesh.sendDirect(pkt, recipient.out_path, recipient.out_path_len);
|
|
||||||
|
|
||||||
txt_send_timeout = the_mesh.futureMillis(SEND_TIMEOUT_BASE_MILLIS +
|
|
||||||
( (t*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (recipient.out_path_len + 1)));
|
|
||||||
|
|
||||||
Serial.printf(" (message sent - DIRECT, t=%d)\n", t);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Serial.println(" ERROR: unable to create packet.");
|
|
||||||
}
|
|
||||||
} else if (strcmp(command, "adv") == 0) {
|
|
||||||
the_mesh.sendSelfAdvert();
|
|
||||||
} else if (strcmp(command, "key") == 0) {
|
|
||||||
mesh::LocalIdentity new_id(the_mesh.getRNG());
|
|
||||||
new_id.printTo(Serial);
|
|
||||||
} else {
|
|
||||||
Serial.print(" ERROR: unknown command: "); Serial.println(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
command[0] = 0; // reset command buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
if (txt_send_timeout && the_mesh.millisHasNowPassed(txt_send_timeout)) {
|
|
||||||
// failed to get an ACK
|
|
||||||
ContactInfo& recipient = the_mesh.contacts[curr_contact_idx];
|
|
||||||
Serial.println(" ERROR: timed out, no ACK.");
|
|
||||||
|
|
||||||
// path to our contact is now possibly broken, fallback to Flood mode
|
|
||||||
recipient.out_path_len = -1;
|
|
||||||
|
|
||||||
txt_send_timeout = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
the_mesh.loop();
|
the_mesh.loop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,23 +90,17 @@ lib_deps =
|
|||||||
${Heltec_lora32_v3.lib_deps}
|
${Heltec_lora32_v3.lib_deps}
|
||||||
adafruit/RTClib @ ^2.1.3
|
adafruit/RTClib @ ^2.1.3
|
||||||
|
|
||||||
[env:Heltec_v3_chat_alice]
|
[env:Heltec_v3_terminal_chat]
|
||||||
extends = Heltec_lora32_v3
|
extends = Heltec_lora32_v3
|
||||||
build_flags =
|
build_flags =
|
||||||
${Heltec_lora32_v3.build_flags}
|
${Heltec_lora32_v3.build_flags}
|
||||||
-D RUN_AS_ALICE=true
|
-D MAX_CONTACTS=100
|
||||||
-D MESH_PACKET_LOGGING=1
|
; -D MESH_PACKET_LOGGING=1
|
||||||
-D MESH_DEBUG=1
|
; -D MESH_DEBUG=1
|
||||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
|
||||||
|
|
||||||
[env:Heltec_v3_chat_bob]
|
|
||||||
extends = Heltec_lora32_v3
|
|
||||||
build_flags =
|
|
||||||
${Heltec_lora32_v3.build_flags}
|
|
||||||
-D RUN_AS_ALICE=false
|
|
||||||
-D MESH_PACKET_LOGGING=1
|
|
||||||
-D MESH_DEBUG=1
|
|
||||||
build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
||||||
|
lib_deps =
|
||||||
|
${Heltec_lora32_v3.lib_deps}
|
||||||
|
adafruit/RTClib @ ^2.1.3
|
||||||
|
|
||||||
[env:Heltec_v3_test_admin]
|
[env:Heltec_v3_test_admin]
|
||||||
extends = Heltec_lora32_v3
|
extends = Heltec_lora32_v3
|
||||||
@@ -261,20 +255,14 @@ lib_deps =
|
|||||||
${rak4631.lib_deps}
|
${rak4631.lib_deps}
|
||||||
adafruit/RTClib @ ^2.1.3
|
adafruit/RTClib @ ^2.1.3
|
||||||
|
|
||||||
[env:RAK_4631_chat_alice]
|
[env:RAK_4631_terminal_chat]
|
||||||
extends = rak4631
|
extends = rak4631
|
||||||
build_flags =
|
build_flags =
|
||||||
${rak4631.build_flags}
|
${rak4631.build_flags}
|
||||||
-D RUN_AS_ALICE=true
|
-D MAX_CONTACTS=100
|
||||||
-D MESH_PACKET_LOGGING=1
|
|
||||||
-D MESH_DEBUG=1
|
|
||||||
build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
|
||||||
|
|
||||||
[env:RAK_4631_chat_bob]
|
|
||||||
extends = rak4631
|
|
||||||
build_flags =
|
|
||||||
${rak4631.build_flags}
|
|
||||||
-D RUN_AS_ALICE=false
|
|
||||||
-D MESH_PACKET_LOGGING=1
|
-D MESH_PACKET_LOGGING=1
|
||||||
-D MESH_DEBUG=1
|
-D MESH_DEBUG=1
|
||||||
build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
build_src_filter = ${rak4631.build_src_filter} +<../examples/simple_secure_chat/main.cpp>
|
||||||
|
lib_deps =
|
||||||
|
${rak4631.lib_deps}
|
||||||
|
adafruit/RTClib @ ^2.1.3
|
||||||
|
|||||||
@@ -51,3 +51,31 @@
|
|||||||
_valid = true;
|
_valid = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_now, bool short_fmt) {
|
||||||
|
const char *suffix;
|
||||||
|
if (seconds_from_now < 0) {
|
||||||
|
suffix = short_fmt ? "" : " ago";
|
||||||
|
seconds_from_now = -seconds_from_now;
|
||||||
|
} else {
|
||||||
|
suffix = short_fmt ? "" : " from now";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds_from_now < 60) {
|
||||||
|
sprintf(dest, "%d secs %s", seconds_from_now, suffix);
|
||||||
|
} else {
|
||||||
|
int32_t mins = seconds_from_now / 60;
|
||||||
|
if (mins < 60) {
|
||||||
|
sprintf(dest, "%d mins %s", mins, suffix);
|
||||||
|
} else {
|
||||||
|
int32_t hours = mins / 60;
|
||||||
|
if (hours < 24) {
|
||||||
|
sprintf(dest, "%d hours %s", hours, suffix);
|
||||||
|
} else {
|
||||||
|
sprintf(dest, "%d days %s", hours / 24, suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,3 +53,8 @@ public:
|
|||||||
double getLat() const { return ((double)_lat) / 1000000.0; }
|
double getLat() const { return ((double)_lat) / 1000000.0; }
|
||||||
double getLon() const { return ((double)_lon) / 1000000.0; }
|
double getLon() const { return ((double)_lon) / 1000000.0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AdvertTimeHelper {
|
||||||
|
public:
|
||||||
|
static void formatRelativeTimeDiff(char dest[], int32_t seconds_from_now, bool short_fmt);
|
||||||
|
};
|
||||||
|
|||||||
258
src/helpers/BaseChatMesh.cpp
Normal file
258
src/helpers/BaseChatMesh.cpp
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
#include <helpers/BaseChatMesh.h>
|
||||||
|
|
||||||
|
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
|
||||||
|
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
||||||
|
uint8_t app_data_len;
|
||||||
|
{
|
||||||
|
AdvertDataBuilder builder(ADV_TYPE_CHAT, name);
|
||||||
|
app_data_len = builder.encodeTo(app_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createAdvert(self_id, app_data, app_data_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
|
||||||
|
AdvertDataParser parser(app_data, app_data_len);
|
||||||
|
if (!(parser.isValid() && parser.hasName())) {
|
||||||
|
MESH_DEBUG_PRINTLN("onAdvertRecv: invalid app_data, or name is missing: len=%d", app_data_len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactInfo* from = NULL;
|
||||||
|
for (int i = 0; i < num_contacts; i++) {
|
||||||
|
if (id.matches(contacts[i].id)) { // is from one of our contacts
|
||||||
|
from = &contacts[i];
|
||||||
|
if (timestamp <= from->last_advert_timestamp) { // check for replay attacks!!
|
||||||
|
MESH_DEBUG_PRINTLN("onAdvertRecv: Possible replay attack, name: %s", from->name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_new = false;
|
||||||
|
if (from == NULL) {
|
||||||
|
is_new = true;
|
||||||
|
if (num_contacts < MAX_CONTACTS) {
|
||||||
|
from = &contacts[num_contacts++];
|
||||||
|
from->id = id;
|
||||||
|
from->out_path_len = -1; // initially out_path is unknown
|
||||||
|
// only need to calculate the shared_secret once, for better performance
|
||||||
|
self_id.calcSharedSecret(from->shared_secret, id);
|
||||||
|
} else {
|
||||||
|
MESH_DEBUG_PRINTLN("onAdvertRecv: contacts table is full!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update
|
||||||
|
strncpy(from->name, parser.getName(), sizeof(from->name)-1);
|
||||||
|
from->name[sizeof(from->name)-1] = 0;
|
||||||
|
from->type = parser.getType();
|
||||||
|
from->last_advert_timestamp = timestamp;
|
||||||
|
|
||||||
|
onDiscoveredContact(*from, is_new); // let UI know
|
||||||
|
}
|
||||||
|
|
||||||
|
int BaseChatMesh::searchPeersByHash(const uint8_t* hash) {
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
|
||||||
|
if (contacts[i].id.isHashMatch(hash)) {
|
||||||
|
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
|
||||||
|
int i = matching_peer_indexes[peer_idx];
|
||||||
|
if (i >= 0 && i < num_contacts) {
|
||||||
|
// lookup pre-calculated shared_secret
|
||||||
|
memcpy(dest_secret, contacts[i].shared_secret, PUB_KEY_SIZE);
|
||||||
|
} else {
|
||||||
|
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
|
||||||
|
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
|
||||||
|
int i = matching_peer_indexes[sender_idx];
|
||||||
|
if (i < 0 || i >= num_contacts) {
|
||||||
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactInfo& from = contacts[i];
|
||||||
|
|
||||||
|
uint32_t timestamp;
|
||||||
|
memcpy(×tamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
|
||||||
|
uint flags = data[4]; // message attempt number, and other flags
|
||||||
|
|
||||||
|
// len can be > original length, but 'text' will be padded with zeroes
|
||||||
|
data[len] = 0; // need to make a C string again, with null terminator
|
||||||
|
|
||||||
|
//if ( ! alreadyReceived timestamp ) {
|
||||||
|
if ((flags >> 2) == 0) { // plain text msg?
|
||||||
|
onMessageRecv(from, packet->isRouteFlood(), timestamp, (const char *) &data[5]); // let UI know
|
||||||
|
|
||||||
|
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
|
||||||
|
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
|
||||||
|
|
||||||
|
if (packet->isRouteFlood()) {
|
||||||
|
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
|
||||||
|
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
|
||||||
|
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
|
||||||
|
if (path) sendFlood(path);
|
||||||
|
} else {
|
||||||
|
mesh::Packet* ack = createAck(ack_hash);
|
||||||
|
if (ack) {
|
||||||
|
if (from.out_path_len < 0) {
|
||||||
|
sendFlood(ack);
|
||||||
|
} else {
|
||||||
|
sendDirect(ack, from.out_path, from.out_path_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) (flags >> 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||||
|
int i = matching_peer_indexes[sender_idx];
|
||||||
|
if (i < 0 || i >= num_contacts) {
|
||||||
|
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactInfo& from = contacts[i];
|
||||||
|
|
||||||
|
// NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
|
||||||
|
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
|
||||||
|
memcpy(from.out_path, path, from.out_path_len = path_len); // store a copy of path, for sendDirect()
|
||||||
|
|
||||||
|
onContactPathUpdated(from);
|
||||||
|
|
||||||
|
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
||||||
|
// also got an encoded ACK!
|
||||||
|
if (processAck(extra)) {
|
||||||
|
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // send reciprocal path if necessary
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||||
|
if (processAck((uint8_t *)&ack_crc)) {
|
||||||
|
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||||
|
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint8_t attempt, const char *text, uint32_t& expected_ack) {
|
||||||
|
int text_len = strlen(text);
|
||||||
|
if (text_len > MAX_TEXT_LEN) return NULL;
|
||||||
|
|
||||||
|
uint8_t temp[5+MAX_TEXT_LEN+1];
|
||||||
|
uint32_t timestamp = getRTCClock()->getCurrentTime();
|
||||||
|
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
||||||
|
temp[4] = (attempt & 3);
|
||||||
|
memcpy(&temp[5], text, text_len + 1);
|
||||||
|
|
||||||
|
// calc expected ACK reply
|
||||||
|
mesh::Utils::sha256((uint8_t *)&expected_ack, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
|
||||||
|
|
||||||
|
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.shared_secret, temp, 5 + text_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint8_t attempt, const char* text, uint32_t& expected_ack) {
|
||||||
|
mesh::Packet* pkt = composeMsgPacket(recipient, attempt, text, expected_ack);
|
||||||
|
if (pkt == NULL) return MSG_SEND_FAILED;
|
||||||
|
|
||||||
|
uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2);
|
||||||
|
|
||||||
|
int rc;
|
||||||
|
if (recipient.out_path_len < 0) {
|
||||||
|
sendFlood(pkt);
|
||||||
|
txt_send_timeout = futureMillis(calcFloodTimeoutMillisFor(t));
|
||||||
|
rc = MSG_SEND_SENT_FLOOD;
|
||||||
|
} else {
|
||||||
|
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
|
||||||
|
txt_send_timeout = futureMillis(calcDirectTimeoutMillisFor(t, recipient.out_path_len));
|
||||||
|
rc = MSG_SEND_SENT_DIRECT;
|
||||||
|
}
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
|
||||||
|
if (recipient.out_path_len >= 0) {
|
||||||
|
recipient.out_path_len = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static ContactInfo* table; // pass via global :-(
|
||||||
|
|
||||||
|
static int cmp_adv_timestamp(const void *a, const void *b) {
|
||||||
|
int a_idx = *((int *)a);
|
||||||
|
int b_idx = *((int *)b);
|
||||||
|
if (table[b_idx].last_advert_timestamp > table[a_idx].last_advert_timestamp) return 1;
|
||||||
|
if (table[b_idx].last_advert_timestamp < table[a_idx].last_advert_timestamp) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::scanRecentContacts(int last_n, ContactVisitor* visitor) {
|
||||||
|
for (int i = 0; i < num_contacts; i++) { // sort the INDEXES into contacts[]
|
||||||
|
sort_array[i] = i;
|
||||||
|
}
|
||||||
|
table = contacts; // pass via global *sigh* :-(
|
||||||
|
qsort(sort_array, num_contacts, sizeof(sort_array[0]), cmp_adv_timestamp);
|
||||||
|
|
||||||
|
if (last_n == 0) {
|
||||||
|
last_n = num_contacts; // scan ALL
|
||||||
|
} else {
|
||||||
|
if (last_n > num_contacts) last_n = num_contacts;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < last_n; i++) {
|
||||||
|
visitor->onContactVisit(contacts[sort_array[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) {
|
||||||
|
int len = strlen(name_prefix);
|
||||||
|
for (int i = 0; i < num_contacts; i++) {
|
||||||
|
auto c = &contacts[i];
|
||||||
|
if (memcmp(c->name, name_prefix, len) == 0) return c;
|
||||||
|
}
|
||||||
|
return NULL; // not found
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BaseChatMesh::addContact(const ContactInfo& contact) {
|
||||||
|
if (num_contacts < MAX_CONTACTS) {
|
||||||
|
auto dest = &contacts[num_contacts++];
|
||||||
|
*dest = contact;
|
||||||
|
|
||||||
|
// calc the ECDH shared secret (just once for performance)
|
||||||
|
self_id.calcSharedSecret(dest->shared_secret, contact.id);
|
||||||
|
|
||||||
|
return true; // success
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) {
|
||||||
|
if (next_idx >= mesh->num_contacts) return false;
|
||||||
|
|
||||||
|
dest = mesh->contacts[next_idx++];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BaseChatMesh::loop() {
|
||||||
|
Mesh::loop();
|
||||||
|
|
||||||
|
if (txt_send_timeout && millisHasNowPassed(txt_send_timeout)) {
|
||||||
|
// failed to get an ACK
|
||||||
|
onSendTimeout();
|
||||||
|
txt_send_timeout = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/helpers/BaseChatMesh.h
Normal file
88
src/helpers/BaseChatMesh.h
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h> // needed for PlatformIO
|
||||||
|
#include <Mesh.h>
|
||||||
|
#include <helpers/AdvertDataHelpers.h>
|
||||||
|
|
||||||
|
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
|
||||||
|
|
||||||
|
struct ContactInfo {
|
||||||
|
mesh::Identity id;
|
||||||
|
char name[32];
|
||||||
|
uint8_t type; // on of ADV_TYPE_*
|
||||||
|
uint8_t flags;
|
||||||
|
int8_t out_path_len;
|
||||||
|
uint8_t out_path[MAX_PATH_SIZE];
|
||||||
|
uint32_t last_advert_timestamp;
|
||||||
|
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
#define MAX_SEARCH_RESULTS 8
|
||||||
|
|
||||||
|
#define MSG_SEND_FAILED 0
|
||||||
|
#define MSG_SEND_SENT_FLOOD 1
|
||||||
|
#define MSG_SEND_SENT_DIRECT 2
|
||||||
|
|
||||||
|
class ContactVisitor {
|
||||||
|
public:
|
||||||
|
virtual void onContactVisit(const ContactInfo& contact) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BaseChatMesh;
|
||||||
|
|
||||||
|
class ContactsIterator {
|
||||||
|
int next_idx = 0;
|
||||||
|
public:
|
||||||
|
bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief abstract Mesh class for common 'chat' client
|
||||||
|
*/
|
||||||
|
class BaseChatMesh : public mesh::Mesh {
|
||||||
|
|
||||||
|
friend class ContactsIterator;
|
||||||
|
|
||||||
|
ContactInfo contacts[MAX_CONTACTS];
|
||||||
|
int num_contacts;
|
||||||
|
int sort_array[MAX_CONTACTS];
|
||||||
|
int matching_peer_indexes[MAX_SEARCH_RESULTS];
|
||||||
|
unsigned long txt_send_timeout;
|
||||||
|
|
||||||
|
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint8_t attempt, const char *text, uint32_t& expected_ack);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
|
||||||
|
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
|
||||||
|
{
|
||||||
|
num_contacts = 0;
|
||||||
|
txt_send_timeout = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'UI' concepts, for sub-classes to implement
|
||||||
|
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new) = 0;
|
||||||
|
virtual bool processAck(const uint8_t *data) = 0;
|
||||||
|
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
|
||||||
|
virtual void onMessageRecv(const ContactInfo& contact, bool was_flood, uint32_t sender_timestamp, const char *text) = 0;
|
||||||
|
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
|
||||||
|
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
|
||||||
|
virtual void onSendTimeout() = 0;
|
||||||
|
|
||||||
|
// Mesh overrides
|
||||||
|
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
|
||||||
|
int searchPeersByHash(const uint8_t* hash) override;
|
||||||
|
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
|
||||||
|
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
|
||||||
|
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||||
|
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
mesh::Packet* createSelfAdvert(const char* name);
|
||||||
|
int sendMessage(const ContactInfo& recipient, uint8_t attempt, const char* text, uint32_t& expected_ack);
|
||||||
|
void resetPathTo(ContactInfo& recipient);
|
||||||
|
void scanRecentContacts(int last_n, ContactVisitor* visitor);
|
||||||
|
ContactInfo* searchContactsByPrefix(const char* name_prefix);
|
||||||
|
bool addContact(const ContactInfo& contact);
|
||||||
|
|
||||||
|
void loop();
|
||||||
|
};
|
||||||
@@ -14,6 +14,25 @@ bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
|
|||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz) {
|
||||||
|
bool loaded = false;
|
||||||
|
char filename[40];
|
||||||
|
sprintf(filename, "%s/%s.id", _dir, name);
|
||||||
|
if (_fs->exists(filename)) {
|
||||||
|
File file = _fs->open(filename);
|
||||||
|
if (file) {
|
||||||
|
loaded = id.readFrom(file);
|
||||||
|
|
||||||
|
int n = min(32, max_name_sz); // up to 32 bytes
|
||||||
|
file.read((uint8_t *) display_name, n);
|
||||||
|
display_name[n - 1] = 0; // ensure null terminator
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
|
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
|
||||||
char filename[40];
|
char filename[40];
|
||||||
sprintf(filename, "%s/%s.id", _dir, name);
|
sprintf(filename, "%s/%s.id", _dir, name);
|
||||||
@@ -31,3 +50,29 @@ bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char display_name[]) {
|
||||||
|
char filename[40];
|
||||||
|
sprintf(filename, "%s/%s.id", _dir, name);
|
||||||
|
|
||||||
|
#if defined(NRF52_PLATFORM)
|
||||||
|
File file = _fs->open(filename, FILE_O_WRITE);
|
||||||
|
if (file) { file.seek(0); file.truncate(); }
|
||||||
|
#else
|
||||||
|
File file = _fs->open(filename, "w", true);
|
||||||
|
#endif
|
||||||
|
if (file) {
|
||||||
|
id.writeTo(file);
|
||||||
|
|
||||||
|
uint8_t tmp[32];
|
||||||
|
memset(tmp, 0, sizeof(tmp));
|
||||||
|
int n = strlen(display_name);
|
||||||
|
if (n > sizeof(tmp)-1) n = sizeof(tmp)-1;
|
||||||
|
memcpy(tmp, display_name, n);
|
||||||
|
file.write(tmp, sizeof(tmp));
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,5 +20,7 @@ public:
|
|||||||
|
|
||||||
void begin() { _fs->mkdir(_dir); }
|
void begin() { _fs->mkdir(_dir); }
|
||||||
bool load(const char *name, mesh::LocalIdentity& id);
|
bool load(const char *name, mesh::LocalIdentity& id);
|
||||||
|
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
|
||||||
bool save(const char *name, const mesh::LocalIdentity& id);
|
bool save(const char *name, const mesh::LocalIdentity& id);
|
||||||
|
bool save(const char *name, const mesh::LocalIdentity& id, const char display_name[]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user