diff --git a/docs/solo_features/settings_screen/settings_screen.md b/docs/solo_features/settings_screen/settings_screen.md index cb395328..f283978f 100644 --- a/docs/solo_features/settings_screen/settings_screen.md +++ b/docs/solo_features/settings_screen/settings_screen.md @@ -40,6 +40,7 @@ Press **Cancel/Back** to save and return to the home screen. | DM Melody | built-in / Melody 1 / Melody 2 / None | Notification sound for incoming private messages. `None` disables the sound for this event. | | Channel Melody | built-in / Melody 1 / Melody 2 / None | Notification sound for incoming channel messages. `None` disables the sound for this event. | | AD sound | built-in / Melody 1 / Melody 2 / None | Sound played whenever an **advert** is received from *any* node — pairs with Auto-Advert as an audible "in range" heartbeat (see Tools › Auto-Advert). `None` disables the sound for this event. | +| AD scope | All / Zero-hop | Filters the AD sound so it plays for every advert or only for local zero-hop adverts. | Melody 1 and Melody 2 are custom sequences editable in **Tools › Ringtone Editor**. diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 7bcf43fa..da2b723d 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -159,7 +159,7 @@ Saved **waypoints are included** in the export as GPX `` elements (with the Periodically broadcasts a 0-hop advert with your GPS position. Configurable interval: off / 30 s / 1 min / 2 min / 5 min / 10 min / 30 min / 1 h. A blinking **A** appears in the status bar while active. > [!TIP] -> **Audible connection heartbeat** — the device chirps each time it *receives* an advert from any node (sound chosen in **Settings › Sound › AD sound**). With Auto-Advert running on both ends (e.g. two people on a hike), each hearing the other's periodic advert becomes a hands-free "in range" beep — no need to look at the screen. It fires for **every** received advert, so in a busy mesh it can get chatty; choose `None` in **Settings › Sound › AD sound** to silence just this event, or set **Settings › Sound › Buzzer** to *Off* (or *Auto*, which mutes while a companion app is connected) to silence all buzzer output. +> **Audible connection heartbeat** — the device chirps each time it *receives* an advert from any node (sound chosen in **Settings › Sound › AD sound**). With Auto-Advert running on both ends (e.g. two people on a hike), each hearing the other's periodic advert becomes a hands-free "in range" beep — no need to look at the screen. It fires for **every** received advert, so in a busy mesh it can get chatty; choose `None` in **Settings › Sound › AD sound** to silence just this event, or set **Settings › Sound › Advert scope** to `Zero-hop` to limit it to local adverts only. You can also set **Settings › Sound › Buzzer** to *Off* (or *Auto*, which mutes while a companion app is connected) to silence all buzzer output. --- diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 6905ecdd..a0db33c6 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -19,6 +19,8 @@ enum class UIEventType { channelMessage, roomMessage, advertReceived, + advertReceivedFlood, + advertReceivedZeroHop, ack }; diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 7f60c9e9..644d2b41 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -320,12 +320,15 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no rd(&_prefs.notif_melody_ad, sizeof(_prefs.notif_melody_ad)); rd(&_prefs.units_imperial, sizeof(_prefs.units_imperial)); rd(&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace)); - // These three were appended together; an older file leaves stray bytes here - // (the old tail sentinel gets partly consumed), so clamp out-of-range values. - // Values: 0=built-in, 1=melody1, 2=melody2, 3=none. + rd(&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope)); + // These fields were appended after ch_fav_only; an older file can leave + // stray bytes here (the old tail sentinel gets partly consumed), so clamp + // out-of-range values back to defaults. + // Values for notif_melody_ad: 0=built-in, 1=melody1, 2=melody2, 3=none. if (_prefs.notif_melody_ad > 3) _prefs.notif_melody_ad = 0; if (_prefs.units_imperial > 1) _prefs.units_imperial = 0; if (_prefs.trail_show_pace > 1) _prefs.trail_show_pace = 0; + if (_prefs.advert_sound_scope > 1) _prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL; // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() already zero-inits any fields not present, @@ -349,11 +352,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (sentinel == 0xC0DE0003) { _prefs.trail_units_idx = 0; } - // → 0xC0DE0007: merge of two parallel 0xC0DE0006 layouts — notif_melody_ad - // (advert sound) and units_imperial + trail_show_pace were appended after - // ch_fav_only. Any older file (incl. either single-feature 0006) leaves - // stray/old bytes in these fields; they're clamped above, so upgraders fall - // back to built-in advert sound + metric + speed. + // → 0xC0DE0008: append advert_sound_scope after the existing 0xC0DE0007 + // tail (notif_melody_ad + units_imperial + trail_show_pace). Older files + // leave stray/old bytes in these fields; they're clamped above, so + // upgraders fall back to built-in advert sound + metric + speed + All. } file.close(); @@ -445,6 +447,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.notif_melody_ad, sizeof(_prefs.notif_melody_ad)); file.write((uint8_t *)&_prefs.units_imperial, sizeof(_prefs.units_imperial)); file.write((uint8_t *)&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace)); + file.write((uint8_t *)&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index dbd621b9..91c634b8 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -360,8 +360,8 @@ void MyMesh::onContactsFull() { } } -void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { - if (_ui) _ui->notify(UIEventType::advertReceived); +void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path, bool was_flood) { + if (_ui) _ui->notify(was_flood ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop); if (_serial->isConnected()) { if (is_new) { @@ -927,7 +927,7 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { memcpy(r.pub_key, pub_key, PUB_KEY_SIZE); r.timestamp = getRTCClock()->getCurrentTime(); } - if (_ui) _ui->notify(UIEventType::advertReceived); + if (_ui) _ui->notify(packet->isRouteFlood() ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop); return; // our discover — don't forward to BLE app } } @@ -1100,6 +1100,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.ringtone_len = 0; // no custom ringtone by default _prefs.ringtone2_bpm_idx = 2; // 120 bpm default _prefs.notif_melody_ad = 0; // built-in advert sound by default + _prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL; // sound every advert by default _prefs.home_pages_mask = 0x01FF; // all pages visible _prefs.bot_enabled = 0; _prefs.bot_channel_enabled = 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 4228e0da..912a402d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,7 +165,7 @@ protected: void onContactsFull() override; void onContactOverwrite(const uint8_t* pub_key) override; 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 onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path, bool was_flood) override; void onContactPathUpdated(const ContactInfo &contact) override; ContactInfo* processAck(const uint8_t *data) override; void queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, uint32_t sender_timestamp, diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 700b00f8..0586d5fe 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -9,6 +9,9 @@ #define ADVERT_LOC_NONE 0 #define ADVERT_LOC_SHARE 1 +#define ADVERT_SOUND_SCOPE_ALL 0 +#define ADVERT_SOUND_SCOPE_ZERO_HOP 1 + struct NodePrefs { // persisted to file float airtime_factor; char node_name[32]; @@ -73,6 +76,8 @@ struct NodePrefs { // persisted to file uint8_t notif_melody_dm; uint8_t notif_melody_ch; uint8_t notif_melody_ad; + // Advert sound filter: 0=all adverts, 1=zero-hop only + uint8_t advert_sound_scope; // Per-channel melody override (2 bitmasks, 1 bit per channel) uint64_t ch_notif_melody_set; // bit i = channel i has explicit melody uint64_t ch_notif_melody_2; // bit i = use melody 2 (else melody 1, when set bit is set) @@ -120,7 +125,7 @@ struct NodePrefs { // persisted to file // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE0007; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0008; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 6740b9da..c6e95e90 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -39,6 +39,7 @@ class SettingsScreen : public UIScreen { DM_MELODY, CH_MELODY, AD_SOUND, + AD_SOUND_SCOPE, // Home pages section SECTION_HOME_PAGES, HOME_CLOCK, HOME_FAVOURITES, HOME_RECENT, HOME_RADIO, HOME_BT, HOME_ADVERT, @@ -91,6 +92,8 @@ class SettingsScreen : public UIScreen { static const int BATT_DISPLAY_COUNT = 3; static const char* SOUND_LABELS[4]; static const int SOUND_COUNT = 4; + static const char* AD_SCOPE_LABELS[2]; + static const int AD_SCOPE_COUNT = 2; #if FEAT_FULL_REFRESH_SETTING static const char* EINK_FULL_REFRESH_LABELS[5]; static const int EINK_FULL_REFRESH_COUNT = 5; @@ -452,6 +455,11 @@ class SettingsScreen : public UIScreen { display.setCursor(display.valCol(), y); { uint8_t v = p ? p->notif_melody_ad : 0; display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); } + } else if (item == AD_SOUND_SCOPE) { + display.print("AD scope"); + display.setCursor(display.valCol(), y); + { uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL; + display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); } } else if (isHomePage(item)) { if (p) ensurePageOrderInit(p); int pos = homePagePosition(item, p); @@ -706,6 +714,10 @@ public: p->notif_melody_ad = (p->notif_melody_ad + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT; _dirty = true; return true; } + if (_selected == AD_SOUND_SCOPE && p && (left || right || enter)) { + p->advert_sound_scope ^= 1; + _dirty = true; return true; + } if (isHomePage(_selected) && p) { if (left || right) { movePageInOrder(_selected, left ? -1 : 1, p); @@ -839,6 +851,7 @@ const uint16_t SettingsScreen::LOW_BAT_OPTS[7] = { 0, 3000, 3100, 3200, 3300, const char* SettingsScreen::LOW_BAT_LABELS[7] = { "off", "3.0V", "3.1V", "3.2V", "3.3V", "3.4V", "3.5V" }; const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "icon", "%", "V" }; const char* SettingsScreen::SOUND_LABELS[4] = { "built-in", "M1", "M2", "None" }; +const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" }; #if FEAT_FULL_REFRESH_SETTING const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "off", "5", "10", "20", "30" }; #endif diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 465a8c77..1ed0ace5 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1469,7 +1469,13 @@ switch(t){ _last_notif_ch_idx = -1; break; } - case UIEventType::advertReceived: { + case UIEventType::advertReceived: + case UIEventType::advertReceivedFlood: + case UIEventType::advertReceivedZeroHop: { + bool is_flood = (t == UIEventType::advertReceivedFlood); + if (_node_prefs && _node_prefs->advert_sound_scope == ADVERT_SOUND_SCOPE_ZERO_HOP && is_flood) { + break; + } if (!buzzer.isQuiet()) { int slot = _node_prefs ? (int)_node_prefs->notif_melody_ad : 0; bool custom_played = false; diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index d5066736..954ddc6e 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -202,7 +202,11 @@ protected: return true; } - void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) override { + void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path, bool was_flood) override { + (void)is_new; + (void)path_len; + (void)path; + (void)was_flood; // TODO: if not in favs, prompt to add as fav(?) Serial.printf("ADVERT from -> %s\n", contact.name); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index d3ef034e..12d30b4b 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -144,7 +144,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, if (!shouldAutoAddContactType(parser.getType())) { ContactInfo ci; populateContactFromAdvert(ci, id, parser, timestamp); - onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know + onDiscoveredContact(ci, true, packet->path_len, packet->path, packet->isRouteFlood()); // let UI know return; } @@ -153,7 +153,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, if (max_hops > 0 && packet->getPathHashCount() >= max_hops) { ContactInfo ci; populateContactFromAdvert(ci, id, parser, timestamp); - onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know + onDiscoveredContact(ci, true, packet->path_len, packet->path, packet->isRouteFlood()); // let UI know return; } @@ -161,7 +161,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, if (from == NULL) { ContactInfo ci; populateContactFromAdvert(ci, id, parser, timestamp); - onDiscoveredContact(ci, true, packet->path_len, packet->path); + onDiscoveredContact(ci, true, packet->path_len, packet->path, packet->isRouteFlood()); onContactsFull(); MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact"); return; @@ -183,7 +183,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, from->last_advert_timestamp = timestamp; from->lastmod = getRTCClock()->getCurrentTime(); - onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know + onDiscoveredContact(*from, is_new, packet->path_len, packet->path, packet->isRouteFlood()); // let UI know } int BaseChatMesh::searchPeersByHash(const uint8_t* hash) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index c04bfda3..4087b1ca 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -102,7 +102,7 @@ protected: virtual bool shouldOverwriteWhenFull() const { return false; } virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops virtual void onContactOverwrite(const uint8_t* pub_key) {}; - virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0; + virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path, bool was_flood) = 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);