Compare commits

...

11 commits
v0.8.0 ... main

Author SHA1 Message Date
Marcus Kida
fa2ad5d0d2 Docs: document zone-based scanning; make version example non-drifting
The Scanning section only said 'per upstream OpenGD77', which left the
scan-list question unanswered. Explain that zones are the scan list, the
Zone Skip / All Skip flags, the start key and the Radio options settings.
Also replace the version example that goes stale every release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:37:14 +02:00
Marcus Kida
51a1c04b9b README: refresh screenshots and show messaging + menu screens
Replace the VFO/DMR screenshots with current captures and add the main
menu, the Messages menu and a received message. All shot with the
Neon 80s theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:34:38 +02:00
Marcus Kida
66aac3587f Release 0.11.0
CRT-style shut-off animation that collapses the current screen when powering
off (colour displays), replacing the 'Power Off' message.

Bump version to 0.11.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:51:39 +02:00
Marcus Kida
bc696ae5b2 Power off: CRT-style collapse animation
Replace the 'Power Off' / 73 message on colour displays with a shut-off
animation that collapses the current screen towards the middle (vertical
squeeze to a line, then horizontal collapse to a centre dot) like an old CRT.
Snapshots the live framebuffer into the (idle) notification offscreen buffer
so no extra RAM is needed; runs blocking with vTaskDelay between frames.
Mono displays keep the existing message. Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:51:24 +02:00
Marcus Kida
91043a006a Release 0.10.1
Remove the muted 'M' status-bar icon that overlapped the GPS text; the
flashing red LED indicates mute.

Bump version to 0.10.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:28:09 +02:00
Marcus Kida
c770693848 Fix: remove muted 'M' header icon that overlapped GPS text
With GPS enabled, the bold 'M' mute indicator in the status bar collided
with the GPS text. Drop the header icon; the flashing red LED already
indicates the muted state. Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:27:43 +02:00
Marcus Kida
d774604d74 Docs: document contact picker, combined Contacts list, build memory summary
- README: SMS bullet mentions picking a recipient from contacts.
- MANUAL: contact picker in the compose flow, a Contacts-list section (all
  types + '#' to cycle the filter), and a current version example.
- TOOLS: note the per-region memory-usage summary printed after each build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:12:42 +02:00
Marcus Kida
6a5731e8a4 Release 0.10.0
SMS recipient picker (GREEN in the compose recipient field selects from your
private-call DMR contacts) and a combined 'Contacts' view in Menu -> Contacts.

Bump version to 0.10.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:08:59 +02:00
Marcus Kida
15bbc1bc3a 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 <noreply@anthropic.com>
2026-07-14 23:08:25 +02:00
Marcus Kida
a94fd59a10 Release 0.9.0
Pride theme: status/header bar and menu title bar now use the top rainbow
stripe colour instead of white.

Bump version to 0.9.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:52:45 +02:00
Marcus Kida
959f7f2a03 Pride theme: paint status and menu title bars in the top stripe colour
Blend the header/status bar and the menu title bar into the rainbow by
filling them with the top pride stripe colour instead of white.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 15:47:26 +02:00
19 changed files with 499 additions and 54 deletions

View file

@ -9,6 +9,34 @@ 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,6 +59,11 @@ 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):
@ -80,8 +85,7 @@ 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, a bold **"M"** appears in the status bar (just left of the battery) While muted, the **red LED flashes** (0.5 s every 2 s). See *Mute* below.
and the **red LED flashes** (0.5 s every 2 s). See *Mute* below.
--- ---
@ -114,8 +118,21 @@ 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]
Channel and VFO scanning with hold/pause behaviour, per the upstream OpenGD77 **There are no scan lists.** Unlike the vendor CPS, OpenGD77 scans **the channels
controls. of the currently selected zone** — so a zone *is* the scan list. To scan a
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
@ -159,6 +176,11 @@ 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
@ -174,12 +196,14 @@ 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:** enter the destination DMR ID with the number keys (**UP/DOWN** **Composing:** in the recipient field, either type the destination DMR ID with
switches between the ID and text fields), type up to 160 characters with the number keys and press **DOWN** to move on to the message, or press **GREEN**
multi-tap on the keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes), to pick a recipient from your private-call DMR contacts (selecting one drops you
then press **GREEN** to send. Sending only works on a DMR channel; progress is straight into the message text). Type up to 160 characters with multi-tap on the
shown as notifications ("Sending", "Sent", "Delivered"). With **Wait for ACK** keypad (**LEFT/RIGHT** move the cursor, **SK2+LEFT** deletes), then press
enabled (default), a missing delivery report pops up a **resend** prompt. **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 — **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.
@ -199,9 +223,10 @@ 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, e.g. **Menu → Firmware info** shows the **FreeTRX version** (semantic versioning,
`FreeTRX v0.4.0`) alongside the git commit ID. Pre-1.0 builds are published as `FreeTRX vMAJOR.MINOR.PATCH`) alongside the git commit ID. Pre-1.0 builds are
pre-releases. See [`CHANGELOG.md`](CHANGELOG.md) for the full history. published as pre-releases. See [`CHANGELOG.md`](CHANGELOG.md) for the full
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 8 #define FREETRX_VERSION_MINOR 11
#define FREETRX_VERSION_PATCH 0 #define FREETRX_VERSION_PATCH 0
#define FREETRX_VERSION "0.8.0" #define FREETRX_VERSION "0.11.0"
#endif // _FREETRX_VERSION_H_ #endif // _FREETRX_VERSION_H_

View file

@ -267,6 +267,9 @@ 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)
@ -328,6 +331,7 @@ 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,
@ -497,8 +501,10 @@ 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,6 +1621,12 @@ 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,6 +62,58 @@ 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)
{ {
@ -78,7 +130,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) ? codeplugContactsGetCount(contactCallType) : codeplugDTMFContactsGetCount(); menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? digitalContactsCount() : codeplugDTMFContactsGetCount();
if (menuDataGlobal.numItems > 0) if (menuDataGlobal.numItems > 0)
{ {
@ -87,7 +139,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)
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
} }
else 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 <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_CALLTYPE_TG; contactCallType = CONTACT_LIST_COMBINED; // default: show all contact types together
dtmfSequenceReset(); dtmfSequenceReset();
} }
@ -146,7 +198,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
{ {
voicePromptsAppendLanguageString(currentLanguage->dmr_contacts); voicePromptsAppendLanguageString(currentLanguage->dmr_contacts);
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); voicePromptsAppendLanguageString(contactCallTypeLabel());
} }
else else
{ {
@ -158,7 +210,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
{ {
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
{ {
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); voicePromptsAppendLanguageString(contactCallTypeLabel());
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
} }
} }
@ -210,7 +262,9 @@ static void updateScreen(bool isFirstRun)
switch (contactListDisplayState) switch (contactListDisplayState)
{ {
case MENU_CONTACT_LIST_DISPLAY: 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) if (menuDataGlobal.numItems == 0)
{ {
@ -235,7 +289,7 @@ static void updateScreen(bool isFirstRun)
} }
idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
? codeplugContactGetDataForNumberInType(mNum + 1, contactCallType, &contact) ? digitalContactGetForNumber(mNum + 1, &contact)
: codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact); : codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact);
if (idx > 0) if (idx > 0)
@ -323,7 +377,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)
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
voicePromptsInit(); voicePromptsInit();
updateScreen(false); updateScreen(false);
@ -333,7 +387,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)
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData) ? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData); : codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
voicePromptsInit(); voicePromptsInit();
updateScreen(false); updateScreen(false);
@ -343,11 +397,24 @@ static void handleEvent(uiEvent_t *ev)
{ {
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) 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); reloadContactList(contactListType);
voicePromptsInit(); voicePromptsInit();
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]); voicePromptsAppendLanguageString(contactCallTypeLabel());
voicePromptsAppendPrompt(PROMPT_SILENCE); voicePromptsAppendPrompt(PROMPT_SILENCE);
updateScreen(false); updateScreen(false);

View file

@ -50,6 +50,11 @@ 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);
@ -74,6 +79,18 @@ 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;
@ -101,6 +118,19 @@ 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)
@ -237,7 +267,24 @@ static void handleEvent(uiEvent_t *ev)
} }
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN)) 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; return;
} }
else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0)) else if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (quickTextEditSlot < 0))

View file

@ -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();
}
}

View file

@ -98,6 +98,7 @@ 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
@ -164,6 +165,7 @@ 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,6 +351,15 @@ 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,18 +25,25 @@
* 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);
static uint32_t initialEventTime; #endif
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun) menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
{ {
@ -49,23 +56,103 @@ 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();
@ -104,3 +191,4 @@ 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 header shows a mute icon and the LED // Toggle the audio mute (speaker) state. The flashing red LED (see the main
// flashes amber (see the main loop) while muted. // loop) indicates the muted state.
void toggleAudioMute(void) void toggleAudioMute(void)
{ {
audioAmpMute(!audioAmpIsMuted()); audioAmpMute(!audioAmpIsMuted());
headerRowIsDirty = true; // refresh the header so the mute icon appears/clears headerRowIsDirty = true;
} }
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay) void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
@ -1878,8 +1878,6 @@ 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();
@ -2000,7 +1998,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 shown by the header "M" indicator, not a strike-through) // (mute is indicated by the flashing red LED, not a strike-through)
} }
if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) && if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) &&
@ -2068,7 +2066,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 shown by the header "M" indicator, not a strike-through) // (mute is indicated by the flashing red LED, not a strike-through)
} }
if (isVFODualWatchScanning == false) if (isVFODualWatchScanning == false)
@ -2180,13 +2178,8 @@ 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 indicator: bold "M" just left of the battery indicator. // Mute state is indicated by the flashing red LED only; no header icon
if (audioIsMuted) // (it collided with the GPS text in the status bar).
{
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,9 +15,15 @@ tagged 🟢 **[FreeTRX]** (new/changed here) or ⚪ **[OpenGD77]** (inherited).
## Screenshots ## Screenshots
| Segmented S-meter & DMR call | 7-segment frequency display | | Segmented S-meter & DMR call | 7-segment frequency display | Main menu |
| --- | --- | --- |
| ![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 |
| --- | --- | | --- | --- |
| ![DMR call with the block S-meter](docs/images/screenshot-dmr.png) | ![VFO with the 7-segment frequency font](docs/images/screenshot-vfo.png) | | ![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) |
*Screens shown with the **Neon 80s** theme.*
## What's different from OpenGD77 ## What's different from OpenGD77
@ -25,8 +31,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, a bold **"M"** appears in the status down** counts as muted too. While muted, the **red LED flashes** (0.5 s every
bar (left of the battery) and the **red LED flashes** (0.5 s every 2 s). SK2's 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.
@ -36,15 +42,18 @@ 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
inbox/sent boxes, quick-text templates, delivery confirmation and an (or pick the recipient from your contacts), inbox/sent boxes, quick-text
incoming-message popup (see the manual for details). templates, delivery confirmation and an incoming-message popup (see the manual
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,6 +34,10 @@ 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.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB