diff --git a/boards/ebyte_eora-s3.json b/boards/ebyte_eora-s3.json new file mode 100644 index 00000000..96945c1d --- /dev/null +++ b/boards/ebyte_eora-s3.json @@ -0,0 +1,45 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_LILYGO_T3_S3_V1_X", + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_MODE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Ebyte EoRa-S3-XXXTB Radio", + "upload": { + "flash_size": "4MB", + "maximum_ram_size": 327680, + "maximum_size": 4194304, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://www.cdebyte.com/products/EoRa-S3-900TB", + "vendor": "Chengdu Ebyte Electronic Technology Co., Ltd" +} \ No newline at end of file diff --git a/boards/tiny_relay.json b/boards/tiny_relay.json new file mode 100644 index 00000000..517d11f0 --- /dev/null +++ b/boards/tiny_relay.json @@ -0,0 +1,33 @@ +{ + "build": { + "arduino": { + "variant_h": "variant_RAK3172_MODULE.h" + }, + "core": "stm32", + "cpu": "cortex-m4", + "extra_flags": "-DSTM32WL -DSTM32WLxx -DSTM32WLE5xx", + "framework_extra_flags": { + "arduino": "-DUSE_CM4_STARTUP_FILE -DARDUINO_RAK3172_MODULE" + }, + "f_cpu": "48000000L", + "mcu": "stm32wle5ccu", + "product_line": "STM32WLE5xx", + "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U" + }, + "debug": { + "default_tools": ["stlink"], + "jlink_device": "STM32WLE5CC", + "openocd_target": "stm32wlx", + "svd_path": "STM32WLE5_CM4.svd" + }, + "frameworks": ["arduino"], + "name": "BB-STM32WL", + "upload": { + "maximum_ram_size": 65536, + "maximum_size": 262144, + "protocol": "stlink", + "protocols": ["stlink", "jlink"] + }, + "url": "https://www.st.com/en/microcontrollers-microprocessors/stm32wle5cc.html", + "vendor": "YAOYAO" +} diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 1277bba9..0eee45ae 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -41,6 +41,6 @@ public: void disableSerial() { _serial->disable(); } virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; - virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0; + virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void loop() = 0; }; diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 7631b905..f1adb05e 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -42,12 +42,17 @@ static File openWrite(FILESYSTEM* fs, const char* filename) { #endif } +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + static uint32_t _ContactsChannelsTotalBlocks = 0; +#endif + void DataStore::begin() { #if defined(RP2040_PLATFORM) identity_store.begin(); #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + _ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count; checkAdvBlobFile(); #if defined(EXTRAFS) || defined(QSPIFLASH) migrateToSecondaryFS(); @@ -74,14 +79,22 @@ void DataStore::begin() { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) int _countLfsBlock(void *p, lfs_block_t block){ + if (block > _ContactsChannelsTotalBlocks) { + MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block); + return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully + } lfs_size_t *size = (lfs_size_t*) p; *size += 1; - return 0; + return 0; } lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) { lfs_size_t size = 0; - lfs_traverse(fs->_getFS(), _countLfsBlock, &size); + int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size); + if (err) { + MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err); + return 0; + } return size; } #endif diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7847d652..a2c2f8f8 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -243,7 +243,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } else { #ifdef DISPLAY_CLASS - if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage); + if (_ui) _ui->notify(UIEventType::newContactMessage); #endif } @@ -294,7 +294,7 @@ void MyMesh::onContactPathUpdated(const ContactInfo &contact) { dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } -bool MyMesh::processAck(const uint8_t *data) { +ContactInfo* MyMesh::processAck(const uint8_t *data) { // see if matches any in a table for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient @@ -306,7 +306,7 @@ bool MyMesh::processAck(const uint8_t *data) { // NOTE: the same ACK can be received multiple times! expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK - return true; + return expected_ack_table[i].contact; } } return checkConnectionsAck(data); @@ -353,7 +353,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe if (should_display && _ui) { _ui->newMsg(path_len, from.name, text, offline_queue_len); if (!_serial->isConnected()) { - _ui->soundBuzzer(UIEventType::contactMessage); + _ui->notify(UIEventType::contactMessage); } } #endif @@ -412,7 +412,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe _serial->writeFrame(frame, 1); } else { #ifdef DISPLAY_CLASS - if (_ui) _ui->soundBuzzer(UIEventType::channelMessage); + if (_ui) _ui->notify(UIEventType::channelMessage); #endif } #ifdef DISPLAY_CLASS @@ -825,6 +825,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (expected_ack) { expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table expected_ack_table[next_ack_idx].ack = expected_ack; + expected_ack_table[next_ack_idx].contact = recipient; next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index e3235128..0dc9ad61 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -112,7 +112,7 @@ protected: 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; void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override; void onContactPathUpdated(const ContactInfo &contact) override; - bool processAck(const uint8_t *data) override; + ContactInfo* processAck(const uint8_t *data) override; void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text); @@ -205,6 +205,7 @@ private: struct AckTableEntry { unsigned long msg_sent; uint32_t ack; + ContactInfo* contact; }; #define EXPECTED_ACK_TABLE_SIZE 8 AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1e03f086..248b9bd5 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -90,6 +90,7 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; + void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V) @@ -157,10 +158,12 @@ public: int render(DisplayDriver& display) override { char tmp[80]; // node name - display.setCursor(0, 0); display.setTextSize(1); display.setColor(DisplayDriver::GREEN); - display.print(_node_prefs->node_name); + char filtered_name[sizeof(_node_prefs->node_name)]; + display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); + display.setCursor(0, 0); + display.print(filtered_name); // battery voltage renderBatteryIndicator(display, _task->getBattMilliVolts()); @@ -199,8 +202,6 @@ public: for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; if (a->name[0] == 0) continue; // empty slot - display.setCursor(0, y); - display.print(a->name); int secs = _rtc->getCurrentTime() - a->recv_timestamp; if (secs < 60) { sprintf(tmp, "%ds", secs); @@ -209,7 +210,14 @@ public: } else { sprintf(tmp, "%dh", secs / (60*60)); } - display.setCursor(display.width() - display.getTextWidth(tmp) - 1, y); + + int timestamp_width = display.getTextWidth(tmp); + int max_name_width = display.width() - timestamp_width - 1; + + char filtered_recent_name[sizeof(a->name)]; + display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name)); + display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name); + display.setCursor(display.width() - timestamp_width - 1, y); display.print(tmp); } } else if (_page == HomePage::RADIO) { @@ -348,9 +356,7 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::ADVERT) { - #ifdef PIN_BUZZER - _task->soundBuzzer(UIEventType::ack); - #endif + _task->notify(UIEventType::ack); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); } else { @@ -427,11 +433,15 @@ public: display.setCursor(0, 14); display.setColor(DisplayDriver::YELLOW); - display.print(p->origin); + char filtered_origin[sizeof(p->origin)]; + display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin)); + display.print(filtered_origin); display.setCursor(0, 25); display.setColor(DisplayDriver::LIGHT); - display.printWordWrap(p->msg, display.width()); + char filtered_msg[sizeof(p->msg)]; + display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg)); + display.printWordWrap(filtered_msg, display.width()); #if AUTO_OFF_MILLIS==0 // probably e-ink return 10000; // 10 s @@ -483,6 +493,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no buzzer.begin(); #endif +#ifdef PIN_VIBRATION + vibration.begin(); +#endif + ui_started_at = millis(); _alert_expiry = 0; @@ -497,9 +511,9 @@ void UITask::showAlert(const char* text, int duration_millis) { _alert_expiry = millis() + duration_millis; } -void UITask::soundBuzzer(UIEventType bet) { +void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) -switch(bet){ +switch(t){ case UIEventType::contactMessage: // gemini's pick buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); @@ -517,8 +531,16 @@ switch(bet){ break; } #endif + +#ifdef PIN_VIBRATION + // Trigger vibration for all UI events except none + if (t != UIEventType::none) { + vibration.trigger(); + } +#endif } + void UITask::msgRead(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { @@ -687,6 +709,10 @@ void UITask::loop() { #endif } +#ifdef PIN_VIBRATION + vibration.loop(); +#endif + #ifdef AUTO_SHUTDOWN_MILLIVOLTS if (millis() > next_batt_chck) { uint16_t milliVolts = getBattMilliVolts(); @@ -755,11 +781,11 @@ void UITask::toggleGPS() { if (strcmp(_sensors->getSettingName(i), "gps") == 0) { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("GPS: Disabled", 800); } else { _sensors->setSettingValue("gps", "1"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("GPS: Enabled", 800); } _next_refresh = 0; @@ -774,7 +800,7 @@ void UITask::toggleBuzzer() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("Buzzer: ON", 800); } else { buzzer.quiet(true); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 769b2c64..5a087eeb 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -11,6 +11,9 @@ #ifdef PIN_BUZZER #include #endif +#ifdef PIN_VIBRATION + #include +#endif #include "../AbstractUITask.h" #include "../NodePrefs.h" @@ -20,6 +23,9 @@ class UITask : public AbstractUITask { SensorManager* _sensors; #ifdef PIN_BUZZER genericBuzzer buzzer; +#endif +#ifdef PIN_VIBRATION + GenericVibration vibration; #endif unsigned long _next_refresh, _auto_off; NodePrefs* _node_prefs; @@ -71,7 +77,7 @@ public: // from AbstractUITask void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; - void soundBuzzer(UIEventType bet = UIEventType::none) override; + void notify(UIEventType t = UIEventType::none) override; void loop() override; void shutdown(bool restart = false); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 29d995a7..045c955d 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -88,9 +88,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no ui_started_at = millis(); } -void UITask::soundBuzzer(UIEventType bet) { +void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) -switch(bet){ +switch(t){ case UIEventType::contactMessage: // gemini's pick buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); @@ -108,8 +108,8 @@ switch(bet){ break; } #endif -// Serial.print("DBG: Buzzzzzz -> "); -// Serial.println((int) bet); +// Serial.print("DBG: Alert user -> "); +// Serial.println((int) t); } void UITask::msgRead(int msgcount) { @@ -370,7 +370,7 @@ void UITask::handleButtonDoublePress() { MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert"); // ADVERT #ifdef PIN_BUZZER - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); #endif if (the_mesh.advert()) { MESH_DEBUG_PRINTLN("Advert sent!"); @@ -388,7 +388,7 @@ void UITask::handleButtonTriplePress() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "Buzzer: ON"); } else { buzzer.quiet(true); @@ -407,11 +407,11 @@ void UITask::handleButtonQuadruplePress() { if (strcmp(_sensors->getSettingName(i), "gps") == 0) { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "GPS: Disabled"); } else { _sensors->setSettingValue("gps", "1"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "GPS: Enabled"); } break; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index a59ddc41..60cd0d04 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -66,7 +66,7 @@ public: // from AbstractUITask void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; - void soundBuzzer(UIEventType bet = UIEventType::none) override; + void notify(UIEventType t = UIEventType::none) override; void loop() override; void shutdown(bool restart = false); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4265ca6a..4265e1cd 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -43,28 +43,13 @@ #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ #define CLI_REPLY_DELAY_MILLIS 600 - -ClientInfo *MyMesh::putClient(const mesh::Identity &id) { - uint32_t min_time = 0xFFFFFFFF; - ClientInfo *oldest = &known_clients[0]; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].last_activity < min_time) { - oldest = &known_clients[i]; - min_time = oldest->last_activity; - } - if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known - } - - oldest->id = id; - oldest->out_path_len = -1; // initially out_path is unknown - oldest->last_timestamp = 0; - return oldest; -} +#define LAZY_CONTACTS_WRITE_DELAY 5000 void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { #if MAX_NEIGHBOURS // check if neighbours enabled @@ -93,15 +78,61 @@ void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float sn #endif } -int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, - size_t payload_len) { +uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data) { + ClientInfo* client; + if (data[0] == 0) { // blank password, just check if sender is in ACL + client = acl.getClient(sender.pub_key, PUB_KEY_SIZE); + if (client == NULL) { + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Login, sender not in ACL"); + #endif + return 0; + } + } else { + uint8_t perms; + if (strcmp((char *)data, _prefs.password) == 0) { // check for valid admin password + perms = PERM_ACL_ADMIN; + } else if (strcmp((char *)data, _prefs.guest_password) == 0) { // check guest password + perms = PERM_ACL_GUEST; + } else { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", data); +#endif + return 0; + } + + client = acl.putClient(sender, 0); // add to contacts (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("Possible login replay attack!"); + return 0; // FATAL: client table is full -OR- replay attack + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = sender_timestamp; + client->last_activity = getRTCClock()->getCurrentTime(); + client->permissions |= perms; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + + uint32_t now = getRTCClock()->getCurrentTimeUnique(); + memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + reply_data[4] = RESP_SERVER_LOGIN_OK; + reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) + reply_data[6] = client->isAdmin() ? 1 : 0; + reply_data[7] = client->permissions; + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + + return 12; // reply length +} + +int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) { // uint32_t now = getRTCClock()->getCurrentTimeUnique(); // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp - memcpy(reply_data, &sender_timestamp, - 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - switch (payload[0]) { - case REQ_TYPE_GET_STATUS: { // guests can also access this now + if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now RepeaterStats stats; stats.batt_milli_volts = board.getBattMilliVolts(); stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); @@ -125,18 +156,31 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t return 4 + sizeof(stats); // reply_len } - case REQ_TYPE_GET_TELEMETRY_DATA: { + if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific - sensors.querySensors((sender->is_admin ? 0xFF : 0x00) & perm_mask, telemetry); + sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry); uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } + if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { + uint8_t res1 = payload[1]; // reserved for future (extra query params) + uint8_t res2 = payload[2]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; // skip deleted entries + memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix + reply_data[ofs++] = c->permissions; + } + return ofs; + } } return 0; // unknown command } @@ -261,65 +305,26 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m uint32_t timestamp; memcpy(×tamp, data, 4); - bool is_admin; - data[len] = 0; // ensure null terminator - if (strcmp((char *)&data[4], _prefs.password) == 0) { // check for valid password - is_admin = true; - } else if (strcmp((char *)&data[4], _prefs.guest_password) == 0) { // check guest password - is_admin = false; - } else { -#if MESH_DEBUG - MESH_DEBUG_PRINTLN("Invalid password: %s", &data[4]); -#endif - return; - } + uint8_t reply_len = handleLoginReq(sender, secret, timestamp, &data[4]); - auto client = putClient(sender); // add to known clients (if not already known) - if (timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("Possible login replay attack!"); - return; // FATAL: client table is full -OR- replay attack - } - - MESH_DEBUG_PRINTLN("Login success!"); - client->last_timestamp = timestamp; - client->last_activity = getRTCClock()->getCurrentTime(); - client->is_admin = is_admin; - memcpy(client->secret, secret, PUB_KEY_SIZE); - - uint32_t now = getRTCClock()->getCurrentTimeUnique(); - memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp -#if 0 - memcpy(&reply_data[4], "OK", 2); // legacy response -#else - reply_data[4] = RESP_SERVER_LOGIN_OK; - reply_data[5] = 0; // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = is_admin ? 1 : 0; - reply_data[7] = 0; // FUTURE: reserved - getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness -#endif + if (reply_len == 0) return; // invalid request if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet *path = createPathReturn(sender, client->secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 12); + mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 12); - if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT - sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); - } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); - } - } + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); } } } int MyMesh::searchPeersByHash(const uint8_t *hash) { int n = 0; - for (int i = 0; i < MAX_CLIENTS; i++) { - if (known_clients[i].id.isHashMatch(hash)) { + for (int i = 0; i < acl.getNumClients(); i++) { + if (acl.getClientByIdx(i)->id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } } @@ -328,9 +333,9 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) { void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < MAX_CLIENTS) { + if (i >= 0 && i < acl.getNumClients()) { // lookup pre-calculated shared_secret - memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); + memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE); } else { MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); } @@ -352,12 +357,12 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { int i = matching_peer_indexes[sender_idx]; - if (i < 0 || - i >= MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); return; } - auto client = &known_clients[i]; + ClientInfo* client = acl.getClientByIdx(i); + if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) uint32_t timestamp; memcpy(×tamp, data, 4); @@ -388,7 +393,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, } else { MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); } - } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->is_admin) { // a CLI command + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->isAdmin()) { // a CLI command uint32_t sender_timestamp; memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint flags = (data[4] >> 2); // message attempt number, and other flags @@ -457,11 +462,12 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t // TODO: prevent replay attacks int i = matching_peer_indexes[sender_idx]; - if (i >= 0 && - i < MAX_CLIENTS) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); - auto client = &known_clients[i]; + auto client = acl.getClientByIdx(i); + memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); } @@ -480,8 +486,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc , bridge(_mgr, &rtc) #endif { - memset(known_clients, 0, sizeof(known_clients)); next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; set_radio_at = revert_radio_at = 0; _logging = false; @@ -515,6 +521,8 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); + acl.load(_fs); + #ifdef WITH_BRIDGE bridge.begin(); #endif @@ -664,7 +672,43 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply command += 3; } - _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + // handle ACL related commands + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} + char* hex = &command[8]; + char* sp = strchr(hex, ' '); // look for separator char + if (sp == NULL) { + strcpy(reply, "Err - bad params"); + } else { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { + uint8_t perms = atoi(sp); + if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save() + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - invalid params"); + } + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + Serial.println("ACL:"); + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; // skip deleted (or guest) entries + + Serial.printf("%02X ", c->permissions); + mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); + Serial.printf("\n"); + } + reply[0] = 0; + } else{ + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } } void MyMesh::loop() { @@ -698,4 +742,10 @@ void MyMesh::loop() { radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); MESH_DEBUG_PRINTLN("Radio params restored"); } + + // is pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + acl.save(_fs); + dirty_contacts_expiry = 0; + } } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 06e7ebe4..798d31f2 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -52,15 +53,6 @@ struct RepeaterStats { uint32_t total_rx_air_time_secs; }; -struct ClientInfo { - mesh::Identity id; - uint32_t last_timestamp, last_activity; - uint8_t secret[PUB_KEY_SIZE]; - bool is_admin; - int8_t out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; -}; - #ifndef MAX_CLIENTS #define MAX_CLIENTS 32 #endif @@ -91,7 +83,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { NodePrefs _prefs; CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; - ClientInfo known_clients[MAX_CLIENTS]; + ClientACL acl; + unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif @@ -108,8 +101,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ESPNowBridge bridge; #endif - ClientInfo* putClient(const mesh::Identity& id); void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data); int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len); mesh::Packet* createSelfAdvert(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 83d18137..24a3a32f 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -15,9 +15,12 @@ #define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ +#define LAZY_CONTACTS_WRITE_DELAY 5000 + struct ServerStats { uint16_t batt_milli_volts; uint16_t curr_tx_queue_len; @@ -35,38 +38,6 @@ struct ServerStats { uint16_t n_posted, n_post_push; }; -ClientInfo *MyMesh::putClient(const mesh::Identity &id) { - for (int i = 0; i < num_clients; i++) { - if (id.matches(known_clients[i].id)) return &known_clients[i]; // already known - } - ClientInfo *newClient; - if (num_clients < MAX_CLIENTS) { - newClient = &known_clients[num_clients++]; - } else { // table is currently full - // evict least active client - uint32_t oldest_timestamp = 0xFFFFFFFF; - newClient = &known_clients[0]; - for (int i = 0; i < num_clients; i++) { - auto c = &known_clients[i]; - if (c->last_activity < oldest_timestamp) { - oldest_timestamp = c->last_activity; - newClient = c; - } - } - } - newClient->id = id; - newClient->out_path_len = -1; // initially out_path is unknown - newClient->last_timestamp = 0; - return newClient; -} - -void MyMesh::evict(ClientInfo *client) { - client->last_activity = 0; // this slot will now be re-used (will be oldest) - memset(client->id.pub_key, 0, sizeof(client->id.pub_key)); - memset(client->secret, 0, sizeof(client->secret)); - client->pending_ack = 0; -} - void MyMesh::addPost(ClientInfo *client, const char *postData) { // TODO: suggested postData format: /<descrption> posts[next_post_idx].author = client->id; // add to cyclic queue @@ -97,22 +68,22 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { len += text_len; // calc expected ACK reply - mesh::Utils::sha256((uint8_t *)&client->pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE); - client->push_post_timestamp = post.post_timestamp; + mesh::Utils::sha256((uint8_t *)&client->extra.room.pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE); + client->extra.room.push_post_timestamp = post.post_timestamp; - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->secret, reply_data, len); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len); if (reply) { if (client->out_path_len < 0) { sendFlood(reply); - client->ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); + client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); } else { sendDirect(reply, client->out_path, client->out_path_len); - client->ack_timeout = + client->extra.room.ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1)); } _num_post_pushes++; // stats } else { - client->pending_ack = 0; + client->extra.room.pending_ack = 0; MESH_DEBUG_PRINTLN("Unable to push post to client"); } } @@ -120,7 +91,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { uint8_t MyMesh::getUnsyncedCount(ClientInfo *client) { uint8_t count = 0; for (int k = 0; k < MAX_UNSYNCED_POSTS; k++) { - if (posts[k].post_timestamp > client->sync_since // is new post for this Client? + if (posts[k].post_timestamp > client->extra.room.sync_since // is new post for this Client? && !posts[k].author.matches(client->id)) { // don't push posts to the author count++; } @@ -129,12 +100,12 @@ uint8_t MyMesh::getUnsyncedCount(ClientInfo *client) { } bool MyMesh::processAck(const uint8_t *data) { - for (int i = 0; i < num_clients; i++) { - auto client = &known_clients[i]; - if (client->pending_ack && memcmp(data, &client->pending_ack, 4) == 0) { // got an ACK from Client! - client->pending_ack = 0; // clear this, so next push can happen - client->push_failures = 0; - client->sync_since = client->push_post_timestamp; // advance Client's SINCE timestamp, to sync next post + for (int i = 0; i < acl.getNumClients(); i++) { + auto client = acl.getClientByIdx(i); + if (client->extra.room.pending_ack && memcmp(data, &client->extra.room.pending_ack, 4) == 0) { // got an ACK from Client! + client->extra.room.pending_ack = 0; // clear this, so next push can happen + client->extra.room.push_failures = 0; + client->extra.room.sync_since = client->extra.room.push_post_timestamp; // advance Client's SINCE timestamp, to sync next post return true; } } @@ -168,8 +139,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - switch (payload[0]) { - case REQ_TYPE_GET_STATUS: { + if (payload[0] == REQ_TYPE_GET_STATUS) { ServerStats stats; stats.batt_milli_volts = board.getBattMilliVolts(); stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); @@ -193,19 +163,31 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t memcpy(&reply_data[4], &stats, sizeof(stats)); return 4 + sizeof(stats); } - - case REQ_TYPE_GET_TELEMETRY_DATA: { + if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { uint8_t perm_mask = ~(payload[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); // query other sensors -- target specific - sensors.querySensors((sender->permission == RoomPermission::ADMIN ? 0xFF : 0x00) & perm_mask, telemetry); + sensors.querySensors((sender->isAdmin() ? 0xFF : 0x00) & perm_mask, telemetry); uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } + if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { + uint8_t res1 = payload[1]; // reserved for future (extra query params) + uint8_t res2 = payload[2]; + if (res1 == 0 && res2 == 0) { + uint8_t ofs = 4; + for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) { + auto c = acl.getClientByIdx(i); + if (!c->isAdmin()) continue; // skip non-Admin entries + memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix + reply_data[ofs++] = c->permissions; + } + return ofs; + } } return 0; // unknown command } @@ -305,56 +287,71 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m memcpy(&sender_timestamp, data, 4); memcpy(&sender_sync_since, &data[4], 4); // sender's "sync messags SINCE x" timestamp - RoomPermission perm; data[len] = 0; // ensure null terminator - if (strcmp((char *)&data[8], _prefs.password) == 0) { // check for valid admin password - perm = RoomPermission::ADMIN; - } else { - if (strcmp((char *)&data[8], _prefs.guest_password) == 0) { // check the room/public password - perm = RoomPermission::GUEST; - } else if (_prefs.allow_read_only) { - perm = RoomPermission::READ_ONLY; - } else { - MESH_DEBUG_PRINTLN("Incorrect room password"); - return; // no response. Client will timeout + + ClientInfo* client; + if (data[8] == 0 && !_prefs.allow_read_only) { // blank password, just check if sender is in ACL + client = acl.getClient(sender.pub_key, PUB_KEY_SIZE); + if (client == NULL) { + #if MESH_DEBUG + MESH_DEBUG_PRINTLN("Login, sender not in ACL"); + #endif + return; } + } else { + uint8_t perm; + if (strcmp((char *)&data[8], _prefs.password) == 0) { // check for valid admin password + perm = PERM_ACL_ADMIN; + } else { + if (strcmp((char *)&data[8], _prefs.guest_password) == 0) { // check the room/public password + perm = PERM_ACL_READ_WRITE; + } else if (_prefs.allow_read_only) { + perm = PERM_ACL_GUEST; + } else { + MESH_DEBUG_PRINTLN("Incorrect room password"); + return; // no response. Client will timeout + } + } + + client = acl.putClient(sender, 0); // add to known clients (if not already known) + if (sender_timestamp <= client->last_timestamp) { + MESH_DEBUG_PRINTLN("possible replay attack!"); + return; + } + + MESH_DEBUG_PRINTLN("Login success!"); + client->last_timestamp = sender_timestamp; + client->extra.room.sync_since = sender_sync_since; + client->extra.room.pending_ack = 0; + client->extra.room.push_failures = 0; + + client->last_activity = getRTCClock()->getCurrentTime(); + client->permissions |= perm; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } - auto client = putClient(sender); // add to known clients (if not already known) - if (sender_timestamp <= client->last_timestamp) { - MESH_DEBUG_PRINTLN("possible replay attack!"); - return; - } - - MESH_DEBUG_PRINTLN("Login success!"); - client->permission = perm; - client->last_timestamp = sender_timestamp; - client->sync_since = sender_sync_since; - client->pending_ack = 0; - client->push_failures = 0; - memcpy(client->secret, secret, PUB_KEY_SIZE); - - uint32_t now = getRTCClock()->getCurrentTime(); - client->last_activity = now; - - now = getRTCClock()->getCurrentTimeUnique(); + uint32_t now = getRTCClock()->getCurrentTimeUnique(); memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp // TODO: maybe reply with count of messages waiting to be synced for THIS client? reply_data[4] = RESP_SERVER_LOGIN_OK; reply_data[5] = (CLIENT_KEEP_ALIVE_SECS >> 4); // NEW: recommended keep-alive interval (secs / 16) - reply_data[6] = (perm == RoomPermission::ADMIN ? 1 : (perm == RoomPermission::GUEST ? 0 : 2)); - reply_data[7] = getUnsyncedCount(client); // NEW - memcpy(&reply_data[8], "OK", 2); // REVISIT: not really needed + reply_data[6] = (client->isAdmin() ? 1 : (client->permissions == 0 ? 2 : 0)); + // LEGACY: reply_data[7] = getUnsyncedCount(client); + reply_data[7] = client->permissions; // NEW + getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness + // LEGACY: memcpy(&reply_data[8], "OK", 2); next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response - mesh::Packet *path = createPathReturn(sender, client->secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 8 + 2); + mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, 12); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->secret, reply_data, 8 + 2); + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 12); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); @@ -368,8 +365,8 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m int MyMesh::searchPeersByHash(const uint8_t *hash) { int n = 0; - for (int i = 0; i < num_clients; i++) { - if (known_clients[i].id.isHashMatch(hash)) { + for (int i = 0; i < acl.getNumClients(); i++) { + if (acl.getClientByIdx(i)->id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } } @@ -378,9 +375,9 @@ int MyMesh::searchPeersByHash(const uint8_t *hash) { void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { int i = matching_peer_indexes[peer_idx]; - if (i >= 0 && i < num_clients) { + if (i >= 0 && i < acl.getNumClients()) { // lookup pre-calculated shared_secret - memcpy(dest_secret, known_clients[i].secret, PUB_KEY_SIZE); + memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE); } else { MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i); } @@ -389,11 +386,11 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { int i = matching_peer_indexes[sender_idx]; - if (i < 0 || i >= num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i < 0 || i >= acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); return; } - auto client = &known_clients[i]; + auto client = acl.getClientByIdx(i); if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) { // a CLI command or new Post uint32_t sender_timestamp; memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) @@ -407,7 +404,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint32_t now = getRTCClock()->getCurrentTimeUnique(); client->last_activity = now; - client->push_failures = 0; // reset so push can resume (if prev failed) + client->extra.room.push_failures = 0; // reset so push can resume (if prev failed) // 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 @@ -420,7 +417,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint8_t temp[166]; bool send_ack; if (flags == TXT_TYPE_CLI_DATA) { - if (client->permission == RoomPermission::ADMIN) { + if (client->isAdmin()) { if (is_retry) { temp[5] = 0; // no reply } else { @@ -433,7 +430,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, send_ack = false; // and no ACK... user shoudn't be sending these } } else { // TXT_TYPE_PLAIN - if (client->permission == RoomPermission::READ_ONLY) { + if ((client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { temp[5] = 0; // no reply send_ack = false; // no ACK } else { @@ -501,7 +498,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint32_t now = getRTCClock()->getCurrentTime(); client->last_activity = now; // <-- THIS will keep client connection alive - client->push_failures = 0; // reset so push can resume (if prev failed) + client->extra.room.push_failures = 0; // reset so push can resume (if prev failed) if (data[4] == REQ_TYPE_KEEP_ALIVE && packet->isRouteDirect()) { // request type uint32_t forceSince = 0; @@ -511,10 +508,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&data[5], &forceSince, 4); // make sure there are zeroes in payload (for ack_hash calc below) } if (forceSince > 0) { - client->sync_since = forceSince; // force-update the 'sync since' + client->extra.room.sync_since = forceSince; // force-update the 'sync since' } - client->pending_ack = 0; + client->extra.room.pending_ack = 0; // TODO: Throttle KEEP_ALIVE requests! // if client sends too quickly, evict() @@ -559,10 +556,11 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t // TODO: prevent replay attacks int i = matching_peer_indexes[sender_idx]; - if (i >= 0 && i < num_clients) { // get from our known_clients table (sender SHOULD already be known in this context) + if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); - auto client = &known_clients[i]; + auto client = acl.getClientByIdx(i); memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); } @@ -587,6 +585,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), _cli(board, rtc, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4) { next_local_advert = next_flood_advert = 0; + dirty_contacts_expiry = 0; _logging = false; set_radio_at = revert_radio_at = 0; @@ -613,7 +612,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc StrHelper::strncpy(_prefs.guest_password, ROOM_PASSWORD, sizeof(_prefs.guest_password)); #endif - num_clients = 0; next_post_idx = 0; next_client_idx = 0; next_push = 0; @@ -627,6 +625,8 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); + acl.load(_fs); + radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); @@ -731,33 +731,73 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply command += 3; } - _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + // handle ACL related commands + if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8} + char* hex = &command[8]; + char* sp = strchr(hex, ' '); // look for separator char + if (sp == NULL) { + strcpy(reply, "Err - bad params"); + } else { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) { + uint8_t perms = atoi(sp); + if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) { + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save() + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - invalid params"); + } + } else { + strcpy(reply, "Err - bad pubkey"); + } + } + } else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) { + Serial.println("ACL:"); + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->permissions == 0) continue; // skip deleted (or guest) entries + + Serial.printf("%02X ", c->permissions); + mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE); + Serial.printf("\n"); + } + reply[0] = 0; + } else{ + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands + } +} + +bool MyMesh::saveFilter(ClientInfo* client) { + return client->isAdmin(); // only save Admins } void MyMesh::loop() { mesh::Mesh::loop(); - if (millisHasNowPassed(next_push) && num_clients > 0) { + if (millisHasNowPassed(next_push) && acl.getNumClients() > 0) { // check for ACK timeouts - for (int i = 0; i < num_clients; i++) { - auto c = &known_clients[i]; - if (c->pending_ack && millisHasNowPassed(c->ack_timeout)) { - c->push_failures++; - c->pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry) + for (int i = 0; i < acl.getNumClients(); i++) { + auto c = acl.getClientByIdx(i); + if (c->extra.room.pending_ack && millisHasNowPassed(c->extra.room.ack_timeout)) { + c->extra.room.push_failures++; + c->extra.room.pending_ack = 0; // reset (TODO: keep prev expected_ack's in a list, incase they arrive LATER, after we retry) MESH_DEBUG_PRINTLN("pending ACK timed out: push_failures: %d", (uint32_t)c->push_failures); } } // check next Round-Robin client, and sync next new post - auto client = &known_clients[next_client_idx]; + auto client = acl.getClientByIdx(next_client_idx); bool did_push = false; - if (client->pending_ack == 0 && client->last_activity != 0 && - client->push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max + if (client->extra.room.pending_ack == 0 && client->last_activity != 0 && + client->extra.room.push_failures < 3) { // not already waiting for ACK, AND not evicted, AND retries not max MESH_DEBUG_PRINTLN("loop - checking for client %02X", (uint32_t)client->id.pub_key[0]); uint32_t now = getRTCClock()->getCurrentTime(); for (int k = 0, idx = next_post_idx; k < MAX_UNSYNCED_POSTS; k++) { auto p = &posts[idx]; if (now >= p->post_timestamp + POST_SYNC_DELAY_SECS && - p->post_timestamp > client->sync_since // is new post for this Client? + p->post_timestamp > client->extra.room.sync_since // is new post for this Client? && !p->author.matches(client->id)) { // don't push posts to the author // push this post to Client, then wait for ACK pushPostToClient(client, *p); @@ -770,7 +810,7 @@ void MyMesh::loop() { } else { MESH_DEBUG_PRINTLN("loop - skipping busy (or evicted) client %02X", (uint32_t)client->id.pub_key[0]); } - next_client_idx = (next_client_idx + 1) % num_clients; // round robin polling for each client + next_client_idx = (next_client_idx + 1) % acl.getNumClients(); // round robin polling for each client if (did_push) { next_push = futureMillis(SYNC_PUSH_INTERVAL); @@ -805,5 +845,11 @@ void MyMesh::loop() { MESH_DEBUG_PRINTLN("Radio params restored"); } + // is pending dirty contacts write needed? + if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { + acl.save(_fs, MyMesh::saveFilter); + dirty_contacts_expiry = 0; + } + // TODO: periodically check for OLD/inactive entries in known_clients[], and evict } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 945cca7d..dc7a6b5a 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -18,6 +18,7 @@ #include <helpers/AdvertDataHelpers.h> #include <helpers/TxtDataHelpers.h> #include <helpers/CommonCLI.h> +#include <helpers/ClientACL.h> #include <RTClib.h> #include <target.h> @@ -61,10 +62,6 @@ #define ADMIN_PASSWORD "password" #endif -#ifndef MAX_CLIENTS - #define MAX_CLIENTS 32 -#endif - #ifndef MAX_UNSYNCED_POSTS #define MAX_UNSYNCED_POSTS 32 #endif @@ -81,27 +78,6 @@ #define PACKET_LOG_FILE "/packet_log" -enum RoomPermission { - ADMIN, - GUEST, - READ_ONLY -}; - -struct ClientInfo { - mesh::Identity id; - uint32_t last_timestamp; // by THEIR clock - uint32_t last_activity; // by OUR clock - uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) - uint32_t pending_ack; - uint32_t push_post_timestamp; - unsigned long ack_timeout; - RoomPermission permission; - uint8_t push_failures; - uint8_t secret[PUB_KEY_SIZE]; - int out_path_len; - uint8_t out_path[MAX_PATH_SIZE]; -}; - #define MAX_POST_TEXT_LEN (160-9) struct PostInfo { @@ -116,9 +92,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { bool _logging; NodePrefs _prefs; CommonCLI _cli; + ClientACL acl; + unsigned long dirty_contacts_expiry; uint8_t reply_data[MAX_PACKET_PAYLOAD]; - int num_clients; - ClientInfo known_clients[MAX_CLIENTS]; unsigned long next_push; uint16_t _num_posted, _num_post_pushes; int next_client_idx; // for round-robin polling @@ -132,8 +108,6 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t pending_cr; int matching_peer_indexes[MAX_CLIENTS]; - ClientInfo* putClient(const mesh::Identity& id); - void evict(ClientInfo* client); void addPost(ClientInfo* client, const char* postData); void pushPostToClient(ClientInfo* client, PostInfo& post); uint8_t getUnsyncedCount(ClientInfo* client); @@ -213,6 +187,8 @@ public: mesh::LocalIdentity& getSelfId() override { return self_id; } + static bool saveFilter(ClientInfo* client); + void saveIdentity(const mesh::LocalIdentity& new_id) override; void clearStats() override; void handleCommand(uint32_t sender_timestamp, char* command, char* reply); diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index a6b048a1..eac35898 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -217,18 +217,18 @@ protected: saveContacts(); } - bool processAck(const uint8_t *data) override { + ContactInfo* processAck(const uint8_t *data) override { 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); // 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 NULL; // TODO: really should return ContactInfo pointer } //uint32_t crc; //memcpy(&crc, data, 4); //MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc); - return false; + return NULL; } void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override { diff --git a/platformio.ini b/platformio.ini index 11814a22..4fe17af9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -107,6 +107,7 @@ build_src_filter = ${arduino_base.build_src_filter} +<helpers/stm32> lib_deps = ${arduino_base.lib_deps} file://arch/stm32/Adafruit_LittleFS_stm32 + adafruit/Adafruit BusIO @ 1.17.2 [sensor_base] build_flags = diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 60366c65..e03dd088 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -158,6 +158,7 @@ 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 (flags == TXT_TYPE_PLAIN) { + from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time onMessageRecv(from, packet, 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 @@ -184,6 +185,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date from.sync_since = timestamp; } + from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it @@ -223,6 +225,10 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { onContactResponse(from, data, len); + if (packet->isRouteFlood() && from.out_path_len >= 0) { + // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) + handleReturnPathRetry(from, packet->path, packet->path_len); + } } } @@ -248,7 +254,7 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_ if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) { // also got an encoded ACK! - if (processAck(extra)) { + if (processAck(extra) != NULL) { txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer } } else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) { @@ -258,12 +264,25 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_ } void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { - if (processAck((uint8_t *)&ack_crc)) { + ContactInfo* from; + if ((from = processAck((uint8_t *)&ack_crc)) != NULL) { txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit + + if (packet->isRouteFlood() && from->out_path_len >= 0) { + // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?) + handleReturnPathRetry(*from, packet->path, packet->path_len); + } } } +void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) { + // NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY) + // override this method in various firmwares, if there's a better strategy + mesh::Packet* rpath = createPathReturn(contact.id, contact.shared_secret, path, path_len, 0, NULL, 0); + if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay +} + #ifdef MAX_GROUP_CHANNELS int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; @@ -550,7 +569,7 @@ void BaseChatMesh::markConnectionActive(const ContactInfo& contact) { } } -bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) { +ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) { for (int i = 0; i < MAX_CONNECTIONS; i++) { if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) { // yes, got an ack for our keep_alive request! @@ -559,10 +578,12 @@ bool BaseChatMesh::checkConnectionsAck(const uint8_t* data) { // re-schedule next KEEP_ALIVE, now that we have heard from server connections[i].next_ping = futureMillis(connections[i].keep_alive_millis); - return true; // yes, a match + + auto id = &connections[i].server_id; + return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match } } - return false; /// no match + return NULL; /// no match } void BaseChatMesh::checkConnections() { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 9a4aa810..9392001e 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -93,7 +93,7 @@ protected: // 'UI' concepts, for sub-classes to implement virtual bool isAutoAddEnabled() const { return true; } virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; - virtual bool processAck(const uint8_t *data) = 0; + virtual ContactInfo* processAck(const uint8_t *data) = 0; virtual void onContactPathUpdated(const ContactInfo& contact) = 0; virtual 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); virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0; @@ -105,6 +105,7 @@ protected: virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0; virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0; virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0; + virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len); // storage concepts, for sub-classes to override/implement virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented @@ -127,7 +128,7 @@ protected: void stopConnection(const uint8_t* pub_key); bool hasConnectionTo(const uint8_t* pub_key); void markConnectionActive(const ContactInfo& contact); - bool checkConnectionsAck(const uint8_t* data); + ContactInfo* checkConnectionsAck(const uint8_t* data); void checkConnections(); public: diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 23f7b337..4ea19fd2 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -24,16 +24,17 @@ void ClientACL::load(FILESYSTEM* _fs) { while (!full) { ClientInfo c; uint8_t pub_key[32]; - uint8_t unused[6]; + uint8_t unused[2]; + + memset(&c, 0, sizeof(c)); bool success = (file.read(pub_key, 32) == 32); success = success && (file.read((uint8_t *) &c.permissions, 1) == 1); - success = success && (file.read(unused, 6) == 6); + success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4); + success = success && (file.read(unused, 2) == 2); success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1); success = success && (file.read(c.out_path, 64) == 64); success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); - c.last_timestamp = 0; // transient - c.last_activity = 0; if (!success) break; // EOF @@ -49,19 +50,20 @@ void ClientACL::load(FILESYSTEM* _fs) { } } -void ClientACL::save(FILESYSTEM* _fs) { +void ClientACL::save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)) { File file = openWrite(_fs, "/s_contacts"); if (file) { - uint8_t unused[6]; + uint8_t unused[2]; memset(unused, 0, sizeof(unused)); for (int i = 0; i < num_clients; i++) { auto c = &clients[i]; - if (c->permissions == 0) continue; // skip deleted entries + if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function bool success = (file.write(c->id.pub_key, 32) == 32); success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); - success = success && (file.write(unused, 6) == 6); + success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4); + success = success && (file.write(unused, 2) == 2); success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index f8cc1233..1b650edd 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -18,7 +18,16 @@ struct ClientInfo { uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) - + union { + struct { + uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) + uint32_t pending_ack; + uint32_t push_post_timestamp; + unsigned long ack_timeout; + uint8_t push_failures; + } room; + } extra; + bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } }; @@ -36,7 +45,7 @@ public: num_clients = 0; } void load(FILESYSTEM* _fs); - void save(FILESYSTEM* _fs); + void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL); ClientInfo* getClient(const uint8_t* pubkey, int key_len); ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms); diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 1be703a8..7ec93723 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -66,7 +66,7 @@ bool SerialBLEInterface::onSecurityRequest() { void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) { if (cmpl.success) { BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success"); - //deviceConnected = true; + deviceConnected = true; } else { BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*"); @@ -88,8 +88,6 @@ void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t 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) { diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index ad5823f1..dbe6f393 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -86,7 +86,7 @@ void SerialBLEInterface::startAdv() { * https://developer.apple.com/library/content/qa/qa1931/_index.html */ Bluefruit.Advertising.restartOnDisconnect(false); // don't restart automatically as we handle it in onDisconnect - Bluefruit.Advertising.setInterval(32, 1600); + Bluefruit.Advertising.setInterval(32, 244); Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index d81d99fb..32839edc 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -1,6 +1,7 @@ #pragma once #include <stdint.h> +#include <string.h> class DisplayDriver { int _w, _h; @@ -31,5 +32,60 @@ public: setCursor(mid_x - w/2, y); print(str); } + + // convert UTF-8 characters to displayable block characters for compatibility + virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { + size_t j = 0; + for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) { + unsigned char c = (unsigned char)src[i]; + if (c >= 32 && c <= 126) { + dest[j++] = c; // ASCII printable + } else if (c >= 0x80) { + dest[j++] = '\xDB'; // CP437 full block █ + while (src[i+1] && (src[i+1] & 0xC0) == 0x80) + i++; // skip UTF-8 continuation bytes + } + } + dest[j] = 0; + } + + // draw text with ellipsis if it exceeds max_width + virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) { + char temp_str[256]; // reasonable buffer size + size_t len = strlen(str); + if (len >= sizeof(temp_str)) len = sizeof(temp_str) - 1; + memcpy(temp_str, str, len); + temp_str[len] = 0; + + if (getTextWidth(temp_str) <= max_width) { + setCursor(x, y); + print(temp_str); + return; + } + + // for variable-width fonts (GxEPD), add space after ellipsis + // for fixed-width fonts (OLED), keep tight spacing to save precious characters + const char* ellipsis; + // use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width + int i_width = getTextWidth("i"); + int l_width = getTextWidth("l"); + if (i_width != l_width) { + ellipsis = "... "; // variable-width fonts: add space + } else { + ellipsis = "..."; // fixed-width fonts: no space + } + + int ellipsis_width = getTextWidth(ellipsis); + int str_len = strlen(temp_str); + + while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) { + temp_str[--str_len] = 0; + } + strcat(temp_str, ellipsis); + + setCursor(x, y); + print(temp_str); + } + virtual void endFrame() = 0; }; diff --git a/src/helpers/ui/GenericVibration.cpp b/src/helpers/ui/GenericVibration.cpp new file mode 100644 index 00000000..9226b812 --- /dev/null +++ b/src/helpers/ui/GenericVibration.cpp @@ -0,0 +1,38 @@ +#ifdef PIN_VIBRATION +#include "GenericVibration.h" + +void GenericVibration::begin() { + pinMode(PIN_VIBRATION, OUTPUT); + digitalWrite(PIN_VIBRATION, LOW); + duration = 0; +} + +void GenericVibration::trigger() { + duration = millis(); + digitalWrite(PIN_VIBRATION, HIGH); +} + +void GenericVibration::loop() { + if (isVibrating()) { + if ((millis() / 1000) % 2 == 0) { + digitalWrite(PIN_VIBRATION, LOW); + } else { + digitalWrite(PIN_VIBRATION, HIGH); + } + + if (millis() - duration > VIBRATION_TIMEOUT) { + stop(); + } + } +} + +bool GenericVibration::isVibrating() { + return duration > 0; +} + +void GenericVibration::stop() { + duration = 0; + digitalWrite(PIN_VIBRATION, LOW); +} + +#endif // ifdef PIN_VIBRATION diff --git a/src/helpers/ui/GenericVibration.h b/src/helpers/ui/GenericVibration.h new file mode 100644 index 00000000..38755bd8 --- /dev/null +++ b/src/helpers/ui/GenericVibration.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef PIN_VIBRATION + +#include <Arduino.h> + +/* + * Vibration motor control class + * + * Provides vibration feedback for events like new messages and new contacts + * Features: + * - 1-second vibration pulse + * - 5-second nag timeout (cooldown between vibrations) + * - Non-blocking operation + */ + +#ifndef VIBRATION_TIMEOUT +#define VIBRATION_TIMEOUT 5000 // 5 seconds default +#endif + +class GenericVibration { +public: + void begin(); // set up vibration pin + void trigger(); // trigger vibration if cooldown has passed + void loop(); // non-blocking timer handling + bool isVibrating(); // returns true if currently vibrating + void stop(); // stop vibration immediately + +private: + unsigned long duration; +}; + +#endif // ifdef PIN_VIBRATION diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini new file mode 100644 index 00000000..a7e6fe22 --- /dev/null +++ b/variants/ebyte_eora_s3/platformio.ini @@ -0,0 +1,136 @@ +[Ebyte_EoRa-S3] +extends = esp32_base +board = ebyte_eora-s3 +build_flags = + ${esp32_base.build_flags} + -I variants/ebyte_eora_s3 + -D EBYTE_EORA_S3 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=7 + -D P_LORA_RESET=8 ; RADIOLIB_NC + -D P_LORA_BUSY=34 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=3 + -D P_LORA_MOSI=6 + -D P_LORA_TX_LED=37 + -D PIN_VBAT_READ=1 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=18 + -D PIN_BOARD_SCL=17 + +; SD_DAT0/MISO - GPIO2 +; SD_DAT1 - GPIO4 +; SD_CMD/MOSI - GPIO11 +; SD_DAT2 - GPIO112 +; SD_DAT3/CS - GPIO113 +; SD_CLK - GPIO114 + -D PIN_BOARD_SDA=18 + -D PIN_BOARD_SCL=17 + + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/ebyte_eora_s3> +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + +; === EByte EORA_S3 with SX1262 environments === +[env:Ebyte_EoRa-S3_Repeater] +extends = Ebyte_EoRa-S3 +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"EORA_S3-1262 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=8 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_repeater> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Ebyte_EoRa-S3_terminal_chat] +extends = Ebyte_EoRa-S3 +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Ebyte_EoRa-S3_room_server] +extends = Ebyte_EoRa-S3 +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"EORA_S3-1262 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<../examples/simple_room_server> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:Ebyte_EoRa-S3_companion_radio_usb] +extends = Ebyte_EoRa-S3 +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=8 +; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 +; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + +<helpers/ui/SSD1306Display.cpp> + +<helpers/ui/MomentaryButton.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Ebyte_EoRa-S3_companion_radio_ble] +extends = Ebyte_EoRa-S3 +build_flags = + ${Ebyte_EoRa-S3.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=300 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} + +<helpers/esp32/*.cpp> + +<helpers/ui/SSD1306Display.cpp> + +<helpers/ui/MomentaryButton.cpp> + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Ebyte_EoRa-S3.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/ebyte_eora_s3/target.cpp b/variants/ebyte_eora_s3/target.cpp new file mode 100644 index 00000000..647f5997 --- /dev/null +++ b/variants/ebyte_eora_s3/target.cpp @@ -0,0 +1,85 @@ +#include <Arduino.h> +#include "target.h" + +ESP32Board board; + +#if defined(P_LORA_SCLK) + static 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 + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + +#ifdef SX126X_DIO3_TCXO_VOLTAGE + float tcxo = SX126X_DIO3_TCXO_VOLTAGE; +#else + float tcxo = 1.6f; +#endif + +#if 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); + return false; // fail + } + + radio.setCRC(1); + +#if defined(SX126X_RXEN) && defined(SX126X_TXEN) + radio.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); +#endif + +#ifdef SX126X_CURRENT_LIMIT + radio.setCurrentLimit(SX126X_CURRENT_LIMIT); +#endif +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio.setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif + + return true; // success +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/ebyte_eora_s3/target.h b/variants/ebyte_eora_s3/target.h new file mode 100644 index 00000000..f184c757 --- /dev/null +++ b/variants/ebyte_eora_s3/target.h @@ -0,0 +1,29 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include <RadioLib.h> +#include <helpers/radiolib/RadioLibWrappers.h> +#include <helpers/ESP32Board.h> +#include <helpers/radiolib/CustomSX1262Wrapper.h> +#include <helpers/AutoDiscoverRTCClock.h> +#include <helpers/SensorManager.h> +#ifdef DISPLAY_CLASS + #include <helpers/ui/SSD1306Display.h> + #include <helpers/ui/MomentaryButton.h> +#endif + +extern ESP32Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index 59d55175..0ee5bfed 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -28,14 +28,12 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BOARD_SCL=PIN_WIRE_SCL build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak_wismesh_tag> - +<helpers/ui/buzzer.cpp> +<helpers/ui/MomentaryButton.cpp> +<helpers/ui/NullDisplayDriver.cpp> +<helpers/sensors> lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_Repeater] extends = rak_wismesh_tag @@ -77,11 +75,13 @@ build_flags = ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak_wismesh_tag.build_src_filter} + +<helpers/ui/buzzer.cpp> +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${rak_wismesh_tag.lib_deps} densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_companion_radio_ble] extends = rak_wismesh_tag @@ -98,12 +98,14 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 -D MESH_DEBUG=1 build_src_filter = ${rak_wismesh_tag.build_src_filter} + +<helpers/ui/buzzer.cpp> +<helpers/nrf52/SerialBLEInterface.cpp> +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 + end2endzone/NonBlockingRTTTL@^1.3.0 [env:RAK_WisMesh_Tag_sensor] extends = rak4631 diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index 31364ffe..8655021c 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -3,10 +3,12 @@ extends = nrf52_base board = seeed_sensecap_solar board_build.ldscript = boards/nrf52840_s140_v7.ld build_flags = ${nrf52_base.build_flags} + ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 -I variants/sensecap_solar -I src/helpers/nrf52 + -UENV_INCLUDE_GPS -D NRF52_PLATFORM=1 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper @@ -22,13 +24,6 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D ENV_INCLUDE_AHTX0=1 - -D ENV_INCLUDE_BME280=1 - -D ENV_INCLUDE_BMP280=1 - -D ENV_INCLUDE_SHTC3=1 - -D ENV_INCLUDE_LPS22HB=1 - -D ENV_INCLUDE_INA3221=1 - -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52_base.build_src_filter} +<helpers/*.cpp> +<helpers/sensors> @@ -37,13 +32,7 @@ debug_tool = jlink upload_protocol = nrfutil lib_deps = ${nrf52_base.lib_deps} - adafruit/Adafruit INA3221 Library @ ^1.0.1 - adafruit/Adafruit INA219 @ ^1.2.3 - adafruit/Adafruit AHTX0 @ ^2.0.5 - adafruit/Adafruit BME280 Library @ ^2.3.0 - adafruit/Adafruit BMP280 Library @ ^2.6.8 - adafruit/Adafruit SHTC3 Library @ ^1.0.1 - arduino-libraries/Arduino_LPS22HB @ ^1.0.2 + ${sensor_base.lib_deps} [env:SenseCap_Solar_repeater] extends = SenseCap_Solar diff --git a/variants/tiny_relay/platformio.ini b/variants/tiny_relay/platformio.ini new file mode 100644 index 00000000..6816ed20 --- /dev/null +++ b/variants/tiny_relay/platformio.ini @@ -0,0 +1,44 @@ +[Tiny_Relay] +extends = stm32_base +board = tiny_relay +board_upload.maximum_size = 229376 ; 32kb for FS +build_flags = ${stm32_base.build_flags} + -D RADIO_CLASS=CustomSTM32WLx + -D WRAPPER_CLASS=CustomSTM32WLxWrapper + -D SPI_INTERFACES_COUNT=0 + -D RX_BOOSTED_GAIN=true +; -D STM32WL_TCXO_VOLTAGE=1.6 ; defaults to 0 if undef +; -D LORA_TX_POWER=14 ; Defaults to 22 for HP, 14 is for LP version + -D LORA_TX_POWER=22 ; Enable 22dBm transmission + -D MAX_LORA_TX_POWER=22 ; Allow setting up to 22dBm in companion radio + -I variants/tiny_relay +build_src_filter = ${stm32_base.build_src_filter} + +<../variants/tiny_relay> + +[env:Tiny_Relay-repeater] +extends = Tiny_Relay +build_flags = ${Tiny_Relay.build_flags} + -D ADVERT_NAME='"tiny_relay Repeater"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${Tiny_Relay.build_src_filter} + +<../examples/simple_repeater/main.cpp> + +[env:Tiny_Relay-sensor] +extends = Tiny_Relay +build_flags = ${Tiny_Relay.build_flags} + -D ADVERT_NAME='"tiny_relay Sensor"' + -D ADMIN_PASSWORD='"password"' +build_src_filter = ${Tiny_Relay.build_src_filter} + +<../examples/simple_sensor> + +[env:Tiny_Relay_companion_radio_usb] +extends = Tiny_Relay +build_flags = ${Tiny_Relay.build_flags} +; -D FORMAT_FS=true + -D MAX_CONTACTS=100 + -D MAX_GROUP_CHANNELS=8 + -D MAX_LORA_TX_POWER=22 +build_src_filter = ${Tiny_Relay.build_src_filter} + +<../examples/companion_radio/*.cpp> +lib_deps = ${Tiny_Relay.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/tiny_relay/target.cpp b/variants/tiny_relay/target.cpp new file mode 100644 index 00000000..f738ac17 --- /dev/null +++ b/variants/tiny_relay/target.cpp @@ -0,0 +1,82 @@ +#include "target.h" +#include <Arduino.h> +#include <helpers/ArduinoHelpers.h> + +TinyRelayBoard board; + +RADIO_CLASS radio = new STM32WLx_Module(); + +WRAPPER_CLASS radio_driver(radio, board); + +static const uint32_t rfswitch_pins[] = {LORAWAN_RFSWITCH_PINS, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; +static const Module::RfSwitchMode_t rfswitch_table[] = { + {STM32WLx::MODE_IDLE, {LOW, LOW}}, + {STM32WLx::MODE_RX, {HIGH, LOW}}, + {STM32WLx::MODE_TX_LP, {LOW, HIGH}}, + {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, + END_OF_MODE_TABLE, +}; + +VolatileRTCClock rtc_clock; +SensorManager sensors; + +#ifndef LORA_CR +#define LORA_CR 5 +#endif + +#ifndef STM32WL_TCXO_VOLTAGE +// TCXO set to 0 for RAK3172 +#define STM32WL_TCXO_VOLTAGE 0 +#endif + +#ifndef LORA_TX_POWER +#define LORA_TX_POWER 22 +#endif + +bool radio_init() +{ + // rtc_clock.begin(Wire); + + radio.setRfSwitchTable(rfswitch_pins, rfswitch_table); + + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, + STM32WL_TCXO_VOLTAGE, 0); + + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; // fail + } + +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + radio.setCRC(1); + + return true; // success +} + +uint32_t radio_get_rng_seed() +{ + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) +{ + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) +{ + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() +{ + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); // create new random identity +} diff --git a/variants/tiny_relay/target.h b/variants/tiny_relay/target.h new file mode 100644 index 00000000..82747cdc --- /dev/null +++ b/variants/tiny_relay/target.h @@ -0,0 +1,59 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include <RadioLib.h> +#include <helpers/ArduinoHelpers.h> +#include <helpers/SensorManager.h> +#include <helpers/radiolib/CustomSTM32WLxWrapper.h> +#include <helpers/radiolib/RadioLibWrappers.h> +#include <helpers/stm32/STM32Board.h> + +#define PIN_VBAT_READ A0 +#define ADC_MULTIPLIER (5 * 1.73 * 1000) + +class TinyRelayBoard : public STM32Board +{ + public: + void begin() override + { + STM32Board::begin(); + pinMode(PA0, OUTPUT); + pinMode(PA1, OUTPUT); + } + + const char *getManufacturerName() const override { return "Tiny Relay"; } + + uint16_t getBattMilliVolts() override + { + analogReadResolution(12); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(PIN_VBAT_READ); + } + return ((double)raw) * ADC_MULTIPLIER / 8 / 4096; + } + + void setGpio(uint32_t values) override + { + // set led values + digitalWrite(PA0, values & 1); + digitalWrite(PA1, (values & 2) >> 1); + } + + uint32_t getGpio() override + { + // get led value + return (digitalRead(PA1) << 1) | digitalRead(PA0); + } +}; + +extern TinyRelayBoard board; +extern WRAPPER_CLASS radio_driver; +extern VolatileRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/tiny_relay/variant.h b/variants/tiny_relay/variant.h new file mode 100644 index 00000000..4405be0b --- /dev/null +++ b/variants/tiny_relay/variant.h @@ -0,0 +1,5 @@ +#pragma once + +#include <variant_RAK3172_MODULE.h> + +#undef RNG diff --git a/variants/xiao_nrf52/XiaoNrf52Board.cpp b/variants/xiao_nrf52/XiaoNrf52Board.cpp index c603b2af..03bb674e 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.cpp +++ b/variants/xiao_nrf52/XiaoNrf52Board.cpp @@ -1,21 +1,19 @@ #ifdef XIAO_NRF52 #include <Arduino.h> -#include "XiaoNrf52Board.h" - -#include <bluefruit.h> #include <Wire.h> +#include <bluefruit.h> + +#include "XiaoNrf52Board.h" static BLEDfu bledfu; -static void connect_callback(uint16_t conn_handle) -{ +static void connect_callback(uint16_t conn_handle) { (void)conn_handle; MESH_DEBUG_PRINTLN("BLE client connected"); } -static void disconnect_callback(uint16_t conn_handle, uint8_t reason) -{ +static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { (void)conn_handle; (void)reason; @@ -41,12 +39,12 @@ void XiaoNrf52Board::begin() { digitalWrite(P_LORA_TX_LED, HIGH); #endif -// pinMode(SX126X_POWER_EN, OUTPUT); -// digitalWrite(SX126X_POWER_EN, HIGH); - delay(10); // give sx1262 some time to power up + // pinMode(SX126X_POWER_EN, OUTPUT); + // digitalWrite(SX126X_POWER_EN, HIGH); + delay(10); // give sx1262 some time to power up } -bool XiaoNrf52Board::startOTAUpdate(const char* id, char reply[]) { +bool XiaoNrf52Board::startOTAUpdate(const char *id, char reply[]) { // Config the peripheral connection with maximum bandwidth // more SRAM required by SoftDevice // Note: All config***() function must be called before begin() @@ -86,10 +84,8 @@ bool XiaoNrf52Board::startOTAUpdate(const char* id, char reply[]) { Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds strcpy(reply, "OK - started"); + return true; - - - return false; } #endif \ No newline at end of file diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index 60b9f5bb..b229507a 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -5,20 +5,6 @@ #ifdef XIAO_NRF52 -// redefine lora pins if using the S3 variant of SX1262 board -#ifdef SX1262_XIAO_S3_VARIANT - #undef P_LORA_DIO_1 - #undef P_LORA_BUSY - #undef P_LORA_RESET - #undef P_LORA_NSS - #undef SX126X_RXEN - #define P_LORA_DIO_1 D0 - #define P_LORA_BUSY D1 - #define P_LORA_RESET D2 - #define P_LORA_NSS D3 - #define SX126X_RXEN D4 -#endif - class XiaoNrf52Board : public mesh::MainBoard { protected: uint8_t startup_reason; @@ -40,13 +26,13 @@ public: // Please read befor going further ;) // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging - // We can't drive VBAT_ENABLE to HIGH as long + // We can't drive VBAT_ENABLE to HIGH as long // as we don't know wether we are charging or not ... // this is a 3mA loss (4/1500) digitalWrite(VBAT_ENABLE, LOW); int adcvalue = 0; analogReadResolution(12); - analogReference(AR_INTERNAL_3_0); + analogReference(AR_INTERNAL_3_0); delay(10); adcvalue = analogRead(PIN_VBAT); return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096; diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index 212a55ea..8054c72d 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -1,37 +1,19 @@ -[nrf52840_xiao] +[Xiao_nrf52] extends = nrf52_base -platform_packages = - toolchain-gccarmnoneeabi@~1.100301.0 - framework-arduinoadafruitnrf52 board = seeed-xiao-afruitnrf52-nrf52840 board_build.ldscript = boards/nrf52840_s140_v7.ld build_flags = ${nrf52_base.build_flags} - -D NRF52_PLATFORM -D XIAO_NRF52 + ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 -lib_ignore = - BluetoothOTA - lvgl - lib5b4 -lib_deps = - ${nrf52_base.lib_deps} - rweather/Crypto @ ^0.4.0 - adafruit/Adafruit INA3221 Library @ ^1.0.1 - adafruit/Adafruit INA219 @ ^1.2.3 - adafruit/Adafruit AHTX0 @ ^2.0.5 - adafruit/Adafruit BME280 Library @ ^2.3.0 - - -[Xiao_nrf52] -extends = nrf52840_xiao -;board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52840_xiao.build_flags} - -D P_LORA_TX_LED=11 -I variants/xiao_nrf52 - -I src/helpers/nrf52 + -UENV_INCLUDE_GPS + -D NRF52_PLATFORM + -D XIAO_NRF52 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 + -D P_LORA_TX_LED=11 -D P_LORA_DIO_1=D1 -D P_LORA_RESET=D2 -D P_LORA_BUSY=D3 @@ -42,18 +24,16 @@ build_flags = ${nrf52840_xiao.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D PIN_WIRE_SCL=6 - -D PIN_WIRE_SDA=7 - -D ENV_INCLUDE_AHTX0=1 - -D ENV_INCLUDE_BME280=1 - -D ENV_INCLUDE_INA3221=1 - -D ENV_INCLUDE_INA219=1 -build_src_filter = ${nrf52840_xiao.build_src_filter} + -D PIN_WIRE_SCL=D6 + -D PIN_WIRE_SDA=D7 +build_src_filter = ${nrf52_base.build_src_filter} +<helpers/*.cpp> +<helpers/sensors> +<../variants/xiao_nrf52> debug_tool = jlink upload_protocol = nrfutil +lib_deps = ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} [env:Xiao_nrf52_companion_radio_ble] extends = Xiao_nrf52 @@ -94,12 +74,6 @@ lib_deps = ${Xiao_nrf52.lib_deps} densaugeo/base64 @ ~1.4.0 -[env:Xiao_nrf52_alt_pinout_companion_radio_ble] -extends = env:Xiao_nrf52_companion_radio_ble -build_flags = - ${env:Xiao_nrf52_companion_radio_ble.build_flags} - -D SX1262_XIAO_S3_VARIANT - [env:Xiao_nrf52_repeater] extends = Xiao_nrf52 build_flags = @@ -114,12 +88,6 @@ build_flags = build_src_filter = ${Xiao_nrf52.build_src_filter} +<../examples/simple_repeater/*.cpp> -[env:Xiao_nrf52_alt_pinout_repeater] -extends = env:Xiao_nrf52_repeater -build_flags = - ${env:Xiao_nrf52_repeater.build_flags} - -D SX1262_XIAO_S3_VARIANT - [env:Xiao_nrf52_room_server] extends = Xiao_nrf52 build_flags = diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index 07af2502..41142eb6 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -10,12 +10,13 @@ WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); + EnvironmentSensorManager sensors; bool radio_init() { - rtc_clock.begin(Wire); - - return radio.std_init(&SPI); + rtc_clock.begin(Wire); + + return radio.std_init(&SPI); } uint32_t radio_get_rng_seed() { @@ -35,5 +36,5 @@ void radio_set_tx_power(uint8_t dbm) { mesh::LocalIdentity radio_new_identity() { RadioNoiseListener rng(radio); - return mesh::LocalIdentity(&rng); // create new random identity + return mesh::LocalIdentity(&rng); // create new random identity } diff --git a/variants/xiao_nrf52/variant.cpp b/variants/xiao_nrf52/variant.cpp index 16542e27..04ef3a92 100644 --- a/variants/xiao_nrf52/variant.cpp +++ b/variants/xiao_nrf52/variant.cpp @@ -1,86 +1,85 @@ #include "variant.h" + +#include "nrf.h" #include "wiring_constants.h" #include "wiring_digital.h" -#include "nrf.h" -const uint32_t g_ADigitalPinMap[] = -{ - // D0 .. D10 - 2, // D0 is P0.02 (A0) - 3, // D1 is P0.03 (A1) - 28, // D2 is P0.28 (A2) - 29, // D3 is P0.29 (A3) - 4, // D4 is P0.04 (A4,SDA) - 5, // D5 is P0.05 (A5,SCL) - 43, // D6 is P1.11 (TX) - 44, // D7 is P1.12 (RX) - 45, // D8 is P1.13 (SCK) - 46, // D9 is P1.14 (MISO) - 47, // D10 is P1.15 (MOSI) +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 + 2, // D0 is P0.02 (A0) + 3, // D1 is P0.03 (A1) + 28, // D2 is P0.28 (A2) + 29, // D3 is P0.29 (A3) + 4, // D4 is P0.04 (A4,SDA) + 5, // D5 is P0.05 (A5,SCL) + 43, // D6 is P1.11 (TX) + 44, // D7 is P1.12 (RX) + 45, // D8 is P1.13 (SCK) + 46, // D9 is P1.14 (MISO) + 47, // D10 is P1.15 (MOSI) - // LEDs - 26, // D11 is P0.26 (LED RED) - 6, // D12 is P0.06 (LED BLUE) - 30, // D13 is P0.30 (LED GREEN) - 14, // D14 is P0.14 (READ_BAT) + // LEDs + 26, // D11 is P0.26 (LED RED) + 6, // D12 is P0.06 (LED BLUE) + 30, // D13 is P0.30 (LED GREEN) + 14, // D14 is P0.14 (READ_BAT) - // LSM6DS3TR - 40, // D15 is P1.08 (6D_PWR) - 27, // D16 is P0.27 (6D_I2C_SCL) - 7, // D17 is P0.07 (6D_I2C_SDA) - 11, // D18 is P0.11 (6D_INT1) + // LSM6DS3TR + 40, // D15 is P1.08 (6D_PWR) + 27, // D16 is P0.27 (6D_I2C_SCL) + 7, // D17 is P0.07 (6D_I2C_SDA) + 11, // D18 is P0.11 (6D_INT1) - // MIC - 42, // D19 is P1.10 (MIC_PWR) - 32, // D20 is P1.00 (PDM_CLK) - 16, // D21 is P0.16 (PDM_DATA) + // MIC + 42, // D19 is P1.10 (MIC_PWR) + 32, // D20 is P1.00 (PDM_CLK) + 16, // D21 is P0.16 (PDM_DATA) - // BQ25100 - 13, // D22 is P0.13 (HICHG) - 17, // D23 is P0.17 (~CHG) + // BQ25100 + 13, // D22 is P0.13 (HICHG) + 17, // D23 is P0.17 (~CHG) - // - 21, // D24 is P0.21 (QSPI_SCK) - 25, // D25 is P0.25 (QSPI_CSN) - 20, // D26 is P0.20 (QSPI_SIO_0 DI) - 24, // D27 is P0.24 (QSPI_SIO_1 DO) - 22, // D28 is P0.22 (QSPI_SIO_2 WP) - 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) + // + 21, // D24 is P0.21 (QSPI_SCK) + 25, // D25 is P0.25 (QSPI_CSN) + 20, // D26 is P0.20 (QSPI_SIO_0 DI) + 24, // D27 is P0.24 (QSPI_SIO_1 DO) + 22, // D28 is P0.22 (QSPI_SIO_2 WP) + 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) - // NFC - 9, // D30 is P0.09 (NFC1) - 10, // D31 is P0.10 (NFC2) + // NFC + 9, // D30 is P0.09 (NFC1) + 10, // D31 is P0.10 (NFC2) - // VBAT - 31, // D32 is P0.31 (VBAT) + // VBAT + 31, // D32 is P0.31 (VBAT) }; -void initVariant() -{ - // Disable reading of the BAT voltage. - // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging - pinMode(VBAT_ENABLE, OUTPUT); - //digitalWrite(VBAT_ENABLE, HIGH); - // This was taken from Seeed github butis not coherent with the doc, - // VBAT_ENABLE should be kept to LOW to protect P0.14, (1500/500)*(4.2-3.3)+3.3 = 3.9V > 3.6V - // This induces a 3mA current in the resistors :( but it's better than burning the nrf - digitalWrite(VBAT_ENABLE, LOW); +void initVariant() { + // Disable reading of the BAT voltage. + // https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging + pinMode(VBAT_ENABLE, OUTPUT); + // digitalWrite(VBAT_ENABLE, HIGH); + // This was taken from Seeed github butis not coherent with the doc, + // VBAT_ENABLE should be kept to LOW to protect P0.14, (1500/500)*(4.2-3.3)+3.3 = 3.9V > 3.6V + // This induces a 3mA current in the resistors :( but it's better than burning the nrf + digitalWrite(VBAT_ENABLE, LOW); - // Low charging current (50mA) - // https://wiki.seeedstudio.com/XIAO_BLE#battery-charging-current - //pinMode(PIN_CHARGING_CURRENT, INPUT); + // Low charging current (50mA) + // https://wiki.seeedstudio.com/XIAO_BLE#battery-charging-current + // pinMode(PIN_CHARGING_CURRENT, INPUT); - // High charging current (100mA) - pinMode(PIN_CHARGING_CURRENT, OUTPUT); - digitalWrite(PIN_CHARGING_CURRENT, LOW); + // High charging current (100mA) + pinMode(PIN_CHARGING_CURRENT, OUTPUT); + digitalWrite(PIN_CHARGING_CURRENT, LOW); - pinMode(PIN_QSPI_CS, OUTPUT); - digitalWrite(PIN_QSPI_CS, HIGH); + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); - pinMode(LED_RED, OUTPUT); - digitalWrite(LED_RED, HIGH); - pinMode(LED_GREEN, OUTPUT); - digitalWrite(LED_GREEN, HIGH); - pinMode(LED_BLUE, OUTPUT); - digitalWrite(LED_BLUE, HIGH); + pinMode(LED_RED, OUTPUT); + digitalWrite(LED_RED, HIGH); + pinMode(LED_GREEN, OUTPUT); + digitalWrite(LED_GREEN, HIGH); + pinMode(LED_BLUE, OUTPUT); + digitalWrite(LED_BLUE, HIGH); } diff --git a/variants/xiao_nrf52/variant.h b/variants/xiao_nrf52/variant.h index d941463e..c54f3c2f 100644 --- a/variants/xiao_nrf52/variant.h +++ b/variants/xiao_nrf52/variant.h @@ -113,8 +113,8 @@ static const uint8_t A5 = PIN_A5; // #define PIN_WIRE_SDA (17) // 4 and 5 are used for the sx1262 ! // #define PIN_WIRE_SCL (16) // use WIRE1_SDA -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; +// static const uint8_t SDA = PIN_WIRE_SDA; +// static const uint8_t SCL = PIN_WIRE_SCL; //#define PIN_WIRE1_SDA (17) //#define PIN_WIRE1_SCL (16)