diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp new file mode 100644 index 00000000..c854857f --- /dev/null +++ b/examples/companion_radio/main.cpp @@ -0,0 +1,614 @@ +#include // needed for PlatformIO +#include + +#if defined(NRF52_PLATFORM) + #include +#elif defined(ESP32) + #include +#endif + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---------------------------------- CONFIGURATION ------------------------------------- */ + +#ifndef LORA_FREQ + #define LORA_FREQ 915.0 +#endif +#ifndef LORA_BW + #define LORA_BW 250 +#endif +#ifndef LORA_SF + #define LORA_SF 10 +#endif +#ifndef LORA_CR + #define LORA_CR 5 +#endif +#ifndef LORA_TX_POWER + #define LORA_TX_POWER 20 +#endif + +#ifndef MAX_CONTACTS + #define MAX_CONTACTS 100 +#endif + +#include + +#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 + +#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" + +#if defined(HELTEC_LORA_V3) + #include + #include + static HeltecV3Board board; +#elif defined(ARDUINO_XIAO_ESP32C3) + #include + #include + #include + static XiaoC3Board board; +#elif defined(SEEED_XIAO_S3) + #include + #include + static ESP32Board board; +#elif defined(RAK_4631) + #include + #include + static RAK4631Board board; +#else + #error "need to provide a 'board' object" +#endif + +// Believe it or not, this std C function is busted on some platforms! +static uint32_t _atoi(const char* sp) { + uint32_t n = 0; + while (*sp && *sp >= '0' && *sp <= '9') { + n *= 10; + n += (*sp++ - '0'); + } + return n; +} + +/*------------ Frame Protocol --------------*/ + +#define CMD_APP_START 1 +#define CMD_SEND_TXT_MSG 2 +#define CMD_SEND_CHANNEL_TXT_MSG 3 +#define CMD_GET_CONTACTS 4 // with optional 'since' (for efficient sync) +#define CMD_GET_DEVICE_TIME 5 +#define CMD_SET_DEVICE_TIME 6 +#define CMD_SEND_SELF_ADVERT 7 +#define CMD_SET_ADVERT_NAME 8 +#define CMD_ADD_UPDATE_CONTACT 9 +#define CMD_SYNC_NEXT_MESSAGE 10 + +#define RESP_CODE_OK 0 +#define RESP_CODE_ERR 1 +#define RESP_CODE_CONTACTS_START 2 // first reply to CMD_GET_CONTACTS +#define RESP_CODE_CONTACT 3 // multiple of these (after CMD_GET_CONTACTS) +#define RESP_CODE_END_OF_CONTACTS 4 // last reply to CMD_GET_CONTACTS +#define RESP_CODE_SELF_INFO 5 // reply to CMD_APP_START +#define RESP_CODE_SENT 6 // reply to CMD_SEND_TXT_MSG +#define RESP_CODE_CONTACT_MSG_RECV 7 // a reply to CMD_SYNC_NEXT_MESSAGE +#define RESP_CODE_GROUP_MSG_RECV 8 // a reply to CMD_SYNC_NEXT_MESSAGE + +// these are _pushed_ to client app at any time +#define PUSH_CODE_ADVERT 0x80 +#define PUSH_CODE_PATH_UPDATED 0x81 +#define PUSH_CODE_SEND_CONFIRMED 0x82 +#define PUSH_CODE_MSG_WAITING 0x83 + +/* -------------------------------------------------------------------------------------- */ + +class MyMesh : public BaseChatMesh, ContactVisitor { + FILESYSTEM* _fs; + uint32_t expected_ack_crc; // TODO: keep table of expected ACKs + mesh::GroupChannel* _public; + BaseSerialInterface* _serial; + unsigned long last_msg_sent; + ContactsIterator _iter; + uint32_t _iter_filter_since; + bool _iter_started; + uint8_t cmd_frame[MAX_FRAME_SIZE+1]; + uint8_t out_frame[MAX_FRAME_SIZE+1]; + + void loadContacts() { + if (_fs->exists("/contacts")) { + File file = _fs->open("/contacts"); + if (file) { + bool full = false; + while (!full) { + ContactInfo c; + uint8_t pub_key[32]; + uint8_t unused; + uint32_t reserved; + + bool success = (file.read(pub_key, 32) == 32); + success = success && (file.read((uint8_t *) &c.name, 32) == 32); + success = success && (file.read(&c.type, 1) == 1); + 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); + + if (!success) break; // EOF + + c.id = mesh::Identity(pub_key); + if (!addContact(c)) full = true; + } + file.close(); + } + } + } + + 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(); + } + } + + void writeOKFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_OK; + _serial->writeFrame(buf, 1); + } + void writeErrFrame() { + uint8_t buf[1]; + buf[0] = RESP_CODE_ERR; + _serial->writeFrame(buf, 1); + } + + void writeContactRespFrame(uint8_t code, const ContactInfo& contact) { + int i = 0; + out_frame[i++] = code; + memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; + out_frame[i++] = contact.type; + out_frame[i++] = contact.flags; + out_frame[i++] = contact.out_path_len; + memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); i += MAX_PATH_SIZE; + memcpy(&out_frame[i], contact.name, 32); i += 32; + memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); i += 4; + memcpy(&out_frame[i], &contact.lastmod, 4); i += 4; + _serial->writeFrame(out_frame, i); + } + + void updateContactFromFrame(ContactInfo& contact, const uint8_t* frame) { + int i = 0; + uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT + memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE; + contact.type = frame[i++]; + contact.flags = frame[i++]; + contact.out_path_len = frame[i++]; + memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); i += MAX_PATH_SIZE; + memcpy(contact.name, &frame[i], 32); i += 32; + memcpy(&contact.last_advert_timestamp, &frame[i], 4); i += 4; + } + + void addToOfflineQueue(const uint8_t frame[], int len) { + // TODO + } + int getFromOfflineQueue(uint8_t frame[]) { + return 0; // queue is empty + } + + void soundBuzzer() { + // TODO + } + +protected: + void onDiscoveredContact(ContactInfo& contact, bool is_new) override { + if (_serial->isConnected()) { + out_frame[0] = PUSH_CODE_ADVERT; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); + } else { + soundBuzzer(); + } + + saveContacts(); + } + + void onContactPathUpdated(const ContactInfo& contact) override { + out_frame[0] = PUSH_CODE_PATH_UPDATED; + memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); + _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected + + saveContacts(); + } + + bool processAck(const uint8_t *data) override { + // TODO: see if matches any in a table + if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient + out_frame[0] = PUSH_CODE_SEND_CONFIRMED; + memcpy(&out_frame[1], data, 4); + uint32_t trip_time = _ms->getMillis() - last_msg_sent; + memcpy(&out_frame[5], &trip_time, 4); + _serial->writeFrame(out_frame, 9); + + // NOTE: the same ACK can be received multiple times! + expected_ack_crc = 0; // reset our expected hash, now that we have received ACK + return true; + } + return false; + } + + void onMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override { + int i = 0; + out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV; + memcpy(&out_frame[i], from.id.pub_key, 6); i += 6; // just 6-byte prefix + out_frame[i++] = path_len; + out_frame[i++] = TXT_TYPE_PLAIN; + memcpy(&out_frame[i], &sender_timestamp, 4); i += 4; + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); i += tlen; + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrame(frame, 1); + } else { + soundBuzzer(); + } + } + + void onChannelMessageRecv(const mesh::GroupChannel& channel, int in_path_len, uint32_t timestamp, const char *text) override { + int i = 0; + out_frame[i++] = RESP_CODE_GROUP_MSG_RECV; + out_frame[i++] = in_path_len < 0 ? 0xFF : in_path_len; + out_frame[i++] = TXT_TYPE_PLAIN; + memcpy(&out_frame[i], ×tamp, 4); i += 4; + int tlen = strlen(text); // TODO: UTF-8 ?? + if (i + tlen > MAX_FRAME_SIZE) { + tlen = MAX_FRAME_SIZE - i; + } + memcpy(&out_frame[i], text, tlen); i += tlen; + addToOfflineQueue(out_frame, i); + + if (_serial->isConnected()) { + uint8_t frame[1]; + frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' + _serial->writeFrame(frame, 1); + } else { + soundBuzzer(); + } + } + + 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: + char self_name[sizeof(ContactInfo::name)]; + + MyMesh(RadioLibWrapper& radio, mesh::RNG& rng, mesh::RTCClock& rtc, SimpleMeshTables& tables) + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), _serial(NULL) + { + _iter_started = false; + } + + void begin(FILESYSTEM& fs, BaseSerialInterface& serial) { + _fs = &fs; + _serial = &serial; + + BaseChatMesh::begin(); + + strcpy(self_name, "UNSET"); + IdentityStore store(fs, "/identity"); + if (!store.load("_main", self_id, self_name, sizeof(self_name))) { + self_id = mesh::LocalIdentity(getRNG()); // create new random identity + store.save("_main", self_id); + } + + loadContacts(); + _public = addChannel(PUBLIC_GROUP_PSK); // pre-configure Andy's public channel + } + + // 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 handleCmdFrame(size_t len) { + if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID + uint8_t app_ver = cmd_frame[1]; + // cmd_frame[2..7] reserved future + char* app_name = (char *) &cmd_frame[8]; + cmd_frame[len] = 0; // make app_name null terminated + MESH_DEBUG_PRINTLN("App %s connected, ver: %d", app_name, (uint32_t)app_ver); + + _iter_started = false; // stop any left-over ContactsIterator + int i = 0; + out_frame[i++] = RESP_CODE_SELF_INFO; + out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-) + out_frame[i++] = 0; // reserved + out_frame[i++] = 0; // reserved + memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); i += PUB_KEY_SIZE; + int32_t latlonsats = 0; + memcpy(&out_frame[i], &latlonsats, 4); i += 4; // reserved future, for companion radios with GPS (like T-Beam, T1000) + memcpy(&out_frame[i], &latlonsats, 4); i += 4; + memcpy(&out_frame[i], &latlonsats, 4); i += 4; + int tlen = strlen(self_name); // revisit: UTF_8 ?? + memcpy(&out_frame[i], self_name, tlen); i += tlen; + _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 9) { + int i = 1; + uint8_t attempt_and_flags = cmd_frame[i++]; + uint8_t* pub_key_prefix = &cmd_frame[i]; i += 6; + ContactInfo* recipient = lookupContactByPubKey(pub_key_prefix, 6); + if (recipient && (attempt_and_flags >> 2) == TXT_TYPE_PLAIN) { + char *text = (char *) &cmd_frame[i]; + int tlen = len - i; + text[tlen] = 0; // ensure null + int result = sendMessage(*recipient, attempt_and_flags, text, expected_ack_crc); + // TODO: add expected ACK to table + if (result == MSG_SEND_FAILED) { + writeErrFrame(); + } else { + last_msg_sent = _ms->getMillis(); + + out_frame[0] = RESP_CODE_SENT; + out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; + memcpy(&out_frame[2], &expected_ack_crc, 4); + _serial->writeFrame(out_frame, 6); + } + } else { + writeErrFrame(); // unknown recipient, or unsuported TXT_TYPE_* + } + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg + #if 0 //TODO + uint8_t temp[5+MAX_TEXT_LEN+32]; + uint32_t timestamp = getRTCClock()->getCurrentTime(); + memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique + temp[4] = 0; // attempt and flags + + sprintf((char *) &temp[5], "%s: %s", self_name, &command[7]); // : + temp[5 + MAX_TEXT_LEN] = 0; // truncate if too long + + int len = strlen((char *) &temp[5]); + auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, *_public, temp, 5 + len); + if (pkt) { + sendFlood(pkt); + Serial.println(" Sent."); + } else { + Serial.println(" ERROR: unable to send"); + } + #else + writeErrFrame(); + #endif + } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list + if (_iter_started) { + writeErrFrame(); // iterator is currently busy + } else { + if (len >= 5) { // has optional 'since' param + memcpy(&_iter_filter_since, &cmd_frame[1], 4); + } else { + _iter_filter_since = 0; + } + + uint8_t reply[5]; + reply[0] = RESP_CODE_CONTACTS_START; + uint32_t count = getNumContacts(); // total, NOT filtered count + memcpy(&reply[1], &count, 4); + _serial->writeFrame(reply, 5); + + // start iterator + _iter = startContactsIterator(); + _iter_started = true; + } + } else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) { + int nlen = len - 1; + if (nlen > sizeof(self_name)-1) nlen = sizeof(self_name)-1; // max len + memcpy(self_name, &cmd_frame[1], nlen); + self_name[nlen] = 0; + IdentityStore store(*_fs, "/identity"); // update IdentityStore + store.save("_main", self_id, self_name); + writeOKFrame(); + } else if (cmd_frame[0] == CMD_GET_DEVICE_TIME) { + uint8_t reply[5]; + reply[0] = RESP_CODE_OK; + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply[1], &now, 4); + _serial->writeFrame(reply, 5); + } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { + uint32_t secs; + memcpy(&secs, &cmd_frame[1], 4); + uint32_t curr = getRTCClock()->getCurrentTime(); + if (secs > curr) { + getRTCClock()->setCurrentTime(secs); + writeOKFrame(); + } else { + writeErrFrame(); + } + } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { + auto pkt = createSelfAdvert(self_name); + if (pkt) { + if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) + sendFlood(pkt); + } else { + sendZeroHop(pkt); + } + writeOKFrame(); + } else { + writeErrFrame(); + } + } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1+32+2+1) { + uint8_t* pub_key = &cmd_frame[1]; + ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); + if (recipient) { + updateContactFromFrame(*recipient, cmd_frame); + recipient->lastmod = 0; + saveContacts(); + writeOKFrame(); + } else { + ContactInfo contact; + updateContactFromFrame(contact, cmd_frame); + contact.lastmod = 0; + if (addContact(contact)) { + saveContacts(); + writeOKFrame(); + } else { + writeErrFrame(); // table is full! + } + } + } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { + int out_len; + if ((out_len = getFromOfflineQueue(out_frame)) > 0) { + _serial->writeFrame(out_frame, out_len); + } + } else { + writeErrFrame(); + MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); + } + } + + void loop() { + BaseChatMesh::loop(); + size_t len = _serial->checkRecvFrame(cmd_frame); + if (len > 0) { + handleCmdFrame(len); + } else if (_iter_started // check if our ContactsIterator is 'running' + && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! + ) { + ContactInfo contact; + if (_iter.hasNext(this, contact)) { + if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter + writeContactRespFrame(RESP_CODE_CONTACT, contact); + } + } else { // EOF + out_frame[0] = RESP_CODE_END_OF_CONTACTS; + _serial->writeFrame(out_frame, 1); + _iter_started = false; + } + } + } +}; + +#ifdef ESP32 +#include +SerialBLEInterface serial_interface; +#else +#error "need to define a serial interface" +#endif + +#if defined(NRF52_PLATFORM) +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); +#elif defined(P_LORA_SCLK) +SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +#else +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY); +#endif +StdRNG fast_rng; +SimpleMeshTables tables; +MyMesh the_mesh(*new WRAPPER_CLASS(radio, board), fast_rng, *new VolatileRTCClock(), tables); + +void halt() { + while (1) ; +} + +void setup() { + Serial.begin(115200); + + board.begin(); +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if defined(NRF52_PLATFORM) + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); +#elif defined(P_LORA_SCLK) + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); +#endif + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8, tcxo); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + halt(); + } + + radio.setCRC(0); + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif + +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif + + fast_rng.begin(radio.random(0x7FFFFFFF)); + +#if defined(NRF52_PLATFORM) + InternalFS.begin(); + + the_mesh.begin(InternalFS, serial_interface); +#elif defined(ESP32) + SPIFFS.begin(true); + + serial_interface.begin("MeshCore", BLE_PIN_CODE); + serial_interface.enable(); + + the_mesh.begin(SPIFFS, serial_interface); +#else + #error "need to define filesystem" +#endif +} + +void loop() { + the_mesh.loop(); +} diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 17ba9638..cd76f517 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -119,6 +119,7 @@ class MyMesh : public BaseChatMesh, ContactVisitor { if (!success) break; // EOF c.id = mesh::Identity(pub_key); + c.lastmod = 0; if (!addContact(c)) full = true; } file.close(); @@ -196,8 +197,8 @@ protected: 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); + void onMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override { + Serial.printf("(%s) MSG -> from %s\n", path_len == 0xFF ? "DIRECT" : "FLOOD", from.name); Serial.printf(" %s\n", text); if (strcmp(text, "clock sync") == 0) { // special text command diff --git a/platformio.ini b/platformio.ini index b40ab251..3b21d405 100644 --- a/platformio.ini +++ b/platformio.ini @@ -106,6 +106,22 @@ lib_deps = adafruit/RTClib @ ^2.1.3 densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=1 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/companion_radio/main.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + adafruit/RTClib @ ^2.1.3 + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v3_test_admin] extends = Heltec_lora32_v3 build_flags = diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 380f2307..afa00a53 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -51,6 +51,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, from->name[sizeof(from->name)-1] = 0; from->type = parser.getType(); from->last_advert_timestamp = timestamp; + from->lastmod = getRTCClock()->getCurrentTime(); onDiscoveredContact(*from, is_new); // let UI know } @@ -93,8 +94,8 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender 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 + if ((flags >> 2) == TXT_TYPE_PLAIN) { + onMessageRecv(from, packet->isRouteFlood() ? packet->path_len : 0xFF, 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); @@ -132,6 +133,7 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui // 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() + from.lastmod = getRTCClock()->getCurrentTime(); onContactPathUpdated(from); @@ -213,9 +215,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint8_t attempt, co } void BaseChatMesh::resetPathTo(ContactInfo& recipient) { - if (recipient.out_path_len >= 0) { - recipient.out_path_len = -1; - } + recipient.out_path_len = -1; } static ContactInfo* table; // pass via global :-( @@ -254,6 +254,14 @@ ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) { return NULL; // not found } +ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int prefix_len) { + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + if (memcmp(c->id.pub_key, pub_key, 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++]; @@ -290,6 +298,10 @@ mesh::GroupChannel* BaseChatMesh::addChannel(const char* psk_base64) { } #endif +ContactsIterator BaseChatMesh::startContactsIterator() { + return ContactsIterator(); +} + bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) { if (next_idx >= mesh->num_contacts) return false; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 4a512771..b9a91f08 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -3,6 +3,7 @@ #include // needed for PlatformIO #include #include +#include #define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1) @@ -13,8 +14,9 @@ struct ContactInfo { uint8_t flags; int8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; - uint32_t last_advert_timestamp; + uint32_t last_advert_timestamp; // by THEIR clock uint8_t shared_secret[PUB_KEY_SIZE]; + uint32_t lastmod; // by OUR clock }; #define MAX_SEARCH_RESULTS 8 @@ -74,7 +76,7 @@ protected: 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 void onMessageRecv(const ContactInfo& contact, uint8_t path_len, 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; @@ -98,7 +100,10 @@ public: void resetPathTo(ContactInfo& recipient); void scanRecentContacts(int last_n, ContactVisitor* visitor); ContactInfo* searchContactsByPrefix(const char* name_prefix); + ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len); bool addContact(const ContactInfo& contact); + int getNumContacts() const { return num_contacts; } + ContactsIterator startContactsIterator(); mesh::GroupChannel* addChannel(const char* psk_base64); void loop(); diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h new file mode 100644 index 00000000..0659e71e --- /dev/null +++ b/src/helpers/BaseSerialInterface.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#define MAX_FRAME_SIZE 160 + +class BaseSerialInterface { +protected: + BaseSerialInterface() { } + +public: + virtual void enable() = 0; + virtual void disable() = 0; + virtual bool isEnabled() const = 0; + + virtual bool isConnected() const = 0; + + virtual bool isWriteBusy() const = 0; + virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; + virtual size_t checkRecvFrame(uint8_t dest[]) = 0; +}; diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp new file mode 100644 index 00000000..6e86785d --- /dev/null +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -0,0 +1,239 @@ +#include "SerialBLEInterface.h" + +// See the following for generating UUIDs: +// https://www.uuidgenerator.net/ + +#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID +#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +void SerialBLEInterface::begin(const char* device_name, uint32_t pin_code) { + _pin_code = pin_code; + + // Create the BLE Device + BLEDevice::init(device_name); + BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT); + BLEDevice::setSecurityCallbacks(this); + BLEDevice::setMTU(MAX_FRAME_SIZE); + + BLESecurity sec; + sec.setStaticPIN(pin_code); + sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND); + + //BLEDevice::setPower(ESP_PWR_LVL_N8); + + // Create the BLE Server + pServer = BLEDevice::createServer(); + pServer->setCallbacks(this); + + // Create the BLE Service + pService = pServer->createService(SERVICE_UUID); + + // Create a BLE Characteristic + pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED); + pTxCharacteristic->addDescriptor(new BLE2902()); + + BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); + pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENCRYPTED); + pRxCharacteristic->setCallbacks(this); + + pServer->getAdvertising()->addServiceUUID(SERVICE_UUID); +} + +// -------- BLESecurityCallbacks methods + +uint32_t SerialBLEInterface::onPassKeyRequest() { + BLE_DEBUG_PRINTLN("onPassKeyRequest()"); + return _pin_code; +} + +void SerialBLEInterface::onPassKeyNotify(uint32_t pass_key) { + BLE_DEBUG_PRINTLN("onPassKeyNotify(%u)", pass_key); +} + +bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) { + BLE_DEBUG_PRINTLN("onConfirmPIN(%u)", pass_key); + return true; +} + +bool SerialBLEInterface::onSecurityRequest() { + BLE_DEBUG_PRINTLN("onSecurityRequest()"); + return true; // allow +} + +void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) { + if (cmpl.success) { + BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success"); + //deviceConnected = true; + } else { + BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*"); + + //pServer->removePeerDevice(pServer->getConnId(), true); + pServer->disconnect(pServer->getConnId()); + checkAdvRestart = true; + } +} + +// -------- BLEServerCallbacks methods + +void SerialBLEInterface::onConnect(BLEServer* pServer) { +} + +void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) { + BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id)); +} + +void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) { + BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id)); + + deviceConnected = true; +} + +void SerialBLEInterface::onDisconnect(BLEServer* pServer) { + BLE_DEBUG_PRINTLN("onDisconnect()"); + if (_isEnabled) { + checkAdvRestart = true; + // loop() will detect this on next loop, and set deviceConnected to false + } +} + +// -------- BLECharacteristicCallbacks methods + +void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) { + uint8_t* rxValue = pCharacteristic->getData(); + int len = pCharacteristic->getLength(); + + if (len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); + } else if (recv_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); + } else { + recv_queue[recv_queue_len].len = len; + memcpy(recv_queue[recv_queue_len].buf, rxValue, len); + recv_queue_len++; + } +} + +// ---------- public methods + +void SerialBLEInterface::enable() { + if (_isEnabled) return; + + _isEnabled = true; + clearBuffers(); + + // Start the service + pService->start(); + + // Start advertising + + //pServer->getAdvertising()->setMinInterval(500); + //pServer->getAdvertising()->setMaxInterval(1000); + + pServer->getAdvertising()->start(); + checkAdvRestart = false; +} + +void SerialBLEInterface::disable() { + _isEnabled = false; + + BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); + + pServer->getAdvertising()->stop(); + pService->stop(); + oldDeviceConnected = deviceConnected = false; + checkAdvRestart = false; +} + +size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +#define BLE_WRITE_MIN_INTERVAL 20 + +bool SerialBLEInterface::isWriteBusy() const { + return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write? +} + +size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { + if (send_queue_len > 0 // first, check send queue + && millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart + ) { + _last_write = millis(); + pTxCharacteristic->setValue(send_queue[0].buf, send_queue[0].len); + pTxCharacteristic->notify(); + + BLE_DEBUG_PRINTLN("writeBytes: sz=%d", (uint32_t)send_queue[0].len); + + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } + + if (recv_queue_len > 0) { // check recv queue + size_t len = recv_queue[0].len; // take from top of queue + memcpy(dest, recv_queue[0].buf, len); + + recv_queue_len--; + for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue + recv_queue[i] = recv_queue[i + 1]; + } + return len; + } + + if (pServer->getConnectedCount() == 0) deviceConnected = false; + + if (deviceConnected != oldDeviceConnected) { + if (!deviceConnected) { // disconnecting + clearBuffers(); + + BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting..."); + delay(500); // give the bluetooth stack the chance to get things ready + + //pServer->getAdvertising()->setMinInterval(500); + //pServer->getAdvertising()->setMaxInterval(1000); + + checkAdvRestart = true; + } else { + BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising"); + BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting..."); + // connecting + // do stuff here on connecting + pServer->getAdvertising()->stop(); + checkAdvRestart = false; + } + oldDeviceConnected = deviceConnected; + } + + if (checkAdvRestart) { + if (pServer->getConnectedCount() == 0) { + BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising"); + pServer->getAdvertising()->start(); // re-Start advertising + } + checkAdvRestart = false; + } + return 0; +} + +bool SerialBLEInterface::isConnected() const { + return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0; +} diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h new file mode 100644 index 00000000..5e1ab850 --- /dev/null +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -0,0 +1,83 @@ +#pragma once + +#include "../BaseSerialInterface.h" +#include +#include +#include +#include + +class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { + BLEServer *pServer; + BLEService *pService; + BLECharacteristic * pTxCharacteristic; + bool deviceConnected; + bool oldDeviceConnected; + bool checkAdvRestart; + bool _isEnabled; + uint32_t _pin_code; + unsigned long _last_write; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int recv_queue_len; + Frame recv_queue[FRAME_QUEUE_SIZE]; + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + +protected: + // BLESecurityCallbacks methods + uint32_t onPassKeyRequest() override; + void onPassKeyNotify(uint32_t pass_key) override; + bool onConfirmPIN(uint32_t pass_key) override; + bool onSecurityRequest() override; + void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) override; + + // BLEServerCallbacks methods + void onConnect(BLEServer* pServer) override; + void onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) override; + void onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) override; + void onDisconnect(BLEServer* pServer) override; + + // BLECharacteristicCallbacks methods + void onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) override; + +public: + SerialBLEInterface() { + pServer = NULL; + pService = NULL; + deviceConnected = false; + oldDeviceConnected = false; + checkAdvRestart = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = recv_queue_len = 0; + } + + void begin(const char* device_name, uint32_t pin_code); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + +#if BLE_DEBUG_LOGGING && ARDUINO + #include + #define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__) + #define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__) +#else + #define BLE_DEBUG_PRINT(...) {} + #define BLE_DEBUG_PRINTLN(...) {} +#endif