Compare commits

..

No commits in common. "main" and "v0.8.0" have entirely different histories.
main ... v0.8.0

19 changed files with 54 additions and 499 deletions

View file

@ -9,34 +9,6 @@ single running list until the first versioned release.
_Nothing yet._ _Nothing yet._
## 0.11.0
- **CRT-style shut-off animation** — when powering off, the current screen
collapses towards the middle (squeezed to a line, then a dot) like an old CRT
switching off, replacing the "Power Off" message. (Colour displays only.)
## 0.10.1
- **Fix:** the muted **"M"** indicator in the status bar overlapped the GPS text
when GPS was enabled. The header icon is removed; the muted state is shown by
the flashing red LED alone.
## 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
in the top rainbow-stripe colour so they blend into the background instead of
showing as white boxes.
## 0.8.0 ## 0.8.0
- **Three new theme presets** (Display Options → "Theme"): **Red** (black-on-red - **Three new theme presets** (Display Options → "Theme"): **Red** (black-on-red

View file

@ -59,11 +59,6 @@ Optionally dims the backlight after inactivity to **100 / 50 / 30 / 15 %** of th
configured brightness (only in **Auto** backlight mode). Any activity restores full configured brightness (only in **Auto** backlight mode). Any activity restores full
brightness. Set it in **Menu → Options → Display Options → Screen Dim**. brightness. Set it in **Menu → Options → Display Options → Screen Dim**.
### Shut-off animation — 🟢 [FreeTRX]
Powering off collapses whatever is on screen towards the middle — squeezed to a
line, then a dot — like an old CRT switching off, in place of the "Power Off"
message. (Once the animation starts, the power-off is committed.)
### Theme presets — 🟢 [FreeTRX] ### Theme presets — 🟢 [FreeTRX]
Pick a colour theme on the radio at **Menu → Options → Display Options → "Theme"** Pick a colour theme on the radio at **Menu → Options → Display Options → "Theme"**
(◀/▶ to change; the screen updates live): (◀/▶ to change; the screen updates live):
@ -85,7 +80,8 @@ Editing colours in the CPS only affects **Custom**; the built-in presets are
fixed. Every element (S-meter, frequencies, menus) follows the selected theme. fixed. Every element (S-meter, frequencies, menus) follows the selected theme.
### Mute indicator — 🟢 [FreeTRX] ### Mute indicator — 🟢 [FreeTRX]
While muted, the **red LED flashes** (0.5 s every 2 s). See *Mute* below. While muted, a bold **"M"** appears in the status bar (just left of the battery)
and the **red LED flashes** (0.5 s every 2 s). See *Mute* below.
--- ---
@ -118,21 +114,8 @@ A real-time signal-strength-vs-frequency graph centred on the VFO frequency.
- The scope is **VFO-only** (not available in Channel mode). - The scope is **VFO-only** (not available in Channel mode).
### Scanning — ⚪ [OpenGD77] ### Scanning — ⚪ [OpenGD77]
**There are no scan lists.** Unlike the vendor CPS, OpenGD77 scans **the channels Channel and VFO scanning with hold/pause behaviour, per the upstream OpenGD77
of the currently selected zone** — so a zone *is* the scan list. To scan a controls.
particular set of channels, put them in a zone and select it. The **All Channels**
virtual zone scans every channel in the codeplug.
- **Start/stop:** long-press **UP** on the Channel or VFO screen (**RED** stops).
- **Exclude a channel:** **Menu → Channel details** (or **SK2 + green** on the
Channel screen) → **Zone Skip** (skipped when scanning a normal zone) and
**All Skip** (skipped when scanning *All Channels*).
- **Settings:** **Menu → Options → Radio options****Scan delay**,
**Scan dwell**, **Scan mode** (Hold / Pause / Stop) and **Scan On Boot**.
Note: in the codeplug these two skip flags reuse the old "Auto Scan" and "Lone
Worker" bits, so a vendor CPS may label them differently — set them on the radio
to avoid confusion.
### Quick menu — ⚪ [OpenGD77] (with 🟢 [FreeTRX] additions) ### Quick menu — ⚪ [OpenGD77] (with 🟢 [FreeTRX] additions)
Press the **top / orange** button to open the quick menu for the current screen Press the **top / orange** button to open the quick menu for the current screen
@ -176,11 +159,6 @@ Talkgroups/contacts, timeslots, colour codes, RX groups, and hotspot mode, all p
upstream OpenGD77. The AMBE voice codec is merged in at flash time (see upstream OpenGD77. The AMBE voice codec is merged in at flash time (see
[`TOOLS.md`](TOOLS.md)). [`TOOLS.md`](TOOLS.md)).
### Contacts list — 🟢 [FreeTRX]
**Menu → Contacts** lists all contact types together under the title "Contacts"
(instead of defaulting to the group filter). Press **`#`** to cycle the filter:
all → groups → private → all-call → all.
### DMR reliability fix — 🟢 [FreeTRX] ### DMR reliability fix — 🟢 [FreeTRX]
FreeTRX resolves a hard-fault (freeze needing a battery pull) on the DMR path that FreeTRX resolves a hard-fault (freeze needing a battery pull) on the DMR path that
affected recent self-built OpenGD77 revisions — the codec is called through C affected recent self-built OpenGD77 revisions — the codec is called through C
@ -196,14 +174,12 @@ decoded on receive).
screen. The hub offers *New message*, *Inbox*, *Sent*, *Quick text* and screen. The hub offers *New message*, *Inbox*, *Sent*, *Quick text* and
*Options*. *Options*.
**Composing:** in the recipient field, either type the destination DMR ID with **Composing:** enter the destination DMR ID with the number keys (**UP/DOWN**
the number keys and press **DOWN** to move on to the message, or press **GREEN** switches between the ID and text fields), type up to 160 characters with
to pick a recipient from your private-call DMR contacts (selecting one drops you multi-tap on the keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes),
straight into the message text). Type up to 160 characters with multi-tap on the then press **GREEN** to send. Sending only works on a DMR channel; progress is
keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes), then press shown as notifications ("Sending", "Sent", "Delivered"). With **Wait for ACK**
**GREEN** to send. Sending only works on a DMR channel; progress is shown as enabled (default), a missing delivery report pops up a **resend** prompt.
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 — **Receiving:** a popup with a chime shows the sender and a preview —
**GREEN** opens the message, **SK2+GREEN** starts a reply, **RED** dismisses. **GREEN** opens the message, **SK2+GREEN** starts a reply, **RED** dismisses.
@ -223,10 +199,9 @@ second or two a message is on air; hotspot mode is unaffected.
## About & versioning — 🟢 [FreeTRX] ## About & versioning — 🟢 [FreeTRX]
**Menu → Firmware info** shows the **FreeTRX version** (semantic versioning, **Menu → Firmware info** shows the **FreeTRX version** (semantic versioning, e.g.
`FreeTRX vMAJOR.MINOR.PATCH`) alongside the git commit ID. Pre-1.0 builds are `FreeTRX v0.4.0`) alongside the git commit ID. Pre-1.0 builds are published as
published as pre-releases. See [`CHANGELOG.md`](CHANGELOG.md) for the full pre-releases. See [`CHANGELOG.md`](CHANGELOG.md) for the full history.
history.
--- ---

