Compare commits
38 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa2ad5d0d2 | ||
|
|
51a1c04b9b | ||
|
|
66aac3587f | ||
|
|
bc696ae5b2 | ||
|
|
91043a006a | ||
|
|
c770693848 | ||
|
|
d774604d74 | ||
|
|
6a5731e8a4 | ||
|
|
15bbc1bc3a | ||
|
|
a94fd59a10 | ||
|
|
959f7f2a03 | ||
|
|
3fe33b931d | ||
|
|
3ec4eafbc9 | ||
|
|
5efd14d8b7 | ||
|
|
975661689a | ||
|
|
c614ba8096 | ||
|
|
5646d2df11 | ||
|
|
9eceb10013 | ||
|
|
d07d3c4b9c | ||
|
|
741578e294 | ||
|
|
31c31df861 | ||
|
|
58e8269500 | ||
|
|
8dcda476aa | ||
|
|
ff1559ec5e | ||
|
|
23fd4df110 | ||
|
|
8c9828e386 | ||
|
|
5ac77c5ae9 | ||
|
|
63a833269a | ||
|
|
682a729e5c | ||
|
|
e20eabd203 | ||
|
|
8fcfb66893 | ||
|
|
35ba6e9975 | ||
|
|
5ae4fa70bd | ||
|
|
d5ddc4748b | ||
|
|
73d40374e7 | ||
|
|
a8c2038552 | ||
|
|
e145a1188d | ||
|
|
7c10a92475 |
7
.gitignore
vendored
|
|
@ -5,3 +5,10 @@ codec_bin_section_1.bin
|
|||
# Generated language plugin files + host-side builder binary
|
||||
*.gla
|
||||
languages_builder
|
||||
|
||||
# Generated docs site
|
||||
website/_build/
|
||||
|
||||
# Screenshot tool venv + scratch output
|
||||
MDUV380_firmware/tools/.venv/
|
||||
screenshots/
|
||||
|
|
|
|||
114
CHANGELOG.md
|
|
@ -9,6 +9,120 @@ 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
|
||||
prompts enabled. Menu strings added after the voice-prompt pack was built map
|
||||
to a table-of-contents slot that doesn't exist; the player read past the end
|
||||
of the table. Such strings now play silence instead (also hardens the
|
||||
pre-existing strings sitting at the end of the table).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- **DMR text messages (SMS)** — full ETSI/Motorola-TMS-compatible messaging:
|
||||
compose on the keypad (multi-tap, up to 160 characters), inbox and sent box
|
||||
(8 messages each, persisted to flash across power cycles), 10 editable
|
||||
quick-text templates, an incoming-message popup with chime and one-key
|
||||
reply, and delivery confirmation (waits for the ETSI response PDU / ACK
|
||||
with a resend prompt on timeout). Transmits in the Motorola TMS format
|
||||
(UDP port 4007) understood by BrandMeister, hotspots and most OEM radios;
|
||||
receives both TMS and the Anytone/"DMR standard" format (port 5016) and
|
||||
answers delivery-report requests from other radios. Reachable from the
|
||||
main menu ("Messages") or a **long-press on GREEN** in channel/VFO mode.
|
||||
Options: "Wait for ACK" and "My ID only" (accept only messages addressed
|
||||
to your own DMR ID). Voice PTT is locked out while a message transmits,
|
||||
and hotspot mode is unaffected.
|
||||
- **Removed the main-menu icons** — the top-level menu entries are plain text again.
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- **Theme presets** — selectable on the radio in Display Options → "Theme":
|
||||
**Custom** (CPS-editable, default), **BASIC 80s** (Commodore 64 blue/light-blue),
|
||||
**Terminal** (green-on-black night / black-on-green day), and **Vertexic**
|
||||
(orange, black-on-orange day / orange-on-black night). The choice is stored
|
||||
in the codeplug custom-data area (migration-safe; defaults to Custom), and CPS
|
||||
theme edits only ever change Custom.
|
||||
- **Fix:** green LED **flickering** between the red flashes while muted during a DMR
|
||||
QSO. The DMR RX handler re-asserts the green LED as soon as it reads it off, racing
|
||||
the main-loop mute clear; the green LED is now **held off at the driver** for the
|
||||
whole mute (and released on un-mute), so only the red flash shows.
|
||||
- **`screenshot.sh`** — a one-command wrapper around the OpenGD77 screen grabber
|
||||
that auto-detects the serial port and sets up a self-contained Python venv
|
||||
(pyserial + pillow) on first run. Saves to `screenshots/`.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- **Fix:** un-muting during an active QSO left the LED **off** instead of returning
|
||||
to **green**. On un-mute the green RX LED is now restored from the current receive
|
||||
state (active DMR slot or FM/digital signal), so an in-progress call lights green
|
||||
again.
|
||||
- **`MANUAL.md`** — a user-facing feature & usage guide covering both inherited
|
||||
OpenGD77 features and FreeTRX additions, each tagged 🟢 [FreeTRX] or ⚪
|
||||
[OpenGD77]. Includes the spectrum scope (VFO sweep, hold `#`), channel add/delete,
|
||||
mute, dimming, and a button reference.
|
||||
- **Documentation website** (`website/`) — MkDocs Material config that renders the
|
||||
markdown into a static, searchable HTML site (client-side search, dark/light
|
||||
theme). Built in Docker via `./website/build-docs.sh`; the public URL is set with
|
||||
`SITE_URL=…`. For self-hosting on Forgejo.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- **On-radio channel add/delete.** The Channel quick menu (top button) has two new
|
||||
entries:
|
||||
- **new channel** — creates a blank 145.500 MHz FM channel in the first free
|
||||
slot, adds it to the current zone, and opens Channel Details to edit it. The
|
||||
zone is reloaded so the channel is immediately navigable.
|
||||
- **Delete Channel** — deletes the selected channel after a confirmation, and
|
||||
scrubs it out of every zone that referenced it. Refuses to delete the last
|
||||
remaining channel, and respects the read-only/APRS channel lock.
|
||||
- The new menu strings are **translated** (German, French, Italian, Spanish,
|
||||
Portuguese, Dutch, Danish, Swedish, Catalan, Finnish, Czech; English fallback
|
||||
for the rest).
|
||||
- **`flash.sh`** now always rebuilds the language (`.gla`) packs, so edits to the
|
||||
language files are picked up instead of a stale pack being re-flashed.
|
||||
- **Release script** now flags every release before v1.0.0 as a **pre-release**
|
||||
(and corrects the flag on re-runs).
|
||||
|
||||
## 0.3.0
|
||||
|
||||
- **Fix a second voice-prompt crash** on the Screen Dim menu row: the item *label*
|
||||
|
|
|
|||
234
MANUAL.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# FreeTRX — User Manual
|
||||
|
||||
Feature and usage guide for **FreeTRX** (a fork of [OpenGD77](https://opengd77.org))
|
||||
on the **TYT MD-UV380 / Retevis RT-3S** and **Baofeng DM-1701 / Retevis RT-84**.
|
||||
|
||||
## How to read this manual
|
||||
|
||||
Each feature is tagged so you can tell what is unique to FreeTRX and what is
|
||||
inherited from upstream OpenGD77:
|
||||
|
||||
- 🟢 **[FreeTRX]** — new or noticeably changed in FreeTRX.
|
||||
- ⚪ **[OpenGD77]** — inherited from upstream OpenGD77, documented here for
|
||||
convenience. For exhaustive coverage of upstream behaviour, see the official
|
||||
OpenGD77 user guide.
|
||||
|
||||
> Most day-to-day operation (VFO/Channel, FM, DMR, scanning, hotspot, CPS sync) is
|
||||
> standard OpenGD77. This manual focuses on **how to reach each feature** and marks
|
||||
> clearly what FreeTRX adds or changes.
|
||||
|
||||
---
|
||||
|
||||
## Buttons at a glance
|
||||
|
||||
The MD-UV380 / RT-3S has **PTT**, **SK1** (upper side key), **SK2** (lower side
|
||||
key), the **top / orange** button, and a keypad with **green** (✓ / menu), **red**
|
||||
(✗ / back), arrows, `0`–`9`, `*` and `#`.
|
||||
|
||||
| Control | Action |
|
||||
| --- | --- |
|
||||
| **Green** | Menu / confirm |
|
||||
| **Red** | Back / exit |
|
||||
| **Top / orange** button | Open the **quick menu** (VFO or Channel) — ⚪ [OpenGD77] |
|
||||
| **SK1** (hold) | Show frequency / secondary info |
|
||||
| **SK2** (hold) | Modifier for other keys |
|
||||
| **SK2** (short tap) | **Toggle mute** on VFO/Channel — 🟢 [FreeTRX] |
|
||||
| **SK2** (extra-long) | Monitor / open squelch — ⚪ [OpenGD77] |
|
||||
| **SK1 + SK2** | Reverse repeater — ⚪ [OpenGD77] |
|
||||
| **`#`** (hold, VFO) | Toggle the **spectrum scope** — ⚪ [OpenGD77] |
|
||||
| **SK2 + green** (Channel) | Open **Channel Details** editor — ⚪ [OpenGD77] |
|
||||
| **Green** (hold) | Open **Messages** (DMR SMS) — 🟢 [FreeTRX] |
|
||||
|
||||
---
|
||||
|
||||
## Display & UI
|
||||
|
||||
### Segmented S-meter — 🟢 [FreeTRX]
|
||||
The signal meter under the top status bar is drawn as discrete blocks: **9 green**
|
||||
blocks for S1–S9 (4 dB each) and **6 red** blocks for S9+10…+60 (10 dB each), with
|
||||
a scale-label row beneath (`S1 S3 S5 S7 S9 / +20 +40 +60`). The leading block fills
|
||||
proportionally.
|
||||
|
||||
### 7-segment "LED" frequency font — 🟢 [FreeTRX]
|
||||
On the VFO and the SK1-held frequency view, the frequency is rendered in a
|
||||
7-segment "LED" style (lit segments only). "MHz" matches the frequency colour
|
||||
(green on RX, red on TX).
|
||||
|
||||
### Screen dimming — 🟢 [FreeTRX]
|
||||
Optionally dims the backlight after inactivity to **100 / 50 / 30 / 15 %** of the
|
||||
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):
|
||||
|
||||
- **Custom** — the theme edited via the CPS or the on-radio colour editor (default).
|
||||
- **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 built-in presets are
|
||||
fixed. Every element (S-meter, frequencies, menus) follows the selected theme.
|
||||
|
||||
### Mute indicator — 🟢 [FreeTRX]
|
||||
While muted, the **red LED flashes** (0.5 s every 2 s). See *Mute* below.
|
||||
|
||||
---
|
||||
|
||||
## Audio
|
||||
|
||||
### One-tap mute — 🟢 [FreeTRX]
|
||||
- A **short tap of SK2** on the VFO/Channel screen toggles the speaker mute on/off.
|
||||
(Exception: while a DMR QSO is on screen, SK2 keeps its "set TX talkgroup to the
|
||||
incoming one" role.)
|
||||
- Turning the **volume knob fully down** also counts as muted.
|
||||
- Mute is shown by the **"M"** indicator and the flashing red LED (above).
|
||||
- Mute is also available in the Channel/VFO **quick menu**.
|
||||
|
||||
---
|
||||
|
||||
## Operating
|
||||
|
||||
### VFO & Channel modes — ⚪ [OpenGD77]
|
||||
Switch between direct-frequency **VFO** mode and stored **Channel** mode with the
|
||||
mode toggle. Most operating happens here.
|
||||
|
||||
### Spectrum scope (VFO sweep) — ⚪ [OpenGD77]
|
||||
A real-time signal-strength-vs-frequency graph centred on the VFO frequency.
|
||||
- **Enter/exit:** in **VFO mode**, **hold `#`** (long press). Hold `#` again to leave.
|
||||
- **Left / Right:** move the centre frequency across the band.
|
||||
- **Up / Down:** change the sweep **span / step** (zoom in / out).
|
||||
- **SK2 + Up/Down:** adjust **gain**; **SK2 + Left/Right:** adjust the
|
||||
**noise-floor** reference (how the trace fills the screen).
|
||||
- The gain / noise-floor / step settings are remembered.
|
||||
- The scope is **VFO-only** (not available in Channel mode).
|
||||
|
||||
### Scanning — ⚪ [OpenGD77]
|
||||
**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
|
||||
(VFO or Channel). It holds filters (FM/DMR), Talkaround, Roaming, Mute, VFO↔Channel
|
||||
copy, and — on the Channel screen — the FreeTRX **new channel** / **Delete Channel**
|
||||
items (see below).
|
||||
|
||||
---
|
||||
|
||||
## Channels & zones
|
||||
|
||||
### Channel Details editor — ⚪ [OpenGD77]
|
||||
Edit an existing channel's parameters (name, frequencies, tones, power, bandwidth,
|
||||
timeslot, colour code, groups, …). From the Channel screen, press **SK2 + green**.
|
||||
Press **green** to save.
|
||||
|
||||
### Create & delete channels on the radio — 🟢 [FreeTRX]
|
||||
No PC/CPS required. Open the **Channel quick menu** (top/orange button) and scroll
|
||||
to the bottom:
|
||||
- **New channel** — creates a blank **145.500 MHz FM** channel in the first free
|
||||
slot, adds it to the current zone, and opens **Channel Details** to edit it. The
|
||||
new channel is immediately selectable in the zone.
|
||||
- **Delete Channel** — deletes the selected channel after a confirmation (**green**
|
||||
confirms, **red** cancels). It is also removed from every zone that referenced it.
|
||||
The last remaining channel can't be deleted, and read-only/APRS channels are
|
||||
protected.
|
||||
|
||||
> **Back up your codeplug** (via the CPS) before bulk edits — changes are written
|
||||
> straight to the radio and there is no on-device undo.
|
||||
|
||||
### Zones — ⚪ [OpenGD77]
|
||||
Select zones from **Menu → Zone**. The virtual **All Channels** zone lists every
|
||||
channel. (On-radio *zone* editing is planned for a future FreeTRX release.)
|
||||
|
||||
---
|
||||
|
||||
## DMR
|
||||
|
||||
### DMR operation — ⚪ [OpenGD77]
|
||||
Talkgroups/contacts, timeslots, colour codes, RX groups, and hotspot mode, all per
|
||||
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
|
||||
function pointers instead of inline assembler that modern GCC miscompiled.
|
||||
|
||||
### Text messages (SMS) — 🟢 [FreeTRX]
|
||||
|
||||
Send and receive DMR text messages, compatible with BrandMeister, hotspots and
|
||||
OEM radios (Motorola TMS format on transmit; TMS **and** Anytone/"DMR standard"
|
||||
decoded on receive).
|
||||
|
||||
**Open it** from **Menu → Messages**, or **hold GREEN** on the channel/VFO
|
||||
screen. The hub offers *New message*, *Inbox*, *Sent*, *Quick text* and
|
||||
*Options*.
|
||||
|
||||
**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.
|
||||
The inbox and sent box keep the last 8 messages each and survive power
|
||||
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:** 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*
|
||||
(ignore messages not addressed to your DMR ID). PTT is locked out for the
|
||||
second or two a message is on air; hotspot mode is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## About & versioning — 🟢 [FreeTRX]
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
*Building, flashing and tools are covered in [`TOOLS.md`](TOOLS.md); the project
|
||||
overview is in [`README.md`](README.md).*
|
||||
|
|
@ -5,9 +5,9 @@
|
|||
#define _FREETRX_VERSION_H_
|
||||
|
||||
#define FREETRX_VERSION_MAJOR 0
|
||||
#define FREETRX_VERSION_MINOR 3
|
||||
#define FREETRX_VERSION_MINOR 11
|
||||
#define FREETRX_VERSION_PATCH 0
|
||||
|
||||
#define FREETRX_VERSION "0.3.0"
|
||||
#define FREETRX_VERSION "0.11.0"
|
||||
|
||||
#endif // _FREETRX_VERSION_H_
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ typedef enum
|
|||
CODEPLUG_CUSTOM_DATA_TYPE_SATELLITE_TLE,
|
||||
CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY,
|
||||
CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT,
|
||||
CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, // FreeTRX: 1 byte, selected theme preset (themePreset_t)
|
||||
} CodeplugCustomDataType_t;
|
||||
|
||||
|
||||
|
|
@ -415,6 +416,7 @@ void codeplugGetVFO_ChannelData(CodeplugChannel_t *vfoBuf, Channel_t VFONumber);
|
|||
void codeplugSetVFO_ChannelData(CodeplugChannel_t *vfoBuf, Channel_t VFONumber);
|
||||
bool codeplugAllChannelsIndexIsInUse(int index);
|
||||
void codeplugAllChannelsIndexSetUsed(int index);
|
||||
void codeplugAllChannelsIndexSetUnused(int index);
|
||||
bool codeplugChannelSaveDataForIndex(int index, CodeplugChannel_t *channelBuf);
|
||||
CodeplugCSSTypes_t codeplugGetCSSType(uint16_t tone);
|
||||
void codeplugConvertChannelInternalToCodeplug(CodeplugChannel_t *codeplugChannel, CodeplugChannel_t *internalChannel);
|
||||
|
|
@ -442,6 +444,7 @@ bool codeplugGetGeneralSettings(CodeplugGeneralSettings_t *generalSettingsBuffer
|
|||
bool codeplugGetSignallingDTMF(CodeplugSignallingDTMF_t *signallingDTMFBuffer);
|
||||
bool codeplugGetSignallingDTMFDurations(CodeplugSignallingDTMFDurations_t *signallingDTMFDurationsBuffer);
|
||||
bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||
bool codeplugZoneRemoveChannelFromZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||
bool codeplugGetDeviceInfo(CodeplugDeviceInfo_t *deviceInfoBuffer);
|
||||
|
||||
uint16_t codeplugGetQuickkeyFunctionID(char key);
|
||||
|
|
|
|||
136
MDUV380_firmware/application/include/functions/dmrDataProtocol.h
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* DMR short-data (SMS) frame and payload codec.
|
||||
*
|
||||
* Pure functions only: no hardware access, no OS calls, no mutable globals,
|
||||
* so the whole module can be compiled and unit-tested on the host.
|
||||
*
|
||||
* The HR-C6000 baseband performs BPTC(196,96)/FEC and the on-air CRC in
|
||||
* hardware; this module only deals with the 12-byte logical payload of each
|
||||
* burst (CSBK preamble, data header, rate-1/2 data block) and with the
|
||||
* IP/UDP-encapsulated text payload spread across the data blocks.
|
||||
*
|
||||
* Message layouts follow ETSI TS 102 361-1/-2 plus the two de-facto text
|
||||
* message formats: Motorola TMS (UDP port 4007) and "DMR standard"/Anytone
|
||||
* (UDP port 5016), as documented in KY4YI's "DMR SMS/Text Message
|
||||
* Specification rev 1.0".
|
||||
*/
|
||||
#ifndef _OPENGD77_DMR_DATA_PROTOCOL_H_
|
||||
#define _OPENGD77_DMR_DATA_PROTOCOL_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DMR_DATA_BURST_LENGTH 12 // logical payload bytes per CSBK / header / rate-1/2 block burst
|
||||
#define DMR_DATA_RATE34_BLOCK_LENGTH 18 // logical payload bytes per rate-3/4 block burst
|
||||
#define DMR_DATA_CONFIRMED_PREFIX_LENGTH 2 // confirmed-data blocks start with a serial + CRC9 prefix
|
||||
|
||||
// DMR data types (high nibble of HR-C6000 register 0x51 on RX / written to register 0x50 on TX)
|
||||
#define DMR_DATA_TYPE_CSBK 0x03
|
||||
#define DMR_DATA_TYPE_DATA_HEADER 0x06
|
||||
#define DMR_DATA_TYPE_RATE_12_DATA 0x07
|
||||
#define DMR_DATA_TYPE_RATE_34_DATA 0x08
|
||||
|
||||
// SMS text limits. We compose up to 160 characters (in line with typical
|
||||
// OEM radio limits): 38 (IP+UDP+TMS headers) + 320 + 4 (CRC32) = 362 bytes
|
||||
// => 31 rate-1/2 blocks. On receive, up to 42 blocks are accepted so that
|
||||
// longer messages from other vendors still decode (text beyond
|
||||
// SMS_MAX_TEXT_LENGTH is truncated).
|
||||
#define SMS_MAX_TEXT_LENGTH 160
|
||||
#define SMS_MAX_TX_DATA_BLOCKS 31
|
||||
#define SMS_MAX_DATA_BLOCKS 42
|
||||
#define SMS_MAX_PAYLOAD_LENGTH (SMS_MAX_DATA_BLOCKS * DMR_DATA_BURST_LENGTH)
|
||||
|
||||
#define SMS_UDP_PORT_MOTOROLA 0x0FA7 // 4007
|
||||
#define SMS_UDP_PORT_STANDARD 0x1398 // 5016
|
||||
#define SMS_TEXT_OFFSET_MOTOROLA 38 // IP(20) + UDP(8) + TMS header(10)
|
||||
#define SMS_TEXT_OFFSET_STANDARD 32 // IP(20) + UDP(8) + vendor header(4)
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SMS_ENCODE_OK = 0,
|
||||
SMS_ENCODE_EMPTY,
|
||||
SMS_ENCODE_TOO_LONG
|
||||
} smsEncodeResult_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t srcId;
|
||||
uint32_t dstId;
|
||||
uint8_t dpf; // Data Packet Format (0x01 response, 0x02 unconfirmed, 0x03 confirmed)
|
||||
uint8_t sap; // Service Access Point (0x04 = UDP/IP)
|
||||
uint8_t blockCount; // blocks to follow
|
||||
uint8_t padOctets;
|
||||
bool isGroup;
|
||||
bool responseRequested;
|
||||
bool isResponsePdu; // delivery confirmation (DPF 0x01, no payload blocks)
|
||||
} dmrDataHeader_t;
|
||||
|
||||
//
|
||||
// Checksums
|
||||
//
|
||||
uint16_t dmrDataCRC16(const uint8_t *data, uint16_t length); // CRC16-CCITT, poly 0x1021, init 0x0000 (unmasked)
|
||||
uint32_t dmrDataCRC32(const uint8_t *data, uint16_t length); // ETSI DMR CRC32, input taken as byte-swapped 16-bit pairs
|
||||
uint16_t dmrDataIPHeaderChecksum(const uint8_t *ipHeader); // RFC 791, over 20 bytes with checksum field zeroed
|
||||
uint16_t dmrDataUDPChecksum(const uint8_t *ipPacket, uint16_t udpLength); // RFC 768, with IPv4 pseudo header
|
||||
|
||||
//
|
||||
// 12-byte burst payload builders (CRC computed and masked internally)
|
||||
//
|
||||
void dmrDataBuildPreambleCSBK(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId, uint8_t blocksToFollow);
|
||||
void dmrDataBuildUnconfirmedHeader(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId,
|
||||
uint8_t blockCount, uint8_t padOctets, bool requestResponse);
|
||||
void dmrDataBuildResponseHeader(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId, bool repeaterProfile);
|
||||
|
||||
//
|
||||
// 12-byte burst payload parser
|
||||
//
|
||||
bool dmrDataParseHeader(const uint8_t frame[DMR_DATA_BURST_LENGTH], dmrDataHeader_t *header);
|
||||
|
||||
//
|
||||
// Text payload (IP/UDP encapsulation spread over the data blocks)
|
||||
//
|
||||
// Builds the complete Motorola TMS payload: IP + UDP + TMS headers, UTF-16LE
|
||||
// text, zero padding to a 12-byte boundary and trailing CRC32. On success
|
||||
// *payloadLength is a multiple of 12 and *padOctets is the number of zero
|
||||
// octets between the text and the CRC32 trailer.
|
||||
smsEncodeResult_t smsPayloadBuildMotorola(uint8_t *payload, uint16_t *payloadLength, uint8_t *padOctets,
|
||||
uint32_t dstId, uint32_t srcId, const char *text, uint16_t ipSequenceNumber);
|
||||
|
||||
// Strict parsers for the two known formats. textOut is NUL terminated ASCII
|
||||
// (non-representable characters become '?').
|
||||
bool smsPayloadParseMotorola(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize);
|
||||
bool smsPayloadParseStandard(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize);
|
||||
|
||||
// Lenient fallback: find the longest plausible UTF-16 printable run anywhere
|
||||
// in the payload (tries little-endian first, then big-endian). Returns false
|
||||
// when nothing readable (>= 4 characters) was found.
|
||||
bool smsPayloadScanText(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize);
|
||||
|
||||
#endif /* _OPENGD77_DMR_DATA_PROTOCOL_H_ */
|
||||
114
MDUV380_firmware/application/include/functions/smsCore.h
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* DMR SMS engine: outgoing message queue and delivery tracking, incoming
|
||||
* message assembly and decoding, and delivery-report (ACK) handling.
|
||||
*
|
||||
* Context rules:
|
||||
* - smsRxProcessDataFrame() and the smsTxNotify*() functions are called from
|
||||
* the HR-C6000 interrupt handlers only.
|
||||
* - Everything else runs in the main application task. Decoding, storage
|
||||
* and event delivery are deferred from ISR to smsCoreTick().
|
||||
*/
|
||||
#ifndef _OPENGD77_SMS_CORE_H_
|
||||
#define _OPENGD77_SMS_CORE_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "functions/dmrDataProtocol.h"
|
||||
|
||||
#define SMS_TX_PREAMBLE_COUNT 8
|
||||
#define SMS_TX_ACK_PREAMBLE_COUNT 24
|
||||
// large enough for a full message (8 preambles + header + 31 blocks) and
|
||||
// for a delivery report (24 preambles + header, no blocks)
|
||||
#define SMS_TX_MAX_FRAMES (SMS_TX_PREAMBLE_COUNT + 1 + SMS_MAX_TX_DATA_BLOCKS)
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SMS_TX_STATE_IDLE = 0,
|
||||
SMS_TX_STATE_WAIT_CHANNEL, // packed, waiting for the channel to become free
|
||||
SMS_TX_STATE_SENDING, // bursts going out
|
||||
SMS_TX_STATE_WAIT_ACK // sent, waiting for a delivery report
|
||||
} smsTxState_t;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SMS_EVENT_NONE = 0,
|
||||
SMS_EVENT_TX_SENDING,
|
||||
SMS_EVENT_TX_SENT,
|
||||
SMS_EVENT_TX_DELIVERED,
|
||||
SMS_EVENT_TX_TIMEOUT, // no delivery report received
|
||||
SMS_EVENT_TX_CHANNEL_BUSY,
|
||||
SMS_EVENT_RX_MESSAGE
|
||||
} smsEvent_t;
|
||||
|
||||
// One prepared over-the-air transmission: preambleCount CSBK frames, then
|
||||
// the data header, then the payload blocks. The per-frame DMR data type
|
||||
// follows from the frame index.
|
||||
typedef struct
|
||||
{
|
||||
uint8_t frameCount;
|
||||
uint8_t preambleCount;
|
||||
uint8_t frames[SMS_TX_MAX_FRAMES][DMR_DATA_BURST_LENGTH];
|
||||
} smsAirJob_t;
|
||||
|
||||
//
|
||||
// Main task context
|
||||
//
|
||||
void smsCoreInit(void);
|
||||
void smsCoreTick(void);
|
||||
bool smsCoreIsIdle(void); // nothing in flight (used to gate flash writes)
|
||||
|
||||
bool smsSendMessage(uint32_t dstId, const char *text);
|
||||
bool smsResendLast(void);
|
||||
bool smsCanResendLast(void);
|
||||
smsTxState_t smsGetTxState(void);
|
||||
|
||||
smsEvent_t smsPopEvent(void);
|
||||
|
||||
bool smsHasUnseenRxMessage(void);
|
||||
void smsAcknowledgeRxMessage(void);
|
||||
|
||||
bool smsOptionWaitForAck(void);
|
||||
void smsOptionSetWaitForAck(bool waitForAck);
|
||||
bool smsOptionOwnIdOnly(void);
|
||||
void smsOptionSetOwnIdOnly(bool ownIdOnly);
|
||||
|
||||
//
|
||||
// HR-C6000 driver interface (ISR context)
|
||||
//
|
||||
// frame carries the burst's logical payload: 18 bytes for rate-3/4 blocks,
|
||||
// 12 bytes for everything else.
|
||||
bool smsRxProcessDataFrame(uint8_t dataType, const uint8_t *frame);
|
||||
void smsTxNotifyAirDone(void);
|
||||
void smsTxNotifyRejected(void);
|
||||
void smsTxNotifyHwAck(void);
|
||||
const smsAirJob_t *smsGetPendingAirJob(void);
|
||||
void smsAirJobStarted(void);
|
||||
|
||||
#endif /* _OPENGD77_SMS_CORE_H_ */
|
||||
91
MDUV380_firmware/application/include/functions/smsStorage.h
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Persistent store for the DMR SMS feature: inbox, sent messages and
|
||||
* quick-text templates, held as a single RAM image and lazily persisted
|
||||
* to a dedicated SPI flash region.
|
||||
*
|
||||
* Physical flash map on this platform (16MB W25Q128):
|
||||
* 0x000000 - 0x00FFFF emulated EEPROM (settings)
|
||||
* 0x010000 calibration copy
|
||||
* 0x020000 - ~0x100000 codeplug (FLASH_ADDRESS_OFFSET 128KB + codeplug regions)
|
||||
* 0x050000 / 0x0D8000 DMR ID databases
|
||||
* 0x0AF400 / 0x100000 voice prompts
|
||||
* 0xC00000 << SMS store (this module, 2 sectors) >>
|
||||
* 0xE00000 - 0xFFFFFF GPS log (last 2MB)
|
||||
*/
|
||||
#ifndef _OPENGD77_SMS_STORAGE_H_
|
||||
#define _OPENGD77_SMS_STORAGE_H_
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "functions/dmrDataProtocol.h"
|
||||
|
||||
#define SMS_STORAGE_FLASH_ADDRESS 0xC00000
|
||||
#define SMS_INBOX_SLOTS 8
|
||||
#define SMS_SENT_SLOTS 8
|
||||
#define SMS_QUICKTEXT_SLOTS 8
|
||||
|
||||
#define SMS_RECORD_FLAG_UNREAD 0x01
|
||||
|
||||
// smsStorageOptions bits
|
||||
#define SMS_OPTION_WAIT_FOR_ACK 0x01
|
||||
#define SMS_OPTION_OWN_ID_ONLY 0x02
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t peerId; // sender (inbox) or destination (sent)
|
||||
uint8_t flags;
|
||||
char text[SMS_MAX_TEXT_LENGTH + 1];
|
||||
} smsRecord_t;
|
||||
|
||||
void smsStorageInit(void);
|
||||
void smsStorageTick(void); // debounced persist, main loop context only
|
||||
void smsStorageMarkDirty(void);
|
||||
|
||||
uint8_t smsInboxCount(void);
|
||||
const smsRecord_t *smsInboxGet(uint8_t index); // index 0 = newest
|
||||
bool smsInboxAdd(uint32_t srcId, const char *text); // newest-first, oldest dropped when full
|
||||
bool smsInboxDelete(uint8_t index);
|
||||
void smsInboxMarkRead(uint8_t index);
|
||||
uint8_t smsInboxUnreadCount(void);
|
||||
|
||||
uint8_t smsSentCount(void);
|
||||
const smsRecord_t *smsSentGet(uint8_t index);
|
||||
bool smsSentAdd(uint32_t dstId, const char *text);
|
||||
bool smsSentDelete(uint8_t index);
|
||||
|
||||
uint8_t smsQuickTextCount(void);
|
||||
const char *smsQuickTextGet(uint8_t index);
|
||||
bool smsQuickTextSet(uint8_t index, const char *text); // index == smsQuickTextCount() appends
|
||||
bool smsQuickTextDelete(uint8_t index);
|
||||
|
||||
uint8_t smsStorageGetOptions(void);
|
||||
void smsStorageSetOptions(uint8_t options);
|
||||
|
||||
#endif /* _OPENGD77_SMS_STORAGE_H_ */
|
||||
|
|
@ -39,6 +39,7 @@ extern Task_t beepTask;
|
|||
extern int16_t melody_generic[512];
|
||||
extern const int16_t MELODY_POWER_ON[];
|
||||
extern const int16_t MELODY_PRIVATE_CALL[];
|
||||
extern const int16_t MELODY_SMS_RX[];
|
||||
extern const int16_t MELODY_KEY_BEEP[];
|
||||
extern const int16_t MELODY_KEY_LONG_BEEP[];
|
||||
extern const int16_t MELODY_ACK_BEEP[];
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ void HRC6000ClearColorCodeSynchronisation(void);
|
|||
void HRC6000SetTalkerAlias(const char *text);
|
||||
void HRC6000SetTalkerAliasLocation(uint32_t Lat, uint32_t Lon);
|
||||
bool HRC6000IRQHandlerIsRunning(void);
|
||||
bool HRC6000StartSmsTransmission(void);
|
||||
bool HRC6000IsSendingSMS(void);
|
||||
void HRC6000CancelSmsTransmission(void);
|
||||
bool HRC6000HasGotSync(void);
|
||||
bool HRC6000CCIsHeld(void);
|
||||
void HRC6000SetDmrRxGain(int8_t gain);
|
||||
|
|
|
|||
|
|
@ -269,8 +269,25 @@ void displayDrawFrequencySevenSeg(int16_t x, int16_t y, int16_t cellW, int16_t c
|
|||
#endif
|
||||
|
||||
#if defined(HAS_COLOURS)
|
||||
// FreeTRX selectable theme presets. CUSTOM is the CPS-editable theme (default);
|
||||
// the others are fixed palettes baked into the firmware.
|
||||
typedef enum
|
||||
{
|
||||
THEME_PRESET_CUSTOM = 0, // editable via CPS / on-device colour editor
|
||||
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;
|
||||
|
||||
void themeInitToDefaultValues(DayTime_t daytime, bool invert);
|
||||
void themeInit(bool SPIFlashAvailable);
|
||||
void themeApplyPreset(uint8_t preset); // rebuild themeItems[] for the given preset
|
||||
uint8_t themeGetPreset(void); // read the stored preset (default CUSTOM)
|
||||
void themeSetPreset(uint8_t preset, bool save); // apply, and optionally persist to the codeplug
|
||||
void displayThemeApply(themeItem_t fgItem, themeItem_t bgItem);
|
||||
void displayThemeResetToDefault(void);
|
||||
bool displayThemeIsForegroundColourEqualTo(themeItem_t fgItem);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ void torchToggle(void);
|
|||
#endif
|
||||
|
||||
void LedWrite(LEDs_t theLED, uint8_t output);
|
||||
void LedGreenSuppress(bool suppress); // hold the green LED off regardless of other writers
|
||||
uint8_t LedRead(LEDs_t theLED);
|
||||
void LedWriteDirect(LEDs_t theLED, uint8_t output);
|
||||
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t catalanLanguage=
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Esborra canal", // MaxLen: 16
|
||||
.keep_one_channel = "Deixa 1 canal", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t croatianLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t czechLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Smazat kanál", // MaxLen: 16
|
||||
.keep_one_channel = "Nech 1 kanál", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t danishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Slet kanal", // MaxLen: 16
|
||||
.keep_one_channel = "Behold 1 kanal", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t dutchLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Kanaal wissen", // MaxLen: 16
|
||||
.keep_one_channel = "Hou 1 kanaal", // MaxLen: 16
|
||||
.messages = "Berichten", // MaxLen: 16
|
||||
.new_message = "Nieuw bericht", // MaxLen: 16
|
||||
.inbox = "Postvak in", // MaxLen: 16
|
||||
.sent_box = "Verzonden", // MaxLen: 16
|
||||
.quick_text = "Snelle tekst", // MaxLen: 16
|
||||
.send_to = "Sturen naar", // MaxLen: 16
|
||||
.sms_sending = "Verzenden...", // MaxLen: 16
|
||||
.sms_sent = "Verzonden", // MaxLen: 16
|
||||
.sms_delivered = "Afgeleverd", // MaxLen: 16
|
||||
.sms_no_ack = "Geen ACK", // MaxLen: 16
|
||||
.sms_resend = "Opnieuw?", // MaxLen: 16
|
||||
.sms_channel_busy = "Kanaal bezet", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wacht op ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Alleen mijn ID", // MaxLen: 16
|
||||
.message_from = "Bericht van", // MaxLen: 16
|
||||
.delete_message = "Bericht wissen?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t englishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t finnishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Poista kanava", // MaxLen: 16
|
||||
.keep_one_channel = "Pidä 1 kanava", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t frenchLanguage =
|
|||
.text_size = "Taille texte",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Der. locut.", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Sourdine", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Effacer canal", // MaxLen: 16
|
||||
.keep_one_channel = "Garder 1 canal", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "Nouveau message", // MaxLen: 16
|
||||
.inbox = "Messages reçus", // MaxLen: 16
|
||||
.sent_box = "Envoyés", // MaxLen: 16
|
||||
.quick_text = "Texte rapide", // MaxLen: 16
|
||||
.send_to = "Envoyer à", // MaxLen: 16
|
||||
.sms_sending = "Envoi...", // MaxLen: 16
|
||||
.sms_sent = "Envoyé", // MaxLen: 16
|
||||
.sms_delivered = "Distribué", // MaxLen: 16
|
||||
.sms_no_ack = "Pas d’ACK", // MaxLen: 16
|
||||
.sms_resend = "Renvoyer ?", // MaxLen: 16
|
||||
.sms_channel_busy = "Canal occupé", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Attendre ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Mon ID seul", // MaxLen: 16
|
||||
.message_from = "Message de", // MaxLen: 16
|
||||
.delete_message = "Suppr. message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t germanLanguage =
|
|||
.text_size = "Text Grösse",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Kanal löschen", // MaxLen: 16
|
||||
.keep_one_channel = "1 Kanal behalten", // MaxLen: 16
|
||||
.messages = "Nachrichten", // MaxLen: 16
|
||||
.new_message = "Neue Nachricht", // MaxLen: 16
|
||||
.inbox = "Posteingang", // MaxLen: 16
|
||||
.sent_box = "Gesendet", // MaxLen: 16
|
||||
.quick_text = "Schnelltext", // MaxLen: 16
|
||||
.send_to = "Senden an", // MaxLen: 16
|
||||
.sms_sending = "Sende...", // MaxLen: 16
|
||||
.sms_sent = "Gesendet", // MaxLen: 16
|
||||
.sms_delivered = "Zugestellt", // MaxLen: 16
|
||||
.sms_no_ack = "Kein ACK", // MaxLen: 16
|
||||
.sms_resend = "Nochmal senden?", // MaxLen: 16
|
||||
.sms_channel_busy = "Kanal belegt", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Auf ACK warten", // MaxLen: 16
|
||||
.sms_own_id_only = "Nur eigene ID", // MaxLen: 16
|
||||
.message_from = "Nachricht von", // MaxLen: 16
|
||||
.delete_message = "Nachricht lösch.", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t hungarianLanguage=
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t italianLanguage =
|
|||
.text_size = "Formato font",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Ultimo OM RX", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Muto", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Elimina canale", // MaxLen: 16
|
||||
.keep_one_channel = "Tieni 1 canale", // MaxLen: 16
|
||||
.messages = "Messaggi", // MaxLen: 16
|
||||
.new_message = "Nuovo messaggio", // MaxLen: 16
|
||||
.inbox = "In arrivo", // MaxLen: 16
|
||||
.sent_box = "Inviati", // MaxLen: 16
|
||||
.quick_text = "Testo rapido", // MaxLen: 16
|
||||
.send_to = "Invia a", // MaxLen: 16
|
||||
.sms_sending = "Invio...", // MaxLen: 16
|
||||
.sms_sent = "Inviato", // MaxLen: 16
|
||||
.sms_delivered = "Consegnato", // MaxLen: 16
|
||||
.sms_no_ack = "Nessun ACK", // MaxLen: 16
|
||||
.sms_resend = "Reinviare?", // MaxLen: 16
|
||||
.sms_channel_busy = "Canale occupato", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Attendi ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Solo mio ID", // MaxLen: 16
|
||||
.message_from = "Messaggio da", // MaxLen: 16
|
||||
.delete_message = "Elimina msg?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t japaneseLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t polishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t portuguesBrazilLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Apagar canal", // MaxLen: 16
|
||||
.keep_one_channel = "Manter 1 canal", // MaxLen: 16
|
||||
.messages = "Mensagens", // MaxLen: 16
|
||||
.new_message = "Nova mensagem", // MaxLen: 16
|
||||
.inbox = "Recebidas", // MaxLen: 16
|
||||
.sent_box = "Enviadas", // MaxLen: 16
|
||||
.quick_text = "Texto rápido", // MaxLen: 16
|
||||
.send_to = "Enviar para", // MaxLen: 16
|
||||
.sms_sending = "Enviando", // MaxLen: 16
|
||||
.sms_sent = "Enviada", // MaxLen: 16
|
||||
.sms_delivered = "Entregue", // MaxLen: 16
|
||||
.sms_no_ack = "Sem ACK", // MaxLen: 16
|
||||
.sms_resend = "Reenviar?", // MaxLen: 16
|
||||
.sms_channel_busy = "Canal ocupado", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Esperar ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Só meu ID", // MaxLen: 16
|
||||
.message_from = "Mensagem de", // MaxLen: 16
|
||||
.delete_message = "Apagar msg?", // MaxLen: 16
|
||||
};
|
||||
/*********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t portuguesLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Apagar canal", // MaxLen: 16
|
||||
.keep_one_channel = "Manter 1 canal", // MaxLen: 16
|
||||
.messages = "Mensagens", // MaxLen: 16
|
||||
.new_message = "Nova mensagem", // MaxLen: 16
|
||||
.inbox = "Recebidas", // MaxLen: 16
|
||||
.sent_box = "Enviadas", // MaxLen: 16
|
||||
.quick_text = "Texto rápido", // MaxLen: 16
|
||||
.send_to = "Enviar para", // MaxLen: 16
|
||||
.sms_sending = "Enviando", // MaxLen: 16
|
||||
.sms_sent = "Enviada", // MaxLen: 16
|
||||
.sms_delivered = "Entregue", // MaxLen: 16
|
||||
.sms_no_ack = "Sem ACK", // MaxLen: 16
|
||||
.sms_resend = "Reenviar?", // MaxLen: 16
|
||||
.sms_channel_busy = "Canal ocupado", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Esperar ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Só meu ID", // MaxLen: 16
|
||||
.message_from = "Mensagem de", // MaxLen: 16
|
||||
.delete_message = "Apagar msg?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t romanianLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t slovenianLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t spanishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Borrar canal", // MaxLen: 16
|
||||
.keep_one_channel = "Dejar 1 canal", // MaxLen: 16
|
||||
.messages = "Mensajes", // MaxLen: 16
|
||||
.new_message = "Nuevo mensaje", // MaxLen: 16
|
||||
.inbox = "Recibidos", // MaxLen: 16
|
||||
.sent_box = "Enviados", // MaxLen: 16
|
||||
.quick_text = "Texto rápido", // MaxLen: 16
|
||||
.send_to = "Enviar a", // MaxLen: 16
|
||||
.sms_sending = "Enviando", // MaxLen: 16
|
||||
.sms_sent = "Enviado", // MaxLen: 16
|
||||
.sms_delivered = "Entregado", // MaxLen: 16
|
||||
.sms_no_ack = "Sin ACK", // MaxLen: 16
|
||||
.sms_resend = "¿Reenviar?", // MaxLen: 16
|
||||
.sms_channel_busy = "Canal ocupado", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Esperar ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "Solo mi ID", // MaxLen: 16
|
||||
.message_from = "Mensaje de", // MaxLen: 16
|
||||
.delete_message = "¿Borrar mensaje?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t swedishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Radera kanal", // MaxLen: 16
|
||||
.keep_one_channel = "Behåll 1 kanal", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -327,6 +327,24 @@ const stringsTable_t turkishLanguage =
|
|||
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
||||
.delete_channel = "Delete Channel", // MaxLen: 16
|
||||
.keep_one_channel = "Keep 1 channel", // MaxLen: 16
|
||||
.messages = "Messages", // MaxLen: 16
|
||||
.new_message = "New message", // MaxLen: 16
|
||||
.inbox = "Inbox", // MaxLen: 16
|
||||
.sent_box = "Sent", // MaxLen: 16
|
||||
.quick_text = "Quick text", // MaxLen: 16
|
||||
.send_to = "Send to", // MaxLen: 16
|
||||
.sms_sending = "Sending", // MaxLen: 16
|
||||
.sms_sent = "Sent", // MaxLen: 16
|
||||
.sms_delivered = "Delivered", // MaxLen: 16
|
||||
.sms_no_ack = "No ACK", // MaxLen: 16
|
||||
.sms_resend = "Resend?", // MaxLen: 16
|
||||
.sms_channel_busy = "Channel busy", // MaxLen: 16
|
||||
.sms_wait_for_ack = "Wait for ACK", // MaxLen: 16
|
||||
.sms_own_id_only = "My ID only", // MaxLen: 16
|
||||
.message_from = "Message from", // MaxLen: 16
|
||||
.delete_message = "Delete message?", // MaxLen: 16
|
||||
};
|
||||
/********************************************************************
|
||||
*
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
#else
|
||||
#define LANGUAGE_TAG_MAGIC_NUMBER { 'G', 'D', '7', '7' }, { 'L', 'A', 'N', 'G' } // Never change this tag.
|
||||
#endif
|
||||
#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x05 } // Bump the version each time the language struct is changed
|
||||
#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x06 } // Bump the version each time the language struct is changed
|
||||
|
||||
typedef struct
|
||||
{
|
||||
|
|
@ -335,6 +335,24 @@ typedef struct
|
|||
const char text_size[LANGUAGE_TEXTS_LENGTH];
|
||||
const char last_talker[LANGUAGE_TEXTS_LENGTH];
|
||||
const char mute[LANGUAGE_TEXTS_LENGTH];
|
||||
const char delete_channel[LANGUAGE_TEXTS_LENGTH];
|
||||
const char keep_one_channel[LANGUAGE_TEXTS_LENGTH];
|
||||
const char messages[LANGUAGE_TEXTS_LENGTH]; // 282, keep in sync with menuSystem.c smsMenuItems/mainMenuItems
|
||||
const char new_message[LANGUAGE_TEXTS_LENGTH]; // 283
|
||||
const char inbox[LANGUAGE_TEXTS_LENGTH]; // 284
|
||||
const char sent_box[LANGUAGE_TEXTS_LENGTH]; // 285
|
||||
const char quick_text[LANGUAGE_TEXTS_LENGTH]; // 286
|
||||
const char send_to[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_sending[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_sent[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_delivered[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_no_ack[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_resend[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_channel_busy[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_wait_for_ack[LANGUAGE_TEXTS_LENGTH];
|
||||
const char sms_own_id_only[LANGUAGE_TEXTS_LENGTH];
|
||||
const char message_from[LANGUAGE_TEXTS_LENGTH];
|
||||
const char delete_message[LANGUAGE_TEXTS_LENGTH];
|
||||
} stringsTable_t;
|
||||
|
||||
#endif // _OPENGD77_UILANGUAGE_H_
|
||||
|
|
|
|||
|
|
@ -172,9 +172,6 @@ typedef struct
|
|||
|
||||
void menuDisplayTitle(const char *title);
|
||||
void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, int32_t optStart, themeItem_t fgItem, themeItem_t fgOptItem, themeItem_t bgItem);
|
||||
#if defined(HAS_COLOURS)
|
||||
void menuSetNextEntryIconByMenuNum(int menuNum); // sets the icon the next menuDisplayEntry() will draw (0/none for menus without an icon)
|
||||
#endif
|
||||
|
||||
// menuGetMenuOffset() return values
|
||||
#define MENU_OFFSET_BEFORE_FIRST_ENTRY -1
|
||||
|
|
@ -270,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)
|
||||
|
|
@ -323,7 +323,16 @@ enum MENU_SCREENS
|
|||
#if !defined(PLATFORM_GD77S)
|
||||
MENU_APRS,
|
||||
#endif
|
||||
MENU_SMS,// DMR text messages hub
|
||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
||||
MENU_SMS_COMPOSE,
|
||||
MENU_SMS_INBOX,
|
||||
MENU_SMS_SENT,
|
||||
MENU_SMS_VIEW,
|
||||
MENU_SMS_QUICKTEXT,
|
||||
MENU_SMS_OPTIONS,
|
||||
MENU_SMS_CONTACT,
|
||||
UI_SMS_RX_POPUP,
|
||||
UI_MESSAGE_BOX,
|
||||
UI_HOTSPOT_MODE,
|
||||
UI_CPS,
|
||||
|
|
@ -356,6 +365,8 @@ enum CHANNEL_SCREEN_QUICK_MENU_ITEMS
|
|||
CH_SCREEN_QUICK_MENU_TALKAROUND,
|
||||
CH_SCREEN_QUICK_MENU_ROAMING,
|
||||
CH_SCREEN_QUICK_MENU_AUDIO_MUTE,
|
||||
CH_SCREEN_QUICK_MENU_NEW_CHANNEL,
|
||||
CH_SCREEN_QUICK_MENU_DELETE_CHANNEL,
|
||||
NUM_CH_SCREEN_QUICK_MENU_ITEMS
|
||||
};
|
||||
|
||||
|
|
@ -438,6 +449,7 @@ extern menuDataGlobal_t menuDataGlobal;
|
|||
extern const menuItemsList_t menuDataMainMenu;
|
||||
extern const menuItemsList_t menuDataContact;
|
||||
extern const menuItemsList_t menuDataOptions;
|
||||
extern const menuItemsList_t menuDataSMS;
|
||||
extern bool lockscreenIsRearming;
|
||||
|
||||
menuStatus_t uiVFOMode(uiEvent_t *event, bool isFirstRun);
|
||||
|
|
@ -484,4 +496,16 @@ menuStatus_t menuThemeItemsBrowser(uiEvent_t *ev, bool isFirstRun);
|
|||
menuStatus_t menuColourPicker(uiEvent_t *ev, bool isFirstRun);
|
||||
#endif
|
||||
|
||||
// DMR text messages (SMS)
|
||||
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);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
#include "io/Leds.h"
|
||||
#include "interfaces/adc.h"
|
||||
#include "hardware/radioHardwareInterface.h"
|
||||
#include "hardware/HR-C6000.h" // slotState, for restoring the green RX LED after unmute
|
||||
|
||||
#include "io/buttons.h"
|
||||
#include "usb/usb_com.h"
|
||||
|
|
@ -52,6 +53,8 @@
|
|||
#include "interfaces/settingsStorage.h"
|
||||
#include "interfaces/adc.h"
|
||||
#include "functions/rxPowerSaving.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
#if defined(USING_EXTERNAL_DEBUGGER)
|
||||
#include "SeggerRTT/RTT/SEGGER_RTT.h"
|
||||
|
|
@ -62,6 +65,68 @@
|
|||
// Couldn't declare into aprs.h, due to header cross-dependence.
|
||||
void aprsBeaconingTick(uiEvent_t *ev);
|
||||
|
||||
static bool smsResendTimeoutCallback(void)
|
||||
{
|
||||
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
||||
{
|
||||
smsResendLast();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void smsHandleEvents(void)
|
||||
{
|
||||
for (smsEvent_t smsEvent = smsPopEvent(); smsEvent != SMS_EVENT_NONE; smsEvent = smsPopEvent())
|
||||
{
|
||||
switch (smsEvent)
|
||||
{
|
||||
case SMS_EVENT_TX_SENDING:
|
||||
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_sending, true);
|
||||
break;
|
||||
|
||||
case SMS_EVENT_TX_SENT:
|
||||
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_sent, true);
|
||||
break;
|
||||
|
||||
case SMS_EVENT_TX_DELIVERED:
|
||||
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_delivered, true);
|
||||
|
||||
if (melody_play == NULL)
|
||||
{
|
||||
soundSetMelody(MELODY_ACK_BEEP);
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_EVENT_TX_CHANNEL_BUSY:
|
||||
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, currentLanguage->sms_channel_busy, true);
|
||||
|
||||
if (melody_play == NULL)
|
||||
{
|
||||
soundSetMelody(MELODY_NACK_BEEP);
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_EVENT_TX_TIMEOUT:
|
||||
if (smsCanResendLast())
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s\n%s",
|
||||
currentLanguage->sms_no_ack, currentLanguage->sms_resend);
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
uiDataGlobal.MessageBox.validatorCallback = smsResendTimeoutCallback;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_EVENT_RX_MESSAGE: // the channel/VFO screens open the popup
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volatile bool mainIsRunning = true;
|
||||
static bool updateMessageOnScreen = false;
|
||||
int8_t lastVolume = 0;
|
||||
|
|
@ -505,6 +570,9 @@ void applicationMainTask(void)
|
|||
}
|
||||
#endif
|
||||
|
||||
smsStorageInit();
|
||||
smsCoreInit();
|
||||
|
||||
menuSystemInit(getRtcTime_custom());
|
||||
|
||||
#if defined(HAS_GPS)
|
||||
|
|
@ -1030,6 +1098,7 @@ void applicationMainTask(void)
|
|||
|
||||
if ((trxGetMode() != RADIO_MODE_NONE) &&
|
||||
(settingsUsbMode != USB_MODE_HOTSPOT) &&
|
||||
(HRC6000IsSendingSMS() == false) && // no voice TX while a message is being sent
|
||||
(currentMenu != UI_POWER_OFF) &&
|
||||
(currentMenu != UI_SPLASH_SCREEN) &&
|
||||
(currentMenu != UI_TX_SCREEN)&&
|
||||
|
|
@ -1378,6 +1447,9 @@ void applicationMainTask(void)
|
|||
voxTick();
|
||||
gpsTick();
|
||||
aprsBeaconingTick(&ev);
|
||||
smsCoreTick();
|
||||
smsStorageTick();
|
||||
smsHandleEvents();
|
||||
settingsSaveIfNeeded(false);
|
||||
|
||||
if (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER))
|
||||
|
|
@ -1440,16 +1512,31 @@ void applicationMainTask(void)
|
|||
|
||||
if (audioAmpIsMuted() || (lastVolume == -99)) // explicit mute or volume knob fully down
|
||||
{
|
||||
if (!muteFlashActive)
|
||||
{
|
||||
// Hold the green LED off for the whole mute, so the DMR RX handler
|
||||
// (which re-asserts green as soon as it reads it off) can't make it
|
||||
// flicker green between the red flashes.
|
||||
LedGreenSuppress(true);
|
||||
muteFlashActive = true;
|
||||
}
|
||||
|
||||
bool flashOn = ((ticksGetMillis() % 2000) < 500);
|
||||
LedWrite(LED_GREEN, 0);
|
||||
LedWrite(LED_RED, (flashOn ? 1 : 0));
|
||||
muteFlashActive = true;
|
||||
}
|
||||
else if (muteFlashActive)
|
||||
{
|
||||
// Just unmuted: release our forced LED state.
|
||||
// Just unmuted: release the green hold, stop the red flash and restore
|
||||
// the green LED to the current RX state, so a QSO in progress lights
|
||||
// green again instead of staying dark. Green is normally lit for an
|
||||
// active DMR slot (matching our filter) or a received FM/digital signal;
|
||||
// if nothing is being received it stays off. The normal RX code then
|
||||
// keeps it correct.
|
||||
LedGreenSuppress(false);
|
||||
bool rxGreen = (slotState == DMR_STATE_RX_1) || (slotState == DMR_STATE_RX_2) ||
|
||||
currentRadioDevice->analogSignalReceived || currentRadioDevice->digitalSignalReceived;
|
||||
LedWrite(LED_RED, 0);
|
||||
LedWrite(LED_GREEN, 0);
|
||||
LedWrite(LED_GREEN, (rxGreen ? 1 : 0));
|
||||
muteFlashActive = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,6 +383,46 @@ bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneB
|
|||
return false;
|
||||
}
|
||||
|
||||
bool codeplugZoneRemoveChannelFromZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf)
|
||||
{
|
||||
// Not valid for the virtual "All Channels" zone (indexNumber -1): its membership
|
||||
// is the channel in-use bitmap, not a stored channels[] list.
|
||||
if (zoneBuf->NOT_IN_CODEPLUGDATA_indexNumber < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the channel in the zone's list.
|
||||
int found = -1;
|
||||
for (int i = 0; i < zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone; i++)
|
||||
{
|
||||
if (zoneBuf->channels[i] == channelIndex)
|
||||
{
|
||||
found = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == -1)
|
||||
{
|
||||
return false; // channel is not a member of this zone
|
||||
}
|
||||
|
||||
// Shift the remaining channels down to close the gap, then zero the freed tail slot
|
||||
// (a zero also terminates the list for codeplugZoneGetDataForNumber()).
|
||||
for (int i = found; i < (zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone - 1); i++)
|
||||
{
|
||||
zoneBuf->channels[i] = zoneBuf->channels[i + 1];
|
||||
}
|
||||
zoneBuf->channels[zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone - 1] = 0;
|
||||
zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone--;
|
||||
zoneBuf->NOT_IN_CODEPLUGDATA_highestIndex = zoneBuf->NOT_IN_CODEPLUGDATA_numChannelsInZone;
|
||||
|
||||
// IMPORTANT. Write size is different from the size of the data (see the add function).
|
||||
return EEPROM_Write(CODEPLUG_ADDR_EX_ZONE_LIST + (zoneBuf->NOT_IN_CODEPLUGDATA_indexNumber * (16 + (sizeof(uint16_t) * codeplugChannelsPerZone))),
|
||||
(uint8_t *)zoneBuf, ((codeplugChannelsPerZone == 16) ? CODEPLUG_ZONE_DATA_ORIGINAL_STRUCT_SIZE : CODEPLUG_ZONE_DATA_OPENGD77_STRUCT_SIZE));
|
||||
}
|
||||
|
||||
static uint16_t codeplugAllChannelsGetCount(void)
|
||||
{
|
||||
uint16_t c = 0;
|
||||
|
|
@ -447,6 +487,48 @@ void codeplugAllChannelsIndexSetUsed(int index)
|
|||
}
|
||||
}
|
||||
|
||||
void codeplugAllChannelsIndexSetUnused(int index)
|
||||
{
|
||||
if ((index >= CODEPLUG_CHANNELS_MIN) && (index <= CODEPLUG_CHANNELS_MAX))
|
||||
{
|
||||
index--;
|
||||
int channelBank = (index / CODEPLUG_CHANNELS_PER_BANK);
|
||||
int byteno = (index % CODEPLUG_CHANNELS_PER_BANK) / 8;
|
||||
int cacheOffset = index / 8;
|
||||
|
||||
codeplugAllChannelsCache[cacheOffset] &= ~(1 << (index % 8));
|
||||
|
||||
if(channelBank == 0)
|
||||
{
|
||||
EEPROM_Write(CODEPLUG_ADDR_CHANNEL_HEADER_EEPROM + byteno, &codeplugAllChannelsCache[cacheOffset], 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
SPI_Flash_write(FLASH_ADDRESS_OFFSET + (CODEPLUG_ADDR_CHANNEL_HEADER_FLASH + ((channelBank - 1) *
|
||||
(CODEPLUG_CHANNELS_PER_BANK * CODEPLUG_CHANNEL_DATA_STRUCT_SIZE + 16))) + byteno, &codeplugAllChannelsCache[cacheOffset], 1);
|
||||
}
|
||||
|
||||
if (allChannelsTotalNumOfChannels > 0)
|
||||
{
|
||||
allChannelsTotalNumOfChannels--;
|
||||
}
|
||||
|
||||
// If we just freed the highest-numbered channel, rescan for the new highest.
|
||||
if ((index + 1) >= allChannelsHighestChannelIndex)
|
||||
{
|
||||
allChannelsHighestChannelIndex = 0;
|
||||
for (int i = CODEPLUG_CHANNELS_MAX; i >= CODEPLUG_CHANNELS_MIN; i--)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(i))
|
||||
{
|
||||
allChannelsHighestChannelIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void codeplugAllChannelsInitCache(void)
|
||||
{
|
||||
// There are 8 banks
|
||||
|
|
|
|||
571
MDUV380_firmware/application/source/functions/dmrDataProtocol.c
Normal file
|
|
@ -0,0 +1,571 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "functions/dmrDataProtocol.h"
|
||||
|
||||
// CRC masks from ETSI TS 102 361-1 B.3.11/B.3.12 (per data type)
|
||||
#define CSBK_CRC_MASK 0xA5A5
|
||||
#define DATA_HEADER_CRC_MASK 0xCCCC
|
||||
|
||||
// Data Packet Format values (data header octet 0, low nibble)
|
||||
#define DPF_RESPONSE 0x01
|
||||
#define DPF_UNCONFIRMED 0x02
|
||||
#define DPF_CONFIRMED 0x03
|
||||
|
||||
#define SAP_UDP_IP 0x04
|
||||
|
||||
#define IP_HEADER_LENGTH 20
|
||||
#define UDP_HEADER_LENGTH 8
|
||||
#define TMS_HEADER_LENGTH 10
|
||||
#define STANDARD_HEADER_LENGTH 4
|
||||
#define CRC32_TRAILER_LENGTH 4
|
||||
|
||||
// TMS message type octet (payload offset 30): 0xA0 plain text,
|
||||
// 0xE0 text with delivery-report request.
|
||||
#define TMS_TYPE_TEXT 0xA0
|
||||
#define TMS_TYPE_TEXT_ACK_REQUEST 0xE0
|
||||
|
||||
|
||||
uint16_t dmrDataCRC16(const uint8_t *data, uint16_t length)
|
||||
{
|
||||
uint16_t crc = 0x0000;
|
||||
|
||||
for (uint16_t i = 0; i < length; i++)
|
||||
{
|
||||
crc ^= ((uint16_t)data[i]) << 8;
|
||||
|
||||
for (uint8_t bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc & 0x8000) ? ((crc << 1) ^ 0x1021) : (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
// ETSI DMR CRC32 (poly 0x04C11DB7, init 0, MSB first). The input is consumed
|
||||
// as byte-swapped 16-bit pairs, hence length must be even.
|
||||
uint32_t dmrDataCRC32(const uint8_t *data, uint16_t length)
|
||||
{
|
||||
uint32_t crc = 0;
|
||||
|
||||
for (uint16_t i = 0; i < length; i++)
|
||||
{
|
||||
crc ^= ((uint32_t)data[i ^ 1]) << 24;
|
||||
|
||||
for (uint8_t bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc & 0x80000000U) ? ((crc << 1) ^ 0x04C11DB7U) : (crc << 1);
|
||||
}
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
// Ones-complement sum used by both the IP header and UDP checksums
|
||||
static uint32_t onesComplementSum(uint32_t sum, const uint8_t *data, uint16_t length)
|
||||
{
|
||||
uint16_t i = 0;
|
||||
|
||||
while (length > 1)
|
||||
{
|
||||
sum += (((uint16_t)data[i]) << 8) | data[i + 1];
|
||||
i += 2;
|
||||
length -= 2;
|
||||
}
|
||||
|
||||
if (length == 1)
|
||||
{
|
||||
sum += ((uint16_t)data[i]) << 8;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
static uint16_t onesComplementFold(uint32_t sum)
|
||||
{
|
||||
while (sum >> 16)
|
||||
{
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
|
||||
return (uint16_t)(~sum);
|
||||
}
|
||||
|
||||
uint16_t dmrDataIPHeaderChecksum(const uint8_t *ipHeader)
|
||||
{
|
||||
return onesComplementFold(onesComplementSum(0, ipHeader, IP_HEADER_LENGTH));
|
||||
}
|
||||
|
||||
uint16_t dmrDataUDPChecksum(const uint8_t *ipPacket, uint16_t udpLength)
|
||||
{
|
||||
uint8_t pseudoHeader[12];
|
||||
|
||||
memcpy(&pseudoHeader[0], &ipPacket[12], 4); // source IP
|
||||
memcpy(&pseudoHeader[4], &ipPacket[16], 4); // destination IP
|
||||
pseudoHeader[8] = 0x00;
|
||||
pseudoHeader[9] = 0x11; // UDP protocol
|
||||
pseudoHeader[10] = (udpLength >> 8) & 0xFF;
|
||||
pseudoHeader[11] = udpLength & 0xFF;
|
||||
|
||||
uint32_t sum = onesComplementSum(0, pseudoHeader, sizeof(pseudoHeader));
|
||||
uint16_t checksum = onesComplementFold(onesComplementSum(sum, &ipPacket[IP_HEADER_LENGTH], udpLength));
|
||||
|
||||
// All-zero means "no checksum" in UDP, transmit as 0xFFFF instead
|
||||
return ((checksum == 0x0000) ? 0xFFFF : checksum);
|
||||
}
|
||||
|
||||
static void put24BE(uint8_t *dest, uint32_t value)
|
||||
{
|
||||
dest[0] = (value >> 16) & 0xFF;
|
||||
dest[1] = (value >> 8) & 0xFF;
|
||||
dest[2] = value & 0xFF;
|
||||
}
|
||||
|
||||
static uint32_t get24BE(const uint8_t *src)
|
||||
{
|
||||
return (((uint32_t)src[0]) << 16) | (((uint32_t)src[1]) << 8) | src[2];
|
||||
}
|
||||
|
||||
// ETSI TS 102 361-1 B.3.11: the CCITT CRC is ones-complemented, then XORed
|
||||
// with the data-type mask (validated against on-air captures).
|
||||
static void applyFrameCRC(uint8_t frame[DMR_DATA_BURST_LENGTH], uint16_t mask)
|
||||
{
|
||||
uint16_t crc = (uint16_t)(~dmrDataCRC16(frame, DMR_DATA_BURST_LENGTH - 2)) ^ mask;
|
||||
|
||||
frame[10] = (crc >> 8) & 0xFF;
|
||||
frame[11] = crc & 0xFF;
|
||||
}
|
||||
|
||||
void dmrDataBuildPreambleCSBK(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId, uint8_t blocksToFollow)
|
||||
{
|
||||
frame[0] = 0xBD; // Last Block flag + Preamble CSBK opcode (0x3D)
|
||||
frame[1] = 0x00; // Feature set ID: standardized
|
||||
frame[2] = 0x80; // data content follows, target is an individual ID
|
||||
frame[3] = blocksToFollow;
|
||||
put24BE(&frame[4], dstId);
|
||||
put24BE(&frame[7], srcId);
|
||||
applyFrameCRC(frame, CSBK_CRC_MASK);
|
||||
}
|
||||
|
||||
void dmrDataBuildUnconfirmedHeader(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId,
|
||||
uint8_t blockCount, uint8_t padOctets, bool requestResponse)
|
||||
{
|
||||
frame[0] = DPF_UNCONFIRMED | (requestResponse ? 0x40 : 0x00) | (padOctets & 0x10);
|
||||
frame[1] = (SAP_UDP_IP << 4) | (padOctets & 0x0F);
|
||||
put24BE(&frame[2], dstId);
|
||||
put24BE(&frame[5], srcId);
|
||||
frame[8] = 0x80 | (blockCount & 0x7F); // full message, blocks to follow
|
||||
frame[9] = 0x00; // fragment sequence number
|
||||
applyFrameCRC(frame, DATA_HEADER_CRC_MASK);
|
||||
}
|
||||
|
||||
void dmrDataBuildResponseHeader(uint8_t frame[DMR_DATA_BURST_LENGTH], uint32_t dstId, uint32_t srcId, bool repeaterProfile)
|
||||
{
|
||||
frame[0] = DPF_RESPONSE;
|
||||
frame[1] = repeaterProfile ? (SAP_UDP_IP << 4) : 0x00;
|
||||
put24BE(&frame[2], dstId);
|
||||
put24BE(&frame[5], srcId);
|
||||
frame[8] = 0x00; // no blocks to follow
|
||||
frame[9] = repeaterProfile ? 0x08 : 0x00; // response type: ACK
|
||||
applyFrameCRC(frame, DATA_HEADER_CRC_MASK);
|
||||
}
|
||||
|
||||
bool dmrDataParseHeader(const uint8_t frame[DMR_DATA_BURST_LENGTH], dmrDataHeader_t *header)
|
||||
{
|
||||
uint16_t receivedCRC = (((uint16_t)frame[10]) << 8) | frame[11];
|
||||
|
||||
if (((uint16_t)(~dmrDataCRC16(frame, DMR_DATA_BURST_LENGTH - 2)) ^ DATA_HEADER_CRC_MASK) != receivedCRC)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
header->isGroup = ((frame[0] & 0x80) != 0);
|
||||
header->responseRequested = ((frame[0] & 0x40) != 0);
|
||||
header->dpf = frame[0] & 0x0F;
|
||||
header->sap = (frame[1] >> 4) & 0x0F;
|
||||
header->padOctets = (frame[0] & 0x10) | (frame[1] & 0x0F);
|
||||
header->dstId = get24BE(&frame[2]);
|
||||
header->srcId = get24BE(&frame[5]);
|
||||
header->blockCount = frame[8] & 0x7F;
|
||||
header->isResponsePdu = ((header->dpf == DPF_RESPONSE) && (frame[8] == 0x00));
|
||||
|
||||
switch (header->dpf)
|
||||
{
|
||||
case DPF_RESPONSE:
|
||||
case DPF_UNCONFIRMED:
|
||||
case DPF_CONFIRMED:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Map one UTF-16 code unit to displayable ASCII. Both endiannesses appear in
|
||||
// the wild (spec examples and BrandMeister use little-endian), so decode is
|
||||
// per-character adaptive.
|
||||
static char utf16ToChar(uint8_t first, uint8_t second)
|
||||
{
|
||||
uint8_t ascii;
|
||||
|
||||
if (second == 0x00)
|
||||
{
|
||||
ascii = first; // little-endian
|
||||
}
|
||||
else if (first == 0x00)
|
||||
{
|
||||
ascii = second; // big-endian
|
||||
}
|
||||
else
|
||||
{
|
||||
return '?'; // non latin-1 code point
|
||||
}
|
||||
|
||||
if ((ascii >= 0x20) && (ascii < 0x7F))
|
||||
{
|
||||
return (char)ascii;
|
||||
}
|
||||
|
||||
if ((ascii == '\r') || (ascii == '\n') || (ascii == '\t'))
|
||||
{
|
||||
return ' '; // messages are displayed as a single flowed text
|
||||
}
|
||||
|
||||
return '?';
|
||||
}
|
||||
|
||||
static void decodeUtf16Text(const uint8_t *data, uint16_t byteCount, char *textOut, uint16_t textOutSize)
|
||||
{
|
||||
uint16_t outPos = 0;
|
||||
|
||||
for (uint16_t i = 0; (i + 1) < byteCount; i += 2)
|
||||
{
|
||||
if (outPos >= (textOutSize - 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ((data[i] == 0x00) && (data[i + 1] == 0x00))
|
||||
{
|
||||
break; // embedded terminator
|
||||
}
|
||||
|
||||
textOut[outPos++] = utf16ToChar(data[i], data[i + 1]);
|
||||
}
|
||||
|
||||
// strip trailing '?' produced by stray padding
|
||||
while ((outPos > 0) && (textOut[outPos - 1] == '?'))
|
||||
{
|
||||
outPos--;
|
||||
}
|
||||
|
||||
textOut[outPos] = 0;
|
||||
}
|
||||
|
||||
smsEncodeResult_t smsPayloadBuildMotorola(uint8_t *payload, uint16_t *payloadLength, uint8_t *padOctets,
|
||||
uint32_t dstId, uint32_t srcId, const char *text, uint16_t ipSequenceNumber)
|
||||
{
|
||||
uint16_t textLength = (uint16_t)strlen(text);
|
||||
|
||||
if (textLength == 0)
|
||||
{
|
||||
return SMS_ENCODE_EMPTY;
|
||||
}
|
||||
|
||||
if (textLength > SMS_MAX_TEXT_LENGTH)
|
||||
{
|
||||
return SMS_ENCODE_TOO_LONG;
|
||||
}
|
||||
|
||||
uint16_t textBytes = textLength * 2;
|
||||
uint16_t ipLength = SMS_TEXT_OFFSET_MOTOROLA + textBytes; // headers + text, excludes padding and CRC32
|
||||
uint16_t udpLength = UDP_HEADER_LENGTH + TMS_HEADER_LENGTH + textBytes;
|
||||
uint16_t totalLength = ipLength + CRC32_TRAILER_LENGTH;
|
||||
|
||||
// round up to a whole number of rate-1/2 blocks
|
||||
totalLength = ((totalLength + (DMR_DATA_BURST_LENGTH - 1)) / DMR_DATA_BURST_LENGTH) * DMR_DATA_BURST_LENGTH;
|
||||
|
||||
uint8_t pad = (uint8_t)(totalLength - ipLength - CRC32_TRAILER_LENGTH);
|
||||
|
||||
memset(payload, 0, totalLength);
|
||||
|
||||
// IPv4 header
|
||||
payload[0] = 0x45; // version 4, IHL 5
|
||||
payload[1] = 0x00;
|
||||
payload[2] = (ipLength >> 8) & 0xFF;
|
||||
payload[3] = ipLength & 0xFF;
|
||||
payload[4] = (ipSequenceNumber >> 8) & 0xFF;
|
||||
payload[5] = ipSequenceNumber & 0xFF;
|
||||
payload[6] = 0x00; // flags / fragment offset
|
||||
payload[7] = 0x00;
|
||||
payload[8] = 0x01; // TTL
|
||||
payload[9] = 0x11; // UDP
|
||||
payload[10] = 0x00; // checksum placeholder
|
||||
payload[11] = 0x00;
|
||||
payload[12] = 0x0C; // individual DMR ID address space
|
||||
put24BE(&payload[13], srcId);
|
||||
payload[16] = 0x0C;
|
||||
put24BE(&payload[17], dstId);
|
||||
|
||||
uint16_t ipChecksum = dmrDataIPHeaderChecksum(payload);
|
||||
payload[10] = (ipChecksum >> 8) & 0xFF;
|
||||
payload[11] = ipChecksum & 0xFF;
|
||||
|
||||
// UDP header
|
||||
payload[20] = (SMS_UDP_PORT_MOTOROLA >> 8) & 0xFF;
|
||||
payload[21] = SMS_UDP_PORT_MOTOROLA & 0xFF;
|
||||
payload[22] = (SMS_UDP_PORT_MOTOROLA >> 8) & 0xFF;
|
||||
payload[23] = SMS_UDP_PORT_MOTOROLA & 0xFF;
|
||||
payload[24] = (udpLength >> 8) & 0xFF;
|
||||
payload[25] = udpLength & 0xFF;
|
||||
payload[26] = 0x00; // checksum placeholder
|
||||
payload[27] = 0x00;
|
||||
|
||||
// TMS header
|
||||
payload[28] = 0x00;
|
||||
payload[29] = (textBytes + 8) & 0xFF;
|
||||
payload[30] = TMS_TYPE_TEXT_ACK_REQUEST;
|
||||
payload[31] = 0x00;
|
||||
payload[32] = (ipSequenceNumber & 0x7F) | 0x80;
|
||||
payload[33] = 0x04;
|
||||
payload[34] = 0x0D;
|
||||
payload[35] = 0x00;
|
||||
payload[36] = 0x0A;
|
||||
payload[37] = 0x00;
|
||||
|
||||
// UTF-16LE text, uppercased ASCII for best cross-vendor display compatibility
|
||||
for (uint16_t i = 0; i < textLength; i++)
|
||||
{
|
||||
char c = text[i];
|
||||
|
||||
if ((c >= 'a') && (c <= 'z'))
|
||||
{
|
||||
c -= ('a' - 'A');
|
||||
}
|
||||
else if (((c < 0x20) || (c >= 0x7F)) && (c != '\r') && (c != '\n'))
|
||||
{
|
||||
c = ' ';
|
||||
}
|
||||
|
||||
payload[SMS_TEXT_OFFSET_MOTOROLA + (i * 2)] = (uint8_t)c;
|
||||
payload[SMS_TEXT_OFFSET_MOTOROLA + (i * 2) + 1] = 0x00;
|
||||
}
|
||||
|
||||
uint16_t udpChecksum = dmrDataUDPChecksum(payload, udpLength);
|
||||
payload[26] = (udpChecksum >> 8) & 0xFF;
|
||||
payload[27] = udpChecksum & 0xFF;
|
||||
|
||||
// trailing CRC32 (little-endian) over everything before it
|
||||
uint32_t crc = dmrDataCRC32(payload, totalLength - CRC32_TRAILER_LENGTH);
|
||||
payload[totalLength - 4] = crc & 0xFF;
|
||||
payload[totalLength - 3] = (crc >> 8) & 0xFF;
|
||||
payload[totalLength - 2] = (crc >> 16) & 0xFF;
|
||||
payload[totalLength - 1] = (crc >> 24) & 0xFF;
|
||||
|
||||
*payloadLength = totalLength;
|
||||
*padOctets = pad;
|
||||
|
||||
return SMS_ENCODE_OK;
|
||||
}
|
||||
|
||||
// Common IP/UDP shell validation for both text formats. Returns the UDP
|
||||
// length, or 0 when the shell is not a plausible SMS packet.
|
||||
static uint16_t validateIPShell(const uint8_t *payload, uint16_t payloadLength, uint16_t udpPort)
|
||||
{
|
||||
if (payloadLength < (IP_HEADER_LENGTH + UDP_HEADER_LENGTH))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (((payload[0] & 0xF0) != 0x40) || (payload[9] != 0x11)) // IPv4, UDP
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (dmrDataIPHeaderChecksum(payload) != 0) // sum including the checksum field yields 0 when valid
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t srcPort = (((uint16_t)payload[20]) << 8) | payload[21];
|
||||
uint16_t dstPort = (((uint16_t)payload[22]) << 8) | payload[23];
|
||||
|
||||
if ((srcPort != udpPort) && (dstPort != udpPort))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t udpLength = (((uint16_t)payload[24]) << 8) | payload[25];
|
||||
|
||||
if ((udpLength < UDP_HEADER_LENGTH) || ((IP_HEADER_LENGTH + udpLength) > payloadLength))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return udpLength;
|
||||
}
|
||||
|
||||
bool smsPayloadParseMotorola(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize)
|
||||
{
|
||||
uint16_t udpLength = validateIPShell(payload, payloadLength, SMS_UDP_PORT_MOTOROLA);
|
||||
|
||||
if (udpLength < (UDP_HEADER_LENGTH + TMS_HEADER_LENGTH))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TMS header shape: start marker, message type, "text" magic
|
||||
if ((payload[28] != 0x00) ||
|
||||
((payload[30] != TMS_TYPE_TEXT) && (payload[30] != TMS_TYPE_TEXT_ACK_REQUEST)) ||
|
||||
(payload[34] != 0x0D) || (payload[36] != 0x0A))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t textBytes = udpLength - UDP_HEADER_LENGTH - TMS_HEADER_LENGTH;
|
||||
|
||||
if ((SMS_TEXT_OFFSET_MOTOROLA + textBytes) > payloadLength)
|
||||
{
|
||||
textBytes = payloadLength - SMS_TEXT_OFFSET_MOTOROLA;
|
||||
}
|
||||
|
||||
decodeUtf16Text(&payload[SMS_TEXT_OFFSET_MOTOROLA], textBytes, textOut, textOutSize);
|
||||
|
||||
return (textOut[0] != 0);
|
||||
}
|
||||
|
||||
bool smsPayloadParseStandard(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize)
|
||||
{
|
||||
uint16_t udpLength = validateIPShell(payload, payloadLength, SMS_UDP_PORT_STANDARD);
|
||||
|
||||
if (udpLength < (UDP_HEADER_LENGTH + STANDARD_HEADER_LENGTH))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((payload[28] != 0x00) || (payload[29] != 0x0D) || (payload[31] != 0x0A))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t textBytes = udpLength - UDP_HEADER_LENGTH - STANDARD_HEADER_LENGTH;
|
||||
|
||||
if ((SMS_TEXT_OFFSET_STANDARD + textBytes) > payloadLength)
|
||||
{
|
||||
textBytes = payloadLength - SMS_TEXT_OFFSET_STANDARD;
|
||||
}
|
||||
|
||||
decodeUtf16Text(&payload[SMS_TEXT_OFFSET_STANDARD], textBytes, textOut, textOutSize);
|
||||
|
||||
return (textOut[0] != 0);
|
||||
}
|
||||
|
||||
static bool isDisplayableAscii(uint8_t c)
|
||||
{
|
||||
return (((c >= 0x20) && (c < 0x7F)) || (c == '\r') || (c == '\n'));
|
||||
}
|
||||
|
||||
bool smsPayloadScanText(const uint8_t *payload, uint16_t payloadLength, char *textOut, uint16_t textOutSize)
|
||||
{
|
||||
uint16_t bestStart = 0;
|
||||
uint16_t bestLength = 0; // in characters
|
||||
bool bestBigEndian = false;
|
||||
|
||||
// four sweeps: both 16-bit alignments x both endiannesses
|
||||
for (uint8_t sweep = 0; sweep < 4; sweep++)
|
||||
{
|
||||
uint16_t start = sweep & 1;
|
||||
bool bigEndian = ((sweep & 2) != 0);
|
||||
uint16_t runStart = start;
|
||||
uint16_t runLength = 0;
|
||||
|
||||
for (uint16_t i = start; (i + 1) < payloadLength; i += 2)
|
||||
{
|
||||
uint8_t asciiByte = bigEndian ? payload[i + 1] : payload[i];
|
||||
uint8_t zeroByte = bigEndian ? payload[i] : payload[i + 1];
|
||||
|
||||
if ((zeroByte == 0x00) && isDisplayableAscii(asciiByte))
|
||||
{
|
||||
if (runLength == 0)
|
||||
{
|
||||
runStart = i;
|
||||
}
|
||||
|
||||
runLength++;
|
||||
|
||||
if (runLength > bestLength)
|
||||
{
|
||||
bestLength = runLength;
|
||||
bestStart = runStart;
|
||||
bestBigEndian = bigEndian;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
runLength = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLength < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t outPos = 0;
|
||||
|
||||
for (uint16_t i = 0; (i < bestLength) && (outPos < (textOutSize - 1)); i++)
|
||||
{
|
||||
uint16_t offset = bestStart + (i * 2);
|
||||
char c = (char)(bestBigEndian ? payload[offset + 1] : payload[offset]);
|
||||
|
||||
if ((c == '\r') || (c == '\n'))
|
||||
{
|
||||
c = ' '; // messages are displayed as a single flowed text
|
||||
}
|
||||
|
||||
textOut[outPos++] = c;
|
||||
}
|
||||
|
||||
textOut[outPos] = 0;
|
||||
|
||||
// collapse the leading whitespace a run picked up from line breaks
|
||||
uint16_t start = 0;
|
||||
|
||||
while ((textOut[start] == ' ') && (start < outPos))
|
||||
{
|
||||
start++;
|
||||
}
|
||||
|
||||
if (start > 0)
|
||||
{
|
||||
memmove(textOut, &textOut[start], (outPos - start) + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
719
MDUV380_firmware/application/source/functions/smsCore.c
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
#include "functions/ticks.h"
|
||||
#include "functions/trx.h"
|
||||
#include "functions/settings.h"
|
||||
#include "hardware/HR-C6000.h"
|
||||
|
||||
#define SMS_WAIT_CHANNEL_TIMEOUT_MS 4000
|
||||
#define SMS_WAIT_ACK_TIMEOUT_MS 6000
|
||||
#define SMS_ACK_RESPONSE_DELAY_MS 1500
|
||||
#define SMS_RX_ASSEMBLY_STALL_MS 2000
|
||||
#define SMS_RX_DEDUPE_WINDOW_MS 30000
|
||||
|
||||
#define SMS_EVENT_QUEUE_SIZE 8
|
||||
|
||||
//
|
||||
// Outgoing transmission
|
||||
//
|
||||
static struct
|
||||
{
|
||||
volatile smsTxState_t state;
|
||||
volatile bool jobPending;
|
||||
bool jobIsAckResponse;
|
||||
uint32_t peerId;
|
||||
char text[SMS_MAX_TEXT_LENGTH + 1]; // kept for the sent box and resend
|
||||
bool haveLastMessage;
|
||||
ticksTimer_t channelTimer;
|
||||
ticksTimer_t ackTimer;
|
||||
uint16_t ipSequence;
|
||||
} tx;
|
||||
|
||||
static smsAirJob_t txAirJob;
|
||||
|
||||
// set from ISR context, consumed in smsCoreTick()
|
||||
static volatile bool txAirDoneFlag = false;
|
||||
static volatile bool txRejectedFlag = false;
|
||||
static volatile bool txDeliveredFlag = false;
|
||||
|
||||
//
|
||||
// Incoming assembly (written from ISR context only).
|
||||
//
|
||||
// The raw bursts are kept as-received: senders differ in whether the blocks
|
||||
// carry a confirmed-data prefix, and it is not guaranteed how many of a
|
||||
// rate-3/4 block's 18 bytes the chip exposes, so the extraction layout is
|
||||
// determined at decode time by trying the possible models against the IP
|
||||
// header checksum.
|
||||
//
|
||||
#define SMS_RX_MAX_BLOCKS 32
|
||||
#define SMS_RX_BLOCK_STRIDE DMR_DATA_RATE34_BLOCK_LENGTH
|
||||
|
||||
static volatile struct
|
||||
{
|
||||
bool active;
|
||||
uint8_t expectedBlocks;
|
||||
uint8_t receivedBlocks;
|
||||
uint8_t framesSeen; // progress marker for the stall watchdog
|
||||
uint32_t srcId;
|
||||
bool responseRequested;
|
||||
bool confirmedData; // header DPF was "confirmed"
|
||||
bool rate34; // payload arrived as rate-3/4 blocks
|
||||
} rxAssembly;
|
||||
|
||||
// Shared between assembly (ISR) and decoding (main task): while
|
||||
// rxDecode.pending is set the buffers belong to the main task and the ISR
|
||||
// won't open a new assembly into them.
|
||||
static uint8_t rxRawBlocks[SMS_RX_MAX_BLOCKS * SMS_RX_BLOCK_STRIDE];
|
||||
static __attribute__((section(".ccmram"))) uint8_t rxAssembledPayload[SMS_RX_MAX_BLOCKS * (SMS_RX_BLOCK_STRIDE - DMR_DATA_CONFIRMED_PREFIX_LENGTH)];
|
||||
|
||||
// completed assembly handed over for decoding in the main task
|
||||
static volatile struct
|
||||
{
|
||||
bool pending;
|
||||
uint8_t blockCount;
|
||||
bool confirmedData;
|
||||
bool rate34;
|
||||
uint32_t srcId;
|
||||
bool responseRequested;
|
||||
} rxDecode;
|
||||
|
||||
//
|
||||
// Delivery report we owe to a sender
|
||||
//
|
||||
static struct
|
||||
{
|
||||
bool pending;
|
||||
uint32_t dstId;
|
||||
ticksTimer_t delayTimer;
|
||||
} ackResponse;
|
||||
|
||||
// UI event queue and unseen-message flag (main task context only)
|
||||
static uint8_t eventQueue[SMS_EVENT_QUEUE_SIZE];
|
||||
static uint8_t eventQueueHead = 0;
|
||||
static uint8_t eventQueueCount = 0;
|
||||
|
||||
static bool unseenRxMessage = false;
|
||||
static uint32_t lastRxStoreTime = 0;
|
||||
|
||||
static void pushEvent(smsEvent_t event)
|
||||
{
|
||||
if (eventQueueCount < SMS_EVENT_QUEUE_SIZE)
|
||||
{
|
||||
eventQueue[(eventQueueHead + eventQueueCount) % SMS_EVENT_QUEUE_SIZE] = (uint8_t)event;
|
||||
eventQueueCount++;
|
||||
}
|
||||
}
|
||||
|
||||
smsEvent_t smsPopEvent(void)
|
||||
{
|
||||
if (eventQueueCount == 0)
|
||||
{
|
||||
return SMS_EVENT_NONE;
|
||||
}
|
||||
|
||||
smsEvent_t event = (smsEvent_t)eventQueue[eventQueueHead];
|
||||
eventQueueHead = (eventQueueHead + 1) % SMS_EVENT_QUEUE_SIZE;
|
||||
eventQueueCount--;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void smsCoreInit(void)
|
||||
{
|
||||
memset(&tx, 0, sizeof(tx));
|
||||
memset((void *)&rxAssembly, 0, sizeof(rxAssembly));
|
||||
memset((void *)&rxDecode, 0, sizeof(rxDecode));
|
||||
memset(&ackResponse, 0, sizeof(ackResponse));
|
||||
eventQueueHead = 0;
|
||||
eventQueueCount = 0;
|
||||
unseenRxMessage = false;
|
||||
txAirDoneFlag = false;
|
||||
txRejectedFlag = false;
|
||||
txDeliveredFlag = false;
|
||||
}
|
||||
|
||||
bool smsCoreIsIdle(void)
|
||||
{
|
||||
return ((tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false) &&
|
||||
(rxAssembly.active == false) && (rxDecode.pending == false) &&
|
||||
(ackResponse.pending == false));
|
||||
}
|
||||
|
||||
smsTxState_t smsGetTxState(void)
|
||||
{
|
||||
return tx.state;
|
||||
}
|
||||
|
||||
//
|
||||
// Air job construction. The payload blocks are built in place: the rows of
|
||||
// txAirJob.frames are contiguous, so the encoder writes the whole payload
|
||||
// directly into the rows following the preambles and the header.
|
||||
//
|
||||
static void buildAirJobFrames(uint32_t dstId, uint32_t srcId, uint8_t preambleCount,
|
||||
const uint8_t *headerFrame, uint8_t blockCount)
|
||||
{
|
||||
uint8_t frameIndex = 0;
|
||||
|
||||
for (uint8_t i = 0; i < preambleCount; i++)
|
||||
{
|
||||
// remaining frames after this preamble: the other preambles, the
|
||||
// header, and the payload blocks
|
||||
uint8_t blocksToFollow = (preambleCount - 1 - i) + 1 + blockCount;
|
||||
|
||||
dmrDataBuildPreambleCSBK(txAirJob.frames[frameIndex], dstId, srcId, blocksToFollow);
|
||||
frameIndex++;
|
||||
}
|
||||
|
||||
memcpy(txAirJob.frames[frameIndex], headerFrame, DMR_DATA_BURST_LENGTH);
|
||||
frameIndex++;
|
||||
|
||||
// the payload block contents are already in place (message jobs), or absent (ACK jobs)
|
||||
txAirJob.preambleCount = preambleCount;
|
||||
txAirJob.frameCount = frameIndex + blockCount;
|
||||
}
|
||||
|
||||
static bool queueMessageTransmission(uint32_t dstId, const char *text)
|
||||
{
|
||||
uint16_t payloadLength = 0;
|
||||
uint8_t padOctets = 0;
|
||||
uint8_t *payloadArea = txAirJob.frames[SMS_TX_PREAMBLE_COUNT + 1]; // rows after preambles + header
|
||||
|
||||
tx.ipSequence++;
|
||||
|
||||
if (smsPayloadBuildMotorola(payloadArea, &payloadLength, &padOctets,
|
||||
dstId, trxDMRID, text, tx.ipSequence) != SMS_ENCODE_OK)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t blockCount = payloadLength / DMR_DATA_BURST_LENGTH;
|
||||
uint8_t headerFrame[DMR_DATA_BURST_LENGTH];
|
||||
|
||||
dmrDataBuildUnconfirmedHeader(headerFrame, dstId, trxDMRID, blockCount, padOctets, smsOptionWaitForAck());
|
||||
buildAirJobFrames(dstId, trxDMRID, SMS_TX_PREAMBLE_COUNT, headerFrame, blockCount);
|
||||
|
||||
tx.peerId = dstId;
|
||||
strncpy(tx.text, text, SMS_MAX_TEXT_LENGTH);
|
||||
tx.text[SMS_MAX_TEXT_LENGTH] = 0;
|
||||
tx.haveLastMessage = true;
|
||||
tx.jobIsAckResponse = false;
|
||||
tx.jobPending = true;
|
||||
tx.state = SMS_TX_STATE_WAIT_CHANNEL;
|
||||
ticksTimerStart(&tx.channelTimer, SMS_WAIT_CHANNEL_TIMEOUT_MS);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void queueAckResponseTransmission(uint32_t dstId)
|
||||
{
|
||||
uint8_t headerFrame[DMR_DATA_BURST_LENGTH];
|
||||
// repeaters/hotspots expect the SAP + response-type variant of the
|
||||
// delivery report, OEM radios on simplex the plain one
|
||||
bool repeaterProfile = (currentChannelData->rxFreq != currentChannelData->txFreq);
|
||||
|
||||
dmrDataBuildResponseHeader(headerFrame, dstId, trxDMRID, repeaterProfile);
|
||||
buildAirJobFrames(dstId, trxDMRID, SMS_TX_ACK_PREAMBLE_COUNT, headerFrame, 0);
|
||||
|
||||
tx.jobIsAckResponse = true;
|
||||
tx.jobPending = true;
|
||||
tx.state = SMS_TX_STATE_WAIT_CHANNEL;
|
||||
ticksTimerStart(&tx.channelTimer, SMS_WAIT_CHANNEL_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
static bool smsTransmissionIsAllowed(void)
|
||||
{
|
||||
return ((trxGetMode() == RADIO_MODE_DIGITAL) &&
|
||||
(settingsUsbMode != USB_MODE_HOTSPOT) &&
|
||||
(settingsIsOptionBitSet(BIT_TX_INHIBIT) == false) &&
|
||||
(trxTransmissionEnabled == false) &&
|
||||
(trxIsTransmitting == false));
|
||||
}
|
||||
|
||||
static bool tryStartAirTransmission(void)
|
||||
{
|
||||
return HRC6000StartSmsTransmission();
|
||||
}
|
||||
|
||||
bool smsSendMessage(uint32_t dstId, const char *text)
|
||||
{
|
||||
if ((tx.state != SMS_TX_STATE_IDLE) || tx.jobPending ||
|
||||
(dstId == 0) || (dstId > 0xFFFFFF) || (smsTransmissionIsAllowed() == false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return queueMessageTransmission(dstId, text);
|
||||
}
|
||||
|
||||
bool smsCanResendLast(void)
|
||||
{
|
||||
return (tx.haveLastMessage && (tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false));
|
||||
}
|
||||
|
||||
bool smsResendLast(void)
|
||||
{
|
||||
if (smsCanResendLast() == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return smsSendMessage(tx.peerId, tx.text);
|
||||
}
|
||||
|
||||
//
|
||||
// RX notification for the popup screen
|
||||
//
|
||||
bool smsHasUnseenRxMessage(void)
|
||||
{
|
||||
return unseenRxMessage;
|
||||
}
|
||||
|
||||
void smsAcknowledgeRxMessage(void)
|
||||
{
|
||||
unseenRxMessage = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Options (persisted with the message store)
|
||||
//
|
||||
bool smsOptionWaitForAck(void)
|
||||
{
|
||||
return ((smsStorageGetOptions() & SMS_OPTION_WAIT_FOR_ACK) != 0);
|
||||
}
|
||||
|
||||
void smsOptionSetWaitForAck(bool waitForAck)
|
||||
{
|
||||
uint8_t options = smsStorageGetOptions();
|
||||
|
||||
smsStorageSetOptions(waitForAck ? (options | SMS_OPTION_WAIT_FOR_ACK) : (options & ~SMS_OPTION_WAIT_FOR_ACK));
|
||||
}
|
||||
|
||||
bool smsOptionOwnIdOnly(void)
|
||||
{
|
||||
return ((smsStorageGetOptions() & SMS_OPTION_OWN_ID_ONLY) != 0);
|
||||
}
|
||||
|
||||
void smsOptionSetOwnIdOnly(bool ownIdOnly)
|
||||
{
|
||||
uint8_t options = smsStorageGetOptions();
|
||||
|
||||
smsStorageSetOptions(ownIdOnly ? (options | SMS_OPTION_OWN_ID_ONLY) : (options & ~SMS_OPTION_OWN_ID_ONLY));
|
||||
}
|
||||
|
||||
//
|
||||
// ISR-side interface
|
||||
//
|
||||
const smsAirJob_t *smsGetPendingAirJob(void)
|
||||
{
|
||||
return (tx.jobPending ? &txAirJob : NULL);
|
||||
}
|
||||
|
||||
void smsAirJobStarted(void)
|
||||
{
|
||||
tx.jobPending = false;
|
||||
}
|
||||
|
||||
void smsTxNotifyAirDone(void)
|
||||
{
|
||||
txAirDoneFlag = true;
|
||||
}
|
||||
|
||||
void smsTxNotifyRejected(void)
|
||||
{
|
||||
txRejectedFlag = true;
|
||||
}
|
||||
|
||||
void smsTxNotifyHwAck(void)
|
||||
{
|
||||
if (tx.state == SMS_TX_STATE_WAIT_ACK)
|
||||
{
|
||||
txDeliveredFlag = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void rxAssemblyReset(void)
|
||||
{
|
||||
rxAssembly.active = false;
|
||||
rxAssembly.expectedBlocks = 0;
|
||||
rxAssembly.receivedBlocks = 0;
|
||||
}
|
||||
|
||||
static void rxAssemblyComplete(void)
|
||||
{
|
||||
rxDecode.blockCount = ((rxAssembly.receivedBlocks < SMS_RX_MAX_BLOCKS) ? rxAssembly.receivedBlocks : SMS_RX_MAX_BLOCKS);
|
||||
rxDecode.confirmedData = rxAssembly.confirmedData;
|
||||
rxDecode.rate34 = rxAssembly.rate34;
|
||||
rxDecode.srcId = rxAssembly.srcId;
|
||||
rxDecode.responseRequested = rxAssembly.responseRequested;
|
||||
rxDecode.pending = true; // buffers now belong to the main task
|
||||
|
||||
rxAssemblyReset();
|
||||
}
|
||||
|
||||
bool smsRxProcessDataFrame(uint8_t dataType, const uint8_t *frame)
|
||||
{
|
||||
rxAssembly.framesSeen++;
|
||||
|
||||
if (dataType == DMR_DATA_TYPE_DATA_HEADER)
|
||||
{
|
||||
dmrDataHeader_t header;
|
||||
|
||||
if (dmrDataParseHeader(frame, &header) == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// delivery report for our outstanding message? (it can arrive while
|
||||
// the state machine is still finishing up the transmission)
|
||||
if (header.isResponsePdu)
|
||||
{
|
||||
if (((tx.state == SMS_TX_STATE_WAIT_ACK) || (tx.state == SMS_TX_STATE_SENDING)) &&
|
||||
(header.srcId == tx.peerId))
|
||||
{
|
||||
txDeliveredFlag = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Longer messages than the raw-block store still assemble; the text
|
||||
// beyond the stored blocks is truncated at decode time
|
||||
if ((header.sap != 0x04) || header.isGroup ||
|
||||
(header.blockCount == 0) || (header.blockCount > SMS_MAX_DATA_BLOCKS))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (smsOptionOwnIdOnly() && (header.dstId != trxDMRID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rxDecode.pending) // buffers still owned by the decoder, drop this message
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
rxAssembly.expectedBlocks = header.blockCount;
|
||||
rxAssembly.receivedBlocks = 0;
|
||||
rxAssembly.srcId = header.srcId;
|
||||
rxAssembly.responseRequested = header.responseRequested;
|
||||
rxAssembly.confirmedData = (header.dpf == 0x03);
|
||||
rxAssembly.rate34 = false;
|
||||
rxAssembly.active = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rxAssembly.active == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((dataType != DMR_DATA_TYPE_RATE_12_DATA) && (dataType != DMR_DATA_TYPE_RATE_34_DATA))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rxAssembly.receivedBlocks < SMS_RX_MAX_BLOCKS)
|
||||
{
|
||||
// keep the burst raw; the extraction layout is decided at decode time
|
||||
memcpy(&rxRawBlocks[rxAssembly.receivedBlocks * SMS_RX_BLOCK_STRIDE], frame,
|
||||
((dataType == DMR_DATA_TYPE_RATE_34_DATA) ? DMR_DATA_RATE34_BLOCK_LENGTH : DMR_DATA_BURST_LENGTH));
|
||||
|
||||
if (rxAssembly.receivedBlocks == 0)
|
||||
{
|
||||
rxAssembly.rate34 = (dataType == DMR_DATA_TYPE_RATE_34_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
rxAssembly.receivedBlocks++;
|
||||
|
||||
if (rxAssembly.receivedBlocks >= rxAssembly.expectedBlocks)
|
||||
{
|
||||
rxAssemblyComplete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// Main task processing
|
||||
//
|
||||
|
||||
// Compact the raw bursts into a contiguous payload, taking `take` bytes
|
||||
// starting at `skip` from each block. Returns the assembled length.
|
||||
static uint16_t rxExtractPayload(uint8_t blockCount, uint8_t skip, uint8_t take)
|
||||
{
|
||||
for (uint8_t i = 0; i < blockCount; i++)
|
||||
{
|
||||
memcpy(&rxAssembledPayload[i * take], &rxRawBlocks[(i * SMS_RX_BLOCK_STRIDE) + skip], take);
|
||||
}
|
||||
|
||||
return (uint16_t)blockCount * take;
|
||||
}
|
||||
|
||||
static void processCompletedRxMessage(void)
|
||||
{
|
||||
char text[SMS_MAX_TEXT_LENGTH + 1];
|
||||
uint32_t srcId = rxDecode.srcId;
|
||||
bool responseRequested = rxDecode.responseRequested;
|
||||
|
||||
// Candidate block layouts, likeliest first (based on the header's
|
||||
// confirmed/unconfirmed DPF). For rate-3/4, the 12-byte layouts cover
|
||||
// the case where the chip exposes only part of the 18-byte block.
|
||||
const uint8_t prefix = DMR_DATA_CONFIRMED_PREFIX_LENGTH;
|
||||
uint8_t blockLengths[4];
|
||||
uint8_t skips[4];
|
||||
uint8_t modelCount;
|
||||
|
||||
if (rxDecode.rate34)
|
||||
{
|
||||
blockLengths[0] = DMR_DATA_RATE34_BLOCK_LENGTH;
|
||||
blockLengths[1] = DMR_DATA_BURST_LENGTH;
|
||||
blockLengths[2] = DMR_DATA_RATE34_BLOCK_LENGTH;
|
||||
blockLengths[3] = DMR_DATA_BURST_LENGTH;
|
||||
skips[0] = skips[1] = (rxDecode.confirmedData ? prefix : 0);
|
||||
skips[2] = skips[3] = (rxDecode.confirmedData ? 0 : prefix);
|
||||
modelCount = 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
blockLengths[0] = blockLengths[1] = DMR_DATA_BURST_LENGTH;
|
||||
skips[0] = (rxDecode.confirmedData ? prefix : 0);
|
||||
skips[1] = (rxDecode.confirmedData ? 0 : prefix);
|
||||
modelCount = 2;
|
||||
}
|
||||
|
||||
bool decoded = false;
|
||||
uint16_t length = 0;
|
||||
|
||||
for (uint8_t m = 0; (m < modelCount) && (decoded == false); m++)
|
||||
{
|
||||
length = rxExtractPayload(rxDecode.blockCount, skips[m], blockLengths[m] - skips[m]);
|
||||
|
||||
decoded = (smsPayloadParseMotorola(rxAssembledPayload, length, text, sizeof(text)) ||
|
||||
smsPayloadParseStandard(rxAssembledPayload, length, text, sizeof(text)));
|
||||
}
|
||||
|
||||
if (decoded == false)
|
||||
{
|
||||
// No layout produced a structurally valid packet: salvage readable
|
||||
// text from every layout and keep the longest result
|
||||
char candidate[SMS_MAX_TEXT_LENGTH + 1];
|
||||
|
||||
text[0] = 0;
|
||||
|
||||
for (uint8_t m = 0; m < modelCount; m++)
|
||||
{
|
||||
length = rxExtractPayload(rxDecode.blockCount, skips[m], blockLengths[m] - skips[m]);
|
||||
|
||||
if (smsPayloadScanText(rxAssembledPayload, length, candidate, sizeof(candidate)) &&
|
||||
(strlen(candidate) > strlen(text)))
|
||||
{
|
||||
memcpy(text, candidate, sizeof(text));
|
||||
}
|
||||
}
|
||||
|
||||
decoded = (text[0] != 0);
|
||||
}
|
||||
|
||||
rxDecode.pending = false; // buffers released back to the ISR assembler
|
||||
|
||||
if (decoded)
|
||||
{
|
||||
// OEM radios repeat a message when they miss our delivery report;
|
||||
// don't store consecutive identical copies arriving close together
|
||||
const smsRecord_t *newest = smsInboxGet(0);
|
||||
bool isDuplicate = ((newest != NULL) && (newest->peerId == srcId) &&
|
||||
(strcmp(newest->text, text) == 0) &&
|
||||
((ticksGetMillis() - lastRxStoreTime) < SMS_RX_DEDUPE_WINDOW_MS));
|
||||
|
||||
if (isDuplicate == false)
|
||||
{
|
||||
smsInboxAdd(srcId, text);
|
||||
lastRxStoreTime = ticksGetMillis();
|
||||
unseenRxMessage = true;
|
||||
pushEvent(SMS_EVENT_RX_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
// acknowledge receipt at the data-link level even when the payload
|
||||
// didn't decode to text - all blocks did arrive
|
||||
if (responseRequested && (srcId != 0) && (srcId != trxDMRID))
|
||||
{
|
||||
ackResponse.pending = true;
|
||||
ackResponse.dstId = srcId;
|
||||
ticksTimerStart(&ackResponse.delayTimer, SMS_ACK_RESPONSE_DELAY_MS);
|
||||
}
|
||||
}
|
||||
|
||||
static void watchRxAssemblyStall(void)
|
||||
{
|
||||
static uint8_t lastFramesSeen = 0;
|
||||
static ticksTimer_t stallTimer = { 0, 0 };
|
||||
static bool watching = false;
|
||||
|
||||
if (rxAssembly.active)
|
||||
{
|
||||
if ((watching == false) || (rxAssembly.framesSeen != lastFramesSeen))
|
||||
{
|
||||
lastFramesSeen = rxAssembly.framesSeen;
|
||||
ticksTimerStart(&stallTimer, SMS_RX_ASSEMBLY_STALL_MS);
|
||||
watching = true;
|
||||
}
|
||||
else if (ticksTimerHasExpired(&stallTimer))
|
||||
{
|
||||
rxAssemblyReset(); // transmission broke off mid-message
|
||||
watching = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
watching = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void finishTransmission(smsEvent_t event)
|
||||
{
|
||||
bool wasAckResponse = tx.jobIsAckResponse;
|
||||
|
||||
tx.state = SMS_TX_STATE_IDLE;
|
||||
tx.jobPending = false;
|
||||
tx.jobIsAckResponse = false;
|
||||
|
||||
if ((wasAckResponse == false) && (event != SMS_EVENT_NONE))
|
||||
{
|
||||
pushEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void smsCoreTick(void)
|
||||
{
|
||||
if (rxDecode.pending)
|
||||
{
|
||||
processCompletedRxMessage();
|
||||
}
|
||||
|
||||
watchRxAssemblyStall();
|
||||
|
||||
// pending delivery report: transmit once the delay elapsed and we're free
|
||||
if (ackResponse.pending && ticksTimerHasExpired(&ackResponse.delayTimer) &&
|
||||
(tx.state == SMS_TX_STATE_IDLE) && (tx.jobPending == false))
|
||||
{
|
||||
if (smsTransmissionIsAllowed())
|
||||
{
|
||||
queueAckResponseTransmission(ackResponse.dstId);
|
||||
}
|
||||
|
||||
ackResponse.pending = false; // single attempt, dropped when not possible
|
||||
}
|
||||
|
||||
switch (tx.state)
|
||||
{
|
||||
case SMS_TX_STATE_WAIT_CHANNEL:
|
||||
if (tryStartAirTransmission())
|
||||
{
|
||||
tx.state = SMS_TX_STATE_SENDING;
|
||||
|
||||
if (tx.jobIsAckResponse == false)
|
||||
{
|
||||
pushEvent(SMS_EVENT_TX_SENDING);
|
||||
}
|
||||
}
|
||||
else if (ticksTimerHasExpired(&tx.channelTimer))
|
||||
{
|
||||
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_TX_STATE_SENDING:
|
||||
if (HRC6000GetIsWakingState() == WAKING_MODE_FAILED)
|
||||
{
|
||||
// The repeater never woke up; tear the transmission down
|
||||
HRC6000CancelSmsTransmission();
|
||||
trxTransmissionEnabled = false;
|
||||
HRC6000ClearIsWakingState();
|
||||
HRC6000TerminateDigital();
|
||||
trxDisableTransmission();
|
||||
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
||||
}
|
||||
else if (txRejectedFlag)
|
||||
{
|
||||
txRejectedFlag = false;
|
||||
trxDisableTransmission();
|
||||
finishTransmission(SMS_EVENT_TX_CHANNEL_BUSY);
|
||||
}
|
||||
else if (txAirDoneFlag)
|
||||
{
|
||||
txAirDoneFlag = false;
|
||||
|
||||
if (tx.jobIsAckResponse)
|
||||
{
|
||||
finishTransmission(SMS_EVENT_NONE);
|
||||
}
|
||||
else
|
||||
{
|
||||
smsSentAdd(tx.peerId, tx.text);
|
||||
pushEvent(SMS_EVENT_TX_SENT);
|
||||
|
||||
if (smsOptionWaitForAck())
|
||||
{
|
||||
tx.state = SMS_TX_STATE_WAIT_ACK;
|
||||
ticksTimerStart(&tx.ackTimer, SMS_WAIT_ACK_TIMEOUT_MS);
|
||||
}
|
||||
else
|
||||
{
|
||||
finishTransmission(SMS_EVENT_NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_TX_STATE_WAIT_ACK:
|
||||
if (txDeliveredFlag)
|
||||
{
|
||||
txDeliveredFlag = false;
|
||||
finishTransmission(SMS_EVENT_TX_DELIVERED);
|
||||
}
|
||||
else if (ticksTimerHasExpired(&tx.ackTimer))
|
||||
{
|
||||
finishTransmission(SMS_EVENT_TX_TIMEOUT);
|
||||
}
|
||||
break;
|
||||
|
||||
case SMS_TX_STATE_IDLE:
|
||||
default:
|
||||
txAirDoneFlag = false;
|
||||
txRejectedFlag = false;
|
||||
txDeliveredFlag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
323
MDUV380_firmware/application/source/functions/smsStorage.c
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "functions/smsStorage.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/ticks.h"
|
||||
#include "functions/trx.h"
|
||||
#include "hardware/SPI_Flash.h"
|
||||
#include "hardware/HR-C6000.h"
|
||||
|
||||
#define SMS_STORAGE_MAGIC 0x4D53444F // 'ODSM'
|
||||
#define SMS_STORAGE_VERSION 2
|
||||
#define SMS_STORAGE_DEBOUNCE_MS 1500
|
||||
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t imageSize;
|
||||
uint8_t inboxCount;
|
||||
uint8_t sentCount;
|
||||
uint8_t quickTextCount;
|
||||
uint8_t options;
|
||||
uint32_t writeCounter;
|
||||
uint32_t crc; // over everything after this field
|
||||
smsRecord_t inbox[SMS_INBOX_SLOTS];
|
||||
smsRecord_t sent[SMS_SENT_SLOTS];
|
||||
char quickTexts[SMS_QUICKTEXT_SLOTS][SMS_MAX_TEXT_LENGTH + 1];
|
||||
} smsStorageImage_t;
|
||||
|
||||
// SPI_Flash_write() can span at most two 4KB sectors, and dmrDataCRC32()
|
||||
// consumes byte-swapped pairs, so the image must stay <= 8KB and even sized
|
||||
typedef char assert_smsImageFitsTwoSectors[(sizeof(smsStorageImage_t) <= 8192) ? 1 : -1];
|
||||
typedef char assert_smsImageSizeIsEven[((sizeof(smsStorageImage_t) % 2) == 0) ? 1 : -1];
|
||||
|
||||
static __attribute__((section(".ccmram"))) smsStorageImage_t storageImage;
|
||||
|
||||
static bool storageDirty = false;
|
||||
static ticksTimer_t debounceTimer = { 0, 0 };
|
||||
|
||||
static const char *defaultQuickTexts[] =
|
||||
{
|
||||
"QRZ?",
|
||||
"QSL 73",
|
||||
"ON MY WAY",
|
||||
"CALL ME",
|
||||
"BACK IN 5 MIN"
|
||||
};
|
||||
|
||||
static uint32_t computeImageCRC(void)
|
||||
{
|
||||
const uint8_t *afterCRC = ((const uint8_t *)&storageImage.crc) + sizeof(storageImage.crc);
|
||||
uint16_t length = sizeof(smsStorageImage_t) - (afterCRC - (const uint8_t *)&storageImage);
|
||||
|
||||
return dmrDataCRC32(afterCRC, length);
|
||||
}
|
||||
|
||||
static void formatStorage(void)
|
||||
{
|
||||
memset(&storageImage, 0, sizeof(storageImage));
|
||||
storageImage.magic = SMS_STORAGE_MAGIC;
|
||||
storageImage.version = SMS_STORAGE_VERSION;
|
||||
storageImage.imageSize = sizeof(smsStorageImage_t);
|
||||
storageImage.options = (SMS_OPTION_WAIT_FOR_ACK | SMS_OPTION_OWN_ID_ONLY);
|
||||
|
||||
storageImage.quickTextCount = (sizeof(defaultQuickTexts) / sizeof(defaultQuickTexts[0]));
|
||||
for (uint8_t i = 0; i < storageImage.quickTextCount; i++)
|
||||
{
|
||||
strncpy(storageImage.quickTexts[i], defaultQuickTexts[i], SMS_MAX_TEXT_LENGTH);
|
||||
}
|
||||
|
||||
storageDirty = true;
|
||||
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
void smsStorageInit(void)
|
||||
{
|
||||
SPI_Flash_read(SMS_STORAGE_FLASH_ADDRESS, (uint8_t *)&storageImage, sizeof(storageImage));
|
||||
|
||||
if ((storageImage.magic != SMS_STORAGE_MAGIC) ||
|
||||
(storageImage.version != SMS_STORAGE_VERSION) ||
|
||||
(storageImage.imageSize != sizeof(smsStorageImage_t)) ||
|
||||
(storageImage.crc != computeImageCRC()))
|
||||
{
|
||||
formatStorage();
|
||||
}
|
||||
}
|
||||
|
||||
void smsStorageMarkDirty(void)
|
||||
{
|
||||
storageDirty = true;
|
||||
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
void smsStorageTick(void)
|
||||
{
|
||||
if ((storageDirty == false) || (ticksTimerHasExpired(&debounceTimer) == false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Writing takes ~100ms (2 sector erases + programming), so keep it away
|
||||
// from live DMR traffic and in-flight SMS work
|
||||
if (trxTransmissionEnabled || trxIsTransmitting ||
|
||||
((trxGetMode() == RADIO_MODE_DIGITAL) && (slotState != DMR_STATE_IDLE)) ||
|
||||
(smsCoreIsIdle() == false))
|
||||
{
|
||||
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
storageImage.writeCounter++;
|
||||
storageImage.crc = computeImageCRC();
|
||||
|
||||
if (SPI_Flash_write(SMS_STORAGE_FLASH_ADDRESS, (uint8_t *)&storageImage, sizeof(storageImage)))
|
||||
{
|
||||
storageDirty = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ticksTimerStart(&debounceTimer, SMS_STORAGE_DEBOUNCE_MS); // retry later
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Generic record-list helpers shared by inbox and sent box
|
||||
//
|
||||
static bool recordListAdd(smsRecord_t *records, uint8_t slots, uint8_t *count, uint32_t peerId, const char *text, uint8_t flags)
|
||||
{
|
||||
if ((text == NULL) || (text[0] == 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// shift down, newest lives at index 0
|
||||
uint8_t used = *count;
|
||||
|
||||
if (used > (slots - 1))
|
||||
{
|
||||
used = slots - 1;
|
||||
}
|
||||
|
||||
memmove(&records[1], &records[0], used * sizeof(smsRecord_t));
|
||||
|
||||
records[0].peerId = peerId;
|
||||
records[0].flags = flags;
|
||||
strncpy(records[0].text, text, SMS_MAX_TEXT_LENGTH);
|
||||
records[0].text[SMS_MAX_TEXT_LENGTH] = 0;
|
||||
|
||||
if (*count < slots)
|
||||
{
|
||||
(*count)++;
|
||||
}
|
||||
|
||||
smsStorageMarkDirty();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool recordListDelete(smsRecord_t *records, uint8_t *count, uint8_t index)
|
||||
{
|
||||
if (index >= *count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memmove(&records[index], &records[index + 1], (*count - index - 1) * sizeof(smsRecord_t));
|
||||
(*count)--;
|
||||
memset(&records[*count], 0, sizeof(smsRecord_t));
|
||||
|
||||
smsStorageMarkDirty();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t smsInboxCount(void)
|
||||
{
|
||||
return storageImage.inboxCount;
|
||||
}
|
||||
|
||||
const smsRecord_t *smsInboxGet(uint8_t index)
|
||||
{
|
||||
return ((index < storageImage.inboxCount) ? &storageImage.inbox[index] : NULL);
|
||||
}
|
||||
|
||||
bool smsInboxAdd(uint32_t srcId, const char *text)
|
||||
{
|
||||
return recordListAdd(storageImage.inbox, SMS_INBOX_SLOTS, &storageImage.inboxCount, srcId, text, SMS_RECORD_FLAG_UNREAD);
|
||||
}
|
||||
|
||||
bool smsInboxDelete(uint8_t index)
|
||||
{
|
||||
return recordListDelete(storageImage.inbox, &storageImage.inboxCount, index);
|
||||
}
|
||||
|
||||
void smsInboxMarkRead(uint8_t index)
|
||||
{
|
||||
if ((index < storageImage.inboxCount) && (storageImage.inbox[index].flags & SMS_RECORD_FLAG_UNREAD))
|
||||
{
|
||||
storageImage.inbox[index].flags &= ~SMS_RECORD_FLAG_UNREAD;
|
||||
smsStorageMarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t smsInboxUnreadCount(void)
|
||||
{
|
||||
uint8_t unread = 0;
|
||||
|
||||
for (uint8_t i = 0; i < storageImage.inboxCount; i++)
|
||||
{
|
||||
if (storageImage.inbox[i].flags & SMS_RECORD_FLAG_UNREAD)
|
||||
{
|
||||
unread++;
|
||||
}
|
||||
}
|
||||
|
||||
return unread;
|
||||
}
|
||||
|
||||
uint8_t smsSentCount(void)
|
||||
{
|
||||
return storageImage.sentCount;
|
||||
}
|
||||
|
||||
const smsRecord_t *smsSentGet(uint8_t index)
|
||||
{
|
||||
return ((index < storageImage.sentCount) ? &storageImage.sent[index] : NULL);
|
||||
}
|
||||
|
||||
bool smsSentAdd(uint32_t dstId, const char *text)
|
||||
{
|
||||
return recordListAdd(storageImage.sent, SMS_SENT_SLOTS, &storageImage.sentCount, dstId, text, 0);
|
||||
}
|
||||
|
||||
bool smsSentDelete(uint8_t index)
|
||||
{
|
||||
return recordListDelete(storageImage.sent, &storageImage.sentCount, index);
|
||||
}
|
||||
|
||||
uint8_t smsQuickTextCount(void)
|
||||
{
|
||||
return storageImage.quickTextCount;
|
||||
}
|
||||
|
||||
const char *smsQuickTextGet(uint8_t index)
|
||||
{
|
||||
return ((index < storageImage.quickTextCount) ? storageImage.quickTexts[index] : NULL);
|
||||
}
|
||||
|
||||
bool smsQuickTextSet(uint8_t index, const char *text)
|
||||
{
|
||||
if ((text == NULL) || (text[0] == 0) || (index >= SMS_QUICKTEXT_SLOTS) || (index > storageImage.quickTextCount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
strncpy(storageImage.quickTexts[index], text, SMS_MAX_TEXT_LENGTH);
|
||||
storageImage.quickTexts[index][SMS_MAX_TEXT_LENGTH] = 0;
|
||||
|
||||
if (index == storageImage.quickTextCount)
|
||||
{
|
||||
storageImage.quickTextCount++;
|
||||
}
|
||||
|
||||
smsStorageMarkDirty();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool smsQuickTextDelete(uint8_t index)
|
||||
{
|
||||
if (index >= storageImage.quickTextCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
memmove(&storageImage.quickTexts[index], &storageImage.quickTexts[index + 1],
|
||||
(storageImage.quickTextCount - index - 1) * sizeof(storageImage.quickTexts[0]));
|
||||
storageImage.quickTextCount--;
|
||||
memset(&storageImage.quickTexts[storageImage.quickTextCount], 0, sizeof(storageImage.quickTexts[0]));
|
||||
|
||||
smsStorageMarkDirty();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t smsStorageGetOptions(void)
|
||||
{
|
||||
return storageImage.options;
|
||||
}
|
||||
|
||||
void smsStorageSetOptions(uint8_t options)
|
||||
{
|
||||
if (storageImage.options != options)
|
||||
{
|
||||
storageImage.options = options;
|
||||
smsStorageMarkDirty();
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +98,14 @@ const int16_t MELODY_PRIVATE_CALL[] = {
|
|||
880, DIT_LENGTH
|
||||
|
||||
-1, -1 };// Morse letters PC for Private Call
|
||||
// Rising two-tone chime, distinct from the private call morse alert
|
||||
const int16_t MELODY_SMS_RX[] = {
|
||||
1047, 80,
|
||||
0, 40,
|
||||
1319, 80,
|
||||
0, 40,
|
||||
1568, 160,
|
||||
-1, -1 };
|
||||
const int16_t MELODY_KEY_BEEP[] = { 600, 60, -1, -1 };
|
||||
const int16_t MELODY_KEY_LONG_BEEP[] = { 880, 60, -1, -1 };
|
||||
/* These melodies are not currently used
|
||||
|
|
|
|||
|
|
@ -352,13 +352,25 @@ void voicePromptsAppendLanguageString(const char *languageStringAdd)
|
|||
return;
|
||||
}
|
||||
|
||||
voicePromptsAppendPrompt(NUM_VOICE_PROMPTS +
|
||||
int promptNumber = NUM_VOICE_PROMPTS +
|
||||
((languageStringAdd - currentLanguage->LANGUAGE_NAME)
|
||||
#if ! defined(HAS_COLOURS)
|
||||
- ((languageStringAdd >= currentLanguage->theme_chooser) ?
|
||||
((currentLanguage->theme_colour_picker_blue - currentLanguage->theme_chooser) + LANGUAGE_TEXTS_LENGTH) : 0)
|
||||
#endif
|
||||
) / LANGUAGE_TEXTS_LENGTH);
|
||||
) / LANGUAGE_TEXTS_LENGTH;
|
||||
|
||||
// Language strings added after the voice prompt pack was built have no
|
||||
// entry in the table of contents. voicePromptsPlay() reads TOC[promptNumber + 1],
|
||||
// so anything at or past the last slot would read past the array and crash;
|
||||
// play silence for those instead.
|
||||
if ((promptNumber < 0) || (promptNumber >= (VOICE_PROMPTS_TOC_SIZE - 1)))
|
||||
{
|
||||
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
||||
return;
|
||||
}
|
||||
|
||||
voicePromptsAppendPrompt(promptNumber);
|
||||
}
|
||||
|
||||
void voicePromptsPlay(void)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#include "interfaces/interrupts.h"
|
||||
#include "functions/rxPowerSaving.h"
|
||||
#include "functions/ticks.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "interfaces/gps.h"
|
||||
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
||||
#include "hardware/radioHardwareInterface.h"
|
||||
|
|
@ -253,6 +254,9 @@ static struct
|
|||
int hotspotPostponedFrameHandling;
|
||||
char talkAliasText[33];
|
||||
uint8_t talkAliasLocation[7];
|
||||
volatile bool smsActive; // an SMS air job is being streamed instead of voice
|
||||
volatile uint8_t smsFrameIndex;
|
||||
const smsAirJob_t *volatile smsJob;
|
||||
} hrc = {
|
||||
.hasEncodedAudio = false,
|
||||
.hasAudioData = false,
|
||||
|
|
@ -299,7 +303,10 @@ static struct
|
|||
.wakeTriesCount = 0,
|
||||
.hotspotPostponedFrameHandling = 0,
|
||||
.talkAliasText = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
|
||||
.talkAliasLocation = { 0, 0, 0, 0, 0, 0, 0}
|
||||
.talkAliasLocation = { 0, 0, 0, 0, 0, 0, 0},
|
||||
.smsActive = false,
|
||||
.smsFrameIndex = 0,
|
||||
.smsJob = NULL
|
||||
};
|
||||
|
||||
volatile int slotState = DMR_STATE_IDLE;
|
||||
|
|
@ -826,6 +833,14 @@ void PORTC_IRQHandler(void)
|
|||
#endif
|
||||
static inline void hrc6000SysSendRejectedInt(void)
|
||||
{
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
// The channel became busy before our data went out
|
||||
hrc.smsActive = false;
|
||||
hrc.transmissionEnabled = false;
|
||||
trxTransmissionEnabled = false;
|
||||
smsTxNotifyRejected();
|
||||
}
|
||||
}
|
||||
|
||||
static inline void hrc6000SysSendStartInt(void)
|
||||
|
|
@ -895,6 +910,11 @@ static inline void hrc6000SysSendEndInt(void)
|
|||
|
||||
In MSK mode, there is no substate interrupt.
|
||||
*/
|
||||
|
||||
if (reg_0x86 & 0x08) // confirmed-data feedback packet was received
|
||||
{
|
||||
smsTxNotifyHwAck();
|
||||
}
|
||||
}
|
||||
|
||||
static inline void hrc6000SysPostAccessInt(void)
|
||||
|
|
@ -1040,6 +1060,24 @@ static inline void hrc6000SysReceivedDataInt(void)
|
|||
hrc.skipCount--;
|
||||
}
|
||||
|
||||
// DMR short data (SMS): data header, rate-1/2 and rate-3/4 data bursts.
|
||||
// The chip has already BPTC/FEC decoded the burst; the logical payload
|
||||
// is read from the LC RAM (rate-3/4 blocks carry 18 bytes, everything
|
||||
// else 12).
|
||||
if (hrc6000CrcIsValid() && (rxPrivacyIndicator == 0) && (rxSyncClass == SYNC_CLASS_DATA) &&
|
||||
((rxDataType == DMR_DATA_TYPE_DATA_HEADER) || (rxDataType == DMR_DATA_TYPE_RATE_12_DATA) || (rxDataType == DMR_DATA_TYPE_RATE_34_DATA)) &&
|
||||
(settingsUsbMode != USB_MODE_HOTSPOT) && (hrc.transmissionEnabled == false) &&
|
||||
hrc6000CheckColourCodeFilter())
|
||||
{
|
||||
uint8_t smsFrame[DMR_DATA_RATE34_BLOCK_LENGTH];
|
||||
uint8_t burstLength = ((rxDataType == DMR_DATA_TYPE_RATE_34_DATA) ? DMR_DATA_RATE34_BLOCK_LENGTH : LC_DATA_LENGTH);
|
||||
|
||||
if (SPI0ReadPageRegByteArray(0x02, 0x00, smsFrame, burstLength) == kStatus_Success)
|
||||
{
|
||||
smsRxProcessDataFrame(rxDataType, smsFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for correct received packet
|
||||
if (hrc6000CrcIsValid() && (rxPrivacyIndicator == 0) && (slotState < DMR_STATE_TX_START_1))
|
||||
{
|
||||
|
|
@ -1470,6 +1508,48 @@ static void hrc6000TransitionToTx(void)
|
|||
hrc.txSequence = 0;
|
||||
}
|
||||
|
||||
// Queue the next SMS burst for the upcoming timeslot: 12-byte logical
|
||||
// payload into the LC RAM, its DMR data type into register 0x50. The chip
|
||||
// performs the BPTC/FEC encoding. Once the last frame is queued the
|
||||
// transmission is ended the same way as a released PTT.
|
||||
static void hrc6000SendSmsFrame(void)
|
||||
{
|
||||
const smsAirJob_t *job = hrc.smsJob;
|
||||
uint8_t dataType;
|
||||
|
||||
if ((job == NULL) || (hrc.smsFrameIndex >= job->frameCount))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// register 0x50 value: high nibble = DMR data type, low bits per datasheet
|
||||
if (hrc.smsFrameIndex < job->preambleCount)
|
||||
{
|
||||
dataType = 0x30; // CSBK
|
||||
}
|
||||
else if (hrc.smsFrameIndex == job->preambleCount)
|
||||
{
|
||||
dataType = 0x64; // data header
|
||||
}
|
||||
else
|
||||
{
|
||||
dataType = 0x70; // rate-1/2 data
|
||||
}
|
||||
|
||||
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t *)job->frames[hrc.smsFrameIndex], LC_DATA_LENGTH);
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, dataType);
|
||||
|
||||
hrc.smsFrameIndex++;
|
||||
|
||||
if (hrc.smsFrameIndex >= job->frameCount)
|
||||
{
|
||||
// all bursts are queued, terminate (the terminator frame is sent by the TX_END path)
|
||||
hrc.transmissionEnabled = false;
|
||||
trxTransmissionEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
//used as a delayed callback to allow time for the final Tx burst and then simulate the final Rx Interrupt and return to receive.
|
||||
void hrc6000TxBurstCallback(void)
|
||||
{
|
||||
|
|
@ -1712,9 +1792,16 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
|
||||
case DMR_STATE_TX_START_1: // Start TX (second step)
|
||||
LedWrite(LED_RED, 1); // for repeater wakeup
|
||||
hrc6000SendPcOrTgLCHeader();
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
hrc6000SendSmsFrame();
|
||||
}
|
||||
else
|
||||
{
|
||||
hrc6000SendPcOrTgLCHeader();
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
}
|
||||
trxIsTransmitting = true;
|
||||
slotState = DMR_STATE_TX_START_2;
|
||||
break;
|
||||
|
|
@ -1725,8 +1812,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
break;
|
||||
|
||||
case DMR_STATE_TX_START_3: // Start TX (fourth step)
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
hrc6000SendSmsFrame();
|
||||
}
|
||||
else
|
||||
{
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
}
|
||||
slotState = DMR_STATE_TX_START_4;
|
||||
break;
|
||||
|
||||
|
|
@ -1734,7 +1828,7 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
SPI0WritePageRegByte(0x04, 0x41, 0x00); // Do nothing on the next TS
|
||||
slotState = DMR_STATE_TX_START_5;
|
||||
|
||||
if (settingsUsbMode != USB_MODE_HOTSPOT)
|
||||
if ((settingsUsbMode != USB_MODE_HOTSPOT) && (hrc.smsActive == false))
|
||||
{
|
||||
hrc.ambeBufferCount = 0;
|
||||
hrc.deferredUpdateBufferOutPtr = deferredUpdateBuffer;
|
||||
|
|
@ -1744,8 +1838,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
break;
|
||||
|
||||
case DMR_STATE_TX_START_5: // Start TX (sixth step)
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
hrc6000SendSmsFrame();
|
||||
}
|
||||
else
|
||||
{
|
||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
||||
}
|
||||
hrc.TAPhase = 0;
|
||||
slotState = DMR_STATE_TX_1;
|
||||
break;
|
||||
|
|
@ -1763,6 +1864,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
break;
|
||||
|
||||
case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
// One data burst per active timeslot; no audio involved, and
|
||||
// txSequence stays at 0 so DMR_STATE_TX_1 can terminate cleanly
|
||||
hrc6000SendSmsFrame();
|
||||
slotState = DMR_STATE_TX_1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hrc.transmissionEnabled)
|
||||
{
|
||||
if (settingsUsbMode == USB_MODE_HOTSPOT)
|
||||
|
|
@ -1826,7 +1936,9 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
break;
|
||||
|
||||
case DMR_STATE_TX_END_1: // Stop TX (first step)
|
||||
if (getCurrentTATxFlag() != TA_TX_OFF)
|
||||
// After an SMS the LC RAM still holds the last data block, so a
|
||||
// valid LC has to be rewritten for the terminator frame
|
||||
if (hrc.smsActive || (getCurrentTATxFlag() != TA_TX_OFF))
|
||||
{
|
||||
hrc6000SendPcOrTgLCHeader();
|
||||
}
|
||||
|
|
@ -1837,6 +1949,13 @@ void hrc6000TimeslotInterruptHandler(void)
|
|||
break;
|
||||
|
||||
case DMR_STATE_TX_END_2: // Stop TX (second step)
|
||||
if (hrc.smsActive)
|
||||
{
|
||||
hrc.smsActive = false;
|
||||
hrc.smsJob = NULL;
|
||||
LedWrite(LED_RED, 0); // no TX screen is handling the LED for SMS
|
||||
smsTxNotifyAirDone();
|
||||
}
|
||||
// Need to hold on this TS after Tx ends otherwise if DMR Mon TS filtering is disabled the radio may switch timeslot
|
||||
dmrMonitorCapturedTS = hrc.tsLockedTS = trxGetDMRTimeSlot();
|
||||
hrc.dmrMonitorCapturedTimeout = nonVolatileSettings.dmrCaptureTimeout * 1000;
|
||||
|
|
@ -2017,6 +2136,9 @@ static void hrc6000InitDigitalState(void)
|
|||
hrc.interruptTimeout = 0;
|
||||
slotState = DMR_STATE_IDLE;
|
||||
trxIsTransmitting = false;
|
||||
hrc.smsActive = false;
|
||||
hrc.smsJob = NULL;
|
||||
hrc.smsFrameIndex = 0;
|
||||
hrc.tickCount = 0;
|
||||
hrc.skipCount = 0;
|
||||
hrc.tsAgreed = 0;
|
||||
|
|
@ -2761,6 +2883,49 @@ bool HRC6000IRQHandlerIsRunning(void)
|
|||
return hrc.inIRQHandler;
|
||||
}
|
||||
|
||||
// Start streaming the prepared SMS air job. The regular DMR TX machinery
|
||||
// (including the repeater wake sequence in RMO) is reused; only the burst
|
||||
// contents differ from a voice call.
|
||||
bool HRC6000StartSmsTransmission(void)
|
||||
{
|
||||
const smsAirJob_t *job = smsGetPendingAirJob();
|
||||
|
||||
if ((job == NULL) || (job->frameCount == 0) ||
|
||||
trxTransmissionEnabled || trxIsTransmitting ||
|
||||
(trxGetMode() != RADIO_MODE_DIGITAL) ||
|
||||
(settingsUsbMode == USB_MODE_HOTSPOT) ||
|
||||
(slotState != DMR_STATE_IDLE))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hrc.isWaking == WAKING_MODE_FAILED)
|
||||
{
|
||||
hrc.isWaking = WAKING_MODE_NONE; // recover from an earlier failed repeater wake
|
||||
}
|
||||
|
||||
hrc.smsJob = job;
|
||||
hrc.smsFrameIndex = 0;
|
||||
hrc.smsActive = true;
|
||||
smsAirJobStarted();
|
||||
|
||||
trxEnableTransmission();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HRC6000IsSendingSMS(void)
|
||||
{
|
||||
return hrc.smsActive;
|
||||
}
|
||||
|
||||
void HRC6000CancelSmsTransmission(void)
|
||||
{
|
||||
hrc.smsActive = false;
|
||||
hrc.smsJob = NULL;
|
||||
hrc.smsFrameIndex = 0;
|
||||
}
|
||||
|
||||
bool HRC6000HasGotSync(void)
|
||||
{
|
||||
return (hrc.timeCode != -1);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1510,28 +1539,210 @@ void themeInitToDefaultValues(DayTime_t daytime, bool invert)
|
|||
themeItems[daytime][THEME_ITEM_FG_BD_COLOUR] = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(0xFF0000U));
|
||||
}
|
||||
|
||||
void themeInit(bool SPIFlashAvailable)
|
||||
// Set every theme item to fgRGB, then override the background items with bgRGB, for
|
||||
// a cohesive single-palette look. The selected-menu-item background is intentionally
|
||||
// left as the foreground colour so the highlighted row still inverts.
|
||||
static void themeSetAllItems(DayTime_t daytime, uint32_t fgRGB, uint32_t bgRGB)
|
||||
{
|
||||
uint16_t fg = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(fgRGB));
|
||||
uint16_t bg = PLATFORM_COLOUR_FORMAT_SWAP_BYTES(RGB888_TO_PLATFORM_COLOUR_FORMAT(bgRGB));
|
||||
|
||||
for (int i = 0; i < THEME_ITEM_MAX; i++)
|
||||
{
|
||||
themeItems[daytime][i] = fg;
|
||||
}
|
||||
themeItems[daytime][THEME_ITEM_BG] = bg;
|
||||
themeItems[daytime][THEME_ITEM_BG_SPLASHSCREEN] = bg;
|
||||
themeItems[daytime][THEME_ITEM_BG_NOTIFICATION] = bg;
|
||||
themeItems[daytime][THEME_ITEM_BG_MENU_NAME] = bg;
|
||||
themeItems[daytime][THEME_ITEM_BG_HEADER_TEXT] = bg;
|
||||
// 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);
|
||||
|
||||
// Read and apply user's theme, if any
|
||||
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)
|
||||
themeSetAllItems(DAY, 0x7C71D5, 0x3E31A2);
|
||||
themeSetAllItems(NIGHT, 0x7C71D5, 0x3E31A2);
|
||||
break;
|
||||
|
||||
case THEME_PRESET_TERMINAL:
|
||||
themeSetAllItems(DAY, 0x000000, 0x00A000); // black on green
|
||||
themeSetAllItems(NIGHT, 0x00E000, 0x000000); // green on black
|
||||
break;
|
||||
|
||||
case THEME_PRESET_VERTEXIC: // orange (Vertex-style)
|
||||
themeSetAllItems(DAY, 0x000000, 0xFF6A00); // black on orange
|
||||
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
|
||||
// only ever touch this).
|
||||
{
|
||||
uint16_t themingTmp[THEME_ITEM_MAX];
|
||||
|
||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY, (uint8_t *) &themingTmp))
|
||||
{
|
||||
memcpy(&themeItems[DAY], &themingTmp, sizeof(themingTmp));
|
||||
}
|
||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT, (uint8_t *) &themingTmp))
|
||||
{
|
||||
memcpy(&themeItems[NIGHT], &themingTmp, sizeof(themingTmp));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
foregroundColour = themeItems[DAY][THEME_ITEM_FG_DEFAULT];
|
||||
backgroundColour = themeItems[DAY][THEME_ITEM_BG];
|
||||
}
|
||||
|
||||
uint8_t themeGetPreset(void)
|
||||
{
|
||||
uint8_t buf[4] = { THEME_PRESET_CUSTOM, 0, 0, 0 };
|
||||
|
||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, buf))
|
||||
{
|
||||
return (buf[0] < NUM_THEME_PRESETS) ? buf[0] : THEME_PRESET_CUSTOM;
|
||||
}
|
||||
return THEME_PRESET_CUSTOM;
|
||||
}
|
||||
|
||||
void themeSetPreset(uint8_t preset, bool save)
|
||||
{
|
||||
if (preset >= NUM_THEME_PRESETS)
|
||||
{
|
||||
preset = THEME_PRESET_CUSTOM;
|
||||
}
|
||||
|
||||
themeApplyPreset(preset);
|
||||
|
||||
if (save)
|
||||
{
|
||||
uint8_t buf[1] = { preset };
|
||||
codeplugSetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, buf, sizeof(buf));
|
||||
}
|
||||
}
|
||||
|
||||
void themeInit(bool SPIFlashAvailable)
|
||||
{
|
||||
if (SPIFlashAvailable)
|
||||
{
|
||||
uint16_t themingTmp[THEME_ITEM_MAX];
|
||||
|
||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY, (uint8_t *) &themingTmp))
|
||||
{
|
||||
memcpy(&themeItems[DAY], &themingTmp, sizeof(themingTmp));
|
||||
|
||||
foregroundColour = themeItems[DAY][THEME_ITEM_FG_DEFAULT];
|
||||
backgroundColour = themeItems[DAY][THEME_ITEM_BG];
|
||||
}
|
||||
|
||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT, (uint8_t *) &themingTmp))
|
||||
{
|
||||
memcpy(&themeItems[NIGHT], &themingTmp, sizeof(themingTmp));
|
||||
}
|
||||
themeApplyPreset(themeGetPreset());
|
||||
}
|
||||
else
|
||||
{
|
||||
themeInitToDefaultValues(NIGHT, true);
|
||||
themeInitToDefaultValues(DAY, false);
|
||||
}
|
||||
|
||||
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT_OVERRIDE) && (uiDataGlobal.daytimeOverridden == NIGHT))
|
||||
|
|
|
|||
|
|
@ -48,9 +48,28 @@ void LEDsInit(void)
|
|||
#endif
|
||||
}
|
||||
|
||||
// When set, the green LED is held off: any LedWrite(LED_GREEN, 1) is forced to 0.
|
||||
// Used while muted so the DMR RX handler (which re-asserts green whenever it reads
|
||||
// it as 0) can't make the green LED flicker between the red mute flashes.
|
||||
static volatile bool greenLedSuppressed = false;
|
||||
|
||||
void LedGreenSuppress(bool suppress)
|
||||
{
|
||||
greenLedSuppressed = suppress;
|
||||
if (suppress)
|
||||
{
|
||||
LedWrite(LED_GREEN, 0); // force it off now
|
||||
}
|
||||
}
|
||||
|
||||
void LedWrite(LEDs_t theLED, uint8_t output)
|
||||
{
|
||||
#if ! defined(PLATFORM_GD77S)
|
||||
if ((theLED == LED_GREEN) && greenLedSuppressed)
|
||||
{
|
||||
output = 0; // green is held off (e.g. while muted)
|
||||
}
|
||||
|
||||
LEDsState[theLED] = output;
|
||||
|
||||
if (settingsIsOptionBitSet(BIT_ALL_LEDS_DISABLED) == 0)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -121,9 +121,6 @@ static void updateScreen(bool isFirstRun)
|
|||
if (menuDataGlobal.currentMenuList[mNum].stringOffset >= 0)
|
||||
{
|
||||
const char *menuName = (currentLanguage->LANGUAGE_NAME + (menuDataGlobal.currentMenuList[mNum].stringOffset * LANGUAGE_TEXTS_LENGTH));
|
||||
#if defined(HAS_COLOURS)
|
||||
menuSetNextEntryIconByMenuNum(menuDataGlobal.currentMenuList[mNum].menuNum);
|
||||
#endif
|
||||
menuDisplayEntry(i, mNum, menuName, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
||||
|
||||
if (i == 0)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ enum
|
|||
DISPLAY_SHOW_DISTANCE,
|
||||
DISPLAY_DMR_LAST_TALKER_ON_SCREEN,
|
||||
DISPLAY_SCREEN_DIM,
|
||||
#if defined(HAS_COLOURS)
|
||||
DISPLAY_THEME_PRESET,
|
||||
#endif
|
||||
NUM_DISPLAY_MENU_ITEMS
|
||||
};
|
||||
|
||||
|
|
@ -269,6 +272,18 @@ static void updateScreen(bool isFirstRun)
|
|||
}
|
||||
break;
|
||||
|
||||
#if defined(HAS_COLOURS)
|
||||
case DISPLAY_THEME_PRESET:
|
||||
{
|
||||
// Preset names are plain literals -> rightSideVar (voice-safe).
|
||||
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]);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
#if ! defined(PLATFORM_GD77S)
|
||||
case DISPLAY_AUTO_NIGHT:
|
||||
leftSide = currentLanguage->auto_night;
|
||||
|
|
@ -630,6 +645,19 @@ static void handleEvent(uiEvent_t *ev)
|
|||
}
|
||||
break;
|
||||
|
||||
#if defined(HAS_COLOURS)
|
||||
case DISPLAY_THEME_PRESET:
|
||||
{
|
||||
uint8_t p = themeGetPreset();
|
||||
if (p < (NUM_THEME_PRESETS - 1))
|
||||
{
|
||||
themeSetPreset(p + 1, true); // apply + persist, live preview
|
||||
displayThemeResetToDefault();
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
#if ! defined(PLATFORM_GD77S)
|
||||
case DISPLAY_AUTO_NIGHT:
|
||||
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
|
||||
|
|
@ -839,6 +867,19 @@ static void handleEvent(uiEvent_t *ev)
|
|||
}
|
||||
break;
|
||||
|
||||
#if defined(HAS_COLOURS)
|
||||
case DISPLAY_THEME_PRESET:
|
||||
{
|
||||
uint8_t p = themeGetPreset();
|
||||
if (p > THEME_PRESET_CUSTOM)
|
||||
{
|
||||
themeSetPreset(p - 1, true); // apply + persist, live preview
|
||||
displayThemeResetToDefault();
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
|
||||
#if ! defined(PLATFORM_GD77S)
|
||||
case DISPLAY_AUTO_NIGHT:
|
||||
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,364 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include "user_interface/uiGlobals.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "io/keyboard.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
#define DEST_DIGITS_MAX 8 // 16777215 is the largest 24-bit DMR ID
|
||||
#define TEXT_CHARS_PER_LINE (DISPLAY_SIZE_X / 8)
|
||||
#define TEXT_VISIBLE_LINES 5
|
||||
#define TEXT_AREA_Y (FONT_SIZE_3_HEIGHT * 2)
|
||||
|
||||
enum { FIELD_DESTINATION = 0, FIELD_TEXT };
|
||||
|
||||
static char composeText[SMS_MAX_TEXT_LENGTH + 1];
|
||||
static char destDigits[DEST_DIGITS_MAX + 1];
|
||||
static int textPos = 0;
|
||||
static int focusedField = FIELD_TEXT;
|
||||
static int quickTextEditSlot = -1;
|
||||
|
||||
// prefill is written straight into the compose buffers, consumed on first run
|
||||
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);
|
||||
static void handleEvent(uiEvent_t *ev);
|
||||
|
||||
void menuSMSComposePrefill(uint32_t dstId, const char *text)
|
||||
{
|
||||
destDigits[0] = 0;
|
||||
composeText[0] = 0;
|
||||
|
||||
if (dstId != 0)
|
||||
{
|
||||
snprintf(destDigits, sizeof(destDigits), "%u", (unsigned int)dstId);
|
||||
}
|
||||
|
||||
if (text != NULL)
|
||||
{
|
||||
strncpy(composeText, text, SMS_MAX_TEXT_LENGTH);
|
||||
composeText[SMS_MAX_TEXT_LENGTH] = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
menuStatus_t menuSMSCompose(uiEvent_t *ev, bool isFirstRun)
|
||||
{
|
||||
if (isFirstRun)
|
||||
{
|
||||
quickTextEditSlot = pendingQuickTextEditSlot;
|
||||
pendingQuickTextEditSlot = -1;
|
||||
|
||||
if (quickTextEditSlot >= 0)
|
||||
{
|
||||
const char *quickText = smsQuickTextGet(quickTextEditSlot);
|
||||
|
||||
composeText[0] = 0;
|
||||
destDigits[0] = 0;
|
||||
|
||||
if (quickText != NULL)
|
||||
{
|
||||
strncpy(composeText, quickText, SMS_MAX_TEXT_LENGTH);
|
||||
composeText[SMS_MAX_TEXT_LENGTH] = 0;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
composeText[0] = 0;
|
||||
destDigits[0] = 0;
|
||||
}
|
||||
|
||||
focusedField = ((destDigits[0] == 0) ? FIELD_DESTINATION : FIELD_TEXT);
|
||||
}
|
||||
|
||||
textPos = strlen(composeText);
|
||||
prefillPending = false;
|
||||
updateScreen();
|
||||
|
||||
return (MENU_STATUS_INPUT_TYPE | MENU_STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
menuSMSComposeExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleEvent(ev);
|
||||
}
|
||||
|
||||
return menuSMSComposeExitCode;
|
||||
}
|
||||
|
||||
static void updateScreen(void)
|
||||
{
|
||||
char lineBuffer[TEXT_CHARS_PER_LINE + 1];
|
||||
|
||||
displayClearBuf();
|
||||
menuDisplayTitle((quickTextEditSlot >= 0) ? currentLanguage->quick_text : currentLanguage->new_message);
|
||||
|
||||
keypadAlphaEnable = (focusedField == FIELD_TEXT);
|
||||
|
||||
// destination line (hidden when editing a quick text template)
|
||||
if (quickTextEditSlot < 0)
|
||||
{
|
||||
snprintf(lineBuffer, sizeof(lineBuffer), "%s:%s%s",
|
||||
currentLanguage->send_to, destDigits,
|
||||
((focusedField == FIELD_DESTINATION) ? "_" : ""));
|
||||
displayPrintAt(0, FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
|
||||
}
|
||||
|
||||
// text area with a scrolling window that follows the cursor
|
||||
int textLength = strlen(composeText);
|
||||
int cursorLine = textPos / TEXT_CHARS_PER_LINE;
|
||||
int firstLine = ((cursorLine >= TEXT_VISIBLE_LINES) ? (cursorLine - TEXT_VISIBLE_LINES + 1) : 0);
|
||||
|
||||
for (int line = 0; line < TEXT_VISIBLE_LINES; line++)
|
||||
{
|
||||
int offset = (firstLine + line) * TEXT_CHARS_PER_LINE;
|
||||
|
||||
if (offset > textLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int chunk = textLength - offset;
|
||||
|
||||
if (chunk > TEXT_CHARS_PER_LINE)
|
||||
{
|
||||
chunk = TEXT_CHARS_PER_LINE;
|
||||
}
|
||||
|
||||
memcpy(lineBuffer, &composeText[offset], chunk);
|
||||
lineBuffer[chunk] = 0;
|
||||
displayPrintAt(0, TEXT_AREA_Y + (line * FONT_SIZE_3_HEIGHT), lineBuffer, FONT_SIZE_3);
|
||||
}
|
||||
|
||||
if (focusedField == FIELD_TEXT)
|
||||
{
|
||||
int cursorX = (textPos % TEXT_CHARS_PER_LINE) * 8;
|
||||
int cursorY = TEXT_AREA_Y + ((cursorLine - firstLine + 1) * FONT_SIZE_3_HEIGHT) - 2;
|
||||
|
||||
displayDrawFastHLine(cursorX, cursorY, 8, true);
|
||||
}
|
||||
|
||||
// footer: remaining characters
|
||||
snprintf(lineBuffer, sizeof(lineBuffer), "%d", SMS_MAX_TEXT_LENGTH - textLength);
|
||||
displayPrintAt(DISPLAY_SIZE_X - (strlen(lineBuffer) * 8), DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static void sendOrSave(void)
|
||||
{
|
||||
if (quickTextEditSlot >= 0)
|
||||
{
|
||||
if (composeText[0] != 0)
|
||||
{
|
||||
smsQuickTextSet(quickTextEditSlot, composeText);
|
||||
keypadAlphaEnable = false;
|
||||
menuSystemPopPreviousMenu();
|
||||
}
|
||||
else
|
||||
{
|
||||
menuSMSComposeExitCode |= MENU_STATUS_ERROR;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t dstId = (uint32_t)strtoul(destDigits, NULL, 10);
|
||||
|
||||
if ((dstId == 0) || (composeText[0] == 0))
|
||||
{
|
||||
menuSMSComposeExitCode |= MENU_STATUS_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
if (smsSendMessage(dstId, composeText))
|
||||
{
|
||||
keypadAlphaEnable = false;
|
||||
menuSystemPopAllAndDisplayRootMenu(); // progress is reported via notifications
|
||||
}
|
||||
else
|
||||
{
|
||||
menuSMSComposeExitCode |= MENU_STATUS_ERROR; // busy or not in DMR mode
|
||||
}
|
||||
}
|
||||
|
||||
static void handleEvent(uiEvent_t *ev)
|
||||
{
|
||||
if (ev->events & KEY_EVENT)
|
||||
{
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
||||
{
|
||||
keypadAlphaEnable = false;
|
||||
menuSystemPopPreviousMenu();
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
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))
|
||||
{
|
||||
focusedField = FIELD_DESTINATION;
|
||||
updateScreen();
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
||||
{
|
||||
focusedField = FIELD_TEXT;
|
||||
updateScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedField == FIELD_DESTINATION)
|
||||
{
|
||||
if (KEYCHECK_PRESS(ev->keys, KEY_LEFT))
|
||||
{
|
||||
int len = strlen(destDigits);
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
destDigits[len - 1] = 0;
|
||||
}
|
||||
|
||||
updateScreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
int keyval = menuGetKeypadKeyValue(ev, true);
|
||||
|
||||
if ((keyval != 99) && (strlen(destDigits) < DEST_DIGITS_MAX))
|
||||
{
|
||||
char digit[2] = { (char)('0' + keyval), 0 };
|
||||
|
||||
strcat(destDigits, digit);
|
||||
updateScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
else // FIELD_TEXT
|
||||
{
|
||||
if (KEYCHECK_PRESS(ev->keys, KEY_LEFT))
|
||||
{
|
||||
moveCursorLeftInString(composeText, &textPos, BUTTONCHECK_DOWN(ev, BUTTON_SK2));
|
||||
updateScreen();
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_RIGHT))
|
||||
{
|
||||
moveCursorRightInString(composeText, &textPos, SMS_MAX_TEXT_LENGTH, BUTTONCHECK_DOWN(ev, BUTTON_SK2));
|
||||
updateScreen();
|
||||
}
|
||||
else if ((ev->keys.event & (KEY_MOD_PREVIEW | KEY_MOD_PRESS)) && (ev->keys.key >= 32) && (ev->keys.key <= 126))
|
||||
{
|
||||
if (textPos < SMS_MAX_TEXT_LENGTH)
|
||||
{
|
||||
bool atEnd = (composeText[textPos] == 0);
|
||||
|
||||
composeText[textPos] = ev->keys.key;
|
||||
|
||||
if (atEnd)
|
||||
{
|
||||
composeText[textPos + 1] = 0;
|
||||
}
|
||||
|
||||
if ((ev->keys.event & KEY_MOD_PRESS) && (textPos < (SMS_MAX_TEXT_LENGTH - 1)))
|
||||
{
|
||||
textPos++;
|
||||
}
|
||||
|
||||
updateScreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
366
MDUV380_firmware/application/source/user_interface/menuSMSList.c
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Message list (inbox and sent box) and single-message view.
|
||||
* One implementation serves MENU_SMS_INBOX, MENU_SMS_SENT and MENU_SMS_VIEW.
|
||||
*/
|
||||
#include "user_interface/uiGlobals.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
#define TEXT_CHARS_PER_LINE (DISPLAY_SIZE_X / 8)
|
||||
#define VIEW_TEXT_LINES ((DISPLAY_SIZE_Y / FONT_SIZE_3_HEIGHT) - 2) // minus title and sender line
|
||||
|
||||
static bool viewIsSentBox = false;
|
||||
static uint8_t viewIndex = 0;
|
||||
static int viewScrollLine = 0;
|
||||
|
||||
static menuStatus_t menuSMSListExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
static void updateListScreen(bool isSentBox);
|
||||
static void updateViewScreen(void);
|
||||
static void handleListEvent(uiEvent_t *ev, bool isSentBox);
|
||||
static void handleViewEvent(uiEvent_t *ev);
|
||||
static bool deleteMessageCallback(void);
|
||||
|
||||
void menuSMSListSetViewTarget(bool sentBox, uint8_t recordIndex)
|
||||
{
|
||||
viewIsSentBox = sentBox;
|
||||
viewIndex = recordIndex;
|
||||
viewScrollLine = 0;
|
||||
}
|
||||
|
||||
static const smsRecord_t *getRecord(bool isSentBox, uint8_t index)
|
||||
{
|
||||
return (isSentBox ? smsSentGet(index) : smsInboxGet(index));
|
||||
}
|
||||
|
||||
static uint8_t getRecordCount(bool isSentBox)
|
||||
{
|
||||
return (isSentBox ? smsSentCount() : smsInboxCount());
|
||||
}
|
||||
|
||||
static void peerNameLookup(uint32_t peerId, char *buffer, size_t bufferLen)
|
||||
{
|
||||
char nameBuf[SCREEN_LINE_BUFFER_SIZE];
|
||||
|
||||
if (contactIDLookup(peerId, CONTACT_CALLTYPE_PC, nameBuf) == false)
|
||||
{
|
||||
dmrIdDataStruct_t record;
|
||||
|
||||
if (dmrIDLookup(peerId, &record))
|
||||
{
|
||||
strncpy(nameBuf, record.text, sizeof(nameBuf) - 1);
|
||||
nameBuf[sizeof(nameBuf) - 1] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(nameBuf, sizeof(nameBuf), "%u", (unsigned int)peerId);
|
||||
}
|
||||
}
|
||||
|
||||
// only keep the callsign / first word for list display
|
||||
char *space = strchr(nameBuf, ' ');
|
||||
|
||||
if (space != NULL)
|
||||
{
|
||||
*space = 0;
|
||||
}
|
||||
|
||||
strncpy(buffer, nameBuf, bufferLen - 1);
|
||||
buffer[bufferLen - 1] = 0;
|
||||
}
|
||||
|
||||
menuStatus_t menuSMSList(uiEvent_t *ev, bool isFirstRun)
|
||||
{
|
||||
int currentMenu = menuSystemGetCurrentMenuNumber();
|
||||
bool isView = (currentMenu == MENU_SMS_VIEW);
|
||||
bool isSentBox = ((currentMenu == MENU_SMS_SENT) || (isView && viewIsSentBox));
|
||||
|
||||
if (isFirstRun)
|
||||
{
|
||||
if (isView)
|
||||
{
|
||||
if (viewIndex >= getRecordCount(viewIsSentBox))
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
return MENU_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
if (viewIsSentBox == false)
|
||||
{
|
||||
smsInboxMarkRead(viewIndex);
|
||||
}
|
||||
|
||||
updateViewScreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
menuDataGlobal.numItems = getRecordCount(isSentBox);
|
||||
menuDataGlobal.currentItemIndex = 0;
|
||||
updateListScreen(isSentBox);
|
||||
}
|
||||
|
||||
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
menuSMSListExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
if (isView)
|
||||
{
|
||||
// the record can vanish underneath us (deleted via the confirm box)
|
||||
if (viewIndex >= getRecordCount(viewIsSentBox))
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
return MENU_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleViewEvent(ev);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleListEvent(ev, isSentBox);
|
||||
}
|
||||
}
|
||||
|
||||
return menuSMSListExitCode;
|
||||
}
|
||||
|
||||
static void updateListScreen(bool isSentBox)
|
||||
{
|
||||
char entryBuffer[SCREEN_LINE_BUFFER_SIZE * 2];
|
||||
char nameBuffer[SCREEN_LINE_BUFFER_SIZE];
|
||||
int mNum;
|
||||
|
||||
displayClearBuf();
|
||||
menuDisplayTitle(isSentBox ? currentLanguage->sent_box : currentLanguage->inbox);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const smsRecord_t *record = getRecord(isSentBox, (uint8_t)mNum);
|
||||
|
||||
if (record == NULL)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
peerNameLookup(record->peerId, nameBuffer, sizeof(nameBuffer));
|
||||
snprintf(entryBuffer, sizeof(entryBuffer), "%s%s:%s",
|
||||
((record->flags & SMS_RECORD_FLAG_UNREAD) ? "*" : ""), nameBuffer, record->text);
|
||||
|
||||
menuDisplayEntry(i, mNum, entryBuffer, 0, THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
||||
}
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static void updateViewScreen(void)
|
||||
{
|
||||
char lineBuffer[TEXT_CHARS_PER_LINE + 1];
|
||||
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
||||
|
||||
if (record == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
displayClearBuf();
|
||||
|
||||
peerNameLookup(record->peerId, lineBuffer, sizeof(lineBuffer));
|
||||
menuDisplayTitle(lineBuffer);
|
||||
|
||||
snprintf(lineBuffer, sizeof(lineBuffer), "%s%u", (viewIsSentBox ? ">" : "<"), (unsigned int)record->peerId);
|
||||
displayPrintAt(0, FONT_SIZE_3_HEIGHT, lineBuffer, FONT_SIZE_3);
|
||||
|
||||
int textLength = strlen(record->text);
|
||||
int totalLines = (textLength + TEXT_CHARS_PER_LINE - 1) / TEXT_CHARS_PER_LINE;
|
||||
|
||||
if (viewScrollLine > (totalLines - VIEW_TEXT_LINES))
|
||||
{
|
||||
viewScrollLine = totalLines - VIEW_TEXT_LINES;
|
||||
}
|
||||
|
||||
if (viewScrollLine < 0)
|
||||
{
|
||||
viewScrollLine = 0;
|
||||
}
|
||||
|
||||
for (int line = 0; line < VIEW_TEXT_LINES; line++)
|
||||
{
|
||||
int offset = (viewScrollLine + line) * TEXT_CHARS_PER_LINE;
|
||||
|
||||
if (offset >= textLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int chunk = textLength - offset;
|
||||
|
||||
if (chunk > TEXT_CHARS_PER_LINE)
|
||||
{
|
||||
chunk = TEXT_CHARS_PER_LINE;
|
||||
}
|
||||
|
||||
memcpy(lineBuffer, &record->text[offset], chunk);
|
||||
lineBuffer[chunk] = 0;
|
||||
displayPrintAt(0, (FONT_SIZE_3_HEIGHT * 2) + (line * FONT_SIZE_3_HEIGHT), lineBuffer, FONT_SIZE_3);
|
||||
}
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static void handleListEvent(uiEvent_t *ev, bool isSentBox)
|
||||
{
|
||||
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)
|
||||
{
|
||||
menuSMSListSetViewTarget(isSentBox, (uint8_t)menuDataGlobal.currentItemIndex);
|
||||
menuSystemPushNewMenu(MENU_SMS_VIEW);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
||||
{
|
||||
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
||||
updateListScreen(isSentBox);
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_UP))
|
||||
{
|
||||
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
||||
updateListScreen(isSentBox);
|
||||
}
|
||||
}
|
||||
|
||||
static bool deleteMessageCallback(void)
|
||||
{
|
||||
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
||||
{
|
||||
if (viewIsSentBox)
|
||||
{
|
||||
smsSentDelete(viewIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
smsInboxDelete(viewIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handleViewEvent(uiEvent_t *ev)
|
||||
{
|
||||
if ((ev->events & KEY_EVENT) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
||||
{
|
||||
if (BUTTONCHECK_DOWN(ev, BUTTON_SK2))
|
||||
{
|
||||
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
||||
|
||||
if (record != NULL)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s", currentLanguage->delete_message);
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
uiDataGlobal.MessageBox.validatorCallback = deleteMessageCallback;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
const smsRecord_t *record = getRecord(viewIsSentBox, viewIndex);
|
||||
|
||||
if (record != NULL)
|
||||
{
|
||||
// inbox: reply to the sender; sent box: re-edit and resend
|
||||
menuSMSComposePrefill(record->peerId, (viewIsSentBox ? record->text : NULL));
|
||||
menuSystemPushNewMenu(MENU_SMS_COMPOSE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
||||
{
|
||||
viewScrollLine++;
|
||||
updateViewScreen();
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_UP))
|
||||
{
|
||||
viewScrollLine--;
|
||||
updateViewScreen();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
#include "user_interface/uiGlobals.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
enum { OPTION_WAIT_FOR_ACK = 0, OPTION_OWN_ID_ONLY, NUM_SMS_OPTIONS };
|
||||
|
||||
static menuStatus_t menuSMSOptionsExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
static void updateScreen(void);
|
||||
static void handleEvent(uiEvent_t *ev);
|
||||
|
||||
menuStatus_t menuSMSOptions(uiEvent_t *ev, bool isFirstRun)
|
||||
{
|
||||
if (isFirstRun)
|
||||
{
|
||||
menuDataGlobal.numItems = NUM_SMS_OPTIONS;
|
||||
menuDataGlobal.currentItemIndex = 0;
|
||||
updateScreen();
|
||||
|
||||
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
menuSMSOptionsExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleEvent(ev);
|
||||
}
|
||||
|
||||
return menuSMSOptionsExitCode;
|
||||
}
|
||||
|
||||
static void updateScreen(void)
|
||||
{
|
||||
char entryBuffer[SCREEN_LINE_BUFFER_SIZE * 2];
|
||||
int mNum;
|
||||
|
||||
displayClearBuf();
|
||||
menuDisplayTitle(currentLanguage->options);
|
||||
|
||||
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
|
||||
{
|
||||
mNum = menuGetMenuOffset(NUM_SMS_OPTIONS, i);
|
||||
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
switch (mNum)
|
||||
{
|
||||
case OPTION_WAIT_FOR_ACK:
|
||||
snprintf(entryBuffer, sizeof(entryBuffer), "%s:%s",
|
||||
currentLanguage->sms_wait_for_ack, (smsOptionWaitForAck() ? currentLanguage->on : currentLanguage->off));
|
||||
break;
|
||||
|
||||
case OPTION_OWN_ID_ONLY:
|
||||
snprintf(entryBuffer, sizeof(entryBuffer), "%s:%s",
|
||||
currentLanguage->sms_own_id_only, (smsOptionOwnIdOnly() ? currentLanguage->on : currentLanguage->off));
|
||||
break;
|
||||
|
||||
default:
|
||||
entryBuffer[0] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
menuDisplayEntry(i, mNum, entryBuffer, 0, THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
||||
}
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static void toggleCurrentOption(void)
|
||||
{
|
||||
switch (menuDataGlobal.currentItemIndex)
|
||||
{
|
||||
case OPTION_WAIT_FOR_ACK:
|
||||
smsOptionSetWaitForAck(smsOptionWaitForAck() == false);
|
||||
break;
|
||||
|
||||
case OPTION_OWN_ID_ONLY:
|
||||
smsOptionSetOwnIdOnly(smsOptionOwnIdOnly() == false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void handleEvent(uiEvent_t *ev)
|
||||
{
|
||||
if ((ev->events & KEY_EVENT) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED) || KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_LEFT) || KEYCHECK_PRESS(ev->keys, KEY_RIGHT))
|
||||
{
|
||||
toggleCurrentOption();
|
||||
updateScreen();
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
|
||||
{
|
||||
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, NUM_SMS_OPTIONS);
|
||||
updateScreen();
|
||||
}
|
||||
else if (KEYCHECK_PRESS(ev->keys, KEY_UP))
|
||||
{
|
||||
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, NUM_SMS_OPTIONS);
|
||||
updateScreen();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Quick text templates: pick one to start a message, SK2+GREEN edits the
|
||||
* slot, SK2+RED deletes it. The last entry creates a new template while
|
||||
* free slots remain.
|
||||
*/
|
||||
#include "user_interface/uiGlobals.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
static menuStatus_t menuSMSQuickTextExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
static void updateScreen(void);
|
||||
static void handleEvent(uiEvent_t *ev);
|
||||
static bool deleteQuickTextCallback(void);
|
||||
|
||||
static bool hasNewEntry(void)
|
||||
{
|
||||
return (smsQuickTextCount() < SMS_QUICKTEXT_SLOTS);
|
||||
}
|
||||
|
||||
static int totalEntries(void)
|
||||
{
|
||||
return (smsQuickTextCount() + (hasNewEntry() ? 1 : 0));
|
||||
}
|
||||
|
||||
menuStatus_t menuSMSQuickText(uiEvent_t *ev, bool isFirstRun)
|
||||
{
|
||||
if (isFirstRun)
|
||||
{
|
||||
menuDataGlobal.numItems = totalEntries();
|
||||
menuDataGlobal.currentItemIndex = 0;
|
||||
updateScreen();
|
||||
|
||||
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
|
||||
}
|
||||
|
||||
menuSMSQuickTextExitCode = MENU_STATUS_SUCCESS;
|
||||
|
||||
// re-sync after returning from the editor
|
||||
if (menuDataGlobal.numItems != totalEntries())
|
||||
{
|
||||
menuDataGlobal.numItems = totalEntries();
|
||||
|
||||
if (menuDataGlobal.currentItemIndex >= menuDataGlobal.numItems)
|
||||
{
|
||||
menuDataGlobal.currentItemIndex = (menuDataGlobal.numItems > 0) ? (menuDataGlobal.numItems - 1) : 0;
|
||||
}
|
||||
|
||||
updateScreen();
|
||||
}
|
||||
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleEvent(ev);
|
||||
}
|
||||
|
||||
return menuSMSQuickTextExitCode;
|
||||
}
|
||||
|
||||
static void updateScreen(void)
|
||||
{
|
||||
int mNum;
|
||||
|
||||
displayClearBuf();
|
||||
menuDisplayTitle(currentLanguage->quick_text);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const char *text = smsQuickTextGet((uint8_t)mNum);
|
||||
|
||||
menuDisplayEntry(i, mNum, ((text != NULL) ? text : currentLanguage->new_message), 0,
|
||||
THEME_ITEM_FG_TEXT_INPUT, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
||||
}
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static bool deleteQuickTextCallback(void)
|
||||
{
|
||||
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
||||
{
|
||||
smsQuickTextDelete((uint8_t)menuDataGlobal.currentItemIndex);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void handleEvent(uiEvent_t *ev)
|
||||
{
|
||||
if ((ev->events & KEY_EVENT) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool onExistingEntry = (menuDataGlobal.currentItemIndex < smsQuickTextCount());
|
||||
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
||||
{
|
||||
if (BUTTONCHECK_DOWN(ev, BUTTON_SK2) && onExistingEntry)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s", currentLanguage->delete_message);
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
uiDataGlobal.MessageBox.validatorCallback = deleteQuickTextCallback;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
}
|
||||
else
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
if (BUTTONCHECK_DOWN(ev, BUTTON_SK2) || (onExistingEntry == false))
|
||||
{
|
||||
// edit the slot (or create a new one)
|
||||
menuSMSComposeSetQuickTextEditTarget(menuDataGlobal.currentItemIndex);
|
||||
menuSystemPushNewMenu(MENU_SMS_COMPOSE);
|
||||
}
|
||||
else
|
||||
{
|
||||
// use the template for a new message
|
||||
menuSMSComposePrefill(0, smsQuickTextGet((uint8_t)menuDataGlobal.currentItemIndex));
|
||||
menuSystemPushNewMenu(MENU_SMS_COMPOSE);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +90,16 @@ menuDataGlobal_t menuDataGlobal =
|
|||
#if !defined(PLATFORM_GD77S)
|
||||
NULL,// APRS options
|
||||
#endif
|
||||
&menuDataSMS,// SMS hub
|
||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
||||
NULL,// SMS compose
|
||||
NULL,// SMS inbox
|
||||
NULL,// SMS sent
|
||||
NULL,// SMS view
|
||||
NULL,// SMS quick texts
|
||||
NULL,// SMS options
|
||||
NULL,// SMS contact picker
|
||||
NULL,// SMS RX popup
|
||||
NULL,// MessageBox
|
||||
NULL,// hotspot mode
|
||||
NULL,// CPS
|
||||
|
|
@ -148,7 +157,16 @@ static menuFunctionData_t menuFunctions[] =
|
|||
#if !defined(PLATFORM_GD77S)
|
||||
{ menuAPRSOptions, NULL, NULL, 0 },
|
||||
#endif
|
||||
{ menuDisplayMenuList, NULL, NULL, 0 },// SMS hub
|
||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
||||
{ menuSMSCompose, NULL, NULL, 0 },
|
||||
{ menuSMSList, NULL, NULL, 0 },// SMS inbox
|
||||
{ menuSMSList, NULL, NULL, 0 },// SMS sent
|
||||
{ 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 },
|
||||
{ uiCPS, NULL, NULL, 0 },
|
||||
|
|
@ -649,6 +667,7 @@ const menuItemNewData_t mainMenuItems[] =
|
|||
{
|
||||
{ 3, MENU_ZONE_LIST },
|
||||
{ 6, MENU_CONTACTS_MENU },
|
||||
{ 282, MENU_SMS },// messages
|
||||
{ 12, MENU_CHANNEL_DETAILS },
|
||||
{ 4, MENU_RSSI_SCREEN },
|
||||
{ 8, MENU_FIRMWARE_INFO },
|
||||
|
|
@ -667,6 +686,21 @@ const menuItemsList_t menuDataMainMenu =
|
|||
.items = mainMenuItems
|
||||
};
|
||||
|
||||
static const menuItemNewData_t smsMenuItems[] =
|
||||
{
|
||||
{ 283, MENU_SMS_COMPOSE },// new message
|
||||
{ 284, MENU_SMS_INBOX },// inbox
|
||||
{ 285, MENU_SMS_SENT },// sent
|
||||
{ 286, MENU_SMS_QUICKTEXT },// quick text
|
||||
{ 9, MENU_SMS_OPTIONS },// options
|
||||
};
|
||||
|
||||
const menuItemsList_t menuDataSMS =
|
||||
{
|
||||
.numItems = (sizeof(smsMenuItems) / sizeof(smsMenuItems[0])),
|
||||
.items = smsMenuItems
|
||||
};
|
||||
|
||||
static const menuItemNewData_t contactMenuItems[] =
|
||||
{
|
||||
{ 15, MENU_CONTACT_LIST },
|
||||
|
|
@ -725,53 +759,11 @@ void menuDisplayTitle(const char *title)
|
|||
// optStart:
|
||||
// - if less than 0, entryText will use fgOptItem colour
|
||||
// - if equal to 0, no coloured option will be handled
|
||||
#if defined(HAS_COLOURS)
|
||||
// 16x16 1-bit main-menu icons (row-major, MSB-first) for displayDrawBitmap.
|
||||
static const uint8_t menuIcon_zone[32] = { 0x00, 0x00, 0x3F, 0xFC, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_contacts[32] = { 0x00, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x05, 0xA0, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_channel[32] = { 0x00, 0x00, 0x00, 0x00, 0x21, 0x08, 0x21, 0x08, 0x71, 0x1C, 0x51, 0x14, 0x73, 0x9C, 0x22, 0x88, 0x23, 0x88, 0x21, 0x08, 0x21, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_rssi[32] = { 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x66, 0x00, 0x66, 0x00, 0x66, 0x06, 0x66, 0x06, 0x66, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_info[32] = { 0x00, 0x00, 0x07, 0xE0, 0x18, 0x18, 0x21, 0x84, 0x21, 0x84, 0x40, 0x08, 0x43, 0xC2, 0x40, 0xC2, 0x40, 0xC2, 0x40, 0xC2, 0x21, 0xE2, 0x20, 0x04, 0x18, 0x18, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_options[32] = { 0x00, 0x00, 0x03, 0xC0, 0x13, 0xC8, 0x1B, 0xD8, 0x3F, 0xFC, 0x38, 0x1C, 0x7B, 0xDE, 0x7B, 0xDE, 0x7B, 0xDE, 0x38, 0x1C, 0x3F, 0xFC, 0x1B, 0xD8, 0x13, 0xC8, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_lastheard[32] = { 0x00, 0x00, 0x3F, 0xF8, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x3C, 0x7C, 0x04, 0x40, 0x02, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_radio[32] = { 0x00, 0x10, 0x00, 0x20, 0x00, 0x40, 0x1F, 0xC0, 0x10, 0x40, 0x17, 0x40, 0x17, 0x40, 0x10, 0x40, 0x15, 0x40, 0x15, 0x40, 0x15, 0x40, 0x10, 0x40, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_satellite[32] = { 0x00, 0x00, 0x30, 0x00, 0x28, 0x10, 0x24, 0x28, 0x02, 0x44, 0x01, 0x94, 0x00, 0xA4, 0x01, 0x38, 0x02, 0x00, 0x0D, 0x00, 0x11, 0x00, 0x11, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||
static const uint8_t menuIcon_gps[32] = { 0x00, 0x00, 0x07, 0xE0, 0x18, 0x18, 0x20, 0x04, 0x43, 0xC2, 0x44, 0x22, 0x44, 0x22, 0x44, 0x22, 0x43, 0xC2, 0x20, 0x04, 0x10, 0x08, 0x08, 0x10, 0x04, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x00 };
|
||||
|
||||
static const uint8_t *menuIconForMenuNumber(int menuNum)
|
||||
{
|
||||
switch (menuNum)
|
||||
{
|
||||
case MENU_ZONE_LIST: return menuIcon_zone;
|
||||
case MENU_CONTACTS_MENU: return menuIcon_contacts;
|
||||
case MENU_CHANNEL_DETAILS: return menuIcon_channel;
|
||||
case MENU_RSSI_SCREEN: return menuIcon_rssi;
|
||||
case MENU_FIRMWARE_INFO: return menuIcon_info;
|
||||
case MENU_OPTIONS: return menuIcon_options;
|
||||
case MENU_LAST_HEARD: return menuIcon_lastheard;
|
||||
case MENU_RADIO_INFOS: return menuIcon_radio;
|
||||
case MENU_SATELLITE: return menuIcon_satellite;
|
||||
case MENU_GPS: return menuIcon_gps;
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot: the next menuDisplayEntry() draws this icon and shifts its text right.
|
||||
static const uint8_t *nextMenuEntryIcon = NULL;
|
||||
void menuSetNextEntryIconByMenuNum(int menuNum)
|
||||
{
|
||||
nextMenuEntryIcon = menuIconForMenuNumber(menuNum);
|
||||
}
|
||||
#endif // HAS_COLOURS
|
||||
|
||||
void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, int32_t optStart, themeItem_t fgItem, themeItem_t fgOptItem, themeItem_t bgItem)
|
||||
{
|
||||
bool focused = (focusedItem == menuDataGlobal.currentItemIndex);
|
||||
int16_t textXPos = DISPLAY_X_POS_MENU_TEXT_OFFSET;
|
||||
#if defined(HAS_COLOURS)
|
||||
const uint8_t *entryIcon = nextMenuEntryIcon;
|
||||
nextMenuEntryIcon = NULL; // consume it (one-shot)
|
||||
#endif
|
||||
|
||||
if (focused)
|
||||
{
|
||||
|
|
@ -785,15 +777,6 @@ void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, in
|
|||
|
||||
displayThemeApply(fgItem, bgItem);
|
||||
|
||||
#if defined(HAS_COLOURS)
|
||||
if (entryIcon != NULL)
|
||||
{
|
||||
int16_t iconY = DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT);
|
||||
displayDrawBitmap((DISPLAY_X_POS_MENU_OFFSET + 2), iconY, (uint8_t *)entryIcon, 16, 16, focused);
|
||||
textXPos = (DISPLAY_X_POS_MENU_OFFSET + 2 + 16 + 3); // icon width + gap
|
||||
}
|
||||
#endif
|
||||
|
||||
if ((focused == false) && ((optStart < 0) || (optStart > 0)))
|
||||
{
|
||||
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#include "user_interface/uiLocalisation.h"
|
||||
#include "functions/voicePrompts.h"
|
||||
#include "functions/rxPowerSaving.h"
|
||||
#include "functions/smsCore.h"
|
||||
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
||||
#include "hardware/radioHardwareInterface.h"
|
||||
#endif
|
||||
|
|
@ -274,6 +275,13 @@ menuStatus_t uiChannelMode(uiEvent_t *ev, bool isFirstRun)
|
|||
{
|
||||
return menuChannelExitStatus;
|
||||
}
|
||||
|
||||
// A new text message arrived
|
||||
if (smsHasUnseenRxMessage() && (trxTransmissionEnabled == false) && (trxIsTransmitting == false))
|
||||
{
|
||||
menuSystemPushNewMenu(UI_SMS_RX_POPUP);
|
||||
return menuChannelExitStatus;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ev->events == NO_EVENT)
|
||||
|
|
@ -1298,6 +1306,20 @@ static void handleEvent(uiEvent_t *ev)
|
|||
return;
|
||||
}
|
||||
else
|
||||
#else
|
||||
if (KEYCHECK_LONGDOWN(ev->keys, KEY_GREEN))
|
||||
{
|
||||
// Direct access to the DMR text messages
|
||||
if (uiDataGlobal.Scan.active)
|
||||
{
|
||||
uiChannelModeStopScanning();
|
||||
}
|
||||
|
||||
menuSystemPushNewMenu(MENU_SMS);
|
||||
menuChannelExitStatus |= (MENU_STATUS_LIST_TYPE | MENU_STATUS_FORCE_FIRST);
|
||||
return;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
|
|
@ -2164,6 +2186,152 @@ static bool validateOverwriteChannel(void)
|
|||
return true;
|
||||
}
|
||||
|
||||
// Shown when channels are locked (read-only / APRS) and an edit is attempted.
|
||||
static void channelEditShowLockedMessageBox(void)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s\n%s", currentLanguage->aprs_channel, currentLanguage->locked);
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
uiDataGlobal.MessageBox.buttons =
|
||||
#if defined(PLATFORM_MD9600)
|
||||
MESSAGEBOX_BUTTONS_ENT;
|
||||
#else
|
||||
MESSAGEBOX_BUTTONS_OK;
|
||||
#endif
|
||||
uiDataGlobal.MessageBox.validatorCallback = NULL;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
|
||||
voicePromptsInit();
|
||||
voicePromptsAppendLanguageString(currentLanguage->aprs_channel);
|
||||
voicePromptsAppendLanguageString(currentLanguage->locked);
|
||||
voicePromptsPlay();
|
||||
}
|
||||
|
||||
// Create a blank/default 145.500 MHz FM simplex channel in the first free slot,
|
||||
// add it to the current zone, select it and open Channel Details to edit it.
|
||||
static void channelEditCreateNewChannel(void)
|
||||
{
|
||||
int16_t newChannelIndex;
|
||||
|
||||
for (newChannelIndex = CODEPLUG_CHANNELS_MIN; newChannelIndex <= CODEPLUG_CHANNELS_MAX; newChannelIndex++)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(newChannelIndex) == false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newChannelIndex > CODEPLUG_CHANNELS_MAX)
|
||||
{
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP; // codeplug full, no free slot
|
||||
return;
|
||||
}
|
||||
|
||||
// Start from the currently loaded channel so all structural fields are valid,
|
||||
// then override the essentials to a blank FM simplex default.
|
||||
char nameBuf[SCREEN_LINE_BUFFER_SIZE];
|
||||
CodeplugChannel_t tempChannel = channelScreenChannelData;
|
||||
|
||||
snprintf(nameBuf, SCREEN_LINE_BUFFER_SIZE, "%s %d", currentLanguage->new_channel, newChannelIndex);
|
||||
memset(&tempChannel.name, 0xFF, sizeof(tempChannel.name)); // codeplug strings are 0xFF-padded
|
||||
memcpy(&tempChannel.name, nameBuf, strlen(nameBuf));
|
||||
|
||||
tempChannel.chMode = RADIO_MODE_ANALOG;
|
||||
tempChannel.rxFreq = tempChannel.txFreq = 14550000; // 145.500 MHz
|
||||
tempChannel.txTone = tempChannel.rxTone = CODEPLUG_CSS_TONE_NONE;
|
||||
tempChannel.sql = 0U; // use the global squelch level
|
||||
tempChannel.rxGroupList = 0;
|
||||
tempChannel.contact = 0;
|
||||
tempChannel.NOT_IN_CODEPLUG_flag = 0x00;
|
||||
tempChannel.NOT_IN_CODEPLUG_CALCULATED_DISTANCE_X10 = -1;
|
||||
|
||||
if (codeplugChannelSaveDataForIndex(newChannelIndex, &tempChannel) == false)
|
||||
{
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_NACK_BEEP;
|
||||
return;
|
||||
}
|
||||
codeplugAllChannelsIndexSetUsed(newChannelIndex); // mark the slot used AFTER the data is written
|
||||
|
||||
if (currentZone.NOT_IN_CODEPLUGDATA_indexNumber == 0xDEADBEEF)
|
||||
{
|
||||
uiChannelInitializeCurrentZone();
|
||||
}
|
||||
|
||||
// Add it to the current zone ("All Channels" already includes every used slot).
|
||||
if (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone))
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, newChannelIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (codeplugZoneAddChannelToZoneAndSave(newChannelIndex, ¤tZone))
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, (currentZone.NOT_IN_CODEPLUGDATA_numChannelsInZone - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the zone so the new channel is immediately navigable without having to
|
||||
// re-enter the zone (refreshes the channel count / highest index for real zones,
|
||||
// and re-derives them from the channel in-use bitmap for "All Channels").
|
||||
uiChannelInitializeCurrentZone();
|
||||
|
||||
// Select the new channel (kept in memory, so Channel Details edits valid data).
|
||||
memcpy(&channelScreenChannelData, &tempChannel, sizeof(tempChannel));
|
||||
uiDataGlobal.currentSelectedChannelNumber = newChannelIndex;
|
||||
uiDataGlobal.VoicePrompts.inhibitInitial = true;
|
||||
|
||||
menuSystemPopAllAndDisplaySpecificRootMenu(UI_CHANNEL_MODE, true);
|
||||
menuSystemPushNewMenu(MENU_CHANNEL_DETAILS); // jump straight into the editor
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
|
||||
}
|
||||
|
||||
// Confirmation callback for deleting the currently-selected channel.
|
||||
static bool validateDeleteChannel(void)
|
||||
{
|
||||
if (uiDataGlobal.MessageBox.keyPressed == KEY_GREEN)
|
||||
{
|
||||
int deletedChannelIndex = uiDataGlobal.currentSelectedChannelNumber;
|
||||
|
||||
// Remove the channel from every real zone that references it, so no zone is
|
||||
// left pointing at a freed slot.
|
||||
int numZones = codeplugZonesGetCount();
|
||||
for (int z = 0; z < numZones; z++)
|
||||
{
|
||||
CodeplugZone_t zoneBuf;
|
||||
if (codeplugZoneGetDataForNumber(z, &zoneBuf) && (CODEPLUG_ZONE_IS_ALLCHANNELS(zoneBuf) == false))
|
||||
{
|
||||
codeplugZoneRemoveChannelFromZoneAndSave(deletedChannelIndex, &zoneBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Free the slot (clears the in-use bitmap bit).
|
||||
codeplugAllChannelsIndexSetUnused(deletedChannelIndex);
|
||||
|
||||
// Point the current zone at a valid remaining channel and force a reload.
|
||||
if (CODEPLUG_ZONE_IS_ALLCHANNELS(currentZone))
|
||||
{
|
||||
int firstCh = CODEPLUG_CHANNELS_MIN;
|
||||
while ((firstCh <= CODEPLUG_CHANNELS_MAX) && (codeplugAllChannelsIndexIsInUse(firstCh) == false))
|
||||
{
|
||||
firstCh++;
|
||||
}
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, ((firstCh <= CODEPLUG_CHANNELS_MAX) ? firstCh : CODEPLUG_CHANNELS_MIN));
|
||||
}
|
||||
else
|
||||
{
|
||||
codeplugSetLastUsedChannelInZone(currentZone.NOT_IN_CODEPLUGDATA_indexNumber, 0); // first channel in the zone
|
||||
}
|
||||
|
||||
uiChannelInitializeCurrentZone(); // its channel list/count changed
|
||||
channelScreenChannelData.rxFreq = 0; // force the channel screen to reload
|
||||
|
||||
menuSystemPopAllAndDisplaySpecificRootMenu(UI_CHANNEL_MODE, true);
|
||||
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void updateQuickMenuScreen(bool isFirstRun)
|
||||
{
|
||||
int mNum = 0;
|
||||
|
|
@ -2266,6 +2434,12 @@ static void updateQuickMenuScreen(bool isFirstRun)
|
|||
leftSide = currentLanguage->mute;
|
||||
rightSideConst = (uiDataGlobal.QuickMenu.tmpAudioMute ? currentLanguage->yes : currentLanguage->no);
|
||||
break;
|
||||
case CH_SCREEN_QUICK_MENU_NEW_CHANNEL:
|
||||
rightSideConst = currentLanguage->new_channel;
|
||||
break;
|
||||
case CH_SCREEN_QUICK_MENU_DELETE_CHANNEL:
|
||||
rightSideConst = currentLanguage->delete_channel;
|
||||
break;
|
||||
default:
|
||||
buf[0] = 0;
|
||||
}
|
||||
|
|
@ -2496,6 +2670,66 @@ static void handleQuickMenuEvent(uiEvent_t *ev)
|
|||
}
|
||||
break;
|
||||
|
||||
case CH_SCREEN_QUICK_MENU_NEW_CHANNEL:
|
||||
if (settingsIsOptionBitSet(BIT_CHANNELS_ARE_READ_ONLY))
|
||||
{
|
||||
channelEditShowLockedMessageBox();
|
||||
}
|
||||
else
|
||||
{
|
||||
channelEditCreateNewChannel();
|
||||
}
|
||||
return;
|
||||
|
||||
case CH_SCREEN_QUICK_MENU_DELETE_CHANNEL:
|
||||
{
|
||||
if (settingsIsOptionBitSet(BIT_CHANNELS_ARE_READ_ONLY))
|
||||
{
|
||||
channelEditShowLockedMessageBox();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow deleting the last remaining channel (would leave the
|
||||
// codeplug with no channels).
|
||||
int inUseCount = 0;
|
||||
for (int i = CODEPLUG_CHANNELS_MIN; (i <= CODEPLUG_CHANNELS_MAX) && (inUseCount < 2); i++)
|
||||
{
|
||||
if (codeplugAllChannelsIndexIsInUse(i))
|
||||
{
|
||||
inUseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
|
||||
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
|
||||
|
||||
if (inUseCount < 2)
|
||||
{
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s", currentLanguage->keep_one_channel);
|
||||
uiDataGlobal.MessageBox.buttons =
|
||||
#if defined(PLATFORM_MD9600)
|
||||
MESSAGEBOX_BUTTONS_ENT;
|
||||
#else
|
||||
MESSAGEBOX_BUTTONS_OK;
|
||||
#endif
|
||||
uiDataGlobal.MessageBox.validatorCallback = NULL;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
return;
|
||||
}
|
||||
|
||||
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%s\n%s", currentLanguage->delete_channel, currentLanguage->please_confirm);
|
||||
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
|
||||
uiDataGlobal.MessageBox.validatorCallback = validateDeleteChannel;
|
||||
menuSystemPushNewMenu(UI_MESSAGE_BOX);
|
||||
|
||||
voicePromptsInit();
|
||||
voicePromptsAppendLanguageString(currentLanguage->delete_channel);
|
||||
voicePromptsAppendLanguageString(currentLanguage->please_confirm);
|
||||
voicePromptsPlay();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// CH_SCREEN_QUICK_MENU_FILTER_FM
|
||||
if (nonVolatileSettings.analogFilterLevel != uiDataGlobal.QuickMenu.tmpAnalogFilterLevel)
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
177
MDUV380_firmware/application/source/user_interface/uiSMSPopup.c
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Incoming message popup: shown over the channel/VFO screen when a new
|
||||
* message arrives. GREEN opens it, SK2+GREEN starts a reply, RED dismisses.
|
||||
*/
|
||||
#include "user_interface/uiGlobals.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "functions/smsStorage.h"
|
||||
|
||||
#define TEXT_CHARS_PER_LINE (DISPLAY_SIZE_X / 8)
|
||||
#define PREVIEW_LINES 3
|
||||
|
||||
static void updateScreen(void);
|
||||
static void handleEvent(uiEvent_t *ev);
|
||||
static void smsAlertTone(void);
|
||||
|
||||
menuStatus_t uiSMSPopup(uiEvent_t *ev, bool isFirstRun)
|
||||
{
|
||||
if (isFirstRun)
|
||||
{
|
||||
smsAcknowledgeRxMessage();
|
||||
|
||||
if (smsInboxGet(0) == NULL)
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
return MENU_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
updateScreen();
|
||||
smsAlertTone();
|
||||
|
||||
return MENU_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
// a newer message replaces the popup content
|
||||
if (smsHasUnseenRxMessage())
|
||||
{
|
||||
smsAcknowledgeRxMessage();
|
||||
updateScreen();
|
||||
smsAlertTone();
|
||||
}
|
||||
|
||||
if (ev->hasEvent)
|
||||
{
|
||||
handleEvent(ev);
|
||||
}
|
||||
|
||||
return MENU_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static void smsAlertTone(void)
|
||||
{
|
||||
if ((voicePromptsIsPlaying() == false) && (melody_play == NULL) && (audioAmpGetStatus() == AUDIO_AMP_CHANNEL_NONE))
|
||||
{
|
||||
soundSetMelody(MELODY_SMS_RX);
|
||||
}
|
||||
}
|
||||
|
||||
static void updateScreen(void)
|
||||
{
|
||||
char lineBuffer[TEXT_CHARS_PER_LINE + 1];
|
||||
char nameBuffer[SCREEN_LINE_BUFFER_SIZE];
|
||||
const smsRecord_t *record = smsInboxGet(0);
|
||||
|
||||
if (record == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
displayClearBuf();
|
||||
menuDisplayTitle(currentLanguage->message_from);
|
||||
|
||||
if (contactIDLookup(record->peerId, CONTACT_CALLTYPE_PC, nameBuffer) == false)
|
||||
{
|
||||
dmrIdDataStruct_t idRecord;
|
||||
|
||||
if (dmrIDLookup(record->peerId, &idRecord))
|
||||
{
|
||||
strncpy(nameBuffer, idRecord.text, sizeof(nameBuffer) - 1);
|
||||
nameBuffer[sizeof(nameBuffer) - 1] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(nameBuffer, sizeof(nameBuffer), "%u", (unsigned int)record->peerId);
|
||||
}
|
||||
}
|
||||
|
||||
displayPrintCentered(FONT_SIZE_3_HEIGHT, nameBuffer, FONT_SIZE_3);
|
||||
|
||||
int textLength = strlen(record->text);
|
||||
|
||||
for (int line = 0; line < PREVIEW_LINES; line++)
|
||||
{
|
||||
int offset = line * TEXT_CHARS_PER_LINE;
|
||||
|
||||
if (offset >= textLength)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
int chunk = textLength - offset;
|
||||
|
||||
if (chunk > TEXT_CHARS_PER_LINE)
|
||||
{
|
||||
chunk = TEXT_CHARS_PER_LINE;
|
||||
}
|
||||
|
||||
memcpy(lineBuffer, &record->text[offset], chunk);
|
||||
lineBuffer[chunk] = 0;
|
||||
displayPrintAt(0, (FONT_SIZE_3_HEIGHT * 2) + (line * FONT_SIZE_3_HEIGHT), lineBuffer, FONT_SIZE_3);
|
||||
}
|
||||
|
||||
displayRender();
|
||||
}
|
||||
|
||||
static void handleEvent(uiEvent_t *ev)
|
||||
{
|
||||
if ((ev->events & KEY_EVENT) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
}
|
||||
else if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||
{
|
||||
menuSystemPopPreviousMenu();
|
||||
|
||||
if (BUTTONCHECK_DOWN(ev, BUTTON_SK2))
|
||||
{
|
||||
const smsRecord_t *record = smsInboxGet(0);
|
||||
|
||||
if (record != NULL)
|
||||
{
|
||||
menuSMSComposePrefill(record->peerId, NULL);
|
||||
menuSystemPushNewMenu(MENU_SMS_COMPOSE);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
smsInboxMarkRead(0);
|
||||
menuSMSListSetViewTarget(false, 0);
|
||||
menuSystemPushNewMenu(MENU_SMS_VIEW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#endif
|
||||
#include "functions/trx.h"
|
||||
#include "functions/rxPowerSaving.h"
|
||||
#include "functions/smsCore.h"
|
||||
#include "user_interface/menuSystem.h"
|
||||
#include "user_interface/uiUtilities.h"
|
||||
#include "user_interface/uiLocalisation.h"
|
||||
|
|
@ -258,6 +259,13 @@ menuStatus_t uiVFOMode(uiEvent_t *ev, bool isFirstRun)
|
|||
{
|
||||
return menuVFOExitStatus;
|
||||
}
|
||||
|
||||
// A new text message arrived
|
||||
if (smsHasUnseenRxMessage() && (trxTransmissionEnabled == false) && (trxIsTransmitting == false))
|
||||
{
|
||||
menuSystemPushNewMenu(UI_SMS_RX_POPUP);
|
||||
return menuVFOExitStatus;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ev->events == NO_EVENT)
|
||||
|
|
@ -1311,6 +1319,20 @@ static void handleEvent(uiEvent_t *ev)
|
|||
return;
|
||||
}
|
||||
else
|
||||
#else
|
||||
if (KEYCHECK_LONGDOWN(ev->keys, KEY_GREEN))
|
||||
{
|
||||
// Direct access to the DMR text messages
|
||||
if (uiDataGlobal.Scan.active)
|
||||
{
|
||||
uiVFOModeStopScanning();
|
||||
}
|
||||
|
||||
menuSystemPushNewMenu(MENU_SMS);
|
||||
menuVFOExitStatus |= (MENU_STATUS_LIST_TYPE | MENU_STATUS_FORCE_FIRST);
|
||||
return;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
if (KEYCHECK_LONGDOWN(ev->keys, KEY_HASH) && (KEYCHECK_LONGDOWN_REPEAT(ev->keys, KEY_HASH) == false))
|
||||
{
|
||||
|
|
|
|||
59
README.md
|
|
@ -6,14 +6,24 @@ 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, and more). See [`CHANGELOG.md`](CHANGELOG.md) for the full list.
|
||||
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
|
||||
[`CHANGELOG.md`](CHANGELOG.md) for the version history.
|
||||
|
||||
## Screenshots
|
||||
|
||||
| Segmented S-meter & DMR call | 7-segment frequency display |
|
||||
| Segmented S-meter & DMR call | 7-segment frequency display | Main menu |
|
||||
| --- | --- | --- |
|
||||
|  |  |  |
|
||||
|
||||
| DMR text messages | Reading a received message |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
*Screens shown with the **Neon 80s** theme.*
|
||||
|
||||
## What's different from OpenGD77
|
||||
|
||||
|
|
@ -21,23 +31,52 @@ 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.
|
||||
- **Segmented block S-meter** with one block per S-level (S1–S9 green, S9+ red)
|
||||
and a scale label row (S1 S3 S5 S7 S9 / +20 +40 +60).
|
||||
- **7-segment "LED" frequency font** with faintly-lit inactive segments, on the
|
||||
VFO and the SK1-held frequency view.
|
||||
- **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 % after
|
||||
- **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.
|
||||
- **Main-menu icons** next to each top-level entry.
|
||||
- **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),
|
||||
**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.
|
||||
|
||||
See [`CHANGELOG.md`](CHANGELOG.md) for the complete list.
|
||||
|
||||
## Managing channels on the radio
|
||||
|
||||
FreeTRX can create and delete channels without a PC. Both actions live in the
|
||||
**Channel quick menu** — from the Channel screen, press the **top (orange)
|
||||
button** to open it, then scroll to the bottom:
|
||||
|
||||
- **New channel** — creates a blank **145.500 MHz FM** channel in the first free
|
||||
slot, adds it to the current zone, and opens **Channel Details** so you can set
|
||||
the name, frequency, tones, and so on. Press the **green** key to save. The new
|
||||
channel is immediately selectable in the zone (no need to re-enter it).
|
||||
- **Delete Channel** — deletes the currently-selected channel after a
|
||||
confirmation (**green** confirms, **red** cancels). It is also scrubbed from
|
||||
every zone that referenced it. The last remaining channel cannot be deleted,
|
||||
and channels locked read-only (e.g. APRS) are protected.
|
||||
|
||||
> **Back up your codeplug** (via the CPS) before making bulk edits — these changes
|
||||
> are written straight to the radio and there is no on-device undo.
|
||||
|
||||
## Supported radios
|
||||
|
||||
This first iteration is developed for and **tested only on the TYT MD-UV380 and
|
||||
|
|
|
|||
20
TOOLS.md
|
|
@ -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
|
||||
|
|
@ -87,9 +91,19 @@ Models: `MD-UV380`, `DM-1701`, `MD-9600`, `MD-2017`, `MD-380`.
|
|||
grabber (the "gd-77" name is historical). It talks over the USB **CDC serial port**,
|
||||
auto-detects the radio model and reads the MD-UV380's 160×128 RGB565 framebuffer into a PNG.
|
||||
|
||||
1. Power the radio on **normally** (not DFU mode) and navigate to the screen you want.
|
||||
Connect USB.
|
||||
2. Run it (`-o` takes the filename **without** `.png`):
|
||||
Power the radio on **normally** (not DFU mode), navigate to the screen you want, and
|
||||
connect USB.
|
||||
|
||||
**Easiest — the `./screenshot.sh` helper** (auto-detects the serial port and sets up a
|
||||
self-contained Python venv with `pyserial` + `pillow` on first run):
|
||||
|
||||
```bash
|
||||
./screenshot.sh # -> screenshots/screenshot-<timestamp>.png
|
||||
./screenshot.sh vfo-7seg # -> screenshots/vfo-7seg.png
|
||||
./screenshot.sh -d /dev/cu.usbmodemXXXX [name] # explicit port
|
||||
```
|
||||
|
||||
**Or call the grabber directly** (`-o` takes the filename **without** `.png`):
|
||||
|
||||
```bash
|
||||
python3 MDUV380_firmware/tools/gd-77_screen_grabber.py \
|
||||
|
|
|
|||
|
|
@ -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 \\"
|
||||
|
|
|
|||
|
|
@ -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 --------------------------------------------------------------
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.2 KiB |
BIN
docs/images/screenshot-dmr2.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
docs/images/screenshot-menu.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/images/screenshot-message-view.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/images/screenshot-messages.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.8 KiB |
10
flash.sh
|
|
@ -58,11 +58,11 @@ done
|
|||
# ---- resolve / build the language file ------------------------------------
|
||||
LANG_ARGS=()
|
||||
if [[ -n "$LANGUAGE" && "$(lc "$LANGUAGE")" != "none" ]]; then
|
||||
# Build the .gla files once if they're missing.
|
||||
if ! ls "$LANG_SRC"/*.gla >/dev/null 2>&1; then
|
||||
echo "==> Building language (.gla) files..."
|
||||
( cd "$LANG_SRC" && gcc -w -I../ -o languages_builder languages_builder.c && ./languages_builder >/dev/null )
|
||||
fi
|
||||
# Always (re)build the .gla language packs so edits to the *.h language files
|
||||
# are picked up. (They're built from the .h sources by languages_builder; a
|
||||
# stale pack would otherwise show blank/old strings on the radio.)
|
||||
echo "==> Building language (.gla) files..."
|
||||
( cd "$LANG_SRC" && gcc -w -I../ -o languages_builder languages_builder.c && ./languages_builder >/dev/null )
|
||||
# Case-insensitive match of the requested name to an actual <Name>.gla file.
|
||||
GLA=""
|
||||
for f in "$LANG_SRC"/*.gla; do
|
||||
|
|
|
|||
55
screenshot.sh
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Grab a screenshot from the radio over USB (CDC serial), using the OpenGD77
|
||||
# screen grabber. Runs natively (USB serial is not available inside Docker) and
|
||||
# sets up a self-contained Python venv (pyserial + pillow) on first use.
|
||||
#
|
||||
# The radio must be powered on, running the firmware, and connected via USB.
|
||||
#
|
||||
# Usage:
|
||||
# ./screenshot.sh # auto-detect port -> screenshots/screenshot-<timestamp>.png
|
||||
# ./screenshot.sh vfo-7seg # -> screenshots/vfo-7seg.png
|
||||
# ./screenshot.sh -d /dev/cu.usbmodem1234 [name] # explicit serial port
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
GRABBER="$REPO_DIR/MDUV380_firmware/tools/gd-77_screen_grabber.py"
|
||||
VENV="$REPO_DIR/MDUV380_firmware/tools/.venv"
|
||||
OUT_DIR="$REPO_DIR/screenshots"
|
||||
|
||||
# ---- optional explicit device (-d /dev/...) -------------------------------
|
||||
DEVICE=""
|
||||
if [[ "${1:-}" == "-d" ]]; then
|
||||
DEVICE="${2:-}"; shift 2 || true
|
||||
fi
|
||||
|
||||
# ---- auto-detect the radio's serial port ----------------------------------
|
||||
if [[ -z "$DEVICE" ]]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin) DEVICE="$(ls /dev/cu.usbmodem* 2>/dev/null | head -1 || true)" ;;
|
||||
*) DEVICE="$(ls /dev/ttyACM* 2>/dev/null | head -1 || true)" ;;
|
||||
esac
|
||||
fi
|
||||
if [[ -z "$DEVICE" ]]; then
|
||||
echo "!! No radio serial port found." >&2
|
||||
echo " Connect the radio via USB (powered on, running firmware) and retry, or pass one:" >&2
|
||||
echo " ./screenshot.sh -d /dev/cu.usbmodemXXXX" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- one-time Python venv with the grabber's dependencies ------------------
|
||||
if [[ ! -x "$VENV/bin/python" ]]; then
|
||||
echo "==> First run: creating Python venv with pyserial + pillow..."
|
||||
python3 -m venv "$VENV"
|
||||
"$VENV/bin/pip" install --quiet --upgrade pip
|
||||
"$VENV/bin/pip" install --quiet pyserial pillow
|
||||
fi
|
||||
|
||||
# ---- output name -----------------------------------------------------------
|
||||
mkdir -p "$OUT_DIR"
|
||||
NAME="${1:-screenshot-$(date +%Y%m%d-%H%M%S)}"
|
||||
OUT="$OUT_DIR/$NAME"
|
||||
|
||||
echo "==> Grabbing from $DEVICE"
|
||||
"$VENV/bin/python" "$GRABBER" -d "$DEVICE" -o "$OUT"
|
||||
echo "==> Saved: $OUT.png"
|
||||
|
|
@ -53,12 +53,18 @@ try:
|
|||
d=json.load(sys.stdin); print(d.get("id","") if isinstance(d,dict) else "")
|
||||
except Exception:
|
||||
print("")')"
|
||||
# Everything before v1.0.0 is flagged as a pre-release.
|
||||
if [ "${VER%%.*}" = "0" ]; then PRERELEASE=true; else PRERELEASE=false; fi
|
||||
|
||||
if [ -z "$ID" ]; then
|
||||
BODY="$(python3 -c 'import json,sys; print(json.dumps({"tag_name":sys.argv[1],"name":"FreeTRX "+sys.argv[1],"body":sys.argv[2],"draft":False,"prerelease":False}))' "$TAG" "$NOTES")"
|
||||
BODY="$(python3 -c 'import json,sys; print(json.dumps({"tag_name":sys.argv[1],"name":"FreeTRX "+sys.argv[1],"body":sys.argv[2],"draft":False,"prerelease":(sys.argv[3]=="true")}))' "$TAG" "$NOTES" "$PRERELEASE")"
|
||||
ID="$(curl -fsS -X POST "$API/releases" -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" -d "$BODY" | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')"
|
||||
echo "==> Created release $TAG (id=$ID)"
|
||||
echo "==> Created release $TAG (id=$ID, prerelease=$PRERELEASE)"
|
||||
else
|
||||
echo "==> Reusing existing release $TAG (id=$ID)"
|
||||
# Keep the pre-release flag correct on re-runs.
|
||||
curl -fsS -X PATCH "$API/releases/$ID" -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
|
||||
-d "$(python3 -c 'import json,sys; print(json.dumps({"prerelease":(sys.argv[1]=="true")}))' "$PRERELEASE")" >/dev/null
|
||||
echo "==> Reusing existing release $TAG (id=$ID, prerelease=$PRERELEASE)"
|
||||
fi
|
||||
|
||||
# Attach each staged binary, replacing an existing asset of the same name.
|
||||
|
|
|
|||
49
website/build-docs.sh
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Build the FreeTRX documentation website (MkDocs Material) entirely in Docker.
|
||||
# Source markdown lives in the repo root; this assembles it into _build/docs and
|
||||
# renders a static, self-contained HTML site with client-side search into
|
||||
# website/_build/site.
|
||||
#
|
||||
# Usage: ./website/build-docs.sh # build
|
||||
# ./website/build-docs.sh serve # live-reload preview on http://localhost:8000
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$HERE/.." && pwd)"
|
||||
SRC="$HERE/_build/docs"
|
||||
IMAGE="squidfunk/mkdocs-material:9"
|
||||
|
||||
# ---- assemble the docs source tree from the root markdown -----------------
|
||||
rm -rf "$HERE/_build"
|
||||
mkdir -p "$SRC/images"
|
||||
|
||||
cp "$HERE/home.md" "$SRC/index.md"
|
||||
cp "$ROOT/MANUAL.md" "$SRC/MANUAL.md"
|
||||
cp "$ROOT/TOOLS.md" "$SRC/TOOLS.md"
|
||||
cp "$ROOT/CHANGELOG.md" "$SRC/CHANGELOG.md"
|
||||
cp "$ROOT"/docs/images/*.png "$SRC/images/" 2>/dev/null || true
|
||||
|
||||
# MANUAL.md links back to README.md (a repo file, not a site page) -> point it at
|
||||
# the site home instead.
|
||||
sed -i.bak 's#(README.md)#(index.md)#g' "$SRC/MANUAL.md" && rm -f "$SRC/MANUAL.md.bak"
|
||||
|
||||
# Optional public URL (sitemap / canonical only). Set SITE_URL to override the
|
||||
# placeholder in mkdocs.yml — the site works at any URL regardless.
|
||||
ENVARGS=()
|
||||
if [[ -n "${SITE_URL:-}" ]]; then
|
||||
echo "==> Site URL: $SITE_URL"
|
||||
ENVARGS=(-e "SITE_URL=$SITE_URL")
|
||||
fi
|
||||
|
||||
# ---- build (or serve) -----------------------------------------------------
|
||||
if [[ "${1:-}" == "serve" ]]; then
|
||||
echo "==> Serving docs on http://localhost:8000 (Ctrl-C to stop)"
|
||||
docker run --rm -it -p 8000:8000 "${ENVARGS[@]+"${ENVARGS[@]}"}" -v "$HERE":/docs "$IMAGE" serve -a 0.0.0.0:8000
|
||||
else
|
||||
echo "==> Building docs site"
|
||||
docker run --rm "${ENVARGS[@]+"${ENVARGS[@]}"}" -v "$HERE":/docs "$IMAGE" build
|
||||
echo
|
||||
echo "==> Done. Static site (upload the *contents* of this folder):"
|
||||
echo " $HERE/_build/site"
|
||||
fi
|
||||
36
website/home.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# FreeTRX
|
||||
|
||||
**FreeTRX** is a fork of the [OpenGD77](https://opengd77.org) firmware for the
|
||||
**TYT MD-UV380 / Retevis RT-3S** and **Baofeng DM-1701 / Retevis RT-84**.
|
||||
|
||||
[Latest release :material-download:](https://git.dk1da.de/DK1DA/FreeTRX/releases){ .md-button .md-button--primary }
|
||||
[Source :material-git:](https://git.dk1da.de/DK1DA/FreeTRX){ .md-button }
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[User Manual](MANUAL.md)** — every feature, tagged 🟢 **FreeTRX** (new/changed)
|
||||
or ⚪ **OpenGD77** (inherited), with a button reference.
|
||||
- **[Building & Flashing](TOOLS.md)** — Docker build (`./build-firmware.sh`),
|
||||
flashing (`./flash.sh`), and grabbing screenshots (`./screenshot.sh`).
|
||||
- **[Changelog](CHANGELOG.md)** — version history.
|
||||
|
||||
## Supported radios
|
||||
|
||||
Developed and tested on the **TYT MD-UV380 / Retevis RT-3S**. A DM-1701 / RT-84
|
||||
build is also produced but not yet officially tested. Want to help test another
|
||||
OpenGD77-supported radio? Email **Marcus, DK1DA** at
|
||||
[qsl@dk1da.de](mailto:qsl@dk1da.de).
|
||||
|
||||
!!! note "License"
|
||||
FreeTRX inherits the OpenGD77 license (modified BSD 3-Clause with a
|
||||
non-commercial clause). It is not GPL-compatible and cannot be relicensed.
|
||||
63
website/mkdocs.yml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
site_name: FreeTRX Documentation
|
||||
site_description: User manual for the FreeTRX firmware (an OpenGD77 fork for MD-UV380 / DM-1701)
|
||||
site_author: Marcus, DK1DA
|
||||
# Public URL where the docs are hosted. Only affects the sitemap / canonical links /
|
||||
# search-share URLs — the site itself works at ANY domain or path (relative links).
|
||||
# Override at build time without editing this file: SITE_URL=https://your.url/ ./build-docs.sh
|
||||
site_url: !ENV [SITE_URL, "https://docs.example.com/"]
|
||||
repo_name: DK1DA/FreeTRX
|
||||
repo_url: https://git.dk1da.de/DK1DA/FreeTRX
|
||||
|
||||
# Source markdown is assembled into _build/docs by build-docs.sh; output goes to _build/site.
|
||||
docs_dir: _build/docs
|
||||
site_dir: _build/site
|
||||
use_directory_urls: true
|
||||
|
||||
theme:
|
||||
name: material
|
||||
language: en
|
||||
palette:
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: green
|
||||
accent: green
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: green
|
||||
accent: green
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
features:
|
||||
- navigation.top
|
||||
- navigation.sections
|
||||
- navigation.instant
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- search.share
|
||||
- content.code.copy
|
||||
- toc.follow
|
||||
|
||||
plugins:
|
||||
- search # built-in client-side (lunr.js) full-text search — no external service
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- User Manual: MANUAL.md
|
||||
- Building & Flashing: TOOLS.md
|
||||
- Changelog: CHANGELOG.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- tables
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.superfences
|
||||
- pymdownx.highlight
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||