rsdeck: prepare dual-mode 2.0 release

This commit is contained in:
DeFiDude
2026-06-13 17:33:00 -06:00
parent e2dad27836
commit f6d6aff288
58 changed files with 984 additions and 447 deletions
+23 -99
View File
@@ -1,4 +1,4 @@
name: PlatformIO Build
name: Firmware Build
on:
push:
@@ -11,55 +11,6 @@ env:
ARDUINO_CLI_VERSION: 1.4.1
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install PlatformIO
run: pip install platformio esptool
- name: Build firmware
run: pio run -e ratdeck_915
- name: Merge single .bin for M5Burner
run: |
esptool.py --chip esp32s3 merge-bin \
--flash-mode dio --flash-size 16MB \
-o ratdeck-merged.bin \
0x0000 .pio/build/ratdeck_915/bootloader.bin \
0x8000 .pio/build/ratdeck_915/partitions.bin \
0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
0x10000 .pio/build/ratdeck_915/firmware.bin
- name: Package firmware ZIP
run: |
mkdir -p ratdeck-firmware
cp .pio/build/ratdeck_915/bootloader.bin ratdeck-firmware/
cp .pio/build/ratdeck_915/partitions.bin ratdeck-firmware/
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin ratdeck-firmware/
cp .pio/build/ratdeck_915/firmware.bin ratdeck-firmware/
cat > ratdeck-firmware/manifest.json << 'MANIFEST'
{
"chipFamily": "ESP32-S3",
"flashSize": "16MB",
"flashMode": "dio",
"flashFreq": "80m",
"parts": [
{ "path": "bootloader.bin", "offset": "0x0000" },
{ "path": "partitions.bin", "offset": "0x8000" },
{ "path": "boot_app0.bin", "offset": "0xe000" },
{ "path": "firmware.bin", "offset": "0x10000" }
]
}
MANIFEST
cd ratdeck-firmware && zip -r ../ratdeck-firmware.zip . && cd ..
package:
runs-on: ubuntu-latest
steps:
@@ -81,9 +32,9 @@ jobs:
run: pip install platformio esptool
- name: Prepare RNode Arduino dependencies
run: make prep-tdeck
run: make -C vendor/rnode_firmware prep-esp32
- name: Build dual-boot package
- name: Build release package
run: make package
- name: Upload firmware artifacts
@@ -93,7 +44,7 @@ jobs:
path: dist/*
release:
needs: [build, package]
needs: package
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
@@ -106,45 +57,6 @@ jobs:
with:
python-version: '3.12'
- name: Install PlatformIO
run: pip install platformio esptool
- name: Build firmware
run: pio run -e ratdeck_915
- name: Merge single .bin for M5Burner
run: |
esptool.py --chip esp32s3 merge-bin \
--flash-mode dio --flash-size 16MB \
-o ratdeck-merged.bin \
0x0000 .pio/build/ratdeck_915/bootloader.bin \
0x8000 .pio/build/ratdeck_915/partitions.bin \
0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin \
0x10000 .pio/build/ratdeck_915/firmware.bin
- name: Package firmware ZIP
run: |
mkdir -p ratdeck-firmware
cp .pio/build/ratdeck_915/bootloader.bin ratdeck-firmware/
cp .pio/build/ratdeck_915/partitions.bin ratdeck-firmware/
cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin ratdeck-firmware/
cp .pio/build/ratdeck_915/firmware.bin ratdeck-firmware/
cat > ratdeck-firmware/manifest.json << 'MANIFEST'
{
"chipFamily": "ESP32-S3",
"flashSize": "16MB",
"flashMode": "dio",
"flashFreq": "80m",
"parts": [
{ "path": "bootloader.bin", "offset": "0x0000" },
{ "path": "partitions.bin", "offset": "0x8000" },
{ "path": "boot_app0.bin", "offset": "0xe000" },
{ "path": "firmware.bin", "offset": "0x10000" }
]
}
MANIFEST
cd ratdeck-firmware && zip -r ../ratdeck-firmware.zip . && cd ..
- name: Install Arduino CLI
run: |
mkdir -p "$RUNNER_TEMP/arduino-cli"
@@ -152,24 +64,36 @@ jobs:
| BINDIR="$RUNNER_TEMP/arduino-cli" sh -s "$ARDUINO_CLI_VERSION"
echo "$RUNNER_TEMP/arduino-cli" >> "$GITHUB_PATH"
- name: Prepare RNode Arduino dependencies
run: make prep-tdeck
- name: Install Python tools
run: pip install platformio esptool
- name: Build dual-boot package
- name: Prepare RNode Arduino dependencies
run: make -C vendor/rnode_firmware prep-esp32
- name: Build release package
run: make package
- name: Create Release
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
release_args=(--generate-notes)
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
release_args+=(--prerelease)
fi
assets=(ratdeck-firmware.zip ratdeck-merged.bin dist/*)
if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" "${assets[@]}" --clobber
artifacts=(
dist/rsdeck-full.zip
dist/rsdeck-standalone.zip
dist/rsdeck-rnode.zip
dist/rsdeck-standalone-m5launcher.bin
dist/rsdeck-rnode-m5launcher.bin
)
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release upload "$GITHUB_REF_NAME" "${artifacts[@]}" --clobber --repo "$GITHUB_REPOSITORY"
else
gh release create "$GITHUB_REF_NAME" "${assets[@]}" "${release_args[@]}"
gh release create "$GITHUB_REF_NAME" "${artifacts[@]}" "${release_args[@]}" --repo "$GITHUB_REPOSITORY"
fi
+14 -7
View File
@@ -1,4 +1,4 @@
STANDALONE_ENV ?= ratdeck_915
STANDALONE_ENV ?= rsdeck_915
RNODE_DIR ?= vendor/rnode_firmware
LAUNCHER_DIR ?= launcher
BUILD_DIR ?= build
@@ -10,8 +10,8 @@ PARTITIONS_BIN := $(BUILD_DIR)/rsdeck_16mb_dual_partitions.bin
FULL_NAME := rsdeck-full
STANDALONE_NAME := rsdeck-standalone
RNODE_ONLY_NAME := rsdeck-rnode
APP_STANDALONE_NAME := rsdeck-standalone-app
APP_RNODE_NAME := rsdeck-rnode-app
M5LAUNCHER_STANDALONE_NAME := rsdeck-standalone-m5launcher
M5LAUNCHER_RNODE_NAME := rsdeck-rnode-m5launcher
FULL_BIN := $(BUILD_DIR)/$(FULL_NAME).bin
STANDALONE_BIN := $(BUILD_DIR)/$(STANDALONE_NAME).bin
@@ -19,7 +19,7 @@ RNODE_ONLY_BIN := $(BUILD_DIR)/$(RNODE_ONLY_NAME).bin
LAUNCHER_BIN := $(LAUNCHER_DIR)/.pio/build/tdeck_launcher/firmware.bin
STANDALONE_APP_BIN := .pio/build/$(STANDALONE_ENV)/firmware.bin
STANDALONE_FACTORY_BIN := ratdeck-merged.bin
STANDALONE_FACTORY_BIN := rsdeck-merged.bin
RNODE_OUTPUT := $(RNODE_DIR)/build/tdeck.esp32.esp32s3
RNODE_BIN := $(RNODE_OUTPUT)/RNode_Firmware.ino.bin
@@ -90,11 +90,18 @@ bundle: full-image
package: full-image standalone-image rnode-only-image
mkdir -p $(DIST_DIR)
rm -f $(DIST_DIR)/$(FULL_NAME).zip \
$(DIST_DIR)/$(STANDALONE_NAME).zip \
$(DIST_DIR)/$(RNODE_ONLY_NAME).zip \
$(DIST_DIR)/$(M5LAUNCHER_STANDALONE_NAME).bin \
$(DIST_DIR)/$(M5LAUNCHER_RNODE_NAME).bin \
$(DIST_DIR)/rsdeck-standalone-app.bin \
$(DIST_DIR)/rsdeck-rnode-app.bin
python3 tools/package_merged_zip.py --image $(FULL_BIN) --name $(FULL_NAME) --output $(DIST_DIR)/$(FULL_NAME).zip
python3 tools/package_merged_zip.py --image $(STANDALONE_BIN) --name $(STANDALONE_NAME) --output $(DIST_DIR)/$(STANDALONE_NAME).zip
python3 tools/package_merged_zip.py --image $(RNODE_ONLY_BIN) --name $(RNODE_ONLY_NAME) --output $(DIST_DIR)/$(RNODE_ONLY_NAME).zip
cp $(STANDALONE_APP_BIN) $(DIST_DIR)/$(APP_STANDALONE_NAME).bin
cp $(RNODE_BIN) $(DIST_DIR)/$(APP_RNODE_NAME).bin
cp $(STANDALONE_APP_BIN) $(DIST_DIR)/$(M5LAUNCHER_STANDALONE_NAME).bin
cp $(RNODE_BIN) $(DIST_DIR)/$(M5LAUNCHER_RNODE_NAME).bin
release: package
@@ -102,4 +109,4 @@ flash: bundle
python3 -m esptool --chip esp32s3 --port $(PORT) --baud 460800 --before default_reset --after hard_reset write-flash 0x0 $(FULL_BIN)
clean:
rm -rf $(BUILD_DIR) $(DIST_DIR)
rm -rf $(BUILD_DIR) $(DIST_DIR) $(STANDALONE_FACTORY_BIN)
+91 -41
View File
@@ -1,54 +1,69 @@
<div align="center">
# [Ratdeck](https://ratspeak.org/)
# [rsDeck](https://ratspeak.org/)
**Standalone Reticulum for the LilyGo T-Deck**
**Dual-mode Ratspeak firmware for the LilyGo T-Deck Plus.**
[![Status](https://img.shields.io/badge/status-beta-yellow.svg)](#install)
[![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-2.0.0-success.svg)](https://github.com/ratspeak/rsDeck/releases)
[Ratspeak](https://github.com/ratspeak/Ratspeak) |
[Docs](https://ratspeak.org/docs.html) |
[Downloads](https://ratspeak.org/download.html) |
[rsReticulum](https://github.com/ratspeak/rsReticulum)
</div>
Ratdeck turns a [LilyGo T-Deck](https://www.lilygo.cc/products/t-deck-plus) into a full standalone [Reticulum](https://reticulum.network/) node. It's not just an RNode which requires another device — it's the complete setup.
End-to-end encrypted [LXMF](https://github.com/markqvist/LXMF) messaging over LoRa, TCP over WiFi for bridging to the wider Reticulum network, node discovery, identity management, and more.
<div align="center">
---
[![Ratspeak Demo](https://img.youtube.com/vi/F6I6fkMPxgI/maxresdefault.jpg)](https://www.youtube.com/watch?v=F6I6fkMPxgI)
<sub>[▶ YouTube: Reticulum Standalone - T-Deck & Cardputer Adv](https://www.youtube.com/watch?v=F6I6fkMPxgI)</sub>
rsDeck turns a [LilyGo T-Deck Plus](https://www.lilygo.cc/products/t-deck-plus)
into a two-mode Reticulum handheld. Standalone mode is an on-device
Ratspeak/LXMF messenger. RNode mode makes the T-Deck a host-controlled radio
for Ratspeak, Sideband, or another Reticulum client over BLE or USB serial.
---
</div>
## Install
## Installing
Use the Ratspeak web flasher:
[ratspeak.org/download.html](https://ratspeak.org/download.html).
The easiest way is the **[web flasher](https://ratspeak.org/download.html)** — enable download mode (hold the trackball while powering on), plug in the USB, click flash, done.
Put the T-Deck Plus in download mode by holding the trackball while powering on,
connect USB, then flash `rsdeck-full`. The standalone-only and RNode-only
images are release artifacts for launcher users or focused testing.
To build from source:
## Modes
```bash
git clone https://github.com/ratspeak/ratdeck
cd ratdeck
pip install platformio
python3 -m platformio run --target upload
```
On boot, the launcher lets you choose:
## Usage
- **Standalone**: a local Reticulum/LXMF messenger with identity management,
contacts, peer discovery, messages, LoRa, and WiFi TCP access.
- **RNode**: a host-controlled RNode-style radio target for Ratspeak or other
Reticulum clients over BLE or USB serial.
On first boot, Ratdeck generates a Reticulum identity and shows a name input screen. Your LXMF address (32-character hex string) is what you share with contacts.
RNode mode self-provisions the T-Deck RNode product/model/default config and
running firmware hash on first boot, so users should not need a separate
`rnodeconf` setup step for the bundled release images.
**Tabs:** Home, Friends, Msgs, Peers, Setup — navigate with the trackball.
## Basic Use
**Manually announce:** To send an announcement manually, press the trackball or enter on the home tab.
On first boot, Standalone mode generates a Reticulum identity and asks for a
display name. Your LXMF address is the 32-character hex string you share with
contacts.
**Add/delete contacts/messages:** Hold the trackball down on a chat or a peer to add or delete.
- Tabs: Home, Friends, Msgs, Peers, Setup.
- Navigation: trackball movement and click/Enter.
- Announce: press the trackball or Enter on the Home tab.
- Add contacts: select a discovered peer, then open or save the chat.
- Send messages: open a chat, type, and press Enter.
- Delivery color: yellow while sending, green after delivery confirmation.
**Sending a message:** Find someone in Peers, select to open chat, type your message, hit Enter. Status goes yellow (sending) → green (delivery confirmed).
## Radio Presets
**Radio presets** (Setup → Radio). Format: name — SF, bandwidth, coding rate, TX power. `Long Fast` is the compiled-in default.
`Long Fast` is the compiled-in default. All radio parameters can also be tuned
from Setup. Changes apply immediately.
| Preset | SF | BW | CR | TXP | Bitrate | Link budget |
|---|---|---|---|---|---|---|
|---|---:|---:|---:|---:|---:|---:|
| Short Turbo | 7 | 500 kHz | 4/5 | 14 dBm | 21.99 kbps | 140 dB |
| Short Fast | 7 | 250 kHz | 4/5 | 14 dBm | 10.84 kbps | 143 dB |
| Short Slow | 8 | 250 kHz | 4/5 | 14 dBm | 6.25 kbps | 145.5 dB |
@@ -58,30 +73,65 @@ On first boot, Ratdeck generates a Reticulum identity and shows a name input scr
| **Long Fast** *(default)* | **11** | **250 kHz** | **4/5** | **22 dBm** | **1.07 kbps** | **153 dB** |
| Long Moderate | 11 | 125 kHz | 4/8 | 22 dBm | 0.34 kbps | 156 dB |
All radio parameters are also individually tunable; changes apply immediately, no reboot. Please operate in accordance with local laws, as you are solely responsible for knowing which regulations and requirements apply to your jurisdiction.
You are responsible for operating within local laws and radio regulations.
### WiFi Bridging (Alpha)
## WiFi Bridging
Use **STA mode** to connect to existing WiFi and reach remote nodes like `rns.ratspeak.org:4242`.
WiFi bridging is experimental. STA mode can connect to existing WiFi and reach
remote Reticulum nodes such as `rns.ratspeak.org:4242`.
To bridge LoRa with Reticulum on your computer:
1. Set WiFi to **AP mode** in Setup → Network (creates `ratdeck-XXXX`, password: `ratspeak`)
2. Connect your computer to that network
3. Add to your Reticulum config:
AP mode exposes a local TCP endpoint for a nearby Reticulum host:
```ini
[[ratdeck]]
[[rsdeck]]
type = TCPClientInterface
target_host = 192.168.4.1
target_port = 4242
```
Note: WiFi bridging methods and interfaces will be revamped with Ratspeak's client release, therefore, it's unlikely AP mode works at all currently.
The bridging UI and interface behavior may change as Ratspeak's client release
stabilizes.
## Build From Source
```bash
git clone https://github.com/ratspeak/rsDeck
cd rsDeck
python3 -m pip install platformio esptool
make prep-tdeck
make package
make flash port=/dev/cu.usbmodem3101
```
Useful build targets:
```bash
make build-launcher # launcher only
make build-standalone # standalone messenger app
make build-rnode # host-controlled RNode target
make full-image # launcher + Standalone + RNode
make package # release zips and launcher bins
```
Release artifacts are written to `dist/`:
```text
dist/rsdeck-full.zip
dist/rsdeck-standalone.zip
dist/rsdeck-rnode.zip
dist/rsdeck-standalone-m5launcher.bin
dist/rsdeck-rnode-m5launcher.bin
```
Use the `.zip` files with the Ratspeak web flasher. The `*-m5launcher.bin`
files are app images for M5Launcher/M5Burner-style launchers that boot
Standalone or RNode directly from SD.
## License
GNU Affero General Public License v3.0 or later. See [LICENSE](LICENSE).
rsDeck standalone firmware, launcher, partition tables, and packaging tools are
licensed under the GNU Affero General Public License v3.0 or later. See
[LICENSE](LICENSE).
Vendored third-party code keeps its own license notices, including
`lib/Crypto` under MIT.
`vendor/rnode_firmware/` and `lib/Crypto`.
+13 -10
View File
@@ -6,7 +6,7 @@ flash, with a boot-time chooser:
| Partition | Slot | Offset | Size | Contents |
|------------|-------|----------|---------|----------|
| launcher | ota_0 | 0x10000 | 1MB | Boot chooser UI |
| standalone | ota_1 | 0x110000 | 4MB | Full Ratdeck messenger |
| standalone | ota_1 | 0x110000 | 4MB | Full rsDeck messenger |
| rnode | ota_2 | 0x510000 | 3MB | RNode (KISS over USB CDC + BLE) |
| littlefs | — | 0x810000 | ~7.9MB | User data (unchanged from single-app layout) |
@@ -32,22 +32,25 @@ Trackball up/down + click, or keys: `W`/`S` select, `Enter` boot,
## RNode mode
The vendored RNode firmware (`vendor/rnode_firmware`, `make firmware-tdeck`)
self-provisions EEPROM on first boot (PRODUCT_TDECK_V1 / MODEL_D9, 915 MHz
defaults) and enables BLE pairing on first run when no bonds exist. The display
shows the standard RNode UI (node address, waterfall, status) and the BLE
pairing PIN during pairing; it blanks after 15s idle. Reset to return to the
launcher.
self-provisions EEPROM on first boot (PRODUCT_TDECK_V1 / MODEL_D9, default
radio config) and adopts the running firmware hash for this app image. BLE stays
connectable for already-bonded hosts; tap the on-screen Pair via BLE panel to
open a 30s pairing window for a new host. The display shows the standard RNode
UI (node address, waterfall, status) and the BLE pairing PIN during explicit
pairing; it blanks after 60s idle. Reset to return to the launcher.
## Build
```bash
make prep-tdeck # once: arduino-cli esp32 core + libs
make package # dist/: rsdeck-full.zip, rsdeck-standalone.zip,
# rsdeck-rnode.zip, rsdeck-*-app.bin
# rsdeck-rnode.zip, rsdeck-*-m5launcher.bin
make flash port=/dev/cu.usbmodemXXXX # flash full image
```
`rsdeck-full.bin` flashes at offset 0x0 and contains all three apps.
`rsdeck-standalone.zip` repackages the legacy single-app `ratdeck-merged.bin`
(old partition table, no launcher). The `*-app.bin` files are bare app images
for external launchers or manual OTA-slot flashing.
`rsdeck-standalone.zip` repackages the legacy single-app `rsdeck-merged.bin`
(old partition table, no launcher). The `*-m5launcher.bin` files are bare app
images for M5Launcher/M5Burner-style launchers, external launchers, or manual
OTA-slot flashing. The RNode-only app image is T-Deck-specific and
self-provisioning; it is not a generic upstream `rnodeconf` image.
+2 -8
View File
@@ -187,8 +187,6 @@ void drawScreen() {
display.setTextColor(kMuted, kBg);
display.setCursor(24, 196);
display.print("Trackball or W/S select, click/Enter boot");
display.setCursor(24, 212);
display.print("Touch: tap card, tap again to boot R/N quick");
drawCountdown(true);
}
@@ -209,7 +207,7 @@ void pauseAutoBoot() {
drawCountdown(true);
}
void showBooting(const char *label, bool rnode) {
void showBooting(const char *label) {
booting = true;
display.fillScreen(kBg);
display.setTextSize(3);
@@ -220,10 +218,6 @@ void showBooting(const char *label, bool rnode) {
display.setTextColor(kMuted, kBg);
display.setCursor(30, 110);
display.print("Starting...");
if (rnode) {
display.setCursor(30, 134);
display.print("Reset the device to return here.");
}
}
void showError(const char *message) {
@@ -244,7 +238,7 @@ void startChoice(Choice choice) {
FirmwareMode mode = choice == Choice::Standalone ? FirmwareMode::Standalone : FirmwareMode::RNode;
Serial.printf("[LAUNCHER] booting %s\n", mode_name(mode));
showBooting(mode_name(mode), mode == FirmwareMode::RNode);
showBooting(mode_name(mode));
SwitchResult result = set_next_boot(mode);
if (!result.ok) {
Serial.printf("[LAUNCHER] boot switch failed: %s\n", result.message);
+4 -4
View File
@@ -3,9 +3,9 @@
#include <stdint.h>
// Forward-declare custom ratdeck fonts for LV_FONT_DEFAULT
// Forward-declare custom rsDeck fonts for LV_FONT_DEFAULT
typedef struct _lv_font_t lv_font_t;
extern const lv_font_t lv_font_ratdeck_14;
extern const lv_font_t lv_font_rsdeck_14;
// Color depth: 16-bit RGB565
#define LV_COLOR_DEPTH 16
@@ -41,13 +41,13 @@ extern const lv_font_t lv_font_ratdeck_14;
#define LV_USE_THEME_DEFAULT 0
#define LV_THEME_DEFAULT_DARK 0
// Fonts - built-in (only 16 still used for titles; 10/12/14 replaced by custom ratdeck fonts)
// Fonts - built-in (only 16 still used for titles; 10/12/14 replaced by custom rsDeck fonts)
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 1 // Delivery-status check glyphs in LvMessageView
#define LV_FONT_MONTSERRAT_14 0
#define LV_FONT_MONTSERRAT_16 1
#define LV_FONT_UNSCII_8 0
#define LV_FONT_DEFAULT &lv_font_ratdeck_14
#define LV_FONT_DEFAULT &lv_font_rsdeck_14
// Widgets — only enable what we actually use
#define LV_USE_LABEL 1
+1 -1
View File
@@ -15,7 +15,7 @@ def merge_bin(source, target, env):
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif32")
boot_app0 = os.path.join(framework_dir, "tools", "partitions", "boot_app0.bin")
output = os.path.join(project_dir, "ratdeck-merged.bin")
output = os.path.join(project_dir, "rsdeck-merged.bin")
python = env.subst("$PYTHONEXE")
env.Execute(
+2 -2
View File
@@ -1,4 +1,4 @@
[env:ratdeck_915]
[env:rsdeck_915]
platform = espressif32@6.7.0
board = esp32-s3-devkitc-1
framework = arduino
@@ -12,7 +12,7 @@ build_flags =
-std=gnu++17
-fexceptions
-O2
-DRATDECK=1
-DRSDECK=1
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_USB_MODE=1
; Silence vfs_api log_e noise — it fires on every open/remove of a missing
+1 -1
View File
@@ -168,7 +168,7 @@ void AudioNotify::playError() {
void AudioNotify::playBoot() {
if (!_enabled || !_i2sReady) return;
// === RATDECK BOOT SEQUENCE ===
// === RSDECK BOOT SEQUENCE ===
// Sci-fi computer startup: sweep -> digital arpeggio -> confirmation
// Total ~550ms
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
// =============================================================================
// Ratdeck — LilyGo T-Deck Plus Pin Definitions
// rsDeck — LilyGo T-Deck Plus Pin Definitions
// =============================================================================
// --- Power Control ---
+15 -13
View File
@@ -1,13 +1,13 @@
#pragma once
// =============================================================================
// Ratdeck — Compile-Time Configuration
// rsDeck — Compile-Time Configuration
// =============================================================================
#define RATDECK_VERSION_MAJOR 1
#define RATDECK_VERSION_MINOR 9
#define RATDECK_VERSION_PATCH 3
#define RATDECK_VERSION_STRING "1.9.3"
#define RSDECK_VERSION_MAJOR 2
#define RSDECK_VERSION_MINOR 0
#define RSDECK_VERSION_PATCH 0
#define RSDECK_VERSION_STRING "2.0.0"
// --- Feature Flags ---
#define HAS_DISPLAY true
@@ -19,10 +19,10 @@
// TODO(BLE): Revisit as a dedicated Ratspeak BLE Peer transport based on the
// rsReticulum GATT/fragmentation/anti-loop design, after T-Deck Plus memory,
// flash, power, and interop behavior are tested end to end.
#ifndef RATDECK_EXPERIMENTAL_BLE
#define RATDECK_EXPERIMENTAL_BLE 0
#ifndef RSDECK_EXPERIMENTAL_BLE
#define RSDECK_EXPERIMENTAL_BLE 0
#endif
#define HAS_BLE RATDECK_EXPERIMENTAL_BLE
#define HAS_BLE RSDECK_EXPERIMENTAL_BLE
#define HAS_SD true
#define HAS_AUDIO true
#define HAS_GPS true // UBlox MIA-M10Q UART GPS
@@ -43,6 +43,8 @@
#define PATH_MESSAGES "/messages"
// --- SD Card Paths ---
// Legacy path kept intentionally so existing Standalone users keep their data.
// TODO: Migrate to /rsdeck only with an explicit data migration plan.
#define SD_PATH_CONFIG_DIR "/ratdeck/config"
#define SD_PATH_USER_CONFIG "/ratdeck/config/user.json"
#define SD_PATH_MESSAGES "/ratdeck/messages"
@@ -56,14 +58,14 @@
#define TCP_CONNECT_TIMEOUT_MS 500
// --- Announce Flood Defense ---
#define RATDECK_MAX_ANNOUNCES_PER_SEC 5 // Transport-level rate limit (before Ed25519 verify)
#define RSDECK_MAX_ANNOUNCES_PER_SEC 5 // Transport-level rate limit (before Ed25519 verify)
// --- Limits ---
#define RATDECK_MAX_NODES 100 // Endpoint device, not transport node
#define RATDECK_MAX_MESSAGES_PER_CONV 100
#define RSDECK_MAX_NODES 100 // Endpoint device, not transport node
#define RSDECK_MAX_MESSAGES_PER_CONV 100
#define FLASH_MSG_CACHE_LIMIT 20
#define RATDECK_MAX_OUTQUEUE 20
#define RATDECK_LXMF_SINGLE_FRAME_MAX 254 // T-Deck-safe payload cap until resource transfers are fixed
#define RSDECK_MAX_OUTQUEUE 20
#define RSDECK_LXMF_SINGLE_FRAME_MAX 254 // T-Deck-safe payload cap until resource transfers are fixed
#define PATH_PERSIST_INTERVAL_MS 60000
// --- Power Management ---
+23 -1
View File
@@ -1,6 +1,26 @@
#include "UserConfig.h"
#include "config/BoardConfig.h"
namespace {
bool validLoRaFrequency(uint32_t freq) {
return (freq >= 863000000UL && freq <= 870000000UL) ||
(freq >= 902000000UL && freq <= 928000000UL) ||
(freq >= 920000000UL && freq <= 925000000UL);
}
}
void UserConfig::sanitizeSettings() {
if (_settings.radioRegion >= REGION_COUNT) _settings.radioRegion = REGION_AMERICAS;
if (!validLoRaFrequency(_settings.loraFrequency)) {
_settings.loraFrequency = REGION_FREQ[constrain((int)_settings.radioRegion, 0, REGION_COUNT - 1)];
}
_settings.loraSF = constrain(_settings.loraSF, 5, 12);
_settings.loraBW = constrain(_settings.loraBW, 7800UL, 500000UL);
_settings.loraCR = constrain(_settings.loraCR, 5, 8);
_settings.loraTxPower = constrain(_settings.loraTxPower, -9, 22);
_settings.loraPreamble = constrain(_settings.loraPreamble, 6L, 65L);
}
bool UserConfig::parseJson(const String& json) {
Serial.printf("[CONFIG] Parsing config (%d bytes)\n", json.length());
@@ -102,11 +122,13 @@ bool UserConfig::parseJson(const String& json) {
if (_settings.announceInterval > 360) _settings.announceInterval = 360;
_settings.devMode = doc["dev_mode"] | false;
sanitizeSettings();
Serial.println("[CONFIG] Settings loaded");
return true;
}
String UserConfig::serializeToJson() const {
String UserConfig::serializeToJson() {
sanitizeSettings();
JsonDocument doc;
doc["radio_region"] = _settings.radioRegion;
+2 -1
View File
@@ -111,7 +111,8 @@ public:
private:
bool parseJson(const String& json);
String serializeToJson() const;
String serializeToJson();
void sanitizeSettings();
UserSettings _settings;
};
+3 -3
View File
@@ -8,6 +8,6 @@
#include <lvgl.h>
LV_FONT_DECLARE(lv_font_ratdeck_10)
LV_FONT_DECLARE(lv_font_ratdeck_12)
LV_FONT_DECLARE(lv_font_ratdeck_14)
LV_FONT_DECLARE(lv_font_rsdeck_10)
LV_FONT_DECLARE(lv_font_rsdeck_12)
LV_FONT_DECLARE(lv_font_rsdeck_14)
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 10 px
* Bpp: 4
* Opts: --no-compress --no-prefilter --bpp 4 --size 10 --font .pio/libdeps/ratdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_ratdeck_10.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 4 --size 10 --font .pio/libdeps/rsdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_rsdeck_10.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -10,11 +10,11 @@
#include <lvgl.h>
#endif
#ifndef LV_FONT_RATDECK_10
#define LV_FONT_RATDECK_10 1
#ifndef LV_FONT_RSDECK_10
#define LV_FONT_RSDECK_10 1
#endif
#if LV_FONT_RATDECK_10
#if LV_FONT_RSDECK_10
/*-----------------
* BITMAPS
@@ -4614,9 +4614,9 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t lv_font_ratdeck_10 = {
const lv_font_t lv_font_rsdeck_10 = {
#else
lv_font_t lv_font_ratdeck_10 = {
lv_font_t lv_font_rsdeck_10 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
@@ -4638,5 +4638,5 @@ lv_font_t lv_font_ratdeck_10 = {
#endif /*#if LV_FONT_RATDECK_10*/
#endif /*#if LV_FONT_RSDECK_10*/
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 12 px
* Bpp: 4
* Opts: --no-compress --no-prefilter --bpp 4 --size 12 --font .pio/libdeps/ratdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_ratdeck_12.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 4 --size 12 --font .pio/libdeps/rsdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_rsdeck_12.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -10,11 +10,11 @@
#include <lvgl.h>
#endif
#ifndef LV_FONT_RATDECK_12
#define LV_FONT_RATDECK_12 1
#ifndef LV_FONT_RSDECK_12
#define LV_FONT_RSDECK_12 1
#endif
#if LV_FONT_RATDECK_12
#if LV_FONT_RSDECK_12
/*-----------------
* BITMAPS
@@ -5565,9 +5565,9 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t lv_font_ratdeck_12 = {
const lv_font_t lv_font_rsdeck_12 = {
#else
lv_font_t lv_font_ratdeck_12 = {
lv_font_t lv_font_rsdeck_12 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
@@ -5589,5 +5589,5 @@ lv_font_t lv_font_ratdeck_12 = {
#endif /*#if LV_FONT_RATDECK_12*/
#endif /*#if LV_FONT_RSDECK_12*/
@@ -1,7 +1,7 @@
/*******************************************************************************
* Size: 14 px
* Bpp: 4
* Opts: --no-compress --no-prefilter --bpp 4 --size 14 --font .pio/libdeps/ratdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_ratdeck_14.c --force-fast-kern-format
* Opts: --no-compress --no-prefilter --bpp 4 --size 14 --font .pio/libdeps/rsdeck_915/lvgl/scripts/built_in_font/Montserrat-Medium.ttf -r 0x20-0x7F,0xA0-0xFF,0x100-0x17F,0x400-0x4FF --format lvgl -o src/fonts/lv_font_rsdeck_14.c --force-fast-kern-format
******************************************************************************/
#ifdef LV_LVGL_H_INCLUDE_SIMPLE
@@ -10,11 +10,11 @@
#include <lvgl.h>
#endif
#ifndef LV_FONT_RATDECK_14
#define LV_FONT_RATDECK_14 1
#ifndef LV_FONT_RSDECK_14
#define LV_FONT_RSDECK_14 1
#endif
#if LV_FONT_RATDECK_14
#if LV_FONT_RSDECK_14
/*-----------------
* BITMAPS
@@ -6251,9 +6251,9 @@ static lv_font_fmt_txt_dsc_t font_dsc = {
/*Initialize a public general font descriptor*/
#if LVGL_VERSION_MAJOR >= 8
const lv_font_t lv_font_ratdeck_14 = {
const lv_font_t lv_font_rsdeck_14 = {
#else
lv_font_t lv_font_ratdeck_14 = {
lv_font_t lv_font_rsdeck_14 = {
#endif
.get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/
.get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/
@@ -6275,5 +6275,5 @@ lv_font_t lv_font_ratdeck_14 = {
#endif /*#if LV_FONT_RATDECK_14*/
#endif /*#if LV_FONT_RSDECK_14*/
+19 -8
View File
@@ -1,5 +1,5 @@
// =============================================================================
// Ratdeck — Main Entry Point
// rsDeck — Main Entry Point
// LilyGo T-Deck Plus: LovyanGFX Direct UI + microReticulum + LXMF Messaging
// =============================================================================
@@ -411,7 +411,7 @@ void onHotkeyAutoIface() {
}
void onHotkeyDiag() {
Serial.println("=== DIAGNOSTIC DUMP ===");
Serial.printf("Device: Ratdeck T-Deck Plus\n");
Serial.printf("Device: rsDeck T-Deck Plus\n");
Serial.printf("Identity: %s\n", rns.identityHash().c_str());
Serial.printf("Transport: %s\n", rns.isTransportActive() ? "ACTIVE" : "OFFLINE");
Serial.printf("Paths: %d Links: %d\n", (int)rns.pathCount(), (int)rns.linkCount());
@@ -425,6 +425,17 @@ void onHotkeyDiag() {
radio.getTxPower());
Serial.printf("Regulator: %s\n", LORA_USE_DCDC_REGULATOR ? "DC-DC" : "LDO");
Serial.printf("Preamble: %ld symbols\n", radio.getPreambleLength());
Serial.printf("Bitrate: %lu bps LDRO: %s frame255: %.0f ms\n",
(unsigned long)radio.getBitrate(),
radio.lowDataRateEnabled() ? "ON" : "off",
radio.getAirtime(MAX_PACKET_SIZE));
if (auto* loraIf = rns.loraInterface()) {
Serial.printf("LoRaIF: bitrate=%lu bps split_timeout=%lu ms frame=%.0f ms airtime=%.2f%%\n",
(unsigned long)loraIf->bitrate(),
loraIf->splitRxTimeoutMs(),
loraIf->singleFrameAirtimeMs(),
loraIf->airtimeUtilization() * 100.0f);
}
Serial.printf("IQ invert: %s\n", radio.getInvertIQ() ? "ON" : "off");
Serial.printf("SyncWord regs: 0x%02X%02X\n",
radio.readRegister(REG_SYNC_WORD_MSB_6X),
@@ -517,7 +528,7 @@ void onHotkeyRssiMonitor() {
void onHotkeyRadioTest() {
Serial.println("[TEST] Sending raw test packet...");
uint8_t header = 0xA0;
const char* testPayload = "RATDECK_TEST_1234567890";
const char* testPayload = "RSDECK_TEST_1234567890";
radio.beginPacket();
radio.write(header);
radio.write((const uint8_t*)testPayload, strlen(testPayload));
@@ -659,7 +670,7 @@ static bool selectDiagnosticPeer(const char* explicitArg, RNS::Bytes& destHash,
}
static std::string makeDiagnosticLxmfPayload(size_t length) {
static constexpr char kPrefix[] = "RATDECK-LXMF-TEST:";
static constexpr char kPrefix[] = "RSDECK-LXMF-TEST:";
static constexpr char kPattern[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
@@ -1055,7 +1066,7 @@ void setup() {
delay(100);
Serial.println();
Serial.println("=================================");
Serial.printf(" Ratdeck v%s\n", RATDECK_VERSION_STRING);
Serial.printf(" rsDeck v%s\n", RSDECK_VERSION_STRING);
Serial.println(" LilyGo T-Deck Plus");
Serial.println("=================================");
@@ -1129,7 +1140,7 @@ void setup() {
delay(10);
if (sdStore.begin(&sharedSPI, SD_CS)) {
sdHadExistingData = sdStore.hasExistingData();
sdStore.formatForRatdeck();
sdStore.formatForRsDeck();
Serial.println("[SD] Card ready");
} else {
Serial.println("[SD] Not detected");
@@ -1715,7 +1726,7 @@ void setup() {
if (wipe) {
Serial.println("[BOOT] User chose to wipe old data");
lvDataCleanScreen.showStatus("Clearing old data...");
sdStore.wipeRatdeck();
sdStore.wipeRsDeck();
if (announceManager) announceManager->clearAll();
Serial.println("[BOOT] Old data cleared");
lvDataCleanScreen.showStatus("Done! Rebooting...");
@@ -1843,7 +1854,7 @@ void setup() {
keyboard.backlightOn();
}
Serial.println("[BOOT] Ratdeck ready");
Serial.println("[BOOT] rsDeck ready");
Serial.printf("[BOOT] Summary: radio=%s flash=%s sd=%s\n",
radioOnline ? "ONLINE" : "OFFLINE",
flash.isReady() ? "OK" : "FAIL",
+34 -14
View File
@@ -770,7 +770,13 @@ void SX1262::implicitHeaderMode() {
}
void SX1262::handleLowDataRate() {
_ldro = long((1 << _sf) / (getSignalBandwidth() / 1000)) > 16;
uint32_t bw = getSignalBandwidth();
if (_sf == 0 || bw == 0) {
_ldro = false;
return;
}
float symbolTimeMs = 1000.0f * (float)(1UL << _sf) / (float)bw;
_ldro = symbolTimeMs > 16.0f;
}
void SX1262::standby() {
@@ -785,23 +791,37 @@ void SX1262::sleep() {
float SX1262::getAirtime(uint16_t written) {
if (!_radioOnline) return 0;
float symbolRate = (float)getSignalBandwidth() / (float)(1 << _sf);
uint32_t bw = getSignalBandwidth();
uint8_t crDen = getCodingRate4();
if (_sf == 0 || bw == 0 || crDen < 5 || crDen > 8) return 0;
float symbolRate = (float)bw / (float)(1UL << _sf);
float symbolTimeMs = 1000.0 / symbolRate;
float loraSymbols;
float payloadSymbols;
if (_sf >= 7) {
loraSymbols = (8.0 * written + PHY_CRC_LORA_BITS - 4.0 * _sf + 8 + PHY_HEADER_LORA_SYMBOLS);
loraSymbols /= 4.0 * (_sf - 2 * (_ldro ? 1 : 0));
if (loraSymbols < 0) loraSymbols = 0;
loraSymbols = ceil(loraSymbols);
loraSymbols += _preambleLength + 0.25 + 8;
payloadSymbols = (8.0 * written + PHY_CRC_LORA_BITS - 4.0 * _sf + 8 + PHY_HEADER_LORA_SYMBOLS);
payloadSymbols /= 4.0 * (_sf - 2 * (_ldro ? 1 : 0));
if (payloadSymbols < 0) payloadSymbols = 0;
payloadSymbols = ceil(payloadSymbols) * crDen;
payloadSymbols += _preambleLength + 0.25 + 8;
} else {
loraSymbols = (8.0 * written + PHY_CRC_LORA_BITS - 4.0 * _sf + PHY_HEADER_LORA_SYMBOLS);
loraSymbols /= 4.0 * _sf;
if (loraSymbols < 0) loraSymbols = 0;
loraSymbols = ceil(loraSymbols);
loraSymbols += _preambleLength + 2.25 + 8;
payloadSymbols = (8.0 * written + PHY_CRC_LORA_BITS - 4.0 * _sf + PHY_HEADER_LORA_SYMBOLS);
payloadSymbols /= 4.0 * _sf;
if (payloadSymbols < 0) payloadSymbols = 0;
payloadSymbols = ceil(payloadSymbols) * crDen;
payloadSymbols += _preambleLength + 2.25 + 8;
}
return loraSymbols * symbolTimeMs;
return payloadSymbols * symbolTimeMs;
}
uint32_t SX1262::getBitrate() {
uint32_t bw = getSignalBandwidth();
uint8_t crDen = getCodingRate4();
if (_sf == 0 || bw == 0 || crDen < 5 || crDen > 8) return 0;
float bitrate = (float)_sf * (4.0f / (float)crDen) * (float)bw / (float)(1UL << _sf);
if (bitrate < 1.0f) return 1;
return (uint32_t)bitrate;
}
void IRAM_ATTR SX1262::onDio0Rise() {
+2
View File
@@ -65,6 +65,8 @@ public:
uint8_t getPacketType();
uint16_t getIrqFlags();
float getAirtime(uint16_t written);
uint32_t getBitrate();
bool lowDataRateEnabled() const { return _ldro; }
// --- FIFO access ---
void readBuffer(uint8_t* buffer, size_t size);
+19 -4
View File
@@ -152,13 +152,14 @@ void AnnounceManager::received_announce(
auto& node = _nodes[it->second];
if (node.lastSeen != 0 && now >= node.lastSeen &&
now - node.lastSeen < ANNOUNCE_MIN_INTERVAL_MS) return;
if (!name.empty()) node.name = name;
// Saved contacts own their local alias. Incoming announce app_data is
// still cached below, but must not reset the user-chosen contact name.
if (!name.empty() && !node.saved) node.name = name;
if (!idHex.empty()) node.identityHex = idHex;
node.lastSeen = now;
// hops_to() is expensive (linear routing table scan) — only call for saved contacts
if (node.saved) node.hops = RNS::Transport::hops_to(destination_hash);
if (_loraIf) { node.rssi = _loraIf->lastRxRssi(); node.snr = _loraIf->lastRxSnr(); }
if (node.saved) _contactsDirty = true;
// Name cache update — skip expensive toHex() for unnamed re-announces
if (!name.empty()) {
std::string destHex = destination_hash.toHex();
@@ -227,7 +228,14 @@ void AnnounceManager::received_announce(
DiscoveredNode node;
node.hash = destination_hash;
node.name = name.empty() ? destHex.substr(0, 12) : name;
if (!name.empty()) {
node.name = name;
} else {
auto cached = _nameCache.find(destHex);
node.name = (cached != _nameCache.end() && !cached->second.empty())
? cached->second
: destHex.substr(0, 12);
}
node.identityHex = idHex;
node.lastSeen = millis();
// Skip hops_to() for new (unsaved) nodes — resolved when added as contact
@@ -296,7 +304,14 @@ void AnnounceManager::addManualContact(const std::string& hexHash, const std::st
DiscoveredNode node;
node.hash = hash;
node.name = safeName.empty() ? hexHash.substr(0, 12) : safeName;
if (!safeName.empty()) {
node.name = safeName;
} else {
auto cached = _nameCache.find(hexHash);
node.name = (cached != _nameCache.end() && !cached->second.empty())
? cached->second
: hexHash.substr(0, 12);
}
node.lastSeen = millis();
node.saved = true;
_hashIndex[key] = (int)_nodes.size();
+9 -9
View File
@@ -26,7 +26,7 @@ bool LXMFManager::begin(ReticulumManager* rns, MessageStore* store) {
}
std::vector<LXMFMessage> pending = _store->loadPendingOutgoing();
for (auto& msg : pending) {
if ((int)_outQueue.size() >= RATDECK_MAX_OUTQUEUE) break;
if ((int)_outQueue.size() >= RSDECK_MAX_OUTQUEUE) break;
msg.lastRetryMs = 0;
_outQueue.push_back(msg);
}
@@ -121,7 +121,7 @@ bool LXMFManager::sendMessage(const RNS::Bytes& destHash, const std::string& con
msg.incoming = false;
msg.status = LXMFStatus::QUEUED;
if ((int)_outQueue.size() >= RATDECK_MAX_OUTQUEUE) {
if ((int)_outQueue.size() >= RSDECK_MAX_OUTQUEUE) {
Serial.println("[LXMF] Outbound queue full; refusing new message");
return false;
}
@@ -131,11 +131,11 @@ bool LXMFManager::sendMessage(const RNS::Bytes& destHash, const std::string& con
Serial.println("[LXMF] Message pack failed");
return false;
}
if (payload.size() > RATDECK_LXMF_SINGLE_FRAME_MAX) {
if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) {
msg.status = LXMFStatus::FAILED;
if (_store) _store->saveMessage(msg);
Serial.printf("[LXMF] Message too large for T-Deck safe path (%d > %d); resource transfer disabled\n",
(int)payload.size(), RATDECK_LXMF_SINGLE_FRAME_MAX);
(int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX);
return true;
}
if (preference == DeliveryPreference::Link) {
@@ -267,9 +267,9 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
if (payload.empty()) { Serial.println("[LXMF] packFull returned empty!"); msg.status = LXMFStatus::FAILED; return true; }
const std::string msgIdHex = msg.messageId.toHex();
const bool requireLink = _linkRequiredIds.count(msgIdHex) > 0;
if (payload.size() > RATDECK_LXMF_SINGLE_FRAME_MAX) {
if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) {
Serial.printf("[LXMF] Refusing resource-sized message (%d bytes); T-Deck safe cap is %d\n",
(int)payload.size(), RATDECK_LXMF_SINGLE_FRAME_MAX);
(int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX);
msg.status = LXMFStatus::FAILED;
return true;
}
@@ -338,7 +338,7 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
// Larger packets require split-frame over LoRa, which is unreliable — any single
// frame loss (CRC error, collision, half-duplex timing) kills the entire transfer
// with no recovery. Link-based delivery handles retransmission at the protocol level.
if (payloadBytes.size() <= RATDECK_LXMF_SINGLE_FRAME_MAX) {
if (payloadBytes.size() <= RSDECK_LXMF_SINGLE_FRAME_MAX) {
// Fits in single LoRa frame — send opportunistic
Serial.printf("[LXMF] sending opportunistic: %d bytes to %s\n",
(int)payloadBytes.size(), outDest.hash().toHex().substr(0, 12).c_str());
@@ -348,7 +348,7 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
} else {
// Too large for single frame — need link + resource transfer
Serial.printf("[LXMF] Message needs link delivery (%d bytes > %d single-frame), retry %d\n",
(int)payloadBytes.size(), RATDECK_LXMF_SINGLE_FRAME_MAX, msg.retries);
(int)payloadBytes.size(), RSDECK_LXMF_SINGLE_FRAME_MAX, msg.retries);
if (msg.retries % 3 == 0 && (!_outLink || _outLinkDestHash != msg.destHash
|| _outLink.status() != RNS::Type::Link::ACTIVE)) {
ensureOutboundLink(outDest, msg.destHash, "resource transfer");
@@ -563,7 +563,7 @@ void LXMFManager::handleProofTimeoutHash(const std::string& receiptHash) {
_pendingProofs.erase(it);
p.proofAttempts++;
bool retry = p.proofAttempts < 3 && (int)_instance->_outQueue.size() < RATDECK_MAX_OUTQUEUE;
bool retry = p.proofAttempts < 3 && (int)_instance->_outQueue.size() < RSDECK_MAX_OUTQUEUE;
LXMFStatus nextStatus = retry ? LXMFStatus::QUEUED : LXMFStatus::FAILED;
if (_instance->_store) {
+2 -2
View File
@@ -244,7 +244,7 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash, bool loraEnabled)
if (packet.context() == RNS::Type::Packet::PATH_RESPONSE) return true;
// Adaptive rate: tighter during first 60s boot flood, then normal
unsigned int maxRate = (now < 60000) ? 3 : RATDECK_MAX_ANNOUNCES_PER_SEC;
unsigned int maxRate = (now < 60000) ? 3 : RSDECK_MAX_ANNOUNCES_PER_SEC;
if (++count > maxRate) { ReticulumManager::_announceFilterCount++; return false; }
return true;
@@ -349,7 +349,7 @@ void ReticulumManager::loop() {
// Synchronous persist — one cycle per call to spread I/O across intervals.
// Runs on core 1 (main loop) to avoid data races with microReticulum's
// single-threaded transport state. Ratdeck is an endpoint, not a transport
// single-threaded transport state. rsDeck is an endpoint, not a transport
// node, so path tables are intentionally not persisted across boots.
void ReticulumManager::persistData() {
unsigned long start = millis();
+1 -1
View File
@@ -1,7 +1,7 @@
#include "FlashStore.h"
bool FlashStore::begin() {
// Standalone ratdeck labels this partition "littlefs"; bmorcelli/Launcher
// Legacy standalone builds label this partition "littlefs"; bmorcelli/Launcher
// labels the equivalent partition "spiffs". Try ours first, fall back.
const char* labels[] = { "littlefs", "spiffs" };
bool mounted = false;
+5 -5
View File
@@ -362,8 +362,8 @@ bool MessageStore::saveMessage(LXMFMessage& msg) {
if (isPendingStatus(msg.status) && s.pendingCount < UINT16_MAX) s.pendingCount++;
if (msg.status == LXMFStatus::FAILED && s.failedCount < UINT16_MAX) s.failedCount++;
}
int limit = sdOk ? RATDECK_MAX_MESSAGES_PER_CONV
: ((_externalStorageEnabled && _sd && _sd->isReady()) ? FLASH_MSG_CACHE_LIMIT : RATDECK_MAX_MESSAGES_PER_CONV);
int limit = sdOk ? RSDECK_MAX_MESSAGES_PER_CONV
: ((_externalStorageEnabled && _sd && _sd->isReady()) ? FLASH_MSG_CACHE_LIMIT : RSDECK_MAX_MESSAGES_PER_CONV);
if (s.totalCount > limit) {
rebuildSummary(peerHex);
}
@@ -965,7 +965,7 @@ void MessageStore::enforceFlashLimit(const std::string& peerHex) {
}
entry = d.openNextFile();
}
int limit = (_externalStorageEnabled && _sd && _sd->isReady()) ? FLASH_MSG_CACHE_LIMIT : RATDECK_MAX_MESSAGES_PER_CONV;
int limit = (_externalStorageEnabled && _sd && _sd->isReady()) ? FLASH_MSG_CACHE_LIMIT : RSDECK_MAX_MESSAGES_PER_CONV;
if ((int)files.size() <= limit) return;
std::sort(files.begin(), files.end());
int excess = files.size() - limit;
@@ -993,9 +993,9 @@ void MessageStore::enforceSDLimit(const std::string& peerHex) {
}
entry = d.openNextFile();
}
if ((int)files.size() <= RATDECK_MAX_MESSAGES_PER_CONV) return;
if ((int)files.size() <= RSDECK_MAX_MESSAGES_PER_CONV) return;
std::sort(files.begin(), files.end());
int excess = files.size() - RATDECK_MAX_MESSAGES_PER_CONV;
int excess = files.size() - RSDECK_MAX_MESSAGES_PER_CONV;
for (int i = 0; i < excess; i++) {
_sd->remove(files[i].c_str());
}
+5 -5
View File
@@ -176,9 +176,9 @@ String SDStore::readString(const char* path) {
return result;
}
bool SDStore::wipeRatdeck() {
bool SDStore::wipeRsDeck() {
if (!_ready) return false;
Serial.println("[SD] Wiping /ratdeck/ ...");
Serial.println("[SD] Wiping rsDeck legacy /ratdeck/ data ...");
wipeDir("/ratdeck/messages");
wipeDir("/ratdeck/contacts");
wipeDir("/ratdeck/identity");
@@ -186,7 +186,7 @@ bool SDStore::wipeRatdeck() {
wipeDir("/ratdeck/transport");
SD.rmdir("/ratdeck");
Serial.println("[SD] Wipe complete, recreating dirs...");
return formatForRatdeck();
return formatForRsDeck();
}
void SDStore::wipeDir(const char* path) {
@@ -221,9 +221,9 @@ bool SDStore::hasExistingData() {
return false;
}
bool SDStore::formatForRatdeck() {
bool SDStore::formatForRsDeck() {
if (!_ready) return false;
Serial.println("[SD] Creating Ratdeck directory structure...");
Serial.println("[SD] Creating rsDeck legacy /ratdeck/ directory structure...");
bool ok = true;
ok &= ensureDir("/ratdeck");
ok &= ensureDir("/ratdeck/config");
+2 -2
View File
@@ -25,8 +25,8 @@ public:
uint64_t totalBytes() const;
uint64_t usedBytes() const;
bool formatForRatdeck();
bool wipeRatdeck();
bool formatForRsDeck();
bool wipeRsDeck();
bool hasExistingData();
private:
+4 -4
View File
@@ -21,7 +21,7 @@ BLEInterface::~BLEInterface() {
bool BLEInterface::start() {
_framesMutex = xSemaphoreCreateMutex();
NimBLEDevice::init("Ratdeck");
NimBLEDevice::init("rsDeck");
NimBLEDevice::setMTU(512);
_pServer = NimBLEDevice::createServer();
@@ -29,13 +29,13 @@ bool BLEInterface::start() {
_pService = _pServer->createService(SERVICE_UUID);
// TX: Ratdeck -> remote (NOTIFY)
// TX: rsDeck -> remote (NOTIFY)
_pTxChar = _pService->createCharacteristic(
TX_CHAR_UUID,
NIMBLE_PROPERTY::NOTIFY
);
// RX: remote -> Ratdeck (WRITE)
// RX: remote -> rsDeck (WRITE)
_pRxChar = _pService->createCharacteristic(
RX_CHAR_UUID,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
@@ -47,7 +47,7 @@ bool BLEInterface::start() {
NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising();
pAdv->addServiceUUID(SERVICE_UUID);
pAdv->setName("Ratdeck");
pAdv->setName("rsDeck");
pAdv->start();
_active = true;
+1 -1
View File
@@ -79,7 +79,7 @@ private:
static constexpr uint8_t FRAME_ESC = 0x7D;
static constexpr uint8_t FRAME_XOR = 0x20;
// Custom UUIDs for Ratdeck BLE transport
// Custom UUIDs for rsDeck BLE transport
static constexpr const char* SERVICE_UUID = "e2f0a5b1-c3d4-4e56-8f90-1a2b3c4d5e6f";
static constexpr const char* TX_CHAR_UUID = "e2f0a5b2-c3d4-4e56-8f90-1a2b3c4d5e6f";
static constexpr const char* RX_CHAR_UUID = "e2f0a5b3-c3d4-4e56-8f90-1a2b3c4d5e6f";
+4 -4
View File
@@ -11,7 +11,7 @@ bool BLESideband::begin(NimBLEServer* existingServer) {
_pServer = existingServer;
_ownServer = false;
} else {
NimBLEDevice::init("Ratdeck");
NimBLEDevice::init("rsDeck");
_pServer = NimBLEDevice::createServer();
_ownServer = true;
}
@@ -19,13 +19,13 @@ bool BLESideband::begin(NimBLEServer* existingServer) {
// Create Nordic UART Service (don't set server callbacks — BLEInterface owns them)
_pService = _pServer->createService(NUS_SERVICE_UUID);
// TX: Ratdeck -> Sideband (NOTIFY)
// TX: rsDeck -> Sideband (NOTIFY)
_pTxChar = _pService->createCharacteristic(
NUS_TX_UUID,
NIMBLE_PROPERTY::NOTIFY
);
// RX: Sideband -> Ratdeck (WRITE)
// RX: Sideband -> rsDeck (WRITE)
_pRxChar = _pService->createCharacteristic(
NUS_RX_UUID,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
@@ -39,7 +39,7 @@ bool BLESideband::begin(NimBLEServer* existingServer) {
_pServer->start();
NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising();
pAdv->addServiceUUID(NUS_SERVICE_UUID);
pAdv->setName("Ratdeck");
pAdv->setName("rsDeck");
pAdv->start();
} else {
// Add NUS UUID to existing advertising
+54 -7
View File
@@ -1,6 +1,7 @@
#include "LoRaInterface.h"
#include "config/BoardConfig.h"
#include <algorithm>
#include <cmath>
// RNode on-air framing constants (from RNode_Firmware Framing.h / Config.h)
// Every LoRa packet has a 1-byte header: upper nibble = random sequence, lower nibble = flags
@@ -14,9 +15,9 @@ LoRaInterface::LoRaInterface(SX1262* radio, const char* name)
{
_IN = true;
_OUT = true;
_bitrate = 2000;
// Reticulum MTU (500 bytes) — split-packet framing allows up to 2x254 = 508 bytes
_HW_MTU = RNS::Type::Reticulum::MTU;
refreshRadioTiming(true);
}
LoRaInterface::~LoRaInterface() {
@@ -30,6 +31,7 @@ bool LoRaInterface::start() {
return false;
}
_online = true;
refreshRadioTiming(true);
_radio->receive();
Serial.println("[LORA_IF] Interface started (split-packet enabled, MTU=500)");
return true;
@@ -42,6 +44,7 @@ void LoRaInterface::stop() {
void LoRaInterface::send_outgoing(const RNS::Bytes& data) {
if (!_online || !_radio) return;
refreshRadioTiming();
// Reject packets exceeding Reticulum MTU (500 bytes)
if (data.size() > RNS::Type::Reticulum::MTU) {
@@ -73,6 +76,7 @@ void LoRaInterface::send_outgoing(const RNS::Bytes& data) {
}
void LoRaInterface::transmitNow(const RNS::Bytes& data) {
refreshRadioTiming();
uint8_t header = (uint8_t)(random(256)) & RNODE_NIBBLE_SEQ;
bool needsSplit = (data.size() > RNODE_SINGLE_MTU);
@@ -129,6 +133,7 @@ void LoRaInterface::transmitNow(const RNS::Bytes& data) {
void LoRaInterface::loop() {
if (!_online || !_radio) return;
refreshRadioTiming();
// Handle async TX completion
if (_txPending) {
@@ -170,8 +175,10 @@ void LoRaInterface::loop() {
}
// Split RX timeout: discard stale partial packets and drain deferred TX
if (_splitRxPending && (millis() - _splitRxTimestamp > SPLIT_RX_TIMEOUT_MS)) {
Serial.println("[LORA_IF] RX SPLIT timeout, discarding partial");
if (_splitRxPending && (millis() - _splitRxTimestamp > _splitRxTimeoutMs)) {
unsigned long age = millis() - _splitRxTimestamp;
Serial.printf("[LORA_IF] RX SPLIT timeout after %lums (limit=%lums frame=%.0fms), discarding partial\n",
age, _splitRxTimeoutMs, _singleFrameAirtimeMs);
_splitRxPending = false;
_splitRxBuffer = RNS::Bytes();
// Drain any TX that was deferred during split RX hold
@@ -226,14 +233,14 @@ void LoRaInterface::loop() {
_splitRxBuffer = RNS::Bytes(raw + RNODE_HEADER_L, payloadSize);
_splitRxTimestamp = millis();
Serial.printf("[LORA_IF] RX SPLIT frame 1: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr);
Serial.printf("[LORA_IF] RX SPLIT frame 1: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f, timeout=%lums\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr, _splitRxTimeoutMs);
_radio->receive();
return;
} else if (seq == _splitRxSeq) {
// Second frame matches — reassemble
Serial.printf("[LORA_IF] RX SPLIT frame 2: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr);
Serial.printf("[LORA_IF] RX SPLIT frame 2: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f, age=%lums\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr, millis() - _splitRxTimestamp);
_splitRxBuffer.append(raw + RNODE_HEADER_L, payloadSize);
int totalSize = _splitRxBuffer.size();
@@ -293,3 +300,43 @@ float LoRaInterface::airtimeUtilization() const {
float windowMs = std::min((float)elapsed, (float)AIRTIME_WINDOW_MS);
return _airtimeAccumMs / windowMs;
}
unsigned long LoRaInterface::computeSplitRxTimeoutMs(float frameAirtimeMs) const {
if (frameAirtimeMs <= 0) return SPLIT_RX_TIMEOUT_FLOOR_MS;
float raw = frameAirtimeMs * SPLIT_RX_TIMEOUT_MULT + SPLIT_RX_TIMEOUT_MARGIN_MS;
unsigned long rounded = (unsigned long)(ceil(raw / 500.0f) * 500.0f);
return std::min(SPLIT_RX_TIMEOUT_CEIL_MS,
std::max(SPLIT_RX_TIMEOUT_FLOOR_MS, rounded));
}
void LoRaInterface::refreshRadioTiming(bool forceLog) {
if (!_radio || !_radio->isRadioOnline()) {
if (_bitrate == 0) _bitrate = 1;
return;
}
unsigned long now = millis();
if (!forceLog && _lastTimingRefreshMs != 0 && (now - _lastTimingRefreshMs) < 1000) return;
_lastTimingRefreshMs = now;
uint32_t newBitrate = _radio->getBitrate();
if (newBitrate == 0) newBitrate = (_bitrate > 0) ? _bitrate : 1;
float newFrameAirtime = _radio->getAirtime(MAX_PACKET_SIZE);
unsigned long newTimeout = computeSplitRxTimeoutMs(newFrameAirtime);
bool changed = forceLog || newBitrate != _bitrate ||
newTimeout != _splitRxTimeoutMs ||
fabsf(newFrameAirtime - _singleFrameAirtimeMs) >= 1.0f;
_bitrate = newBitrate;
_singleFrameAirtimeMs = newFrameAirtime;
_splitRxTimeoutMs = newTimeout;
if (changed) {
Serial.printf("[LORA_IF] timing: bitrate=%lu bps frame=%.0fms split_timeout=%lums ldro=%s\n",
(unsigned long)_bitrate,
_singleFrameAirtimeMs,
_splitRxTimeoutMs,
_radio->lowDataRateEnabled() ? "on" : "off");
}
}
+12 -1
View File
@@ -23,12 +23,17 @@ public:
int lastRxRssi() const { return _lastRxRssi; }
float lastRxSnr() const { return _lastRxSnr; }
bool isOnline() const { return _online; }
unsigned long splitRxTimeoutMs() const { return _splitRxTimeoutMs; }
float singleFrameAirtimeMs() const { return _singleFrameAirtimeMs; }
uint32_t bitrate() const { return _bitrate; }
protected:
virtual void send_outgoing(const RNS::Bytes& data) override;
private:
void transmitNow(const RNS::Bytes& data);
void refreshRadioTiming(bool forceLog = false);
unsigned long computeSplitRxTimeoutMs(float frameAirtimeMs) const;
SX1262* _radio;
bool _txPending = false;
@@ -44,11 +49,17 @@ private:
uint8_t _splitTxHeader = 0;
// Split-packet RX state: reassemble two LoRa frames into one Reticulum packet
static constexpr unsigned long SPLIT_RX_TIMEOUT_MS = 5000;
static constexpr unsigned long SPLIT_RX_TIMEOUT_FLOOR_MS = 5000;
static constexpr unsigned long SPLIT_RX_TIMEOUT_CEIL_MS = 60000;
static constexpr unsigned long SPLIT_RX_TIMEOUT_MARGIN_MS = 2000;
static constexpr float SPLIT_RX_TIMEOUT_MULT = 1.5f;
bool _splitRxPending = false;
uint8_t _splitRxSeq = 0;
RNS::Bytes _splitRxBuffer;
unsigned long _splitRxTimestamp = 0;
unsigned long _splitRxTimeoutMs = SPLIT_RX_TIMEOUT_FLOOR_MS;
float _singleFrameAirtimeMs = 0;
unsigned long _lastTimingRefreshMs = 0;
int _lastRxRssi = 0;
float _lastRxSnr = 0;
+1 -1
View File
@@ -37,7 +37,7 @@ void WiFiInterface::startAP() {
if (_apSSID.isEmpty()) {
uint32_t chip = ESP.getEfuseMac() & 0xFFFF;
char ssid[32];
snprintf(ssid, sizeof(ssid), "ratdeck-%04x", chip);
snprintf(ssid, sizeof(ssid), "rsdeck-%04x", chip);
_apSSID = ssid;
}
+4 -4
View File
@@ -32,7 +32,7 @@ void LvStatusBar::create(lv_obj_t* parent) {
_lblTime = lv_label_create(_bar);
lv_obj_set_size(_lblTime, kTimeW, Theme::STATUS_BAR_H);
lv_label_set_long_mode(_lblTime, LV_LABEL_LONG_CLIP);
lv_obj_set_style_text_font(_lblTime, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(_lblTime, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_align(_lblTime, LV_TEXT_ALIGN_LEFT, 0);
lv_label_set_text(_lblTime, "--:--");
lv_obj_align(_lblTime, LV_ALIGN_LEFT_MID, 4, 0);
@@ -42,14 +42,14 @@ void LvStatusBar::create(lv_obj_t* parent) {
lv_obj_set_size(_lblLinks, kLinksW, Theme::STATUS_BAR_H);
lv_label_set_long_mode(_lblLinks, LV_LABEL_LONG_CLIP);
lv_label_set_recolor(_lblLinks, true);
lv_obj_set_style_text_font(_lblLinks, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_lblLinks, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_align(_lblLinks, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(_lblLinks, LV_ALIGN_LEFT_MID, kLinksX, 0);
// Right: battery percentage.
_lblBatt = lv_label_create(_bar);
lv_obj_set_size(_lblBatt, kBattW, Theme::STATUS_BAR_H);
lv_obj_set_style_text_font(_lblBatt, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_lblBatt, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_align(_lblBatt, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_long_mode(_lblBatt, LV_LABEL_LONG_CLIP);
lv_obj_align(_lblBatt, LV_ALIGN_RIGHT_MID, -4, 0);
@@ -66,7 +66,7 @@ void LvStatusBar::create(lv_obj_t* parent) {
_lblToast = lv_label_create(_toast);
lv_obj_set_width(_lblToast, Theme::SCREEN_W - 12);
lv_label_set_long_mode(_lblToast, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_font(_lblToast, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(_lblToast, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(_lblToast, lv_color_hex(Theme::BG), 0);
lv_obj_set_style_text_align(_lblToast, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_center(_lblToast);
+2 -2
View File
@@ -49,7 +49,7 @@ void LvTabBar::create(lv_obj_t* parent) {
_labels[i] = lv_label_create(_cells[i]);
lv_obj_set_size(_labels[i], Theme::TAB_W - 4, 14);
lv_label_set_long_mode(_labels[i], LV_LABEL_LONG_CLIP);
lv_obj_set_style_text_font(_labels[i], &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_labels[i], &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_align(_labels[i], LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(_labels[i], TAB_NAMES[i]);
lv_obj_align(_labels[i], LV_ALIGN_CENTER, 0, 2);
@@ -63,7 +63,7 @@ void LvTabBar::create(lv_obj_t* parent) {
_badgeLabels[i] = lv_label_create(_badges[i]);
lv_label_set_long_mode(_badgeLabels[i], LV_LABEL_LONG_CLIP);
lv_obj_set_style_text_font(_badgeLabels[i], &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_badgeLabels[i], &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_align(_badgeLabels[i], LV_TEXT_ALIGN_CENTER, 0);
lv_obj_center(_badgeLabels[i]);
}
+14 -14
View File
@@ -120,23 +120,23 @@ void init(lv_disp_t* disp) {
// Screen background (LV_USE_THEME_DEFAULT is disabled in lv_conf.h).
lv_style_init(&s_screen);
lv_style_set_bg_opa(&s_screen, LV_OPA_COVER);
lv_style_set_text_font(&s_screen, &lv_font_ratdeck_14);
lv_style_set_text_font(&s_screen, &lv_font_rsdeck_14);
// Labels
lv_style_init(&s_label);
lv_style_set_text_font(&s_label, &lv_font_ratdeck_14);
lv_style_set_text_font(&s_label, &lv_font_rsdeck_14);
lv_style_init(&s_labelMuted);
lv_style_set_text_font(&s_labelMuted, &lv_font_ratdeck_12);
lv_style_set_text_font(&s_labelMuted, &lv_font_rsdeck_12);
lv_style_init(&s_labelAccent);
lv_style_set_text_font(&s_labelAccent, &lv_font_ratdeck_14);
lv_style_set_text_font(&s_labelAccent, &lv_font_rsdeck_14);
// Buttons
lv_style_init(&s_btn);
lv_style_set_bg_opa(&s_btn, LV_OPA_COVER);
lv_style_set_border_width(&s_btn, 1);
lv_style_set_text_font(&s_btn, &lv_font_ratdeck_12);
lv_style_set_text_font(&s_btn, &lv_font_rsdeck_12);
lv_style_set_radius(&s_btn, 3);
lv_style_set_pad_top(&s_btn, 5);
lv_style_set_pad_bottom(&s_btn, 5);
@@ -171,7 +171,7 @@ void init(lv_disp_t* disp) {
lv_style_init(&s_textarea);
lv_style_set_bg_opa(&s_textarea, LV_OPA_COVER);
lv_style_set_border_width(&s_textarea, 1);
lv_style_set_text_font(&s_textarea, &lv_font_ratdeck_14);
lv_style_set_text_font(&s_textarea, &lv_font_rsdeck_14);
lv_style_set_radius(&s_textarea, 3);
lv_style_set_pad_all(&s_textarea, 6);
@@ -189,7 +189,7 @@ void init(lv_disp_t* disp) {
// List items
lv_style_init(&s_listBtn);
lv_style_set_bg_opa(&s_listBtn, LV_OPA_COVER);
lv_style_set_text_font(&s_listBtn, &lv_font_ratdeck_12);
lv_style_set_text_font(&s_listBtn, &lv_font_rsdeck_12);
lv_style_set_border_width(&s_listBtn, 1);
lv_style_set_border_side(&s_listBtn, LV_BORDER_SIDE_BOTTOM);
lv_style_set_pad_top(&s_listBtn, 5);
@@ -208,7 +208,7 @@ void init(lv_disp_t* disp) {
lv_style_init(&s_dropdown);
lv_style_set_bg_opa(&s_dropdown, LV_OPA_COVER);
lv_style_set_border_width(&s_dropdown, 1);
lv_style_set_text_font(&s_dropdown, &lv_font_ratdeck_12);
lv_style_set_text_font(&s_dropdown, &lv_font_rsdeck_12);
lv_style_set_radius(&s_dropdown, 3);
lv_style_set_pad_all(&s_dropdown, 4);
@@ -220,7 +220,7 @@ void init(lv_disp_t* disp) {
// Section header (for list section dividers like "Contacts (3)")
lv_style_init(&s_sectionHeader);
lv_style_set_bg_opa(&s_sectionHeader, LV_OPA_TRANSP);
lv_style_set_text_font(&s_sectionHeader, &lv_font_ratdeck_10);
lv_style_set_text_font(&s_sectionHeader, &lv_font_rsdeck_10);
lv_style_set_text_letter_space(&s_sectionHeader, 0);
lv_style_set_border_width(&s_sectionHeader, 1);
lv_style_set_border_side(&s_sectionHeader, LV_BORDER_SIDE_BOTTOM);
@@ -249,7 +249,7 @@ void init(lv_disp_t* disp) {
// Roller - no border so selected row highlight extends edge-to-edge
lv_style_init(&s_roller);
lv_style_set_bg_opa(&s_roller, LV_OPA_COVER);
lv_style_set_text_font(&s_roller, &lv_font_ratdeck_14);
lv_style_set_text_font(&s_roller, &lv_font_rsdeck_14);
lv_style_set_border_width(&s_roller, 0);
// Persistent shell bars.
@@ -262,7 +262,7 @@ void init(lv_disp_t* disp) {
lv_style_init(&s_statusToast);
lv_style_set_bg_opa(&s_statusToast, LV_OPA_COVER);
lv_style_set_text_font(&s_statusToast, &lv_font_ratdeck_12);
lv_style_set_text_font(&s_statusToast, &lv_font_rsdeck_12);
lv_style_set_pad_all(&s_statusToast, 0);
lv_style_set_radius(&s_statusToast, 0);
@@ -277,7 +277,7 @@ void init(lv_disp_t* disp) {
lv_style_init(&s_tabCell);
lv_style_set_bg_opa(&s_tabCell, LV_OPA_COVER);
lv_style_set_border_width(&s_tabCell, 0);
lv_style_set_text_font(&s_tabCell, &lv_font_ratdeck_10);
lv_style_set_text_font(&s_tabCell, &lv_font_rsdeck_10);
lv_style_set_pad_all(&s_tabCell, 0);
lv_style_set_radius(&s_tabCell, 0);
@@ -288,7 +288,7 @@ void init(lv_disp_t* disp) {
lv_style_init(&s_badge);
lv_style_set_bg_opa(&s_badge, LV_OPA_COVER);
lv_style_set_text_font(&s_badge, &lv_font_ratdeck_10);
lv_style_set_text_font(&s_badge, &lv_font_rsdeck_10);
lv_style_set_radius(&s_badge, 3);
lv_style_set_pad_all(&s_badge, 0);
@@ -297,7 +297,7 @@ void init(lv_disp_t* disp) {
// Apply screen style to default theme
lv_obj_add_style(lv_scr_act(), &s_screen, 0);
Serial.println("[LVGL] Ratdeck field-console theme initialized");
Serial.println("[LVGL] rsDeck field-console theme initialized");
}
void refresh() {
+1 -1
View File
@@ -2,7 +2,7 @@
#include <lvgl.h>
// Ratdeck LVGL theme: compact field-console styles for LVGL 8.3.
// rsDeck LVGL theme: compact field-console styles for LVGL 8.3.
namespace LvTheme {
void init(lv_disp_t* disp);
+1 -1
View File
@@ -1,6 +1,6 @@
#include "Theme.h"
// Dark column is the original Ratdeck palette, verbatim. Light column is
// Dark column is the original rsDeck palette, verbatim. Light column is
// derived: same hue family, brand greens/cyan darkened for light-bg contrast.
#define THEME_COLORS(X) \
X(BG, 0x05080A, 0xF2F6F4) \
+1 -1
View File
@@ -3,7 +3,7 @@
#include <cstdint>
// =============================================================================
// Ratdeck design constants
// rsDeck design constants
// Field-console palette tuned for the 320x240 LVGL surface.
// Colors are runtime-switchable (Dark = original palette, Light = derived);
// the value tables live in Theme.cpp. Identifiers keep their original
+7 -7
View File
@@ -84,13 +84,13 @@ lv_obj_t* createEmptyState(lv_obj_t* parent) {
lv_obj_clear_flag(shoulders, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title = lv_label_create(box);
lv_obj_set_style_text_font(title, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(title, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_text(title, "No trusted contacts");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 49);
lv_obj_t* hint = lv_label_create(box);
lv_obj_set_style_text_font(hint, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(hint, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(hint, "Saved peers appear here");
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 70);
@@ -194,13 +194,13 @@ void LvContactsScreen::rebuildList() {
}, LV_EVENT_FOCUSED, nullptr);
lv_obj_t* qrLbl = lv_label_create(qrRow);
lv_obj_set_style_text_font(qrLbl, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(qrLbl, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(qrLbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(qrLbl, "Share My QR");
lv_obj_align(qrLbl, LV_ALIGN_LEFT_MID, 12, 0);
lv_obj_t* hintLbl = lv_label_create(qrRow);
lv_obj_set_style_text_font(hintLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(hintLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(hintLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(hintLbl, "Enter");
lv_obj_align(hintLbl, LV_ALIGN_RIGHT_MID, -12, 0);
@@ -247,7 +247,7 @@ void LvContactsScreen::rebuildList() {
LxmFaceAvatar::render(avatar.canvas, String(hashHex.c_str()));
lv_obj_t* nameLbl = lv_label_create(row);
lv_obj_set_style_text_font(nameLbl, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(nameLbl, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(nameLbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_long_mode(nameLbl, LV_LABEL_LONG_CLIP);
lv_obj_set_width(nameLbl, Theme::CONTENT_W - kContactTextX - 72);
@@ -256,7 +256,7 @@ void LvContactsScreen::rebuildList() {
lv_obj_set_pos(nameLbl, kContactTextX, 2);
lv_obj_t* metaLbl = lv_label_create(row);
lv_obj_set_style_text_font(metaLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(metaLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(metaLbl, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_obj_set_style_text_align(metaLbl, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_long_mode(metaLbl, LV_LABEL_LONG_CLIP);
@@ -266,7 +266,7 @@ void LvContactsScreen::rebuildList() {
lv_obj_set_pos(metaLbl, Theme::CONTENT_W - 72, 5);
lv_obj_t* idLbl = lv_label_create(row);
lv_obj_set_style_text_font(idLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(idLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(idLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_long_mode(idLbl, LV_LABEL_LONG_CLIP);
lv_obj_set_width(idLbl, Theme::CONTENT_W - kContactTextX - 8);
+11 -11
View File
@@ -61,7 +61,7 @@ void LvDataCleanScreen::createUI(lv_obj_t* parent) {
_selectedYes = false;
_confirmWipe = false;
lv_obj_t* eyebrow = makeLabel(parent, "STORAGE CHECK", &lv_font_ratdeck_10,
lv_obj_t* eyebrow = makeLabel(parent, "STORAGE CHECK", &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, 220, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
lv_obj_align(eyebrow, LV_ALIGN_TOP_MID, 0, 10);
@@ -72,17 +72,17 @@ void LvDataCleanScreen::createUI(lv_obj_t* parent) {
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 28);
lv_obj_t* msg = makeLabel(parent,
"Ratdeck found older data on the SD card.",
&lv_font_ratdeck_12, Theme::TEXT_PRIMARY, 268, LV_TEXT_ALIGN_CENTER);
"rsDeck found older data on the SD card.",
&lv_font_rsdeck_12, Theme::TEXT_PRIMARY, 268, LV_TEXT_ALIGN_CENTER);
lv_obj_align(msg, LV_ALIGN_TOP_MID, 0, 58);
lv_obj_t* prompt = makeLabel(parent,
"Keeping data is safest. Erase only for a clean setup.",
&lv_font_ratdeck_12, Theme::TEXT_SECONDARY, 276, LV_TEXT_ALIGN_CENTER);
&lv_font_rsdeck_12, Theme::TEXT_SECONDARY, 276, LV_TEXT_ALIGN_CENTER);
lv_obj_align(prompt, LV_ALIGN_TOP_MID, 0, 82);
_noBox = makeChoice(parent, 28, 122);
_noLabel = makeLabel(_noBox, "KEEP DATA", &lv_font_ratdeck_12,
_noLabel = makeLabel(_noBox, "KEEP DATA", &lv_font_rsdeck_12,
Theme::TEXT_PRIMARY, 112, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
lv_obj_center(_noLabel);
@@ -96,7 +96,7 @@ void LvDataCleanScreen::createUI(lv_obj_t* parent) {
}, LV_EVENT_CLICKED, this);
_yesBox = makeChoice(parent, 166, 122);
_yesLabel = makeLabel(_yesBox, "ERASE", &lv_font_ratdeck_12,
_yesLabel = makeLabel(_yesBox, "ERASE", &lv_font_rsdeck_12,
Theme::TEXT_PRIMARY, 112, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
lv_obj_center(_yesLabel);
@@ -112,26 +112,26 @@ void LvDataCleanScreen::createUI(lv_obj_t* parent) {
if (self->_doneCb) self->_doneCb(true);
}, LV_EVENT_CLICKED, this);
_confirmLabel = makeLabel(parent, "", &lv_font_ratdeck_12,
_confirmLabel = makeLabel(parent, "", &lv_font_rsdeck_12,
Theme::WARNING_CLR, 280, LV_TEXT_ALIGN_CENTER);
lv_obj_align(_confirmLabel, LV_ALIGN_TOP_MID, 0, 164);
_hintLabel = makeLabel(parent, "Left/Right choose Enter continues",
&lv_font_ratdeck_12, Theme::ACCENT,
&lv_font_rsdeck_12, Theme::ACCENT,
286, LV_TEXT_ALIGN_CENTER, LV_LABEL_LONG_DOT);
lv_obj_align(_hintLabel, LV_ALIGN_TOP_MID, 0, 188);
_statusLabel = makeLabel(parent, "", &lv_font_ratdeck_12,
_statusLabel = makeLabel(parent, "", &lv_font_rsdeck_12,
Theme::PRIMARY, 286, LV_TEXT_ALIGN_CENTER);
lv_obj_align(_statusLabel, LV_ALIGN_TOP_MID, 0, 140);
lv_obj_add_flag(_statusLabel, LV_OBJ_FLAG_HIDDEN);
updateSelection();
lv_obj_t* ver = makeLabel(parent, "", &lv_font_ratdeck_10,
lv_obj_t* ver = makeLabel(parent, "", &lv_font_rsdeck_10,
Theme::TEXT_MUTED, 120, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
char verBuf[32];
snprintf(verBuf, sizeof(verBuf), "v%s", RATDECK_VERSION_STRING);
snprintf(verBuf, sizeof(verBuf), "v%s", RSDECK_VERSION_STRING);
lv_label_set_text(ver, verBuf);
lv_obj_align(ver, LV_ALIGN_BOTTOM_MID, 0, -10);
}
+5 -5
View File
@@ -34,8 +34,8 @@ lv_obj_t* makeRow(lv_obj_t* parent) {
void addHelpRow(lv_obj_t* parent, const char* keys, const char* action) {
lv_obj_t* row = makeRow(parent);
makeLabel(row, keys, &lv_font_ratdeck_10, Theme::ACCENT, 72, LV_TEXT_ALIGN_LEFT);
makeLabel(row, action, &lv_font_ratdeck_10, Theme::TEXT_PRIMARY, 178, LV_TEXT_ALIGN_LEFT);
makeLabel(row, keys, &lv_font_rsdeck_10, Theme::ACCENT, 72, LV_TEXT_ALIGN_LEFT);
makeLabel(row, action, &lv_font_rsdeck_10, Theme::TEXT_PRIMARY, 178, LV_TEXT_ALIGN_LEFT);
}
} // namespace
@@ -60,8 +60,8 @@ void LvHelpOverlay::create() {
lv_obj_t* header = makeRow(_overlay);
lv_obj_set_height(header, 16);
makeLabel(header, "HELP", &lv_font_ratdeck_12, Theme::ACCENT, 70, LV_TEXT_ALIGN_LEFT);
makeLabel(header, "Ratspeak.org", &lv_font_ratdeck_10,
makeLabel(header, "HELP", &lv_font_rsdeck_12, Theme::ACCENT, 70, LV_TEXT_ALIGN_LEFT);
makeLabel(header, "Ratspeak.org", &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, 180, LV_TEXT_ALIGN_RIGHT);
addHelpRow(_overlay, "Trackball", "Move selection");
@@ -75,7 +75,7 @@ void LvHelpOverlay::create() {
addHelpRow(_overlay, "Ctrl+D/I/T/R", "Diagnostics tools");
lv_obj_t* footer = makeLabel(_overlay, "Any key or tap closes",
&lv_font_ratdeck_10, Theme::TEXT_MUTED,
&lv_font_rsdeck_10, Theme::TEXT_MUTED,
260, LV_TEXT_ALIGN_CENTER);
lv_obj_set_style_pad_top(footer, 2, 0);
+12 -12
View File
@@ -60,7 +60,7 @@ lv_obj_t* makeLabel(lv_obj_t* parent, lv_coord_t x, lv_coord_t y, lv_coord_t w,
}
lv_obj_t* makeCaption(lv_obj_t* parent, const char* text) {
lv_obj_t* lbl = makeLabel(parent, 7, 4, 84, &lv_font_ratdeck_10,
lv_obj_t* lbl = makeLabel(parent, 7, 4, 84, &lv_font_rsdeck_10,
Theme::TEXT_MUTED, text);
lv_obj_set_style_text_letter_space(lbl, 1, 0);
return lbl;
@@ -71,7 +71,7 @@ lv_obj_t* makeChip(lv_obj_t* parent, lv_coord_t x, const char* title,
lv_obj_t* chip = makePanel(parent, x, kChipY, kCellW, kChipH,
Theme::BG_ELEVATED, Theme::BORDER);
makeCaption(chip, title);
*valueLabel = makeLabel(chip, 7, 18, 84, &lv_font_ratdeck_12,
*valueLabel = makeLabel(chip, 7, 18, 84, &lv_font_rsdeck_12,
Theme::TEXT_SECONDARY, "...");
return chip;
}
@@ -81,7 +81,7 @@ lv_obj_t* makeStat(lv_obj_t* parent, lv_coord_t x, const char* title,
lv_obj_t* stat = makePanel(parent, x, kStatY, kCellW, kStatH,
Theme::BG_ELEVATED, Theme::BORDER);
makeCaption(stat, title);
*valueLabel = makeLabel(stat, 7, 17, 84, &lv_font_ratdeck_14,
*valueLabel = makeLabel(stat, 7, 17, 84, &lv_font_rsdeck_14,
Theme::TEXT_PRIMARY, "0");
return stat;
}
@@ -172,23 +172,23 @@ void LvHomeScreen::createUI(lv_obj_t* parent) {
_avatarSeed = "";
_lblConsoleTitle = makeLabel(_identityPanel, 58, 6, 116,
&lv_font_ratdeck_10, Theme::ACCENT,
&lv_font_rsdeck_10, Theme::ACCENT,
"IDENTITY:");
lv_obj_set_style_text_letter_space(_lblConsoleTitle, 1, 0);
_lblStatus = makeLabel(_identityPanel, 184, 6, 114, &lv_font_ratdeck_10,
_lblStatus = makeLabel(_identityPanel, 184, 6, 114, &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, "OFFLINE",
LV_TEXT_ALIGN_RIGHT);
lv_obj_set_style_text_letter_space(_lblStatus, 1, 0);
_lblName = makeLabel(_identityPanel, 58, 21, 240, &lv_font_ratdeck_14,
_lblName = makeLabel(_identityPanel, 58, 21, 240, &lv_font_rsdeck_14,
Theme::TEXT_PRIMARY, "...");
lv_label_set_long_mode(_lblName, LV_LABEL_LONG_DOT);
_lblId = makeLabel(_identityPanel, 58, 41, 136, &lv_font_ratdeck_10,
_lblId = makeLabel(_identityPanel, 58, 41, 136, &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, "LXMF ----");
_lblIdentity = makeLabel(_identityPanel, 198, 41, 100,
&lv_font_ratdeck_10, Theme::TEXT_MUTED,
&lv_font_rsdeck_10, Theme::TEXT_MUTED,
"ID ----", LV_TEXT_ALIGN_RIGHT);
_chipLora = makeChip(parent, kPad, "LORA", &_lblLoraState);
@@ -226,10 +226,10 @@ void LvHomeScreen::createUI(lv_obj_t* parent) {
lv_obj_t* footer = makePanel(parent, kPad, kFooterY, 198, kFooterH,
Theme::BG_ELEVATED, Theme::BORDER);
_lblSummary = makeLabel(footer, 8, 6, 182, &lv_font_ratdeck_12,
_lblSummary = makeLabel(footer, 8, 6, 182, &lv_font_rsdeck_12,
Theme::TEXT_SECONDARY, "Waiting for transport");
lv_label_set_long_mode(_lblSummary, LV_LABEL_LONG_DOT);
_lblLastAnnounce = makeLabel(footer, 8, 23, 182, &lv_font_ratdeck_10,
_lblLastAnnounce = makeLabel(footer, 8, 23, 182, &lv_font_rsdeck_10,
Theme::TEXT_MUTED, "Announced: never");
lv_label_set_long_mode(_lblLastAnnounce, LV_LABEL_LONG_CLIP);
@@ -246,7 +246,7 @@ void LvHomeScreen::createUI(lv_obj_t* parent) {
lv_obj_set_style_pad_all(_btnAnnounce, 0, 0);
_lblAnnounceAction = lv_label_create(_btnAnnounce);
lv_obj_set_style_text_font(_lblAnnounceAction, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(_lblAnnounceAction, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(_lblAnnounceAction, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(_lblAnnounceAction, "ANNOUNCE");
lv_obj_center(_lblAnnounceAction);
@@ -291,7 +291,7 @@ void LvHomeScreen::refreshUI() {
String dh = _rns->destinationHashHex();
displayName = "Ratspeak.org-" + dh.substring(0, 3);
} else {
displayName = "Ratdeck";
displayName = "rsDeck";
}
lv_label_set_text(_lblName, displayName.c_str());
+1 -1
View File
@@ -7,7 +7,7 @@ void LvMapScreen::createUI(lv_obj_t* parent) {
lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* lbl = lv_label_create(parent);
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(lbl, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(lbl, "Map\n\nComing soon\n\nNode topology view\nwill appear here");
lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
+13 -13
View File
@@ -155,7 +155,7 @@ void LvMessageView::createUI(lv_obj_t* parent) {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, 0);
const lv_font_t* font = &lv_font_ratdeck_12;
const lv_font_t* font = &lv_font_rsdeck_12;
// Header bar (top)
_header = lv_obj_create(parent);
@@ -170,27 +170,27 @@ void LvMessageView::createUI(lv_obj_t* parent) {
lv_obj_clear_flag(_header, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* backLbl = lv_label_create(_header);
lv_obj_set_style_text_font(backLbl, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(backLbl, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(backLbl, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(backLbl, "<");
lv_obj_align(backLbl, LV_ALIGN_LEFT_MID, 6, 0);
_lblHeader = lv_label_create(_header);
lv_obj_set_style_text_font(_lblHeader, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(_lblHeader, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(_lblHeader, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_long_mode(_lblHeader, LV_LABEL_LONG_DOT);
lv_obj_set_width(_lblHeader, 188);
lv_obj_set_pos(_lblHeader, 22, 3);
_lblHeaderMeta = lv_label_create(_header);
lv_obj_set_style_text_font(_lblHeaderMeta, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_lblHeaderMeta, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_lblHeaderMeta, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_long_mode(_lblHeaderMeta, LV_LABEL_LONG_DOT);
lv_obj_set_width(_lblHeaderMeta, Theme::CONTENT_W - 30);
lv_obj_set_pos(_lblHeaderMeta, 22, 21);
_lblHeaderState = lv_label_create(_header);
lv_obj_set_style_text_font(_lblHeaderState, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_lblHeaderState, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_lblHeaderState, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(_lblHeaderState, "UNKNOWN");
lv_obj_align(_lblHeaderState, LV_ALIGN_TOP_RIGHT, -8, 8);
@@ -255,7 +255,7 @@ void LvMessageView::createUI(lv_obj_t* parent) {
lv_obj_add_style(_btnSend, LvTheme::styleBtn(), 0);
lv_obj_set_style_pad_all(_btnSend, 0, 0);
lv_obj_t* sendLbl = lv_label_create(_btnSend);
lv_obj_set_style_text_font(sendLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(sendLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(sendLbl, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(sendLbl, "SEND");
lv_obj_center(sendLbl);
@@ -384,7 +384,7 @@ void LvMessageView::refreshUI() {
void LvMessageView::appendMessage(const LXMFMessage& msg) {
if (!_msgScroll) return;
const lv_font_t* font = &lv_font_ratdeck_12;
const lv_font_t* font = &lv_font_rsdeck_12;
int textW = textWidthForBubble(msg.content);
if (!msg.incoming && textW < 96) textW = 96;
int boxW = textW + 16;
@@ -455,7 +455,7 @@ void LvMessageView::appendMessage(const LXMFMessage& msg) {
if (hasTime) {
lv_obj_t* timeLbl = lv_label_create(meta);
lv_obj_set_style_text_font(timeLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(timeLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(timeLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(timeLbl, timeBuf);
lv_obj_align(timeLbl, LV_ALIGN_LEFT_MID, 0, 0);
@@ -463,7 +463,7 @@ void LvMessageView::appendMessage(const LXMFMessage& msg) {
if (!msg.incoming) {
statusLbl = lv_label_create(meta);
lv_obj_set_style_text_font(statusLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(statusLbl, &lv_font_rsdeck_10, 0);
applyStatusGlyph(statusLbl, msg.status);
lv_obj_align(statusLbl, LV_ALIGN_RIGHT_MID, 0, 0);
}
@@ -519,13 +519,13 @@ void LvMessageView::rebuildMessages() {
lv_obj_align(icon, LV_ALIGN_TOP_MID, 0, 13);
lv_obj_t* title = lv_label_create(empty);
lv_obj_set_style_text_font(title, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(title, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::TEXT_PRIMARY), 0);
lv_label_set_text(title, "No messages yet");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 32);
lv_obj_t* sub = lv_label_create(empty);
lv_obj_set_style_text_font(sub, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(sub, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(sub, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_text(sub, "Thread is quiet");
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 54);
@@ -724,7 +724,7 @@ void LvMessageView::showSendModeMenu() {
lv_obj_clear_flag(_sendOverlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title = lv_label_create(_sendOverlay);
lv_obj_set_style_text_font(title, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(title, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(title, "Send mode");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0);
@@ -748,7 +748,7 @@ void LvMessageView::showSendModeMenu() {
}, LV_EVENT_CLICKED, this);
_sendLabels[i] = lv_label_create(row);
lv_obj_set_style_text_font(_sendLabels[i], &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(_sendLabels[i], &lv_font_rsdeck_12, 0);
lv_label_set_text(_sendLabels[i], labels[i]);
lv_obj_center(_sendLabels[i]);
_sendRows[i] = row;
+8 -8
View File
@@ -121,13 +121,13 @@ void LvMessagesScreen::createUI(lv_obj_t* parent) {
lv_obj_align(emptyIcon, LV_ALIGN_TOP_MID, 0, 15);
lv_obj_t* emptyTitle = lv_label_create(_lblEmpty);
lv_obj_set_style_text_font(emptyTitle, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(emptyTitle, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(emptyTitle, lv_color_hex(Theme::TEXT_PRIMARY), 0);
lv_label_set_text(emptyTitle, "No conversations");
lv_obj_align(emptyTitle, LV_ALIGN_TOP_MID, 0, 34);
lv_obj_t* emptySub = lv_label_create(_lblEmpty);
lv_obj_set_style_text_font(emptySub, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(emptySub, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(emptySub, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_text(emptySub, "LXMF inbox is clear");
lv_obj_align(emptySub, LV_ALIGN_TOP_MID, 0, 56);
@@ -241,8 +241,8 @@ void LvMessagesScreen::rebuildList() {
for (auto& ci : _sortedConvs) _sortedPeers.push_back(ci.peerHex);
// Build list items with focus group support
const lv_font_t* nameFont = &lv_font_ratdeck_14;
const lv_font_t* smallFont = &lv_font_ratdeck_12;
const lv_font_t* nameFont = &lv_font_rsdeck_14;
const lv_font_t* smallFont = &lv_font_rsdeck_12;
for (int i = 0; i < count; i++) {
const auto& ci = _sortedConvs[i];
@@ -319,7 +319,7 @@ void LvMessagesScreen::rebuildList() {
char timeBuf[8];
if (formatClock(ci.lastTs, timeBuf, sizeof(timeBuf))) {
lv_obj_t* timeLbl = lv_label_create(row);
lv_obj_set_style_text_font(timeLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(timeLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(timeLbl, lv_color_hex(ci.hasUnread ? Theme::PRIMARY : Theme::TEXT_MUTED), 0);
lv_label_set_text(timeLbl, timeBuf);
lv_obj_align(timeLbl, LV_ALIGN_TOP_RIGHT, -8, 7);
@@ -370,7 +370,7 @@ void LvMessagesScreen::rebuildList() {
lv_obj_clear_flag(chip, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* chipLbl = lv_label_create(chip);
lv_obj_set_style_text_font(chipLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(chipLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(chipLbl, lv_color_hex(chipColor), 0);
lv_label_set_text(chipLbl, chipBuf);
lv_obj_center(chipLbl);
@@ -430,7 +430,7 @@ void LvMessagesScreen::rebuildActionOverlay(const char* title, const char* const
lv_obj_clear_flag(_actionOverlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* titleLbl = lv_label_create(_actionOverlay);
lv_obj_set_style_text_font(titleLbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(titleLbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(titleLbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_long_mode(titleLbl, LV_LABEL_LONG_DOT);
lv_obj_set_width(titleLbl, 220);
@@ -458,7 +458,7 @@ void LvMessagesScreen::rebuildActionOverlay(const char* title, const char* const
}, LV_EVENT_CLICKED, this);
lv_obj_t* lbl = lv_label_create(row);
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(lbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::TEXT_PRIMARY), 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT);
lv_obj_set_width(lbl, 200);
+8 -8
View File
@@ -46,7 +46,7 @@ lv_obj_t* makeActionButton(lv_obj_t* parent, const char* text, lv_coord_t y) {
lv_obj_set_style_pad_all(btn, 0, 0);
lv_obj_t* lbl = lv_label_create(btn);
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(lbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT);
lv_obj_set_width(lbl, 150);
@@ -66,7 +66,7 @@ void LvNameInputScreen::createUI(lv_obj_t* parent) {
lv_obj_set_style_bg_color(parent, lv_color_hex(Theme::BG), 0);
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
lv_obj_t* eyebrow = makeLabel(parent, "FIRST BOOT", &lv_font_ratdeck_10,
lv_obj_t* eyebrow = makeLabel(parent, "FIRST BOOT", &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, 220, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
lv_obj_align(eyebrow, LV_ALIGN_TOP_MID, 0, 13);
@@ -79,7 +79,7 @@ void LvNameInputScreen::createUI(lv_obj_t* parent) {
lv_obj_t* prompt = makeLabel(parent,
"Choose the name other operators see on announces.",
&lv_font_ratdeck_12, Theme::TEXT_PRIMARY, 250, LV_TEXT_ALIGN_CENTER);
&lv_font_rsdeck_12, Theme::TEXT_PRIMARY, 250, LV_TEXT_ALIGN_CENTER);
lv_obj_align(prompt, LV_ALIGN_TOP_MID, 0, 70);
_textarea = lv_textarea_create(parent);
@@ -90,13 +90,13 @@ void LvNameInputScreen::createUI(lv_obj_t* parent) {
lv_textarea_set_placeholder_text(_textarea, "Optional");
lv_obj_add_style(_textarea, LvTheme::styleTextarea(), 0);
lv_obj_add_style(_textarea, LvTheme::styleTextareaFocused(), LV_STATE_FOCUSED);
lv_obj_set_style_text_font(_textarea, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(_textarea, &lv_font_rsdeck_14, 0);
lv_group_add_obj(LvInput::group(), _textarea);
lv_group_focus_obj(_textarea);
lv_obj_t* note = makeLabel(parent,
"Leave blank to generate a Ratdeck name from this identity.",
&lv_font_ratdeck_12, Theme::TEXT_SECONDARY, 260, LV_TEXT_ALIGN_CENTER);
"Leave blank to generate a rsDeck name from this identity.",
&lv_font_rsdeck_12, Theme::TEXT_SECONDARY, 260, LV_TEXT_ALIGN_CENTER);
lv_obj_align(note, LV_ALIGN_TOP_MID, 0, 154);
_doneButton = makeActionButton(parent, "DONE", 184);
@@ -106,11 +106,11 @@ void LvNameInputScreen::createUI(lv_obj_t* parent) {
self->submit(false);
}, LV_EVENT_CLICKED, this);
lv_obj_t* ver = makeLabel(parent, "", &lv_font_ratdeck_10,
lv_obj_t* ver = makeLabel(parent, "", &lv_font_rsdeck_10,
Theme::TEXT_MUTED, 120, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
char verBuf[32];
snprintf(verBuf, sizeof(verBuf), "v%s", RATDECK_VERSION_STRING);
snprintf(verBuf, sizeof(verBuf), "v%s", RSDECK_VERSION_STRING);
lv_label_set_text(ver, verBuf);
lv_obj_align(ver, LV_ALIGN_BOTTOM_MID, 0, -10);
}
+13 -13
View File
@@ -80,13 +80,13 @@ lv_obj_t* createEmptyState(lv_obj_t* parent) {
}
lv_obj_t* title = lv_label_create(box);
lv_obj_set_style_text_font(title, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(title, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_text(title, "No peers heard");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 34);
lv_obj_t* hint = lv_label_create(box);
lv_obj_set_style_text_font(hint, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(hint, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(hint, "Listening for announces");
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 55);
@@ -122,21 +122,21 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
lv_obj_add_flag(_overlay, LV_OBJ_FLAG_HIDDEN);
_overlayTitle = lv_label_create(_overlay);
lv_obj_set_style_text_font(_overlayTitle, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(_overlayTitle, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(_overlayTitle, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_long_mode(_overlayTitle, LV_LABEL_LONG_CLIP);
lv_obj_set_width(_overlayTitle, 236);
lv_obj_set_pos(_overlayTitle, 12, 9);
_overlayMeta = lv_label_create(_overlay);
lv_obj_set_style_text_font(_overlayMeta, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_overlayMeta, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_overlayMeta, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_long_mode(_overlayMeta, LV_LABEL_LONG_CLIP);
lv_obj_set_width(_overlayMeta, 236);
lv_obj_set_pos(_overlayMeta, 12, 29);
_overlayReach = lv_label_create(_overlay);
lv_obj_set_style_text_font(_overlayReach, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_overlayReach, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_overlayReach, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_long_mode(_overlayReach, LV_LABEL_LONG_CLIP);
lv_obj_set_width(_overlayReach, 236);
@@ -166,7 +166,7 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
}, LV_EVENT_CLICKED, this);
_menuLabels[i] = lv_label_create(btn);
lv_obj_set_style_text_font(_menuLabels[i], &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(_menuLabels[i], &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(_menuLabels[i], lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_text(_menuLabels[i], menuText[i]);
lv_obj_center(_menuLabels[i]);
@@ -185,13 +185,13 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
lv_obj_add_flag(_nicknameBox, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* nickTitle = lv_label_create(_nicknameBox);
lv_obj_set_style_text_font(nickTitle, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(nickTitle, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(nickTitle, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_label_set_text(nickTitle, "Contact name");
lv_obj_set_pos(nickTitle, 0, 0);
_nicknameLbl = lv_label_create(_nicknameBox);
lv_obj_set_style_text_font(_nicknameLbl, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(_nicknameLbl, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(_nicknameLbl, lv_color_hex(Theme::PRIMARY), 0);
lv_label_set_long_mode(_nicknameLbl, LV_LABEL_LONG_CLIP);
lv_obj_set_width(_nicknameLbl, 232);
@@ -199,7 +199,7 @@ void LvNodesScreen::createUI(lv_obj_t* parent) {
lv_obj_set_pos(_nicknameLbl, 0, 22);
_nicknameHint = lv_label_create(_nicknameBox);
lv_obj_set_style_text_font(_nicknameHint, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_nicknameHint, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_nicknameHint, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(_nicknameHint, "Enter saves / Esc keeps");
lv_obj_set_pos(_nicknameHint, 0, 52);
@@ -291,7 +291,7 @@ void LvNodesScreen::rebuildList() {
lv_obj_set_style_pad_all(hdr, 0, 0);
lv_obj_clear_flag(hdr, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE);
lv_obj_t* lbl = lv_label_create(hdr);
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(lbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(lbl, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(lbl, text);
lv_obj_set_pos(lbl, 8, 5);
@@ -327,7 +327,7 @@ void LvNodesScreen::rebuildList() {
}, LV_EVENT_FOCUSED, nullptr);
lv_obj_t* nameLbl = lv_label_create(row);
lv_obj_set_style_text_font(nameLbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(nameLbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(nameLbl, lv_color_hex(
node.saved ? Theme::ACCENT : Theme::PRIMARY), 0);
lv_label_set_long_mode(nameLbl, LV_LABEL_LONG_CLIP);
@@ -337,7 +337,7 @@ void LvNodesScreen::rebuildList() {
lv_obj_set_pos(nameLbl, 8, 3);
lv_obj_t* infoLbl = lv_label_create(row);
lv_obj_set_style_text_font(infoLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(infoLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(infoLbl, lv_color_hex(Theme::TEXT_SECONDARY), 0);
lv_obj_set_style_text_align(infoLbl, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_long_mode(infoLbl, LV_LABEL_LONG_CLIP);
@@ -347,7 +347,7 @@ void LvNodesScreen::rebuildList() {
lv_obj_set_pos(infoLbl, Theme::CONTENT_W - 124, 5);
lv_obj_t* idLbl = lv_label_create(row);
lv_obj_set_style_text_font(idLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(idLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(idLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_long_mode(idLbl, LV_LABEL_LONG_CLIP);
lv_obj_set_width(idLbl, Theme::CONTENT_W - 16);
+3 -3
View File
@@ -20,7 +20,7 @@ void LvQrOverlay::create() {
lv_obj_clear_flag(_overlay, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* title = lv_label_create(_overlay);
lv_obj_set_style_text_font(title, &lv_font_ratdeck_14, 0);
lv_obj_set_style_text_font(title, &lv_font_rsdeck_14, 0);
lv_obj_set_style_text_color(title, lv_color_hex(Theme::ACCENT), 0);
lv_label_set_text(title, "Scan to add me");
@@ -31,7 +31,7 @@ void LvQrOverlay::create() {
lv_color_hex(0xFFFFFF));
_lblAddr = lv_label_create(_overlay);
lv_obj_set_style_text_font(_lblAddr, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(_lblAddr, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(_lblAddr, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_long_mode(_lblAddr, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_lblAddr, Theme::SCREEN_W - 12);
@@ -39,7 +39,7 @@ void LvQrOverlay::create() {
lv_label_set_text(_lblAddr, "");
lv_obj_t* hint = lv_label_create(_overlay);
lv_obj_set_style_text_font(hint, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(hint, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(hint, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(hint, "Any key to close");
+23 -23
View File
@@ -203,20 +203,20 @@ bool LvSettingsScreen::categoryNeedsReboot(int catIdx) const {
bool LvSettingsScreen::confirmableAction(const SettingItem& item) const {
return labelEq(item.label, "Developer Radio Controls")
|| labelEq(item.label, "Format SD Card")
|| labelEq(item.label, "Erase Ratdeck SD Data")
|| labelEq(item.label, "Erase rsDeck SD Data")
|| labelEq(item.label, "Erase Device");
}
bool LvSettingsScreen::armedAction(const SettingItem& item) const {
return (_confirmingInitSD && labelEq(item.label, "Format SD Card")) ||
(_confirmingWipeSD && labelEq(item.label, "Erase Ratdeck SD Data")) ||
(_confirmingWipeSD && labelEq(item.label, "Erase rsDeck SD Data")) ||
(_confirmingReset && labelEq(item.label, "Erase Device")) ||
(_confirmingDevMode && labelEq(item.label, "Developer Radio Controls"));
}
bool LvSettingsScreen::destructiveAction(const SettingItem& item) const {
return labelEq(item.label, "Format SD Card")
|| labelEq(item.label, "Erase Ratdeck SD Data")
|| labelEq(item.label, "Erase rsDeck SD Data")
|| labelEq(item.label, "Erase Device");
}
@@ -255,7 +255,7 @@ void LvSettingsScreen::runFormatSD() {
return;
}
if (_ui) _ui->lvStatusBar().showToast("Formatting SD card...", 2000);
bool ok = _sd->formatForRatdeck();
bool ok = _sd->formatForRsDeck();
if (_ui) _ui->lvStatusBar().showToast(ok ? "SD card formatted" : "SD format failed", 1500);
rebuildItemList();
}
@@ -267,8 +267,8 @@ void LvSettingsScreen::runWipeSD() {
rebuildItemList();
return;
}
if (_ui) _ui->lvStatusBar().showToast("Erasing Ratdeck SD data...", 2000);
bool ok = _sd->wipeRatdeck();
if (_ui) _ui->lvStatusBar().showToast("Erasing rsDeck SD data...", 2000);
bool ok = _sd->wipeRsDeck();
if (_ui) _ui->lvStatusBar().showToast(ok ? "SD data erased" : "SD erase failed", 1500);
rebuildItemList();
}
@@ -276,7 +276,7 @@ void LvSettingsScreen::runWipeSD() {
void LvSettingsScreen::runFactoryReset() {
_confirmingReset = false;
if (_ui) _ui->lvStatusBar().showToast("Erasing device...", 3000);
if (_sd && _sd->isReady()) _sd->wipeRatdeck();
if (_sd && _sd->isReady()) _sd->wipeRsDeck();
if (_flash) _flash->format();
nvs_flash_erase();
delay(1500); // Long enough for key state to clear before reboot
@@ -336,7 +336,7 @@ void LvSettingsScreen::firmwareCheckTask(void* arg) {
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(5000);
if (http.begin("https://api.github.com/repos/ratspeak/ratdeck/releases/latest")) {
if (http.begin("https://api.github.com/repos/ratspeak/rsDeck/releases/latest")) {
http.addHeader("Accept", "application/vnd.github.v3+json");
int httpCode = http.GET();
if (httpCode == 200) {
@@ -355,7 +355,7 @@ void LvSettingsScreen::firmwareCheckTask(void* arg) {
delay(10);
}
if (extractReleaseTag(payload, version, sizeof(version))) {
result = strcmp(version, RATDECK_VERSION_STRING) > 0
result = strcmp(version, RSDECK_VERSION_STRING) > 0
? FirmwareCheckState::AVAILABLE
: FirmwareCheckState::CURRENT;
}
@@ -379,7 +379,7 @@ void LvSettingsScreen::buildItems() {
// Identity & Device
int devStart = idx;
_items.push_back({"Firmware", SettingType::READONLY, nullptr, nullptr,
[](int) { return String(RATDECK_VERSION_STRING); }});
[](int) { return String(RSDECK_VERSION_STRING); }});
idx++;
_items.push_back({"LXMF Address", SettingType::READONLY, nullptr, nullptr,
[this](int) { return _destinationHash.length() > 0 ? _destinationHash : String("unknown"); }});
@@ -1005,7 +1005,7 @@ void LvSettingsScreen::buildItems() {
}
{
SettingItem wipeSD;
wipeSD.label = "Erase Ratdeck SD Data";
wipeSD.label = "Erase rsDeck SD Data";
wipeSD.type = SettingType::ACTION;
wipeSD.formatter = [this](int) {
if (!_sd || !_sd->isReady()) return String("No Card");
@@ -1156,7 +1156,7 @@ void LvSettingsScreen::rebuildCategoryList() {
_rowObjs.clear();
lv_obj_clean(_scrollContainer);
const lv_font_t* font = &lv_font_ratdeck_12;
const lv_font_t* font = &lv_font_rsdeck_12;
// Title
lv_obj_t* titleRow = lv_obj_create(_scrollContainer);
@@ -1177,7 +1177,7 @@ void LvSettingsScreen::rebuildCategoryList() {
if (_rebootNeeded) {
lv_obj_t* pendingLbl = lv_label_create(titleRow);
lv_obj_set_style_text_font(pendingLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(pendingLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(pendingLbl, lv_color_hex(Theme::WARNING_CLR), 0);
lv_label_set_text(pendingLbl, "REBOOT PENDING");
lv_obj_align(pendingLbl, LV_ALIGN_RIGHT_MID, -8, 0);
@@ -1222,7 +1222,7 @@ void LvSettingsScreen::rebuildCategoryList() {
char countBuf[8];
snprintf(countBuf, sizeof(countBuf), "%d", cat.count);
lv_obj_t* countLbl = lv_label_create(row);
lv_obj_set_style_text_font(countLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(countLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(countLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(countLbl, countBuf);
lv_obj_align(countLbl, LV_ALIGN_TOP_RIGHT, -24, 5);
@@ -1230,7 +1230,7 @@ void LvSettingsScreen::rebuildCategoryList() {
// Summary
if (cat.summary) {
lv_obj_t* sumLbl = lv_label_create(row);
lv_obj_set_style_text_font(sumLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(sumLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(sumLbl, lv_color_hex(pending ? Theme::WARNING_CLR : Theme::TEXT_MUTED), 0);
clipLabel(sumLbl, Theme::CONTENT_W - 44);
lv_label_set_text(sumLbl, cat.summary().c_str());
@@ -1259,7 +1259,7 @@ void LvSettingsScreen::rebuildItemList() {
_editValueLbl = nullptr; // Invalidate cached label before destroying widgets
lv_obj_clean(_scrollContainer);
const lv_font_t* font = &lv_font_ratdeck_12;
const lv_font_t* font = &lv_font_rsdeck_12;
// Category header
lv_obj_t* headerRow = lv_obj_create(_scrollContainer);
@@ -1289,7 +1289,7 @@ void LvSettingsScreen::rebuildItemList() {
if (_rebootNeeded) {
lv_obj_t* pendingLbl = lv_label_create(headerRow);
lv_obj_set_style_text_font(pendingLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(pendingLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(pendingLbl, lv_color_hex(Theme::WARNING_CLR), 0);
lv_label_set_text(pendingLbl, "REBOOT NEEDED");
lv_obj_align(pendingLbl, LV_ALIGN_RIGHT_MID, -8, 0);
@@ -1308,7 +1308,7 @@ void LvSettingsScreen::rebuildItemList() {
lv_obj_clear_flag(notice, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* noticeLbl = lv_label_create(notice);
lv_obj_set_style_text_font(noticeLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(noticeLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(noticeLbl, lv_color_hex(Theme::WARNING_CLR), 0);
clipLabel(noticeLbl, Theme::CONTENT_W - 16);
lv_label_set_text(noticeLbl, "Saved interface config is pending reboot");
@@ -1331,14 +1331,14 @@ void LvSettingsScreen::rebuildItemList() {
lv_obj_clear_flag(confirm, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* confirmTitleLbl = lv_label_create(confirm);
lv_obj_set_style_text_font(confirmTitleLbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(confirmTitleLbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(confirmTitleLbl, lv_color_hex(confirmColor), 0);
clipLabel(confirmTitleLbl, Theme::CONTENT_W - 16);
lv_label_set_text(confirmTitleLbl, confirmTitle);
lv_obj_align(confirmTitleLbl, LV_ALIGN_TOP_LEFT, 8, 3);
lv_obj_t* confirmDetailLbl = lv_label_create(confirm);
lv_obj_set_style_text_font(confirmDetailLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(confirmDetailLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(confirmDetailLbl, lv_color_hex(Theme::TEXT_PRIMARY), 0);
clipLabel(confirmDetailLbl, Theme::CONTENT_W - 16);
lv_label_set_text(confirmDetailLbl, confirmDetail);
@@ -1515,7 +1515,7 @@ void LvSettingsScreen::rebuildWifiList() {
_rowObjs.clear();
lv_obj_clean(_scrollContainer);
const lv_font_t* font = &lv_font_ratdeck_12;
const lv_font_t* font = &lv_font_rsdeck_12;
// Header
lv_obj_t* headerRow = lv_obj_create(_scrollContainer);
@@ -1591,7 +1591,7 @@ void LvSettingsScreen::rebuildWifiList() {
char sigBuf[12];
snprintf(sigBuf, sizeof(sigBuf), "%ddBm", net.rssi);
lv_obj_t* sigLbl = lv_label_create(row);
lv_obj_set_style_text_font(sigLbl, &lv_font_ratdeck_10, 0);
lv_obj_set_style_text_font(sigLbl, &lv_font_rsdeck_10, 0);
lv_obj_set_style_text_color(sigLbl, lv_color_hex(Theme::TEXT_MUTED), 0);
lv_label_set_text(sigLbl, sigBuf);
lv_obj_align(sigLbl, LV_ALIGN_RIGHT_MID, -4, 0);
@@ -1919,7 +1919,7 @@ bool LvSettingsScreen::handleLongPress() {
runFormatSD();
return true;
}
if (_confirmingWipeSD && labelEq(item.label, "Erase Ratdeck SD Data")) {
if (_confirmingWipeSD && labelEq(item.label, "Erase rsDeck SD Data")) {
runWipeSD();
return true;
}
+5 -5
View File
@@ -54,7 +54,7 @@ lv_obj_t* makeActionButton(lv_obj_t* parent, const char* text, lv_coord_t x,
lv_obj_set_style_pad_all(btn, 0, 0);
lv_obj_t* lbl = lv_label_create(btn);
lv_obj_set_style_text_font(lbl, &lv_font_ratdeck_12, 0);
lv_obj_set_style_text_font(lbl, &lv_font_rsdeck_12, 0);
lv_obj_set_style_text_color(lbl,
lv_color_hex(primary ? Theme::ACCENT : Theme::TEXT_PRIMARY), 0);
lv_label_set_long_mode(lbl, LV_LABEL_LONG_DOT);
@@ -78,7 +78,7 @@ void LvTimezoneScreen::createUI(lv_obj_t* parent) {
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
_selectedIdx = clampTimezoneIndex(_selectedIdx);
lv_obj_t* eyebrow = makeLabel(parent, "REGION SETUP", &lv_font_ratdeck_10,
lv_obj_t* eyebrow = makeLabel(parent, "REGION SETUP", &lv_font_rsdeck_10,
Theme::TEXT_SECONDARY, 220, LV_TEXT_ALIGN_CENTER,
LV_LABEL_LONG_DOT);
lv_obj_align(eyebrow, LV_ALIGN_TOP_MID, 0, 8);
@@ -89,7 +89,7 @@ void LvTimezoneScreen::createUI(lv_obj_t* parent) {
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 24);
lv_obj_t* sub = makeLabel(parent, "Pick the nearest city for time and radio-region hints.",
&lv_font_ratdeck_12, Theme::TEXT_PRIMARY,
&lv_font_rsdeck_12, Theme::TEXT_PRIMARY,
280, LV_TEXT_ALIGN_CENTER);
lv_obj_align(sub, LV_ALIGN_TOP_MID, 0, 48);
@@ -115,12 +115,12 @@ void LvTimezoneScreen::createUI(lv_obj_t* parent) {
lv_roller_set_selected(_roller, _selectedIdx, LV_ANIM_OFF);
lv_obj_add_style(_roller, LvTheme::styleRoller(), LV_PART_MAIN);
lv_obj_set_style_text_font(_roller, &lv_font_ratdeck_14, LV_PART_MAIN);
lv_obj_set_style_text_font(_roller, &lv_font_rsdeck_14, LV_PART_MAIN);
lv_obj_set_style_text_align(_roller, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_bg_color(_roller, lv_color_hex(Theme::BG_HOVER), LV_PART_SELECTED);
lv_obj_set_style_bg_opa(_roller, LV_OPA_COVER, LV_PART_SELECTED);
lv_obj_set_style_text_color(_roller, lv_color_hex(Theme::ACCENT), LV_PART_SELECTED);
lv_obj_set_style_text_font(_roller, &lv_font_ratdeck_14, LV_PART_SELECTED);
lv_obj_set_style_text_font(_roller, &lv_font_rsdeck_14, LV_PART_SELECTED);
lv_obj_set_style_text_align(_roller, LV_TEXT_ALIGN_CENTER, LV_PART_SELECTED);
// Add to focus group for trackball navigation
+4 -4
View File
@@ -58,10 +58,10 @@ char bt_da[BT_DEV_ADDR_LEN];
char bt_dh[BT_DEV_HASH_LEN];
char bt_devname[11];
#if BOARD_MODEL == BOARD_CARDPUTER_ADV
#if BOARD_MODEL == BOARD_CARDPUTER_ADV || BOARD_MODEL == BOARD_TDECK
extern void stopRadio();
void cardputer_adv_ble_host_detached() {
void ble_host_detached_cleanup() {
if (radio_online) stopRadio();
cable_state = CABLE_STATE_DISCONNECTED;
current_rssi = -292;
@@ -363,8 +363,8 @@ char bt_devname[11];
display_unblank();
ble_authenticated = false;
bt_state = BT_STATE_ON;
#if BOARD_MODEL == BOARD_CARDPUTER_ADV
cardputer_adv_ble_host_detached();
#if BOARD_MODEL == BOARD_CARDPUTER_ADV || BOARD_MODEL == BOARD_TDECK
ble_host_detached_cleanup();
#endif
}
+51 -7
View File
@@ -198,7 +198,10 @@ float epd_update_fps = 0.5;
#define CARDPUTER_ADV_DISPLAY_INTENSITY_DEFAULT 96
#elif BOARD_MODEL == BOARD_TDECK
#define DISPLAY_BLANKING_TIMEOUT 60*1000
#define TDECK_DISPLAY_INTENSITY_DEFAULT 12
#define TDECK_DISPLAY_INTENSITY_DEFAULT 80
#define TDECK_DISPLAY_INTENSITY_MIN 5
#define TDECK_DISPLAY_INTENSITY_STEP 5
#define TDECK_DISPLAY_INTENSITY_TAG 0x80
#else
#define DISPLAY_BLANKING_TIMEOUT 15*1000
#endif
@@ -324,29 +327,72 @@ uint8_t display_contrast = 0x00;
else { analogWrite(pin_backlight, value); }
}
#elif BOARD_MODEL == BOARD_TDECK
uint8_t tdeck_display_clamp_percent(uint8_t value) {
if (value < TDECK_DISPLAY_INTENSITY_MIN) return TDECK_DISPLAY_INTENSITY_MIN;
if (value > 100) return 100;
uint8_t snapped = ((value + (TDECK_DISPLAY_INTENSITY_STEP / 2)) / TDECK_DISPLAY_INTENSITY_STEP) * TDECK_DISPLAY_INTENSITY_STEP;
if (snapped < TDECK_DISPLAY_INTENSITY_MIN) return TDECK_DISPLAY_INTENSITY_MIN;
if (snapped > 100) return 100;
return snapped;
}
uint8_t tdeck_display_step_to_percent(uint8_t step) {
if (step == 0) return 0;
if (step > 15) step = 15;
uint8_t pct = (((uint16_t)step * 100) / 15 / TDECK_DISPLAY_INTENSITY_STEP) * TDECK_DISPLAY_INTENSITY_STEP;
if (pct < TDECK_DISPLAY_INTENSITY_MIN) return TDECK_DISPLAY_INTENSITY_MIN;
if (pct > 100) return 100;
return pct;
}
uint8_t tdeck_display_decode_intensity(uint8_t stored) {
if (stored == 0 || stored == 0xFF) return TDECK_DISPLAY_INTENSITY_DEFAULT;
if (stored >= TDECK_DISPLAY_INTENSITY_TAG) {
uint8_t index = stored - TDECK_DISPLAY_INTENSITY_TAG;
uint16_t pct = (uint16_t)(index + 1) * TDECK_DISPLAY_INTENSITY_STEP;
if (pct > 100) pct = 100;
return tdeck_display_clamp_percent((uint8_t)pct);
}
if (stored <= 15) return tdeck_display_step_to_percent(stored);
return tdeck_display_clamp_percent(stored);
}
uint8_t tdeck_display_encode_intensity(uint8_t pct) {
pct = tdeck_display_clamp_percent(pct);
return TDECK_DISPLAY_INTENSITY_TAG + (pct / TDECK_DISPLAY_INTENSITY_STEP) - 1;
}
uint8_t tdeck_display_percent_to_step(uint8_t pct) {
pct = tdeck_display_clamp_percent(pct);
uint8_t step = (uint8_t)(((uint16_t)pct * 15 + 50) / 100);
if (step < 1) step = 1;
if (step > 15) step = 15;
return step;
}
void set_contrast(Adafruit_ST7789 *display, uint8_t value) {
static uint8_t level = 0;
static uint8_t steps = 16;
if (value > 15) value = 15;
if (value == 0) {
digitalWrite(DISPLAY_BL_PIN, 0);
delay(3);
level = 0;
return;
}
uint8_t target = tdeck_display_percent_to_step(value);
if (level == 0) {
digitalWrite(DISPLAY_BL_PIN, 1);
level = steps;
delayMicroseconds(30);
}
int from = steps - level;
int to = steps - value;
int to = steps - target;
int num = (steps + to - from) % steps;
for (int i = 0; i < num; i++) {
digitalWrite(DISPLAY_BL_PIN, 0);
digitalWrite(DISPLAY_BL_PIN, 1);
}
level = value;
level = target;
}
#elif BOARD_MODEL == BOARD_CARDPUTER_ADV
void set_contrast(M5Canvas *display, uint8_t value) {
@@ -608,9 +654,7 @@ bool display_init() {
display_intensity = CARDPUTER_ADV_DISPLAY_INTENSITY_DEFAULT;
}
#elif BOARD_MODEL == BOARD_TDECK
if (display_intensity == 0xFF) {
display_intensity = TDECK_DISPLAY_INTENSITY_DEFAULT;
}
display_intensity = tdeck_display_decode_intensity(display_intensity);
#endif
display_unblank_intensity = display_intensity;
+5 -1
View File
@@ -40,7 +40,11 @@ TDECK_SKETCH = build/tdeck_sketch/RNode_Firmware
TDECK_BUILD = build/tdeck_build
TDECK_OUTPUT = build/tdeck.esp32.esp32s3
TDECK_BIN = $(TDECK_OUTPUT)/RNode_Firmware.ino.bin
TDECK_EXTRA_FLAGS = \"-DBOARD_MODEL=0x3B\"
RSDECK_CONFIG_H ?= ../../src/config/Config.h
RSDECK_VERSION_MAJOR ?= $(shell awk '/\#define RSDECK_VERSION_MAJOR/ {print $$3; exit}' $(RSDECK_CONFIG_H))
RSDECK_VERSION_MINOR ?= $(shell awk '/\#define RSDECK_VERSION_MINOR/ {print $$3; exit}' $(RSDECK_CONFIG_H))
RSDECK_VERSION_PATCH ?= $(shell awk '/\#define RSDECK_VERSION_PATCH/ {print $$3; exit}' $(RSDECK_CONFIG_H))
TDECK_EXTRA_FLAGS = \"-DBOARD_MODEL=0x3B\" \"-DRSDECK_VERSION_MAJOR=$(RSDECK_VERSION_MAJOR)\" \"-DRSDECK_VERSION_MINOR=$(RSDECK_VERSION_MINOR)\" \"-DRSDECK_VERSION_PATCH=$(RSDECK_VERSION_PATCH)\"
ARDUINO_DATA_DIR ?= $(shell if [ -d "$(HOME)/Library/Arduino15/packages" ]; then printf "$(HOME)/Library/Arduino15"; else printf "$(HOME)/.arduino15"; fi)
ESP32_CORE_DIR = $(ARDUINO_DATA_DIR)/packages/esp32/hardware/esp32/$(ARDUINO_ESP_CORE_VER)
+6
View File
@@ -622,8 +622,14 @@ bool startRadio() {
}
void stopRadio() {
if (!radio_online) return;
#if BOARD_MODEL == BOARD_TDECK
radio_online = false;
LoRa->end();
#else
LoRa->end();
radio_online = false;
#endif
}
void handleModemTimeout() {
+378 -21
View File
@@ -10,7 +10,9 @@
void display_unblank();
void bt_enable_pairing();
void bt_disable_pairing();
void bt_debond_all();
int bt_bond_count();
void di_conf_save(uint8_t dint);
extern uint32_t bt_pairing_started;
#define TD_PAIRING_WINDOW_MS 30000
@@ -36,8 +38,61 @@ extern uint32_t bt_pairing_started;
#define TD_WF_X 212
#define TD_WF_W 107
#define TD_WF_Y 30
#define TD_WF_H 192
#define TD_BTN_X (TD_WF_X + 5)
#define TD_BTN_W (TD_WF_W - 10)
#define TD_PAIR_BTN_H 42
#define TD_SETTINGS_BTN_H TD_PAIR_BTN_H
#define TD_PANEL_H (TD_FOOT_Y - TD_WF_Y - 4)
#define TD_ONLINE_SETTINGS_BTN_Y (TD_FOOT_Y - TD_SETTINGS_BTN_H - 4)
#define TD_WF_H (TD_ONLINE_SETTINGS_BTN_Y - TD_WF_Y - 8)
#define TD_WF_SIZE TD_WF_H
#define TD_PAIR_BTN_Y (TD_WF_Y + 42)
#define TD_SETTINGS_BTN_Y (TD_PAIR_BTN_Y + TD_PAIR_BTN_H + 10)
#define TD_CANCEL_BTN_Y (TD_WF_Y + 130)
#define TD_SETTINGS_BACK_X 252
#define TD_SETTINGS_BACK_Y 32
#define TD_SETTINGS_BACK_W 58
#define TD_SETTINGS_BACK_H 28
#define TD_SETTINGS_MINUS_X 18
#define TD_SETTINGS_MINUS_Y 88
#define TD_SETTINGS_STEP_W 36
#define TD_SETTINGS_STEP_H 30
#define TD_SETTINGS_PLUS_X 266
#define TD_SETTINGS_PLUS_Y 88
#define TD_SETTINGS_BAR_X 66
#define TD_SETTINGS_BAR_Y 99
#define TD_SETTINGS_BAR_W 188
#define TD_SETTINGS_BAR_H 8
#define TD_SETTINGS_ABOUT_X 18
#define TD_SETTINGS_ABOUT_Y 128
#define TD_SETTINGS_ABOUT_W 132
#define TD_SETTINGS_ABOUT_H 34
#define TD_SETTINGS_SLEEP_X 166
#define TD_SETTINGS_SLEEP_Y 128
#define TD_SETTINGS_SLEEP_W 136
#define TD_SETTINGS_SLEEP_H 34
#define TD_SETTINGS_FORGET_X 18
#define TD_SETTINGS_FORGET_Y 174
#define TD_SETTINGS_FORGET_W 284
#define TD_SETTINGS_FORGET_H 34
#define TD_ABOUT_CLOSE_X 126
#define TD_ABOUT_CLOSE_Y 178
#define TD_ABOUT_CLOSE_W 68
#define TD_ABOUT_CLOSE_H 32
#define TD_BRIGHTNESS_MIN_PCT 5
#define TD_BRIGHTNESS_STEP_PCT 5
#ifndef RSDECK_VERSION_MAJOR
#define RSDECK_VERSION_MAJOR 0
#endif
#ifndef RSDECK_VERSION_MINOR
#define RSDECK_VERSION_MINOR 0
#endif
#ifndef RSDECK_VERSION_PATCH
#define RSDECK_VERSION_PATCH 0
#endif
static const uint16_t td_wf_palette[9] = {
0x0000, 0x0011, 0x001F, 0x07FF,
@@ -50,6 +105,18 @@ int td_waterfall[TD_WF_SIZE];
int td_waterfall_head = 0;
uint8_t td_charge_tick = 0;
enum TdView : uint8_t {
TD_VIEW_MAIN = 0,
TD_VIEW_SETTINGS = 1,
TD_VIEW_ABOUT = 2
};
TdView td_view = TD_VIEW_MAIN;
uint8_t td_settings_brightness_pct = 80;
uint32_t td_settings_forget_until = 0;
uint32_t td_settings_status_until = 0;
const char* td_settings_status_msg = nullptr;
// --- GT911 touch (poll-only, tap toggles display power) ---
#define TD_TOUCH_SDA 18
#define TD_TOUCH_SCL 8
@@ -119,6 +186,135 @@ bool td_pair_button_visible() {
return !radio_online && bt_state != BT_STATE_CONNECTED && bt_state != BT_STATE_PAIRING;
}
bool td_settings_button_visible() {
return bt_state != BT_STATE_PAIRING;
}
bool td_hit(int16_t x, int16_t y, int rx, int ry, int rw, int rh) {
return x >= rx && x < rx + rw && y >= ry && y < ry + rh;
}
uint8_t td_clamp_brightness_pct(uint8_t pct) {
if (pct < TD_BRIGHTNESS_MIN_PCT) return TD_BRIGHTNESS_MIN_PCT;
if (pct > 100) return 100;
uint8_t snapped = ((pct + (TD_BRIGHTNESS_STEP_PCT / 2)) / TD_BRIGHTNESS_STEP_PCT) * TD_BRIGHTNESS_STEP_PCT;
if (snapped < TD_BRIGHTNESS_MIN_PCT) return TD_BRIGHTNESS_MIN_PCT;
if (snapped > 100) return 100;
return snapped;
}
uint8_t td_intensity_to_pct(uint8_t intensity) {
if (intensity == 0 || intensity == 0xFF) return TDECK_DISPLAY_INTENSITY_DEFAULT;
return td_clamp_brightness_pct(intensity);
}
uint8_t td_pct_to_intensity(uint8_t pct) {
return td_clamp_brightness_pct(pct);
}
void td_settings_set_status(const char* msg, uint16_t duration_ms = 1800) {
td_settings_status_msg = msg;
td_settings_status_until = millis() + duration_ms;
}
void td_settings_load_brightness() {
uint8_t intensity = display_intensity;
if (intensity == 0 && display_unblank_intensity != 0) intensity = display_unblank_intensity;
td_settings_brightness_pct = td_intensity_to_pct(intensity);
}
void td_settings_preview_brightness(uint8_t pct) {
td_settings_brightness_pct = td_clamp_brightness_pct(pct);
display_intensity = td_pct_to_intensity(td_settings_brightness_pct);
display_unblank_intensity = 0;
di_conf_save(display_intensity);
display_blank_frame_drawn = false;
display_unblank();
}
void td_open_settings() {
td_view = TD_VIEW_SETTINGS;
td_settings_forget_until = 0;
td_settings_status_msg = nullptr;
td_settings_load_brightness();
display_unblank();
last_disp_update = 0;
}
void td_sleep_display() {
uint8_t current = display_intensity;
if (current == 0 || current == 0xFF) current = TDECK_DISPLAY_INTENSITY_DEFAULT;
display_unblank_intensity = current;
display_intensity = 0;
display_blanked = true;
display_blank_frame_drawn = false;
td_view = TD_VIEW_MAIN;
last_unblank_event = millis() - display_blanking_timeout - 1;
last_disp_update = 0;
}
bool td_handle_settings_touch(int16_t x, int16_t y) {
if (td_hit(x, y, TD_SETTINGS_BACK_X, TD_SETTINGS_BACK_Y, TD_SETTINGS_BACK_W, TD_SETTINGS_BACK_H)) {
td_view = TD_VIEW_MAIN;
td_settings_forget_until = 0;
display_unblank();
return true;
}
if (td_hit(x, y, TD_SETTINGS_MINUS_X, TD_SETTINGS_MINUS_Y, TD_SETTINGS_STEP_W, TD_SETTINGS_STEP_H)) {
uint8_t next = td_settings_brightness_pct;
if (next > TD_BRIGHTNESS_MIN_PCT + TD_BRIGHTNESS_STEP_PCT) next -= TD_BRIGHTNESS_STEP_PCT;
else next = TD_BRIGHTNESS_MIN_PCT;
td_settings_preview_brightness(next);
return true;
}
if (td_hit(x, y, TD_SETTINGS_PLUS_X, TD_SETTINGS_PLUS_Y, TD_SETTINGS_STEP_W, TD_SETTINGS_STEP_H)) {
uint8_t next = td_settings_brightness_pct;
if (next < 100 - TD_BRIGHTNESS_STEP_PCT) next += TD_BRIGHTNESS_STEP_PCT;
else next = 100;
td_settings_preview_brightness(next);
return true;
}
if (td_hit(x, y, TD_SETTINGS_ABOUT_X, TD_SETTINGS_ABOUT_Y, TD_SETTINGS_ABOUT_W, TD_SETTINGS_ABOUT_H)) {
td_view = TD_VIEW_ABOUT;
display_unblank();
return true;
}
if (td_hit(x, y, TD_SETTINGS_SLEEP_X, TD_SETTINGS_SLEEP_Y, TD_SETTINGS_SLEEP_W, TD_SETTINGS_SLEEP_H)) {
td_sleep_display();
return true;
}
if (td_hit(x, y, TD_SETTINGS_FORGET_X, TD_SETTINGS_FORGET_Y, TD_SETTINGS_FORGET_W, TD_SETTINGS_FORGET_H)) {
if ((int32_t)(td_settings_forget_until - millis()) > 0) {
bt_debond_all();
bt_disable_pairing();
td_settings_forget_until = 0;
td_settings_set_status("BLE bonds removed", 2200);
} else {
td_settings_forget_until = millis() + 5000;
td_settings_set_status("Tap Forget again", 5000);
}
display_unblank();
return true;
}
return false;
}
bool td_handle_about_touch(int16_t x, int16_t y) {
if (td_hit(x, y, TD_ABOUT_CLOSE_X, TD_ABOUT_CLOSE_Y, TD_ABOUT_CLOSE_W, TD_ABOUT_CLOSE_H)) {
td_view = TD_VIEW_SETTINGS;
display_unblank();
return true;
}
return false;
}
void td_poll_touch() {
if (millis() - td_last_touch_poll < 50) return;
td_last_touch_poll = millis();
@@ -138,15 +334,34 @@ void td_poll_touch() {
// inactivity timeout alone, never from a tap.
if (display_blanked) {
display_unblank();
td_view = TD_VIEW_MAIN;
last_disp_update = 0;
return;
}
bool in_panel = td_touch_x >= (TD_WF_X - 6) && td_touch_y >= TD_BAR_H && td_touch_y < TD_FOOT_Y;
if (in_panel && td_pair_button_visible()) {
bt_enable_pairing(); // 30s window, auto-disarms via BT_PAIRING_TIMEOUT
} else if (in_panel && bt_state == BT_STATE_PAIRING) {
if (td_view == TD_VIEW_SETTINGS) {
if (!td_handle_settings_touch(td_touch_x, td_touch_y)) {
display_unblank();
}
last_disp_update = 0;
return;
}
if (td_view == TD_VIEW_ABOUT) {
if (!td_handle_about_touch(td_touch_x, td_touch_y)) {
display_unblank();
}
last_disp_update = 0;
return;
}
int settings_y = radio_online ? TD_ONLINE_SETTINGS_BTN_Y : TD_SETTINGS_BTN_Y;
if (td_pair_button_visible() && td_hit(td_touch_x, td_touch_y, TD_BTN_X, TD_PAIR_BTN_Y, TD_BTN_W, TD_PAIR_BTN_H)) {
bt_enable_pairing(); // Auto-disarms via BT_PAIRING_TIMEOUT.
} else if (bt_state == BT_STATE_PAIRING && td_hit(td_touch_x, td_touch_y, TD_BTN_X, TD_CANCEL_BTN_Y, TD_BTN_W, TD_SETTINGS_BTN_H)) {
bt_disable_pairing(); // tap again to cancel
} else if (td_settings_button_visible() && td_hit(td_touch_x, td_touch_y, TD_BTN_X, settings_y, TD_BTN_W, TD_SETTINGS_BTN_H)) {
td_open_settings();
} else {
display_unblank(); // count as activity, extend the timeout
}
@@ -203,6 +418,29 @@ void td_draw_gradient_bar(int x, int y, int w, int h, float percent) {
if (fill_w > 0) td_canvas->fillRect(x + 1, y + 1, fill_w, h - 2, fill_clr);
}
int td_text_width(const char* label, int text_size = 1) {
int len = 0;
while (label && label[len] != '\0') len++;
return len * 6 * text_size;
}
void td_draw_centered_text(const char* label, int x, int y, int w, int text_size, uint16_t colour) {
td_canvas->setTextSize(text_size);
td_canvas->setTextColor(colour);
int tw = td_text_width(label, text_size);
int tx = x + ((w - tw) / 2);
if (tx < x + 2) tx = x + 2;
td_canvas->setCursor(tx, y);
td_canvas->print(label);
}
void td_draw_button(int x, int y, int w, int h, const char* label, uint16_t border, uint16_t text, bool armed = false) {
uint16_t fill = armed ? 0x3000 : TD_CLR_BG_PANEL;
td_canvas->fillRoundRect(x, y, w, h, 6, fill);
td_canvas->drawRoundRect(x, y, w, h, 6, border);
td_draw_centered_text(label, x, y + ((h - 8) / 2), w, 1, text);
}
void td_draw_usb_icon(int x, int y, uint16_t colour) {
td_canvas->fillRect(x + 3, y, 7, 10, colour);
td_canvas->fillRect(x + 4, y + 10, 5, 4, colour);
@@ -306,10 +544,7 @@ void td_draw_pair_panel() {
td_canvas->setTextColor(TD_CLR_TEXT_PRIMARY);
td_canvas->setCursor(remain_s >= 10 ? cx - 23 : cx - 11, TD_WF_Y + 76);
td_canvas->print(rbuf);
td_canvas->setTextSize(1);
td_canvas->setTextColor(TD_CLR_TEXT_DIM);
td_canvas->setCursor(cx - 39, TD_WF_Y + 118);
td_canvas->print("tap to cancel");
td_draw_button(TD_BTN_X, TD_CANCEL_BTN_Y, TD_BTN_W, TD_SETTINGS_BTN_H, "Cancel", TD_CLR_WARN, TD_CLR_WARN);
return;
}
@@ -321,37 +556,36 @@ void td_draw_pair_panel() {
td_canvas->setTextColor(TD_CLR_TEXT_DIM);
td_canvas->setCursor(cx - 47, TD_WF_Y + 96);
td_canvas->print("waiting for host");
td_draw_button(TD_BTN_X, TD_SETTINGS_BTN_Y, TD_BTN_W, TD_SETTINGS_BTN_H, "Settings", TD_CLR_ACCENT, TD_CLR_ACCENT);
return;
}
// Idle: paired devices can connect without opening pairing. The same panel
// remains tappable to pair an additional or re-paired host.
int bw = 98; int bh = 48;
int bx = TD_WF_X + (TD_WF_W - bw) / 2;
int by = TD_WF_Y + 72;
bool has_bond = bt_bond_count() > 0;
td_canvas->fillRoundRect(bx, by, bw, bh, 8, TD_CLR_BG_PANEL);
td_canvas->drawRoundRect(bx, by, bw, bh, 8, has_bond ? TD_CLR_GREEN : TD_CLR_ACCENT);
td_canvas->fillRoundRect(TD_BTN_X, TD_PAIR_BTN_Y, TD_BTN_W, TD_PAIR_BTN_H, 8, TD_CLR_BG_PANEL);
td_canvas->drawRoundRect(TD_BTN_X, TD_PAIR_BTN_Y, TD_BTN_W, TD_PAIR_BTN_H, 8, has_bond ? TD_CLR_GREEN : TD_CLR_ACCENT);
td_canvas->setTextSize(1);
if (has_bond) {
td_canvas->setTextColor(TD_CLR_GREEN);
td_canvas->setCursor(bx + 23, by + 10);
td_canvas->setCursor(TD_BTN_X + 23, TD_PAIR_BTN_Y + 8);
td_canvas->print("BLE ready");
td_canvas->setTextColor(TD_CLR_TEXT_DIM);
td_canvas->setCursor(bx + 19, by + 25);
td_canvas->setCursor(TD_BTN_X + 19, TD_PAIR_BTN_Y + 23);
td_canvas->print("tap to pair");
} else {
td_canvas->setTextColor(TD_CLR_ACCENT);
td_canvas->setCursor(bx + 25, by + 14);
td_canvas->setCursor(TD_BTN_X + 25, TD_PAIR_BTN_Y + 10);
td_canvas->print("Pair via");
td_canvas->setCursor(bx + 40, by + 28);
td_canvas->setCursor(TD_BTN_X + 40, TD_PAIR_BTN_Y + 24);
td_canvas->print("BLE");
}
td_draw_button(TD_BTN_X, TD_SETTINGS_BTN_Y, TD_BTN_W, TD_SETTINGS_BTN_H, "Settings", TD_CLR_ACCENT, TD_CLR_ACCENT);
}
void td_draw_waterfall() {
td_canvas->fillRect(TD_WF_X, TD_WF_Y, TD_WF_W, TD_WF_H + 2, TD_CLR_BG);
td_canvas->drawFastVLine(TD_WF_X - 2, TD_WF_Y, TD_WF_H, TD_CLR_BORDER);
td_canvas->fillRect(TD_WF_X, TD_WF_Y, TD_WF_W, TD_PANEL_H, TD_CLR_BG);
td_canvas->drawFastVLine(TD_WF_X - 2, TD_WF_Y, TD_PANEL_H, TD_CLR_BORDER);
if (!radio_online) {
td_draw_pair_panel();
@@ -389,6 +623,10 @@ void td_draw_waterfall() {
}
}
}
if (td_settings_button_visible()) {
td_draw_button(TD_BTN_X, TD_ONLINE_SETTINGS_BTN_Y, TD_BTN_W, TD_SETTINGS_BTN_H, "Settings", TD_CLR_ACCENT, TD_CLR_ACCENT);
}
}
void td_draw_idle_content() {
@@ -548,6 +786,114 @@ void td_draw_error_screen() {
}
}
void td_draw_settings_screen() {
td_canvas->fillRect(0, TD_BAR_H + 1, TD_W, TD_FOOT_Y - TD_BAR_H - 1, TD_CLR_BG);
int x = 12;
int y = TD_CONTENT_Y + 6;
td_canvas->setTextSize(2);
td_canvas->setTextColor(TD_CLR_TEXT_PRIMARY);
td_canvas->setCursor(x, y);
td_canvas->print("Settings");
td_draw_button(TD_SETTINGS_BACK_X, TD_SETTINGS_BACK_Y, TD_SETTINGS_BACK_W, TD_SETTINGS_BACK_H, "Back", TD_CLR_BORDER, TD_CLR_TEXT_SECONDARY);
y += 34;
td_canvas->setTextSize(1);
td_canvas->setTextColor(TD_CLR_TEXT_SECONDARY);
td_canvas->setCursor(x, y);
td_canvas->print("Brightness");
char pctbuf[8];
snprintf(pctbuf, sizeof(pctbuf), "%u%%", (unsigned int)td_settings_brightness_pct);
td_canvas->setTextSize(2);
td_canvas->setTextColor(TD_CLR_ACCENT);
td_canvas->setCursor(214, y - 5);
td_canvas->print(pctbuf);
td_draw_button(TD_SETTINGS_MINUS_X, TD_SETTINGS_MINUS_Y, TD_SETTINGS_STEP_W, TD_SETTINGS_STEP_H, "-", TD_CLR_BORDER, TD_CLR_TEXT_PRIMARY);
td_draw_button(TD_SETTINGS_PLUS_X, TD_SETTINGS_PLUS_Y, TD_SETTINGS_STEP_W, TD_SETTINGS_STEP_H, "+", TD_CLR_BORDER, TD_CLR_TEXT_PRIMARY);
td_canvas->drawRect(TD_SETTINGS_BAR_X, TD_SETTINGS_BAR_Y, TD_SETTINGS_BAR_W, TD_SETTINGS_BAR_H, TD_CLR_BORDER);
int usable = TD_SETTINGS_BAR_W - 2;
int fill = ((td_settings_brightness_pct - TD_BRIGHTNESS_MIN_PCT) * usable) / (100 - TD_BRIGHTNESS_MIN_PCT);
if (fill > 0) td_canvas->fillRect(TD_SETTINGS_BAR_X + 1, TD_SETTINGS_BAR_Y + 1, fill, TD_SETTINGS_BAR_H - 2, TD_CLR_ACCENT);
td_canvas->setTextSize(1);
td_canvas->setTextColor(TD_CLR_TEXT_DIM);
td_canvas->setCursor(TD_SETTINGS_BAR_X, TD_SETTINGS_BAR_Y + 14);
td_canvas->print("min 5%");
td_draw_button(TD_SETTINGS_ABOUT_X, TD_SETTINGS_ABOUT_Y, TD_SETTINGS_ABOUT_W, TD_SETTINGS_ABOUT_H, "About", TD_CLR_GREEN, TD_CLR_GREEN);
td_draw_button(TD_SETTINGS_SLEEP_X, TD_SETTINGS_SLEEP_Y, TD_SETTINGS_SLEEP_W, TD_SETTINGS_SLEEP_H, "Sleep Screen", TD_CLR_ACCENT, TD_CLR_ACCENT);
bool confirm_forget = (int32_t)(td_settings_forget_until - millis()) > 0;
td_draw_button(
TD_SETTINGS_FORGET_X,
TD_SETTINGS_FORGET_Y,
TD_SETTINGS_FORGET_W,
TD_SETTINGS_FORGET_H,
confirm_forget ? "Confirm Forget BLE" : "Forget BLE Bonds",
confirm_forget ? TD_CLR_WARN : TD_CLR_BORDER,
confirm_forget ? TD_CLR_WARN : TD_CLR_TEXT_SECONDARY,
confirm_forget
);
if (td_settings_status_msg && (int32_t)(td_settings_status_until - millis()) > 0) {
td_draw_centered_text(td_settings_status_msg, 10, TD_FOOT_Y - 17, TD_W - 20, 1, TD_CLR_TEXT_DIM);
} else {
td_settings_status_msg = nullptr;
}
}
void td_draw_about_screen() {
td_canvas->fillRect(0, TD_BAR_H + 1, TD_W, TD_FOOT_Y - TD_BAR_H - 1, TD_CLR_BG);
int px = 34;
int py = 44;
int pw = 252;
int ph = 168;
td_canvas->fillRoundRect(px, py, pw, ph, 8, TD_CLR_BG_PANEL);
td_canvas->drawRoundRect(px, py, pw, ph, 8, TD_CLR_ACCENT);
td_canvas->setTextSize(2);
td_canvas->setTextColor(TD_CLR_TEXT_PRIMARY);
td_canvas->setCursor(px + 20, py + 16);
td_canvas->print("About RNode");
char line[36];
td_canvas->setTextSize(1);
td_canvas->setTextColor(TD_CLR_TEXT_SECONDARY);
snprintf(line, sizeof(line), "rsDeck v%d.%d.%d", RSDECK_VERSION_MAJOR, RSDECK_VERSION_MINOR, RSDECK_VERSION_PATCH);
td_canvas->setCursor(px + 20, py + 54);
td_canvas->print(line);
snprintf(line, sizeof(line), "RNode v%d.%02d", MAJ_VERS, MIN_VERS);
td_canvas->setCursor(px + 20, py + 72);
td_canvas->print(line);
uint32_t total_s = millis() / 1000;
uint32_t days = total_s / 86400;
uint32_t hours = (total_s / 3600) % 24;
uint32_t mins = (total_s / 60) % 60;
uint32_t secs = total_s % 60;
if (days > 0) {
snprintf(line, sizeof(line), "Uptime %lud %02lu:%02lu:%02lu",
(unsigned long)days, (unsigned long)hours, (unsigned long)mins, (unsigned long)secs);
} else {
snprintf(line, sizeof(line), "Uptime %02lu:%02lu:%02lu",
(unsigned long)hours, (unsigned long)mins, (unsigned long)secs);
}
td_canvas->setCursor(px + 20, py + 90);
td_canvas->print(line);
int bonds = bt_bond_count();
snprintf(line, sizeof(line), "BLE bonds %d", bonds);
td_canvas->setCursor(px + 20, py + 108);
td_canvas->print(line);
td_draw_button(TD_ABOUT_CLOSE_X, TD_ABOUT_CLOSE_Y, TD_ABOUT_CLOSE_W, TD_ABOUT_CLOSE_H, "Close", TD_CLR_ACCENT, TD_CLR_ACCENT);
}
void td_draw_external_framebuffer() {
int scale = 3;
int ox = (TD_LEFT_W - 64 * scale) / 2;
@@ -622,9 +968,20 @@ void td_render_frame() {
td_canvas->fillScreen(TD_CLR_BG);
td_draw_statusbar();
td_draw_waterfall();
td_draw_footer();
if (td_view == TD_VIEW_SETTINGS) {
td_draw_settings_screen();
return;
}
if (td_view == TD_VIEW_ABOUT) {
td_draw_about_screen();
return;
}
td_draw_waterfall();
if (disp_ext_fb && bt_ssp_pin == 0) {
td_draw_external_framebuffer();
} else if (!hw_ready || radio_error || !device_firmware_ok()) {
+5
View File
@@ -1767,6 +1767,11 @@ void bt_conf_save(bool is_enabled) {
}
void di_conf_save(uint8_t dint) {
#if BOARD_MODEL == BOARD_TDECK
if (dint != 0 && dint != 0xFF) {
dint = tdeck_display_encode_intensity(dint);
}
#endif
eeprom_update(eeprom_addr(ADDR_CONF_DINT), dint);
}
+13 -1
View File
@@ -529,7 +529,16 @@ int sx126x::begin(long frequency) {
return 1;
}
void sx126x::end() { sleep(); LORA_SPI.end(); _preinit_done = false; }
void sx126x::end() {
onReceive(NULL);
sleep();
#if BOARD_MODEL != BOARD_TDECK
LORA_SPI.end();
#else
// T-Deck display and LoRa share Arduino SPI; ending it crashes the next UI refresh.
#endif
_preinit_done = false;
}
int sx126x::beginPacket(int implicitHeader) {
#if HAS_LORA_PA
@@ -749,6 +758,9 @@ void sx126x::onReceive(void(*callback)(int)){
#ifdef SPI_HAS_NOTUSINGINTERRUPT
LORA_SPI.notUsingInterrupt(digitalPinToInterrupt(_dio0));
#endif
#if BOARD_MODEL == BOARD_TDECK
_irq_pending = false;
#endif
}
}