diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 4c99f992..9832de42 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -258,6 +258,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx)); file.read((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger)); file.read((uint8_t *)_prefs.bot_reply, sizeof(_prefs.bot_reply)); + if (file.available()) { + file.read((uint8_t *)&_prefs.bot_target_type, sizeof(_prefs.bot_target_type)); + file.read((uint8_t *)_prefs.bot_dm_pubkey, sizeof(_prefs.bot_dm_pubkey)); + } } } } @@ -322,6 +326,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx)); file.write((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger)); file.write((uint8_t *)_prefs.bot_reply, sizeof(_prefs.bot_reply)); + file.write((uint8_t *)&_prefs.bot_target_type, sizeof(_prefs.bot_target_type)); + file.write((uint8_t *)_prefs.bot_dm_pubkey, sizeof(_prefs.bot_dm_pubkey)); file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a5e40a84..1147577b 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -512,6 +512,33 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t const char *text) { markConnectionActive(from); // in case this is from a server, and we have a connection queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); + + // DM auto-reply bot + if (_prefs.bot_enabled && _prefs.bot_target_type == 1 && + _prefs.bot_trigger[0] && _prefs.bot_reply[0] && + millis() - _bot_last_reply_ms > 10000UL) { + // all-zero pubkey = any sender; otherwise match first 4 bytes + bool key_ok = (_prefs.bot_dm_pubkey[0] == 0 && _prefs.bot_dm_pubkey[1] == 0 && + _prefs.bot_dm_pubkey[2] == 0 && _prefs.bot_dm_pubkey[3] == 0) || + memcmp(from.id.pub_key, _prefs.bot_dm_pubkey, 4) == 0; + if (key_ok) { + const char* t = text; + const char* tr = _prefs.bot_trigger; + int tlen = strlen(tr); + bool matched = false; + for (; *t && !matched; t++) { + int i = 0; + while (i < tlen && tolower((uint8_t)t[i]) == tolower((uint8_t)tr[i])) i++; + if (i == tlen) matched = true; + } + if (matched) { + uint32_t ts = getRTCClock()->getCurrentTime(); + uint32_t expected_ack, est_timeout; + if (sendMessage(from, ts, 0, _prefs.bot_reply, expected_ack, est_timeout) != MSG_SEND_FAILED) + _bot_last_reply_ms = millis(); + } + } + } } void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, @@ -899,6 +926,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.bot_channel_idx = 0; _prefs.bot_trigger[0] = '\0'; _prefs.bot_reply[0] = '\0'; + _prefs.bot_target_type = 0; + memset(_prefs.bot_dm_pubkey, 0, sizeof(_prefs.bot_dm_pubkey)); _prefs.auto_off_secs = 15; // 15 seconds auto-off by default _prefs.tz_offset_hours = 0; // UTC by default _prefs.low_batt_mv = 3400; // auto-shutdown at 3.4V by default diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 57302120..68bf8dc6 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -50,7 +50,9 @@ struct NodePrefs { // persisted to file uint8_t ringtone_notes[32]; // packed: bits0-2=pitch, bits3-4=octave-4, bits5-6=dur_idx uint16_t home_pages_mask; // bitmask of visible home pages (bit0=Clock..bit8=Shutdown); 0=all visible uint8_t bot_enabled; // 0=disabled, 1=enabled - uint8_t bot_channel_idx; // channel index to monitor + uint8_t bot_channel_idx; // channel index to monitor (when bot_target_type==0) char bot_trigger[64]; // trigger phrase (case-insensitive contains match) char bot_reply[140]; // auto-reply text + uint8_t bot_target_type; // 0=channel (default), 1=DM contact + uint8_t bot_dm_pubkey[4]; // pubkey prefix of DM target; all-zero = any DM sender }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c00ca6e9..65751a32 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2108,10 +2108,16 @@ class BotScreen : public UIScreen { static const char KB_CHARS[4][10]; static const int KB_ROWS = 4, KB_COLS = 10; - // channel count cache (refreshed on enter) - int _num_channels; + // channel + DM contact caches (refreshed on enter) + static const int MAX_BOT_DM = 16; + + int _num_channels; uint8_t _channel_indices[MAX_GROUP_CHANNELS]; + int _num_dm; + uint8_t _dm_pubkeys[MAX_BOT_DM][4]; + char _dm_names[MAX_BOT_DM][32]; + void refreshChannels() { _num_channels = 0; ChannelDetails ch; @@ -2121,11 +2127,32 @@ class BotScreen : public UIScreen { } } - // returns display index of current bot_channel_idx in _channel_indices - int currentChanDisplayIdx() const { - for (int i = 0; i < _num_channels; i++) - if (_channel_indices[i] == _prefs->bot_channel_idx) return i; - return 0; + void refreshContacts() { + _num_dm = 0; + ContactInfo ci; + int n = the_mesh.getNumContacts(); + for (int i = 0; i < n && _num_dm < MAX_BOT_DM; i++) { + if (!the_mesh.getContactByIdx(i, ci)) continue; + if (ci.type != ADV_TYPE_CHAT) continue; + memcpy(_dm_pubkeys[_num_dm], ci.id.pub_key, 4); + strncpy(_dm_names[_num_dm], ci.name, sizeof(_dm_names[0]) - 1); + _dm_names[_num_dm][sizeof(_dm_names[0]) - 1] = '\0'; + _num_dm++; + } + } + + // returns combined index in [channels..., DM contacts...] + int currentTargetIdx() const { + if (_prefs->bot_target_type == 0) { + for (int i = 0; i < _num_channels; i++) + if (_channel_indices[i] == _prefs->bot_channel_idx) return i; + return 0; + } else { + for (int i = 0; i < _num_dm; i++) + if (memcmp(_dm_pubkeys[i], _prefs->bot_dm_pubkey, 4) == 0) + return _num_channels + i; + return _num_channels; // fallback: first DM + } } public: @@ -2135,6 +2162,7 @@ public: _sel = 0; _kb_field = -1; refreshChannels(); + refreshContacts(); } int render(DisplayDriver& display) override { @@ -2194,7 +2222,7 @@ public: display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT"); display.fillRect(0, 10, display.width(), 1); - static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply" }; + static const char* labels[] = { "Enable", NULL, "Trigger", "Reply" }; for (int i = 0; i < ITEM_COUNT; i++) { int y = START_Y + i * ITEM_H; bool sel = (i == _sel); @@ -2203,20 +2231,33 @@ public: display.setColor(DisplayDriver::DARK); } display.setCursor(2, y); - display.print(labels[i]); + if (i == 1) + display.print(_prefs->bot_target_type == 0 ? "Channel" : "Contact"); + else + display.print(labels[i]); display.setCursor(VAL_X, y); if (i == 0) { display.print(_prefs->bot_enabled ? "ON" : "OFF"); } else if (i == 1) { - if (_num_channels == 0) { + int total = _num_channels + _num_dm; + if (total == 0) { display.print("none"); - } else { + } else if (_prefs->bot_target_type == 0) { ChannelDetails ch; - if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0]) + if (_num_channels > 0 && the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0]) display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, ch.name); else - display.print("Ch?"); + display.print("?"); + } else { + bool found = false; + for (int j = 0; j < _num_dm && !found; j++) { + if (memcmp(_dm_pubkeys[j], _prefs->bot_dm_pubkey, 4) == 0) { + display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, _dm_names[j]); + found = true; + } + } + if (!found) display.print("?"); } } else if (i == 2) { const char* tr = _prefs->bot_trigger; @@ -2297,11 +2338,23 @@ public: _prefs->bot_enabled ^= 1; return true; } - if (_sel == 1 && _num_channels > 0) { - int idx = currentChanDisplayIdx(); - if (right || enter) idx = (idx + 1) % _num_channels; - if (left) idx = (idx + _num_channels - 1) % _num_channels; - if (left || right || enter) { _prefs->bot_channel_idx = _channel_indices[idx]; return true; } + if (_sel == 1) { + int total = _num_channels + _num_dm; + if (total == 0) return false; + int idx = currentTargetIdx(); + if (right || enter) idx = (idx + 1) % total; + if (left) idx = (idx + total - 1) % total; + if (left || right || enter) { + if (idx < _num_channels) { + _prefs->bot_target_type = 0; + _prefs->bot_channel_idx = _channel_indices[idx]; + } else { + int di = idx - _num_channels; + _prefs->bot_target_type = 1; + memcpy(_prefs->bot_dm_pubkey, _dm_pubkeys[di], 4); + } + return true; + } } if ((_sel == 2 || _sel == 3) && enter) { _kb_field = _sel;