View file

@ -5,9 +5,9 @@
#define _FREETRX_VERSION_H_ #define _FREETRX_VERSION_H_
#define FREETRX_VERSION_MAJOR 0 #define FREETRX_VERSION_MAJOR 0
#define FREETRX_VERSION_MINOR 11 #define FREETRX_VERSION_MINOR 8
#define FREETRX_VERSION_PATCH 0 #define FREETRX_VERSION_PATCH 0
#define FREETRX_VERSION "0.11.0" #define FREETRX_VERSION "0.8.0"
#endif // _FREETRX_VERSION_H_ #endif // _FREETRX_VERSION_H_

View file

@ -267,9 +267,6 @@ bool uiNotificationHasTimedOut(void);
bool uiNotificationIsVisible(void); bool uiNotificationIsVisible(void);
void uiNotificationHide(bool immediateRender); void uiNotificationHide(bool immediateRender);
uiNotificationID_t uiNotificationGetId(void); uiNotificationID_t uiNotificationGetId(void);
#if defined(HAS_COLOURS)
uint16_t *uiNotificationGetOffscreenBuffer(void); // full-screen scratch (borrowed by the shutdown animation)
#endif
#if defined(HAS_GPS) #if defined(HAS_GPS)
@ -331,7 +328,6 @@ enum MENU_SCREENS
MENU_SMS_VIEW, MENU_SMS_VIEW,
MENU_SMS_QUICKTEXT, MENU_SMS_QUICKTEXT,
MENU_SMS_OPTIONS, MENU_SMS_OPTIONS,
MENU_SMS_CONTACT,
UI_SMS_RX_POPUP, UI_SMS_RX_POPUP,
UI_MESSAGE_BOX, UI_MESSAGE_BOX,
UI_HOTSPOT_MODE, UI_HOTSPOT_MODE,
@ -501,10 +497,8 @@ menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun);
menuStatus_t menuSMSList(uiEvent_t *ev, bool isFirstRun); // serves inbox, sent and view menuStatus_t menuSMSList(uiEvent_t *ev, bool isFirstRun); // serves inbox, sent and view
menuStatus_t menuSMSQuickText(uiEvent_t *ev, bool isFirstRun); menuStatus_t menuSMSQuickText(uiEvent_t *ev, bool isFirstRun);
menuStatus_t menuSMSOptions(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); menuStatus_t uiSMSPopup(uiEvent_t *ev, bool isFirstRun);
void menuSMSComposePrefill(uint32_t dstId, const char *text); // 0 / NULL leave the field untouched 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 menuSMSComposeSetQuickTextEditTarget(int slotIndex); // compose edits a quick text instead of sending
void menuSMSListSetViewTarget(bool sentBox, uint8_t recordIndex); void menuSMSListSetViewTarget(bool sentBox, uint8_t recordIndex);

