Compare commits

...

16 commits
v0.7.1 ... 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
Marcus Kida
3fe33b931d Release 0.8.0
New colour theme presets (Red, Neon 80s, and the rainbow-background Pride),
plus a per-region memory-usage summary printed after every build.

Bump version to 0.8.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:48:17 +02:00
Marcus Kida
3ec4eafbc9 Themes: add 'Pride' rainbow-background preset
The theme system stores one flat background colour per element, so a rainbow
background can't be expressed as a preset alone. Add a small display hook:
when Pride is selected, displayClearBuf()/displayClearRows() paint the six
pride-flag stripes across the framebuffer instead of a flat fill (gated on a
flag only the Pride preset sets, and behind HAS_COLOURS so mono builds are
untouched). Non-selected menu rows draw text straight over the canvas, while
the title/header/notifications paint solid white boxes, so chrome stays
readable over the stripes; blue/violet are lightened slightly for black-text
contrast. Docs and changelog updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:47:45 +02:00
Marcus Kida
5efd14d8b7 Docs: reflect removed menu icons and new themes/SMS details
- README/MANUAL: drop 'menu icons' (reverted to plain-text entries),
  add the Red and Neon 80s theme presets, and surface DMR text messaging
  in the README feature list.
- MANUAL: correct the quick-text count (10 -> 8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:37:01 +02:00
Marcus Kida
975661689a Build: report per-region memory usage after each build
Pass --print-memory-usage to the linker and re-print its FLASH/RAM/CCMRAM
table in the build-script summary with a computed Free column, so the
headroom for further features is visible at the end of every build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:49:22 +02:00
Marcus Kida
c614ba8096 Themes: add 'Red' and multi-colour 'Neon 80s' presets
Red mirrors the two-tone Terminal/Vertexic presets (black-on-red day,
red-on-black night). Neon 80s is a synthwave palette that assigns distinct
neon colours (cyan, magenta, violet, green, yellow, red, orange) to the
individual theme items over a dark background, demonstrating that presets
are not limited to two colours.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:40:37 +02:00
23 changed files with 682 additions and 60 deletions

View file

@ -9,6 +9,46 @@ single running list until the first versioned release.
_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
- **Three new theme presets** (Display Options → "Theme"): **Red** (black-on-red
day / red-on-black night); **Neon 80s**, an 80s-synthwave palette that maps
several neon colours (cyan, magenta, violet, green, yellow) across the
individual UI elements over a dark background; and **Pride**, which paints the
background as a rainbow pride flag with black text and white menu/header boxes
kept readable over the stripes.
- **Build:** each build now prints a per-region memory-usage summary
(FLASH / RAM / CCMRAM used, size and free) so the remaining headroom is
visible after every build.
## 0.7.1
- **Fix:** the radio **crashed** when navigating the Messages menu with voice

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
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]
Pick a colour theme on the radio at **Menu → Options → Display Options → "Theme"**
(◀/▶ to change; the screen updates live):
@ -67,13 +72,20 @@ Pick a colour theme on the radio at **Menu → Options → Display Options → "
- **BASIC 80s** — a Commodore-64 look (light blue on blue).
- **Terminal** — a green terminal (green-on-black at night, black-on-green by day).
- **Vertexic** — amber/orange (black-on-amber by day, amber-on-black at night).
- **Red** — black-on-red by day, red-on-black at night.
- **Neon 80s** — an 80s-synthwave palette with several neon colours (cyan,
magenta, violet, green, yellow) across the UI over a dark background.
- **Pride** — the background is painted as a rainbow pride flag; body text is
black over the stripes, and the menu title, header and notifications use white
boxes so they stay readable. (Text drawn directly over the brightest stripes
has lower contrast than a solid-background theme — that's inherent to a
rainbow background.)
Editing colours in the CPS only affects **Custom**; the two built-in presets are
Editing colours in the CPS only affects **Custom**; the built-in presets are
fixed. Every element (S-meter, frequencies, menus) follows the selected theme.
### Mute indicator — 🟢 [FreeTRX]
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.
While muted, the **red LED flashes** (0.5 s every 2 s). See *Mute* below.
---
@ -106,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).
### Scanning — ⚪ [OpenGD77]
Channel and VFO scanning with hold/pause behaviour, per the upstream OpenGD77
controls.
**There are no scan lists.** Unlike the vendor CPS, OpenGD77 scans **the channels
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)
Press the **top / orange** button to open the quick menu for the current screen
@ -151,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
[`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]
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
@ -166,12 +196,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.
@ -180,7 +212,7 @@ cycles; unread messages are marked `*`. In a message view, **GREEN**
replies (inbox) or re-edits/resends (sent), **SK2+RED** deletes. The radio
automatically answers delivery-report requests from other radios.
**Quick texts:** 10 editable templates (**GREEN** uses one in a new message,
**Quick texts:** up to 8 editable templates (**GREEN** uses one in a new message,
**SK2+GREEN** edits, **SK2+RED** deletes).
**Options:** *Wait for ACK* (delivery confirmation) and *My ID only*
@ -191,9 +223,10 @@ second or two a message is on air; hotspot mode is unaffected.
## About & versioning — 🟢 [FreeTRX]
**Menu → Firmware info** shows the **FreeTRX version** (semantic versioning, e.g.
`FreeTRX v0.4.0`) alongside the git commit ID. Pre-1.0 builds are published as
pre-releases. See [`CHANGELOG.md`](CHANGELOG.md) for the full history.
**Menu → Firmware info** shows the **FreeTRX version** (semantic versioning,
`FreeTRX vMAJOR.MINOR.PATCH`) alongside the git commit ID. Pre-1.0 builds are
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_MAJOR 0
#define FREETRX_VERSION_MINOR 7
#define FREETRX_VERSION_PATCH 1
#define FREETRX_VERSION_MINOR 11
#define FREETRX_VERSION_PATCH 0
#define FREETRX_VERSION "0.7.1"
#define FREETRX_VERSION "0.11.0"
#endif // _FREETRX_VERSION_H_

View file

@ -277,6 +277,9 @@ typedef enum
THEME_PRESET_BASIC80S, // Commodore 64 BASIC (light blue on blue)
THEME_PRESET_TERMINAL, // green terminal (green-on-black night / black-on-green day)
THEME_PRESET_VERTEXIC, // amber/orange (black-on-amber day / amber-on-black night)
THEME_PRESET_RED, // red (black-on-red day / red-on-black night)
THEME_PRESET_NEON80S, // 80s synthwave: cyan/magenta/violet/green neon on dark
THEME_PRESET_PRIDE, // pride flag: black text + white chrome over a rainbow background
NUM_THEME_PRESETS
} themePreset_t;

View file

@ -267,6 +267,9 @@ bool uiNotificationHasTimedOut(void);
bool uiNotificationIsVisible(void);
void uiNotificationHide(bool immediateRender);
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)
@ -328,6 +331,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 +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 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);

View file

@ -67,6 +67,21 @@ static uint16_t backgroundColour = 0xFFFFU;
static uint16_t screenBufData[DISPLAY_SIZE_X * DISPLAY_SIZE_Y];
uint16_t *screenBuf = screenBufData;
#if defined(HAS_COLOURS)
// When the Pride theme is active the background canvas is painted as six
// horizontal rainbow stripes instead of a single flat colour.
static bool prideBackgroundActive = false;
static uint16_t prideStripes[6];
// Pride stripe colour for a linear screen-buffer index (row = index / width).
static inline uint16_t pridePixelColour(int index)
{
int band = ((index / DISPLAY_SIZE_X) * 6) / DISPLAY_SIZE_Y;
return prideStripes[(band < 0) ? 0 : ((band > 5) ? 5 : band)];
}
#endif
//#define DISPLAY_CHECK_BOUNDS
#ifdef DISPLAY_CHECK_BOUNDS
@ -326,6 +341,16 @@ int displayPrintCoreDoubleHeight(int16_t xPos, int16_t yPos, const char *szMsg,
void displayClearBuf(void)
{
#if defined(HAS_COLOURS)
if (prideBackgroundActive)
{
for (int i = 0; i < DISPLAY_SIZE_X * DISPLAY_SIZE_Y; i++)
{
screenBuf[i] = pridePixelColour(i);
}
return;
}
#endif
// may be able to do this using DMA
for(int i = 0; i < DISPLAY_SIZE_X * DISPLAY_SIZE_Y; i++)
{
@ -353,7 +378,11 @@ void displayClearRows(int16_t startRow, int16_t endRow, bool isInverted)
for(int i = startRow; i < endRow; i++)
{
#if defined(HAS_COLOURS)
screenBuf[i] = ((prideBackgroundActive && (isInverted == false)) ? pridePixelColour(i) : fillColour);
#else
screenBuf[i] = fillColour;
#endif
}
}
@ -1530,12 +1559,87 @@ static void themeSetAllItems(DayTime_t daytime, uint32_t fgRGB, uint32_t bgRGB)
// THEME_ITEM_BG_MENU_ITEM_SELECTED intentionally left = fg (inverts the selected row).
}
static void themeSetItem(DayTime_t daytime, themeItem_t item, uint32_t rgb)
{
themeItems[daytime][item] = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(rgb));
}
// 80s synthwave: a dark background with several neon accents mapped onto the
// individual UI elements. Both day and night use the same neon palette so the
// look is preserved; only the background darkens for night.
static void themeSetNeon80s(DayTime_t daytime, uint32_t bg)
{
const uint32_t cyan = 0x00F0FF;
const uint32_t magenta = 0xFF2CA5;
const uint32_t green = 0x39FF14;
const uint32_t violet = 0xB16CFF;
const uint32_t yellow = 0xFFE600;
const uint32_t red = 0xFF3131;
const uint32_t orange = 0xFF7A18;
// Base: cyan text on the dark background, everything else overridden below.
themeSetAllItems(daytime, cyan, bg);
themeSetItem(daytime, THEME_ITEM_FG_DECORATION, magenta); // borders / separators / drop shadow
themeSetItem(daytime, THEME_ITEM_FG_TEXT_INPUT, green);
themeSetItem(daytime, THEME_ITEM_FG_SPLASHSCREEN, magenta);
themeSetItem(daytime, THEME_ITEM_FG_NOTIFICATION, cyan);
themeSetItem(daytime, THEME_ITEM_FG_WARNING_NOTIFICATION, yellow);
themeSetItem(daytime, THEME_ITEM_FG_ERROR_NOTIFICATION, red);
themeSetItem(daytime, THEME_ITEM_FG_MENU_NAME, magenta); // menu header
themeSetItem(daytime, THEME_ITEM_BG_MENU_ITEM_SELECTED, magenta); // selected row = magenta bar
themeSetItem(daytime, THEME_ITEM_FG_OPTIONS_VALUE, green);
themeSetItem(daytime, THEME_ITEM_FG_HEADER_TEXT, violet); // mode/PWR/battery header
themeSetItem(daytime, THEME_ITEM_FG_RSSI_BAR, cyan);
themeSetItem(daytime, THEME_ITEM_FG_RSSI_BAR_S9P, magenta); // over S9 turns hot pink
themeSetItem(daytime, THEME_ITEM_FG_CHANNEL_NAME, yellow);
themeSetItem(daytime, THEME_ITEM_FG_CHANNEL_CONTACT, magenta);
themeSetItem(daytime, THEME_ITEM_FG_CHANNEL_CONTACT_INFO, cyan);
themeSetItem(daytime, THEME_ITEM_FG_ZONE_NAME, violet);
themeSetItem(daytime, THEME_ITEM_FG_RX_FREQ, cyan);
themeSetItem(daytime, THEME_ITEM_FG_TX_FREQ, red);
themeSetItem(daytime, THEME_ITEM_FG_CSS_SQL_VALUES, green);
themeSetItem(daytime, THEME_ITEM_FG_TX_COUNTER, yellow);
themeSetItem(daytime, THEME_ITEM_FG_POLAR_DRAWING, violet);
themeSetItem(daytime, THEME_ITEM_FG_SATELLITE_COLOUR, magenta);
themeSetItem(daytime, THEME_ITEM_FG_GPS_NUMBER, cyan);
themeSetItem(daytime, THEME_ITEM_FG_GPS_COLOUR, green);
themeSetItem(daytime, THEME_ITEM_FG_BD_COLOUR, orange);
}
// Pride: the background canvas is painted as a rainbow (see pridePixelColour);
// the theme items only need black text plus white "chrome" boxes (menu title,
// header, notifications) so those stay readable over any stripe. The selected
// menu row already inverts to a black bar with white text.
static void themeSetPride(DayTime_t daytime)
{
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.
themeSetItem(daytime, THEME_ITEM_FG_WARNING_NOTIFICATION, 0xC07800);
themeSetItem(daytime, THEME_ITEM_FG_ERROR_NOTIFICATION, 0xC00000);
}
void themeApplyPreset(uint8_t preset)
{
// Start from the monochrome defaults, then overlay the chosen preset.
themeInitToDefaultValues(NIGHT, true);
themeInitToDefaultValues(DAY, false);
prideBackgroundActive = false; // only the Pride preset re-enables the rainbow canvas
switch (preset)
{
case THEME_PRESET_BASIC80S: // Commodore 64 BASIC: light blue on blue (day & night)
@ -1553,6 +1657,32 @@ void themeApplyPreset(uint8_t preset)
themeSetAllItems(NIGHT, 0xFF6A00, 0x000000); // orange on black
break;
case THEME_PRESET_RED:
themeSetAllItems(DAY, 0x000000, 0xFF0000); // black on red
themeSetAllItems(NIGHT, 0xFF0000, 0x000000); // red on black
break;
case THEME_PRESET_NEON80S: // 80s synthwave neon on dark
themeSetNeon80s(DAY, 0x1B0B2E); // dark indigo
themeSetNeon80s(NIGHT, 0x08040F); // near-black purple
break;
case THEME_PRESET_PRIDE: // rainbow background canvas
{
// Classic 6-stripe pride flag; blue/violet lightened a little so
// black text stays legible over the lower stripes.
static const uint32_t prideRGB[6] = { 0xE40303, 0xFF8C00, 0xFFED00, 0x22A93F, 0x3D7DF0, 0xB05CD6 };
for (int s = 0; s < 6; s++)
{
prideStripes[s] = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(prideRGB[s]));
}
prideBackgroundActive = true;
themeSetPride(DAY);
themeSetPride(NIGHT);
}
break;
case THEME_PRESET_CUSTOM:
default:
// Overlay the user's Custom theme from the codeplug, if present (CPS edits

View file

@ -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 <SK2>+# 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);

View file

@ -276,7 +276,7 @@ static void updateScreen(bool isFirstRun)
case DISPLAY_THEME_PRESET:
{
// Preset names are plain literals -> rightSideVar (voice-safe).
const char *presetNames[] = { "Custom", "BASIC 80s", "Terminal", "Vertexic" };
const char *presetNames[] = { "Custom", "BASIC 80s", "Terminal", "Vertexic", "Red", "Neon 80s", "Pride" };
uint8_t p = themeGetPreset();
leftSide = "Theme";
snprintf(rightSideVar, SCREEN_LINE_BUFFER_SIZE, "%s", presetNames[(p < NUM_THEME_PRESETS) ? p : 0]);

View file

@ -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))

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 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 },

View file

@ -351,6 +351,15 @@ uiNotificationID_t uiNotificationGetId(void)
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)
{
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.
*
*/
#include <string.h>
#include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.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)
#include "interfaces/batteryAndPowerManagement.h"
#include "hardware/radioHardwareInterface.h"
#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 handleEvent(uiEvent_t *ev);
static uint32_t initialEventTime;
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
#endif
menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
{
@ -49,23 +56,103 @@ menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
}
#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();
initialEventTime = ev->time;
#endif
}
else
{
#if !defined(HAS_COLOURS)
handleEvent(ev);
#endif
}
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)
{
#if defined(HAS_COLOURS)
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
#else
const bool dblHeight = false;
#endif
uint16_t fontHeight = (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1));
displayClearBuf();
@ -104,3 +191,4 @@ static void handleEvent(uiEvent_t *ev)
}
}
#endif

View file

@ -1853,12 +1853,12 @@ void uiUtilityRenderQSOData(void)
displayThemeResetToDefault();
}
// Toggle the audio mute (speaker) state. The header shows a mute icon and the LED
// flashes amber (see the main loop) while muted.
// Toggle the audio mute (speaker) state. The flashing red LED (see the main
// loop) indicates the muted state.
void toggleAudioMute(void)
{
audioAmpMute(!audioAmpIsMuted());
headerRowIsDirty = true; // refresh the header so the mute icon appears/clears
headerRowIsDirty = true;
}
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
@ -1878,8 +1878,6 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
static uint32_t blinkTime = 0;
int radioMode = trxGetMode();
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 scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive);
bool batteryIsLow = batteryIsLowWarning();
@ -2000,7 +1998,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
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);
// (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) &&
@ -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);
// (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)
@ -2180,13 +2178,8 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
#endif
#endif
// Mute indicator: bold "M" just left of the battery indicator.
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();
}
// Mute state is indicated by the flashing red LED only; no header icon
// (it collided with the GPS text in the status bar).
// Display battery percentage/voltage
bool apoEnabled = (nonVolatileSettings.apo > 0);

View file

@ -6,8 +6,8 @@ chipset — the **TYT MD-UV380 / Retevis RT-3S** and **Baofeng DM-1701 / Retevis
RT-84**.
It adds a number of UI and quality-of-life changes on top of OpenGD77 (segmented
S-meter, 7-segment frequency font, screen dimming, an audio-mute function, menu
icons, on-radio channel editing, and more).
S-meter, 7-segment frequency font, screen dimming, an audio-mute function,
DMR text messaging, colour theme presets, on-radio channel editing, and more).
📖 **[`MANUAL.md`](MANUAL.md)** is the full feature & usage guide — every feature is
tagged 🟢 **[FreeTRX]** (new/changed here) or ⚪ **[OpenGD77]** (inherited). See
@ -15,9 +15,15 @@ tagged 🟢 **[FreeTRX]** (new/changed here) or ⚪ **[OpenGD77]** (inherited).
## 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
@ -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
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
bar (left of the battery) and the **red LED flashes** (0.5 s every 2 s). SK2's
down** counts as muted too. While muted, the **red LED flashes** (0.5 s every
2 s). SK2's
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
TX talkgroup to the incoming one.
@ -35,11 +41,19 @@ FreeTRX keeps OpenGD77's core (DMR/FM, hotspot, CPS compatibility) and adds:
- **7-segment "LED" frequency font** on the VFO and the SK1-held frequency view.
- **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 / 15 % after
inactivity (Auto backlight mode); new setting in Display Options.
- **DMR text messages (SMS)** — send and receive text messages on the radio,
compatible with BrandMeister, hotspots and OEM radios. Compose on the keypad
(or pick the recipient from your contacts), inbox/sent boxes, quick-text
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,
default), **BASIC 80s** (Commodore 64), **Terminal** (green-on-black), or
**Vertexic** (amber/orange). CPS edits only affect Custom.
default), **BASIC 80s** (Commodore 64), **Terminal** (green-on-black),
**Vertexic** (amber/orange), **Red**, **Neon 80s** (multi-colour synthwave),
or **Pride** (rainbow background). CPS edits only affect Custom.
- **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)).
- **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)
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`),
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

View file

@ -22,14 +22,38 @@ echo "==> Building toolchain image ($IMAGE)"
docker build --platform linux/amd64 -t "$IMAGE" "$REPO_DIR/docker"
echo "==> Running build in container ($PLATFORM)"
BUILD_LOG="$(mktemp)"
trap 'rm -f "$BUILD_LOG"' EXIT
# Stream to the terminal and capture a copy so we can re-show the linker's
# memory-usage table at the end (pipefail makes a build failure still abort).
docker run --rm --platform linux/amd64 \
-v "$REPO_DIR":/work \
"$IMAGE" \
bash /work/docker/build.sh "$PLATFORM"
bash /work/docker/build.sh "$PLATFORM" 2>&1 | tee "$BUILD_LOG"
echo
echo "==> Firmware ready:"
echo " $REPO_DIR/MDUV380_firmware/build/${OUT}.bin"
# Re-print the linker's per-region memory usage (emitted by --print-memory-usage
# during the link) with the free bytes worked out for each region.
if grep -q "Memory region" "$BUILD_LOG"; then
echo
echo "==> Memory usage (FLASH = code budget, RAM/CCMRAM = data budget):"
grep -A3 "Memory region" "$BUILD_LOG" | awk '
function toBytes(n, unit) {
if (unit == "KB") return n * 1024
if (unit == "MB") return n * 1024 * 1024
if (unit == "GB") return n * 1024 * 1024 * 1024
return n # already bytes
}
NR == 1 { printf " %-9s %11s %12s %8s %11s\n", "Region", "Used", "Size", "Used%", "Free"; next }
/:/ {
gsub(":", "", $1)
used = toBytes($2, $3); size = toBytes($4, $5); free = size - used
printf " %-9s %9d B %10d B %7s %9d B\n", $1, used, size, $6, free
}'
fi
echo
echo "Flash it (with the radio in firmware-update mode) using:"
echo " ./MDUV380_firmware/tools/opengd77_stm32_firmware_loader.py \\"

View file

@ -63,6 +63,7 @@ ASFLAGS := $(CPU) -x assembler-with-cpp -DDEBUG -g3 -MMD -MP
LDSCRIPT := ../STM32F405VGTX_FLASH.ld
LDFLAGS := $(CPU) -T$(LDSCRIPT) --specs=nano.specs \
-Wl,-Map=$(TARGET).map,--cref -Wl,--gc-sections -static \
-Wl,--print-memory-usage \
-Wl,--start-group -lc -lm -Wl,--end-group
# ---- Sources --------------------------------------------------------------

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