From 15bbc1bc3aac0857572b04481d0f3543c5a315c8 Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 14 Jul 2026 23:08:25 +0200 Subject: [PATCH] SMS: pick recipient from contacts; contacts list shows all types - New SMS recipient picker: GREEN in the compose recipient field opens a list of private-call DMR contacts; selecting one fills the destination (using the contact's plain DMR ID) and moves to the message text. Manual ID entry still works (DOWN advances to the text field). - Menu -> Contacts now defaults to a combined 'Contacts' view of all contact types instead of the group-only filter; HASH still cycles the per-type filters. Per-type code paths are unchanged. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++ MANUAL.md | 14 +- .../include/user_interface/menuSystem.h | 3 + .../source/user_interface/menuContactList.c | 89 ++++++++-- .../source/user_interface/menuSMSCompose.c | 49 +++++- .../source/user_interface/menuSMSContact.c | 161 ++++++++++++++++++ .../source/user_interface/menuSystem.c | 2 + 7 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 MDUV380_firmware/application/source/user_interface/menuSMSContact.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cb8e7e..aa4f09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ single running list until the first versioned release. _Nothing yet._ +## 0.10.0 + +- **SMS recipient from contacts** — when composing a message, press **GREEN** in + the recipient field to pick the destination from your private-call DMR + contacts (or type a DMR ID manually and press **DOWN** to move to the text). +- **Contacts list shows everything** — Menu → Contacts now lists all contact + types together under the title "Contacts" instead of defaulting to the group + filter; **HASH** still cycles the per-type filters (all / groups / private / + all-call). + ## 0.9.0 - **Pride theme** — the status/header bar and the menu title bar are now painted diff --git a/MANUAL.md b/MANUAL.md index 7e2b839..4665c96 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -174,12 +174,14 @@ decoded on receive). screen. The hub offers *New message*, *Inbox*, *Sent*, *Quick text* and *Options*. -**Composing:** enter the destination DMR ID with the number keys (**UP/DOWN** -switches between the ID and text fields), type up to 160 characters with -multi-tap on the keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes), -then press **GREEN** to send. Sending only works on a DMR channel; progress is -shown as notifications ("Sending", "Sent", "Delivered"). With **Wait for ACK** -enabled (default), a missing delivery report pops up a **resend** prompt. +**Composing:** in the recipient field, either type the destination DMR ID with +the number keys and press **DOWN** to move on to the message, or press **GREEN** +to pick a recipient from your private-call DMR contacts (selecting one drops you +straight into the message text). Type up to 160 characters with multi-tap on the +keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes), then press +**GREEN** to send. Sending only works on a DMR channel; progress is shown as +notifications ("Sending", "Sent", "Delivered"). With **Wait for ACK** enabled +(default), a missing delivery report pops up a **resend** prompt. **Receiving:** a popup with a chime shows the sender and a preview — **GREEN** opens the message, **SK2+GREEN** starts a reply, **RED** dismisses. diff --git a/MDUV380_firmware/application/include/user_interface/menuSystem.h b/MDUV380_firmware/application/include/user_interface/menuSystem.h index aad576e..f003f88 100644 --- a/MDUV380_firmware/application/include/user_interface/menuSystem.h +++ b/MDUV380_firmware/application/include/user_interface/menuSystem.h @@ -328,6 +328,7 @@ enum MENU_SCREENS MENU_SMS_VIEW, MENU_SMS_QUICKTEXT, MENU_SMS_OPTIONS, + MENU_SMS_CONTACT, UI_SMS_RX_POPUP, UI_MESSAGE_BOX, UI_HOTSPOT_MODE, @@ -497,8 +498,10 @@ menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun); menuStatus_t menuSMSList(uiEvent_t *ev, bool isFirstRun); // serves inbox, sent and view menuStatus_t menuSMSQuickText(uiEvent_t *ev, bool isFirstRun); menuStatus_t menuSMSOptions(uiEvent_t *ev, bool isFirstRun); +menuStatus_t menuSMSContact(uiEvent_t *ev, bool isFirstRun); // pick an SMS recipient from the DMR contacts menuStatus_t uiSMSPopup(uiEvent_t *ev, bool isFirstRun); void menuSMSComposePrefill(uint32_t dstId, const char *text); // 0 / NULL leave the field untouched +void menuSMSComposeSetDestination(uint32_t dstId); // set only the recipient, keep any typed text void menuSMSComposeSetQuickTextEditTarget(int slotIndex); // compose edits a quick text instead of sending void menuSMSListSetViewTarget(bool sentBox, uint8_t recordIndex); diff --git a/MDUV380_firmware/application/source/user_interface/menuContactList.c b/MDUV380_firmware/application/source/user_interface/menuContactList.c index 0c4b467..30dea92 100644 --- a/MDUV380_firmware/application/source/user_interface/menuContactList.c +++ b/MDUV380_firmware/application/source/user_interface/menuContactList.c @@ -62,6 +62,58 @@ static menuStatus_t menuContactListSubMenuExitCode = MENU_STATUS_SUCCESS; static const char *calltypeVoices[3] = { NULL, NULL, NULL }; +// Pseudo call-type for the digital list: show every contact (groups, private +// calls and all-calls) together. This is the default; HASH still cycles the +// per-type filters. +#define CONTACT_LIST_COMBINED 0xFFU + +// Number of digital contacts under the current filter (all types when combined). +static int digitalContactsCount(void) +{ + if (contactCallType == CONTACT_LIST_COMBINED) + { + return codeplugContactsGetCount(CONTACT_CALLTYPE_TG) + + codeplugContactsGetCount(CONTACT_CALLTYPE_PC) + + codeplugContactsGetCount(CONTACT_CALLTYPE_ALL); + } + + return codeplugContactsGetCount(contactCallType); +} + +// Fetch the (1-based) digital contact under the current filter. When combined, +// the list runs groups first, then private calls, then all-calls. +static int digitalContactGetForNumber(int number, CodeplugContact_t *c) +{ + if (contactCallType != CONTACT_LIST_COMBINED) + { + return codeplugContactGetDataForNumberInType(number, contactCallType, c); + } + + int row = number - 1; + int count = codeplugContactsGetCount(CONTACT_CALLTYPE_TG); + + if (row < count) + { + return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_TG, c); + } + row -= count; + count = codeplugContactsGetCount(CONTACT_CALLTYPE_PC); + + if (row < count) + { + return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_PC, c); + } + row -= count; + + return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_ALL, c); +} + +// Voice/title label for the current filter. +static const char *contactCallTypeLabel(void) +{ + return ((contactCallType == CONTACT_LIST_COMBINED) ? currentLanguage->contacts : calltypeVoices[contactCallType]); +} + // Apply contact + its TS on selection for TX (contact list of quick list). static void overrideWithSelectedContact(void) { @@ -78,7 +130,7 @@ static void overrideWithSelectedContact(void) static void reloadContactList(contactListContactType_t type) { - menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? codeplugContactsGetCount(contactCallType) : codeplugDTMFContactsGetCount(); + menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? digitalContactsCount() : codeplugDTMFContactsGetCount(); if (menuDataGlobal.numItems > 0) { @@ -87,7 +139,7 @@ static void reloadContactList(contactListContactType_t type) menuDataGlobal.currentItemIndex = 0; } uiDataGlobal.currentSelectedContactIndex = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) - ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) + ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); } else @@ -119,7 +171,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun) // Shows digital contact list if called from "contact list" menu entry, or from +# in digital. // Otherwise displays DTMF contact list contactListType = ((currentMenu == MENU_CONTACT_LIST) || ((currentMenu == MENU_CONTACT_QUICKLIST) && (trxGetMode() != RADIO_MODE_ANALOG))) ? MENU_CONTACT_LIST_CONTACT_DIGITAL : MENU_CONTACT_LIST_CONTACT_DTMF; - contactCallType = CONTACT_CALLTYPE_TG; + contactCallType = CONTACT_LIST_COMBINED; // default: show all contact types together dtmfSequenceReset(); } @@ -146,7 +198,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun) { voicePromptsAppendLanguageString(currentLanguage->dmr_contacts); voicePromptsAppendPrompt(PROMPT_SILENCE); - voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); + voicePromptsAppendLanguageString(contactCallTypeLabel()); } else { @@ -158,7 +210,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun) { if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) { - voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); + voicePromptsAppendLanguageString(contactCallTypeLabel()); voicePromptsAppendPrompt(PROMPT_SILENCE); } } @@ -210,7 +262,9 @@ static void updateScreen(bool isFirstRun) switch (contactListDisplayState) { case MENU_CONTACT_LIST_DISPLAY: - menuDisplayTitle((char *) calltypeName[((contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? contactCallType : 3)]); + menuDisplayTitle((contactListType != MENU_CONTACT_LIST_CONTACT_DIGITAL) + ? (char *) calltypeName[3] // "DTMF" + : ((contactCallType == CONTACT_LIST_COMBINED) ? (char *) currentLanguage->contacts : (char *) calltypeName[contactCallType])); if (menuDataGlobal.numItems == 0) { @@ -235,7 +289,7 @@ static void updateScreen(bool isFirstRun) } idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) - ? codeplugContactGetDataForNumberInType(mNum + 1, contactCallType, &contact) + ? digitalContactGetForNumber(mNum + 1, &contact) : codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact); if (idx > 0) @@ -323,7 +377,7 @@ static void handleEvent(uiEvent_t *ev) { menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) - ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) + ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); voicePromptsInit(); updateScreen(false); @@ -333,7 +387,7 @@ static void handleEvent(uiEvent_t *ev) { menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) - ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) + ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); voicePromptsInit(); updateScreen(false); @@ -343,11 +397,24 @@ static void handleEvent(uiEvent_t *ev) { if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) { - contactCallType = (contactCallType + 1) % (CONTACT_CALLTYPE_ALL + 1); + // cycle: all combined -> group -> private -> all-call -> combined + if (contactCallType == CONTACT_LIST_COMBINED) + { + contactCallType = CONTACT_CALLTYPE_TG; + } + else if (contactCallType == CONTACT_CALLTYPE_ALL) + { + contactCallType = CONTACT_LIST_COMBINED; + } + else + { + contactCallType = contactCallType + 1; + } + menuDataGlobal.currentItemIndex = 0; reloadContactList(contactListType); voicePromptsInit(); - voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); + voicePromptsAppendLanguageString(contactCallTypeLabel()); voicePromptsAppendPrompt(PROMPT_SILENCE); updateScreen(false); diff --git a/MDUV380_firmware/application/source/user_interface/menuSMSCompose.c b/MDUV380_firmware/application/source/user_interface/menuSMSCompose.c index bc0e987..d3686ab 100644 --- a/MDUV380_firmware/application/source/user_interface/menuSMSCompose.c +++ b/MDUV380_firmware/application/source/user_interface/menuSMSCompose.c @@ -50,6 +50,11 @@ static int quickTextEditSlot = -1; static bool prefillPending = false; static int pendingQuickTextEditSlot = -1; +// set while the contact picker is open, so returning from it preserves the +// typed text and only moves to the message field when a contact was chosen +static bool pickerReturnPending = false; +static bool pickerSelected = false; + static menuStatus_t menuSMSComposeExitCode = MENU_STATUS_SUCCESS; static void updateScreen(void); @@ -74,6 +79,18 @@ void menuSMSComposePrefill(uint32_t dstId, const char *text) prefillPending = true; } +void menuSMSComposeSetDestination(uint32_t dstId) +{ + if (dstId != 0) + { + snprintf(destDigits, sizeof(destDigits), "%u", (unsigned int)dstId); + } + + // a contact was chosen in the picker: on return, keep the text and move + // the focus to the message field + pickerSelected = true; +} + void menuSMSComposeSetQuickTextEditTarget(int slotIndex) { pendingQuickTextEditSlot = slotIndex; @@ -101,6 +118,19 @@ menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun) focusedField = FIELD_TEXT; } + else if (pickerReturnPending) + { + // returning from the contact picker: keep the typed text and + // recipient; only advance to the message field if a contact was + // actually selected (a cancel stays on the recipient field) + if (pickerSelected) + { + focusedField = FIELD_TEXT; + } + + pickerReturnPending = false; + pickerSelected = false; + } else { if (prefillPending == false) @@ -237,7 +267,24 @@ static void handleEvent(uiEvent_t *ev) } else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN)) { - sendOrSave(); + if (quickTextEditSlot >= 0) + { + sendOrSave(); // save the quick-text template + } + else if (focusedField == FIELD_DESTINATION) + { + // in the recipient field, GREEN opens the contact picker; + // preserve any typed text/recipient whether the picker returns + // a selection or is cancelled + keypadAlphaEnable = false; + pickerReturnPending = true; + pickerSelected = false; + menuSystemPushNewMenu(MENU_SMS_CONTACT); + } + else + { + sendOrSave(); // send the message + } return; } else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0)) diff --git a/MDUV380_firmware/application/source/user_interface/menuSMSContact.c b/MDUV380_firmware/application/source/user_interface/menuSMSContact.c new file mode 100644 index 0000000..8b161fc --- /dev/null +++ b/MDUV380_firmware/application/source/user_interface/menuSMSContact.c @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2026-today Marcus Kida, DK1DA + * + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation, + * incorporation in a commercial product or incorporation into a product or project which allows commercial use. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +/* + * SMS recipient picker: lists the private-call (individual) DMR contacts from + * the codeplug. GREEN selects one as the message destination and returns to the + * compose screen; RED cancels. + */ +#include "user_interface/uiGlobals.h" +#include "user_interface/menuSystem.h" +#include "user_interface/uiLocalisation.h" +#include "user_interface/uiUtilities.h" +#include "functions/codeplug.h" + +static menuStatus_t menuSMSContactExitCode = MENU_STATUS_SUCCESS; + +static void updateScreen(void); +static void handleEvent(uiEvent_t *ev); + +// Fetch the private-call contact for the given list row. +static bool getContactForRow(int row, CodeplugContact_t *contact) +{ + return (codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_PC, contact) > 0); +} + +menuStatus_t menuSMSContact(uiEvent_t *ev, bool isFirstRun) +{ + if (isFirstRun) + { + menuDataGlobal.numItems = codeplugContactsGetCount(CONTACT_CALLTYPE_PC); + menuDataGlobal.currentItemIndex = 0; + updateScreen(); + + return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS); + } + + menuSMSContactExitCode = MENU_STATUS_SUCCESS; + + if (ev->hasEvent) + { + handleEvent(ev); + } + + return menuSMSContactExitCode; +} + +static void updateScreen(void) +{ + char entryBuffer[SCREEN_LINE_BUFFER_SIZE * 2]; + char nameBuffer[SCREEN_LINE_BUFFER_SIZE]; + CodeplugContact_t contact; + int mNum; + + displayClearBuf(); + menuDisplayTitle(currentLanguage->contacts); + + if (menuDataGlobal.numItems == 0) + { + displayPrintCentered(DISPLAY_SIZE_Y / 2, currentLanguage->none, FONT_SIZE_3); + } + + for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++) + { + if (menuDataGlobal.numItems == 0) + { + break; + } + + mNum = menuGetMenuOffset(menuDataGlobal.numItems, i); + if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY) + { + continue; + } + else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY) + { + break; + } + + if (getContactForRow(mNum, &contact)) + { + codeplugUtilConvertBufToString(contact.name, nameBuffer, 16); + snprintf(entryBuffer, sizeof(entryBuffer), "%s", nameBuffer); + } + else + { + entryBuffer[0] = 0; + } + + menuDisplayEntry(i, mNum, entryBuffer, 0, THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG); + } + + displayRender(); +} + +static void handleEvent(uiEvent_t *ev) +{ + if ((ev->events & KEY_EVENT) == 0) + { + return; + } + + if (KEYCHECK_SHORTUP(ev->keys, KEY_RED)) + { + menuSystemPopPreviousMenu(); + return; + } + else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN)) + { + if (menuDataGlobal.numItems > 0) + { + CodeplugContact_t contact; + + if (getContactForRow(menuDataGlobal.currentItemIndex, &contact)) + { + // tgNumber is the plain DMR ID; codeplugContactGetPackedId() + // would OR the call-type flag into the top byte, which is not a + // valid destination ID. + menuSMSComposeSetDestination(contact.tgNumber); + menuSystemPopPreviousMenu(); // back to compose, recipient filled in + return; + } + } + + menuSMSContactExitCode |= MENU_STATUS_ERROR; + return; + } + else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN)) + { + menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); + updateScreen(); + } + else if (KEYCHECK_PRESS(ev->keys, KEY_UP)) + { + menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); + updateScreen(); + } +} diff --git a/MDUV380_firmware/application/source/user_interface/menuSystem.c b/MDUV380_firmware/application/source/user_interface/menuSystem.c index 44044d9..d359d7f 100644 --- a/MDUV380_firmware/application/source/user_interface/menuSystem.c +++ b/MDUV380_firmware/application/source/user_interface/menuSystem.c @@ -98,6 +98,7 @@ menuDataGlobal_t menuDataGlobal = NULL,// SMS view NULL,// SMS quick texts NULL,// SMS options + NULL,// SMS contact picker NULL,// SMS RX popup NULL,// MessageBox NULL,// hotspot mode @@ -164,6 +165,7 @@ static menuFunctionData_t menuFunctions[] = { menuSMSList, NULL, NULL, 0 },// SMS view { menuSMSQuickText, NULL, NULL, 0 }, { menuSMSOptions, NULL, NULL, 0 }, + { menuSMSContact, NULL, NULL, 0 }, { uiSMSPopup, NULL, NULL, 0 }, { uiMessageBox, NULL, NULL, 0 }, { menuHotspotMode, NULL, NULL, 0 },