View file

@ -1621,12 +1621,6 @@ static void themeSetPride(DayTime_t daytime)
{ {
themeSetAllItems(daytime, 0x000000, 0xFFFFFF); // black text, white chrome backgrounds themeSetAllItems(daytime, 0x000000, 0xFFFFFF); // black text, white chrome backgrounds
// Paint the status/header bar and the menu title bar in the top stripe
// colour (prideRGB[0]) so they blend into the rainbow instead of showing
// as white boxes.
themeSetItem(daytime, THEME_ITEM_BG_HEADER_TEXT, 0xE40303);
themeSetItem(daytime, THEME_ITEM_BG_MENU_NAME, 0xE40303);
// Keep warning/error notifications colour-coded on their white box. // Keep warning/error notifications colour-coded on their white box.
themeSetItem(daytime, THEME_ITEM_FG_WARNING_NOTIFICATION, 0xC07800); themeSetItem(daytime, THEME_ITEM_FG_WARNING_NOTIFICATION, 0xC07800);
themeSetItem(daytime, THEME_ITEM_FG_ERROR_NOTIFICATION, 0xC00000); themeSetItem(daytime, THEME_ITEM_FG_ERROR_NOTIFICATION, 0xC00000);

View file

@ -62,58 +62,6 @@ static menuStatus_t menuContactListSubMenuExitCode = MENU_STATUS_SUCCESS;
static const char *calltypeVoices[3] = { NULL, NULL, NULL }; 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). // Apply contact + its TS on selection for TX (contact list of quick list).
static void overrideWithSelectedContact(void) static void overrideWithSelectedContact(void)
{ {
@ -130,7 +78,7 @@ static void overrideWithSelectedContact(void)
static void reloadContactList(contactListContactType_t type) static void reloadContactList(contactListContactType_t type)
{ {
menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? digitalContactsCount() : codeplugDTMFContactsGetCount(); menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? codeplugContactsGetCount(contactCallType) : codeplugDTMFContactsGetCount();
if (menuDataGlobal.numItems > 0) if (menuDataGlobal.numItems > 0)
{ {
@ -139,7 +87,7 @@ static void reloadContactList(contactListContactType_t type)
menuDataGlobal.currentItemIndex = 0; menuDataGlobal.currentItemIndex = 0;
} }
uiDataGlobal.currentSelectedContactIndex = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) uiDataGlobal.currentSelectedContactIndex = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL)
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
} }
else else
@ -171,7 +119,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
// Shows digital contact list if called from "contact list" menu entry, or from <SK2>+# in digital. // Shows digital contact list if called from "contact list" menu entry, or from <SK2>+# in digital.
// Otherwise displays DTMF contact list // 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; 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_LIST_COMBINED; // default: show all contact types together contactCallType = CONTACT_CALLTYPE_TG;
dtmfSequenceReset(); dtmfSequenceReset();
} }
@ -198,7 +146,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
{ {
voicePromptsAppendLanguageString(currentLanguage->dmr_contacts); voicePromptsAppendLanguageString(currentLanguage->dmr_contacts);
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendLanguageString(contactCallTypeLabel()); voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
} }
else else
{ {
@ -210,7 +158,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
{ {
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
{ {
voicePromptsAppendLanguageString(contactCallTypeLabel()); voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
} }
} }
@ -262,9 +210,7 @@ static void updateScreen(bool isFirstRun)
switch (contactListDisplayState) switch (contactListDisplayState)
{ {
case MENU_CONTACT_LIST_DISPLAY: case MENU_CONTACT_LIST_DISPLAY:
menuDisplayTitle((contactListType != MENU_CONTACT_LIST_CONTACT_DIGITAL) menuDisplayTitle((char *) calltypeName[((contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? contactCallType : 3)]);
? (char *) calltypeName[3] // "DTMF"
: ((contactCallType == CONTACT_LIST_COMBINED) ? (char *) currentLanguage->contacts : (char *) calltypeName[contactCallType]));
if (menuDataGlobal.numItems == 0) if (menuDataGlobal.numItems == 0)
{ {
@ -289,7 +235,7 @@ static void updateScreen(bool isFirstRun)
} }
idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
? digitalContactGetForNumber(mNum + 1, &contact) ? codeplugContactGetDataForNumberInType(mNum + 1, contactCallType, &contact)
: codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact); : codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact);
if (idx > 0) if (idx > 0)
@ -377,7 +323,7 @@ static void handleEvent(uiEvent_t *ev)
{ {
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
voicePromptsInit(); voicePromptsInit();
updateScreen(false); updateScreen(false);
@ -387,7 +333,7 @@ static void handleEvent(uiEvent_t *ev)
{ {
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems); menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData) ? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
voicePromptsInit(); voicePromptsInit();
updateScreen(false); updateScreen(false);
@ -397,24 +343,11 @@ static void handleEvent(uiEvent_t *ev)
{ {
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
{ {
// cycle: all combined -> group -> private -> all-call -> combined contactCallType = (contactCallType + 1) % (CONTACT_CALLTYPE_ALL + 1);
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); reloadContactList(contactListType);
voicePromptsInit(); voicePromptsInit();
voicePromptsAppendLanguageString(contactCallTypeLabel()); voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
updateScreen(false); updateScreen(false);

View file

@ -50,11 +50,6 @@ static int quickTextEditSlot = -1;
static bool prefillPending = false; static bool prefillPending = false;
static int pendingQuickTextEditSlot = -1; 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 menuStatus_t menuSMSComposeExitCode = MENU_STATUS_SUCCESS;
static void updateScreen(void); static void updateScreen(void);
@ -79,18 +74,6 @@ void menuSMSComposePrefill(uint32_t dstId, const char *text)
prefillPending = true; 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) void menuSMSComposeSetQuickTextEditTarget(int slotIndex)
{ {
pendingQuickTextEditSlot = slotIndex; pendingQuickTextEditSlot = slotIndex;
@ -118,19 +101,6 @@ menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun)
focusedField = FIELD_TEXT; 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 else
{ {
if (prefillPending == false) if (prefillPending == false)
@ -267,24 +237,7 @@ static void handleEvent(uiEvent_t *ev)
} }
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN)) else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{ {
if (quickTextEditSlot >= 0) sendOrSave();
{
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; return;
} }
else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0)) else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0))

View file

@ -1,161 +0,0 @@
/*
* 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();
}
}

View file

@ -98,7 +98,6 @@ menuDataGlobal_t menuDataGlobal =
NULL,// SMS view NULL,// SMS view
NULL,// SMS quick texts NULL,// SMS quick texts
NULL,// SMS options NULL,// SMS options
NULL,// SMS contact picker
NULL,// SMS RX popup NULL,// SMS RX popup
NULL,// MessageBox NULL,// MessageBox
NULL,// hotspot mode NULL,// hotspot mode
@ -165,7 +164,6 @@ static menuFunctionData_t menuFunctions[] =
{ menuSMSList, NULL, NULL, 0 },// SMS view { menuSMSList, NULL, NULL, 0 },// SMS view
{ menuSMSQuickText, NULL, NULL, 0 }, { menuSMSQuickText, NULL, NULL, 0 },
{ menuSMSOptions, NULL, NULL, 0 }, { menuSMSOptions, NULL, NULL, 0 },
{ menuSMSContact, NULL, NULL, 0 },
{ uiSMSPopup, NULL, NULL, 0 }, { uiSMSPopup, NULL, NULL, 0 },
{ uiMessageBox, NULL, NULL, 0 }, { uiMessageBox, NULL, NULL, 0 },
{ menuHotspotMode, NULL, NULL, 0 }, { menuHotspotMode, NULL, NULL, 0 },

View file

@ -351,15 +351,6 @@ uiNotificationID_t uiNotificationGetId(void)
return notificationData.id; return notificationData.id;
} }
#if defined(HAS_COLOURS)
// Expose the full-screen offscreen buffer so the shutdown animation can borrow
// it as scratch (no notification is active while powering off).
uint16_t *uiNotificationGetOffscreenBuffer(void)
{
return screenNotificationBufData;
}
#endif
static void displayMessage(void) static void displayMessage(void)
{ {
char msg[SCREEN_LINE_BUFFER_SIZE]; char msg[SCREEN_LINE_BUFFER_SIZE];

View file

@ -25,25 +25,18 @@
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
*/ */
#include <string.h>
#include "user_interface/uiGlobals.h" #include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.h" #include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h" #include "user_interface/uiLocalisation.h"
#include "functions/ticks.h"
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) #if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
#include "interfaces/batteryAndPowerManagement.h" #include "interfaces/batteryAndPowerManagement.h"
#include "hardware/radioHardwareInterface.h" #include "hardware/radioHardwareInterface.h"
#endif #endif
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
#if defined(HAS_COLOURS)
static void crtShutdownAnimation(void);
#else
static uint32_t initialEventTime;
static void updateScreen(void); static void updateScreen(void);
static void handleEvent(uiEvent_t *ev); static void handleEvent(uiEvent_t *ev);
#endif static uint32_t initialEventTime;
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun) menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
{ {
@ -56,103 +49,23 @@ menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
} }
#endif #endif
#if defined(HAS_COLOURS)
// Old-CRT-style collapse of whatever is currently on screen; replaces
// the "Power Off" / 73 message, then shuts down.
crtShutdownAnimation();
powerOffFinalStage(false, false);
#else
updateScreen(); updateScreen();
initialEventTime = ev->time; initialEventTime = ev->time;
#endif
} }
else else
{ {
#if !defined(HAS_COLOURS)
handleEvent(ev); handleEvent(ev);
#endif
} }
return MENU_STATUS_SUCCESS; return MENU_STATUS_SUCCESS;
} }
#if defined(HAS_COLOURS)
static void crtShutdownAnimation(void)
{
const int W = DISPLAY_SIZE_X;
const int H = DISPLAY_SIZE_Y;
const int cx = W / 2;
const int cy = H / 2;
const int VSTEPS = 12;
const int HSTEPS = 12;
const uint16_t white = 0xFFFF;
displayRestorePrimaryScreenBuffer();
uint16_t *fb = displayGetPrimaryScreenBuffer();
uint16_t *snap = uiNotificationGetOffscreenBuffer();
// snapshot the screen as it is right now
memcpy(snap, fb, W * H * sizeof(uint16_t));
// Phase 1: squeeze the whole image vertically into a thin central band.
for (int step = 1; step <= VSTEPS; step++)
{
int bandH = H - ((H - 2) * step) / VSTEPS; // 128 -> 2
int top = cy - (bandH / 2);
memset(fb, 0, W * H * sizeof(uint16_t));
for (int dy = 0; dy < bandH; dy++)
{
int srcY = (dy * H) / bandH;
if (srcY >= H)
{
srcY = H - 1;
}
memcpy(&fb[(top + dy) * W], &snap[srcY * W], W * sizeof(uint16_t));
}
displayRender();
vTaskDelay((15 / portTICK_PERIOD_MS));
}
// Phase 2: the band becomes a bright line that collapses horizontally.
for (int step = 1; step <= HSTEPS; step++)
{
int lineW = W - ((W - 2) * step) / HSTEPS; // 160 -> 2
int left = cx - (lineW / 2);
memset(fb, 0, W * H * sizeof(uint16_t));
for (int dx = 0; dx < lineW; dx++)
{
fb[((cy - 1) * W) + (left + dx)] = white;
fb[(cy * W) + (left + dx)] = white;
}
displayRender();
vTaskDelay((15 / portTICK_PERIOD_MS));
}
// Phase 3: a brief central dot, then black.
memset(fb, 0, W * H * sizeof(uint16_t));
fb[((cy - 1) * W) + (cx - 1)] = white;
fb[((cy - 1) * W) + cx] = white;
fb[(cy * W) + (cx - 1)] = white;
fb[(cy * W) + cx] = white;
displayRender();
vTaskDelay((70 / portTICK_PERIOD_MS));
memset(fb, 0, W * H * sizeof(uint16_t));
displayRender();
}
#endif
#if !defined(HAS_COLOURS)
static void updateScreen(void) static void updateScreen(void)
{ {
#if defined(HAS_COLOURS)
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
#else
const bool dblHeight = false; const bool dblHeight = false;
#endif
uint16_t fontHeight = (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1)); uint16_t fontHeight = (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1));
displayClearBuf(); displayClearBuf();
@ -191,4 +104,3 @@ static void handleEvent(uiEvent_t *ev)
} }
} }
#endif

View file

@ -1853,12 +1853,12 @@ void uiUtilityRenderQSOData(void)
displayThemeResetToDefault(); displayThemeResetToDefault();
} }
// Toggle the audio mute (speaker) state. The flashing red LED (see the main // Toggle the audio mute (speaker) state. The header shows a mute icon and the LED
// loop) indicates the muted state. // flashes amber (see the main loop) while muted.
void toggleAudioMute(void) void toggleAudioMute(void)
{ {
audioAmpMute(!audioAmpIsMuted()); audioAmpMute(!audioAmpIsMuted());
headerRowIsDirty = true; headerRowIsDirty = true; // refresh the header so the mute icon appears/clears
} }
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay) void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
@ -1878,6 +1878,8 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
static uint32_t blinkTime = 0; static uint32_t blinkTime = 0;
int radioMode = trxGetMode(); int radioMode = trxGetMode();
uint8_t powerLevel = trxGetPowerLevel(); uint8_t powerLevel = trxGetPowerLevel();
// Muted either explicitly (SK2) or by the volume knob turned fully down (-99).
bool audioIsMuted = (audioAmpIsMuted() || (lastVolume == -99));
bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00); bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00);
bool scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive); bool scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive);
bool batteryIsLow = batteryIsLowWarning(); bool batteryIsLow = batteryIsLowWarning();
@ -1998,7 +2000,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer, displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer,
(((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (uiDataGlobal.dmrDisabled == false)) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted); (((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (uiDataGlobal.dmrDisabled == false)) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
// (mute is indicated by the flashing red LED, not a strike-through) // (mute is shown by the header "M" indicator, not a strike-through)
} }
if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) && if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) &&
@ -2066,7 +2068,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, (isVFODualWatchScanning ? "[DW]" : "DMR"), ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted); displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, (isVFODualWatchScanning ? "[DW]" : "DMR"), ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
// (mute is indicated by the flashing red LED, not a strike-through) // (mute is shown by the header "M" indicator, not a strike-through)
} }
if (isVFODualWatchScanning == false) if (isVFODualWatchScanning == false)
@ -2178,8 +2180,13 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false)); bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
#endif #endif
#endif #endif
// Mute state is indicated by the flashing red LED only; no header icon // Mute indicator: bold "M" just left of the battery indicator.
// (it collided with the GPS text in the status bar). if (audioIsMuted)
{
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
displayPrintCore((DISPLAY_SIZE_X - 40), DISPLAY_Y_POS_HEADER, "M", FONT_SIZE_1_BOLD, TEXT_ALIGN_LEFT, false);
displayThemeResetToDefault();
}
// Display battery percentage/voltage // Display battery percentage/voltage
bool apoEnabled = (nonVolatileSettings.apo > 0); bool apoEnabled = (nonVolatileSettings.apo > 0);

View file

@ -15,15 +15,9 @@ tagged 🟢 **[FreeTRX]** (new/changed here) or ⚪ **[OpenGD77]** (inherited).
## Screenshots ## Screenshots
| Segmented S-meter & DMR call | 7-segment frequency display | Main menu | | Segmented S-meter & DMR call | 7-segment frequency display |
| --- | --- | --- |
| ![DMR channel showing the talkgroup, repeater name and the block S-meter](docs/images/screenshot-dmr.png) | ![VFO with the 7-segment frequency font](docs/images/screenshot-vfo.png) | ![Main menu with the Messages entry highlighted](docs/images/screenshot-menu.png) |
| DMR text messages | Reading a received message |
| --- | --- | | --- | --- |
| ![Messages menu: quick text, options, new message, inbox and sent](docs/images/screenshot-messages.png) | ![A received weather-report message from a BrandMeister service](docs/images/screenshot-message-view.png) | | ![DMR call with the block S-meter](docs/images/screenshot-dmr.png) | ![VFO with the 7-segment frequency font](docs/images/screenshot-vfo.png) |
*Screens shown with the **Neon 80s** theme.*
## What's different from OpenGD77 ## What's different from OpenGD77
@ -31,8 +25,8 @@ FreeTRX keeps OpenGD77's core (DMR/FM, hotspot, CPS compatibility) and adds:
- **Audio mute — new SK2 short-tap.** A short tap of **SK2** on the VFO/Channel - **Audio mute — new SK2 short-tap.** A short tap of **SK2** on the VFO/Channel
screen toggles the speaker mute on and off. Turning the **volume knob fully screen toggles the speaker mute on and off. Turning the **volume knob fully
down** counts as muted too. While muted, the **red LED flashes** (0.5 s every down** counts as muted too. While muted, a bold **"M"** appears in the status
2 s). SK2's bar (left of the battery) and the **red LED flashes** (0.5 s every 2 s). SK2's
existing roles are unchanged: held as a modifier, extra-long-press for existing roles are unchanged: held as a modifier, extra-long-press for
monitor/squelch-off, and — while a DMR QSO is on screen — a tap still sets the monitor/squelch-off, and — while a DMR QSO is on screen — a tap still sets the
TX talkgroup to the incoming one. TX talkgroup to the incoming one.
@ -42,18 +36,15 @@ FreeTRX keeps OpenGD77's core (DMR/FM, hotspot, CPS compatibility) and adds:
- **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 / 15 % after - **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 / 15 % after
inactivity (Auto backlight mode); new setting in Display Options. inactivity (Auto backlight mode); new setting in Display Options.
- **DMR text messages (SMS)** — send and receive text messages on the radio, - **DMR text messages (SMS)** — send and receive text messages on the radio,
compatible with BrandMeister, hotspots and OEM radios. Compose on the keypad compatible with BrandMeister, hotspots and OEM radios. Compose on the keypad,
(or pick the recipient from your contacts), inbox/sent boxes, quick-text inbox/sent boxes, quick-text templates, delivery confirmation and an
templates, delivery confirmation and an incoming-message popup (see the manual incoming-message popup (see the manual for details).
for details).
- **Theme presets** — pick a colour theme on the radio: **Custom** (CPS-editable, - **Theme presets** — pick a colour theme on the radio: **Custom** (CPS-editable,
default), **BASIC 80s** (Commodore 64), **Terminal** (green-on-black), default), **BASIC 80s** (Commodore 64), **Terminal** (green-on-black),
**Vertexic** (amber/orange), **Red**, **Neon 80s** (multi-colour synthwave), **Vertexic** (amber/orange), **Red**, **Neon 80s** (multi-colour synthwave),
or **Pride** (rainbow background). CPS edits only affect Custom. or **Pride** (rainbow background). CPS edits only affect Custom.
- **On-radio channel management** — create and delete channels directly from the - **On-radio channel management** — create and delete channels directly from the
Channel quick menu, no PC/CPS required (see [Managing channels](#managing-channels-on-the-radio)). Channel quick menu, no PC/CPS required (see [Managing channels](#managing-channels-on-the-radio)).
- **CRT-style shut-off animation** — the current screen collapses to a line and
then a dot, like an old CRT switching off (replaces the "Power Off" message).
- **DMR reliability fix** — resolves a hard-fault (freeze needing a battery pull) - **DMR reliability fix** — resolves a hard-fault (freeze needing a battery pull)
on the DMR path that affected recent self-built OpenGD77 revisions. on the DMR path that affected recent self-built OpenGD77 revisions.

View file

@ -34,10 +34,6 @@ Output: `MDUV380_firmware/build/Open<platform>.bin` (the flashable, FM-only imag
codec is merged in at flash time — see below). The build is always clean (`rm -rf build`), codec is merged in at flash time — see below). The build is always clean (`rm -rf build`),
so building one platform overwrites the other's output. so building one platform overwrites the other's output.
Each build finishes with a **memory-usage summary** — used, size and free bytes for the
FLASH (code), RAM and CCMRAM regions — so the remaining headroom for new features is
visible at a glance.
--- ---
## 3. Flashing ## 3. Flashing

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB