Compare commits
47 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 | ||
|
|
dd1cc0ccdf | ||
|
|
1c59622a77 | ||
|
|
734b861917 | ||
|
|
7f5f3ab06f | ||
|
|
e50993b98f | ||
|
|
a838f9636c | ||
|
|
c842068896 | ||
|
|
9c2b597397 | ||
|
|
32f0e559bb |
80 changed files with 5825 additions and 168 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -5,3 +5,10 @@ codec_bin_section_1.bin
|
||||||
# Generated language plugin files + host-side builder binary
|
# Generated language plugin files + host-side builder binary
|
||||||
*.gla
|
*.gla
|
||||||
languages_builder
|
languages_builder
|
||||||
|
|
||||||
|
# Generated docs site
|
||||||
|
website/_build/
|
||||||
|
|
||||||
|
# Screenshot tool venv + scratch output
|
||||||
|
MDUV380_firmware/tools/.venv/
|
||||||
|
screenshots/
|
||||||
|
|
|
||||||
143
CHANGELOG.md
143
CHANGELOG.md
|
|
@ -9,6 +9,149 @@ single running list until the first versioned release.
|
||||||
|
|
||||||
_Nothing yet._
|
_Nothing yet._
|
||||||
|
|
||||||
|
## 0.11.0
|
||||||
|
|
||||||
|
- **CRT-style shut-off animation** — when powering off, the current screen
|
||||||
|
collapses towards the middle (squeezed to a line, then a dot) like an old CRT
|
||||||
|
switching off, replacing the "Power Off" message. (Colour displays only.)
|
||||||
|
|
||||||
|
## 0.10.1
|
||||||
|
|
||||||
|
- **Fix:** the muted **"M"** indicator in the status bar overlapped the GPS text
|
||||||
|
when GPS was enabled. The header icon is removed; the muted state is shown by
|
||||||
|
the flashing red LED alone.
|
||||||
|
|
||||||
|
## 0.10.0
|
||||||
|
|
||||||
|
- **SMS recipient from contacts** — when composing a message, press **GREEN** in
|
||||||
|
the recipient field to pick the destination from your private-call DMR
|
||||||
|
contacts (or type a DMR ID manually and press **DOWN** to move to the text).
|
||||||
|
- **Contacts list shows everything** — Menu → Contacts now lists all contact
|
||||||
|
types together under the title "Contacts" instead of defaulting to the group
|
||||||
|
filter; **HASH** still cycles the per-type filters (all / groups / private /
|
||||||
|
all-call).
|
||||||
|
|
||||||
|
## 0.9.0
|
||||||
|
|
||||||
|
- **Pride theme** — the status/header bar and the menu title bar are now painted
|
||||||
|
in the top rainbow-stripe colour so they blend into the background instead of
|
||||||
|
showing as white boxes.
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- **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*
|
||||||
|
("Screen Dim") is a raw literal, and the shared menu code voices labels via
|
||||||
|
`voicePromptsAppendLanguageString()`, which crashes on any pointer outside the
|
||||||
|
language table. That function now ignores non-language pointers, so a stray
|
||||||
|
literal is skipped instead of hard-faulting. (The dim value is still announced.)
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
- **Fix crash** opening Display Options with **voice announcements on**: the Screen
|
||||||
|
Dim value ("100%"/"50%"/…) is a plain literal, but the shared menu code fed it to
|
||||||
|
`voicePromptsAppendLanguageString()`, which derives the prompt index from the
|
||||||
|
string's offset within the language table — a foreign pointer produced a garbage
|
||||||
|
index and crashed. The value now goes through `voicePromptsAppendString()`. Also
|
||||||
|
moved the dim-level high bit off a low legacy option bit (bit 2 → 29) and clamped
|
||||||
|
the level so it can never index past the option arrays.
|
||||||
|
- **7-segment frequency font:** inactive segments are now off (background) instead
|
||||||
|
of faintly lit — no dim "ghosting". "MHz" now matches the frequency colour, and
|
||||||
|
there's 1px more space between the RX and TX frequency lines.
|
||||||
|
- **Screen dimming:** added a **15%** idle-dim level (Off / 100 / 50 / 30 / 15).
|
||||||
|
- **DM-1701 build support** — the Docker build is now parameterized by platform;
|
||||||
|
`./build-firmware.sh DM1701` produces `OpenDM1701.bin` (Baofeng DM-1701 /
|
||||||
|
Retevis RT-84). Not officially tested yet.
|
||||||
|
- **Release script** (`scripts/forgejo-release.sh`) now builds and attaches a
|
||||||
|
single platform or all of them, and reuses an existing release.
|
||||||
|
- README: added "What's different from OpenGD77" and a dedicated "Flashing"
|
||||||
|
section.
|
||||||
|
|
||||||
## 0.1.0
|
## 0.1.0
|
||||||
|
|
||||||
First versioned release of FreeTRX (fork of OpenGD77 R20260131).
|
First versioned release of FreeTRX (fork of OpenGD77 R20260131).
|
||||||
|
|
|
||||||
234
MANUAL.md
Normal file
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_H_
|
||||||
|
|
||||||
#define FREETRX_VERSION_MAJOR 0
|
#define FREETRX_VERSION_MAJOR 0
|
||||||
#define FREETRX_VERSION_MINOR 1
|
#define FREETRX_VERSION_MINOR 11
|
||||||
#define FREETRX_VERSION_PATCH 0
|
#define FREETRX_VERSION_PATCH 0
|
||||||
|
|
||||||
#define FREETRX_VERSION "0.1.0"
|
#define FREETRX_VERSION "0.11.0"
|
||||||
|
|
||||||
#endif // _FREETRX_VERSION_H_
|
#endif // _FREETRX_VERSION_H_
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,7 @@ typedef enum
|
||||||
CODEPLUG_CUSTOM_DATA_TYPE_SATELLITE_TLE,
|
CODEPLUG_CUSTOM_DATA_TYPE_SATELLITE_TLE,
|
||||||
CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY,
|
CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY,
|
||||||
CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT,
|
CODEPLUG_CUSTOM_DATA_TYPE_THEME_NIGHT,
|
||||||
|
CODEPLUG_CUSTOM_DATA_TYPE_THEME_PRESET, // FreeTRX: 1 byte, selected theme preset (themePreset_t)
|
||||||
} CodeplugCustomDataType_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);
|
void codeplugSetVFO_ChannelData(CodeplugChannel_t *vfoBuf, Channel_t VFONumber);
|
||||||
bool codeplugAllChannelsIndexIsInUse(int index);
|
bool codeplugAllChannelsIndexIsInUse(int index);
|
||||||
void codeplugAllChannelsIndexSetUsed(int index);
|
void codeplugAllChannelsIndexSetUsed(int index);
|
||||||
|
void codeplugAllChannelsIndexSetUnused(int index);
|
||||||
bool codeplugChannelSaveDataForIndex(int index, CodeplugChannel_t *channelBuf);
|
bool codeplugChannelSaveDataForIndex(int index, CodeplugChannel_t *channelBuf);
|
||||||
CodeplugCSSTypes_t codeplugGetCSSType(uint16_t tone);
|
CodeplugCSSTypes_t codeplugGetCSSType(uint16_t tone);
|
||||||
void codeplugConvertChannelInternalToCodeplug(CodeplugChannel_t *codeplugChannel, CodeplugChannel_t *internalChannel);
|
void codeplugConvertChannelInternalToCodeplug(CodeplugChannel_t *codeplugChannel, CodeplugChannel_t *internalChannel);
|
||||||
|
|
@ -442,6 +444,7 @@ bool codeplugGetGeneralSettings(CodeplugGeneralSettings_t *generalSettingsBuffer
|
||||||
bool codeplugGetSignallingDTMF(CodeplugSignallingDTMF_t *signallingDTMFBuffer);
|
bool codeplugGetSignallingDTMF(CodeplugSignallingDTMF_t *signallingDTMFBuffer);
|
||||||
bool codeplugGetSignallingDTMFDurations(CodeplugSignallingDTMFDurations_t *signallingDTMFDurationsBuffer);
|
bool codeplugGetSignallingDTMFDurations(CodeplugSignallingDTMFDurations_t *signallingDTMFDurationsBuffer);
|
||||||
bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||||
|
bool codeplugZoneRemoveChannelFromZoneAndSave(int channelIndex, CodeplugZone_t *zoneBuf);
|
||||||
bool codeplugGetDeviceInfo(CodeplugDeviceInfo_t *deviceInfoBuffer);
|
bool codeplugGetDeviceInfo(CodeplugDeviceInfo_t *deviceInfoBuffer);
|
||||||
|
|
||||||
uint16_t codeplugGetQuickkeyFunctionID(char key);
|
uint16_t codeplugGetQuickkeyFunctionID(char key);
|
||||||
|
|
|
||||||
136
MDUV380_firmware/application/include/functions/dmrDataProtocol.h
Normal file
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_ */
|
||||||
|
|
@ -139,7 +139,7 @@ typedef enum
|
||||||
BIT_POWEROFF_SUSPEND = (SETTINGS_BITS_BANK_0 | (1 << 8)),
|
BIT_POWEROFF_SUSPEND = (SETTINGS_BITS_BANK_0 | (1 << 8)),
|
||||||
#endif
|
#endif
|
||||||
BIT_SATELLITE_MANUAL_AUTO = (SETTINGS_BITS_BANK_0 | (1 << 9)),
|
BIT_SATELLITE_MANUAL_AUTO = (SETTINGS_BITS_BANK_0 | (1 << 9)),
|
||||||
BIT_SCREEN_DIM_0 = (SETTINGS_BITS_BANK_0 | (1 << 10)), // low bit of the 2-bit screen-dim level
|
BIT_SCREEN_DIM_0 = (SETTINGS_BITS_BANK_0 | (1 << 10)), // low bit of the 3-bit screen-dim level
|
||||||
#if defined(PLATFORM_MD9600)
|
#if defined(PLATFORM_MD9600)
|
||||||
BIT_SPEAKER_CLICK_SUPPRESS = (SETTINGS_BITS_BANK_0 | (1 << 11)),
|
BIT_SPEAKER_CLICK_SUPPRESS = (SETTINGS_BITS_BANK_0 | (1 << 11)),
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -153,7 +153,8 @@ typedef enum
|
||||||
BIT_VISUAL_VOLUME = (SETTINGS_BITS_BANK_0 | (1 << 18)),
|
BIT_VISUAL_VOLUME = (SETTINGS_BITS_BANK_0 | (1 << 18)),
|
||||||
#endif
|
#endif
|
||||||
BIT_SECONDARY_LANGUAGE = (SETTINGS_BITS_BANK_0 | (1 << 19)),
|
BIT_SECONDARY_LANGUAGE = (SETTINGS_BITS_BANK_0 | (1 << 19)),
|
||||||
BIT_SCREEN_DIM_1 = (SETTINGS_BITS_BANK_0 | (1 << 20)), // high bit of the 2-bit screen-dim level
|
BIT_SCREEN_DIM_1 = (SETTINGS_BITS_BANK_0 | (1 << 20)), // middle bit of the 3-bit screen-dim level
|
||||||
|
BIT_SCREEN_DIM_2 = (SETTINGS_BITS_BANK_0 | (1 << 29)), // high bit of the 3-bit screen-dim level (kept off low legacy bits)
|
||||||
BIT_DISPLAY_CHANNEL_DISTANCE = (SETTINGS_BITS_BANK_0 | (1 << 21)),
|
BIT_DISPLAY_CHANNEL_DISTANCE = (SETTINGS_BITS_BANK_0 | (1 << 21)),
|
||||||
#if defined(PLATFORM_MD2017)
|
#if defined(PLATFORM_MD2017)
|
||||||
BIT_TRACKBALL_ENABLED = (SETTINGS_BITS_BANK_0 | (1 << 22)),
|
BIT_TRACKBALL_ENABLED = (SETTINGS_BITS_BANK_0 | (1 << 22)),
|
||||||
|
|
@ -420,6 +421,7 @@ typedef enum
|
||||||
SCREEN_DIM_100, // stay at 100% of configured brightness when idle
|
SCREEN_DIM_100, // stay at 100% of configured brightness when idle
|
||||||
SCREEN_DIM_50, // dim to 50%
|
SCREEN_DIM_50, // dim to 50%
|
||||||
SCREEN_DIM_30, // dim to 30%
|
SCREEN_DIM_30, // dim to 30%
|
||||||
|
SCREEN_DIM_15, // dim to 15%
|
||||||
SCREEN_DIM_NUM
|
SCREEN_DIM_NUM
|
||||||
} screenDimLevel_t;
|
} screenDimLevel_t;
|
||||||
uint8_t settingsGetScreenDimLevel(void);
|
uint8_t settingsGetScreenDimLevel(void);
|
||||||
|
|
|
||||||
114
MDUV380_firmware/application/include/functions/smsCore.h
Normal file
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
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 int16_t melody_generic[512];
|
||||||
extern const int16_t MELODY_POWER_ON[];
|
extern const int16_t MELODY_POWER_ON[];
|
||||||
extern const int16_t MELODY_PRIVATE_CALL[];
|
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_BEEP[];
|
||||||
extern const int16_t MELODY_KEY_LONG_BEEP[];
|
extern const int16_t MELODY_KEY_LONG_BEEP[];
|
||||||
extern const int16_t MELODY_ACK_BEEP[];
|
extern const int16_t MELODY_ACK_BEEP[];
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,9 @@ void HRC6000ClearColorCodeSynchronisation(void);
|
||||||
void HRC6000SetTalkerAlias(const char *text);
|
void HRC6000SetTalkerAlias(const char *text);
|
||||||
void HRC6000SetTalkerAliasLocation(uint32_t Lat, uint32_t Lon);
|
void HRC6000SetTalkerAliasLocation(uint32_t Lat, uint32_t Lon);
|
||||||
bool HRC6000IRQHandlerIsRunning(void);
|
bool HRC6000IRQHandlerIsRunning(void);
|
||||||
|
bool HRC6000StartSmsTransmission(void);
|
||||||
|
bool HRC6000IsSendingSMS(void);
|
||||||
|
void HRC6000CancelSmsTransmission(void);
|
||||||
bool HRC6000HasGotSync(void);
|
bool HRC6000HasGotSync(void);
|
||||||
bool HRC6000CCIsHeld(void);
|
bool HRC6000CCIsHeld(void);
|
||||||
void HRC6000SetDmrRxGain(int8_t gain);
|
void HRC6000SetDmrRxGain(int8_t gain);
|
||||||
|
|
|
||||||
|
|
@ -269,8 +269,25 @@ void displayDrawFrequencySevenSeg(int16_t x, int16_t y, int16_t cellW, int16_t c
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(HAS_COLOURS)
|
#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 themeInitToDefaultValues(DayTime_t daytime, bool invert);
|
||||||
void themeInit(bool SPIFlashAvailable);
|
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 displayThemeApply(themeItem_t fgItem, themeItem_t bgItem);
|
||||||
void displayThemeResetToDefault(void);
|
void displayThemeResetToDefault(void);
|
||||||
bool displayThemeIsForegroundColourEqualTo(themeItem_t fgItem);
|
bool displayThemeIsForegroundColourEqualTo(themeItem_t fgItem);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ void torchToggle(void);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void LedWrite(LEDs_t theLED, uint8_t output);
|
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);
|
uint8_t LedRead(LEDs_t theLED);
|
||||||
void LedWriteDirect(LEDs_t theLED, uint8_t output);
|
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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Taille texte",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Der. locut.", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Der. locut.", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Sourdine", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text Grösse",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Formato font",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Ultimo OM RX", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Ultimo OM RX", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Muto", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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)
|
.text_size = "Text size",//MaxLen: 16 (with ':' + 1 digit)
|
||||||
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
.last_talker = "Last talker", // MaxLen: 16 (with ':' + .off or 2 digits value + 's')
|
||||||
.mute = "Mute", // MaxLen: 16 (with ':' + .on or .off)
|
.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
|
#else
|
||||||
#define LANGUAGE_TAG_MAGIC_NUMBER { 'G', 'D', '7', '7' }, { 'L', 'A', 'N', 'G' } // Never change this tag.
|
#define LANGUAGE_TAG_MAGIC_NUMBER { 'G', 'D', '7', '7' }, { 'L', 'A', 'N', 'G' } // Never change this tag.
|
||||||
#endif
|
#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
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -335,6 +335,24 @@ typedef struct
|
||||||
const char text_size[LANGUAGE_TEXTS_LENGTH];
|
const char text_size[LANGUAGE_TEXTS_LENGTH];
|
||||||
const char last_talker[LANGUAGE_TEXTS_LENGTH];
|
const char last_talker[LANGUAGE_TEXTS_LENGTH];
|
||||||
const char mute[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;
|
} stringsTable_t;
|
||||||
|
|
||||||
#endif // _OPENGD77_UILANGUAGE_H_
|
#endif // _OPENGD77_UILANGUAGE_H_
|
||||||
|
|
|
||||||
|
|
@ -172,9 +172,6 @@ typedef struct
|
||||||
|
|
||||||
void menuDisplayTitle(const char *title);
|
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);
|
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
|
// menuGetMenuOffset() return values
|
||||||
#define MENU_OFFSET_BEFORE_FIRST_ENTRY -1
|
#define MENU_OFFSET_BEFORE_FIRST_ENTRY -1
|
||||||
|
|
@ -270,6 +267,9 @@ bool uiNotificationHasTimedOut(void);
|
||||||
bool uiNotificationIsVisible(void);
|
bool uiNotificationIsVisible(void);
|
||||||
void uiNotificationHide(bool immediateRender);
|
void uiNotificationHide(bool immediateRender);
|
||||||
uiNotificationID_t uiNotificationGetId(void);
|
uiNotificationID_t uiNotificationGetId(void);
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
uint16_t *uiNotificationGetOffscreenBuffer(void); // full-screen scratch (borrowed by the shutdown animation)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#if defined(HAS_GPS)
|
#if defined(HAS_GPS)
|
||||||
|
|
@ -323,7 +323,16 @@ enum MENU_SCREENS
|
||||||
#if !defined(PLATFORM_GD77S)
|
#if !defined(PLATFORM_GD77S)
|
||||||
MENU_APRS,
|
MENU_APRS,
|
||||||
#endif
|
#endif
|
||||||
|
MENU_SMS,// DMR text messages hub
|
||||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
// *** 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_MESSAGE_BOX,
|
||||||
UI_HOTSPOT_MODE,
|
UI_HOTSPOT_MODE,
|
||||||
UI_CPS,
|
UI_CPS,
|
||||||
|
|
@ -356,6 +365,8 @@ enum CHANNEL_SCREEN_QUICK_MENU_ITEMS
|
||||||
CH_SCREEN_QUICK_MENU_TALKAROUND,
|
CH_SCREEN_QUICK_MENU_TALKAROUND,
|
||||||
CH_SCREEN_QUICK_MENU_ROAMING,
|
CH_SCREEN_QUICK_MENU_ROAMING,
|
||||||
CH_SCREEN_QUICK_MENU_AUDIO_MUTE,
|
CH_SCREEN_QUICK_MENU_AUDIO_MUTE,
|
||||||
|
CH_SCREEN_QUICK_MENU_NEW_CHANNEL,
|
||||||
|
CH_SCREEN_QUICK_MENU_DELETE_CHANNEL,
|
||||||
NUM_CH_SCREEN_QUICK_MENU_ITEMS
|
NUM_CH_SCREEN_QUICK_MENU_ITEMS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -438,6 +449,7 @@ extern menuDataGlobal_t menuDataGlobal;
|
||||||
extern const menuItemsList_t menuDataMainMenu;
|
extern const menuItemsList_t menuDataMainMenu;
|
||||||
extern const menuItemsList_t menuDataContact;
|
extern const menuItemsList_t menuDataContact;
|
||||||
extern const menuItemsList_t menuDataOptions;
|
extern const menuItemsList_t menuDataOptions;
|
||||||
|
extern const menuItemsList_t menuDataSMS;
|
||||||
extern bool lockscreenIsRearming;
|
extern bool lockscreenIsRearming;
|
||||||
|
|
||||||
menuStatus_t uiVFOMode(uiEvent_t *event, bool isFirstRun);
|
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);
|
menuStatus_t menuColourPicker(uiEvent_t *ev, bool isFirstRun);
|
||||||
#endif
|
#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
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ typedef uint32_t time_t_custom; /* date/time in unix secs past 1-Jan-70 */
|
||||||
#define DISPLAY_Y_POS_SQL_INFO (25 + 8 + DISPLAY_MAIN_CONTENT_TOP_PAD)
|
#define DISPLAY_Y_POS_SQL_INFO (25 + 8 + DISPLAY_MAIN_CONTENT_TOP_PAD)
|
||||||
#define DISPLAY_Y_POS_TX_TIMER (8 + 16)
|
#define DISPLAY_Y_POS_TX_TIMER (8 + 16)
|
||||||
#define DISPLAY_Y_POS_RX_FREQ (40 + 40)
|
#define DISPLAY_Y_POS_RX_FREQ (40 + 40)
|
||||||
#define DISPLAY_Y_POS_TX_FREQ (48 + 48)
|
#define DISPLAY_Y_POS_TX_FREQ (48 + 48 + 1) // +1px extra gap below the RX frequency line
|
||||||
#define DISPLAY_Y_POS_ZONE (50 + DISPLAY_V_EXTRA_PIXELS)
|
#define DISPLAY_Y_POS_ZONE (50 + DISPLAY_V_EXTRA_PIXELS)
|
||||||
#define DISPLAY_Y_POS_RSSI_VALUE (18 + 16)
|
#define DISPLAY_Y_POS_RSSI_VALUE (18 + 16)
|
||||||
#define DISPLAY_Y_POS_RSSI_BAR (40 + DISPLAY_V_OFFSET)
|
#define DISPLAY_Y_POS_RSSI_BAR (40 + DISPLAY_V_OFFSET)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
#include "io/Leds.h"
|
#include "io/Leds.h"
|
||||||
#include "interfaces/adc.h"
|
#include "interfaces/adc.h"
|
||||||
#include "hardware/radioHardwareInterface.h"
|
#include "hardware/radioHardwareInterface.h"
|
||||||
|
#include "hardware/HR-C6000.h" // slotState, for restoring the green RX LED after unmute
|
||||||
|
|
||||||
#include "io/buttons.h"
|
#include "io/buttons.h"
|
||||||
#include "usb/usb_com.h"
|
#include "usb/usb_com.h"
|
||||||
|
|
@ -52,6 +53,8 @@
|
||||||
#include "interfaces/settingsStorage.h"
|
#include "interfaces/settingsStorage.h"
|
||||||
#include "interfaces/adc.h"
|
#include "interfaces/adc.h"
|
||||||
#include "functions/rxPowerSaving.h"
|
#include "functions/rxPowerSaving.h"
|
||||||
|
#include "functions/smsCore.h"
|
||||||
|
#include "functions/smsStorage.h"
|
||||||
|
|
||||||
#if defined(USING_EXTERNAL_DEBUGGER)
|
#if defined(USING_EXTERNAL_DEBUGGER)
|
||||||
#include "SeggerRTT/RTT/SEGGER_RTT.h"
|
#include "SeggerRTT/RTT/SEGGER_RTT.h"
|
||||||
|
|
@ -62,6 +65,68 @@
|
||||||
// Couldn't declare into aprs.h, due to header cross-dependence.
|
// Couldn't declare into aprs.h, due to header cross-dependence.
|
||||||
void aprsBeaconingTick(uiEvent_t *ev);
|
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;
|
volatile bool mainIsRunning = true;
|
||||||
static bool updateMessageOnScreen = false;
|
static bool updateMessageOnScreen = false;
|
||||||
int8_t lastVolume = 0;
|
int8_t lastVolume = 0;
|
||||||
|
|
@ -505,6 +570,9 @@ void applicationMainTask(void)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
smsStorageInit();
|
||||||
|
smsCoreInit();
|
||||||
|
|
||||||
menuSystemInit(getRtcTime_custom());
|
menuSystemInit(getRtcTime_custom());
|
||||||
|
|
||||||
#if defined(HAS_GPS)
|
#if defined(HAS_GPS)
|
||||||
|
|
@ -1030,6 +1098,7 @@ void applicationMainTask(void)
|
||||||
|
|
||||||
if ((trxGetMode() != RADIO_MODE_NONE) &&
|
if ((trxGetMode() != RADIO_MODE_NONE) &&
|
||||||
(settingsUsbMode != USB_MODE_HOTSPOT) &&
|
(settingsUsbMode != USB_MODE_HOTSPOT) &&
|
||||||
|
(HRC6000IsSendingSMS() == false) && // no voice TX while a message is being sent
|
||||||
(currentMenu != UI_POWER_OFF) &&
|
(currentMenu != UI_POWER_OFF) &&
|
||||||
(currentMenu != UI_SPLASH_SCREEN) &&
|
(currentMenu != UI_SPLASH_SCREEN) &&
|
||||||
(currentMenu != UI_TX_SCREEN)&&
|
(currentMenu != UI_TX_SCREEN)&&
|
||||||
|
|
@ -1378,6 +1447,9 @@ void applicationMainTask(void)
|
||||||
voxTick();
|
voxTick();
|
||||||
gpsTick();
|
gpsTick();
|
||||||
aprsBeaconingTick(&ev);
|
aprsBeaconingTick(&ev);
|
||||||
|
smsCoreTick();
|
||||||
|
smsStorageTick();
|
||||||
|
smsHandleEvents();
|
||||||
settingsSaveIfNeeded(false);
|
settingsSaveIfNeeded(false);
|
||||||
|
|
||||||
if (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER))
|
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 (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);
|
bool flashOn = ((ticksGetMillis() % 2000) < 500);
|
||||||
LedWrite(LED_GREEN, 0);
|
|
||||||
LedWrite(LED_RED, (flashOn ? 1 : 0));
|
LedWrite(LED_RED, (flashOn ? 1 : 0));
|
||||||
muteFlashActive = true;
|
|
||||||
}
|
}
|
||||||
else if (muteFlashActive)
|
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_RED, 0);
|
||||||
LedWrite(LED_GREEN, 0);
|
LedWrite(LED_GREEN, (rxGreen ? 1 : 0));
|
||||||
muteFlashActive = false;
|
muteFlashActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,46 @@ bool codeplugZoneAddChannelToZoneAndSave(int channelIndex, CodeplugZone_t *zoneB
|
||||||
return false;
|
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)
|
static uint16_t codeplugAllChannelsGetCount(void)
|
||||||
{
|
{
|
||||||
uint16_t c = 0;
|
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)
|
void codeplugAllChannelsInitCache(void)
|
||||||
{
|
{
|
||||||
// There are 8 banks
|
// There are 8 banks
|
||||||
|
|
|
||||||
571
MDUV380_firmware/application/source/functions/dmrDataProtocol.c
Normal file
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;
|
||||||
|
}
|
||||||
|
|
@ -607,13 +607,16 @@ bool settingsIsOptionBitSet(bitfieldOptions_t bit)
|
||||||
// no new settings field (existing codeplugs keep both bits clear = SCREEN_DIM_OFF).
|
// no new settings field (existing codeplugs keep both bits clear = SCREEN_DIM_OFF).
|
||||||
uint8_t settingsGetScreenDimLevel(void)
|
uint8_t settingsGetScreenDimLevel(void)
|
||||||
{
|
{
|
||||||
return (settingsIsOptionBitSet(BIT_SCREEN_DIM_0) ? 1 : 0) | (settingsIsOptionBitSet(BIT_SCREEN_DIM_1) ? 2 : 0);
|
uint8_t level = (settingsIsOptionBitSet(BIT_SCREEN_DIM_0) ? 1 : 0) | (settingsIsOptionBitSet(BIT_SCREEN_DIM_1) ? 2 : 0) | (settingsIsOptionBitSet(BIT_SCREEN_DIM_2) ? 4 : 0);
|
||||||
|
// Never return an out-of-range level: dimOpts[]/dimPercent[] are SCREEN_DIM_NUM long.
|
||||||
|
return (level < SCREEN_DIM_NUM) ? level : SCREEN_DIM_OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
void settingsSetScreenDimLevel(uint8_t level)
|
void settingsSetScreenDimLevel(uint8_t level)
|
||||||
{
|
{
|
||||||
settingsSetOptionBit(BIT_SCREEN_DIM_0, ((level & 1) != 0));
|
settingsSetOptionBit(BIT_SCREEN_DIM_0, ((level & 1) != 0));
|
||||||
settingsSetOptionBit(BIT_SCREEN_DIM_1, ((level & 2) != 0));
|
settingsSetOptionBit(BIT_SCREEN_DIM_1, ((level & 2) != 0));
|
||||||
|
settingsSetOptionBit(BIT_SCREEN_DIM_2, ((level & 4) != 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
void settingsSetDirty(void)
|
void settingsSetDirty(void)
|
||||||
|
|
|
||||||
719
MDUV380_firmware/application/source/functions/smsCore.c
Normal file
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
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
|
880, DIT_LENGTH
|
||||||
|
|
||||||
-1, -1 };// Morse letters PC for Private Call
|
-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_BEEP[] = { 600, 60, -1, -1 };
|
||||||
const int16_t MELODY_KEY_LONG_BEEP[] = { 880, 60, -1, -1 };
|
const int16_t MELODY_KEY_LONG_BEEP[] = { 880, 60, -1, -1 };
|
||||||
/* These melodies are not currently used
|
/* These melodies are not currently used
|
||||||
|
|
|
||||||
|
|
@ -343,13 +343,34 @@ void voicePromptsAppendLanguageString(const char *languageStringAdd)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
voicePromptsAppendPrompt(NUM_VOICE_PROMPTS +
|
// The prompt index is derived from the pointer's offset within the currentLanguage
|
||||||
|
// table, so a pointer that isn't actually inside that table (e.g. a raw string
|
||||||
|
// literal passed by mistake) would yield an out-of-range prompt and crash. Ignore it.
|
||||||
|
const char *languageBase = ¤tLanguage->LANGUAGE_NAME[0];
|
||||||
|
if ((languageStringAdd < languageBase) || (languageStringAdd >= (languageBase + sizeof(*currentLanguage))))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int promptNumber = NUM_VOICE_PROMPTS +
|
||||||
((languageStringAdd - currentLanguage->LANGUAGE_NAME)
|
((languageStringAdd - currentLanguage->LANGUAGE_NAME)
|
||||||
#if ! defined(HAS_COLOURS)
|
#if ! defined(HAS_COLOURS)
|
||||||
- ((languageStringAdd >= currentLanguage->theme_chooser) ?
|
- ((languageStringAdd >= currentLanguage->theme_chooser) ?
|
||||||
((currentLanguage->theme_colour_picker_blue - currentLanguage->theme_chooser) + LANGUAGE_TEXTS_LENGTH) : 0)
|
((currentLanguage->theme_colour_picker_blue - currentLanguage->theme_chooser) + LANGUAGE_TEXTS_LENGTH) : 0)
|
||||||
#endif
|
#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)
|
void voicePromptsPlay(void)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
#include "interfaces/interrupts.h"
|
#include "interfaces/interrupts.h"
|
||||||
#include "functions/rxPowerSaving.h"
|
#include "functions/rxPowerSaving.h"
|
||||||
#include "functions/ticks.h"
|
#include "functions/ticks.h"
|
||||||
|
#include "functions/smsCore.h"
|
||||||
#include "interfaces/gps.h"
|
#include "interfaces/gps.h"
|
||||||
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
||||||
#include "hardware/radioHardwareInterface.h"
|
#include "hardware/radioHardwareInterface.h"
|
||||||
|
|
@ -253,6 +254,9 @@ static struct
|
||||||
int hotspotPostponedFrameHandling;
|
int hotspotPostponedFrameHandling;
|
||||||
char talkAliasText[33];
|
char talkAliasText[33];
|
||||||
uint8_t talkAliasLocation[7];
|
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 = {
|
} hrc = {
|
||||||
.hasEncodedAudio = false,
|
.hasEncodedAudio = false,
|
||||||
.hasAudioData = false,
|
.hasAudioData = false,
|
||||||
|
|
@ -299,7 +303,10 @@ static struct
|
||||||
.wakeTriesCount = 0,
|
.wakeTriesCount = 0,
|
||||||
.hotspotPostponedFrameHandling = 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 },
|
.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;
|
volatile int slotState = DMR_STATE_IDLE;
|
||||||
|
|
@ -826,6 +833,14 @@ void PORTC_IRQHandler(void)
|
||||||
#endif
|
#endif
|
||||||
static inline void hrc6000SysSendRejectedInt(void)
|
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)
|
static inline void hrc6000SysSendStartInt(void)
|
||||||
|
|
@ -895,6 +910,11 @@ static inline void hrc6000SysSendEndInt(void)
|
||||||
|
|
||||||
In MSK mode, there is no substate interrupt.
|
In MSK mode, there is no substate interrupt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
if (reg_0x86 & 0x08) // confirmed-data feedback packet was received
|
||||||
|
{
|
||||||
|
smsTxNotifyHwAck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void hrc6000SysPostAccessInt(void)
|
static inline void hrc6000SysPostAccessInt(void)
|
||||||
|
|
@ -1040,6 +1060,24 @@ static inline void hrc6000SysReceivedDataInt(void)
|
||||||
hrc.skipCount--;
|
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
|
// Check for correct received packet
|
||||||
if (hrc6000CrcIsValid() && (rxPrivacyIndicator == 0) && (slotState < DMR_STATE_TX_START_1))
|
if (hrc6000CrcIsValid() && (rxPrivacyIndicator == 0) && (slotState < DMR_STATE_TX_START_1))
|
||||||
{
|
{
|
||||||
|
|
@ -1470,6 +1508,48 @@ static void hrc6000TransitionToTx(void)
|
||||||
hrc.txSequence = 0;
|
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.
|
//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)
|
void hrc6000TxBurstCallback(void)
|
||||||
{
|
{
|
||||||
|
|
@ -1712,9 +1792,16 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
|
|
||||||
case DMR_STATE_TX_START_1: // Start TX (second step)
|
case DMR_STATE_TX_START_1: // Start TX (second step)
|
||||||
LedWrite(LED_RED, 1); // for repeater wakeup
|
LedWrite(LED_RED, 1); // for repeater wakeup
|
||||||
hrc6000SendPcOrTgLCHeader();
|
if (hrc.smsActive)
|
||||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
{
|
||||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
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;
|
trxIsTransmitting = true;
|
||||||
slotState = DMR_STATE_TX_START_2;
|
slotState = DMR_STATE_TX_START_2;
|
||||||
break;
|
break;
|
||||||
|
|
@ -1725,8 +1812,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DMR_STATE_TX_START_3: // Start TX (fourth step)
|
case DMR_STATE_TX_START_3: // Start TX (fourth step)
|
||||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
|
if (hrc.smsActive)
|
||||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
{
|
||||||
|
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;
|
slotState = DMR_STATE_TX_START_4;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -1734,7 +1828,7 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
SPI0WritePageRegByte(0x04, 0x41, 0x00); // Do nothing on the next TS
|
SPI0WritePageRegByte(0x04, 0x41, 0x00); // Do nothing on the next TS
|
||||||
slotState = DMR_STATE_TX_START_5;
|
slotState = DMR_STATE_TX_START_5;
|
||||||
|
|
||||||
if (settingsUsbMode != USB_MODE_HOTSPOT)
|
if ((settingsUsbMode != USB_MODE_HOTSPOT) && (hrc.smsActive == false))
|
||||||
{
|
{
|
||||||
hrc.ambeBufferCount = 0;
|
hrc.ambeBufferCount = 0;
|
||||||
hrc.deferredUpdateBufferOutPtr = deferredUpdateBuffer;
|
hrc.deferredUpdateBufferOutPtr = deferredUpdateBuffer;
|
||||||
|
|
@ -1744,8 +1838,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DMR_STATE_TX_START_5: // Start TX (sixth step)
|
case DMR_STATE_TX_START_5: // Start TX (sixth step)
|
||||||
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
|
if (hrc.smsActive)
|
||||||
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
|
{
|
||||||
|
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;
|
hrc.TAPhase = 0;
|
||||||
slotState = DMR_STATE_TX_1;
|
slotState = DMR_STATE_TX_1;
|
||||||
break;
|
break;
|
||||||
|
|
@ -1763,6 +1864,15 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
|
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 (hrc.transmissionEnabled)
|
||||||
{
|
{
|
||||||
if (settingsUsbMode == USB_MODE_HOTSPOT)
|
if (settingsUsbMode == USB_MODE_HOTSPOT)
|
||||||
|
|
@ -1826,7 +1936,9 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DMR_STATE_TX_END_1: // Stop TX (first step)
|
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();
|
hrc6000SendPcOrTgLCHeader();
|
||||||
}
|
}
|
||||||
|
|
@ -1837,6 +1949,13 @@ void hrc6000TimeslotInterruptHandler(void)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DMR_STATE_TX_END_2: // Stop TX (second step)
|
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
|
// 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();
|
dmrMonitorCapturedTS = hrc.tsLockedTS = trxGetDMRTimeSlot();
|
||||||
hrc.dmrMonitorCapturedTimeout = nonVolatileSettings.dmrCaptureTimeout * 1000;
|
hrc.dmrMonitorCapturedTimeout = nonVolatileSettings.dmrCaptureTimeout * 1000;
|
||||||
|
|
@ -2017,6 +2136,9 @@ static void hrc6000InitDigitalState(void)
|
||||||
hrc.interruptTimeout = 0;
|
hrc.interruptTimeout = 0;
|
||||||
slotState = DMR_STATE_IDLE;
|
slotState = DMR_STATE_IDLE;
|
||||||
trxIsTransmitting = false;
|
trxIsTransmitting = false;
|
||||||
|
hrc.smsActive = false;
|
||||||
|
hrc.smsJob = NULL;
|
||||||
|
hrc.smsFrameIndex = 0;
|
||||||
hrc.tickCount = 0;
|
hrc.tickCount = 0;
|
||||||
hrc.skipCount = 0;
|
hrc.skipCount = 0;
|
||||||
hrc.tsAgreed = 0;
|
hrc.tsAgreed = 0;
|
||||||
|
|
@ -2761,6 +2883,49 @@ bool HRC6000IRQHandlerIsRunning(void)
|
||||||
return hrc.inIRQHandler;
|
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)
|
bool HRC6000HasGotSync(void)
|
||||||
{
|
{
|
||||||
return (hrc.timeCode != -1);
|
return (hrc.timeCode != -1);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ static uint16_t backgroundColour = 0xFFFFU;
|
||||||
|
|
||||||
static uint16_t screenBufData[DISPLAY_SIZE_X * DISPLAY_SIZE_Y];
|
static uint16_t screenBufData[DISPLAY_SIZE_X * DISPLAY_SIZE_Y];
|
||||||
uint16_t *screenBuf = screenBufData;
|
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
|
//#define DISPLAY_CHECK_BOUNDS
|
||||||
|
|
||||||
#ifdef 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)
|
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
|
// may be able to do this using DMA
|
||||||
for(int i = 0; i < DISPLAY_SIZE_X * DISPLAY_SIZE_Y; i++)
|
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++)
|
for(int i = startRow; i < endRow; i++)
|
||||||
{
|
{
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
screenBuf[i] = ((prideBackgroundActive && (isInverted == false)) ? pridePixelColour(i) : fillColour);
|
||||||
|
#else
|
||||||
screenBuf[i] = fillColour;
|
screenBuf[i] = fillColour;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1106,11 +1135,9 @@ void displayDrawFrequencySevenSeg(int16_t x, int16_t y, int16_t cellW, int16_t c
|
||||||
uint16_t litColour = themeItems[DAYTIME_CURRENT][fgItem];
|
uint16_t litColour = themeItems[DAYTIME_CURRENT][fgItem];
|
||||||
uint16_t bgColour = themeItems[DAYTIME_CURRENT][THEME_ITEM_BG];
|
uint16_t bgColour = themeItems[DAYTIME_CURRENT][THEME_ITEM_BG];
|
||||||
|
|
||||||
// dimColour: same hue at ~3/8 intensity (round-trip through RGB888) so the
|
// Inactive segments are drawn in the background colour (i.e. off), so only the
|
||||||
// inactive segments stay clearly visible without washing out the lit ones.
|
// lit segments show (no dim "ghosting").
|
||||||
uint32_t rgb = PLATFORM_COLOUR_FORMAT_TO_RGB888(__builtin_bswap16(litColour));
|
uint16_t dimColour = bgColour;
|
||||||
uint32_t dimRgb = (((((rgb >> 16) & 0xFF) * 3) / 8) << 16) | (((((rgb >> 8) & 0xFF) * 3) / 8) << 8) | ((((rgb) & 0xFF) * 3) / 8);
|
|
||||||
uint16_t dimColour = __builtin_bswap16(RGB888_TO_PLATFORM_COLOUR_FORMAT(dimRgb));
|
|
||||||
|
|
||||||
for (const char *p = str; *p != '\0'; p++)
|
for (const char *p = str; *p != '\0'; p++)
|
||||||
{
|
{
|
||||||
|
|
@ -1512,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));
|
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(NIGHT, true);
|
||||||
themeInitToDefaultValues(DAY, false);
|
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)
|
if (SPIFlashAvailable)
|
||||||
{
|
{
|
||||||
uint16_t themingTmp[THEME_ITEM_MAX];
|
themeApplyPreset(themeGetPreset());
|
||||||
|
}
|
||||||
if (codeplugGetOpenGD77CustomData(CODEPLUG_CUSTOM_DATA_TYPE_THEME_DAY, (uint8_t *) &themingTmp))
|
else
|
||||||
{
|
{
|
||||||
memcpy(&themeItems[DAY], &themingTmp, sizeof(themingTmp));
|
themeInitToDefaultValues(NIGHT, true);
|
||||||
|
themeInitToDefaultValues(DAY, false);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT_OVERRIDE) && (uiDataGlobal.daytimeOverridden == NIGHT))
|
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT_OVERRIDE) && (uiDataGlobal.daytimeOverridden == NIGHT))
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,28 @@ void LEDsInit(void)
|
||||||
#endif
|
#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)
|
void LedWrite(LEDs_t theLED, uint8_t output)
|
||||||
{
|
{
|
||||||
#if ! defined(PLATFORM_GD77S)
|
#if ! defined(PLATFORM_GD77S)
|
||||||
|
if ((theLED == LED_GREEN) && greenLedSuppressed)
|
||||||
|
{
|
||||||
|
output = 0; // green is held off (e.g. while muted)
|
||||||
|
}
|
||||||
|
|
||||||
LEDsState[theLED] = output;
|
LEDsState[theLED] = output;
|
||||||
|
|
||||||
if (settingsIsOptionBitSet(BIT_ALL_LEDS_DISABLED) == 0)
|
if (settingsIsOptionBitSet(BIT_ALL_LEDS_DISABLED) == 0)
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,7 @@ int displayGetIdleBacklightPercentage(void)
|
||||||
{
|
{
|
||||||
if (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO)
|
if (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO)
|
||||||
{
|
{
|
||||||
static const uint8_t dimPercent[SCREEN_DIM_NUM] = { 0, 100, 50, 30 };
|
static const uint8_t dimPercent[SCREEN_DIM_NUM] = { 0, 100, 50, 30, 15 };
|
||||||
uint8_t level = settingsGetScreenDimLevel();
|
uint8_t level = settingsGetScreenDimLevel();
|
||||||
|
|
||||||
if (level != SCREEN_DIM_OFF)
|
if (level != SCREEN_DIM_OFF)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,58 @@ static menuStatus_t menuContactListSubMenuExitCode = MENU_STATUS_SUCCESS;
|
||||||
|
|
||||||
static const char *calltypeVoices[3] = { NULL, NULL, NULL };
|
static const char *calltypeVoices[3] = { NULL, NULL, NULL };
|
||||||
|
|
||||||
|
// Pseudo call-type for the digital list: show every contact (groups, private
|
||||||
|
// calls and all-calls) together. This is the default; HASH still cycles the
|
||||||
|
// per-type filters.
|
||||||
|
#define CONTACT_LIST_COMBINED 0xFFU
|
||||||
|
|
||||||
|
// Number of digital contacts under the current filter (all types when combined).
|
||||||
|
static int digitalContactsCount(void)
|
||||||
|
{
|
||||||
|
if (contactCallType == CONTACT_LIST_COMBINED)
|
||||||
|
{
|
||||||
|
return codeplugContactsGetCount(CONTACT_CALLTYPE_TG)
|
||||||
|
+ codeplugContactsGetCount(CONTACT_CALLTYPE_PC)
|
||||||
|
+ codeplugContactsGetCount(CONTACT_CALLTYPE_ALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeplugContactsGetCount(contactCallType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the (1-based) digital contact under the current filter. When combined,
|
||||||
|
// the list runs groups first, then private calls, then all-calls.
|
||||||
|
static int digitalContactGetForNumber(int number, CodeplugContact_t *c)
|
||||||
|
{
|
||||||
|
if (contactCallType != CONTACT_LIST_COMBINED)
|
||||||
|
{
|
||||||
|
return codeplugContactGetDataForNumberInType(number, contactCallType, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
int row = number - 1;
|
||||||
|
int count = codeplugContactsGetCount(CONTACT_CALLTYPE_TG);
|
||||||
|
|
||||||
|
if (row < count)
|
||||||
|
{
|
||||||
|
return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_TG, c);
|
||||||
|
}
|
||||||
|
row -= count;
|
||||||
|
count = codeplugContactsGetCount(CONTACT_CALLTYPE_PC);
|
||||||
|
|
||||||
|
if (row < count)
|
||||||
|
{
|
||||||
|
return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_PC, c);
|
||||||
|
}
|
||||||
|
row -= count;
|
||||||
|
|
||||||
|
return codeplugContactGetDataForNumberInType(row + 1, CONTACT_CALLTYPE_ALL, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voice/title label for the current filter.
|
||||||
|
static const char *contactCallTypeLabel(void)
|
||||||
|
{
|
||||||
|
return ((contactCallType == CONTACT_LIST_COMBINED) ? currentLanguage->contacts : calltypeVoices[contactCallType]);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply contact + its TS on selection for TX (contact list of quick list).
|
// Apply contact + its TS on selection for TX (contact list of quick list).
|
||||||
static void overrideWithSelectedContact(void)
|
static void overrideWithSelectedContact(void)
|
||||||
{
|
{
|
||||||
|
|
@ -78,7 +130,7 @@ static void overrideWithSelectedContact(void)
|
||||||
|
|
||||||
static void reloadContactList(contactListContactType_t type)
|
static void reloadContactList(contactListContactType_t type)
|
||||||
{
|
{
|
||||||
menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? codeplugContactsGetCount(contactCallType) : codeplugDTMFContactsGetCount();
|
menuDataGlobal.numItems = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? digitalContactsCount() : codeplugDTMFContactsGetCount();
|
||||||
|
|
||||||
if (menuDataGlobal.numItems > 0)
|
if (menuDataGlobal.numItems > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +139,7 @@ static void reloadContactList(contactListContactType_t type)
|
||||||
menuDataGlobal.currentItemIndex = 0;
|
menuDataGlobal.currentItemIndex = 0;
|
||||||
}
|
}
|
||||||
uiDataGlobal.currentSelectedContactIndex = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
uiDataGlobal.currentSelectedContactIndex = (type == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
|
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
|
||||||
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -119,7 +171,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
|
||||||
// Shows digital contact list if called from "contact list" menu entry, or from <SK2>+# in digital.
|
// Shows digital contact list if called from "contact list" menu entry, or from <SK2>+# in digital.
|
||||||
// Otherwise displays DTMF contact list
|
// Otherwise displays DTMF contact list
|
||||||
contactListType = ((currentMenu == MENU_CONTACT_LIST) || ((currentMenu == MENU_CONTACT_QUICKLIST) && (trxGetMode() != RADIO_MODE_ANALOG))) ? MENU_CONTACT_LIST_CONTACT_DIGITAL : MENU_CONTACT_LIST_CONTACT_DTMF;
|
contactListType = ((currentMenu == MENU_CONTACT_LIST) || ((currentMenu == MENU_CONTACT_QUICKLIST) && (trxGetMode() != RADIO_MODE_ANALOG))) ? MENU_CONTACT_LIST_CONTACT_DIGITAL : MENU_CONTACT_LIST_CONTACT_DTMF;
|
||||||
contactCallType = CONTACT_CALLTYPE_TG;
|
contactCallType = CONTACT_LIST_COMBINED; // default: show all contact types together
|
||||||
|
|
||||||
dtmfSequenceReset();
|
dtmfSequenceReset();
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +198,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
|
||||||
{
|
{
|
||||||
voicePromptsAppendLanguageString(currentLanguage->dmr_contacts);
|
voicePromptsAppendLanguageString(currentLanguage->dmr_contacts);
|
||||||
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
||||||
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
|
voicePromptsAppendLanguageString(contactCallTypeLabel());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -158,7 +210,7 @@ menuStatus_t menuContactList(uiEvent_t *ev, bool isFirstRun)
|
||||||
{
|
{
|
||||||
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
{
|
{
|
||||||
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
|
voicePromptsAppendLanguageString(contactCallTypeLabel());
|
||||||
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +262,9 @@ static void updateScreen(bool isFirstRun)
|
||||||
switch (contactListDisplayState)
|
switch (contactListDisplayState)
|
||||||
{
|
{
|
||||||
case MENU_CONTACT_LIST_DISPLAY:
|
case MENU_CONTACT_LIST_DISPLAY:
|
||||||
menuDisplayTitle((char *) calltypeName[((contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL) ? contactCallType : 3)]);
|
menuDisplayTitle((contactListType != MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
|
? (char *) calltypeName[3] // "DTMF"
|
||||||
|
: ((contactCallType == CONTACT_LIST_COMBINED) ? (char *) currentLanguage->contacts : (char *) calltypeName[contactCallType]));
|
||||||
|
|
||||||
if (menuDataGlobal.numItems == 0)
|
if (menuDataGlobal.numItems == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -235,7 +289,7 @@ static void updateScreen(bool isFirstRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
idx = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
? codeplugContactGetDataForNumberInType(mNum + 1, contactCallType, &contact)
|
? digitalContactGetForNumber(mNum + 1, &contact)
|
||||||
: codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact);
|
: codeplugDTMFContactGetDataForNumber(mNum + 1, &dtmfContact);
|
||||||
|
|
||||||
if (idx > 0)
|
if (idx > 0)
|
||||||
|
|
@ -323,7 +377,7 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
{
|
{
|
||||||
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
||||||
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
|
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
|
||||||
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
||||||
voicePromptsInit();
|
voicePromptsInit();
|
||||||
updateScreen(false);
|
updateScreen(false);
|
||||||
|
|
@ -333,7 +387,7 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
{
|
{
|
||||||
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, menuDataGlobal.numItems);
|
||||||
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
uiDataGlobal.currentSelectedContactIndex = (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
? codeplugContactGetDataForNumberInType(menuDataGlobal.currentItemIndex + 1, contactCallType, &contactListContactData)
|
? digitalContactGetForNumber(menuDataGlobal.currentItemIndex + 1, &contactListContactData)
|
||||||
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
: codeplugDTMFContactGetDataForNumber(menuDataGlobal.currentItemIndex + 1, &contactListDTMFContactData);
|
||||||
voicePromptsInit();
|
voicePromptsInit();
|
||||||
updateScreen(false);
|
updateScreen(false);
|
||||||
|
|
@ -343,11 +397,24 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
{
|
{
|
||||||
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
if (contactListType == MENU_CONTACT_LIST_CONTACT_DIGITAL)
|
||||||
{
|
{
|
||||||
contactCallType = (contactCallType + 1) % (CONTACT_CALLTYPE_ALL + 1);
|
// cycle: all combined -> group -> private -> all-call -> combined
|
||||||
|
if (contactCallType == CONTACT_LIST_COMBINED)
|
||||||
|
{
|
||||||
|
contactCallType = CONTACT_CALLTYPE_TG;
|
||||||
|
}
|
||||||
|
else if (contactCallType == CONTACT_CALLTYPE_ALL)
|
||||||
|
{
|
||||||
|
contactCallType = CONTACT_LIST_COMBINED;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contactCallType = contactCallType + 1;
|
||||||
|
}
|
||||||
|
menuDataGlobal.currentItemIndex = 0;
|
||||||
reloadContactList(contactListType);
|
reloadContactList(contactListType);
|
||||||
|
|
||||||
voicePromptsInit();
|
voicePromptsInit();
|
||||||
voicePromptsAppendLanguageString(calltypeVoices[contactCallType]);
|
voicePromptsAppendLanguageString(contactCallTypeLabel());
|
||||||
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
voicePromptsAppendPrompt(PROMPT_SILENCE);
|
||||||
|
|
||||||
updateScreen(false);
|
updateScreen(false);
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,6 @@ static void updateScreen(bool isFirstRun)
|
||||||
if (menuDataGlobal.currentMenuList[mNum].stringOffset >= 0)
|
if (menuDataGlobal.currentMenuList[mNum].stringOffset >= 0)
|
||||||
{
|
{
|
||||||
const char *menuName = (currentLanguage->LANGUAGE_NAME + (menuDataGlobal.currentMenuList[mNum].stringOffset * LANGUAGE_TEXTS_LENGTH));
|
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);
|
menuDisplayEntry(i, mNum, menuName, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
|
||||||
|
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ enum
|
||||||
DISPLAY_SHOW_DISTANCE,
|
DISPLAY_SHOW_DISTANCE,
|
||||||
DISPLAY_DMR_LAST_TALKER_ON_SCREEN,
|
DISPLAY_DMR_LAST_TALKER_ON_SCREEN,
|
||||||
DISPLAY_SCREEN_DIM,
|
DISPLAY_SCREEN_DIM,
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
DISPLAY_THEME_PRESET,
|
||||||
|
#endif
|
||||||
NUM_DISPLAY_MENU_ITEMS
|
NUM_DISPLAY_MENU_ITEMS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -249,12 +252,38 @@ static void updateScreen(bool isFirstRun)
|
||||||
|
|
||||||
case DISPLAY_SCREEN_DIM:
|
case DISPLAY_SCREEN_DIM:
|
||||||
{
|
{
|
||||||
const char *dimOpts[] = { currentLanguage->off, "100%", "50%", "30%" };
|
// The "%" values are plain string literals, not currentLanguage entries,
|
||||||
|
// so they must go through rightSideVar -> voicePromptsAppendString().
|
||||||
|
// Passing them to voicePromptsAppendLanguageString() (as the shared code
|
||||||
|
// does for rightSideConst) would treat the pointer as an offset into the
|
||||||
|
// language table and crash when voice prompts are enabled. "Off" is a real
|
||||||
|
// language string, so it can stay in rightSideConst.
|
||||||
|
const char *dimOpts[] = { NULL, "100%", "50%", "30%", "15%" };
|
||||||
|
uint8_t dimLevel = settingsGetScreenDimLevel();
|
||||||
leftSide = "Screen Dim";
|
leftSide = "Screen Dim";
|
||||||
rightSideConst = dimOpts[settingsGetScreenDimLevel()];
|
if (dimLevel == SCREEN_DIM_OFF)
|
||||||
|
{
|
||||||
|
rightSideConst = currentLanguage->off;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snprintf(rightSideVar, SCREEN_LINE_BUFFER_SIZE, "%s", dimOpts[dimLevel]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
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)
|
#if ! defined(PLATFORM_GD77S)
|
||||||
case DISPLAY_AUTO_NIGHT:
|
case DISPLAY_AUTO_NIGHT:
|
||||||
leftSide = currentLanguage->auto_night;
|
leftSide = currentLanguage->auto_night;
|
||||||
|
|
@ -616,6 +645,19 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
}
|
}
|
||||||
break;
|
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)
|
#if ! defined(PLATFORM_GD77S)
|
||||||
case DISPLAY_AUTO_NIGHT:
|
case DISPLAY_AUTO_NIGHT:
|
||||||
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
|
if (settingsIsOptionBitSet(BIT_AUTO_NIGHT) == false)
|
||||||
|
|
@ -825,6 +867,19 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
}
|
}
|
||||||
break;
|
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)
|
#if ! defined(PLATFORM_GD77S)
|
||||||
case DISPLAY_AUTO_NIGHT:
|
case DISPLAY_AUTO_NIGHT:
|
||||||
if (settingsIsOptionBitSet(BIT_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
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)
|
#if !defined(PLATFORM_GD77S)
|
||||||
NULL,// APRS options
|
NULL,// APRS options
|
||||||
#endif
|
#endif
|
||||||
|
&menuDataSMS,// SMS hub
|
||||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
// *** 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,// MessageBox
|
||||||
NULL,// hotspot mode
|
NULL,// hotspot mode
|
||||||
NULL,// CPS
|
NULL,// CPS
|
||||||
|
|
@ -148,7 +157,16 @@ static menuFunctionData_t menuFunctions[] =
|
||||||
#if !defined(PLATFORM_GD77S)
|
#if !defined(PLATFORM_GD77S)
|
||||||
{ menuAPRSOptions, NULL, NULL, 0 },
|
{ menuAPRSOptions, NULL, NULL, 0 },
|
||||||
#endif
|
#endif
|
||||||
|
{ menuDisplayMenuList, NULL, NULL, 0 },// SMS hub
|
||||||
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
|
// *** 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 },
|
{ uiMessageBox, NULL, NULL, 0 },
|
||||||
{ menuHotspotMode, NULL, NULL, 0 },
|
{ menuHotspotMode, NULL, NULL, 0 },
|
||||||
{ uiCPS, NULL, NULL, 0 },
|
{ uiCPS, NULL, NULL, 0 },
|
||||||
|
|
@ -649,6 +667,7 @@ const menuItemNewData_t mainMenuItems[] =
|
||||||
{
|
{
|
||||||
{ 3, MENU_ZONE_LIST },
|
{ 3, MENU_ZONE_LIST },
|
||||||
{ 6, MENU_CONTACTS_MENU },
|
{ 6, MENU_CONTACTS_MENU },
|
||||||
|
{ 282, MENU_SMS },// messages
|
||||||
{ 12, MENU_CHANNEL_DETAILS },
|
{ 12, MENU_CHANNEL_DETAILS },
|
||||||
{ 4, MENU_RSSI_SCREEN },
|
{ 4, MENU_RSSI_SCREEN },
|
||||||
{ 8, MENU_FIRMWARE_INFO },
|
{ 8, MENU_FIRMWARE_INFO },
|
||||||
|
|
@ -667,6 +686,21 @@ const menuItemsList_t menuDataMainMenu =
|
||||||
.items = mainMenuItems
|
.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[] =
|
static const menuItemNewData_t contactMenuItems[] =
|
||||||
{
|
{
|
||||||
{ 15, MENU_CONTACT_LIST },
|
{ 15, MENU_CONTACT_LIST },
|
||||||
|
|
@ -725,53 +759,11 @@ void menuDisplayTitle(const char *title)
|
||||||
// optStart:
|
// optStart:
|
||||||
// - if less than 0, entryText will use fgOptItem colour
|
// - if less than 0, entryText will use fgOptItem colour
|
||||||
// - if equal to 0, no coloured option will be handled
|
// - 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)
|
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);
|
bool focused = (focusedItem == menuDataGlobal.currentItemIndex);
|
||||||
int16_t textXPos = DISPLAY_X_POS_MENU_TEXT_OFFSET;
|
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)
|
if (focused)
|
||||||
{
|
{
|
||||||
|
|
@ -785,15 +777,6 @@ void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, in
|
||||||
|
|
||||||
displayThemeApply(fgItem, bgItem);
|
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)))
|
if ((focused == false) && ((optStart < 0) || (optStart > 0)))
|
||||||
{
|
{
|
||||||
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
char buffer[SCREEN_LINE_BUFFER_SIZE];
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#include "user_interface/uiLocalisation.h"
|
#include "user_interface/uiLocalisation.h"
|
||||||
#include "functions/voicePrompts.h"
|
#include "functions/voicePrompts.h"
|
||||||
#include "functions/rxPowerSaving.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)
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
||||||
#include "hardware/radioHardwareInterface.h"
|
#include "hardware/radioHardwareInterface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -274,6 +275,13 @@ menuStatus_t uiChannelMode(uiEvent_t *ev, bool isFirstRun)
|
||||||
{
|
{
|
||||||
return menuChannelExitStatus;
|
return menuChannelExitStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A new text message arrived
|
||||||
|
if (smsHasUnseenRxMessage() && (trxTransmissionEnabled == false) && (trxIsTransmitting == false))
|
||||||
|
{
|
||||||
|
menuSystemPushNewMenu(UI_SMS_RX_POPUP);
|
||||||
|
return menuChannelExitStatus;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (ev->events == NO_EVENT)
|
if (ev->events == NO_EVENT)
|
||||||
|
|
@ -1298,6 +1306,20 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
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
|
#endif
|
||||||
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
|
||||||
{
|
{
|
||||||
|
|
@ -2164,6 +2186,152 @@ static bool validateOverwriteChannel(void)
|
||||||
return true;
|
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)
|
static void updateQuickMenuScreen(bool isFirstRun)
|
||||||
{
|
{
|
||||||
int mNum = 0;
|
int mNum = 0;
|
||||||
|
|
@ -2266,6 +2434,12 @@ static void updateQuickMenuScreen(bool isFirstRun)
|
||||||
leftSide = currentLanguage->mute;
|
leftSide = currentLanguage->mute;
|
||||||
rightSideConst = (uiDataGlobal.QuickMenu.tmpAudioMute ? currentLanguage->yes : currentLanguage->no);
|
rightSideConst = (uiDataGlobal.QuickMenu.tmpAudioMute ? currentLanguage->yes : currentLanguage->no);
|
||||||
break;
|
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:
|
default:
|
||||||
buf[0] = 0;
|
buf[0] = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -2496,6 +2670,66 @@ static void handleQuickMenuEvent(uiEvent_t *ev)
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
// CH_SCREEN_QUICK_MENU_FILTER_FM
|
// CH_SCREEN_QUICK_MENU_FILTER_FM
|
||||||
if (nonVolatileSettings.analogFilterLevel != uiDataGlobal.QuickMenu.tmpAnalogFilterLevel)
|
if (nonVolatileSettings.analogFilterLevel != uiDataGlobal.QuickMenu.tmpAnalogFilterLevel)
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,15 @@ uiNotificationID_t uiNotificationGetId(void)
|
||||||
return notificationData.id;
|
return notificationData.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
// Expose the full-screen offscreen buffer so the shutdown animation can borrow
|
||||||
|
// it as scratch (no notification is active while powering off).
|
||||||
|
uint16_t *uiNotificationGetOffscreenBuffer(void)
|
||||||
|
{
|
||||||
|
return screenNotificationBufData;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static void displayMessage(void)
|
static void displayMessage(void)
|
||||||
{
|
{
|
||||||
char msg[SCREEN_LINE_BUFFER_SIZE];
|
char msg[SCREEN_LINE_BUFFER_SIZE];
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,25 @@
|
||||||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
#include <string.h>
|
||||||
#include "user_interface/uiGlobals.h"
|
#include "user_interface/uiGlobals.h"
|
||||||
#include "user_interface/menuSystem.h"
|
#include "user_interface/menuSystem.h"
|
||||||
#include "user_interface/uiLocalisation.h"
|
#include "user_interface/uiLocalisation.h"
|
||||||
|
#include "functions/ticks.h"
|
||||||
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
#if defined(PLATFORM_MD9600) || defined(PLATFORM_MD380) || defined(PLATFORM_MDUV380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
|
||||||
#include "interfaces/batteryAndPowerManagement.h"
|
#include "interfaces/batteryAndPowerManagement.h"
|
||||||
#include "hardware/radioHardwareInterface.h"
|
#include "hardware/radioHardwareInterface.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
|
||||||
|
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
static void crtShutdownAnimation(void);
|
||||||
|
#else
|
||||||
|
static uint32_t initialEventTime;
|
||||||
static void updateScreen(void);
|
static void updateScreen(void);
|
||||||
static void handleEvent(uiEvent_t *ev);
|
static void handleEvent(uiEvent_t *ev);
|
||||||
static uint32_t initialEventTime;
|
#endif
|
||||||
const uint32_t POWEROFF_DURATION_MILLISECONDS = 500;
|
|
||||||
|
|
||||||
menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
|
menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
|
||||||
{
|
{
|
||||||
|
|
@ -49,23 +56,103 @@ menuStatus_t uiPowerOff(uiEvent_t *ev, bool isFirstRun)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
// Old-CRT-style collapse of whatever is currently on screen; replaces
|
||||||
|
// the "Power Off" / 73 message, then shuts down.
|
||||||
|
crtShutdownAnimation();
|
||||||
|
powerOffFinalStage(false, false);
|
||||||
|
#else
|
||||||
updateScreen();
|
updateScreen();
|
||||||
initialEventTime = ev->time;
|
initialEventTime = ev->time;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
#if !defined(HAS_COLOURS)
|
||||||
handleEvent(ev);
|
handleEvent(ev);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
return MENU_STATUS_SUCCESS;
|
return MENU_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(HAS_COLOURS)
|
||||||
|
static void crtShutdownAnimation(void)
|
||||||
|
{
|
||||||
|
const int W = DISPLAY_SIZE_X;
|
||||||
|
const int H = DISPLAY_SIZE_Y;
|
||||||
|
const int cx = W / 2;
|
||||||
|
const int cy = H / 2;
|
||||||
|
const int VSTEPS = 12;
|
||||||
|
const int HSTEPS = 12;
|
||||||
|
const uint16_t white = 0xFFFF;
|
||||||
|
|
||||||
|
displayRestorePrimaryScreenBuffer();
|
||||||
|
|
||||||
|
uint16_t *fb = displayGetPrimaryScreenBuffer();
|
||||||
|
uint16_t *snap = uiNotificationGetOffscreenBuffer();
|
||||||
|
|
||||||
|
// snapshot the screen as it is right now
|
||||||
|
memcpy(snap, fb, W * H * sizeof(uint16_t));
|
||||||
|
|
||||||
|
// Phase 1: squeeze the whole image vertically into a thin central band.
|
||||||
|
for (int step = 1; step <= VSTEPS; step++)
|
||||||
|
{
|
||||||
|
int bandH = H - ((H - 2) * step) / VSTEPS; // 128 -> 2
|
||||||
|
int top = cy - (bandH / 2);
|
||||||
|
|
||||||
|
memset(fb, 0, W * H * sizeof(uint16_t));
|
||||||
|
|
||||||
|
for (int dy = 0; dy < bandH; dy++)
|
||||||
|
{
|
||||||
|
int srcY = (dy * H) / bandH;
|
||||||
|
|
||||||
|
if (srcY >= H)
|
||||||
|
{
|
||||||
|
srcY = H - 1;
|
||||||
|
}
|
||||||
|
memcpy(&fb[(top + dy) * W], &snap[srcY * W], W * sizeof(uint16_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRender();
|
||||||
|
vTaskDelay((15 / portTICK_PERIOD_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: the band becomes a bright line that collapses horizontally.
|
||||||
|
for (int step = 1; step <= HSTEPS; step++)
|
||||||
|
{
|
||||||
|
int lineW = W - ((W - 2) * step) / HSTEPS; // 160 -> 2
|
||||||
|
int left = cx - (lineW / 2);
|
||||||
|
|
||||||
|
memset(fb, 0, W * H * sizeof(uint16_t));
|
||||||
|
|
||||||
|
for (int dx = 0; dx < lineW; dx++)
|
||||||
|
{
|
||||||
|
fb[((cy - 1) * W) + (left + dx)] = white;
|
||||||
|
fb[(cy * W) + (left + dx)] = white;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRender();
|
||||||
|
vTaskDelay((15 / portTICK_PERIOD_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: a brief central dot, then black.
|
||||||
|
memset(fb, 0, W * H * sizeof(uint16_t));
|
||||||
|
fb[((cy - 1) * W) + (cx - 1)] = white;
|
||||||
|
fb[((cy - 1) * W) + cx] = white;
|
||||||
|
fb[(cy * W) + (cx - 1)] = white;
|
||||||
|
fb[(cy * W) + cx] = white;
|
||||||
|
displayRender();
|
||||||
|
vTaskDelay((70 / portTICK_PERIOD_MS));
|
||||||
|
|
||||||
|
memset(fb, 0, W * H * sizeof(uint16_t));
|
||||||
|
displayRender();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(HAS_COLOURS)
|
||||||
static void updateScreen(void)
|
static void updateScreen(void)
|
||||||
{
|
{
|
||||||
#if defined(HAS_COLOURS)
|
|
||||||
bool dblHeight = settingsIsOptionBitSet(BIT_UI_USES_DOUBLE_HEIGHT);
|
|
||||||
#else
|
|
||||||
const bool dblHeight = false;
|
const bool dblHeight = false;
|
||||||
#endif
|
|
||||||
uint16_t fontHeight = (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1));
|
uint16_t fontHeight = (FONT_SIZE_3_HEIGHT * (dblHeight ? 2 : 1));
|
||||||
|
|
||||||
displayClearBuf();
|
displayClearBuf();
|
||||||
|
|
@ -104,3 +191,4 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
|
||||||
177
MDUV380_firmware/application/source/user_interface/uiSMSPopup.c
Normal file
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();
|
displayThemeResetToDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle the audio mute (speaker) state. The header shows a mute icon and the LED
|
// Toggle the audio mute (speaker) state. The flashing red LED (see the main
|
||||||
// flashes amber (see the main loop) while muted.
|
// loop) indicates the muted state.
|
||||||
void toggleAudioMute(void)
|
void toggleAudioMute(void)
|
||||||
{
|
{
|
||||||
audioAmpMute(!audioAmpIsMuted());
|
audioAmpMute(!audioAmpIsMuted());
|
||||||
headerRowIsDirty = true; // refresh the header so the mute icon appears/clears
|
headerRowIsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
|
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
|
||||||
|
|
@ -1878,8 +1878,6 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
|
||||||
static uint32_t blinkTime = 0;
|
static uint32_t blinkTime = 0;
|
||||||
int radioMode = trxGetMode();
|
int radioMode = trxGetMode();
|
||||||
uint8_t powerLevel = trxGetPowerLevel();
|
uint8_t powerLevel = trxGetPowerLevel();
|
||||||
// Muted either explicitly (SK2) or by the volume knob turned fully down (-99).
|
|
||||||
bool audioIsMuted = (audioAmpIsMuted() || (lastVolume == -99));
|
|
||||||
bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00);
|
bool isPerChannelPower = (currentChannelData->libreDMR_Power != 0x00);
|
||||||
bool scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive);
|
bool scanIsActive = (uiDataGlobal.Scan.active || uiDataGlobal.Scan.toneActive);
|
||||||
bool batteryIsLow = batteryIsLowWarning();
|
bool batteryIsLow = batteryIsLowWarning();
|
||||||
|
|
@ -2000,7 +1998,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
|
||||||
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer,
|
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, buffer,
|
||||||
(((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (uiDataGlobal.dmrDisabled == false)) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
|
(((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) && (uiDataGlobal.dmrDisabled == false)) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
|
||||||
|
|
||||||
// (mute is shown by the header "M" indicator, not a strike-through)
|
// (mute is indicated by the flashing red LED, not a strike-through)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) &&
|
if ((aprsIsTransmittingOnSameQRG == false) && (monitorModeData.isEnabled == false) && (isVFOSweepScanning == false) && (isVFODualWatchScanning == false) &&
|
||||||
|
|
@ -2068,7 +2066,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
|
||||||
|
|
||||||
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, (isVFODualWatchScanning ? "[DW]" : "DMR"), ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
|
displayPrintCore(MODE_TEXT_X_OFFSET, DISPLAY_Y_POS_HEADER, (isVFODualWatchScanning ? "[DW]" : "DMR"), ((nonVolatileSettings.hotspotType != HOTSPOT_TYPE_OFF) ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, isInverted);
|
||||||
|
|
||||||
// (mute is shown by the header "M" indicator, not a strike-through)
|
// (mute is indicated by the flashing red LED, not a strike-through)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isVFODualWatchScanning == false)
|
if (isVFODualWatchScanning == false)
|
||||||
|
|
@ -2180,13 +2178,8 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
|
||||||
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
|
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
// Mute indicator: bold "M" just left of the battery indicator.
|
// Mute state is indicated by the flashing red LED only; no header icon
|
||||||
if (audioIsMuted)
|
// (it collided with the GPS text in the status bar).
|
||||||
{
|
|
||||||
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
|
|
||||||
displayPrintCore((DISPLAY_SIZE_X - 40), DISPLAY_Y_POS_HEADER, "M", FONT_SIZE_1_BOLD, TEXT_ALIGN_LEFT, false);
|
|
||||||
displayThemeResetToDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display battery percentage/voltage
|
// Display battery percentage/voltage
|
||||||
bool apoEnabled = (nonVolatileSettings.apo > 0);
|
bool apoEnabled = (nonVolatileSettings.apo > 0);
|
||||||
|
|
@ -2524,6 +2517,9 @@ void uiUtilityDisplayFrequency(uint8_t y, bool isTX, bool hasFocus, uint32_t fre
|
||||||
#else
|
#else
|
||||||
displayPrintAtDoubleHeight(FREQUENCY_X_POS, yFont3, buffer, FONT_SIZE_3, displayFocusAndDblHeight);
|
displayPrintAtDoubleHeight(FREQUENCY_X_POS, yFont3, buffer, FONT_SIZE_3, displayFocusAndDblHeight);
|
||||||
#endif
|
#endif
|
||||||
|
// Re-apply the frequency colour: the 7-segment renderer leaves the foreground
|
||||||
|
// set to the last segment drawn, so "MHz" would otherwise use the wrong colour.
|
||||||
|
displayThemeApply(isTX ? THEME_ITEM_FG_TX_FREQ : THEME_ITEM_FG_RX_FREQ, THEME_ITEM_BG);
|
||||||
displayPrintAtDoubleHeight(DISPLAY_SIZE_X - (3 * 8), yFont3, "MHz", FONT_SIZE_3, displayFocusAndDblHeight);
|
displayPrintAtDoubleHeight(DISPLAY_SIZE_X - (3 * 8), yFont3, "MHz", FONT_SIZE_3, displayFocusAndDblHeight);
|
||||||
|
|
||||||
displayThemeResetToDefault();
|
displayThemeResetToDefault();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#endif
|
#endif
|
||||||
#include "functions/trx.h"
|
#include "functions/trx.h"
|
||||||
#include "functions/rxPowerSaving.h"
|
#include "functions/rxPowerSaving.h"
|
||||||
|
#include "functions/smsCore.h"
|
||||||
#include "user_interface/menuSystem.h"
|
#include "user_interface/menuSystem.h"
|
||||||
#include "user_interface/uiUtilities.h"
|
#include "user_interface/uiUtilities.h"
|
||||||
#include "user_interface/uiLocalisation.h"
|
#include "user_interface/uiLocalisation.h"
|
||||||
|
|
@ -258,6 +259,13 @@ menuStatus_t uiVFOMode(uiEvent_t *ev, bool isFirstRun)
|
||||||
{
|
{
|
||||||
return menuVFOExitStatus;
|
return menuVFOExitStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A new text message arrived
|
||||||
|
if (smsHasUnseenRxMessage() && (trxTransmissionEnabled == false) && (trxIsTransmitting == false))
|
||||||
|
{
|
||||||
|
menuSystemPushNewMenu(UI_SMS_RX_POPUP);
|
||||||
|
return menuVFOExitStatus;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (ev->events == NO_EVENT)
|
if (ev->events == NO_EVENT)
|
||||||
|
|
@ -1311,6 +1319,20 @@ static void handleEvent(uiEvent_t *ev)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
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
|
#endif
|
||||||
if (KEYCHECK_LONGDOWN(ev->keys, KEY_HASH) && (KEYCHECK_LONGDOWN_REPEAT(ev->keys, KEY_HASH) == false))
|
if (KEYCHECK_LONGDOWN(ev->keys, KEY_HASH) && (KEYCHECK_LONGDOWN_REPEAT(ev->keys, KEY_HASH) == false))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
105
README.md
105
README.md
|
|
@ -6,8 +6,76 @@ chipset — the **TYT MD-UV380 / Retevis RT-3S** and **Baofeng DM-1701 / Retevis
|
||||||
RT-84**.
|
RT-84**.
|
||||||
|
|
||||||
It adds a number of UI and quality-of-life changes on top of OpenGD77 (segmented
|
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
|
S-meter, 7-segment frequency font, screen dimming, an audio-mute function,
|
||||||
icons, and more). See [`CHANGELOG.md`](CHANGELOG.md) for the full list.
|
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 | Main menu |
|
||||||
|
| --- | --- | --- |
|
||||||
|
|  |  |  |
|
||||||
|
|
||||||
|
| DMR text messages | Reading a received message |
|
||||||
|
| --- | --- |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
*Screens shown with the **Neon 80s** theme.*
|
||||||
|
|
||||||
|
## What's different from OpenGD77
|
||||||
|
|
||||||
|
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, 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** on the VFO and the SK1-held frequency view.
|
||||||
|
- **Screen dimming** — optionally dim the backlight to 100 / 50 / 30 / 15 % after
|
||||||
|
inactivity (Auto backlight mode); new setting in Display Options.
|
||||||
|
- **DMR text messages (SMS)** — send and receive text messages on the radio,
|
||||||
|
compatible with BrandMeister, hotspots and OEM radios. Compose on the keypad
|
||||||
|
(or pick the recipient from your contacts), inbox/sent boxes, quick-text
|
||||||
|
templates, delivery confirmation and an incoming-message popup (see the manual
|
||||||
|
for details).
|
||||||
|
- **Theme presets** — pick a colour theme on the radio: **Custom** (CPS-editable,
|
||||||
|
default), **BASIC 80s** (Commodore 64), **Terminal** (green-on-black),
|
||||||
|
**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
|
## Supported radios
|
||||||
|
|
||||||
|
|
@ -16,7 +84,7 @@ Retevis RT-3S**. No other radios are officially supported at this time.
|
||||||
|
|
||||||
If you have another radio that OpenGD77 supports (Baofeng DM-1701 / Retevis RT-84,
|
If you have another radio that OpenGD77 supports (Baofeng DM-1701 / Retevis RT-84,
|
||||||
MD-9600, MD-380, MD-2017, …) and would like to help test FreeTRX on it, that would
|
MD-9600, MD-380, MD-2017, …) and would like to help test FreeTRX on it, that would
|
||||||
be very welcome — please email **Marcus, DK1DA** at <marcus.kida@bearologics.com>.
|
be very welcome — please email **Marcus, DK1DA** at <qsl@dk1da.de>.
|
||||||
|
|
||||||
## Upstream source
|
## Upstream source
|
||||||
|
|
||||||
|
|
@ -34,10 +102,35 @@ As with OpenGD77, the source does **not** contain the AMBE codec required for DM
|
||||||
audio. It is merged in from an official manufacturer firmware (V26.45) at flash
|
audio. It is merged in from an official manufacturer firmware (V26.45) at flash
|
||||||
time by the firmware loader. See [`TOOLS.md`](TOOLS.md).
|
time by the firmware loader. See [`TOOLS.md`](TOOLS.md).
|
||||||
|
|
||||||
## Building, flashing & tools
|
## Flashing
|
||||||
|
|
||||||
See [`TOOLS.md`](TOOLS.md) for building (Docker + Arm GNU Toolchain 14), flashing
|
Pre-built firmware is attached to each
|
||||||
(`./flash.sh`), grabbing screenshots, and building language files.
|
[release](https://git.dk1da.de/DK1DA/FreeTRX/releases): `OpenMDUV380.bin` for the
|
||||||
|
MD-UV380 / RT-3S, `OpenDM1701.bin` for the DM-1701 / RT-84. (Or build your own —
|
||||||
|
see below.)
|
||||||
|
|
||||||
|
1. **Enter firmware-update (DFU) mode:** power the radio **off**, then power it
|
||||||
|
**on while holding PTT + SK1** (the button above PTT). The screen stays blank
|
||||||
|
and the top LED glows steadily. Connect the USB cable.
|
||||||
|
2. **Flash** with the helper (it applies the DMR codec donor and, optionally, a
|
||||||
|
secondary language):
|
||||||
|
```bash
|
||||||
|
./flash.sh -m MD-UV380 -f MDUV380_firmware/build/OpenMDUV380.bin
|
||||||
|
# DM-1701 / RT-84:
|
||||||
|
./flash.sh -m DM-1701 -f MDUV380_firmware/build/OpenDM1701.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
**DMR note:** the firmware image is FM-only until the **AMBE codec donor** (the
|
||||||
|
official V26.45 manufacturer firmware) is merged in at flash time. Register it
|
||||||
|
once with `./flash.sh -s /path/to/official_V26.45.bin`; the path is then
|
||||||
|
remembered. Full options, the DFU device list and the underlying loader are
|
||||||
|
documented in [`TOOLS.md`](TOOLS.md).
|
||||||
|
|
||||||
|
## Building & tools
|
||||||
|
|
||||||
|
See [`TOOLS.md`](TOOLS.md) for building in Docker
|
||||||
|
(`./build-firmware.sh [MDUV380|DM1701]`, Arm GNU Toolchain 14), grabbing
|
||||||
|
screenshots, and building language files.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
28
TOOLS.md
28
TOOLS.md
|
|
@ -26,11 +26,17 @@ the official OpenGD77 firmware uses. (Ubuntu's stock GCC 10 miscompiles the DMR
|
||||||
path; see the git history.)
|
path; see the git history.)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./build-firmware.sh
|
./build-firmware.sh # MD-UV380 / RT-3S -> build/OpenMDUV380.bin (default)
|
||||||
|
./build-firmware.sh DM1701 # DM-1701 / RT-84 -> build/OpenDM1701.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: `MDUV380_firmware/build/OpenMDUV380.bin` (the flashable, FM-only image; the DMR
|
Output: `MDUV380_firmware/build/Open<platform>.bin` (the flashable, FM-only image; the DMR
|
||||||
codec is merged in at flash time — see below). The build is always clean (`rm -rf build`).
|
codec is merged in at flash time — see below). The build is always clean (`rm -rf build`),
|
||||||
|
so building one platform overwrites the other's output.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -85,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**,
|
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.
|
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.
|
Power the radio on **normally** (not DFU mode), navigate to the screen you want, and
|
||||||
Connect USB.
|
connect USB.
|
||||||
2. Run it (`-o` takes the filename **without** `.png`):
|
|
||||||
|
**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
|
```bash
|
||||||
python3 MDUV380_firmware/tools/gd-77_screen_grabber.py \
|
python3 MDUV380_firmware/tools/gd-77_screen_grabber.py \
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,60 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Build the OpenGD77 firmware for the Retevis RT-3S / TYT MD-UV380 in Docker.
|
# Build the FreeTRX firmware in Docker. Nothing is installed on the host: the
|
||||||
# Nothing is installed on the host: the ARM toolchain lives only in the image,
|
# ARM toolchain lives only in the image.
|
||||||
# and the build output lands in MDUV380_firmware/build/OpenMDUV380.bin.
|
|
||||||
#
|
#
|
||||||
# Usage: ./build-firmware.sh
|
# Usage: ./build-firmware.sh [MDUV380|DM1701] (default: MDUV380)
|
||||||
|
# MDUV380 -> TYT MD-UV380 / Retevis RT-3S -> MDUV380_firmware/build/OpenMDUV380.bin
|
||||||
|
# DM1701 -> Baofeng DM-1701 / Retevis RT-84 -> MDUV380_firmware/build/OpenDM1701.bin
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
IMAGE=opengd77-mduv380-builder
|
IMAGE=opengd77-mduv380-builder
|
||||||
|
|
||||||
|
PLATFORM="${1:-MDUV380}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
MDUV380) OUT="OpenMDUV380"; MODEL="MD-UV380" ;;
|
||||||
|
DM1701) OUT="OpenDM1701"; MODEL="DM-1701" ;;
|
||||||
|
*) echo "Unknown platform '$PLATFORM' (use MDUV380 or DM1701)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "==> Building toolchain image ($IMAGE)"
|
echo "==> Building toolchain image ($IMAGE)"
|
||||||
docker build --platform linux/amd64 -t "$IMAGE" "$REPO_DIR/docker"
|
docker build --platform linux/amd64 -t "$IMAGE" "$REPO_DIR/docker"
|
||||||
|
|
||||||
echo "==> Running build in container"
|
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 \
|
docker run --rm --platform linux/amd64 \
|
||||||
-v "$REPO_DIR":/work \
|
-v "$REPO_DIR":/work \
|
||||||
"$IMAGE" \
|
"$IMAGE" \
|
||||||
bash /work/docker/build.sh
|
bash /work/docker/build.sh "$PLATFORM" 2>&1 | tee "$BUILD_LOG"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "==> Firmware ready:"
|
echo "==> Firmware ready:"
|
||||||
echo " $REPO_DIR/MDUV380_firmware/build/OpenMDUV380.bin"
|
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
|
||||||
echo "Flash it (with the radio in firmware-update mode) using:"
|
echo "Flash it (with the radio in firmware-update mode) using:"
|
||||||
echo " ./MDUV380_firmware/tools/opengd77_stm32_firmware_loader.py \\"
|
echo " ./MDUV380_firmware/tools/opengd77_stm32_firmware_loader.py \\"
|
||||||
echo " -m MD-UV380 -f MDUV380_firmware/build/OpenMDUV380.bin"
|
echo " -m ${MODEL} -f MDUV380_firmware/build/${OUT}.bin"
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,35 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# In-container build script. Runs inside the toolchain image with the repo
|
# In-container build script. Runs inside the toolchain image with the repo
|
||||||
# mounted at /work. Produces MDUV380_firmware/build/OpenMDUV380.bin
|
# mounted at /work. Produces MDUV380_firmware/build/Open<PLATFORM>.bin.
|
||||||
# (the flashable OpenGD77 image for the MD-UV380 / Retevis RT-3S).
|
#
|
||||||
|
# Usage: build.sh [MDUV380|DM1701] (default: MDUV380)
|
||||||
|
# MDUV380 -> TYT MD-UV380 / Retevis RT-3S -> OpenMDUV380.bin
|
||||||
|
# DM1701 -> Baofeng DM-1701 / Retevis RT-84 -> OpenDM1701.bin
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
PLATFORM="${1:-MDUV380}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
MDUV380) OUT="OpenMDUV380" ;;
|
||||||
|
DM1701) OUT="OpenDM1701" ;;
|
||||||
|
*) echo "Unknown platform '$PLATFORM' (use MDUV380 or DM1701)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
cd /work/MDUV380_firmware
|
cd /work/MDUV380_firmware
|
||||||
|
|
||||||
|
echo "==> Building for $PLATFORM"
|
||||||
echo "==> Preparing codec section (codec_cleaner -C)"
|
echo "==> Preparing codec section (codec_cleaner -C)"
|
||||||
chmod +x tools/codec_cleaner.Linux
|
chmod +x tools/codec_cleaner.Linux
|
||||||
( cd application/source/linkerdata && ../../../tools/codec_cleaner.Linux -C )
|
( cd application/source/linkerdata && ../../../tools/codec_cleaner.Linux -C )
|
||||||
|
|
||||||
echo "==> Building firmware"
|
echo "==> Building firmware"
|
||||||
# Clean build: obj/ lives on the host mount and would otherwise be reused across
|
# Clean build: obj/ lives on the host mount and would otherwise be reused across
|
||||||
# toolchain changes, silently relinking stale .o files (e.g. an old GCC build).
|
# toolchain/platform changes, silently relinking stale .o files.
|
||||||
rm -rf build
|
rm -rf build
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
cd build
|
cd build
|
||||||
make -f /work/docker/firmware.mk -j"$(nproc)"
|
make -f /work/docker/firmware.mk PLATFORM="$PLATFORM" -j"$(nproc)"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "==> Build complete:"
|
echo "==> Build complete:"
|
||||||
ls -l /work/MDUV380_firmware/build/OpenMDUV380.bin
|
ls -l "/work/MDUV380_firmware/build/${OUT}.bin"
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,24 @@ CC := $(PREFIX)gcc
|
||||||
OBJCOPY := $(PREFIX)objcopy
|
OBJCOPY := $(PREFIX)objcopy
|
||||||
SIZE := $(PREFIX)size
|
SIZE := $(PREFIX)size
|
||||||
|
|
||||||
TARGET := OpenMDUV380
|
# ---- Platform selection ---------------------------------------------------
|
||||||
|
# PLATFORM=MDUV380 (default) -> TYT MD-UV380 / Retevis RT-3S
|
||||||
|
# PLATFORM=DM1701 -> Baofeng DM-1701 / Retevis RT-84
|
||||||
|
PLATFORM ?= MDUV380
|
||||||
|
ifeq ($(PLATFORM),DM1701)
|
||||||
|
PLATFORM_DEFS := -DPLATFORM_RT84_DM1701 -DPLATFORM_VARIANT_DM1701
|
||||||
|
TARGET := OpenDM1701
|
||||||
|
else
|
||||||
|
PLATFORM_DEFS := -DPLATFORM_MDUV380
|
||||||
|
TARGET := OpenMDUV380
|
||||||
|
endif
|
||||||
|
|
||||||
# ---- MCU / FPU ------------------------------------------------------------
|
# ---- MCU / FPU ------------------------------------------------------------
|
||||||
CPU := -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb
|
CPU := -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mthumb
|
||||||
|
|
||||||
# ---- Preprocessor defines -------------------------------------------------
|
# ---- Preprocessor defines -------------------------------------------------
|
||||||
# NDEBUG + DEBUG mirrors the STM32CubeIDE "MDUV380_FW" release config exactly.
|
# NDEBUG + DEBUG mirrors the STM32CubeIDE release configs exactly.
|
||||||
DEFS := -DUSE_HAL_DRIVER -DSTM32F405xx -DPLATFORM_MDUV380 -DNDEBUG -DDEBUG
|
DEFS := -DUSE_HAL_DRIVER -DSTM32F405xx $(PLATFORM_DEFS) -DNDEBUG -DDEBUG
|
||||||
|
|
||||||
# Short git hash if available, otherwise UNKNOWN (matches the .cproject rule).
|
# Short git hash if available, otherwise UNKNOWN (matches the .cproject rule).
|
||||||
GITVERSION := $(shell git -C .. rev-parse --short HEAD 2>/dev/null || echo UNKNOWN)
|
GITVERSION := $(shell git -C .. rev-parse --short HEAD 2>/dev/null || echo UNKNOWN)
|
||||||
|
|
@ -53,6 +63,7 @@ ASFLAGS := $(CPU) -x assembler-with-cpp -DDEBUG -g3 -MMD -MP
|
||||||
LDSCRIPT := ../STM32F405VGTX_FLASH.ld
|
LDSCRIPT := ../STM32F405VGTX_FLASH.ld
|
||||||
LDFLAGS := $(CPU) -T$(LDSCRIPT) --specs=nano.specs \
|
LDFLAGS := $(CPU) -T$(LDSCRIPT) --specs=nano.specs \
|
||||||
-Wl,-Map=$(TARGET).map,--cref -Wl,--gc-sections -static \
|
-Wl,-Map=$(TARGET).map,--cref -Wl,--gc-sections -static \
|
||||||
|
-Wl,--print-memory-usage \
|
||||||
-Wl,--start-group -lc -lm -Wl,--end-group
|
-Wl,--start-group -lc -lm -Wl,--end-group
|
||||||
|
|
||||||
# ---- Sources --------------------------------------------------------------
|
# ---- Sources --------------------------------------------------------------
|
||||||
|
|
|
||||||
BIN
docs/images/screenshot-dmr.png
Normal file
BIN
docs/images/screenshot-dmr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
docs/images/screenshot-dmr2.png
Normal file
BIN
docs/images/screenshot-dmr2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
docs/images/screenshot-menu.png
Normal file
BIN
docs/images/screenshot-menu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
docs/images/screenshot-message-view.png
Normal file
BIN
docs/images/screenshot-message-view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/images/screenshot-messages.png
Normal file
BIN
docs/images/screenshot-messages.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/images/screenshot-vfo.png
Normal file
BIN
docs/images/screenshot-vfo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
10
flash.sh
10
flash.sh
|
|
@ -58,11 +58,11 @@ done
|
||||||
# ---- resolve / build the language file ------------------------------------
|
# ---- resolve / build the language file ------------------------------------
|
||||||
LANG_ARGS=()
|
LANG_ARGS=()
|
||||||
if [[ -n "$LANGUAGE" && "$(lc "$LANGUAGE")" != "none" ]]; then
|
if [[ -n "$LANGUAGE" && "$(lc "$LANGUAGE")" != "none" ]]; then
|
||||||
# Build the .gla files once if they're missing.
|
# Always (re)build the .gla language packs so edits to the *.h language files
|
||||||
if ! ls "$LANG_SRC"/*.gla >/dev/null 2>&1; then
|
# are picked up. (They're built from the .h sources by languages_builder; a
|
||||||
echo "==> Building language (.gla) files..."
|
# stale pack would otherwise show blank/old strings on the radio.)
|
||||||
( cd "$LANG_SRC" && gcc -w -I../ -o languages_builder languages_builder.c && ./languages_builder >/dev/null )
|
echo "==> Building language (.gla) files..."
|
||||||
fi
|
( 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.
|
# Case-insensitive match of the requested name to an actual <Name>.gla file.
|
||||||
GLA=""
|
GLA=""
|
||||||
for f in "$LANG_SRC"/*.gla; do
|
for f in "$LANG_SRC"/*.gla; do
|
||||||
|
|
|
||||||
55
screenshot.sh
Normal file
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"
|
||||||
80
scripts/forgejo-release.sh
Normal file
80
scripts/forgejo-release.sh
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Build and publish a FreeTRX release on Forgejo, attaching the firmware binaries.
|
||||||
|
#
|
||||||
|
# Requires a Forgejo API token in FORGEJO_TOKEN
|
||||||
|
# (Forgejo -> Settings -> Applications -> Generate New Token, scope: write:repository).
|
||||||
|
# Release notes are taken from the matching "## <version>" section of CHANGELOG.md.
|
||||||
|
# If the release already exists it is reused; assets of the same name are replaced.
|
||||||
|
#
|
||||||
|
# Usage: FORGEJO_TOKEN=xxx scripts/forgejo-release.sh <tag> [platform ...]
|
||||||
|
# platform: MDUV380 | DM1701 | all (default: all)
|
||||||
|
# Examples:
|
||||||
|
# scripts/forgejo-release.sh v0.1.0 # build + attach both binaries
|
||||||
|
# scripts/forgejo-release.sh v0.1.0 MDUV380 # just the MD-UV380 binary
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TAG="${1:?usage: FORGEJO_TOKEN=... $0 <tag> [MDUV380|DM1701|all]}"; shift || true
|
||||||
|
VER="${TAG#v}"
|
||||||
|
API="https://git.dk1da.de/api/v1/repos/DK1DA/FreeTRX"
|
||||||
|
: "${FORGEJO_TOKEN:?set FORGEJO_TOKEN (Forgejo -> Settings -> Applications, scope write:repository)}"
|
||||||
|
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
|
|
||||||
|
# Resolve requested platforms (default / "all" -> both).
|
||||||
|
PLATFORMS=("$@")
|
||||||
|
if [ ${#PLATFORMS[@]} -eq 0 ] || [ "${PLATFORMS[0]}" = "all" ]; then
|
||||||
|
PLATFORMS=(MDUV380 DM1701)
|
||||||
|
fi
|
||||||
|
bin_for() { case "$1" in MDUV380) echo OpenMDUV380 ;; DM1701) echo OpenDM1701 ;; *) return 1 ;; esac; }
|
||||||
|
|
||||||
|
# Sanity check: the on-screen version should match the tag.
|
||||||
|
FW_VER="$(grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' MDUV380_firmware/application/include/freetrxVersion.h | head -1 | tr -d '"')"
|
||||||
|
[ "$FW_VER" = "$VER" ] || echo "WARNING: freetrxVersion.h is $FW_VER but tag is $VER — bump it before releasing?"
|
||||||
|
|
||||||
|
# Build each platform and stage its binary (clean builds overwrite build/).
|
||||||
|
STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
for P in "${PLATFORMS[@]}"; do
|
||||||
|
OUT="$(bin_for "$P")" || { echo "Unknown platform '$P' (use MDUV380, DM1701 or all)"; exit 1; }
|
||||||
|
echo "==> Building $P"
|
||||||
|
./build-firmware.sh "$P" >/dev/null
|
||||||
|
cp "MDUV380_firmware/build/${OUT}.bin" "$STAGE/${OUT}.bin"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Release notes = the CHANGELOG section for this version.
|
||||||
|
NOTES="$(awk -v v="## ${VER}" '$0==v{f=1} f&&/^## /&&$0!=v{exit} f{print}' CHANGELOG.md)"
|
||||||
|
[ -n "$NOTES" ] || NOTES="FreeTRX ${TAG}"
|
||||||
|
|
||||||
|
# Create the release, or reuse it if it already exists.
|
||||||
|
ID="$(curl -sS "$API/releases/tags/$TAG" -H "Authorization: token $FORGEJO_TOKEN" | python3 -c '
|
||||||
|
import json,sys
|
||||||
|
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":(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, prerelease=$PRERELEASE)"
|
||||||
|
else
|
||||||
|
# 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.
|
||||||
|
ASSETS="$(curl -fsS "$API/releases/$ID/assets" -H "Authorization: token $FORGEJO_TOKEN")"
|
||||||
|
for f in "$STAGE"/*.bin; do
|
||||||
|
name="$(basename "$f")"
|
||||||
|
aid="$(printf '%s' "$ASSETS" | python3 -c 'import json,sys; n=sys.argv[1]; print(next((str(a["id"]) for a in json.load(sys.stdin) if a.get("name")==n),""))' "$name")"
|
||||||
|
[ -z "$aid" ] || curl -fsS -X DELETE "$API/releases/$ID/assets/$aid" -H "Authorization: token $FORGEJO_TOKEN" >/dev/null
|
||||||
|
curl -fsS -X POST "$API/releases/$ID/assets?name=$name" -H "Authorization: token $FORGEJO_TOKEN" -F "attachment=@$f" >/dev/null
|
||||||
|
echo "==> Attached $name"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Done -> https://git.dk1da.de/DK1DA/FreeTRX/releases/tag/$TAG"
|
||||||
49
website/build-docs.sh
Normal file
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
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
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
|
||||||
Loading…
Reference in a new issue