diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index ebd974e..d499bc7 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -230,6 +230,18 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88 + // Fields added later — may not exist in older prefs files + if (file.read((uint8_t *)&_prefs.kb_flash_notify, sizeof(_prefs.kb_flash_notify)) != sizeof(_prefs.kb_flash_notify)) { + _prefs.kb_flash_notify = 0; // default OFF for old files + } + if (file.read((uint8_t *)&_prefs.ringtone_enabled, sizeof(_prefs.ringtone_enabled)) != sizeof(_prefs.ringtone_enabled)) { + _prefs.ringtone_enabled = 0; // default OFF for old files + } + + // Clamp booleans to 0/1 in case of garbage + if (_prefs.kb_flash_notify > 1) _prefs.kb_flash_notify = 0; + if (_prefs.ringtone_enabled > 1) _prefs.ringtone_enabled = 0; + file.close(); } } @@ -265,6 +277,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88 + file.write((uint8_t *)&_prefs.kb_flash_notify, sizeof(_prefs.kb_flash_notify)); // 89 + file.write((uint8_t *)&_prefs.ringtone_enabled, sizeof(_prefs.ringtone_enabled)); // 90 file.close(); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 951c9d0..d02af39 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -30,4 +30,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_config; // bitmask for auto-add contacts config int8_t utc_offset_hours; // UTC offset in hours (-12 to +14), default 0 uint8_t kb_flash_notify; // Keyboard backlight flash on new message (0=off, 1=on) + uint8_t ringtone_enabled; // Ringtone on incoming call (0=off, 1=on) — 4G only }; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/ModemManager.cpp b/examples/companion_radio/ui-new/ModemManager.cpp index 62e6042..7dd7339 100644 --- a/examples/companion_radio/ui-new/ModemManager.cpp +++ b/examples/companion_radio/ui-new/ModemManager.cpp @@ -33,6 +33,10 @@ void ModemManager::begin() { _operator[0] = '\0'; _callPhone[0] = '\0'; _callStartTime = 0; + _ringtoneEnabled = false; + _ringing = false; + _nextRingTone = 0; + _toneActive = false; _urcPos = 0; _imei[0] = '\0'; _imsi[0] = '\0'; @@ -605,6 +609,46 @@ bool ModemManager::doSetVolume(uint8_t level) { return ok; } +// --------------------------------------------------------------------------- +// Incoming call ringtone — tone bursts via AT+SIMTONE on modem speaker +// Pattern: 400ms tone → 1200ms silence → repeat +// --------------------------------------------------------------------------- + +void ModemManager::handleRingtone() { + bool nowRinging = (_state == ModemState::RINGING_IN); + + if (nowRinging && !_ringing) { + // Just started ringing + _ringing = true; + _nextRingTone = 0; // Play first burst immediately + _toneActive = false; + } else if (!nowRinging && _ringing) { + // Ringing stopped (answered, rejected, missed) + _ringing = false; + if (_toneActive) { + sendAT("AT+SIMTONE=0", "OK", 500); + _toneActive = false; + } + return; + } + + if (!_ringing || !_ringtoneEnabled) return; + + unsigned long now = millis(); + if (now < _nextRingTone) return; + + if (!_toneActive) { + // Play tone burst: 1000 Hz, level 5000 (of 50-25500), 400ms duration + sendAT("AT+SIMTONE=1,1000,5000,400", "OK", 500); + _toneActive = true; + _nextRingTone = now + 400; // Tone plays for 400ms + } else { + // Tone just finished — gap before next burst + _toneActive = false; + _nextRingTone = now + 1200; // 1.2s silence (classic ring cadence) + } +} + // --------------------------------------------------------------------------- // FreeRTOS Task // --------------------------------------------------------------------------- @@ -829,6 +873,11 @@ restart: // ================================================================ drainURCs(); + // ================================================================ + // Step 1b: Ringtone — play tone bursts while incoming call rings + // ================================================================ + handleRingtone(); + // ================================================================ // Step 2: Process call commands from main loop // ================================================================ diff --git a/examples/companion_radio/ui-new/ModemManager.h b/examples/companion_radio/ui-new/ModemManager.h index 785202d..e87979f 100644 --- a/examples/companion_radio/ui-new/ModemManager.h +++ b/examples/companion_radio/ui-new/ModemManager.h @@ -142,6 +142,10 @@ public: bool setCallVolume(uint8_t level); // Set volume 0-5 bool pollCallEvent(CallEvent& out); // Poll from main loop + // Ringtone control — called from main loop + void setRingtoneEnabled(bool en) { _ringtoneEnabled = en; } + bool isRingtoneEnabled() const { return _ringtoneEnabled; } + // --- State queries (lock-free reads) --- ModemState getState() const { return _state; } int getSignalBars() const; // 0-5 @@ -203,6 +207,12 @@ private: char _callPhone[SMS_PHONE_LEN] = {0}; // Current call number volatile uint32_t _callStartTime = 0; // millis() when call connected + // Ringtone state + volatile bool _ringtoneEnabled = false; + bool _ringing = false; // Shadow of RINGING_IN for tone logic + unsigned long _nextRingTone = 0; // Next tone burst timestamp (modem task) + bool _toneActive = false; // Is a tone currently sounding + TaskHandle_t _taskHandle = nullptr; // SMS queues @@ -242,6 +252,7 @@ private: bool doSendDTMF(char digit); bool doSetVolume(uint8_t level); void queueCallEvent(CallEventType type, const char* phone = nullptr, uint32_t duration = 0); + void handleRingtone(); // Play tone bursts while incoming call rings // FreeRTOS task static void taskEntry(void* param); diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 3ed56af..f055e4f 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -15,24 +15,6 @@ class UITask; class MyMesh; extern MyMesh the_mesh; -// --------------------------------------------------------------------------- -// Auto-add config bitmask (mirrored from MyMesh.cpp for UI access) -// --------------------------------------------------------------------------- -#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full -#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) -#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) -#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) -#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) - -// All type bits combined (excludes overwrite flag) -#define AUTO_ADD_ALL_TYPES (AUTO_ADD_CHAT | AUTO_ADD_REPEATER | AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR) - -// Contact mode indices for picker -#define CONTACT_MODE_AUTO_ALL 0 // Add all contacts automatically -#define CONTACT_MODE_CUSTOM 1 // Per-type toggles -#define CONTACT_MODE_MANUAL 2 // No auto-add, companion app only -#define CONTACT_MODE_COUNT 3 - // --------------------------------------------------------------------------- // Radio presets // --------------------------------------------------------------------------- @@ -69,35 +51,29 @@ static const RadioPreset RADIO_PRESETS[] = { // Settings row types // --------------------------------------------------------------------------- enum SettingsRowType : uint8_t { - ROW_NAME, // Device name (text editor) - ROW_RADIO_PRESET, // Radio preset picker - ROW_FREQ, // Frequency (float) - ROW_BW, // Bandwidth (float) - ROW_SF, // Spreading factor (5-12) - ROW_CR, // Coding rate (5-8) - ROW_TX_POWER, // TX power (1-20 dBm) - ROW_UTC_OFFSET, // UTC offset (-12 to +14) - ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle + ROW_NAME, // Device name (text editor) + ROW_RADIO_PRESET, // Radio preset picker + ROW_FREQ, // Frequency (float) + ROW_BW, // Bandwidth (float) + ROW_SF, // Spreading factor (5-12) + ROW_CR, // Coding rate (5-8) + ROW_TX_POWER, // TX power (1-20 dBm) + ROW_UTC_OFFSET, // UTC offset (-12 to +14) + ROW_MSG_NOTIFY, // Keyboard flash on new msg toggle #ifdef HAS_4G_MODEM - ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only) + ROW_MODEM_TOGGLE, // 4G modem enable/disable toggle (4G builds only) + ROW_RINGTONE, // Incoming call ringtone toggle (4G builds only) #endif - ROW_CONTACT_HEADER, // "--- Contacts ---" separator - ROW_CONTACT_MODE, // Contact auto-add mode picker (Auto All / Custom / Manual) - ROW_AUTOADD_CHAT, // Toggle: auto-add Chat clients - ROW_AUTOADD_REPEATER, // Toggle: auto-add Repeaters - ROW_AUTOADD_ROOM, // Toggle: auto-add Room Servers - ROW_AUTOADD_SENSOR, // Toggle: auto-add Sensors - ROW_AUTOADD_OVERWRITE, // Toggle: overwrite oldest non-favourite when full - ROW_CH_HEADER, // "--- Channels ---" separator - ROW_CHANNEL, // A channel entry (dynamic, index stored separately) - ROW_ADD_CHANNEL, // "+ Add Hashtag Channel" - ROW_INFO_HEADER, // "--- Info ---" separator - ROW_PUB_KEY, // Public key display - ROW_FIRMWARE, // Firmware version + ROW_CH_HEADER, // "--- Channels ---" separator + ROW_CHANNEL, // A channel entry (dynamic, index stored separately) + ROW_ADD_CHANNEL, // "+ Add Hashtag Channel" + ROW_INFO_HEADER, // "--- Info ---" separator + ROW_PUB_KEY, // Public key display + ROW_FIRMWARE, // Firmware version #ifdef HAS_4G_MODEM - ROW_IMEI, // IMEI display (read-only) - ROW_OPERATOR_INFO, // Carrier/operator display (read-only) - ROW_APN, // APN setting (editable) + ROW_IMEI, // IMEI display (read-only) + ROW_OPERATOR_INFO, // Carrier/operator display (read-only) + ROW_APN, // APN setting (editable) #endif }; @@ -107,16 +83,16 @@ enum SettingsRowType : uint8_t { enum EditMode : uint8_t { EDIT_NONE, // Just browsing EDIT_TEXT, // Typing into a text buffer (name, channel name) - EDIT_PICKER, // A/D cycles options (radio preset, contact mode) + EDIT_PICKER, // A/D cycles options (radio preset) EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC) EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio) }; -// Max rows in the settings list (increased for contact sub-toggles) +// Max rows in the settings list #ifdef HAS_4G_MODEM -#define SETTINGS_MAX_ROWS 54 // Extra rows for IMEI, Carrier, APN + contacts +#define SETTINGS_MAX_ROWS 46 // Extra rows for IMEI, Carrier, APN #else -#define SETTINGS_MAX_ROWS 48 +#define SETTINGS_MAX_ROWS 40 #endif #define SETTINGS_TEXT_BUF 33 // 32 chars + null @@ -126,7 +102,7 @@ private: mesh::RTCClock* _rtc; NodePrefs* _prefs; - // Row table — rebuilt whenever channels or contact mode change + // Row table — rebuilt whenever channels change struct Row { SettingsRowType type; uint8_t param; // channel index for ROW_CHANNEL, preset index for ROW_RADIO_PRESET @@ -142,7 +118,7 @@ private: EditMode _editMode; char _editBuf[SETTINGS_TEXT_BUF]; int _editPos; - int _editPickerIdx; // for preset picker / contact mode picker + int _editPickerIdx; // for preset picker float _editFloat; // for freq/BW editing int _editInt; // for SF/CR/TX/UTC editing int _confirmAction; // 0=none, 1=delete channel, 2=apply radio @@ -150,7 +126,7 @@ private: // Onboarding mode bool _onboarding; - // Dirty flag for radio params — prompt to apply + // Dirty flag for radio params — prompt to apply bool _radioChanged; // 4G modem state (runtime cache of config) @@ -158,57 +134,6 @@ private: bool _modemEnabled; #endif - // --------------------------------------------------------------------------- - // Contact mode helpers - // --------------------------------------------------------------------------- - - // Determine current contact mode from prefs - int getContactMode() const { - if ((_prefs->manual_add_contacts & 1) == 0) { - return CONTACT_MODE_AUTO_ALL; - } - // manual_add_contacts bit 0 is set — check if any type bits are enabled - if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) != 0) { - return CONTACT_MODE_CUSTOM; - } - return CONTACT_MODE_MANUAL; - } - - // Get display label for a contact mode - static const char* contactModeLabel(int mode) { - switch (mode) { - case CONTACT_MODE_AUTO_ALL: return "Auto All"; - case CONTACT_MODE_CUSTOM: return "Custom"; - case CONTACT_MODE_MANUAL: return "Manual Only"; - default: return "?"; - } - } - - // Apply a contact mode selection from picker - void applyContactMode(int mode) { - switch (mode) { - case CONTACT_MODE_AUTO_ALL: - _prefs->manual_add_contacts &= ~1; // clear bit 0 → auto all - break; - case CONTACT_MODE_CUSTOM: - _prefs->manual_add_contacts |= 1; // set bit 0 → selective - // If no type bits are set, default to all types enabled - if ((_prefs->autoadd_config & AUTO_ADD_ALL_TYPES) == 0) { - _prefs->autoadd_config |= AUTO_ADD_ALL_TYPES; - } - break; - case CONTACT_MODE_MANUAL: - _prefs->manual_add_contacts |= 1; // set bit 0 → selective - _prefs->autoadd_config &= ~AUTO_ADD_ALL_TYPES; // clear all type bits - // Note: keeps AUTO_ADD_OVERWRITE_OLDEST bit unchanged - break; - } - the_mesh.savePrefs(); - rebuildRows(); // show/hide sub-toggles - Serial.printf("Settings: Contact mode = %s (manual=%d, autoadd=0x%02X)\n", - contactModeLabel(mode), _prefs->manual_add_contacts, _prefs->autoadd_config); - } - // --------------------------------------------------------------------------- // Row table management // --------------------------------------------------------------------------- @@ -227,22 +152,8 @@ private: addRow(ROW_MSG_NOTIFY); #ifdef HAS_4G_MODEM addRow(ROW_MODEM_TOGGLE); + addRow(ROW_RINGTONE); #endif - - // --- Contacts section --- - addRow(ROW_CONTACT_HEADER); - addRow(ROW_CONTACT_MODE); - - // Show per-type sub-toggles only in Custom mode - if (getContactMode() == CONTACT_MODE_CUSTOM) { - addRow(ROW_AUTOADD_CHAT); - addRow(ROW_AUTOADD_REPEATER); - addRow(ROW_AUTOADD_ROOM); - addRow(ROW_AUTOADD_SENSOR); - addRow(ROW_AUTOADD_OVERWRITE); - } - - // --- Channels section --- addRow(ROW_CH_HEADER); // Enumerate current channels @@ -283,7 +194,7 @@ private: bool isSelectable(int idx) const { if (idx < 0 || idx >= _numRows) return false; SettingsRowType t = _rows[idx].type; - return t != ROW_CH_HEADER && t != ROW_INFO_HEADER && t != ROW_CONTACT_HEADER + return t != ROW_CH_HEADER && t != ROW_INFO_HEADER #ifdef HAS_4G_MODEM && t != ROW_IMEI && t != ROW_OPERATOR_INFO #endif @@ -337,11 +248,11 @@ private: strncpy(newCh.name, chanName, sizeof(newCh.name)); newCh.name[31] = '\0'; - // SHA-256 the channel name → first 16 bytes become the secret + // SHA-256 the channel name → first 16 bytes become the secret uint8_t hash[32]; mesh::Utils::sha256(hash, 32, (const uint8_t*)chanName, strlen(chanName)); memcpy(newCh.channel.secret, hash, 16); - // Upper 16 bytes left as zero → setChannel uses 128-bit mode + // Upper 16 bytes left as zero → setChannel uses 128-bit mode // Find next empty slot for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { @@ -607,56 +518,14 @@ public: _modemEnabled ? "ON" : "OFF"); display.print(tmp); break; + + case ROW_RINGTONE: + snprintf(tmp, sizeof(tmp), "Incoming Call Ring: %s", + _prefs->ringtone_enabled ? "ON" : "OFF"); + display.print(tmp); + break; #endif - // --- Contacts section --- - case ROW_CONTACT_HEADER: - display.setColor(DisplayDriver::YELLOW); - display.print("--- Contacts ---"); - break; - - case ROW_CONTACT_MODE: - if (editing && _editMode == EDIT_PICKER) { - snprintf(tmp, sizeof(tmp), "< Add Mode: %s >", - contactModeLabel(_editPickerIdx)); - } else { - snprintf(tmp, sizeof(tmp), "Add Mode: %s", - contactModeLabel(getContactMode())); - } - display.print(tmp); - break; - - case ROW_AUTOADD_CHAT: - snprintf(tmp, sizeof(tmp), " Chat: %s", - (_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF"); - display.print(tmp); - break; - - case ROW_AUTOADD_REPEATER: - snprintf(tmp, sizeof(tmp), " Repeater: %s", - (_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF"); - display.print(tmp); - break; - - case ROW_AUTOADD_ROOM: - snprintf(tmp, sizeof(tmp), " Room Server: %s", - (_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF"); - display.print(tmp); - break; - - case ROW_AUTOADD_SENSOR: - snprintf(tmp, sizeof(tmp), " Sensor: %s", - (_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF"); - display.print(tmp); - break; - - case ROW_AUTOADD_OVERWRITE: - snprintf(tmp, sizeof(tmp), " Overwrite Oldest: %s", - (_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF"); - display.print(tmp); - break; - - // --- Channels section --- case ROW_CH_HEADER: display.setColor(DisplayDriver::YELLOW); display.print("--- Channels ---"); @@ -748,7 +617,7 @@ public: char apnShort[29]; strncpy(apnShort, apn, 28); apnShort[28] = '\0'; - // Abbreviate source: auto->A, network->N, user->U, none->? + // Abbreviate source: auto→A, network→N, user→U, none→? char srcChar = '?'; if (strcmp(src, "auto") == 0) srcChar = 'A'; else if (strcmp(src, "network") == 0) srcChar = 'N'; @@ -914,53 +783,34 @@ public: return true; // consume all keys in text edit } - // --- Picker mode (radio preset or contact mode) --- + // --- Picker mode (radio preset) --- if (_editMode == EDIT_PICKER) { - SettingsRowType type = _rows[_cursor].type; - if (c == 'a' || c == 'A') { - if (type == ROW_CONTACT_MODE) { - _editPickerIdx--; - if (_editPickerIdx < 0) _editPickerIdx = CONTACT_MODE_COUNT - 1; - } else { - // Radio preset - _editPickerIdx--; - if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1; - } + _editPickerIdx--; + if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1; return true; } if (c == 'd' || c == 'D') { - if (type == ROW_CONTACT_MODE) { - _editPickerIdx++; - if (_editPickerIdx >= CONTACT_MODE_COUNT) _editPickerIdx = 0; - } else { - // Radio preset - _editPickerIdx++; - if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0; - } + _editPickerIdx++; + if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0; return true; } if (c == '\r' || c == 13) { - if (type == ROW_CONTACT_MODE) { - applyContactMode(_editPickerIdx); - _editMode = EDIT_NONE; - } else { - // Apply radio preset - if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { - const RadioPreset& p = RADIO_PRESETS[_editPickerIdx]; - _prefs->freq = p.freq; - _prefs->bw = p.bw; - _prefs->sf = p.sf; - _prefs->cr = p.cr; - _prefs->tx_power_dbm = p.tx_power; - _radioChanged = true; - } - _editMode = EDIT_NONE; - if (_onboarding) { - // Apply and finish onboarding - applyRadioParams(); - _onboarding = false; - } + // Apply preset + if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { + const RadioPreset& p = RADIO_PRESETS[_editPickerIdx]; + _prefs->freq = p.freq; + _prefs->bw = p.bw; + _prefs->sf = p.sf; + _prefs->cr = p.cr; + _prefs->tx_power_dbm = p.tx_power; + _radioChanged = true; + } + _editMode = EDIT_NONE; + if (_onboarding) { + // Apply and finish onboarding + applyRadioParams(); + _onboarding = false; } return true; } @@ -1114,6 +964,13 @@ public: Serial.println("Settings: 4G modem DISABLED (shutdown)"); } break; + case ROW_RINGTONE: + _prefs->ringtone_enabled = _prefs->ringtone_enabled ? 0 : 1; + modemManager.setRingtoneEnabled(_prefs->ringtone_enabled); + the_mesh.savePrefs(); + Serial.printf("Settings: Ringtone = %s\n", + _prefs->ringtone_enabled ? "ON" : "OFF"); + break; case ROW_APN: { // Start text editing with current APN as initial value const char* currentApn = modemManager.getAPN(); @@ -1121,44 +978,6 @@ public: break; } #endif - - // --- Contact mode picker --- - case ROW_CONTACT_MODE: - startEditPicker(getContactMode()); - break; - - // --- Contact sub-toggles (flip bit and save) --- - case ROW_AUTOADD_CHAT: - _prefs->autoadd_config ^= AUTO_ADD_CHAT; - the_mesh.savePrefs(); - Serial.printf("Settings: Auto-add Chat = %s\n", - (_prefs->autoadd_config & AUTO_ADD_CHAT) ? "ON" : "OFF"); - break; - case ROW_AUTOADD_REPEATER: - _prefs->autoadd_config ^= AUTO_ADD_REPEATER; - the_mesh.savePrefs(); - Serial.printf("Settings: Auto-add Repeater = %s\n", - (_prefs->autoadd_config & AUTO_ADD_REPEATER) ? "ON" : "OFF"); - break; - case ROW_AUTOADD_ROOM: - _prefs->autoadd_config ^= AUTO_ADD_ROOM_SERVER; - the_mesh.savePrefs(); - Serial.printf("Settings: Auto-add Room = %s\n", - (_prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "ON" : "OFF"); - break; - case ROW_AUTOADD_SENSOR: - _prefs->autoadd_config ^= AUTO_ADD_SENSOR; - the_mesh.savePrefs(); - Serial.printf("Settings: Auto-add Sensor = %s\n", - (_prefs->autoadd_config & AUTO_ADD_SENSOR) ? "ON" : "OFF"); - break; - case ROW_AUTOADD_OVERWRITE: - _prefs->autoadd_config ^= AUTO_ADD_OVERWRITE_OLDEST; - the_mesh.savePrefs(); - Serial.printf("Settings: Overwrite oldest = %s\n", - (_prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "ON" : "OFF"); - break; - case ROW_ADD_CHANNEL: startEditText(""); break; @@ -1182,7 +1001,7 @@ public: } } - // Q: back — if radio changed, prompt to apply first + // Q: back — if radio changed, prompt to apply first if (c == 'q' || c == 'Q') { if (_radioChanged) { _editMode = EDIT_CONFIRM; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 4fa344d..c6d2bc4 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -906,6 +906,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no digitalWrite(KB_BL_PIN, LOW); #endif +#ifdef HAS_4G_MODEM + // Sync ringtone enabled state to modem manager + modemManager.setRingtoneEnabled(node_prefs->ringtone_enabled); +#endif + ui_started_at = millis(); _alert_expiry = 0; @@ -1190,11 +1195,60 @@ void UITask::loop() { // Turn off keyboard flash after timeout #ifdef KB_BL_PIN if (_kb_flash_off_at && millis() >= _kb_flash_off_at) { + #ifdef HAS_4G_MODEM + // Don't turn off LED if incoming call flash is active + if (!_incomingCallRinging) { + digitalWrite(KB_BL_PIN, LOW); + } + #else digitalWrite(KB_BL_PIN, LOW); + #endif _kb_flash_off_at = 0; } #endif + // Incoming call LED flash — rapid repeated pulse while ringing +#if defined(HAS_4G_MODEM) && defined(KB_BL_PIN) + { + bool ringing = modemManager.isRinging(); + + if (ringing && !_incomingCallRinging) { + // Ringing just started + _incomingCallRinging = true; + _callFlashState = false; + _nextCallFlash = 0; // Start immediately + + // Wake display for incoming call + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + 60000; // Keep display on while ringing (60s) + + } else if (!ringing && _incomingCallRinging) { + // Ringing stopped + _incomingCallRinging = false; + // Only turn off LED if message flash isn't also active + if (!_kb_flash_off_at) { + digitalWrite(KB_BL_PIN, LOW); + } + _callFlashState = false; + } + + // Rapid LED flash while ringing (if kb_flash_notify is ON) + if (_incomingCallRinging && _node_prefs->kb_flash_notify) { + unsigned long now = millis(); + if (now >= _nextCallFlash) { + _callFlashState = !_callFlashState; + digitalWrite(KB_BL_PIN, _callFlashState ? HIGH : LOW); + // 250ms on, 250ms off — fast pulse to distinguish from single msg flash + _nextCallFlash = now + 250; + } + // Extend auto-off while ringing + _auto_off = millis() + 60000; + } + } +#endif + #ifdef PIN_BUZZER if (buzzer.isPlaying()) buzzer.loop(); #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 1490428..6a34a79 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -44,6 +44,11 @@ class UITask : public AbstractUITask { #endif unsigned long _next_refresh, _auto_off; unsigned long _kb_flash_off_at; // Keyboard flash turn-off timer +#ifdef HAS_4G_MODEM + bool _incomingCallRinging; // Currently ringing (incoming call) + unsigned long _nextCallFlash; // Next LED toggle time + bool _callFlashState; // Current LED state during ring +#endif NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; @@ -94,6 +99,11 @@ public: UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; _kb_flash_off_at = 0; + #ifdef HAS_4G_MODEM + _incomingCallRinging = false; + _nextCallFlash = 0; + _callFlashState = false; + #endif ui_started_at = 0; curr = NULL; }