mirror of
https://github.com/meshtastic/firmware.git
synced 2026-06-07 16:26:26 -04:00
Merge remote-tracking branch 'origin/develop' into t-watch-ultra
This commit is contained in:
8
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
8
.github/ISSUE_TEMPLATE/Bug Report.yml
vendored
@@ -75,11 +75,11 @@ body:
|
||||
- type: checkboxes
|
||||
id: mui
|
||||
attributes:
|
||||
label: Is this bug report about any UI component firmware like InkHUD or Meshtatic UI (MUI)?
|
||||
label: Is this bug report about any UI (https://meshtastic.org/docs/configuration/device-uis/) component firmware?
|
||||
options:
|
||||
- label: Meshtastic UI aka MUI colorTFT
|
||||
- label: InkHUD ePaper
|
||||
- label: OLED slide UI on any display
|
||||
- label: Meshtastic UI aka MUI
|
||||
- label: InkHUD
|
||||
- label: BaseUI
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
|
||||
8
.github/workflows/test_native.yml
vendored
8
.github/workflows/test_native.yml
vendored
@@ -86,7 +86,13 @@ jobs:
|
||||
run: sed -i 's/-DBUILD_EPOCH=$UNIX_TIME/#-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
|
||||
|
||||
- name: PlatformIO Tests
|
||||
run: platformio test -e coverage -v --junit-output-path testreport.xml
|
||||
run: |
|
||||
set -o pipefail
|
||||
# Filter out SKIPPED summary rows for hardware variants that can't run on the
|
||||
# native host. They flood the log and make it harder to spot real failures.
|
||||
# The JUnit XML is written directly to testreport.xml before the pipe, so
|
||||
# the test artifact is unaffected.
|
||||
platformio test -e coverage -v --junit-output-path testreport.xml 2>&1 | grep -v "[[:space:]]SKIPPED$"
|
||||
|
||||
- name: Save test results
|
||||
if: always() # run this step even if previous step failed
|
||||
|
||||
@@ -8,18 +8,18 @@ plugins:
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.517
|
||||
- renovate@43.110.9
|
||||
- prettier@3.8.1
|
||||
- trufflehog@3.94.3
|
||||
- checkov@3.2.524
|
||||
- renovate@43.141.0
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.2
|
||||
- yamllint@1.38.0
|
||||
- bandit@1.9.4
|
||||
- trivy@0.69.3
|
||||
- trivy@0.70.0
|
||||
- taplo@0.10.0
|
||||
- ruff@0.15.9
|
||||
- ruff@0.15.11
|
||||
- isort@8.0.1
|
||||
- markdownlint@0.48.0
|
||||
- oxipng@10.1.0
|
||||
- oxipng@10.1.1
|
||||
- svgo@4.0.1
|
||||
- actionlint@1.7.12
|
||||
- flake8@7.3.0
|
||||
|
||||
118
bin/show-unmerged-prs.sh
Executable file
118
bin/show-unmerged-prs.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to show commits in develop that are not in master
|
||||
# with their associated PR info and commit hashes
|
||||
#
|
||||
# Usage:
|
||||
# ./show-unmerged-prs.sh # Show all unmerged commits
|
||||
# ./show-unmerged-prs.sh --bugfix # Show only bugfix-labeled PRs
|
||||
|
||||
set -e
|
||||
|
||||
REPO="firmware"
|
||||
OWNER="meshtastic"
|
||||
BASE_BRANCH="master"
|
||||
HEAD_BRANCH="develop"
|
||||
LIMIT=100
|
||||
FILTER_LABEL=""
|
||||
|
||||
# Parse arguments
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--bugfix)
|
||||
FILTER_LABEL="bugfix"
|
||||
shift
|
||||
;;
|
||||
--feature)
|
||||
FILTER_LABEL="feature"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --bugfix Show only PRs labeled with 'bugfix'"
|
||||
echo " --feature Show only PRs labeled with 'feature'"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH (filtered by label: $FILTER_LABEL)..."
|
||||
else
|
||||
echo "Fetching commits in $HEAD_BRANCH that are not in $BASE_BRANCH..."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check if gh CLI is available
|
||||
if ! command -v gh &> /dev/null; then
|
||||
echo "ERROR: GitHub CLI (gh) not found. Please install it first."
|
||||
echo "Visit: https://cli.github.com/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get commits in develop that are not in master
|
||||
# For each commit, try to find associated PR
|
||||
git fetch origin develop master 2>/dev/null || true
|
||||
|
||||
# Use git to get the list of commits
|
||||
commits=$(git log --pretty=format:"%H|%s" origin/master..origin/develop | head -n $LIMIT)
|
||||
|
||||
count=0
|
||||
displayed=0
|
||||
echo "Commits in $HEAD_BRANCH not in $BASE_BRANCH:"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
while IFS='|' read -r hash subject; do
|
||||
((count++))
|
||||
|
||||
# Try to find the PR for this commit
|
||||
# Extract PR number, title, description, and labels
|
||||
pr_response=$(gh api -X GET "/repos/$OWNER/$REPO/commits/$hash/pulls" \
|
||||
-H "Accept: application/vnd.github.v3+json" 2>/dev/null | \
|
||||
jq -r '.[0] | "\(.number)|\(.title)|\(.body // "No description")|\(.labels | map(.name) | join(","))"' 2>/dev/null || echo "||||")
|
||||
|
||||
if [ -z "$pr_response" ] || [ "$pr_response" = "||||" ]; then
|
||||
# If no PR found, skip if filter is active, otherwise show the commit
|
||||
if [ -z "$FILTER_LABEL" ]; then
|
||||
((displayed++))
|
||||
echo "[$displayed] Commit: $hash"
|
||||
echo " Subject: $subject"
|
||||
echo " PR: Not found in GitHub"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
IFS='|' read -r pr_num pr_title pr_desc pr_labels <<< "$pr_response"
|
||||
|
||||
# Check if filter matches
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
# Only show if the label is in the labels list
|
||||
if ! echo "$pr_labels" | grep -q "$FILTER_LABEL"; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
((displayed++))
|
||||
echo "[$displayed] PR #$pr_num - $pr_title"
|
||||
echo " Commit: $hash"
|
||||
if [ -n "$pr_desc" ] && [ "$pr_desc" != "No description" ]; then
|
||||
# Truncate description to 200 chars
|
||||
desc_short="${pr_desc:0:200}"
|
||||
[ ${#pr_desc} -gt 200 ] && desc_short+="..."
|
||||
echo " Description: $desc_short"
|
||||
fi
|
||||
if [ -n "$pr_labels" ] && [ "$pr_labels" != "" ]; then
|
||||
echo " Labels: $pr_labels"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
done <<< "$commits"
|
||||
|
||||
echo ""
|
||||
if [ -n "$FILTER_LABEL" ]; then
|
||||
echo "Done. Showing $displayed PRs with label '$FILTER_LABEL' from $displayed commits checked."
|
||||
else
|
||||
echo "Done. Showing $displayed commits from $HEAD_BRANCH not in $BASE_BRANCH."
|
||||
fi
|
||||
@@ -29,6 +29,7 @@ build_flags = -Wno-missing-field-initializers
|
||||
-DUSE_THREAD_NAMES
|
||||
-DTINYGPS_OPTION_NO_CUSTOM_FIELDS
|
||||
-DPB_ENABLE_MALLOC=1
|
||||
-DPB_VALIDATE_UTF8=1
|
||||
-DRADIOLIB_EXCLUDE_CC1101=1
|
||||
-DRADIOLIB_EXCLUDE_NRF24=1
|
||||
-DRADIOLIB_EXCLUDE_RF69=1
|
||||
@@ -66,7 +67,7 @@ monitor_speed = 115200
|
||||
monitor_filters = direct
|
||||
lib_deps =
|
||||
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/21e484f409cde18d44012caef84c244eb5ca28f3.zip
|
||||
https://github.com/meshtastic/esp8266-oled-ssd1306/archive/6bfd1f135e1ebe37afd6050bb4b9964cea3fcfda.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
|
||||
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip
|
||||
# renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
|
||||
|
||||
Submodule protobufs updated: 4d5b500df5...249a80855a
228
src/Power.cpp
228
src/Power.cpp
@@ -94,16 +94,31 @@ static const adc_atten_t atten = ADC_ATTENUATION;
|
||||
#endif
|
||||
#endif // BATTERY_PIN && ARCH_ESP32
|
||||
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#ifndef EXT_PWR_DETECT_MODE
|
||||
#define EXT_PWR_DETECT_MODE INPUT
|
||||
// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE
|
||||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP
|
||||
#define EXT_PWR_DETECT_VALUE LOW
|
||||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN
|
||||
#define EXT_PWR_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#ifndef EXT_PWR_DETECT_VALUE
|
||||
#define EXT_PWR_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
#ifndef EXT_CHRG_DETECT_MODE
|
||||
static const uint8_t ext_chrg_detect_mode = INPUT;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_mode = EXT_CHRG_DETECT_MODE;
|
||||
#define EXT_CHRG_DETECT_MODE INPUT
|
||||
// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE
|
||||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP
|
||||
#define EXT_CHRG_DETECT_VALUE LOW
|
||||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#ifndef EXT_CHRG_DETECT_VALUE
|
||||
static const uint8_t ext_chrg_detect_value = HIGH;
|
||||
#else
|
||||
static const uint8_t ext_chrg_detect_value = EXT_CHRG_DETECT_VALUE;
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -469,28 +484,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||||
#endif
|
||||
|
||||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||||
/// pumping in power On some boards we don't have the power management chip
|
||||
/// (like AXPxxxx) so we use EXT_PWR_DETECT GPIO pin to detect external power
|
||||
/// source
|
||||
// Detect if an external power source is connected if we don’t have a PMIC;
|
||||
// Firstly prefer EXT_PWR_DETECT GPIO if available,
|
||||
// secondly try an nRF52-specific routine on some variants,
|
||||
// lastly provide a fallback to indicate external power when fully charged.
|
||||
virtual bool isVbusIn() override
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
||||
// if external powered that pin will be pulled down
|
||||
if (digitalRead(EXT_PWR_DETECT) == LOW) {
|
||||
return true;
|
||||
}
|
||||
// if it's not LOW - check the battery
|
||||
#else
|
||||
// if external powered that pin will be pulled up
|
||||
if (digitalRead(EXT_PWR_DETECT) == HIGH) {
|
||||
return true;
|
||||
}
|
||||
// if it's not HIGH - check the battery
|
||||
#endif
|
||||
// If we have an EXT_PWR_DETECT pin and it indicates no external power, believe it.
|
||||
return false;
|
||||
return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE;
|
||||
|
||||
// technically speaking this should work for all(?) NRF52 boards
|
||||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||||
@@ -511,9 +512,9 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
#endif
|
||||
#if defined(ELECROW_ThinkNode_M6)
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value || isVbusIn();
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
|
||||
#elif EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
#elif defined(BATTERY_CHARGING_INV)
|
||||
return !digitalRead(BATTERY_CHARGING_INV);
|
||||
#else
|
||||
@@ -646,14 +647,10 @@ Power::Power() : OSThread("Power")
|
||||
bool Power::analogInit()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB)
|
||||
pinMode(EXT_PWR_DETECT, INPUT_PULLUP);
|
||||
#else
|
||||
pinMode(EXT_PWR_DETECT, INPUT);
|
||||
#endif
|
||||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
||||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
@@ -746,37 +743,17 @@ bool Power::setup()
|
||||
found = true;
|
||||
#endif
|
||||
}
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
attachInterrupt(
|
||||
BATTERY_CHARGING_INV,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
attachPowerInterrupts();
|
||||
enabled = found;
|
||||
low_voltage_counter = 0;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
// Used to detach and reattach interrupts
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
@@ -1023,6 +1000,14 @@ int32_t Power::runOnce()
|
||||
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
||||
}
|
||||
|
||||
#ifdef PMU_POWER_BUTTON_IS_CANCEL
|
||||
// cancel action also turns the screen on and off.
|
||||
if (PMU->isPekeyShortPressIrq()) {
|
||||
LOG_INFO("Input: Corona Button Click");
|
||||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||||
inputBroker->injectInputEvent(&event);
|
||||
}
|
||||
#endif
|
||||
/*
|
||||
Other things we could check if we cared...
|
||||
|
||||
@@ -1039,13 +1024,6 @@ int32_t Power::runOnce()
|
||||
LOG_DEBUG("Battery removed");
|
||||
}
|
||||
*/
|
||||
#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3?
|
||||
if (PMU->isPekeyLongPressIrq()) {
|
||||
LOG_DEBUG("PEK long button press");
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
}
|
||||
@@ -1055,6 +1033,97 @@ int32_t Power::runOnce()
|
||||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int Power::beforeLightSleep(void *unused)
|
||||
{
|
||||
LOG_WARN("Detaching power interrupts for sleep");
|
||||
detachPowerInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
attachPowerInterrupts();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Attach (or re-attach) hardware interrupts for power management
|
||||
* Public method. Used outside class when waking from MCU sleep
|
||||
*/
|
||||
void Power::attachPowerInterrupts()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
attachInterrupt(
|
||||
EXT_PWR_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
attachInterrupt(
|
||||
BATTERY_CHARGING_INV,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
attachInterrupt(
|
||||
EXT_CHRG_DETECT,
|
||||
[]() {
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
BaseType_t higherWake = 0;
|
||||
},
|
||||
CHANGE);
|
||||
#endif
|
||||
#ifdef PMU_IRQ
|
||||
if (PMU) {
|
||||
attachInterrupt(
|
||||
PMU_IRQ,
|
||||
[]() {
|
||||
pmu_irq = true;
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
FALLING);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Detach the "normal" button interrupts.
|
||||
* Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
|
||||
*/
|
||||
void Power::detachPowerInterrupts()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
detachInterrupt(EXT_PWR_DETECT);
|
||||
#endif
|
||||
#ifdef BATTERY_CHARGING_INV
|
||||
detachInterrupt(BATTERY_CHARGING_INV);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
detachInterrupt(EXT_CHRG_DETECT);
|
||||
#endif
|
||||
#ifdef PMU_IRQ
|
||||
if (PMU) {
|
||||
detachInterrupt(PMU_IRQ);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the power manager chip
|
||||
*
|
||||
@@ -1368,21 +1437,16 @@ bool Power::axpChipInit()
|
||||
uint64_t pmuIrqMask = 0;
|
||||
|
||||
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
||||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||||
}
|
||||
|
||||
pinMode(PMU_IRQ, INPUT);
|
||||
attachInterrupt(
|
||||
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
||||
|
||||
// we do not look for AXPXXX_CHARGING_FINISHED_IRQ & AXPXXX_CHARGING_IRQ
|
||||
// because it occurs repeatedly while there is no battery also it could cause
|
||||
// inadvertent waking from light sleep just because the battery filled we
|
||||
// don't look for AXPXXX_BATT_REMOVED_IRQ because it occurs repeatedly while
|
||||
// no battery installed we don't look at AXPXXX_VBUS_REMOVED_IRQ because we
|
||||
// don't have anything hooked to vbus
|
||||
// We wake on IRQ, so only enable the IRQs that we care about.
|
||||
// we want USB plug and unplug to update the screen and LED status,
|
||||
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
|
||||
PMU->enableIRQ(pmuIrqMask);
|
||||
|
||||
PMU->clearIrqStatus();
|
||||
@@ -1848,7 +1912,7 @@ class SerialBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
#if defined(EXT_CHRG_DETECT)
|
||||
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
return false;
|
||||
@@ -1857,7 +1921,7 @@ class SerialBatteryLevel : public HasBatteryLevel
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == ext_chrg_detect_value;
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
// by default, we check the battery voltage only
|
||||
@@ -1879,10 +1943,10 @@ SerialBatteryLevel serialBatteryLevel;
|
||||
bool Power::serialBatteryInit()
|
||||
{
|
||||
#ifdef EXT_PWR_DETECT
|
||||
pinMode(EXT_PWR_DETECT, INPUT);
|
||||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||||
#endif
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
pinMode(EXT_CHRG_DETECT, ext_chrg_detect_mode);
|
||||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||||
#endif
|
||||
|
||||
bool result = serialBatteryLevel.runOnce();
|
||||
|
||||
@@ -58,6 +58,35 @@ static bool isPowered()
|
||||
return !isPowerSavingMode && powerStatus && (!powerStatus->getHasBattery() || powerStatus->getHasUSB());
|
||||
}
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
static void t5BacklightOffForSleep()
|
||||
{
|
||||
t5BacklightSetForcedBySleep(true);
|
||||
}
|
||||
|
||||
static void t5BacklightWakeFromSleep()
|
||||
{
|
||||
t5BacklightSetForcedBySleep(false);
|
||||
}
|
||||
|
||||
static void t5BacklightOffForTimeout()
|
||||
{
|
||||
t5BacklightSetForcedByTimeout(true);
|
||||
t5TouchSetForcedByTimeout(true);
|
||||
}
|
||||
|
||||
static void t5BacklightOnFromUserInput()
|
||||
{
|
||||
t5BacklightHandleUserInput();
|
||||
t5TouchHandleUserInput();
|
||||
}
|
||||
#else
|
||||
static void t5BacklightOffForSleep() {}
|
||||
static void t5BacklightWakeFromSleep() {}
|
||||
static void t5BacklightOffForTimeout() {}
|
||||
static void t5BacklightOnFromUserInput() {}
|
||||
#endif
|
||||
|
||||
static void sdsEnter()
|
||||
{
|
||||
LOG_POWERFSM("State: SDS");
|
||||
@@ -87,6 +116,7 @@ static void lsEnter()
|
||||
LOG_POWERFSM("lsEnter begin, ls_secs=%u", config.power.ls_secs);
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
t5BacklightOffForSleep();
|
||||
secsSlept = 0; // How long have we been sleeping this time
|
||||
|
||||
// LOG_INFO("lsEnter end");
|
||||
@@ -159,6 +189,8 @@ static void lsIdle()
|
||||
static void lsExit()
|
||||
{
|
||||
LOG_POWERFSM("State: lsExit");
|
||||
// Lift the light-sleep force-off gate when leaving LS.
|
||||
t5BacklightWakeFromSleep();
|
||||
}
|
||||
|
||||
static void nbEnter()
|
||||
@@ -180,6 +212,8 @@ static void darkEnter()
|
||||
setBluetoothEnable(true);
|
||||
if (screen)
|
||||
screen->setOn(false);
|
||||
// Screen timeout enters DARK; ensure backlight also turns off.
|
||||
t5BacklightOffForTimeout();
|
||||
}
|
||||
|
||||
static void serialEnter()
|
||||
@@ -289,12 +323,13 @@ void PowerFSM_setup()
|
||||
powerFSM.add_transition(&stateNB, &stateNB, EVENT_PACKET_FOR_PHONE, NULL, "Received packet, resetting win wake");
|
||||
|
||||
// Handle press events - note: we ignore button presses when in API mode
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL,
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, t5BacklightOnFromUserInput, "Press");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, t5BacklightOnFromUserInput,
|
||||
"Press"); // reenter On to restart our timers
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, t5BacklightOnFromUserInput,
|
||||
"Press"); // Allow button to work while in serial API
|
||||
|
||||
// Handle critically low power battery by forcing deep sleep
|
||||
@@ -314,11 +349,13 @@ void PowerFSM_setup()
|
||||
powerFSM.add_transition(&stateSERIAL, &stateSHUTDOWN, EVENT_SHUTDOWN, NULL, "Shutdown");
|
||||
|
||||
// Inputbroker
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, NULL, "Input Device");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, NULL, "Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&stateLS, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateNB, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput, "Input Device");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_INPUT, t5BacklightOnFromUserInput,
|
||||
"Input Device"); // restarts the sleep timer
|
||||
powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_INPUT, t5BacklightOnFromUserInput,
|
||||
"Input Device"); // restarts the sleep timer
|
||||
|
||||
powerFSM.add_transition(&stateDARK, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing");
|
||||
powerFSM.add_transition(&stateON, &stateON, EVENT_BLUETOOTH_PAIR, NULL, "Bluetooth pairing");
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel.
|
||||
|
||||
TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including
|
||||
other lora radios.
|
||||
TX_LOG + RX_ALL_LOG = Total air time for a particular meshtastic channel, including
|
||||
other lora radios.
|
||||
|
||||
RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel.
|
||||
*/
|
||||
|
||||
@@ -499,6 +499,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#define MESHTASTIC_EXCLUDE_PKI 1
|
||||
#define MESHTASTIC_EXCLUDE_POWER_FSM 1
|
||||
#define MESHTASTIC_EXCLUDE_TZ 1
|
||||
#define MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH 1
|
||||
#endif
|
||||
|
||||
// Turn off all optional modules
|
||||
|
||||
@@ -415,30 +415,45 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
case INA_ADDR: // Same as SHT2X
|
||||
case INA_ADDR_ALTERNATE:
|
||||
case INA_ADDR_WAVESHARE_UPS:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
LOG_DEBUG("Register MFG_UID: 0x%x", registerValue);
|
||||
if (registerValue == 0x5449) {
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
||||
LOG_DEBUG("Register DIE_UID: 0x%x", registerValue);
|
||||
case INA_ADDR_WAVESHARE_UPS: {
|
||||
uint16_t mfg = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
|
||||
if (registerValue == 0x2260) {
|
||||
LOG_DEBUG("Register MFG_UID: 0x%x", mfg);
|
||||
|
||||
// Only read DIE_UID for vendors we recognize as INA-compatible to avoid
|
||||
// an extra I2C transaction + delay on other devices sharing this address.
|
||||
if (mfg == 0x5449 || mfg == 0x190F) {
|
||||
uint16_t die = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFF), 2);
|
||||
LOG_DEBUG("Register DIE_UID: 0x%x", die);
|
||||
|
||||
// TI INA226 or fully compatible clones (e.g. TPA626)
|
||||
if (mfg == 0x5449 && die == 0x2260) {
|
||||
logFoundDevice("INA226", (uint8_t)addr.address);
|
||||
type = INA226;
|
||||
} else {
|
||||
}
|
||||
// Silergy SQ52201 (INA226-compatible with different IDs)
|
||||
else if (mfg == 0x190F && die == 0x0000) {
|
||||
logFoundDevice("INA226 (SQ52201)", (uint8_t)addr.address);
|
||||
type = INA226;
|
||||
}
|
||||
// TI INA260
|
||||
else if (mfg == 0x5449) {
|
||||
logFoundDevice("INA260", (uint8_t)addr.address);
|
||||
type = INA260;
|
||||
}
|
||||
}
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
} else if (detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
|
||||
if (type == NONE && detectSHT21SerialNumber(i2cBus, (uint8_t)addr.address)) {
|
||||
logFoundDevice("SHTXX (SHT2X)", (uint8_t)addr.address);
|
||||
type = SHTXX;
|
||||
}
|
||||
#endif
|
||||
} else { // Assume INA219 if none of the above ones are found
|
||||
else { // Assume INA219 if none of the above ones are found
|
||||
logFoundDevice("INA219", (uint8_t)addr.address);
|
||||
type = INA219;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case INA3221_ADDR:
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xFE), 2);
|
||||
LOG_DEBUG("Register MFG_UID FE: 0x%x", registerValue);
|
||||
|
||||
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "draw/NotificationRenderer.h"
|
||||
#include "draw/UIRenderer.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_GPS
|
||||
@@ -54,6 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
@@ -69,12 +71,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "target_specific.h"
|
||||
extern MessageStore messageStore;
|
||||
|
||||
#if USE_TFTDISPLAY
|
||||
extern uint16_t TFT_MESH;
|
||||
#else
|
||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||
#endif
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
@@ -109,6 +105,27 @@ namespace graphics
|
||||
// A text message frame + debug frame + all the node infos
|
||||
FrameCallback *normalFrames;
|
||||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static inline void prepareFrameColorRegions()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
clearTFTColorRegions();
|
||||
// Full-frame FrameMono inversion for themes that need it (e.g. light themes).
|
||||
if (isThemeFullFrameInvert()) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::FrameMono, getThemeBodyFg(), getThemeBodyBg(), 0, 0, screen->getWidth(),
|
||||
screen->getHeight());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void updateUiFrame(OLEDDisplayUi *ui)
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
ui->update();
|
||||
}
|
||||
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
||||
|
||||
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
||||
@@ -227,7 +244,7 @@ void Screen::showOverlayBanner(BannerOverlayOptions banner_overlay_options)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
@@ -249,7 +266,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
@@ -273,7 +290,7 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
@@ -296,7 +313,7 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
ui->setTargetFPS(60);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
|
||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
@@ -388,30 +405,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
{
|
||||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||||
|
||||
int32_t rawRGB = uiconfig.screen_rgb_color;
|
||||
|
||||
// Only validate the combined value once
|
||||
if (rawRGB > 0 && rawRGB <= 255255255) {
|
||||
LOG_INFO("Setting screen RGB color to user chosen: 0x%06X", rawRGB);
|
||||
// Extract each component as a normal int first
|
||||
int r = (rawRGB >> 16) & 0xFF;
|
||||
int g = (rawRGB >> 8) & 0xFF;
|
||||
int b = rawRGB & 0xFF;
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
TFT_MESH = COLOR565(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b));
|
||||
}
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
} else if (rawRGB == 0) {
|
||||
LOG_INFO("Setting screen RGB color to TFT_MESH_OVERRIDE: 0x%04X", TFT_MESH_OVERRIDE);
|
||||
// Default to TFT_MESH_OVERRIDE if available
|
||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#endif
|
||||
} else {
|
||||
// Default best readable yellow color
|
||||
LOG_INFO("Setting screen RGB color to default: (255,255,128)");
|
||||
TFT_MESH = COLOR565(255, 255, 128);
|
||||
}
|
||||
|
||||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||||
@@ -475,9 +468,13 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
|
||||
#endif
|
||||
|
||||
#if defined(USE_ST7789)
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
// Keep firmware and ST7789 driver region structs layout-compatible:
|
||||
// we pass `graphics::colorRegions` through a type cast below.
|
||||
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||
#elif defined(USE_ST7796)
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||
#endif
|
||||
|
||||
ui = new OLEDDisplayUi(dispdev);
|
||||
@@ -664,16 +661,16 @@ void Screen::setup()
|
||||
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ST7789) && defined(TFT_MESH)
|
||||
// Apply custom RGB color (e.g. Heltec T114/T190)
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#if defined(USE_ST7789)
|
||||
static_assert(sizeof(graphics::TFTColorRegion) == sizeof(::TFTColorRegion),
|
||||
"graphics::TFTColorRegion layout must match ST7789 TFTColorRegion");
|
||||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFTPalette::White, (::TFTColorRegion *)colorRegions);
|
||||
#endif
|
||||
#if defined(MUZI_BASE)
|
||||
dispdev->delayPoweron = true;
|
||||
#endif
|
||||
#if defined(USE_ST7796) && defined(TFT_MESH)
|
||||
// Custom text color, if defined in variant.h
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFT_MESH);
|
||||
#if defined(USE_ST7796)
|
||||
static_cast<ST7796Spi *>(dispdev)->setRGB(TFTPalette::White);
|
||||
#endif
|
||||
|
||||
// Initialize display and UI system
|
||||
@@ -719,7 +716,7 @@ void Screen::setup()
|
||||
#endif
|
||||
{
|
||||
const char *region = myRegion ? myRegion->name : nullptr;
|
||||
graphics::UIRenderer::drawIconScreen(region, display, state, x, y);
|
||||
graphics::UIRenderer::drawBootIconScreen(region, display, state, x, y);
|
||||
}
|
||||
};
|
||||
ui->setFrames(alertFrames, 1);
|
||||
@@ -759,9 +756,9 @@ void Screen::setup()
|
||||
// Turn on display and trigger first draw
|
||||
handleSetOn(true);
|
||||
graphics::currentResolution = graphics::determineScreenResolution(dispdev->height(), dispdev->width());
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#ifndef USE_EINK
|
||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||||
updateUiFrame(ui); // Some SSD1306 clones drop the first draw, so run twice
|
||||
#endif
|
||||
serialSinceMsec = millis();
|
||||
|
||||
@@ -834,7 +831,7 @@ void Screen::forceDisplay(bool forceUiUpdate)
|
||||
do {
|
||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||
delay(10);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||
|
||||
// Return to normal frame rate
|
||||
@@ -905,9 +902,9 @@ int32_t Screen::runOnce()
|
||||
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
||||
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
||||
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#ifndef USE_EINK
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
#endif
|
||||
showingOEMBootScreen = false;
|
||||
}
|
||||
@@ -998,7 +995,7 @@ int32_t Screen::runOnce()
|
||||
|
||||
// this must be before the frameState == FIXED check, because we always
|
||||
// want to draw at least one FIXED frame before doing forceDisplay
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
|
||||
// Switch to a low framerate (to save CPU) when we are not in transition
|
||||
// but we should only call setTargetFPS when framestate changes, because
|
||||
@@ -1060,7 +1057,7 @@ void Screen::setSSLFrames()
|
||||
// LOG_DEBUG("Show SSL frames");
|
||||
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
||||
ui->setFrames(sslFrames, 1);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1096,7 +1093,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||||
do {
|
||||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||||
delay(1);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||||
|
||||
#if defined(USE_EINK_PARALLELDISPLAY)
|
||||
@@ -1471,9 +1468,15 @@ void Screen::blink()
|
||||
dispdev->setBrightness(254);
|
||||
while (count > 0) {
|
||||
dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight());
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
delay(50);
|
||||
dispdev->clear();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
delay(50);
|
||||
count = count - 1;
|
||||
@@ -1607,6 +1610,9 @@ void Screen::setFastFramerate()
|
||||
{
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
dispdev->clear();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
prepareFrameColorRegions();
|
||||
#endif
|
||||
dispdev->display();
|
||||
#endif
|
||||
// We are about to start a transition so speed up fps
|
||||
@@ -1818,7 +1824,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1833,7 +1839,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
setFastFramerate(); // Draw ASAP
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
|
||||
menuHandler::handleMenuSwitch(dispdev);
|
||||
return 0;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "RTC.h"
|
||||
#include "draw/NodeListRenderer.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "main.h"
|
||||
#include "meshtastic/config.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "power.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <cctype>
|
||||
#include <graphics/images.h>
|
||||
|
||||
namespace graphics
|
||||
@@ -65,6 +69,12 @@ uint32_t lastBlinkShared = 0;
|
||||
bool isMailIconVisible = true;
|
||||
uint32_t lastMailBlink = 0;
|
||||
|
||||
static inline bool useClockHeaderAccentTheme(uint32_t themeId)
|
||||
{
|
||||
return themeId == ThemeID::Pink || themeId == ThemeID::Creamsicle || themeId == ThemeID::MeshtasticGreen ||
|
||||
themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||
}
|
||||
|
||||
// *********************************
|
||||
// * Rounded Header when inverted *
|
||||
// *********************************
|
||||
@@ -85,7 +95,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
||||
// *************************
|
||||
// * Common Header Drawing *
|
||||
// *************************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date,
|
||||
bool transparent_background, bool use_title_color_override, uint16_t title_color_override)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
@@ -100,30 +111,93 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
const int headerHeight = highlightHeight + 2;
|
||||
const uint16_t headerColorForRoles = getThemeHeaderBg();
|
||||
// Color TFT headers use a fixed dark background + white glyphs.
|
||||
// Keep legacy inverted bitmap behavior only for monochrome displays.
|
||||
const bool useInvertedHeaderStyle = (isInverted && !force_no_invert && !isTFTColoringEnabled() && !transparent_background);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
int statusLeftEndX = 0;
|
||||
int statusRightStartX = screenW;
|
||||
const bool isClockHeader = transparent_background && show_date && (!titleStr || titleStr[0] == '\0');
|
||||
const auto activeThemeId = getActiveTheme().id;
|
||||
const bool useClockHeaderAccent = isClockHeader && useClockHeaderAccentTheme(activeThemeId);
|
||||
#endif
|
||||
|
||||
{
|
||||
const uint16_t headerColor = getThemeHeaderBg();
|
||||
const uint16_t headerTextColor = getThemeHeaderText();
|
||||
const uint16_t headerTitleColorForRole = use_title_color_override ? title_color_override : headerTextColor;
|
||||
uint16_t headerStatusColor = getThemeHeaderStatus();
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Clock frame uses transparent header + date + empty title.
|
||||
// For accent clock themes (Pink/Creamsicle + classic monochrome), tint
|
||||
// status items (battery outline, %, date, mail icon) to the header accent.
|
||||
if (useClockHeaderAccent) {
|
||||
headerStatusColor = getThemeHeaderBg();
|
||||
}
|
||||
|
||||
if (transparent_background) {
|
||||
// Transparent clock headers should inherit whatever body off-color is
|
||||
// already active under the header (important for light/inverted themes).
|
||||
const uint16_t transparentBgColor = resolveTFTOffColorAt(0, headerHeight + 1, getThemeBodyBg());
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, transparentBgColor, transparentBgColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, transparentBgColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, transparentBgColor);
|
||||
} else if (useInvertedHeaderStyle) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, headerColor, TFTPalette::Black, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerColor, headerTitleColorForRole);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerColor, headerStatusColor);
|
||||
} else {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::HeaderBackground, TFTPalette::Black, headerColor, 0, 0, screenW,
|
||||
headerHeight);
|
||||
setTFTColorRole(TFTColorRole::HeaderTitle, headerTitleColorForRole, headerColor);
|
||||
setTFTColorRole(TFTColorRole::HeaderStatus, headerStatusColor, headerColor);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!force_no_invert) {
|
||||
// === Inverted Header Background ===
|
||||
if (isInverted) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->fillRect(0, 0, screenW, headerHeight);
|
||||
display->setColor(WHITE);
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
display->fillRect(0, 0, screenW, headerHeight);
|
||||
// Keep the legacy white separator for monochrome displays only when header background is visible.
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (!transparent_background) {
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (transparent_background) {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// TFT role coloring expects foreground glyph bits to be "set".
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
|
||||
// === Screen Title ===
|
||||
const char *headerTitle = titleStr ? titleStr : "";
|
||||
const int titleWidth = UIRenderer::measureStringWithEmotes(display, headerTitle);
|
||||
const int titleX = (SCREEN_WIDTH - titleWidth) / 2;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const int titleRegionWidth = titleWidth + (config.display.heading_bold ? 3 : 2);
|
||||
registerTFTColorRegion(TFTColorRole::HeaderTitle, titleX - 1, y, titleRegionWidth, FONT_HEIGHT_SMALL);
|
||||
#endif
|
||||
UIRenderer::drawStringWithEmotes(display, titleX, y, headerTitle, FONT_HEIGHT_SMALL, 1, config.display.heading_bold);
|
||||
}
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@@ -152,6 +226,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
|
||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
bool hasBatteryFillRegion = false;
|
||||
int16_t batteryFillRegionX = 0;
|
||||
int16_t batteryFillRegionY = 0;
|
||||
int16_t batteryFillRegionW = 0;
|
||||
int16_t batteryFillRegionH = 0;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t batteryFillColor = getThemeBatteryFillColor(chargePercent);
|
||||
if (useClockHeaderAccent) {
|
||||
batteryFillColor = getThemeHeaderBg();
|
||||
}
|
||||
#endif
|
||||
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
@@ -180,6 +265,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||
int fillWidth = 14 * chargePercent / 100;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (fillWidth > 0) {
|
||||
hasBatteryFillRegion = true;
|
||||
batteryFillRegionX = batteryX + 1;
|
||||
batteryFillRegionY = batteryY + 1;
|
||||
batteryFillRegionW = fillWidth;
|
||||
batteryFillRegionH = 11;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
batteryX += 18; // Icon + 2 pixels
|
||||
} else {
|
||||
@@ -194,21 +288,41 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (fillHeight > 0) {
|
||||
hasBatteryFillRegion = true;
|
||||
batteryFillRegionX = batteryX + 1;
|
||||
batteryFillRegionY = fillY + 10;
|
||||
batteryFillRegionW = 5;
|
||||
batteryFillRegionH = fillHeight;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
batteryX += 9; // Icon + 2 pixels
|
||||
}
|
||||
}
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = batteryX + 2;
|
||||
#endif
|
||||
|
||||
if (chargePercent != 101) {
|
||||
// === Battery % Display ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
const int percentWidth = display->getStringWidth("%");
|
||||
const int percentX = batteryX + chargeNumWidth - 1;
|
||||
display->drawString(batteryX, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||||
display->drawString(percentX, textY, "%");
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = percentX + percentWidth + 2;
|
||||
#endif
|
||||
if (isBold) {
|
||||
display->drawString(batteryX + 1, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||||
display->drawString(percentX + 1, textY, "%");
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusLeftEndX = percentX + percentWidth + 3;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +367,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
}
|
||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusRightStartX = timeX - (useHorizontalBattery ? 22 : 16);
|
||||
#endif
|
||||
|
||||
// === Show Mail or Mute Icon to the Left of Time ===
|
||||
int iconRightEdge = timeX - 2;
|
||||
@@ -278,7 +395,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -293,7 +410,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
} else {
|
||||
int iconX = iconRightEdge - (mail_width - 2);
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -309,7 +426,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -323,7 +440,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
if (useInvertedHeaderStyle) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(BLACK);
|
||||
@@ -351,7 +468,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
} else {
|
||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||
int iconRightEdge = screenW - xOffset;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
statusRightStartX = screenW - (useHorizontalBattery ? 22 : 12);
|
||||
#endif
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
@@ -393,6 +512,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTColorRegion(TFTColorRole::HeaderStatus, 0, 0, statusLeftEndX, headerHeight);
|
||||
if (statusRightStartX < screenW) {
|
||||
registerTFTColorRegion(TFTColorRole::HeaderStatus, statusRightStartX, 0, screenW - statusRightStartX, headerHeight);
|
||||
}
|
||||
if (hasBatteryFillRegion) {
|
||||
registerTFTColorRegionDirect(batteryFillRegionX, batteryFillRegionY, batteryFillRegionW, batteryFillRegionH,
|
||||
batteryFillColor, headerColorForRoles);
|
||||
}
|
||||
#endif
|
||||
display->setColor(WHITE); // Reset for other UI
|
||||
}
|
||||
@@ -430,14 +559,23 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
return;
|
||||
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
const int footerY = SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale);
|
||||
const int footerH = (connection_icon_height * scale) + (2 * scale);
|
||||
const int iconX = 0;
|
||||
const int iconY = SCREEN_HEIGHT - (connection_icon_height * scale);
|
||||
const int iconW = connection_icon_width * scale;
|
||||
const int iconH = connection_icon_height * scale;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Only tint the link glyph itself on TFT; keep the footer background black.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ConnectionIcon, TFTPalette::Blue, TFTPalette::Black, iconX, iconY, iconW, iconH);
|
||||
#endif
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
@@ -451,65 +589,127 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||
connection_icon);
|
||||
display->drawXbm(iconX, iconY, connection_icon_width, connection_icon_height, connection_icon);
|
||||
}
|
||||
}
|
||||
|
||||
bool isAllowedPunctuation(char c)
|
||||
{
|
||||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||
return allowed.find(c) != std::string::npos;
|
||||
switch (c) {
|
||||
case '.':
|
||||
case ',':
|
||||
case '!':
|
||||
case '?':
|
||||
case ';':
|
||||
case ':':
|
||||
case '-':
|
||||
case '_':
|
||||
case '(':
|
||||
case ')':
|
||||
case '[':
|
||||
case ']':
|
||||
case '{':
|
||||
case '}':
|
||||
case '\'':
|
||||
case '"':
|
||||
case '@':
|
||||
case '#':
|
||||
case '$':
|
||||
case '/':
|
||||
case '\\':
|
||||
case '&':
|
||||
case '+':
|
||||
case '=':
|
||||
case '%':
|
||||
case '~':
|
||||
case '^':
|
||||
case ' ':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
||||
static inline size_t utf8CodePointLength(unsigned char lead)
|
||||
{
|
||||
if (from.empty())
|
||||
return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.size(), to);
|
||||
pos += to.size();
|
||||
if ((lead & 0x80) == 0x00) {
|
||||
return 1;
|
||||
}
|
||||
if ((lead & 0xE0) == 0xC0) {
|
||||
return 2;
|
||||
}
|
||||
if ((lead & 0xF0) == 0xE0) {
|
||||
return 3;
|
||||
}
|
||||
if ((lead & 0xF8) == 0xF0) {
|
||||
return 4;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string sanitizeString(const std::string &input)
|
||||
{
|
||||
static constexpr char kReplacementChar = static_cast<char>(0xBF); // Inverted question mark in ISO-8859-1.
|
||||
std::string output;
|
||||
output.reserve(input.size());
|
||||
bool inReplacement = false;
|
||||
|
||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
||||
std::string s = input;
|
||||
|
||||
// Curly single quotes: ‘ ’
|
||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
||||
|
||||
// Curly double quotes: “ ”
|
||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
||||
|
||||
// En dash / Em dash: – —
|
||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
||||
|
||||
// Non-breaking space
|
||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
||||
|
||||
// Now do your original sanitize pass over the normalized string.
|
||||
for (unsigned char uc : s) {
|
||||
char c = static_cast<char>(uc);
|
||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
||||
output += c;
|
||||
inReplacement = false;
|
||||
} else {
|
||||
const size_t inputSize = input.size();
|
||||
size_t i = 0;
|
||||
while (i < inputSize) {
|
||||
const unsigned char byte0 = static_cast<unsigned char>(input[i]);
|
||||
char normalized = '\0';
|
||||
size_t consumed = 0;
|
||||
if (byte0 < 0x80) {
|
||||
normalized = static_cast<char>(byte0);
|
||||
consumed = 1;
|
||||
} else if ((i + 2) < inputSize && byte0 == 0xE2 && static_cast<unsigned char>(input[i + 1]) == 0x80) {
|
||||
// Smart punctuation: ' ' \" \" - -
|
||||
switch (static_cast<unsigned char>(input[i + 2])) {
|
||||
case 0x98:
|
||||
case 0x99:
|
||||
normalized = '\'';
|
||||
consumed = 3;
|
||||
break;
|
||||
case 0x9C:
|
||||
case 0x9D:
|
||||
normalized = '\"';
|
||||
consumed = 3;
|
||||
break;
|
||||
case 0x93:
|
||||
case 0x94:
|
||||
normalized = '-';
|
||||
consumed = 3;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if ((i + 1) < inputSize && byte0 == 0xC2 && static_cast<unsigned char>(input[i + 1]) == 0xA0) {
|
||||
// Non-breaking space.
|
||||
normalized = ' ';
|
||||
consumed = 2;
|
||||
}
|
||||
if (consumed == 0) {
|
||||
size_t seqLen = utf8CodePointLength(byte0);
|
||||
if (seqLen > (inputSize - i)) {
|
||||
seqLen = 1;
|
||||
}
|
||||
if (!inReplacement) {
|
||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
||||
output.push_back(kReplacementChar);
|
||||
inReplacement = true;
|
||||
}
|
||||
i += seqLen;
|
||||
continue;
|
||||
}
|
||||
const unsigned char normalizedUc = static_cast<unsigned char>(normalized);
|
||||
if (std::isalnum(normalizedUc) || isAllowedPunctuation(normalized)) {
|
||||
output.push_back(normalized);
|
||||
inReplacement = false;
|
||||
} else if (!inReplacement) {
|
||||
output.push_back(kReplacementChar);
|
||||
inReplacement = true;
|
||||
}
|
||||
i += consumed;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
@@ -52,7 +53,8 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||
bool show_date = false);
|
||||
bool show_date = false, bool transparent_background = false, bool use_title_color_override = false,
|
||||
uint16_t title_color_override = 0);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
819
src/graphics/TFTColorRegions.cpp
Normal file
819
src/graphics/TFTColorRegions.cpp
Normal file
@@ -0,0 +1,819 @@
|
||||
#include "TFTColorRegions.h"
|
||||
#include "NodeDB.h"
|
||||
#include "TFTPalette.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct TFTRoleColorsBe {
|
||||
uint16_t onColorBe;
|
||||
uint16_t offColorBe;
|
||||
};
|
||||
|
||||
static uint8_t colorRegionCount = 0;
|
||||
static constexpr uint32_t kFnv1aOffsetBasis = 2166136261u;
|
||||
static constexpr uint32_t kFnv1aPrime = 16777619u;
|
||||
|
||||
static constexpr uint16_t toBe565(uint16_t color)
|
||||
{
|
||||
return static_cast<uint16_t>((color >> 8) | (color << 8));
|
||||
}
|
||||
|
||||
static constexpr bool kRoleIsBody[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||
false, // HeaderBackground
|
||||
false, // HeaderTitle
|
||||
false, // HeaderStatus
|
||||
true, // SignalBars
|
||||
true, // ConnectionIcon
|
||||
true, // UtilizationFill
|
||||
true, // FavoriteNode
|
||||
true, // ActionMenuBorder
|
||||
true, // ActionMenuBody
|
||||
true, // ActionMenuTitle
|
||||
true, // FrameMono
|
||||
false, // BootSplash
|
||||
true, // FavoriteNodeBGHighlight
|
||||
false, // NavigationBar
|
||||
false // NavigationArrow
|
||||
};
|
||||
|
||||
static inline bool isBodyColorRole(TFTColorRole role)
|
||||
{
|
||||
return kRoleIsBody[static_cast<size_t>(role)];
|
||||
}
|
||||
|
||||
static inline bool isMonochromeTheme(uint32_t themeId)
|
||||
{
|
||||
return themeId == ThemeID::MeshtasticGreen || themeId == ThemeID::ClassicRed || themeId == ThemeID::MonochromeWhite;
|
||||
}
|
||||
|
||||
static inline uint16_t getMonochromeAccent(uint32_t themeId)
|
||||
{
|
||||
return (themeId == ThemeID::MeshtasticGreen) ? TFTPalette::MeshtasticGreen
|
||||
: (themeId == ThemeID::ClassicRed) ? TFTPalette::ClassicRed
|
||||
: TFTPalette::White;
|
||||
}
|
||||
|
||||
static inline void replaceColor(uint16_t &value, uint16_t from, uint16_t to)
|
||||
{
|
||||
if (value == from) {
|
||||
value = to;
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint32_t fnv1aAppendByte(uint32_t hash, uint8_t value)
|
||||
{
|
||||
return (hash ^ value) * kFnv1aPrime;
|
||||
}
|
||||
|
||||
static inline uint32_t fnv1aAppendU16(uint32_t hash, uint16_t value)
|
||||
{
|
||||
hash = fnv1aAppendByte(hash, static_cast<uint8_t>(value & 0xFF));
|
||||
hash = fnv1aAppendByte(hash, static_cast<uint8_t>((value >> 8) & 0xFF));
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Compile-time header color overrides (backward-compatible)
|
||||
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kHeaderBackground = TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kHeaderBackground = TFTPalette::DarkGray;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kTitleColor = TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kTitleColor = TFTPalette::White;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||
static constexpr uint16_t kStatusColor = TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||
#else
|
||||
static constexpr uint16_t kStatusColor = TFTPalette::White;
|
||||
#endif
|
||||
|
||||
// Theme definitions
|
||||
// Stored in kThemes[] and looked up by matching uiconfig.screen_rgb_color
|
||||
// against each entry's .uniqueIdentifier field.
|
||||
|
||||
static const TFTThemeDef kThemes[] = {
|
||||
|
||||
// Default Dark (ThemeID::DefaultDark = 0)
|
||||
{
|
||||
ThemeID::DefaultDark, // id
|
||||
"Default Dark", // name
|
||||
0, // uniqueIdentifier
|
||||
// roles[TFTColorRole::Count]
|
||||
{
|
||||
{kHeaderBackground, TFTPalette::Black}, // HeaderBackground
|
||||
{kHeaderBackground, kTitleColor}, // HeaderTitle
|
||||
{kHeaderBackground, kStatusColor}, // HeaderStatus
|
||||
{TFTPalette::Good, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::Blue, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::Good, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::Yellow, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{kStatusColor, kHeaderBackground}, // NavigationBar (icon fg, bar bg)
|
||||
{kTitleColor, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::Good, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
false, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Default Light (ThemeID::DefaultLight = 1)
|
||||
{
|
||||
ThemeID::DefaultLight, // id
|
||||
"Default Light", // name
|
||||
1, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::LightGray, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::Good, TFTPalette::White}, // SignalBars
|
||||
{TFTPalette::Blue, TFTPalette::White}, // ConnectionIcon
|
||||
{TFTPalette::Good, TFTPalette::White}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNode
|
||||
{TFTPalette::DarkGray, TFTPalette::White}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::White}, // ActionMenuBody
|
||||
{TFTPalette::DarkGray, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::Yellow}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::Black, TFTPalette::LightGray}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::Black, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::Good, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Christmas (ThemeID::Christmas = 2)
|
||||
{
|
||||
ThemeID::Christmas, // id
|
||||
"Christmas", // name
|
||||
2, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderTitle
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Gold}, // HeaderStatus
|
||||
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // SignalBars
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // ConnectionIcon
|
||||
{TFTPalette::ChristmasGreen, TFTPalette::Pine}, // UtilizationFill
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNode
|
||||
{TFTPalette::ChristmasRed, TFTPalette::Pine}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Pine}, // ActionMenuBody
|
||||
{TFTPalette::ChristmasRed, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Pine, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::ChristmasRed}, // BootSplash
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::Gold, TFTPalette::ChristmasRed}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::Gold, TFTPalette::Pine}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::ChristmasGreen, // batteryFillGood
|
||||
TFTPalette::Gold, // batteryFillMedium
|
||||
TFTPalette::ChristmasRed, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
false, // visible
|
||||
},
|
||||
|
||||
// Pink (ThemeID::Pink = 3) light variant
|
||||
{
|
||||
ThemeID::Pink, // id
|
||||
"Pink", // name
|
||||
3, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::HotPink, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // HeaderStatus
|
||||
{TFTPalette::DeepPink, TFTPalette::PalePink}, // SignalBars
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // ConnectionIcon
|
||||
{TFTPalette::DeepPink, TFTPalette::PalePink}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNode
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::PalePink}, // ActionMenuBody
|
||||
{TFTPalette::HotPink, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::HotPink}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::HotPink}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::HotPink}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::HotPink, TFTPalette::PalePink}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::DeepPink, // batteryFillGood
|
||||
TFTPalette::HotPink, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Blue (ThemeID::Blue = 4) dark variant
|
||||
{
|
||||
ThemeID::Blue, // id
|
||||
"Blue", // name
|
||||
4, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::DeepBlue, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::DeepBlue, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::DeepBlue, TFTPalette::SkyBlue}, // HeaderStatus
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // SignalBars
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // ConnectionIcon
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // UtilizationFill
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNode
|
||||
{TFTPalette::DeepBlue, TFTPalette::Navy}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Navy}, // ActionMenuBody
|
||||
{TFTPalette::DeepBlue, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Navy, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::DeepBlue}, // BootSplash
|
||||
{TFTPalette::SkyBlue, TFTPalette::Navy}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::SkyBlue, TFTPalette::DeepBlue}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::SkyBlue, TFTPalette::Black}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::SkyBlue, // batteryFillGood
|
||||
TFTPalette::Medium, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Creamsicle (ThemeID::Creamsicle = 5)light variant
|
||||
{
|
||||
ThemeID::Creamsicle, // id
|
||||
"Creamsicle", // name
|
||||
5, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::CreamOrange, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderTitle
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // HeaderStatus
|
||||
{TFTPalette::DeepOrange, TFTPalette::Cream}, // SignalBars
|
||||
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ConnectionIcon
|
||||
{TFTPalette::DeepOrange, TFTPalette::Cream}, // UtilizationFill
|
||||
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNode
|
||||
{TFTPalette::CreamOrange, TFTPalette::Cream}, // ActionMenuBorder
|
||||
{TFTPalette::Black, TFTPalette::Cream}, // ActionMenuBody
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono
|
||||
{TFTPalette::White, TFTPalette::CreamOrange}, // BootSplash
|
||||
{TFTPalette::Black, TFTPalette::CreamOrange}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::CreamOrange}, // NavigationBar (icon fg, bar bg)
|
||||
{TFTPalette::CreamOrange, TFTPalette::White}, // NavigationArrow (arrow fg, body bg)
|
||||
},
|
||||
TFTPalette::DeepOrange, // batteryFillGood
|
||||
TFTPalette::Gold, // batteryFillMedium
|
||||
TFTPalette::Bad, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Meshtastic Green (ThemeID::MeshtasticGreen = 6) classic monochrome
|
||||
// Pure single-color-on-black look. Every role maps foreground pixels to
|
||||
// the theme color and background pixels to Black.
|
||||
{
|
||||
ThemeID::MeshtasticGreen, // id
|
||||
"Meshtastic Green", // name
|
||||
6, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::MeshtasticGreen}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::MeshtasticGreen, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Classic Red (ThemeID::ClassicRed = 7) classic monochrome
|
||||
{
|
||||
ThemeID::ClassicRed, // id
|
||||
"Classic Red", // name
|
||||
7, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::ClassicRed}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::ClassicRed, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
|
||||
// Monochrome White (ThemeID::MonochromeWhite = 8) classic monochrome
|
||||
{
|
||||
ThemeID::MonochromeWhite, // id
|
||||
"Monochrome White", // name
|
||||
8, // uniqueIdentifier
|
||||
{
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderBackground
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderTitle
|
||||
{TFTPalette::White, TFTPalette::Black}, // HeaderStatus
|
||||
{TFTPalette::White, TFTPalette::Black}, // SignalBars
|
||||
{TFTPalette::White, TFTPalette::Black}, // ConnectionIcon
|
||||
{TFTPalette::White, TFTPalette::Black}, // UtilizationFill
|
||||
{TFTPalette::White, TFTPalette::Black}, // FavoriteNode
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBorder
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuBody
|
||||
{TFTPalette::White, TFTPalette::Black}, // ActionMenuTitle
|
||||
{TFTPalette::Black, TFTPalette::White}, // FrameMono (bodyBg, bodyFg)
|
||||
{TFTPalette::White, TFTPalette::Black}, // BootSplash
|
||||
{TFTPalette::White, TFTPalette::Black}, // FavoriteNodeBGHighlight
|
||||
{TFTPalette::White, TFTPalette::Black}, // NavigationBar
|
||||
{TFTPalette::White, TFTPalette::Black}, // NavigationArrow
|
||||
},
|
||||
TFTPalette::Black, // batteryFillGood
|
||||
TFTPalette::Black, // batteryFillMedium
|
||||
TFTPalette::Black, // batteryFillBad
|
||||
true, // fullFrameInvert
|
||||
true, // visible
|
||||
},
|
||||
};
|
||||
|
||||
static constexpr size_t kInternalThemeCount = sizeof(kThemes) / sizeof(kThemes[0]);
|
||||
|
||||
// Resolve the kThemes[] index for the currently persisted theme. Called at
|
||||
// boot (indirectly via getActiveTheme()) and whenever the active theme is
|
||||
// queried, so uiconfig.screen_rgb_color remains the single source of truth.
|
||||
// Matches against .uniqueIdentifier - that's the field whose value is stored
|
||||
// in the user's config. Falls back to 0 (DefaultDark) if no match is found,
|
||||
// which gracefully handles removed or retired themes.
|
||||
static inline size_t resolveThemeIndex()
|
||||
{
|
||||
const uint32_t savedIdentifier = uiconfig.screen_rgb_color & 0x1F;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (kThemes[i].uniqueIdentifier == savedIdentifier)
|
||||
return i;
|
||||
}
|
||||
return 0; // Default Dark fallback
|
||||
}
|
||||
|
||||
static inline bool normalizeRegion(int16_t &x, int16_t &y, int16_t &width, int16_t &height)
|
||||
{
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
width += x;
|
||||
x = 0;
|
||||
}
|
||||
if (y < 0) {
|
||||
height += y;
|
||||
y = 0;
|
||||
}
|
||||
|
||||
return width > 0 && height > 0;
|
||||
}
|
||||
|
||||
static inline void appendColorRegion(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColorBe, uint16_t offColorBe)
|
||||
{
|
||||
// Keep the last slot permanently disabled as a sentinel for ST7789 scans.
|
||||
// This leaves MAX_TFT_COLOR_REGIONS - 1 usable entries.
|
||||
if (colorRegionCount >= MAX_TFT_COLOR_REGIONS - 1) {
|
||||
memmove(&colorRegions[0], &colorRegions[1], sizeof(TFTColorRegion) * (MAX_TFT_COLOR_REGIONS - 2));
|
||||
colorRegionCount = MAX_TFT_COLOR_REGIONS - 2;
|
||||
}
|
||||
|
||||
TFTColorRegion ®ion = colorRegions[colorRegionCount++];
|
||||
region.x = x;
|
||||
region.y = y;
|
||||
region.width = width;
|
||||
region.height = height;
|
||||
region.onColorBe = onColorBe;
|
||||
region.offColorBe = offColorBe;
|
||||
region.enabled = true;
|
||||
|
||||
// Keep one disabled sentinel after the active range for ST7789 countColorRegions().
|
||||
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||
colorRegions[colorRegionCount].enabled = false;
|
||||
}
|
||||
colorRegions[MAX_TFT_COLOR_REGIONS - 1].enabled = false;
|
||||
}
|
||||
|
||||
// Current working role colors (big-endian). Initialised to Dark defaults;
|
||||
// call loadThemeDefaults() after boot / theme change to refresh.
|
||||
static TFTRoleColorsBe roleColors[static_cast<size_t>(TFTColorRole::Count)] = {
|
||||
{toBe565(kHeaderBackground), toBe565(TFTPalette::Black)}, // HeaderBackground
|
||||
{toBe565(kHeaderBackground), toBe565(kTitleColor)}, // HeaderTitle
|
||||
{toBe565(kHeaderBackground), toBe565(kStatusColor)}, // HeaderStatus
|
||||
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // SignalBars
|
||||
{toBe565(TFTPalette::Blue), toBe565(TFTPalette::Black)}, // ConnectionIcon
|
||||
{toBe565(TFTPalette::Good), toBe565(TFTPalette::Black)}, // UtilizationFill
|
||||
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNode
|
||||
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::Black)}, // ActionMenuBorder
|
||||
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // ActionMenuBody
|
||||
{toBe565(TFTPalette::DarkGray), toBe565(TFTPalette::White)}, // ActionMenuTitle
|
||||
{toBe565(TFTPalette::Black), toBe565(TFTPalette::White)}, // FrameMono
|
||||
{toBe565(TFTPalette::White), toBe565(TFTPalette::Black)}, // BootSplash
|
||||
{toBe565(TFTPalette::Yellow), toBe565(TFTPalette::Black)}, // FavoriteNodeBGHighlight
|
||||
{toBe565(kStatusColor), toBe565(kHeaderBackground)}, // NavigationBar
|
||||
{toBe565(kTitleColor), toBe565(TFTPalette::Black)} // NavigationArrow
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Theme accessors
|
||||
|
||||
const TFTThemeDef &getActiveTheme()
|
||||
{
|
||||
return kThemes[resolveThemeIndex()];
|
||||
}
|
||||
|
||||
// Visible-theme accessors
|
||||
// These iterate only themes flagged .visible = true, preserving kThemes[]
|
||||
// order. Menu code should use these so hidden themes don't appear in the
|
||||
// picker while still applying correctly if their ID is persisted.
|
||||
|
||||
size_t getVisibleThemeCount()
|
||||
{
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (kThemes[i].visible)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex)
|
||||
{
|
||||
size_t seen = 0;
|
||||
for (size_t i = 0; i < kInternalThemeCount; i++) {
|
||||
if (!kThemes[i].visible)
|
||||
continue;
|
||||
if (seen == visibleIndex)
|
||||
return kThemes[i];
|
||||
seen++;
|
||||
}
|
||||
// Fallback: return first theme (never trust a bad index).
|
||||
return kThemes[0];
|
||||
}
|
||||
|
||||
size_t getActiveVisibleThemeIndex()
|
||||
{
|
||||
const size_t active = resolveThemeIndex();
|
||||
if (!kThemes[active].visible)
|
||||
return SIZE_MAX;
|
||||
size_t visibleIdx = 0;
|
||||
for (size_t i = 0; i < active; i++) {
|
||||
if (kThemes[i].visible)
|
||||
visibleIdx++;
|
||||
}
|
||||
return visibleIdx;
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderBg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_BG_COLOR_OVERRIDE
|
||||
return TFT_HEADER_BG_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderBackground)].onColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::DarkGray;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderText()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_TITLE_COLOR_OVERRIDE
|
||||
return TFT_HEADER_TITLE_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderTitle)].offColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeHeaderStatus()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
#ifdef TFT_HEADER_STATUS_COLOR_OVERRIDE
|
||||
return TFT_HEADER_STATUS_COLOR_OVERRIDE;
|
||||
#else
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::HeaderStatus)].offColor;
|
||||
#endif
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBodyBg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].onColor;
|
||||
#else
|
||||
return TFTPalette::Black;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBodyFg()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].roles[static_cast<size_t>(TFTColorRole::FrameMono)].offColor;
|
||||
#else
|
||||
return TFTPalette::White;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isThemeFullFrameInvert()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return kThemes[resolveThemeIndex()].fullFrameInvert;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t getThemeBatteryFillColor(int batteryPercent)
|
||||
{
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
if (batteryPercent <= 20) {
|
||||
return theme.batteryFillBad;
|
||||
}
|
||||
if (batteryPercent <= 50) {
|
||||
return theme.batteryFillMedium;
|
||||
}
|
||||
return theme.batteryFillGood;
|
||||
}
|
||||
|
||||
void loadThemeDefaults()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
for (uint8_t i = 0; i < static_cast<uint8_t>(TFTColorRole::Count); i++) {
|
||||
roleColors[i].onColorBe = toBe565(theme.roles[i].onColor);
|
||||
roleColors[i].offColorBe = toBe565(theme.roles[i].offColor);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Role color assignment with theme-aware transforms
|
||||
|
||||
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
const uint8_t index = static_cast<uint8_t>(role);
|
||||
if (index >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t themeId = uiconfig.screen_rgb_color & 0x1F;
|
||||
const bool isHighlightRole = (role == TFTColorRole::FavoriteNode || role == TFTColorRole::FavoriteNodeBGHighlight);
|
||||
const bool isBodyRole = !isHighlightRole && isBodyColorRole(role);
|
||||
|
||||
// Classic monochrome themes collapse all non-black accents into one tone.
|
||||
if (isMonochromeTheme(themeId)) {
|
||||
if (onColor != TFTPalette::Black) {
|
||||
onColor = getMonochromeAccent(themeId);
|
||||
}
|
||||
} else {
|
||||
switch (themeId) {
|
||||
case ThemeID::DefaultLight:
|
||||
if (isHighlightRole) {
|
||||
// High-contrast highlight chips on light UI.
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::Yellow;
|
||||
} else if (isBodyRole) {
|
||||
// Invert body colors for readability on white frames.
|
||||
if (offColor == TFTPalette::Black && role != TFTColorRole::ActionMenuTitle) {
|
||||
offColor = TFTPalette::White;
|
||||
}
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Christmas:
|
||||
if (isHighlightRole || isBodyRole) {
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::Gold);
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Pine);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Pink:
|
||||
if (isHighlightRole) {
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::HotPink;
|
||||
} else if (isBodyRole) {
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::PalePink);
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepPink);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Creamsicle:
|
||||
if (isHighlightRole) {
|
||||
onColor = TFTPalette::Black;
|
||||
offColor = TFTPalette::CreamOrange;
|
||||
} else if (isBodyRole) {
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Cream);
|
||||
replaceColor(onColor, TFTPalette::White, TFTPalette::Black);
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::DeepOrange);
|
||||
}
|
||||
break;
|
||||
case ThemeID::Blue:
|
||||
if (isHighlightRole || isBodyRole) {
|
||||
replaceColor(onColor, TFTPalette::Yellow, TFTPalette::SkyBlue);
|
||||
replaceColor(offColor, TFTPalette::Black, TFTPalette::Navy);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
roleColors[index].onColorBe = toBe565(onColor);
|
||||
roleColors[index].offColorBe = toBe565(offColor);
|
||||
}
|
||||
|
||||
// Region registration
|
||||
|
||||
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
const uint8_t roleIndex = static_cast<uint8_t>(role);
|
||||
if (roleIndex >= static_cast<uint8_t>(TFTColorRole::Count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizeRegion(x, y, width, height)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TFTRoleColorsBe &colors = roleColors[roleIndex];
|
||||
appendColorRegion(x, y, width, height, colors.onColorBe, colors.offColorBe);
|
||||
}
|
||||
|
||||
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||
int16_t height)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)role;
|
||||
(void)onColor;
|
||||
(void)offColor;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)width;
|
||||
(void)height;
|
||||
return;
|
||||
#else
|
||||
setTFTColorRole(role, onColor, offColor);
|
||||
registerTFTColorRegion(role, x, y, width, height);
|
||||
#endif
|
||||
}
|
||||
|
||||
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (!normalizeRegion(x, y, width, height))
|
||||
return;
|
||||
|
||||
appendColorRegion(x, y, width, height, toBe565(onColor), toBe565(offColor));
|
||||
}
|
||||
|
||||
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)boxLeft;
|
||||
(void)boxTop;
|
||||
(void)boxWidth;
|
||||
(void)boxHeight;
|
||||
return;
|
||||
#else
|
||||
// Use theme-appropriate menu colors.
|
||||
const TFTThemeDef &theme = kThemes[resolveThemeIndex()];
|
||||
const TFTThemeRoleColor &menuBody = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBody)];
|
||||
const TFTThemeRoleColor &menuBorder = theme.roles[static_cast<size_t>(TFTColorRole::ActionMenuBorder)];
|
||||
|
||||
// Fill role includes a 1px shadow guard so stale frame edges are overwritten uniformly.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBody, menuBody.onColor, menuBody.offColor, boxLeft - 1, boxTop - 1,
|
||||
boxWidth + 2, boxHeight + 2);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop - 2, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft - 2, boxTop, 1, boxHeight);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuBorder, menuBorder.onColor, menuBorder.offColor, boxLeft, boxTop, boxWidth,
|
||||
1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop + boxHeight - 1, boxWidth, 1);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft, boxTop, 1, boxHeight);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBorder, boxLeft + boxWidth - 1, boxTop, 1, boxHeight);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Frame signature & utilities
|
||||
|
||||
uint32_t getTFTColorFrameSignature()
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return 0;
|
||||
#else
|
||||
uint32_t hash = kFnv1aOffsetBasis;
|
||||
hash = fnv1aAppendByte(hash, colorRegionCount);
|
||||
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||
const TFTColorRegion &r = colorRegions[i];
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.x));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.y));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.width));
|
||||
hash = fnv1aAppendU16(hash, static_cast<uint16_t>(r.height));
|
||||
hash = fnv1aAppendU16(hash, r.onColorBe);
|
||||
hash = fnv1aAppendU16(hash, r.offColorBe);
|
||||
}
|
||||
|
||||
return hash;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t getTFTColorRegionCount()
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
return 0;
|
||||
#else
|
||||
return colorRegionCount;
|
||||
#endif
|
||||
}
|
||||
|
||||
void clearTFTColorRegions()
|
||||
{
|
||||
for (uint8_t i = 0; i < colorRegionCount; i++) {
|
||||
colorRegions[i].enabled = false;
|
||||
}
|
||||
if (colorRegionCount < MAX_TFT_COLOR_REGIONS) {
|
||||
colorRegions[colorRegionCount].enabled = false;
|
||||
}
|
||||
colorRegionCount = 0;
|
||||
}
|
||||
|
||||
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor)
|
||||
{
|
||||
for (int i = static_cast<int>(colorRegionCount) - 1; i >= 0; i--) {
|
||||
const TFTColorRegion &r = colorRegions[i];
|
||||
if (x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height) {
|
||||
return isset ? r.onColorBe : r.offColorBe;
|
||||
}
|
||||
}
|
||||
return isset ? defaultOnColor : defaultOffColor;
|
||||
}
|
||||
|
||||
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor)
|
||||
{
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
(void)x;
|
||||
(void)y;
|
||||
return defaultOffColor;
|
||||
#else
|
||||
const uint16_t defaultOffBe = toBe565(defaultOffColor);
|
||||
const uint16_t sampledBe = resolveTFTColorPixel(x, y, false, defaultOffBe, defaultOffBe);
|
||||
return static_cast<uint16_t>((sampledBe >> 8) | (sampledBe << 8));
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
163
src/graphics/TFTColorRegions.h
Normal file
163
src/graphics/TFTColorRegions.h
Normal file
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
struct TFTColorRegion {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
uint16_t onColorBe;
|
||||
uint16_t offColorBe;
|
||||
// Required by ST7789 driver: it scans until the first disabled entry.
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
static constexpr size_t MAX_TFT_COLOR_REGIONS = 48;
|
||||
extern TFTColorRegion colorRegions[MAX_TFT_COLOR_REGIONS];
|
||||
|
||||
enum class TFTColorRole : uint8_t {
|
||||
HeaderBackground = 0,
|
||||
HeaderTitle,
|
||||
HeaderStatus,
|
||||
SignalBars,
|
||||
ConnectionIcon,
|
||||
UtilizationFill,
|
||||
FavoriteNode,
|
||||
ActionMenuBorder,
|
||||
ActionMenuBody,
|
||||
ActionMenuTitle,
|
||||
FrameMono,
|
||||
BootSplash,
|
||||
FavoriteNodeBGHighlight,
|
||||
NavigationBar,
|
||||
NavigationArrow,
|
||||
Count
|
||||
};
|
||||
|
||||
#if HAS_TFT || defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
|
||||
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 1
|
||||
#else
|
||||
#define GRAPHICS_TFT_COLORING_ENABLED 0
|
||||
#endif
|
||||
|
||||
static constexpr bool kTFTColoringEnabled = GRAPHICS_TFT_COLORING_ENABLED != 0;
|
||||
constexpr bool isTFTColoringEnabled()
|
||||
{
|
||||
return kTFTColoringEnabled;
|
||||
}
|
||||
|
||||
void setTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor);
|
||||
void registerTFTColorRegion(TFTColorRole role, int16_t x, int16_t y, int16_t width, int16_t height);
|
||||
// Convenience helper for the common "set role then register one region" flow.
|
||||
void setAndRegisterTFTColorRole(TFTColorRole role, uint16_t onColor, uint16_t offColor, int16_t x, int16_t y, int16_t width,
|
||||
int16_t height);
|
||||
// Register a region using explicit colors (no role lookup). Use when the
|
||||
// color comes from a theme field rather than a role (e.g. battery fill).
|
||||
void registerTFTColorRegionDirect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t onColor, uint16_t offColor);
|
||||
void registerTFTActionMenuRegions(int16_t boxLeft, int16_t boxTop, int16_t boxWidth, int16_t boxHeight);
|
||||
uint32_t getTFTColorFrameSignature();
|
||||
uint8_t getTFTColorRegionCount();
|
||||
void clearTFTColorRegions();
|
||||
uint16_t resolveTFTColorPixel(int16_t x, int16_t y, bool isset, uint16_t defaultOnColor, uint16_t defaultOffColor);
|
||||
// Resolve effective region-mapped OFF color at a coordinate in native-endian RGB565.
|
||||
uint16_t resolveTFTOffColorAt(int16_t x, int16_t y, uint16_t defaultOffColor);
|
||||
|
||||
// -- Theme engine ------------------------------------------------------
|
||||
// Each theme has four fields that work together:
|
||||
//
|
||||
// id - ThemeID:: constant, used for in-code references.
|
||||
// name - human-readable label shown in the theme picker.
|
||||
// uniqueIdentifier - the stable numeric value persisted to
|
||||
// uiconfig.screen_rgb_color and restored at boot.
|
||||
// This is a CONTRACT with saved configs on disk - once
|
||||
// assigned, never reuse or renumber, even if the theme is
|
||||
// deleted or the kThemes[] array is reordered.
|
||||
// visible - controls whether a theme appears in the picker menu.
|
||||
// Hidden themes can still be restored and applied if their
|
||||
// uniqueIdentifier is persisted.
|
||||
//
|
||||
// Display order in the menu is controlled by kThemes[] array order among
|
||||
// themes where visible == true, NOT by any numeric value above.
|
||||
//
|
||||
// To add a new theme:
|
||||
// 1. Add a unique constant in ThemeID below (next unused value).
|
||||
// 2. Add a kThemes[] entry at the desired menu position, with a unique
|
||||
// uniqueIdentifier that has never been used by any prior theme.
|
||||
// 3. Set visible=true if it should appear in the picker.
|
||||
//
|
||||
// To retire a theme without breaking saved configs:
|
||||
// - Preferred: keep the entry and set visible=false so existing saved
|
||||
// uniqueIdentifier values still resolve to the same theme.
|
||||
// - If you remove the entry, resolveThemeIndex() falls back to DefaultDark
|
||||
// when the persisted uniqueIdentifier no longer matches any theme.
|
||||
// - Do NOT reuse a retired uniqueIdentifier for a future theme.
|
||||
namespace ThemeID
|
||||
{
|
||||
constexpr uint32_t DefaultDark = 0;
|
||||
constexpr uint32_t DefaultLight = 1;
|
||||
constexpr uint32_t Christmas = 2;
|
||||
constexpr uint32_t Pink = 3;
|
||||
constexpr uint32_t Blue = 4;
|
||||
constexpr uint32_t Creamsicle = 5;
|
||||
constexpr uint32_t MeshtasticGreen = 6;
|
||||
constexpr uint32_t ClassicRed = 7;
|
||||
constexpr uint32_t MonochromeWhite = 8;
|
||||
} // namespace ThemeID
|
||||
|
||||
// Per-role color pair stored in native (little-endian) RGB565 format.
|
||||
struct TFTThemeRoleColor {
|
||||
uint16_t onColor;
|
||||
uint16_t offColor;
|
||||
};
|
||||
|
||||
// Complete theme definition.
|
||||
struct TFTThemeDef {
|
||||
uint32_t id; // ThemeID constant - in-code identifier for this theme.
|
||||
const char *name; // Human-readable label shown in the theme picker.
|
||||
uint32_t uniqueIdentifier; // Stable persisted value copied into uiconfig.screen_rgb_color.
|
||||
// Never reuse or renumber - see file-level notes above.
|
||||
TFTThemeRoleColor roles[static_cast<size_t>(TFTColorRole::Count)];
|
||||
uint16_t batteryFillGood;
|
||||
uint16_t batteryFillMedium;
|
||||
uint16_t batteryFillBad;
|
||||
bool fullFrameInvert; // Apply full-frame FrameMono inversion (ST7789 light themes)
|
||||
bool visible; // Show in the theme picker menu. Hidden themes still apply
|
||||
// correctly if their uniqueIdentifier is persisted (dev/legacy themes).
|
||||
};
|
||||
|
||||
// Count of themes whose .visible flag is true. Use this when building menus.
|
||||
size_t getVisibleThemeCount();
|
||||
|
||||
// Access the Nth visible theme (0 .. getVisibleThemeCount()-1). Hidden themes
|
||||
// are skipped, preserving kThemes[] order among the visible entries.
|
||||
const TFTThemeDef &getVisibleThemeByIndex(size_t visibleIndex);
|
||||
|
||||
// Return the theme that matches uiconfig.screen_rgb_color (falls back to Dark).
|
||||
const TFTThemeDef &getActiveTheme();
|
||||
|
||||
// Return the visible-theme index for the currently active theme, or SIZE_MAX
|
||||
// if the active theme is hidden (so menus can show "no selection").
|
||||
size_t getActiveVisibleThemeIndex();
|
||||
|
||||
// Convenience accessors - safe to call even when coloring is compiled out.
|
||||
uint16_t getThemeHeaderBg();
|
||||
uint16_t getThemeHeaderText();
|
||||
uint16_t getThemeHeaderStatus();
|
||||
uint16_t getThemeBodyBg();
|
||||
uint16_t getThemeBodyFg();
|
||||
bool isThemeFullFrameInvert();
|
||||
uint16_t getThemeBatteryFillColor(int batteryPercent);
|
||||
|
||||
// Reinitialise default roleColors from the active theme. Call after a
|
||||
// theme change so that any role registered without a prior setTFTColorRole()
|
||||
// picks up theme-appropriate defaults.
|
||||
void loadThemeDefaults();
|
||||
|
||||
} // namespace graphics
|
||||
@@ -1220,7 +1220,9 @@ static LGFX *tft = nullptr;
|
||||
#endif
|
||||
|
||||
#include "SPILock.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTDisplay.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <SPI.h>
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -1230,6 +1232,25 @@ extern unPhone unphone;
|
||||
|
||||
GpioPin *TFTDisplay::backlightEnable = NULL;
|
||||
|
||||
namespace
|
||||
{
|
||||
static constexpr uint8_t kFullRepaintChunkRows = 8;
|
||||
|
||||
static inline uint16_t getThemeDefaultOnColor()
|
||||
{
|
||||
return graphics::TFTPalette::White;
|
||||
}
|
||||
|
||||
static inline uint16_t getThemeDefaultOffColor()
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
return graphics::getThemeBodyBg();
|
||||
#else
|
||||
return TFT_BLACK;
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
|
||||
{
|
||||
LOG_DEBUG("TFTDisplay!");
|
||||
@@ -1269,14 +1290,15 @@ TFTDisplay::~TFTDisplay()
|
||||
free(linePixelBuffer);
|
||||
linePixelBuffer = nullptr;
|
||||
}
|
||||
if (repaintChunkBuffer != nullptr) {
|
||||
free(repaintChunkBuffer);
|
||||
repaintChunkBuffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the buffer to the display memory
|
||||
void TFTDisplay::display(bool fromBlank)
|
||||
{
|
||||
if (fromBlank)
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
|
||||
concurrency::LockGuard g(spiLock);
|
||||
|
||||
uint32_t x, y;
|
||||
@@ -1285,12 +1307,70 @@ void TFTDisplay::display(bool fromBlank)
|
||||
uint32_t x_FirstPixelUpdate;
|
||||
uint32_t x_LastPixelUpdate;
|
||||
bool isset, dblbuf_isset;
|
||||
uint16_t colorTftMesh, colorTftBlack;
|
||||
uint16_t colorTftWhite, colorTftBlack;
|
||||
bool somethingChanged = false;
|
||||
|
||||
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step
|
||||
colorTftMesh = __builtin_bswap16(TFT_MESH);
|
||||
colorTftBlack = __builtin_bswap16(TFT_BLACK);
|
||||
// Theme defaults for non-role pixels.
|
||||
const uint16_t defaultOnColor = getThemeDefaultOnColor();
|
||||
const uint16_t defaultOffColor = getThemeDefaultOffColor();
|
||||
static uint16_t lastDefaultOnColor = 0;
|
||||
static uint16_t lastDefaultOffColor = 0;
|
||||
static bool haveLastDefaults = false;
|
||||
const bool themeDefaultsChanged =
|
||||
!haveLastDefaults || (defaultOnColor != lastDefaultOnColor) || (defaultOffColor != lastDefaultOffColor);
|
||||
const bool forceFullRepaint = fromBlank || themeDefaultsChanged;
|
||||
|
||||
// If theme defaults changed, reset panel background immediately so stale pixels don't linger.
|
||||
if (forceFullRepaint) {
|
||||
tft->fillScreen(defaultOffColor);
|
||||
}
|
||||
|
||||
colorTftWhite = (defaultOnColor >> 8) | ((defaultOnColor & 0xFF) << 8);
|
||||
colorTftBlack = (defaultOffColor >> 8) | ((defaultOffColor & 0xFF) << 8);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static uint32_t lastColorFrameSignature = 0;
|
||||
const bool hasColorRegions = graphics::getTFTColorRegionCount() > 0;
|
||||
const uint32_t colorFrameSignature = graphics::getTFTColorFrameSignature();
|
||||
const bool forceFullColorRepaint = forceFullRepaint || (colorFrameSignature != lastColorFrameSignature);
|
||||
|
||||
// When region roles/layout changed, color can differ even with identical monochrome glyph bits.
|
||||
// Repaint full frame only for those frames, then return to diff-based updates.
|
||||
if (forceFullColorRepaint) {
|
||||
for (uint32_t yStart = 0; yStart < displayHeight; yStart += kFullRepaintChunkRows) {
|
||||
const uint32_t rowsThisChunk = min<uint32_t>(kFullRepaintChunkRows, displayHeight - yStart);
|
||||
for (uint32_t row = 0; row < rowsThisChunk; row++) {
|
||||
y = yStart + row;
|
||||
y_byteIndex = (y / 8) * displayWidth;
|
||||
y_byteMask = (1 << (y & 7));
|
||||
|
||||
uint16_t *chunkRow = repaintChunkBuffer + (row * displayWidth);
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
isset = (buffer[x + y_byteIndex] & y_byteMask) != 0;
|
||||
if (hasColorRegions) {
|
||||
chunkRow[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||
colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
chunkRow[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if defined(HACKADAY_COMMUNICATOR)
|
||||
tft->draw16bitBeRGBBitmap(0, yStart, repaintChunkBuffer, displayWidth, rowsThisChunk);
|
||||
#else
|
||||
tft->pushImage(0, yStart, displayWidth, rowsThisChunk, repaintChunkBuffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
lastColorFrameSignature = colorFrameSignature;
|
||||
haveLastDefaults = true;
|
||||
lastDefaultOnColor = defaultOnColor;
|
||||
lastDefaultOffColor = defaultOffColor;
|
||||
graphics::clearTFTColorRegions();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
y = 0;
|
||||
while (y < displayHeight) {
|
||||
@@ -1299,7 +1379,7 @@ void TFTDisplay::display(bool fromBlank)
|
||||
|
||||
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
|
||||
if (y_byteMask == 1) {
|
||||
if (!fromBlank) {
|
||||
if (!forceFullRepaint) {
|
||||
for (x = 0; x < displayWidth; x++) {
|
||||
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex])
|
||||
break;
|
||||
@@ -1317,13 +1397,14 @@ void TFTDisplay::display(bool fromBlank)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating
|
||||
for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) {
|
||||
isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
// Step 2: Scan this row for changed span (first and last changed pixel).
|
||||
uint32_t x_FirstChanged = 0;
|
||||
for (x_FirstChanged = 0; x_FirstChanged < displayWidth; x_FirstChanged++) {
|
||||
isset = buffer[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||
|
||||
if (!fromBlank) {
|
||||
if (!forceFullRepaint) {
|
||||
// get src pixel in the page based ordering the OLED lib uses
|
||||
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask;
|
||||
dblbuf_isset = buffer_back[x_FirstChanged + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
break;
|
||||
}
|
||||
@@ -1333,37 +1414,45 @@ void TFTDisplay::display(bool fromBlank)
|
||||
}
|
||||
|
||||
// Did we find a pixel that needs updating on this row?
|
||||
if (x_FirstPixelUpdate < displayWidth) {
|
||||
// Align the first pixel for update to an even number so the total alignment of
|
||||
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
||||
x_FirstPixelUpdate &= ~1;
|
||||
|
||||
// Step 3a: copy rest of the pixels in this row into the pixel line buffer,
|
||||
// while also recording the last pixel in the row that needs updating.
|
||||
// Since the first changed pixel will be looked up, the x_LastPixelUpdate will be set.
|
||||
for (x = x_FirstPixelUpdate; x < displayWidth; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack;
|
||||
|
||||
if (!fromBlank) {
|
||||
dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask;
|
||||
if (x_FirstChanged < displayWidth) {
|
||||
uint32_t x_LastChanged = displayWidth - 1;
|
||||
while (x_LastChanged > x_FirstChanged) {
|
||||
isset = buffer[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||
if (!forceFullRepaint) {
|
||||
dblbuf_isset = buffer_back[x_LastChanged + y_byteIndex] & y_byteMask;
|
||||
if (isset != dblbuf_isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
break;
|
||||
}
|
||||
} else if (isset) {
|
||||
x_LastPixelUpdate = x;
|
||||
break;
|
||||
}
|
||||
x_LastChanged--;
|
||||
}
|
||||
// Step 3b: Round up the last pixel to odd number to maintain 32-bit alignment for SPIs.
|
||||
// Most displays will have even number of pixels in a row -- this will be in bounds
|
||||
// of the displayWidth. (Hopefully odd displays will just ignore that extra pixel.)
|
||||
x_LastPixelUpdate |= 1;
|
||||
// Ensure the last pixel index does not exceed the display width.
|
||||
|
||||
// Align the first pixel for update to an even number so the total alignment of
|
||||
// the data will be at 32-bit boundary, which is required by GDMA SPI transfers.
|
||||
x_FirstPixelUpdate = x_FirstChanged & ~1U;
|
||||
x_LastPixelUpdate = x_LastChanged | 1U;
|
||||
if (x_LastPixelUpdate >= displayWidth) {
|
||||
x_LastPixelUpdate = displayWidth - 1;
|
||||
}
|
||||
|
||||
int y_offset = 0;
|
||||
// Step 3: Copy only the changed span into the pixel line buffer.
|
||||
for (x = x_FirstPixelUpdate; x <= x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y), isset,
|
||||
colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(CO5300_CS)
|
||||
uint8_t lines_updated = 2;
|
||||
if (y % 2 == 0) {
|
||||
@@ -1372,7 +1461,16 @@ void TFTDisplay::display(bool fromBlank)
|
||||
uint32_t bufferIndex = 1;
|
||||
for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
linePixelBuffer[bufferIndex++ + x_LastPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y),
|
||||
isset, colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
y_offset = -1;
|
||||
@@ -1383,7 +1481,16 @@ void TFTDisplay::display(bool fromBlank)
|
||||
uint32_t bufferIndex = 0;
|
||||
for (x = x_FirstPixelUpdate; x < x_LastPixelUpdate; x++) {
|
||||
isset = buffer[x + y_byteIndex] & y_byteMask;
|
||||
linePixelBuffer[bufferIndex++ + x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (hasColorRegions) {
|
||||
linePixelBuffer[x] = graphics::resolveTFTColorPixel(static_cast<int16_t>(x), static_cast<int16_t>(y),
|
||||
isset, colorTftWhite, colorTftBlack);
|
||||
} else {
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
}
|
||||
#else
|
||||
linePixelBuffer[x] = isset ? colorTftWhite : colorTftBlack;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#else
|
||||
@@ -1396,8 +1503,8 @@ void TFTDisplay::display(bool fromBlank)
|
||||
#else
|
||||
// Step 4: Send the changed pixels on this line to the screen as a single block transfer.
|
||||
// This function accepts pixel data MSB first so it can dump the memory straight out the SPI port.
|
||||
tft->pushRect(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
tft->pushImage(x_FirstPixelUpdate, y + y_offset, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), lines_updated,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
#endif
|
||||
somethingChanged = true;
|
||||
}
|
||||
@@ -1406,6 +1513,14 @@ void TFTDisplay::display(bool fromBlank)
|
||||
// Copy the Buffer to the Back Buffer
|
||||
if (somethingChanged)
|
||||
memcpy(buffer_back, buffer, displayBufferSize);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
lastColorFrameSignature = colorFrameSignature;
|
||||
#endif
|
||||
haveLastDefaults = true;
|
||||
lastDefaultOnColor = defaultOnColor;
|
||||
lastDefaultOffColor = defaultOffColor;
|
||||
graphics::clearTFTColorRegions();
|
||||
}
|
||||
|
||||
void TFTDisplay::sdlLoop()
|
||||
@@ -1619,7 +1734,7 @@ bool TFTDisplay::connect()
|
||||
#else
|
||||
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
|
||||
#endif
|
||||
tft->fillScreen(TFT_BLACK);
|
||||
tft->fillScreen(getThemeDefaultOffColor());
|
||||
|
||||
if (this->linePixelBuffer == NULL) {
|
||||
#if defined(CO5300_CS)
|
||||
@@ -1633,6 +1748,14 @@ bool TFTDisplay::connect()
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this->repaintChunkBuffer == NULL) {
|
||||
this->repaintChunkBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth * kFullRepaintChunkRows);
|
||||
|
||||
if (!this->repaintChunkBuffer) {
|
||||
LOG_ERROR("Not enough memory to create TFT repaint chunk buffer\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,4 +63,5 @@ class TFTDisplay : public OLEDDisplay
|
||||
virtual bool connect() override;
|
||||
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
};
|
||||
uint16_t *repaintChunkBuffer = nullptr;
|
||||
};
|
||||
|
||||
70
src/graphics/TFTPalette.h
Normal file
70
src/graphics/TFTPalette.h
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace TFTPalette
|
||||
{
|
||||
|
||||
constexpr uint16_t rgb565(uint8_t red, uint8_t green, uint8_t blue)
|
||||
{
|
||||
return static_cast<uint16_t>(((red & 0xF8) << 8) | ((green & 0xFC) << 3) | ((blue & 0xF8) >> 3));
|
||||
}
|
||||
|
||||
constexpr uint16_t Black = 0x0000;
|
||||
constexpr uint16_t White = 0xFFFF;
|
||||
constexpr uint16_t DarkGray = 0x4208;
|
||||
constexpr uint16_t Gray = 0x8410;
|
||||
constexpr uint16_t LightGray = 0xC618;
|
||||
|
||||
constexpr uint16_t Red = rgb565(255, 0, 0);
|
||||
constexpr uint16_t Green = rgb565(0, 255, 0);
|
||||
constexpr uint16_t Blue = rgb565(0, 130, 252);
|
||||
constexpr uint16_t Yellow = rgb565(255, 255, 0);
|
||||
constexpr uint16_t Orange = rgb565(255, 165, 0);
|
||||
constexpr uint16_t Cyan = rgb565(0, 255, 255);
|
||||
constexpr uint16_t Magenta = rgb565(255, 0, 255);
|
||||
|
||||
constexpr uint16_t Good = Green;
|
||||
constexpr uint16_t Medium = Yellow;
|
||||
constexpr uint16_t Bad = Red;
|
||||
|
||||
// Christmas / seasonal accent colors
|
||||
constexpr uint16_t ChristmasRed = rgb565(178, 34, 34);
|
||||
constexpr uint16_t ChristmasGreen = rgb565(0, 128, 0);
|
||||
constexpr uint16_t Gold = rgb565(255, 215, 0);
|
||||
constexpr uint16_t Pine = rgb565(15, 35, 10);
|
||||
|
||||
// Pink theme colors (light variant)
|
||||
constexpr uint16_t HotPink = rgb565(255, 105, 180);
|
||||
constexpr uint16_t PalePink = rgb565(255, 228, 235);
|
||||
constexpr uint16_t DeepPink = rgb565(200, 50, 120);
|
||||
|
||||
// Blue theme colors (dark variant)
|
||||
constexpr uint16_t SkyBlue = rgb565(100, 180, 255);
|
||||
constexpr uint16_t Navy = rgb565(15, 15, 50);
|
||||
constexpr uint16_t DeepBlue = rgb565(30, 60, 120);
|
||||
|
||||
// Creamsicle theme colors (light variant)
|
||||
constexpr uint16_t CreamOrange = rgb565(255, 140, 50);
|
||||
constexpr uint16_t DeepOrange = rgb565(220, 100, 20);
|
||||
constexpr uint16_t Cream = rgb565(255, 248, 235);
|
||||
|
||||
// Classic monochrome theme accent colors (single-color-on-black themes)
|
||||
constexpr uint16_t MeshtasticGreen = rgb565(0x67, 0xEA, 0x94);
|
||||
constexpr uint16_t ClassicRed = rgb565(255, 64, 64);
|
||||
// Monochrome White reuses TFTPalette::White above.
|
||||
|
||||
// Fast contrast picker for monochrome glyph overlays on arbitrary RGB565 backgrounds.
|
||||
// Uses channel-sum brightness approximation to keep code size small.
|
||||
constexpr uint16_t pickReadableMonoFg(uint16_t backgroundColor)
|
||||
{
|
||||
const uint16_t r = (backgroundColor >> 11) & 0x1F;
|
||||
const uint16_t g = (backgroundColor >> 5) & 0x3F;
|
||||
const uint16_t b = backgroundColor & 0x1F;
|
||||
return ((r + g + b) >= 70) ? DarkGray : White;
|
||||
}
|
||||
|
||||
} // namespace TFTPalette
|
||||
} // namespace graphics
|
||||
@@ -145,7 +145,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
@@ -293,11 +293,15 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
// Draw an analog clock
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
// Clear previous frame pixels so moving hands don't leave stale artifacts on TFT light theme.
|
||||
display->clear();
|
||||
#endif
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true, true);
|
||||
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
@@ -478,4 +482,4 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
} // namespace ClockRenderer
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -9,113 +9,61 @@ namespace graphics
|
||||
{
|
||||
namespace CompassRenderer
|
||||
{
|
||||
|
||||
// Point helper class for compass calculations
|
||||
struct Point {
|
||||
float x, y;
|
||||
Point(float x, float y) : x(x), y(y) {}
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cosf(angle);
|
||||
float sin_a = sinf(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
y = new_y;
|
||||
}
|
||||
|
||||
void scale(float factor)
|
||||
{
|
||||
x *= factor;
|
||||
y *= factor;
|
||||
}
|
||||
|
||||
void translate(float dx, float dy)
|
||||
{
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
};
|
||||
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||
{
|
||||
// Show the compass heading (not implemented in original)
|
||||
// This could draw a "N" indicator or north arrow
|
||||
// For now, we'll draw a simple north indicator
|
||||
// const float radius = 17.0f;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
float northX = 0.0f;
|
||||
float northY = -radius;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
||||
const float c = cosf(-myHeading);
|
||||
const float s = sinf(-myHeading);
|
||||
const float rx = northX * c - northY * s;
|
||||
const float ry = northX * s + northY * c;
|
||||
northX = rx;
|
||||
northY = ry;
|
||||
}
|
||||
northX += compassX;
|
||||
northY += compassY;
|
||||
|
||||
const float northAngle = (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) ? -myHeading : 0.0f;
|
||||
const int16_t nX = compassX + static_cast<int16_t>((radius - 1) * sinf(northAngle));
|
||||
const int16_t nY = compassY - static_cast<int16_t>((radius - 1) * cosf(northAngle));
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
#if !GRAPHICS_TFT_COLORING_ENABLED
|
||||
display->setColor(BLACK);
|
||||
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(nX - 8, nY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(nX - 4, nY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(northX, northY - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||
|
||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
arrowPoints[i]->rotate(headingRadian);
|
||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||
arrowPoints[i]->translate(compassX, compassY);
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#else
|
||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#endif
|
||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||
display->setColor(WHITE);
|
||||
display->drawString(nX, nY - 3, "N");
|
||||
}
|
||||
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||
{
|
||||
float radians = bearing * DEG_TO_RAD;
|
||||
const float radians = bearing * DEG_TO_RAD;
|
||||
const float sinA = sinf(radians);
|
||||
const float cosA = cosf(radians);
|
||||
const float tipHalf = size * 0.5f;
|
||||
const float lx = -(size / 6.0f);
|
||||
const float ly = size / 4.0f;
|
||||
const float rx = (size / 6.0f);
|
||||
const float ry = size / 4.0f;
|
||||
const float tx = 0.0f;
|
||||
const float ty = size / 4.5f;
|
||||
|
||||
Point tip(0, -size / 2);
|
||||
Point left(-size / 6, size / 4);
|
||||
Point right(size / 6, size / 4);
|
||||
Point tail(0, size / 4.5);
|
||||
const int16_t tipX = static_cast<int16_t>(x + (tipHalf * sinA));
|
||||
const int16_t tipY = static_cast<int16_t>(y - (tipHalf * cosA));
|
||||
const int16_t leftX = static_cast<int16_t>(x + (lx * cosA) - (ly * sinA));
|
||||
const int16_t leftY = static_cast<int16_t>(y + (lx * sinA) + (ly * cosA));
|
||||
const int16_t rightX = static_cast<int16_t>(x + (rx * cosA) - (ry * sinA));
|
||||
const int16_t rightY = static_cast<int16_t>(y + (rx * sinA) + (ry * cosA));
|
||||
const int16_t tailX = static_cast<int16_t>(x + (tx * cosA) - (ty * sinA));
|
||||
const int16_t tailY = static_cast<int16_t>(y + (tx * sinA) + (ty * cosA));
|
||||
|
||||
tip.rotate(radians);
|
||||
left.rotate(radians);
|
||||
right.rotate(radians);
|
||||
tail.rotate(radians);
|
||||
display->fillTriangle(tipX, tipY, leftX, leftY, tailX, tailY);
|
||||
display->fillTriangle(tipX, tipY, rightX, rightY, tailX, tailY);
|
||||
}
|
||||
|
||||
tip.translate(x, y);
|
||||
left.translate(x, y);
|
||||
right.translate(x, y);
|
||||
tail.translate(x, y);
|
||||
|
||||
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
const int16_t size = static_cast<int16_t>(compassDiam * 0.6f);
|
||||
drawArrowToNode(display, compassX, compassY, size, headingRadian * RAD_TO_DEG);
|
||||
}
|
||||
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
@@ -469,9 +471,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||
|
||||
int chutil_bar_width = (currentResolution == ScreenResolution::High) ? 100 : 50;
|
||||
int chutil_bar_max_fill = chutil_bar_width - 2; // Account for border
|
||||
int chutil_bar_height = (currentResolution == ScreenResolution::High) ? 12 : 7;
|
||||
int extraoffset = (currentResolution == ScreenResolution::High) ? 6 : 3;
|
||||
int chutil_percent = airTime->channelUtilizationPercent();
|
||||
const int raw_chutil_percent = chutil_percent;
|
||||
|
||||
int centerofscreen = SCREEN_WIDTH / 2;
|
||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||
@@ -479,7 +483,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
|
||||
display->drawString(starting_position, getTextPositions(display)[line], chUtil);
|
||||
|
||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||
// Force 61% or higher to show a full 100% bar, text would still show related percent.
|
||||
if (chutil_percent >= 61) {
|
||||
chutil_percent = 100;
|
||||
}
|
||||
@@ -492,9 +496,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
float weight3 = 0.20; // Weight for 40–100%
|
||||
float totalWeight = weight1 + weight2 + weight3;
|
||||
|
||||
int seg1 = chutil_bar_width * (weight1 / totalWeight);
|
||||
int seg2 = chutil_bar_width * (weight2 / totalWeight);
|
||||
int seg3 = chutil_bar_width * (weight3 / totalWeight);
|
||||
int seg1 = chutil_bar_max_fill * (weight1 / totalWeight);
|
||||
int seg2 = chutil_bar_max_fill * (weight2 / totalWeight);
|
||||
int seg3 = chutil_bar_max_fill - seg1 - seg2; // Remainder absorbs rounding errors
|
||||
|
||||
int fillRight = 0;
|
||||
|
||||
@@ -511,7 +515,17 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
|
||||
// Fill progress
|
||||
if (fillRight > 0) {
|
||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||
if (raw_chutil_percent >= 60) {
|
||||
UtilizationFillColor = TFTPalette::Bad;
|
||||
} else if (raw_chutil_percent >= 35) {
|
||||
UtilizationFillColor = TFTPalette::Medium;
|
||||
}
|
||||
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black,
|
||||
starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||
#endif
|
||||
display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillRight, chutil_bar_height - 2);
|
||||
}
|
||||
|
||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++],
|
||||
@@ -584,6 +598,17 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
uint16_t UtilizationFillColor = TFTPalette::Good;
|
||||
if (percent >= 80) {
|
||||
UtilizationFillColor = TFTPalette::Bad;
|
||||
} else if (percent >= 60) {
|
||||
UtilizationFillColor = TFTPalette::Medium;
|
||||
}
|
||||
setAndRegisterTFTColorRole(TFTColorRole::UtilizationFill, UtilizationFillColor, TFTPalette::Black, barX + 1, barY + 1,
|
||||
fillWidth - 1, barHeight - 2);
|
||||
#endif
|
||||
|
||||
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||
display->setColor(WHITE);
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "buzz.h"
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/draw/MessageRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
@@ -30,8 +31,6 @@
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
|
||||
extern uint16_t TFT_MESH;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
@@ -2028,109 +2027,6 @@ void menuHandler::switchToMUIMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
|
||||
{
|
||||
static const ScreenColorOption colorOptions[] = {
|
||||
{"Back", OptionsAction::Back},
|
||||
{"Default", OptionsAction::Select, ScreenColor(0, 0, 0, true)},
|
||||
{"Meshtastic Green", OptionsAction::Select, ScreenColor(0x67, 0xEA, 0x94)},
|
||||
{"Yellow", OptionsAction::Select, ScreenColor(255, 255, 128)},
|
||||
{"Red", OptionsAction::Select, ScreenColor(255, 64, 64)},
|
||||
{"Orange", OptionsAction::Select, ScreenColor(255, 160, 20)},
|
||||
{"Purple", OptionsAction::Select, ScreenColor(204, 153, 255)},
|
||||
{"Blue", OptionsAction::Select, ScreenColor(0, 0, 255)},
|
||||
{"Teal", OptionsAction::Select, ScreenColor(16, 102, 102)},
|
||||
{"Cyan", OptionsAction::Select, ScreenColor(0, 255, 255)},
|
||||
{"Ice", OptionsAction::Select, ScreenColor(173, 216, 230)},
|
||||
{"Pink", OptionsAction::Select, ScreenColor(255, 105, 180)},
|
||||
{"White", OptionsAction::Select, ScreenColor(255, 255, 255)},
|
||||
{"Gray", OptionsAction::Select, ScreenColor(128, 128, 128)},
|
||||
};
|
||||
|
||||
constexpr size_t colorCount = sizeof(colorOptions) / sizeof(colorOptions[0]);
|
||||
static std::array<const char *, colorCount> colorLabels{};
|
||||
|
||||
auto bannerOptions = createStaticBannerOptions(
|
||||
"Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void {
|
||||
if (option.action == OptionsAction::Back) {
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!option.hasValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
||||
const ScreenColor &color = option.value;
|
||||
if (color.useVariant) {
|
||||
LOG_INFO("Setting color to system default or defined variant");
|
||||
} else {
|
||||
LOG_INFO("Setting color to %s", option.label);
|
||||
}
|
||||
|
||||
uint8_t r = color.r;
|
||||
uint8_t g = color.g;
|
||||
uint8_t b = color.b;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
||||
display->setColor(WHITE);
|
||||
|
||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#else
|
||||
TFT_MESH = COLOR565(255, 255, 128);
|
||||
#endif
|
||||
} else {
|
||||
TFT_MESH = COLOR565(r, g, b);
|
||||
}
|
||||
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
|
||||
static_cast<ST7789Spi *>(screen->getDisplayDevice())->setRGB(TFT_MESH);
|
||||
#endif
|
||||
|
||||
screen->setFrames(graphics::Screen::FOCUS_SYSTEM);
|
||||
if (color.useVariant || (r == 0 && g == 0 && b == 0)) {
|
||||
uiconfig.screen_rgb_color = 0;
|
||||
} else {
|
||||
uiconfig.screen_rgb_color =
|
||||
(static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | static_cast<uint32_t>(b);
|
||||
}
|
||||
LOG_INFO("Storing Value of %d to uiconfig.screen_rgb_color", uiconfig.screen_rgb_color);
|
||||
saveUIConfig();
|
||||
#endif
|
||||
});
|
||||
|
||||
int initialSelection = 0;
|
||||
if (uiconfig.screen_rgb_color == 0) {
|
||||
initialSelection = 1;
|
||||
} else {
|
||||
uint32_t currentColor = uiconfig.screen_rgb_color;
|
||||
for (size_t i = 0; i < colorCount; ++i) {
|
||||
if (!colorOptions[i].hasValue) {
|
||||
continue;
|
||||
}
|
||||
const ScreenColor &color = colorOptions[i].value;
|
||||
if (color.useVariant) {
|
||||
continue;
|
||||
}
|
||||
uint32_t encoded =
|
||||
(static_cast<uint32_t>(color.r) << 16) | (static_cast<uint32_t>(color.g) << 8) | static_cast<uint32_t>(color.b);
|
||||
if (encoded == currentColor) {
|
||||
initialSelection = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bannerOptions.InitialSelected = initialSelection;
|
||||
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::rebootMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
@@ -2318,9 +2214,9 @@ void menuHandler::screenOptionsMenu()
|
||||
bool hasSupportBrightness = false;
|
||||
#endif
|
||||
|
||||
enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles };
|
||||
static const char *optionsArray[6] = {"Back"};
|
||||
static int optionsEnumArray[6] = {Back};
|
||||
enum optionsNumbers { Back, Brightness, FrameToggles, DisplayUnits, MessageBubbles, Theme };
|
||||
static const char *optionsArray[7] = {"Back"};
|
||||
static int optionsEnumArray[7] = {Back};
|
||||
int options = 1;
|
||||
|
||||
// Only show brightness for B&W displays
|
||||
@@ -2329,13 +2225,6 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsEnumArray[options++] = Brightness;
|
||||
}
|
||||
|
||||
// Only show screen color for TFT displays
|
||||
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || \
|
||||
HAS_TFT || defined(HACKADAY_COMMUNICATOR)
|
||||
optionsArray[options] = "Screen Color";
|
||||
optionsEnumArray[options++] = ScreenColor;
|
||||
#endif
|
||||
|
||||
optionsArray[options] = "Frame Visibility";
|
||||
optionsEnumArray[options++] = FrameToggles;
|
||||
|
||||
@@ -2345,6 +2234,11 @@ void menuHandler::screenOptionsMenu()
|
||||
optionsArray[options] = "Message Bubbles";
|
||||
optionsEnumArray[options++] = MessageBubbles;
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
optionsArray[options] = "Theme";
|
||||
optionsEnumArray[options++] = Theme;
|
||||
#endif
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Display Options";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
@@ -2354,9 +2248,6 @@ void menuHandler::screenOptionsMenu()
|
||||
if (selected == Brightness) {
|
||||
menuHandler::menuQueue = menuHandler::BrightnessPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == ScreenColor) {
|
||||
menuHandler::menuQueue = menuHandler::TftColorMenuPicker;
|
||||
screen->runNow();
|
||||
} else if (selected == FrameToggles) {
|
||||
menuHandler::menuQueue = menuHandler::FrameToggles;
|
||||
screen->runNow();
|
||||
@@ -2366,6 +2257,9 @@ void menuHandler::screenOptionsMenu()
|
||||
} else if (selected == MessageBubbles) {
|
||||
menuHandler::menuQueue = menuHandler::MessageBubblesMenu;
|
||||
screen->runNow();
|
||||
} else if (selected == Theme) {
|
||||
menuHandler::menuQueue = menuHandler::ThemeMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
menuQueue = SystemBaseMenu;
|
||||
screen->runNow();
|
||||
@@ -2649,6 +2543,53 @@ void menuHandler::messageBubblesMenu()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::themeMenu()
|
||||
{
|
||||
// Build menu dynamically from the theme table.
|
||||
// Only visible themes appear!
|
||||
// Slot budget: 1 for "Back" + up to kMaxThemesInMenu visible themes.
|
||||
// Bump kMaxThemesInMenu if you add more themes than will fit here.
|
||||
constexpr size_t kMaxThemesInMenu = 15;
|
||||
const size_t visibleCount = getVisibleThemeCount();
|
||||
static const char *optionsArray[kMaxThemesInMenu + 1] = {"Back"};
|
||||
const size_t shownCount = (visibleCount < kMaxThemesInMenu) ? visibleCount : kMaxThemesInMenu;
|
||||
const int options = static_cast<int>(shownCount) + 1; // +1 for Back
|
||||
|
||||
for (size_t i = 0; i < shownCount; i++) {
|
||||
optionsArray[i + 1] = getVisibleThemeByIndex(i).name;
|
||||
}
|
||||
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Theme";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = options;
|
||||
|
||||
// Highlight the currently active theme (visible index + 1 for the Back
|
||||
// offset). If the active theme is hidden, leave selection on "Back".
|
||||
const size_t activeVisible = getActiveVisibleThemeIndex();
|
||||
bannerOptions.InitialSelected = (activeVisible == SIZE_MAX) ? 0 : static_cast<int>(activeVisible) + 1;
|
||||
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 0) {
|
||||
// Back
|
||||
menuHandler::menuQueue = menuHandler::ScreenOptionsMenu;
|
||||
screen->runNow();
|
||||
} else {
|
||||
// Selection is an index into the VISIBLE themes (1-based, slot 0 is Back).
|
||||
const size_t visibleIdx = static_cast<size_t>(selected - 1);
|
||||
if (visibleIdx < getVisibleThemeCount()) {
|
||||
// Persist the theme's uniqueIdentifier so boot-time
|
||||
// resolveThemeIndex() can restore this theme on next startup.
|
||||
uiconfig.screen_rgb_color = COLOR565(255, 255, (getVisibleThemeByIndex(visibleIdx).uniqueIdentifier & 0x1F) << 3);
|
||||
loadThemeDefaults();
|
||||
saveUIConfig();
|
||||
screen->runNow();
|
||||
}
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
{
|
||||
if (menuQueue != MenuNone)
|
||||
@@ -2724,9 +2665,6 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case MuiPicker:
|
||||
switchToMUIMenu();
|
||||
break;
|
||||
case TftColorMenuPicker:
|
||||
TFTColorPickerMenu(display);
|
||||
break;
|
||||
case BrightnessPicker:
|
||||
BrightnessPickerMenu();
|
||||
break;
|
||||
@@ -2799,6 +2737,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case MessageBubblesMenu:
|
||||
messageBubblesMenu();
|
||||
break;
|
||||
case ThemeMenu:
|
||||
themeMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
@@ -2810,4 +2751,4 @@ void menuHandler::saveUIConfig()
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -30,7 +30,6 @@ class menuHandler
|
||||
ResetNodeDbMenu,
|
||||
BuzzerModeMenuPicker,
|
||||
MuiPicker,
|
||||
TftColorMenuPicker,
|
||||
BrightnessPicker,
|
||||
RebootMenu,
|
||||
ShutdownMenu,
|
||||
@@ -55,7 +54,8 @@ class menuHandler
|
||||
NodeNameLengthMenu,
|
||||
FrameToggles,
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
@@ -89,7 +89,6 @@ class menuHandler
|
||||
static void GPSPositionBroadcastMenu();
|
||||
static void BuzzerModeMenu();
|
||||
static void switchToMUIMenu();
|
||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||
static void nodeListMenu();
|
||||
static void resetNodeDBMenu();
|
||||
static void BrightnessPickerMenu();
|
||||
@@ -110,6 +109,7 @@ class menuHandler
|
||||
static void frameTogglesMenu();
|
||||
static void displayUnitsMenu();
|
||||
static void messageBubblesMenu();
|
||||
static void themeMenu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
@@ -136,23 +136,10 @@ template <typename T> struct MenuOption {
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
||||
};
|
||||
|
||||
struct ScreenColor {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
|
||||
explicit ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
||||
using LoraRegionOption = MenuOption<meshtastic_Config_LoRaConfig_RegionCode>;
|
||||
using TimezoneOption = MenuOption<const char *>;
|
||||
using CompassOption = MenuOption<meshtastic_CompassMode>;
|
||||
using ScreenColorOption = MenuOption<ScreenColor>;
|
||||
using GPSToggleOption = MenuOption<meshtastic_Config_PositionConfig_GpsMode>;
|
||||
using GPSFormatOption = MenuOption<meshtastic_DeviceUIConfig_GpsCoordinateFormat>;
|
||||
using NodeNameOption = MenuOption<bool>;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
@@ -254,6 +256,76 @@ struct MessageBlock {
|
||||
bool mine;
|
||||
};
|
||||
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
static void setDarkModeBubbleRoleColors(uint32_t themeId, bool mine)
|
||||
{
|
||||
uint16_t bubbleOnColor;
|
||||
uint16_t bubbleOffColor;
|
||||
|
||||
if (themeId == ThemeID::Blue) {
|
||||
bubbleOnColor = mine ? TFTPalette::Navy : TFTPalette::White;
|
||||
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DeepBlue;
|
||||
} else {
|
||||
bubbleOnColor = mine ? TFTPalette::Black : getThemeBodyFg();
|
||||
bubbleOffColor = mine ? TFTPalette::SkyBlue : TFTPalette::DarkGray;
|
||||
}
|
||||
|
||||
setTFTColorRole(TFTColorRole::ActionMenuBody, bubbleOnColor, bubbleOffColor);
|
||||
}
|
||||
|
||||
static void registerRoundedBubbleFillRegion(int x, int y, int w, int h, int radius)
|
||||
{
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (radius <= 0 || w < 3 || h < 3) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep region count low so we don't churn MAX_TFT_COLOR_REGIONS while
|
||||
// scrolling long message lists (which can flatten older bubble corners).
|
||||
int capRows = 0;
|
||||
if (radius >= 4 && h >= 5) {
|
||||
capRows = 2; // 5 regions total (2 top caps + middle + 2 bottom caps)
|
||||
} else if (radius >= 2 && h >= 3) {
|
||||
capRows = 1; // 3 regions total
|
||||
}
|
||||
if (capRows <= 0) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, y, w, h);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int row = 0; row < capRows; ++row) {
|
||||
int inset = 0;
|
||||
if (radius >= 4) {
|
||||
inset = (row == 0) ? 2 : 1;
|
||||
} else if (radius >= 2) {
|
||||
inset = 1;
|
||||
}
|
||||
const int stripW = w - (inset * 2);
|
||||
if (stripW <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int topY = y + row;
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, topY, stripW, 1);
|
||||
|
||||
const int bottomY = y + h - 1 - row;
|
||||
if (bottomY != topY) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x + inset, bottomY, stripW, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const int middleY = y + capRows;
|
||||
const int middleH = h - (capRows * 2);
|
||||
if (middleH > 0) {
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, x, middleY, w, middleH);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static int getDrawnLinePixelBottom(int lineTopY, const std::string &line, bool isHeaderLine)
|
||||
{
|
||||
if (isHeaderLine) {
|
||||
@@ -648,6 +720,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const int contentBottom = scrollBottom; // already excludes nav line
|
||||
const int rightEdge = SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN;
|
||||
const int bubbleGapY = std::max(1, MESSAGE_BLOCK_GAP / 2);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const uint32_t themeId = getActiveTheme().id;
|
||||
// Blue is a dark variant but uses full frame inversion, Keep it on the same filled bubble style as Default Dark.
|
||||
const bool useDarkModeBubbleFill = showBubbles && (!isThemeFullFrameInvert() || themeId == ThemeID::Blue);
|
||||
#endif
|
||||
|
||||
std::vector<int> lineTop;
|
||||
lineTop.resize(cachedLines.size());
|
||||
@@ -686,6 +763,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]);
|
||||
int bottomY = visualBottom + BUBBLE_PAD_Y;
|
||||
|
||||
// On high-res screens, keep a 1px gap under the header
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int minTopY = contentTop + 1;
|
||||
if (topY < minTopY) {
|
||||
// Preserve bubble height when we push it down from the header.
|
||||
const int shift = minTopY - topY;
|
||||
topY = minTopY;
|
||||
bottomY += shift;
|
||||
}
|
||||
}
|
||||
|
||||
if (bi + 1 < blocks.size()) {
|
||||
int nextHeaderIndex = (int)blocks[bi + 1].start;
|
||||
int nextTop = lineTop[nextHeaderIndex];
|
||||
@@ -735,24 +823,56 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
const int by = topY;
|
||||
const int bw = bubbleW;
|
||||
const int bh = bubbleH;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||
#else
|
||||
const bool drawBubbleOutline = true;
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||
registerRoundedBubbleFillRegion(bx, by, bw, bh, r);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
if (drawBubbleOutline) {
|
||||
// Draw the 4 corner arcs using drawCircleQuads
|
||||
display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right
|
||||
display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left
|
||||
display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right
|
||||
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
// Draw the 4 edges between corners
|
||||
display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge
|
||||
display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge
|
||||
display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge
|
||||
display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge
|
||||
}
|
||||
} else if (bubbleW > 1 && bubbleH > 1) {
|
||||
// Fallback to simple rectangle for very small bubbles
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
const bool drawBubbleOutline = !useDarkModeBubbleFill;
|
||||
#else
|
||||
const bool drawBubbleOutline = true;
|
||||
#endif
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
setDarkModeBubbleRoleColors(themeId, b.mine);
|
||||
registerTFTColorRegion(TFTColorRole::ActionMenuBody, bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
#endif
|
||||
if (drawBubbleOutline) {
|
||||
display->drawRect(bubbleX, topY, bubbleW, bubbleH);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end if (showBubbles)
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (useDarkModeBubbleFill) {
|
||||
// Restore theme role defaults so other screens keep their intended palette.
|
||||
loadThemeDefaults();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Render visible lines
|
||||
int lineY = yOffset;
|
||||
@@ -772,7 +892,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
headerX = x + textIndent;
|
||||
}
|
||||
graphics::UIRenderer::drawStringWithEmotes(display, headerX, lineY, cachedLines[i].c_str(), FONT_HEIGHT_SMALL, 1,
|
||||
false);
|
||||
true);
|
||||
|
||||
// Draw underline just under header text
|
||||
int underlineY = lineY + FONT_HEIGHT_SMALL;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/images.h"
|
||||
#include "meshUtils.h"
|
||||
#include <algorithm>
|
||||
@@ -213,6 +215,33 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
}
|
||||
}
|
||||
|
||||
static inline void applyFavoriteNodeNameColor(OLEDDisplay *display, const meshtastic_NodeInfoLite *node, const char *nodeName,
|
||||
int16_t nameX, int16_t y, int nameMaxWidth)
|
||||
{
|
||||
if (!display || !node || !node->is_favorite || !isTFTColoringEnabled() || !nodeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int textWidth = UIRenderer::measureStringWithEmotes(display, nodeName);
|
||||
const int regionWidth = min(textWidth, max(0, nameMaxWidth));
|
||||
if (regionWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Node list rows can begin a couple of pixels inside header space.
|
||||
// Clamp favorite-name color region below the header to avoid black overlap there.
|
||||
const int16_t minContentY = static_cast<int16_t>(FONT_HEIGHT_SMALL + 1);
|
||||
const int16_t regionY = max(y, minContentY);
|
||||
const int16_t yClip = regionY - y;
|
||||
const int16_t regionHeight = static_cast<int16_t>(FONT_HEIGHT_SMALL - yClip);
|
||||
if (regionHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAndRegisterTFTColorRole(TFTColorRole::FavoriteNode, TFTPalette::Yellow, TFTPalette::Black, nameX, regionY, regionWidth,
|
||||
regionHeight);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Entry Renderers
|
||||
// =============================
|
||||
@@ -227,6 +256,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
char timeStr[10];
|
||||
@@ -275,14 +307,20 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
constexpr int kBarCount = 4;
|
||||
constexpr int kBarWidth = 2;
|
||||
constexpr int kBarGap = 1;
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
|
||||
|
||||
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@@ -304,28 +342,48 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal strength bars
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barWidth = 2;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||
// Show signal only for direct neighbors (0 hops)
|
||||
if (isZeroHop) {
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
|
||||
if (bars > 0) {
|
||||
uint16_t signalBarsColor = TFTPalette::Bad;
|
||||
if (bars >= 3) {
|
||||
signalBarsColor = TFTPalette::Good;
|
||||
} else if (bars == 2) {
|
||||
signalBarsColor = TFTPalette::Medium;
|
||||
}
|
||||
|
||||
// Highest bar reaches 6 px in this renderer.
|
||||
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, barStartX, barStartY - 6,
|
||||
(kBarCount * kBarWidth) + ((kBarCount - 1) * kBarGap), 6);
|
||||
}
|
||||
|
||||
for (int b = 0; b < kBarCount; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hop count
|
||||
char hopStr[6] = "";
|
||||
if (node->has_hops_away && node->hops_away > 0)
|
||||
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||
// Draw hop count + hop icon
|
||||
if (node->has_hops_away && node->hops_away > 0) {
|
||||
char hopCount[6];
|
||||
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
||||
|
||||
if (hopStr[0] != '\0') {
|
||||
int rightEdge = x + columnWidth - hopOffset;
|
||||
int textWidth = display->getStringWidth(hopStr);
|
||||
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||
const int hopCountWidth = display->getStringWidth(hopCount);
|
||||
const int gap = 1;
|
||||
const int totalWidth = hopCountWidth + gap + hop_width;
|
||||
const int hopX = barsRightEdge - totalWidth;
|
||||
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||
|
||||
display->drawString(hopX, y, hopCount);
|
||||
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +398,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
char distStr[10] = "";
|
||||
|
||||
@@ -445,6 +506,9 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
char nodeName[96];
|
||||
UIRenderer::truncateStringWithEmotes(display, getSafeNodeName(display, node, columnWidth).c_str(), nodeName, sizeof(nodeName),
|
||||
nameMaxWidth);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
applyFavoriteNodeNameColor(display, node, nodeName, nameX, y, nameMaxWidth);
|
||||
#endif
|
||||
bool isMuted = (node->bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@@ -700,6 +764,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
#endif
|
||||
|
||||
// Text
|
||||
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "UIRenderer.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/TFTColorRegions.h"
|
||||
#include "graphics/TFTPalette.h"
|
||||
#include "graphics/images.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
@@ -608,6 +610,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
registerTFTActionMenuRegions(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
#endif
|
||||
|
||||
// Draw Content
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
@@ -630,7 +635,21 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) {
|
||||
background_yOffset = -1;
|
||||
}
|
||||
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
||||
const int16_t titleBarY = boxTop + 1;
|
||||
const int16_t titleBarHeight = effectiveLineHeight - background_yOffset;
|
||||
display->fillRect(boxLeft, titleBarY, boxWidth, titleBarHeight);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (alertBannerOptions > 0) {
|
||||
const uint16_t titleTextColor =
|
||||
(getActiveTheme().id == ThemeID::DefaultLight) ? TFTPalette::Black : getThemeHeaderText();
|
||||
// Keep title role away from border/corner pixels so rounded-corner masks are not remapped to the title text
|
||||
// color.
|
||||
if (boxWidth > 2 && titleBarHeight > 0) {
|
||||
setAndRegisterTFTColorRole(TFTColorRole::ActionMenuTitle, getThemeHeaderBg(), titleTextColor, boxLeft + 1,
|
||||
titleBarY, boxWidth - 2, titleBarHeight);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
display->setColor(BLACK);
|
||||
int yOffset = 3;
|
||||
if (current_notification_type == notificationTypeEnum::node_picker) {
|
||||
@@ -650,6 +669,7 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
const int barSpacing = 2;
|
||||
const int barHeightStep = 2;
|
||||
const int gap = 6;
|
||||
const int maxBarHeight = totalBars * barHeightStep;
|
||||
|
||||
int textWidth = display->getStringWidth(lineBuffer, strlen(lineBuffer), true);
|
||||
int barsWidth = totalBars * barWidth + (totalBars - 1) * barSpacing + gap;
|
||||
@@ -664,6 +684,20 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
int baseX = groupStartX + textWidth + gap;
|
||||
int baseY = lineY + effectiveLineHeight - 1;
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
if (graphics::bannerSignalBars > 0) {
|
||||
uint16_t signalBarsColor = TFTPalette::Medium;
|
||||
if (graphics::bannerSignalBars <= 1) {
|
||||
signalBarsColor = TFTPalette::Bad;
|
||||
} else if (graphics::bannerSignalBars >= 4) {
|
||||
signalBarsColor = TFTPalette::Good;
|
||||
}
|
||||
const int activeBars = min(graphics::bannerSignalBars, totalBars);
|
||||
const int regionWidth = activeBars * barWidth + (activeBars - 1) * barSpacing;
|
||||
setAndRegisterTFTColorRole(TFTColorRole::SignalBars, signalBarsColor, TFTPalette::Black, baseX,
|
||||
baseY - maxBarHeight, regionWidth, maxBarHeight);
|
||||
}
|
||||
#endif
|
||||
for (int b = 0; b < totalBars; b++) {
|
||||
int barHeight = (b + 1) * barHeightStep;
|
||||
int x = baseX + b * (barWidth + barSpacing);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,8 @@ class UIRenderer
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawBootIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Icon and screen drawing functions
|
||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
255
src/graphics/niche/Drivers/EInk/ED047TC1.cpp
Normal file
255
src/graphics/niche/Drivers/EInk/ED047TC1.cpp
Normal file
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
|
||||
NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1).
|
||||
|
||||
InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
|
||||
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
|
||||
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
|
||||
960×540 FastEPD buffer so content clears the panel's inactive edge border.
|
||||
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
|
||||
|
||||
*/
|
||||
|
||||
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
|
||||
// #define EINK_EDGE_LINES
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
#include "./ED047TC1.h"
|
||||
|
||||
#include "FastEPD.h"
|
||||
#include "configuration.h"
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
// FastEPD helper symbols are defined in FastEPD.inl with C++ linkage.
|
||||
extern void bbepPCA9535DigitalWrite(uint8_t pin, uint8_t value);
|
||||
extern uint8_t bbepPCA9535DigitalRead(uint8_t pin);
|
||||
extern int bbepI2CWrite(unsigned char iAddr, unsigned char *pData, int iLen);
|
||||
extern int bbepI2CReadRegister(unsigned char iAddr, unsigned char u8Register, unsigned char *pData, int iLen);
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
// FastEPD default V2 power callback blocks forever waiting for PWRGOOD.
|
||||
// Replace it with a timeout-safe version so boot never deadlocks.
|
||||
int safeEPDiyV7EinkPower(void *pBBEP, int bOn)
|
||||
{
|
||||
static bool warnedPgood = false;
|
||||
static bool warnedTpsPg = false;
|
||||
static bool warnedTpsWrite = false;
|
||||
|
||||
FASTEPDSTATE *pState = static_cast<FASTEPDSTATE *>(pBBEP);
|
||||
if (!pState) {
|
||||
return BBEP_ERROR_BAD_PARAMETER;
|
||||
}
|
||||
|
||||
if (bOn == pState->pwr_on) {
|
||||
return BBEP_SUCCESS;
|
||||
}
|
||||
|
||||
if (bOn) {
|
||||
bbepPCA9535DigitalWrite(8, 1); // OE on
|
||||
bbepPCA9535DigitalWrite(9, 1); // GMOD on
|
||||
bbepPCA9535DigitalWrite(13, 1); // WAKEUP on
|
||||
bbepPCA9535DigitalWrite(11, 1); // PWRUP on
|
||||
bbepPCA9535DigitalWrite(12, 1); // VCOM CTRL on
|
||||
delay(1);
|
||||
|
||||
const uint32_t pgoodStart = millis();
|
||||
bool pgoodSeen = false;
|
||||
while (!bbepPCA9535DigitalRead(14)) { // CFG_PIN_PWRGOOD
|
||||
if ((millis() - pgoodStart) > 1200) {
|
||||
if (!warnedPgood) {
|
||||
LOG_WARN("ED047TC1: PWRGOOD timeout, continuing with fallback power-on path");
|
||||
warnedPgood = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
delay(1);
|
||||
}
|
||||
if (bbepPCA9535DigitalRead(14)) {
|
||||
pgoodSeen = true;
|
||||
}
|
||||
|
||||
uint8_t ucTemp[4] = {0};
|
||||
ucTemp[0] = 0x01; // TPS_REG_ENABLE
|
||||
ucTemp[1] = 0x3f; // enable rails
|
||||
const int tpsEnableRc = bbepI2CWrite(0x68, ucTemp, 2);
|
||||
|
||||
const int vcom = pState->iVCOM / -10;
|
||||
ucTemp[0] = 3; // VCOM registers 3+4 (L + H)
|
||||
ucTemp[1] = static_cast<uint8_t>(vcom);
|
||||
ucTemp[2] = static_cast<uint8_t>(vcom >> 8);
|
||||
const int tpsVcomRc = bbepI2CWrite(0x68, ucTemp, 3);
|
||||
if ((tpsEnableRc == 0 || tpsVcomRc == 0) && !warnedTpsWrite) {
|
||||
LOG_WARN("ED047TC1: TPS write did not ACK, continuing with fallback");
|
||||
warnedTpsWrite = true;
|
||||
}
|
||||
|
||||
int iTimeout = 0;
|
||||
uint8_t u8Value = 0;
|
||||
while (iTimeout < 220 && ((u8Value & 0xfa) != 0xfa)) {
|
||||
bbepI2CReadRegister(0x68, 0x0F, &u8Value, 1); // TPS_REG_PG
|
||||
iTimeout++;
|
||||
delay(1);
|
||||
}
|
||||
if (iTimeout >= 220 && !warnedTpsPg) {
|
||||
if (pgoodSeen) {
|
||||
LOG_WARN("ED047TC1: TPS power-good register timeout, panel may still work");
|
||||
} else {
|
||||
LOG_WARN("ED047TC1: TPS power-good register timeout after PWRGOOD fallback");
|
||||
}
|
||||
warnedTpsPg = true;
|
||||
}
|
||||
|
||||
pState->pwr_on = 1;
|
||||
} else {
|
||||
bbepPCA9535DigitalWrite(8, 0); // OE off
|
||||
bbepPCA9535DigitalWrite(9, 0); // GMOD off
|
||||
bbepPCA9535DigitalWrite(11, 0); // PWRUP off
|
||||
bbepPCA9535DigitalWrite(12, 0); // VCOM CTRL off
|
||||
delay(1);
|
||||
bbepPCA9535DigitalWrite(13, 0); // WAKEUP off
|
||||
pState->pwr_on = 0;
|
||||
}
|
||||
|
||||
return BBEP_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
class SafeFastEPD : public FASTEPD
|
||||
{
|
||||
public:
|
||||
void installSafePowerHandler()
|
||||
{
|
||||
#if defined(T5_S3_EPAPER_PRO_V2)
|
||||
_state.pfnEinkPower = safeEPDiyV7EinkPower;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
// Parallel display — SPI parameters are not used
|
||||
(void)spi;
|
||||
(void)pin_dc;
|
||||
(void)pin_cs;
|
||||
(void)pin_busy;
|
||||
(void)pin_rst;
|
||||
|
||||
SafeFastEPD *safeEpaper = new SafeFastEPD;
|
||||
epaper = safeEpaper;
|
||||
|
||||
int initRc = BBEP_ERROR_BAD_PARAMETER;
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
// Initialize all PCA9535 port-0 pins as outputs / HIGH
|
||||
for (int i = 0; i < 8; i++) {
|
||||
epaper->ioPinMode(i, OUTPUT);
|
||||
epaper->ioWrite(i, HIGH);
|
||||
}
|
||||
// On this board, the physical side key is labeled IO48; electrically it maps to PCA9535 IO12 (bit 2 on port-1).
|
||||
// FastEPD's generic V7 init drives 8..13 as outputs; force IO12 back to input
|
||||
// so variant touch-control polling can read the key reliably.
|
||||
epaper->ioPinMode(10, INPUT);
|
||||
#else
|
||||
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||
#endif
|
||||
|
||||
if (initRc != BBEP_SUCCESS) {
|
||||
LOG_ERROR("ED047TC1 initPanel failed rc=%d", initRc);
|
||||
return;
|
||||
}
|
||||
|
||||
safeEpaper->installSafePowerHandler();
|
||||
|
||||
const int modeRc = epaper->setMode(BB_MODE_1BPP);
|
||||
if (modeRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 setMode failed rc=%d", modeRc);
|
||||
}
|
||||
|
||||
const int clearRc = epaper->clearWhite();
|
||||
if (clearRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 clearWhite failed rc=%d", clearRc);
|
||||
}
|
||||
|
||||
const int fullRc = epaper->fullUpdate(true); // Blocking initial clear
|
||||
if (fullRc != BBEP_SUCCESS) {
|
||||
LOG_WARN("ED047TC1 initial fullUpdate failed rc=%d", fullRc);
|
||||
}
|
||||
}
|
||||
|
||||
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
if (!epaper)
|
||||
return;
|
||||
|
||||
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
|
||||
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
|
||||
// leaving blank margins at every edge to avoid the panel's inactive border.
|
||||
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
|
||||
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
|
||||
const uint32_t dstTotalRows = 540;
|
||||
|
||||
uint8_t *cur = epaper->currentBuffer();
|
||||
|
||||
// Fill physical buffer with white (0xFF = white in FastEPD 1bpp)
|
||||
memset(cur, 0xFF, dstRowBytes * dstTotalRows);
|
||||
|
||||
// Copy each InkHUD row into the physical buffer with horizontal + vertical offsets
|
||||
for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) {
|
||||
const uint8_t *srcRow = imageData + row * srcRowBytes;
|
||||
uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES;
|
||||
memcpy(dstRow, srcRow, srcRowBytes);
|
||||
}
|
||||
|
||||
#ifdef EINK_EDGE_LINES
|
||||
// Draw a 1px black box at the exact boundary of the safe area within the
|
||||
// physical buffer. If the margins are correct, all 4 lines should be
|
||||
// fully visible and right at the edge of the usable display area.
|
||||
|
||||
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
|
||||
|
||||
const uint32_t safeX = H_OFFSET_BYTES * 8;
|
||||
const uint32_t safeY = V_OFFSET_TOP;
|
||||
const uint32_t safeW = DISPLAY_WIDTH;
|
||||
const uint32_t safeH = DISPLAY_HEIGHT;
|
||||
|
||||
// Top edge: horizontal line at safeY
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY);
|
||||
|
||||
// Bottom edge: horizontal line at safeY + safeH - 1
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY + safeH - 1);
|
||||
|
||||
// Left edge: vertical line at safeX
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX, row);
|
||||
|
||||
// Right edge: vertical line at safeX + safeW - 1
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX + safeW - 1, row);
|
||||
#endif
|
||||
|
||||
if (type == FULL) {
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
} else {
|
||||
// FAST: true partial update - compares pCurrent vs pPrevious and only applies
|
||||
// update waveform to rows that changed. partialUpdate() updates pPrevious.
|
||||
epaper->partialUpdate(false, 0, dstTotalRows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // T5_S3_EPAPER_PRO
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
90
src/graphics/niche/Drivers/EInk/ED047TC1.h
Normal file
90
src/graphics/niche/Drivers/EInk/ED047TC1.h
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver adapter
|
||||
- ED047TC1 (via FastEPD library)
|
||||
- Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro
|
||||
- Size: 4.7 inch
|
||||
- Physical resolution: 960px x 540px
|
||||
- Interface: 8-bit parallel (NOT SPI)
|
||||
|
||||
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
|
||||
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
|
||||
|
||||
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
|
||||
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
|
||||
that content is never drawn into this dead zone. The update() method copies the
|
||||
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
|
||||
H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte),
|
||||
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||
|
||||
Changing these constants shifts content inward from each physical edge:
|
||||
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
|
||||
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
|
||||
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
// Forward declare to avoid pulling FastEPD into all translation units
|
||||
class FASTEPD;
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class ED047TC1 : public EInk
|
||||
{
|
||||
// Safe-area dimensions exposed to InkHUD (physical panel is 960×540).
|
||||
//
|
||||
// The ED047TC1 has an inactive pixel border on all physical edges.
|
||||
// The physical buffer coordinates do NOT directly match the visual orientation
|
||||
// due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW):
|
||||
//
|
||||
// Physical buffer Visual on device (rotation=3)
|
||||
// ───────────────── ──────────────────────────────
|
||||
// Physical LEFT cols → Visual TOP edge
|
||||
// Physical RIGHT cols → Visual BOTTOM edge
|
||||
// Physical TOP rows → Visual RIGHT edge
|
||||
// Physical BOTTOM rows → Visual LEFT edge
|
||||
//
|
||||
// Offset constants shift the InkHUD safe-area away from each physical dead zone:
|
||||
// H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP)
|
||||
// Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM)
|
||||
// V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT)
|
||||
// V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT)
|
||||
//
|
||||
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
|
||||
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
|
||||
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
|
||||
// visual BOTTOM: 960−8−944=8px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
|
||||
|
||||
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
|
||||
|
||||
public:
|
||||
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
|
||||
|
||||
// EInk interface — SPI params are not used for this parallel display
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
|
||||
|
||||
private:
|
||||
FASTEPD *epaper = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
1467
src/graphics/niche/Fonts/FreeSans18pt_Win1253.h
Normal file
1467
src/graphics/niche/Fonts/FreeSans18pt_Win1253.h
Normal file
File diff suppressed because it is too large
Load Diff
2429
src/graphics/niche/Fonts/FreeSans24pt_Win1253.h
Normal file
2429
src/graphics/niche/Fonts/FreeSans24pt_Win1253.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -104,6 +104,15 @@ class Applet : public GFX
|
||||
virtual void onFreeText(char c) {}
|
||||
virtual void onFreeTextDone() {}
|
||||
virtual void onFreeTextCancel() {}
|
||||
// Absolute display-space touch point, for touch-friendly UI interactions.
|
||||
// Return true if consumed.
|
||||
virtual bool onTouchPoint(uint16_t x, uint16_t y, bool longPress)
|
||||
{
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)longPress;
|
||||
return false;
|
||||
}
|
||||
// List of inputs which can be subscribed to
|
||||
enum InputMask { // | No Joystick | With Joystick |
|
||||
BUTTON_SHORT = 1, // | Button Click | Joystick Center Click |
|
||||
|
||||
@@ -88,8 +88,12 @@ class AppletFont
|
||||
|
||||
// Greek
|
||||
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
|
||||
#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3)
|
||||
#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2)
|
||||
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
|
||||
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
|
||||
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
|
||||
|
||||
@@ -43,8 +43,8 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
|
||||
// Add white halo outline first
|
||||
constexpr int outlinePad = 1;
|
||||
int boxSize = 11;
|
||||
int radius = 2; // rounded corner radius
|
||||
int boxSize = fontSmall.lineHeight() + 2; // scale with font so digit fits
|
||||
int radius = max(2, boxSize / 6);
|
||||
|
||||
// White halo background
|
||||
fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
|
||||
@@ -143,17 +143,19 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||
|
||||
int16_t r = fontSmall.lineHeight() / 2; // scale marker with font
|
||||
|
||||
// White fill background + halo
|
||||
fillCircle(centerX, centerY, 8, WHITE); // big white base
|
||||
drawCircle(centerX, centerY, 8, WHITE); // crisp edge
|
||||
fillCircle(centerX, centerY, r + 2, WHITE);
|
||||
drawCircle(centerX, centerY, r + 2, WHITE);
|
||||
|
||||
// Black bullseye on top
|
||||
drawCircle(centerX, centerY, 6, BLACK);
|
||||
fillCircle(centerX, centerY, 2, BLACK);
|
||||
drawCircle(centerX, centerY, r, BLACK);
|
||||
fillCircle(centerX, centerY, max(2, r / 4), BLACK);
|
||||
|
||||
// Crosshairs
|
||||
drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK);
|
||||
drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK);
|
||||
drawLine(centerX - r - 2, centerY, centerX + r + 2, centerY, BLACK);
|
||||
drawLine(centerX, centerY - r - 2, centerX, centerY + r + 2, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,9 +384,9 @@ void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
|
||||
constexpr uint16_t paddingH = 2;
|
||||
constexpr uint16_t paddingW = 4;
|
||||
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
|
||||
constexpr uint16_t markerSizeMin = 5;
|
||||
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||
uint16_t markerSizeMax = fontSmall.lineHeight(); // Scale cross with font
|
||||
uint16_t markerSizeMin = max(5, fontSmall.lineHeight() / 3);
|
||||
|
||||
int16_t textX;
|
||||
int16_t textY;
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./AppSwitcherApplet.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/Tile.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
namespace
|
||||
{
|
||||
static constexpr uint16_t BODY_MARGIN_X = 8;
|
||||
static constexpr uint16_t BODY_MARGIN_Y = 6;
|
||||
static constexpr uint16_t SLOT_GAP_X = 8;
|
||||
static constexpr uint16_t SLOT_GAP_Y = 8;
|
||||
static constexpr uint8_t ICON_RADIUS = 8;
|
||||
static constexpr uint16_t FOOTER_PAD = 4;
|
||||
static constexpr uint16_t LABEL_BOTTOM_PAD = 1;
|
||||
static constexpr uint16_t LABEL_GAP_Y = 1;
|
||||
static constexpr uint16_t TITLE_H_PAD = 8;
|
||||
|
||||
static constexpr uint8_t GRID_COLS = 3;
|
||||
static constexpr uint8_t GRID_ROWS = 4;
|
||||
static constexpr uint8_t ICON_NATIVE_SIZE = 48;
|
||||
static constexpr uint8_t ICON_OUTLINE_STROKE = 1;
|
||||
|
||||
enum class IconKind : uint8_t { GENERIC, ALL_MESSAGES, DMS, CHANNEL, POSITIONS, RECENTS, HEARD, FAVORITES };
|
||||
|
||||
struct GridLayout {
|
||||
uint16_t footerH = 0;
|
||||
uint16_t bodyTop = 0;
|
||||
uint16_t bodyBottom = 0;
|
||||
uint16_t slotW = 0;
|
||||
uint16_t slotH = 0;
|
||||
uint16_t iconBox = 0;
|
||||
};
|
||||
|
||||
/*
|
||||
* Icons sourced from Material Design Icons PNG set (Apache 2.0):
|
||||
* https://github.com/material-icons/material-icons-png
|
||||
*
|
||||
* Families used: outline-2x (48x48)
|
||||
* apps, markunread, chat, forum, place, history, hearing, star_border
|
||||
*/
|
||||
static constexpr uint64_t icon_generic_apps[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
|
||||
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
|
||||
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL,
|
||||
0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x00FF0FF0FF00ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_all_messages[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL,
|
||||
0x0FC0000003F0ULL, 0x0FE0000007F0ULL, 0x0FF800001FF0ULL, 0x0FFE00007FF0ULL, 0x0FFF0000FFF0ULL, 0x0F7FC003FEF0ULL,
|
||||
0x0F1FE007F8F0ULL, 0x0F07F81FE0F0ULL, 0x0F03FE7FC0F0ULL, 0x0F00FFFF00F0ULL, 0x0F007FFE00F0ULL, 0x0F001FF800F0ULL,
|
||||
0x0F0007E000F0ULL, 0x0F0003C000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x07FFFFFFFFE0ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_dms[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x07FFFFFFFFE0ULL, 0x0FFFFFFFFFF0ULL,
|
||||
0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F0FFFFFF0F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F0FFFF000F0ULL, 0x0F00000000F0ULL, 0x0F00000000F0ULL,
|
||||
0x0F00000000F0ULL, 0x0F00000000F0ULL, 0x0F7FFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFF0ULL, 0x0FFFFFFFFFE0ULL,
|
||||
0x0FF000000000ULL, 0x0FE000000000ULL, 0x0FC000000000ULL, 0x0F8000000000ULL, 0x0F0000000000ULL, 0x0E0000000000ULL,
|
||||
0x0C0000000000ULL, 0x080000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_channel[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL,
|
||||
0x0FFFFFFFC000ULL, 0x0FFFFFFFC000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL, 0x0F000003C000ULL,
|
||||
0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL,
|
||||
0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F000003C3F0ULL, 0x0F7FFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL,
|
||||
0x0FFFFFFFC3F0ULL, 0x0FFFFFFFC3F0ULL, 0x0FF0000003F0ULL, 0x0FE0000003F0ULL, 0x0FC0000003F0ULL, 0x0F80000003F0ULL,
|
||||
0x0F0FFFFFFFF0ULL, 0x0E0FFFFFFFF0ULL, 0x0C0FFFFFFFF0ULL, 0x080FFFFFFFF0ULL, 0x000FFFFFFFF0ULL, 0x000FFFFFFFF0ULL,
|
||||
0x000000000FF0ULL, 0x0000000007F0ULL, 0x0000000003F0ULL, 0x0000000001F0ULL, 0x0000000000F0ULL, 0x000000000070ULL,
|
||||
0x000000000030ULL, 0x000000000010ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_positions[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x00001FF80000ULL, 0x00007FFE0000ULL,
|
||||
0x0001FFFF8000ULL, 0x0003FFFFC000ULL, 0x0007FFFFE000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x001F8001F800ULL,
|
||||
0x001F0000F800ULL, 0x003F07E0FC00ULL, 0x003E0FF07C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL,
|
||||
0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E1FF87C00ULL, 0x003E0FF07C00ULL, 0x003E07E07C00ULL, 0x001F0000F800ULL,
|
||||
0x001F0000F800ULL, 0x001F8001F800ULL, 0x000F8001F000ULL, 0x000FC003F000ULL, 0x0007C003E000ULL, 0x0007E007E000ULL,
|
||||
0x0003F00FC000ULL, 0x0003F00FC000ULL, 0x0001F81F8000ULL, 0x0001FC3F8000ULL, 0x0000FC3F0000ULL, 0x00007E7E0000ULL,
|
||||
0x00003FFC0000ULL, 0x00003FFC0000ULL, 0x00001FF80000ULL, 0x00000FF00000ULL, 0x00000FF00000ULL, 0x000007E00000ULL,
|
||||
0x000003C00000ULL, 0x000001800000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_recents[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
0x00000FFF0000ULL, 0x00003FFFC000ULL, 0x0000FFFFF000ULL, 0x0001FFFFF800ULL, 0x0007FFFFFE00ULL, 0x000FF801FF00ULL,
|
||||
0x000FE0007F00ULL, 0x001FC0003F80ULL, 0x003F00000FC0ULL, 0x003F00000FC0ULL, 0x007E00E007E0ULL, 0x007C00E003E0ULL,
|
||||
0x00FC00E003F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL, 0x00F800E001F0ULL,
|
||||
0x3FFFC0F001F0ULL, 0x1FFF80FC01F0ULL, 0x0FFF00FF01F0ULL, 0x07FE003F81F0ULL, 0x03FC001FC1F0ULL, 0x01F80007C3F0ULL,
|
||||
0x00F0000183E0ULL, 0x0060000007E0ULL, 0x000000000FC0ULL, 0x000000000FC0ULL, 0x0001C0003F80ULL, 0x0003E0007F00ULL,
|
||||
0x0007F801FF00ULL, 0x0007FFFFFE00ULL, 0x0001FFFFF800ULL, 0x0000FFFFF000ULL, 0x00003FFFC000ULL, 0x00000FFF0000ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_heard[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000800000000ULL, 0x001C00000000ULL, 0x003E01FF8000ULL, 0x007F07FFE000ULL,
|
||||
0x007E1FFFF800ULL, 0x00FC3FFFFC00ULL, 0x00F87FFFFE00ULL, 0x01F8FF00FF00ULL, 0x01F0FC003F00ULL, 0x01F1F8001F80ULL,
|
||||
0x03E1F0000F80ULL, 0x03E3F07E0FC0ULL, 0x03E3E0FF07C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL, 0x03E3E1FF87C0ULL,
|
||||
0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E1FF8000ULL, 0x03E3E0FF0000ULL, 0x03E3F07E0000ULL, 0x03E1F0000000ULL,
|
||||
0x01F1F8000000ULL, 0x01F1F8000000ULL, 0x01F8FC000000ULL, 0x00F87E000000ULL, 0x00FC7F800000ULL, 0x007E3FC00000ULL,
|
||||
0x007F1FE00000ULL, 0x003E0FF00000ULL, 0x001C03F00000ULL, 0x000801F80000ULL, 0x000000F80000ULL, 0x000000FC0000ULL,
|
||||
0x0000007C07C0ULL, 0x0000007E07C0ULL, 0x0000003F0FC0ULL, 0x0000003FFFC0ULL, 0x0000001FFF80ULL, 0x0000000FFF00ULL,
|
||||
0x00000007FE00ULL, 0x00000003FC00ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
static constexpr uint64_t icon_favorites[48] = {
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000001800000ULL, 0x000001800000ULL,
|
||||
0x000003C00000ULL, 0x000003C00000ULL, 0x000003C00000ULL, 0x000007E00000ULL, 0x000007E00000ULL, 0x00000FF00000ULL,
|
||||
0x00000FF00000ULL, 0x00001FF80000ULL, 0x00001FF80000ULL, 0x00001E780000ULL, 0x00003E7C0000ULL, 0x003FFC3FFC00ULL,
|
||||
0x0FFFFC3FFFF0ULL, 0x0FFFF81FFFF0ULL, 0x03FFF81FFFC0ULL, 0x01F800001F80ULL, 0x00FC00003F00ULL, 0x007E00007E00ULL,
|
||||
0x003F8001FC00ULL, 0x001FC003F800ULL, 0x000FE007F000ULL, 0x0003E007C000ULL, 0x0003E007C000ULL, 0x0003C003C000ULL,
|
||||
0x0003C003C000ULL, 0x0003C3C3C000ULL, 0x0007CFF3E000ULL, 0x00079FF9E000ULL, 0x0007FFFFE000ULL, 0x0007FE7FE000ULL,
|
||||
0x000FFC3FF000ULL, 0x000FF00FF000ULL, 0x000FC003F000ULL, 0x000F8001F000ULL, 0x001E00007000ULL, 0x001800001800ULL,
|
||||
0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL, 0x000000000000ULL,
|
||||
};
|
||||
|
||||
using IconBitmap = const uint64_t *;
|
||||
|
||||
IconBitmap iconBitmapForKind(IconKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case IconKind::ALL_MESSAGES:
|
||||
return icon_all_messages;
|
||||
case IconKind::DMS:
|
||||
return icon_dms;
|
||||
case IconKind::CHANNEL:
|
||||
return icon_channel;
|
||||
case IconKind::POSITIONS:
|
||||
return icon_positions;
|
||||
case IconKind::RECENTS:
|
||||
return icon_recents;
|
||||
case IconKind::HEARD:
|
||||
return icon_heard;
|
||||
case IconKind::FAVORITES:
|
||||
return icon_favorites;
|
||||
case IconKind::GENERIC:
|
||||
default:
|
||||
return icon_generic_apps;
|
||||
}
|
||||
}
|
||||
|
||||
GridLayout computeLayout(const InkHUD::Applet *applet)
|
||||
{
|
||||
GridLayout layout;
|
||||
|
||||
const uint16_t w = applet->width();
|
||||
const uint16_t h = applet->height();
|
||||
|
||||
layout.footerH = InkHUD::Applet::fontSmall.lineHeight() + (FOOTER_PAD * 2);
|
||||
layout.bodyTop = BODY_MARGIN_Y;
|
||||
layout.bodyBottom = (h > (layout.footerH + BODY_MARGIN_Y)) ? (h - layout.footerH - BODY_MARGIN_Y) : layout.bodyTop;
|
||||
|
||||
const uint16_t bodyW = (w > (BODY_MARGIN_X * 2)) ? (w - (BODY_MARGIN_X * 2)) : 1;
|
||||
const uint16_t bodyH = (layout.bodyBottom > layout.bodyTop) ? (layout.bodyBottom - layout.bodyTop) : 1;
|
||||
const uint16_t gapsX = SLOT_GAP_X * (GRID_COLS - 1);
|
||||
const uint16_t gapsY = SLOT_GAP_Y * (GRID_ROWS - 1);
|
||||
|
||||
layout.slotW = (bodyW > gapsX) ? ((bodyW - gapsX) / GRID_COLS) : 1;
|
||||
layout.slotH = (bodyH > gapsY) ? ((bodyH - gapsY) / GRID_ROWS) : 1;
|
||||
|
||||
const uint16_t maxIconW = (layout.slotW > 6) ? (layout.slotW - 6) : layout.slotW;
|
||||
const uint16_t maxIconH = (layout.slotH > (InkHUD::Applet::fontSmall.lineHeight() + LABEL_GAP_Y + LABEL_BOTTOM_PAD + 6))
|
||||
? (layout.slotH - InkHUD::Applet::fontSmall.lineHeight() - LABEL_GAP_Y - LABEL_BOTTOM_PAD - 6)
|
||||
: layout.slotH / 2;
|
||||
layout.iconBox = std::max<uint16_t>(20, std::min<uint16_t>(maxIconW, maxIconH));
|
||||
return layout;
|
||||
}
|
||||
|
||||
std::string lowercase(const char *name)
|
||||
{
|
||||
if (!name)
|
||||
return "";
|
||||
std::string out(name);
|
||||
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { return (char)std::tolower(c); });
|
||||
return out;
|
||||
}
|
||||
|
||||
IconKind iconKindForAppletName(const char *name)
|
||||
{
|
||||
const std::string lower = lowercase(name);
|
||||
if (lower.find("all message") != std::string::npos || lower.find("messages") != std::string::npos)
|
||||
return IconKind::ALL_MESSAGES;
|
||||
if (lower.find("dm") != std::string::npos)
|
||||
return IconKind::DMS;
|
||||
if (lower.find("channel") != std::string::npos)
|
||||
return IconKind::CHANNEL;
|
||||
if (lower.find("position") != std::string::npos)
|
||||
return IconKind::POSITIONS;
|
||||
if (lower.find("recent") != std::string::npos)
|
||||
return IconKind::RECENTS;
|
||||
if (lower.find("heard") != std::string::npos)
|
||||
return IconKind::HEARD;
|
||||
if (lower.find("favorite") != std::string::npos)
|
||||
return IconKind::FAVORITES;
|
||||
return IconKind::GENERIC;
|
||||
}
|
||||
|
||||
void drawIconBitmapScaled(InkHUD::Applet *applet, IconBitmap bmp48, int16_t left, int16_t top, uint16_t boxSize, uint16_t color)
|
||||
{
|
||||
if (!bmp48 || boxSize == 0)
|
||||
return;
|
||||
|
||||
auto srcOn = [bmp48](int16_t sx, int16_t sy) -> bool {
|
||||
if (sx < 0 || sy < 0 || sx >= ICON_NATIVE_SIZE || sy >= ICON_NATIVE_SIZE)
|
||||
return false;
|
||||
const uint64_t rowBits = bmp48[sy];
|
||||
return (rowBits & (1ULL << (47 - sx))) != 0;
|
||||
};
|
||||
|
||||
for (uint16_t y = 0; y < boxSize; y++) {
|
||||
const uint8_t srcY = (uint8_t)((y * ICON_NATIVE_SIZE) / boxSize);
|
||||
for (uint16_t x = 0; x < boxSize; x++) {
|
||||
const uint8_t srcX = (uint8_t)((x * ICON_NATIVE_SIZE) / boxSize);
|
||||
if (!srcOn(srcX, srcY))
|
||||
continue;
|
||||
|
||||
const uint16_t w = std::min<uint16_t>(ICON_OUTLINE_STROKE, boxSize - x);
|
||||
const uint16_t h = std::min<uint16_t>(ICON_OUTLINE_STROKE, boxSize - y);
|
||||
applet->fillRect(left + x, top + y, w, h, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InkHUD::AppSwitcherApplet::AppSwitcherApplet()
|
||||
{
|
||||
alwaysRender = true;
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::rebuildActiveAppletList()
|
||||
{
|
||||
activeAppletIndices.clear();
|
||||
|
||||
const auto &settings = inkhud->persistence->settings;
|
||||
const uint8_t tileCount = std::min<uint8_t>(settings.userTiles.count, Persistence::MAX_TILES_GLOBAL);
|
||||
const uint8_t focusedTile = (tileCount > 0) ? std::min<uint8_t>(settings.userTiles.focused, tileCount - 1) : 0;
|
||||
|
||||
// Applets displayed on other tiles should not be selectable here.
|
||||
std::vector<bool> occupiedOnOtherTiles(inkhud->userApplets.size(), false);
|
||||
for (uint8_t tile = 0; tile < tileCount; tile++) {
|
||||
if (tile == focusedTile)
|
||||
continue;
|
||||
|
||||
const uint8_t appletIndex = settings.userTiles.displayedUserApplet[tile];
|
||||
if (appletIndex < occupiedOnOtherTiles.size())
|
||||
occupiedOnOtherTiles[appletIndex] = true;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++) {
|
||||
Applet *a = inkhud->userApplets.at(i);
|
||||
if (a && a->isActive() && !occupiedOnOtherTiles[i])
|
||||
activeAppletIndices.push_back(i);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t InkHUD::AppSwitcherApplet::cardsPerPage() const
|
||||
{
|
||||
return GRID_COLS * GRID_ROWS;
|
||||
}
|
||||
|
||||
uint8_t InkHUD::AppSwitcherApplet::currentPage() const
|
||||
{
|
||||
const uint8_t cpp = cardsPerPage();
|
||||
if (cpp == 0)
|
||||
return 0;
|
||||
return selectedIndex / cpp;
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::stepPage(int8_t delta)
|
||||
{
|
||||
if (activeAppletIndices.empty())
|
||||
return;
|
||||
|
||||
const uint8_t cpp = cardsPerPage();
|
||||
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
|
||||
int16_t nextPage = (int16_t)currentPage() + delta;
|
||||
while (nextPage < 0)
|
||||
nextPage += pageCount;
|
||||
while (nextPage >= pageCount)
|
||||
nextPage -= pageCount;
|
||||
|
||||
selectedIndex = std::min<uint8_t>((uint8_t)(nextPage * cpp), activeAppletIndices.size() - 1);
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::clampSelection()
|
||||
{
|
||||
if (activeAppletIndices.empty()) {
|
||||
selectedIndex = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIndex >= activeAppletIndices.size())
|
||||
selectedIndex = activeAppletIndices.size() - 1;
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::activateSelectedApplet()
|
||||
{
|
||||
if (activeAppletIndices.empty()) {
|
||||
sendToBackground();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t appletIndex = activeAppletIndices.at(selectedIndex);
|
||||
|
||||
sendToBackground();
|
||||
inkhud->showApplet(appletIndex);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onForeground()
|
||||
{
|
||||
rebuildActiveAppletList();
|
||||
clampSelection();
|
||||
handleInput = true;
|
||||
lockRequests = true;
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onBackground()
|
||||
{
|
||||
handleInput = false;
|
||||
lockRequests = false;
|
||||
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->bringToForeground();
|
||||
|
||||
Tile *t = getTile();
|
||||
if (t)
|
||||
t->assignApplet(borrowedTileOwner);
|
||||
borrowedTileOwner = nullptr;
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::show(Tile *t)
|
||||
{
|
||||
if (!t)
|
||||
return;
|
||||
|
||||
borrowedTileOwner = t->getAssignedApplet();
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->sendToBackground();
|
||||
|
||||
t->assignApplet(this);
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onRender(bool full)
|
||||
{
|
||||
(void)full;
|
||||
|
||||
const GridLayout layout = computeLayout(this);
|
||||
const uint8_t cpp = cardsPerPage();
|
||||
const uint8_t page = currentPage();
|
||||
const uint8_t pageStart = page * cpp;
|
||||
|
||||
setFont(fontMedium);
|
||||
setTextColor(BLACK);
|
||||
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
drawRect(0, 0, width(), height(), BLACK);
|
||||
|
||||
if (activeAppletIndices.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(width() / 2, height() / 2, "No Available Applets", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < cpp; i++) {
|
||||
const uint8_t idx = pageStart + i;
|
||||
if (idx >= activeAppletIndices.size())
|
||||
break;
|
||||
|
||||
const uint8_t row = i / GRID_COLS;
|
||||
const uint8_t col = i % GRID_COLS;
|
||||
const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X));
|
||||
const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y));
|
||||
const bool selected = (idx == selectedIndex);
|
||||
|
||||
const uint8_t appletIndex = activeAppletIndices.at(idx);
|
||||
Applet *a = inkhud->userApplets.at(appletIndex);
|
||||
if (!a)
|
||||
continue;
|
||||
|
||||
const int16_t iconLeft = slotL + ((layout.slotW - layout.iconBox) / 2);
|
||||
const int16_t iconTop = slotT + 1;
|
||||
|
||||
// Requested style: icon in outlined rounded square only (no filled box, no outer app card).
|
||||
drawRoundRect(iconLeft, iconTop, layout.iconBox, layout.iconBox, ICON_RADIUS, BLACK);
|
||||
if (selected)
|
||||
drawRoundRect(iconLeft + 2, iconTop + 2, layout.iconBox - 4, layout.iconBox - 4, ICON_RADIUS, BLACK);
|
||||
|
||||
const IconBitmap bmp = iconBitmapForKind(iconKindForAppletName(a->name));
|
||||
drawIconBitmapScaled(this, bmp, iconLeft + 3, iconTop + 3, layout.iconBox - 6, BLACK);
|
||||
|
||||
setFont(fontSmall);
|
||||
std::string label = a->name ? a->name : "Applet";
|
||||
const uint16_t maxLabelW = layout.slotW > 4 ? (layout.slotW - 4) : layout.slotW;
|
||||
if (getTextWidth(label) > maxLabelW) {
|
||||
while (!label.empty() && getTextWidth(label + "...") > maxLabelW)
|
||||
label.pop_back();
|
||||
label = label.empty() ? "..." : label + "...";
|
||||
}
|
||||
const int16_t labelY = iconTop + layout.iconBox + LABEL_GAP_Y;
|
||||
setTextColor(BLACK);
|
||||
printAt(slotL + (layout.slotW / 2), labelY, label.c_str(), CENTER, TOP);
|
||||
|
||||
if (a->isForeground())
|
||||
fillCircle(iconLeft + layout.iconBox - 4, iconTop + 4, 2, BLACK);
|
||||
}
|
||||
|
||||
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
|
||||
if (pageCount > 1) {
|
||||
setFont(fontSmall);
|
||||
setTextColor(BLACK);
|
||||
const int16_t footerY = height() - layout.footerH + FOOTER_PAD;
|
||||
printAt(TITLE_H_PAD, footerY, "<", LEFT, TOP);
|
||||
printAt(width() - TITLE_H_PAD, footerY, ">", RIGHT, TOP);
|
||||
const std::string pageText = std::to_string(page + 1) + "/" + std::to_string(pageCount);
|
||||
printAt(width() / 2, footerY, pageText.c_str(), CENTER, TOP);
|
||||
}
|
||||
}
|
||||
|
||||
bool InkHUD::AppSwitcherApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress)
|
||||
{
|
||||
(void)longPress;
|
||||
|
||||
Tile *t = getTile();
|
||||
if (!t || activeAppletIndices.empty())
|
||||
return true;
|
||||
|
||||
const uint16_t tileL = t->getLeft();
|
||||
const uint16_t tileT = t->getTop();
|
||||
const uint16_t tileR = tileL + t->getWidth();
|
||||
const uint16_t tileB = tileT + t->getHeight();
|
||||
if (x < tileL || x >= tileR || y < tileT || y >= tileB)
|
||||
return false;
|
||||
|
||||
const GridLayout layout = computeLayout(this);
|
||||
const uint8_t cpp = cardsPerPage();
|
||||
const uint8_t page = currentPage();
|
||||
const uint8_t pageStart = page * cpp;
|
||||
const int16_t localX = (int16_t)x - (int16_t)tileL;
|
||||
const int16_t localY = (int16_t)y - (int16_t)tileT;
|
||||
|
||||
for (uint8_t i = 0; i < cpp; i++) {
|
||||
const uint8_t idx = pageStart + i;
|
||||
if (idx >= activeAppletIndices.size())
|
||||
break;
|
||||
|
||||
const uint8_t row = i / GRID_COLS;
|
||||
const uint8_t col = i % GRID_COLS;
|
||||
const int16_t slotL = BODY_MARGIN_X + (col * (layout.slotW + SLOT_GAP_X));
|
||||
const int16_t slotT = layout.bodyTop + (row * (layout.slotH + SLOT_GAP_Y));
|
||||
|
||||
if (localX < slotL || localX >= (slotL + (int16_t)layout.slotW))
|
||||
continue;
|
||||
if (localY < slotT || localY >= (slotT + (int16_t)layout.slotH))
|
||||
continue;
|
||||
|
||||
selectedIndex = idx;
|
||||
clampSelection();
|
||||
activateSelectedApplet();
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t pageCount = std::max<uint8_t>(1, (activeAppletIndices.size() + cpp - 1) / cpp);
|
||||
if (pageCount <= 1)
|
||||
return true;
|
||||
|
||||
const int16_t footerTop = height() - layout.footerH;
|
||||
if (localY >= footerTop) {
|
||||
if (localX < (int16_t)(width() / 3))
|
||||
stepPage(-1);
|
||||
else if (localX >= (int16_t)((width() * 2) / 3))
|
||||
stepPage(1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onButtonShortPress()
|
||||
{
|
||||
if (activeAppletIndices.empty())
|
||||
return;
|
||||
|
||||
selectedIndex = (selectedIndex + 1) % activeAppletIndices.size();
|
||||
clampSelection();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onButtonLongPress()
|
||||
{
|
||||
activateSelectedApplet();
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onExitShort()
|
||||
{
|
||||
sendToBackground();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onNavUp()
|
||||
{
|
||||
if (activeAppletIndices.empty())
|
||||
return;
|
||||
|
||||
if (selectedIndex == 0)
|
||||
selectedIndex = activeAppletIndices.size() - 1;
|
||||
else
|
||||
selectedIndex--;
|
||||
|
||||
clampSelection();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::AppSwitcherApplet::onNavDown()
|
||||
{
|
||||
if (activeAppletIndices.empty())
|
||||
return;
|
||||
|
||||
selectedIndex = (selectedIndex + 1) % activeAppletIndices.size();
|
||||
clampSelection();
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class Tile;
|
||||
|
||||
class AppSwitcherApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
AppSwitcherApplet();
|
||||
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override;
|
||||
|
||||
// Open the app switcher on a user tile and temporarily replace the tile's owner.
|
||||
void show(Tile *t);
|
||||
|
||||
private:
|
||||
void rebuildActiveAppletList();
|
||||
void clampSelection();
|
||||
uint8_t cardsPerPage() const;
|
||||
uint8_t currentPage() const;
|
||||
void stepPage(int8_t delta);
|
||||
void activateSelectedApplet();
|
||||
|
||||
std::vector<uint8_t> activeAppletIndices;
|
||||
uint8_t selectedIndex = 0;
|
||||
|
||||
Applet *borrowedTileOwner = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -1,155 +1,100 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
#include "./KeyboardApplet.h"
|
||||
|
||||
#include <cctype>
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool usePortraitKeyboardSizing()
|
||||
{
|
||||
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
|
||||
return inkhud && inkhud->height() > inkhud->width();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InkHUD::KeyboardApplet::KeyboardApplet()
|
||||
{
|
||||
// Calculate row widths
|
||||
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||
rowWidths[row] = 0;
|
||||
for (uint8_t col = 0; col < KBD_COLS; col++)
|
||||
rowWidths[row] += keyWidths[row * KBD_COLS + col];
|
||||
}
|
||||
mode = MODE_TEXT;
|
||||
lastTypingMode = MODE_TEXT;
|
||||
emotePage = 0;
|
||||
selectedKey = 0;
|
||||
prevSelectedKey = 0;
|
||||
normalizeSelection();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onRender(bool full)
|
||||
{
|
||||
uint16_t em = fontSmall.lineHeight(); // 16 pt
|
||||
uint16_t keyH = Y(1.0) / KBD_ROWS;
|
||||
int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
|
||||
const bool showSelection = showSelectionHighlight();
|
||||
|
||||
if (full) { // Draw full keyboard
|
||||
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||
|
||||
// Calculate the remaining space to be used as padding
|
||||
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
|
||||
// Draw keys
|
||||
uint16_t xPos = 0;
|
||||
for (uint8_t col = 0; col < KBD_COLS; col++) {
|
||||
Color fgcolor = BLACK;
|
||||
uint8_t index = row * KBD_COLS + col;
|
||||
uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
|
||||
uint16_t keyY = row * keyH;
|
||||
uint16_t keyW = (keyWidths[index] * em) >> 4;
|
||||
if (index == selectedKey) {
|
||||
fgcolor = WHITE;
|
||||
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||
}
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
|
||||
xPos += keyWidths[index];
|
||||
}
|
||||
}
|
||||
} else { // Only draw the difference
|
||||
if (selectedKey != prevSelectedKey) {
|
||||
// Draw previously selected key
|
||||
uint8_t row = prevSelectedKey / KBD_COLS;
|
||||
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
uint16_t xPos = 0;
|
||||
for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
|
||||
xPos += keyWidths[i];
|
||||
uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||
uint16_t keyY = row * keyH;
|
||||
uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
|
||||
fillRect(keyX, keyY, keyW, keyH, WHITE);
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
|
||||
|
||||
// Draw newly selected key
|
||||
row = selectedKey / KBD_COLS;
|
||||
keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
xPos = 0;
|
||||
for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
|
||||
xPos += keyWidths[i];
|
||||
keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||
keyY = row * keyH;
|
||||
keyW = (keyWidths[selectedKey] * em) >> 4;
|
||||
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
|
||||
}
|
||||
if (full) {
|
||||
for (uint8_t i = 0; i < KBD_KEY_COUNT; i++)
|
||||
drawKey(i, showSelection && i == selectedKey);
|
||||
} else if (showSelection && selectedKey != prevSelectedKey) {
|
||||
drawKey(prevSelectedKey, false);
|
||||
drawKey(selectedKey, true);
|
||||
}
|
||||
|
||||
prevSelectedKey = selectedKey;
|
||||
}
|
||||
|
||||
// Draw the key label corresponding to the char
|
||||
// for most keys it draws the character itself
|
||||
// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
|
||||
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
|
||||
void InkHUD::KeyboardApplet::drawKey(uint8_t index, bool selected)
|
||||
{
|
||||
if (key == '\b') {
|
||||
// Draw backspace glyph: 13 x 9 px
|
||||
/**
|
||||
* [][][][][][][][][]
|
||||
* [][] []
|
||||
* [][] [] [] []
|
||||
* [][] [] [] []
|
||||
* [][] [] []
|
||||
* [][] [] [] []
|
||||
* [][] [] [] []
|
||||
* [][] []
|
||||
* [][][][][][][][][]
|
||||
*/
|
||||
const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
|
||||
0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
|
||||
uint16_t leftPadding = (width - 13) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
|
||||
} else if (key == '\n') {
|
||||
// Draw done glyph: 12 x 9 px
|
||||
/**
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][] [][]
|
||||
* [][] [][]
|
||||
* [][][]
|
||||
* []
|
||||
*/
|
||||
const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
|
||||
0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
|
||||
uint16_t leftPadding = (width - 12) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
|
||||
} else if (key == ' ') {
|
||||
// Draw space glyph: 13 x 9 px
|
||||
/**
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* [] []
|
||||
* [] []
|
||||
* [][][][][][][][][][][][][]
|
||||
*
|
||||
*
|
||||
*/
|
||||
const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
|
||||
uint16_t leftPadding = (width - 13) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
|
||||
} else if (key == '\x1b') {
|
||||
setTextColor(color);
|
||||
std::string keyText = "ESC";
|
||||
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||
printAt(left + leftPadding, top, keyText);
|
||||
} else {
|
||||
setTextColor(color);
|
||||
if (key >= 0x61)
|
||||
key -= 32; // capitalize
|
||||
std::string keyText = std::string(1, key);
|
||||
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||
printAt(left + leftPadding, top, keyText);
|
||||
uint16_t keyX = 0;
|
||||
uint16_t keyY = 0;
|
||||
uint16_t keyW = 0;
|
||||
uint16_t keyH = 0;
|
||||
if (!getKeyBounds(index, keyX, keyY, keyW, keyH))
|
||||
return;
|
||||
if (keyW == 0 || keyH == 0)
|
||||
return;
|
||||
|
||||
// Translate absolute tile coordinates into applet-local coordinates.
|
||||
const int16_t localX = keyX - getTile()->getLeft();
|
||||
const int16_t localY = keyY - getTile()->getTop();
|
||||
const bool enabled = isKeyEnabledAt(index);
|
||||
|
||||
// Clean background first so hidden keys never leave stale pixels when mode changes.
|
||||
fillRect(localX, localY, keyW, keyH, WHITE);
|
||||
|
||||
if (!enabled)
|
||||
return;
|
||||
|
||||
fillRoundRect(localX, localY, keyW, keyH, KEY_RADIUS, selected ? BLACK : WHITE);
|
||||
drawRoundRect(localX, localY, keyW, keyH, KEY_RADIUS, BLACK);
|
||||
|
||||
const int16_t labelTop = localY + ((keyH - fontSmall.lineHeight()) / 2);
|
||||
drawKeyLabel(localX, labelTop, keyW, getKeyLabelAt(index), selected ? WHITE : BLACK);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, const std::string &label, Color color)
|
||||
{
|
||||
if (label.empty())
|
||||
return;
|
||||
|
||||
setTextColor(color);
|
||||
uint16_t textW = getTextWidth(label);
|
||||
if (textW > width) {
|
||||
// Keep labels readable in narrow keys.
|
||||
textW = getTextWidth("..");
|
||||
printAt(left + ((width - textW) >> 1), top, "..");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t leftPadding = (width - textW) >> 1;
|
||||
printAt(left + leftPadding, top, label);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onForeground()
|
||||
{
|
||||
handleInput = true; // Intercept the button input for our applet
|
||||
|
||||
// Select the first key
|
||||
handleInput = true;
|
||||
mode = MODE_TEXT;
|
||||
lastTypingMode = MODE_TEXT;
|
||||
emotePage = 0;
|
||||
selectedKey = 0;
|
||||
prevSelectedKey = 0;
|
||||
normalizeSelection();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onBackground()
|
||||
@@ -159,32 +104,12 @@ void InkHUD::KeyboardApplet::onBackground()
|
||||
|
||||
void InkHUD::KeyboardApplet::onButtonShortPress()
|
||||
{
|
||||
char key = keys[selectedKey];
|
||||
if (key == '\n') {
|
||||
inkhud->freeTextDone();
|
||||
inkhud->closeKeyboard();
|
||||
} else if (key == '\x1b') {
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
} else {
|
||||
inkhud->freeText(key);
|
||||
}
|
||||
inputSelectedKey(false);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onButtonLongPress()
|
||||
{
|
||||
char key = keys[selectedKey];
|
||||
if (key == '\n') {
|
||||
inkhud->freeTextDone();
|
||||
inkhud->closeKeyboard();
|
||||
} else if (key == '\x1b') {
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
} else {
|
||||
if (key >= 0x61)
|
||||
key -= 32; // capitalize
|
||||
inkhud->freeText(key);
|
||||
}
|
||||
inputSelectedKey(true);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onExitShort()
|
||||
@@ -201,57 +126,377 @@ void InkHUD::KeyboardApplet::onExitLong()
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavUp()
|
||||
{
|
||||
if (selectedKey < KBD_COLS) // wrap
|
||||
if (selectedKey < KBD_COLS)
|
||||
selectedKey += KBD_COLS * (KBD_ROWS - 1);
|
||||
else // move 1 row back
|
||||
else
|
||||
selectedKey -= KBD_COLS;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavDown()
|
||||
{
|
||||
selectedKey += KBD_COLS;
|
||||
selectedKey %= (KBD_COLS * KBD_ROWS);
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
selectedKey %= KBD_KEY_COUNT;
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavLeft()
|
||||
{
|
||||
if (selectedKey % KBD_COLS == 0) // wrap
|
||||
if (selectedKey % KBD_COLS == 0)
|
||||
selectedKey += KBD_COLS - 1;
|
||||
else // move 1 column back
|
||||
else
|
||||
selectedKey--;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavRight()
|
||||
{
|
||||
if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
|
||||
if (selectedKey % KBD_COLS == KBD_COLS - 1)
|
||||
selectedKey -= KBD_COLS - 1;
|
||||
else // move 1 column forward
|
||||
else
|
||||
selectedKey++;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh();
|
||||
}
|
||||
|
||||
bool InkHUD::KeyboardApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress)
|
||||
{
|
||||
// If touch is outside our tile, let other handlers process it.
|
||||
if (!getTile())
|
||||
return false;
|
||||
const uint16_t tileL = getTile()->getLeft();
|
||||
const uint16_t tileT = getTile()->getTop();
|
||||
const uint16_t tileR = tileL + getTile()->getWidth();
|
||||
const uint16_t tileB = tileT + getTile()->getHeight();
|
||||
if (x < tileL || x >= tileR || y < tileT || y >= tileB)
|
||||
return false;
|
||||
|
||||
const int16_t hitIndex = getKeyIndexAt(x, y);
|
||||
// Consume touches that land in keyboard whitespace/disabled cells so we don't
|
||||
// fall back to generic short-press behavior (which would type the old selection).
|
||||
if (hitIndex < 0)
|
||||
return true;
|
||||
|
||||
const uint8_t newSelected = (uint8_t)hitIndex;
|
||||
if (selectedKey != newSelected) {
|
||||
selectedKey = newSelected;
|
||||
normalizeSelection();
|
||||
if (showSelectionHighlight())
|
||||
requestFastKeyboardRefresh();
|
||||
}
|
||||
|
||||
if (!isKeyEnabledAt(selectedKey))
|
||||
return true;
|
||||
|
||||
inputSelectedKey(longPress);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InkHUD::KeyboardApplet::getKeyBounds(uint8_t index, uint16_t &left, uint16_t &top, uint16_t &width, uint16_t &height)
|
||||
{
|
||||
if (index >= KBD_KEY_COUNT || !getTile())
|
||||
return false;
|
||||
|
||||
const uint16_t tileW = getTile()->getWidth();
|
||||
const uint16_t tileH = getTile()->getHeight();
|
||||
const uint16_t tileL = getTile()->getLeft();
|
||||
const uint16_t tileT = getTile()->getTop();
|
||||
const uint8_t row = index / KBD_COLS;
|
||||
const uint8_t col = index % KBD_COLS;
|
||||
|
||||
const uint16_t totalGapY = KEY_GAP_Y * (KBD_ROWS + 1);
|
||||
const uint16_t keyH = (tileH > totalGapY) ? ((tileH - totalGapY) / KBD_ROWS) : (tileH / KBD_ROWS);
|
||||
top = tileT + KEY_GAP_Y + row * (keyH + KEY_GAP_Y);
|
||||
height = keyH;
|
||||
|
||||
const uint16_t totalGapX = KEY_GAP_X * (KBD_COLS + 1);
|
||||
const uint16_t rowSpace = (tileW > totalGapX) ? (tileW - totalGapX) : tileW;
|
||||
uint32_t rowUnits = 0;
|
||||
const uint8_t rowStart = row * KBD_COLS;
|
||||
for (uint8_t i = 0; i < KBD_COLS; i++) {
|
||||
rowUnits += getKeyWidthAt(rowStart + i);
|
||||
}
|
||||
if (rowUnits == 0)
|
||||
return false;
|
||||
|
||||
uint32_t cursorX = tileL + KEY_GAP_X;
|
||||
for (uint8_t i = 0; i < col; i++) {
|
||||
const uint8_t rowIndex = rowStart + i;
|
||||
const uint32_t keyW = ((uint32_t)rowSpace * getKeyWidthAt(rowIndex)) / rowUnits;
|
||||
cursorX += keyW + KEY_GAP_X;
|
||||
}
|
||||
|
||||
left = (uint16_t)cursorX;
|
||||
|
||||
if (col == (KBD_COLS - 1)) {
|
||||
const uint32_t rightEdge = tileL + tileW - KEY_GAP_X;
|
||||
width = (rightEdge > cursorX) ? (uint16_t)(rightEdge - cursorX) : 0;
|
||||
} else {
|
||||
width = (uint16_t)(((uint32_t)rowSpace * getKeyWidthAt(index)) / rowUnits);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int16_t InkHUD::KeyboardApplet::getKeyIndexAt(uint16_t x, uint16_t y)
|
||||
{
|
||||
for (uint8_t i = 0; i < KBD_KEY_COUNT; i++) {
|
||||
uint16_t keyL = 0;
|
||||
uint16_t keyT = 0;
|
||||
uint16_t keyW = 0;
|
||||
uint16_t keyH = 0;
|
||||
if (!getKeyBounds(i, keyL, keyT, keyW, keyH))
|
||||
return -1;
|
||||
|
||||
if (keyW == 0 || keyH == 0)
|
||||
continue;
|
||||
|
||||
if (x >= keyL && x < (keyL + keyW) && y >= keyT && y < (keyT + keyH))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::inputSelectedKey(bool longPress)
|
||||
{
|
||||
inputKeyCode(getKeyCodeAt(selectedKey), longPress);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::inputKeyCode(int16_t keyCode, bool longPress)
|
||||
{
|
||||
if (keyCode == KEY_NONE)
|
||||
return;
|
||||
|
||||
if (keyCode >= KEY_EMOTE_SLOT_BASE) {
|
||||
const uint8_t slot = (uint8_t)(keyCode - KEY_EMOTE_SLOT_BASE);
|
||||
const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + slot;
|
||||
if (emoteIndex < fontEmoteCount)
|
||||
inkhud->freeText((char)fontEmotes[emoteIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY_BACKSPACE:
|
||||
inkhud->freeText('\b');
|
||||
return;
|
||||
case KEY_SEND:
|
||||
inkhud->freeTextDone();
|
||||
inkhud->closeKeyboard();
|
||||
return;
|
||||
case KEY_EMOTE_TOGGLE:
|
||||
toggleEmoteMode();
|
||||
return;
|
||||
case KEY_PUNCT_TOGGLE:
|
||||
case KEY_ALPHA_TOGGLE:
|
||||
togglePunctuationMode();
|
||||
return;
|
||||
case KEY_EMOTE_UP:
|
||||
pageEmotes(false);
|
||||
return;
|
||||
case KEY_EMOTE_DOWN:
|
||||
pageEmotes(true);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (keyCode >= 0 && keyCode <= 0xFF) {
|
||||
char key = (char)keyCode;
|
||||
if (longPress && key >= 'a' && key <= 'z')
|
||||
key = (char)std::toupper((unsigned char)key);
|
||||
inkhud->freeText(key);
|
||||
}
|
||||
}
|
||||
|
||||
int16_t InkHUD::KeyboardApplet::getKeyCodeAt(uint8_t index) const
|
||||
{
|
||||
if (index >= KBD_KEY_COUNT)
|
||||
return KEY_NONE;
|
||||
|
||||
if (mode == MODE_TEXT)
|
||||
return textKeys[index];
|
||||
if (mode == MODE_PUNCT)
|
||||
return punctKeys[index];
|
||||
|
||||
// Emote mode
|
||||
if (index < EMOTE_SLOT_COUNT) {
|
||||
const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + index;
|
||||
if (emoteIndex < fontEmoteCount)
|
||||
return KEY_EMOTE_SLOT_BASE + index;
|
||||
return KEY_NONE;
|
||||
}
|
||||
|
||||
// Emote controls on the bottom row
|
||||
switch (index - EMOTE_SLOT_COUNT) {
|
||||
case 0:
|
||||
return KEY_EMOTE_UP;
|
||||
case 1:
|
||||
return KEY_EMOTE_DOWN;
|
||||
case 2:
|
||||
return KEY_ALPHA_TOGGLE;
|
||||
case 3:
|
||||
return ',';
|
||||
case 4:
|
||||
return ' ';
|
||||
case 5:
|
||||
return '.';
|
||||
case 6:
|
||||
return KEY_SEND;
|
||||
case 7:
|
||||
return KEY_BACKSPACE;
|
||||
default:
|
||||
return KEY_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t InkHUD::KeyboardApplet::getKeyWidthAt(uint8_t index) const
|
||||
{
|
||||
if (index >= KBD_KEY_COUNT)
|
||||
return 0;
|
||||
|
||||
if (mode == MODE_EMOTE)
|
||||
return emoteKeyWidths[index];
|
||||
return typingKeyWidths[index];
|
||||
}
|
||||
|
||||
std::string InkHUD::KeyboardApplet::getKeyLabelAt(uint8_t index) const
|
||||
{
|
||||
const int16_t keyCode = getKeyCodeAt(index);
|
||||
if (keyCode == KEY_NONE)
|
||||
return "";
|
||||
|
||||
if (keyCode >= KEY_EMOTE_SLOT_BASE) {
|
||||
const uint8_t slot = (uint8_t)(keyCode - KEY_EMOTE_SLOT_BASE);
|
||||
const uint16_t emoteIndex = emotePage * EMOTE_SLOT_COUNT + slot;
|
||||
if (emoteIndex < fontEmoteCount)
|
||||
return std::string(1, (char)fontEmotes[emoteIndex]);
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
case KEY_BACKSPACE:
|
||||
return "DEL";
|
||||
case KEY_SEND:
|
||||
return "SEND";
|
||||
case KEY_EMOTE_TOGGLE:
|
||||
return std::string(1, (char)0x03); // Smiling face icon from InkHUD emote font map
|
||||
case KEY_PUNCT_TOGGLE:
|
||||
return "!#1";
|
||||
case KEY_ALPHA_TOGGLE:
|
||||
return "ABC";
|
||||
case KEY_EMOTE_UP:
|
||||
return "UP";
|
||||
case KEY_EMOTE_DOWN:
|
||||
return "DN";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (keyCode >= 0 && keyCode <= 0xFF) {
|
||||
const char c = (char)keyCode;
|
||||
if (c == ' ')
|
||||
return "SPACE";
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return std::string(1, (char)std::toupper((unsigned char)c));
|
||||
return std::string(1, c);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
bool InkHUD::KeyboardApplet::isKeyEnabledAt(uint8_t index) const
|
||||
{
|
||||
return getKeyCodeAt(index) != KEY_NONE;
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::normalizeSelection()
|
||||
{
|
||||
if (selectedKey >= KBD_KEY_COUNT)
|
||||
selectedKey = 0;
|
||||
|
||||
if (isKeyEnabledAt(selectedKey))
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < KBD_KEY_COUNT; i++) {
|
||||
if (isKeyEnabledAt(i)) {
|
||||
selectedKey = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::togglePunctuationMode()
|
||||
{
|
||||
if (mode == MODE_EMOTE) {
|
||||
mode = lastTypingMode;
|
||||
} else {
|
||||
mode = (mode == MODE_TEXT) ? MODE_PUNCT : MODE_TEXT;
|
||||
lastTypingMode = mode;
|
||||
}
|
||||
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh(true);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::toggleEmoteMode()
|
||||
{
|
||||
if (mode == MODE_EMOTE) {
|
||||
mode = lastTypingMode;
|
||||
} else {
|
||||
lastTypingMode = mode;
|
||||
mode = MODE_EMOTE;
|
||||
}
|
||||
|
||||
emotePage = 0;
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh(true);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::pageEmotes(bool down)
|
||||
{
|
||||
if (mode != MODE_EMOTE)
|
||||
return;
|
||||
|
||||
const uint8_t maxPage = (fontEmoteCount == 0) ? 0 : (uint8_t)((fontEmoteCount - 1) / EMOTE_SLOT_COUNT);
|
||||
|
||||
if (down) {
|
||||
if (emotePage < maxPage)
|
||||
emotePage++;
|
||||
} else {
|
||||
if (emotePage > 0)
|
||||
emotePage--;
|
||||
}
|
||||
|
||||
normalizeSelection();
|
||||
requestFastKeyboardRefresh(true);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::requestFastKeyboardRefresh(bool full)
|
||||
{
|
||||
requestUpdate(EInk::UpdateTypes::FAST, full);
|
||||
}
|
||||
|
||||
bool InkHUD::KeyboardApplet::showSelectionHighlight() const
|
||||
{
|
||||
// On touch-capable devices, prioritize input throughput over per-key highlight updates.
|
||||
// E-ink refresh can lag rapid taps; skipping highlight avoids update-induced input latency.
|
||||
return !inkhud->hasTouchEnabledProvider();
|
||||
}
|
||||
|
||||
uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
|
||||
{
|
||||
const uint16_t keyH = fontSmall.lineHeight() * 1.2;
|
||||
return keyH * KBD_ROWS;
|
||||
// Keep touch keys tall and roomy for finger input.
|
||||
// In portrait orientation we increase row height for larger touch targets.
|
||||
const uint16_t rowUnit = fontSmall.lineHeight() + 8;
|
||||
const uint8_t rowScale = usePortraitKeyboardSizing() ? 3 : 2;
|
||||
const uint16_t keyH = rowUnit * rowScale;
|
||||
return (keyH * KBD_ROWS) + (KEY_GAP_Y * (KBD_ROWS + 1));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,7 @@ System Applet to render an on-screen keyboard
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
#include <string>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
@@ -31,34 +32,111 @@ class KeyboardApplet : public SystemApplet
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override;
|
||||
|
||||
static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
|
||||
|
||||
private:
|
||||
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
|
||||
enum KeyCode : int16_t {
|
||||
KEY_NONE = -1,
|
||||
KEY_BACKSPACE = 256,
|
||||
KEY_SEND,
|
||||
KEY_EMOTE_TOGGLE,
|
||||
KEY_PUNCT_TOGGLE,
|
||||
KEY_ALPHA_TOGGLE,
|
||||
KEY_EMOTE_UP,
|
||||
KEY_EMOTE_DOWN,
|
||||
KEY_EMOTE_SLOT_BASE = 512
|
||||
};
|
||||
|
||||
enum KeyboardMode : uint8_t { MODE_TEXT = 0, MODE_PUNCT = 1, MODE_EMOTE = 2 };
|
||||
|
||||
void drawKey(uint8_t index, bool selected);
|
||||
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, const std::string &label, Color color);
|
||||
bool getKeyBounds(uint8_t index, uint16_t &left, uint16_t &top, uint16_t &width, uint16_t &height);
|
||||
int16_t getKeyIndexAt(uint16_t x, uint16_t y);
|
||||
void inputSelectedKey(bool longPress);
|
||||
void inputKeyCode(int16_t keyCode, bool longPress);
|
||||
int16_t getKeyCodeAt(uint8_t index) const;
|
||||
uint16_t getKeyWidthAt(uint8_t index) const;
|
||||
std::string getKeyLabelAt(uint8_t index) const;
|
||||
bool isKeyEnabledAt(uint8_t index) const;
|
||||
void normalizeSelection();
|
||||
void togglePunctuationMode();
|
||||
void toggleEmoteMode();
|
||||
void pageEmotes(bool down);
|
||||
void requestFastKeyboardRefresh(bool full = false);
|
||||
bool showSelectionHighlight() const;
|
||||
|
||||
static const uint8_t KBD_COLS = 11;
|
||||
static const uint8_t KBD_ROWS = 4;
|
||||
static const uint8_t KBD_ROWS = 5;
|
||||
static const uint8_t KBD_KEY_COUNT = KBD_COLS * KBD_ROWS;
|
||||
static const uint8_t EMOTE_SLOT_COUNT = KBD_COLS * (KBD_ROWS - 1); // top 4 rows
|
||||
static constexpr uint8_t fontEmotes[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, 0x09, 0x0B, 0x0C, 0x0E, 0x0F, 0x10, 0x11,
|
||||
0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F};
|
||||
static constexpr uint8_t fontEmoteCount = sizeof(fontEmotes) / sizeof(fontEmotes[0]);
|
||||
|
||||
const char keys[KBD_COLS * KBD_ROWS] = {
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
|
||||
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
|
||||
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
|
||||
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
|
||||
};
|
||||
// Text keyboard (requested layout):
|
||||
// row 0: 1..0
|
||||
// row 1: q..p
|
||||
// row 2: a..l
|
||||
// row 3: EMO, z..m, DEL
|
||||
// row 4: !#1, comma, space, period, SEND
|
||||
const int16_t textKeys[KBD_KEY_COUNT] = {
|
||||
// row 0
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', KEY_NONE,
|
||||
// row 1
|
||||
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', KEY_NONE,
|
||||
// row 2
|
||||
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', KEY_NONE, KEY_NONE,
|
||||
// row 3
|
||||
KEY_EMOTE_TOGGLE, 'z', 'x', 'c', 'v', 'b', 'n', 'm', KEY_BACKSPACE, KEY_NONE, KEY_NONE,
|
||||
// row 4
|
||||
KEY_PUNCT_TOGGLE, ',', ' ', '.', KEY_SEND, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE};
|
||||
|
||||
// This array represents the widths of each key in points
|
||||
// 16 pt = line height of the text
|
||||
const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
|
||||
16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
|
||||
};
|
||||
// Punctuation keyboard (toggle via !#1/ABC)
|
||||
const int16_t punctKeys[KBD_KEY_COUNT] = {
|
||||
// row 0
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', KEY_NONE,
|
||||
// row 1
|
||||
'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', KEY_NONE,
|
||||
// row 2
|
||||
'-', '_', '=', '+', '[', ']', '{', '}', '/', '?', KEY_NONE,
|
||||
// row 3
|
||||
KEY_EMOTE_TOGGLE, ';', ':', '\'', '"', '<', '>', '\\', KEY_BACKSPACE, KEY_NONE, KEY_NONE,
|
||||
// row 4
|
||||
KEY_ALPHA_TOGGLE, ',', ' ', '.', KEY_SEND, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE, KEY_NONE};
|
||||
|
||||
uint16_t rowWidths[KBD_ROWS];
|
||||
uint8_t selectedKey = 0; // selected key index
|
||||
const uint16_t typingKeyWidths[KBD_KEY_COUNT] = {// row 0
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0,
|
||||
// row 1
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0,
|
||||
// row 2
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 0,
|
||||
// row 3
|
||||
18, 12, 12, 12, 12, 12, 12, 12, 20, 0, 0,
|
||||
// row 4
|
||||
20, 12, 56, 12, 24, 0, 0, 0, 0, 0, 0};
|
||||
|
||||
const uint16_t emoteKeyWidths[KBD_KEY_COUNT] = {// row 0
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||
// row 1
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||
// row 2
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||
// row 3
|
||||
12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
||||
// row 4 controls
|
||||
14, 14, 18, 12, 40, 12, 20, 18, 0, 0, 0};
|
||||
|
||||
uint8_t selectedKey = 0;
|
||||
uint8_t prevSelectedKey = 0;
|
||||
uint8_t emotePage = 0;
|
||||
KeyboardMode mode = MODE_TEXT;
|
||||
KeyboardMode lastTypingMode = MODE_TEXT;
|
||||
static constexpr uint8_t KEY_GAP_X = 3;
|
||||
static constexpr uint8_t KEY_GAP_Y = 4;
|
||||
static constexpr uint8_t KEY_RADIUS = 4;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -109,6 +109,7 @@ enum MenuAction {
|
||||
TOGGLE_CHANNEL_POSITION,
|
||||
SET_CHANNEL_PRECISION,
|
||||
// Display
|
||||
SET_DISPLAY_TIMEOUT,
|
||||
TOGGLE_DISPLAY_UNITS,
|
||||
// Network
|
||||
TOGGLE_WIFI,
|
||||
@@ -119,4 +120,4 @@ enum MenuAction {
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "RTC.h"
|
||||
#include "Router.h"
|
||||
#include "airtime.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
#include "main.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "power.h"
|
||||
@@ -27,6 +28,16 @@ static constexpr uint8_t MENU_TIMEOUT_SEC = 60; // How many seconds before menu
|
||||
// These are offered to users as possible values for settings.recentlyActiveSeconds
|
||||
static constexpr uint8_t RECENTS_OPTIONS_MINUTES[] = {2, 5, 10, 30, 60, 120};
|
||||
|
||||
struct DisplayTimeoutOption {
|
||||
uint32_t seconds;
|
||||
const char *label;
|
||||
};
|
||||
|
||||
static constexpr DisplayTimeoutOption DISPLAY_TIMEOUT_OPTIONS[] = {
|
||||
{0, "Forever"}, {30, "30 secs"}, {60, "1 min"}, {5 * 60, "5 min"},
|
||||
{15 * 60, "15 min"}, {30 * 60, "30 min"}, {60 * 60, "1 hr"},
|
||||
};
|
||||
|
||||
struct PositionPrecisionOption {
|
||||
uint8_t value; // proto value
|
||||
const char *metric;
|
||||
@@ -39,6 +50,77 @@ static constexpr PositionPrecisionOption POSITION_PRECISION_OPTIONS[] = {
|
||||
{12, "5.8 km", "3.6 mi"}, {11, "12 km", "7.3 mi"}, {10, "23 km", "15 mi"},
|
||||
};
|
||||
|
||||
static const char *getDisplayTimeoutLabel(uint32_t timeoutSeconds)
|
||||
{
|
||||
constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]);
|
||||
for (uint8_t i = 0; i < optionCount; i++) {
|
||||
if (DISPLAY_TIMEOUT_OPTIONS[i].seconds == timeoutSeconds) {
|
||||
return DISPLAY_TIMEOUT_OPTIONS[i].label;
|
||||
}
|
||||
}
|
||||
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
static bool supportsFreeTextKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings)
|
||||
{
|
||||
return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider());
|
||||
}
|
||||
|
||||
static bool useTouchFriendlyMenuLayout(const InkHUD::InkHUD *inkhud)
|
||||
{
|
||||
return inkhud != nullptr && inkhud->hasTouchEnabledProvider();
|
||||
}
|
||||
|
||||
static uint16_t getMenuItemHeightPx(const InkHUD::InkHUD *inkhud)
|
||||
{
|
||||
const bool touchFriendly = useTouchFriendlyMenuLayout(inkhud);
|
||||
const uint16_t lineH = touchFriendly ? InkHUD::Applet::fontMedium.lineHeight() : InkHUD::Applet::fontSmall.lineHeight();
|
||||
const float rowScale = touchFriendly ? 1.9f : 1.6f;
|
||||
uint16_t itemH = (uint16_t)(lineH * rowScale);
|
||||
if (itemH == 0) {
|
||||
itemH = 1;
|
||||
}
|
||||
return itemH;
|
||||
}
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
namespace
|
||||
{
|
||||
static constexpr uint32_t T5_BACKLIGHT_PREFS_VERSION = 1;
|
||||
|
||||
struct T5BacklightPrefs {
|
||||
uint32_t version = T5_BACKLIGHT_PREFS_VERSION;
|
||||
bool keepOn = true;
|
||||
};
|
||||
|
||||
T5BacklightPrefs t5BacklightPrefs;
|
||||
bool t5BacklightPrefsLoaded = false;
|
||||
|
||||
bool loadT5BacklightKeepOn()
|
||||
{
|
||||
if (!t5BacklightPrefsLoaded) {
|
||||
T5BacklightPrefs loaded;
|
||||
const bool ok = FlashData<T5BacklightPrefs>::load(&loaded, "t5_backlight");
|
||||
if (ok && loaded.version == T5_BACKLIGHT_PREFS_VERSION) {
|
||||
t5BacklightPrefs = loaded;
|
||||
}
|
||||
t5BacklightPrefsLoaded = true;
|
||||
}
|
||||
|
||||
return t5BacklightPrefs.keepOn;
|
||||
}
|
||||
|
||||
void saveT5BacklightKeepOn(bool keepOn)
|
||||
{
|
||||
loadT5BacklightKeepOn();
|
||||
t5BacklightPrefs.version = T5_BACKLIGHT_PREFS_VERSION;
|
||||
t5BacklightPrefs.keepOn = keepOn;
|
||||
FlashData<T5BacklightPrefs>::save(&t5BacklightPrefs, "t5_backlight");
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
||||
{
|
||||
// No timer tasks at boot
|
||||
@@ -47,7 +129,11 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
||||
// Note: don't get instance if we're not actually using the backlight,
|
||||
// or else you will unintentionally instantiate it
|
||||
if (settings->optionalMenuItems.backlight) {
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
t5BacklightSetUserEnabled(loadT5BacklightKeepOn());
|
||||
#else
|
||||
backlight = Drivers::LatchingBacklight::getInstance();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Initialize the Canned Message store
|
||||
@@ -76,9 +162,11 @@ void InkHUD::MenuApplet::onForeground()
|
||||
// backlight on always when menu opens.
|
||||
// Courtesy to T-Echo users who removed the capacitive touch button
|
||||
if (settings->optionalMenuItems.backlight) {
|
||||
#if !defined(T5_S3_EPAPER_PRO)
|
||||
assert(backlight);
|
||||
if (!backlight->isOn())
|
||||
backlight->peek();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Prevent user applets requesting update while menu is open
|
||||
@@ -106,9 +194,11 @@ void InkHUD::MenuApplet::onBackground()
|
||||
// Item in options submenu allows keeping backlight on after menu is closed
|
||||
// If this item is deselected we will turn backlight off again, now that menu is closing
|
||||
if (settings->optionalMenuItems.backlight) {
|
||||
#if !defined(T5_S3_EPAPER_PRO)
|
||||
assert(backlight);
|
||||
if (!backlight->isLatched())
|
||||
backlight->off();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Stop the auto-timeout
|
||||
@@ -333,17 +423,14 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
handleFreeText = true;
|
||||
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
|
||||
freeTextMode = true; // render input field instead of normal menu
|
||||
// Open the on-screen keyboard only for full joystick devices
|
||||
if (settings->joystick.enabled && !inkhud->twoWayRocker)
|
||||
if (supportsFreeTextKeyboard(inkhud, settings))
|
||||
inkhud->openKeyboard();
|
||||
break;
|
||||
|
||||
case STORE_CANNEDMESSAGE_SELECTION:
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||
else
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
|
||||
break;
|
||||
case STORE_CANNEDMESSAGE_SELECTION: {
|
||||
const uint8_t prefixItems = supportsFreeTextKeyboard(inkhud, settings) ? 2 : 1;
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - prefixItems);
|
||||
} break;
|
||||
|
||||
case SEND_CANNEDMESSAGE:
|
||||
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||
@@ -422,14 +509,27 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
break;
|
||||
|
||||
case TOGGLE_BACKLIGHT:
|
||||
// Note: backlight is already on in this situation
|
||||
// We're marking that it should *remain* on once menu closes
|
||||
assert(backlight);
|
||||
// Note: backlight is already on in this situation.
|
||||
// This toggle controls whether it should remain on when menu closes.
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
{
|
||||
const bool keepOn = !t5BacklightIsUserEnabled();
|
||||
t5BacklightSetUserEnabled(keepOn);
|
||||
saveT5BacklightKeepOn(keepOn);
|
||||
if (item.checkState)
|
||||
*(item.checkState) = keepOn;
|
||||
}
|
||||
#else
|
||||
if (!backlight)
|
||||
backlight = Drivers::LatchingBacklight::getInstance();
|
||||
if (backlight->isLatched())
|
||||
backlight->off();
|
||||
else
|
||||
backlight->latch();
|
||||
break;
|
||||
if (item.checkState)
|
||||
*(item.checkState) = backlight->isLatched();
|
||||
#endif
|
||||
break;
|
||||
|
||||
case TOGGLE_12H_CLOCK:
|
||||
config.display.use_12h_clock = !config.display.use_12h_clock;
|
||||
@@ -527,6 +627,17 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
}
|
||||
|
||||
// Display
|
||||
case SET_DISPLAY_TIMEOUT: {
|
||||
// cursor - 1 because index 0 is "Back"
|
||||
const uint8_t index = cursor - 1;
|
||||
constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]);
|
||||
if (index < optionCount) {
|
||||
config.display.screen_on_secs = DISPLAY_TIMEOUT_OPTIONS[index].seconds;
|
||||
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TOGGLE_DISPLAY_UNITS:
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
|
||||
config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC;
|
||||
@@ -893,11 +1004,16 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
previousPage = MenuPage::ROOT;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
// Optional: backlight
|
||||
if (settings->optionalMenuItems.backlight)
|
||||
items.push_back(MenuItem(backlight->isLatched() ? "Backlight Off" : "Keep Backlight On", // Label
|
||||
MenuAction::TOGGLE_BACKLIGHT, // Action
|
||||
MenuPage::EXIT // Exit once complete
|
||||
));
|
||||
if (settings->optionalMenuItems.backlight) {
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
keepBacklightOn = t5BacklightIsUserEnabled();
|
||||
#else
|
||||
if (!backlight)
|
||||
backlight = Drivers::LatchingBacklight::getInstance();
|
||||
keepBacklightOn = backlight->isLatched();
|
||||
#endif
|
||||
items.push_back(MenuItem("Keep Backlight On", MenuAction::TOGGLE_BACKLIGHT, MenuPage::OPTIONS, &keepBacklightOn));
|
||||
}
|
||||
|
||||
// Options Toggles
|
||||
items.push_back(MenuItem("Applets", MenuPage::APPLETS));
|
||||
@@ -1109,6 +1225,9 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
items.push_back(MenuItem("12-Hour Clock", MenuAction::TOGGLE_12H_CLOCK, MenuPage::NODE_CONFIG_DISPLAY,
|
||||
&config.display.use_12h_clock));
|
||||
|
||||
nodeConfigLabels.emplace_back("Screen Timeout: " + std::string(getDisplayTimeoutLabel(config.display.screen_on_secs)));
|
||||
items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::NO_ACTION, MenuPage::NODE_CONFIG_DISPLAY_TIMEOUT));
|
||||
|
||||
const char *unitsLabel =
|
||||
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "Units: Imperial" : "Units: Metric";
|
||||
|
||||
@@ -1118,6 +1237,13 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
break;
|
||||
}
|
||||
|
||||
case NODE_CONFIG_DISPLAY_TIMEOUT:
|
||||
previousPage = MenuPage::NODE_CONFIG_DISPLAY;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
populateDisplayTimeoutPage();
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
break;
|
||||
|
||||
case NODE_CONFIG_BLUETOOTH: {
|
||||
previousPage = MenuPage::NODE_CONFIG;
|
||||
items.push_back(MenuItem("Back", previousPage));
|
||||
@@ -1386,10 +1512,14 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
if (items.size() == 0)
|
||||
LOG_ERROR("Empty Menu");
|
||||
|
||||
const bool touchFriendlyLayout = useTouchFriendlyMenuLayout(inkhud);
|
||||
AppletFont menuItemFont = touchFriendlyLayout ? fontMedium : fontSmall;
|
||||
setFont(menuItemFont);
|
||||
|
||||
// Dimensions for the slots where we will draw menuItems
|
||||
const float padding = 0.05;
|
||||
const uint16_t itemH = fontSmall.lineHeight() * 1.6;
|
||||
const int16_t selectInsetY = 2;
|
||||
const uint16_t itemH = getMenuItemHeightPx(inkhud);
|
||||
const int16_t selectInsetY = touchFriendlyLayout ? 3 : 2;
|
||||
const int16_t itemW = width() - X(padding) - X(padding);
|
||||
const int16_t itemL = X(padding);
|
||||
const int16_t itemR = X(1 - padding);
|
||||
@@ -1422,6 +1552,11 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
itemT = max(siT + siH, 0); // Offset the first menu entry, so menu starts below the system info panel
|
||||
}
|
||||
|
||||
// drawSystemInfoPanel() changes font state (clock/info text).
|
||||
// Restore the active menu font so ROOT page item text matches other menu pages,
|
||||
// including touch-friendly layouts.
|
||||
setFont(menuItemFont);
|
||||
|
||||
// Draw menu items
|
||||
// ===================
|
||||
|
||||
@@ -1449,8 +1584,6 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
|
||||
// Header (non-selectable section label)
|
||||
if (item.isHeader) {
|
||||
setFont(fontSmall);
|
||||
|
||||
// Header text (flush left)
|
||||
printAt(itemL + X(padding), center, item.label, LEFT, MIDDLE);
|
||||
|
||||
@@ -1459,8 +1592,17 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
drawLine(itemL + X(padding), underlineY, itemR - X(padding), underlineY, BLACK);
|
||||
} else {
|
||||
// Box, if currently selected
|
||||
if (cursorShown && i == cursor)
|
||||
drawRect(itemL, itemT + selectInsetY, itemW, itemH - (selectInsetY * 2), BLACK);
|
||||
if (cursorShown && i == cursor && (!touchFriendlyLayout || !hideTouchSelectionHighlight)) {
|
||||
const int16_t selTop = itemT + selectInsetY;
|
||||
const int16_t selH = itemH - (selectInsetY * 2);
|
||||
drawRect(itemL, selTop, itemW, selH, BLACK);
|
||||
// Touch layouts need a stronger visual cue than a thin outline.
|
||||
if (touchFriendlyLayout) {
|
||||
const int16_t markerInset = 3;
|
||||
const int16_t markerW = 4;
|
||||
fillRect(itemL + markerInset, selTop + markerInset, markerW, max(1, selH - (markerInset * 2)), BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
// Indented normal item text
|
||||
printAt(itemL + X(padding * 2), center, item.label, LEFT, MIDDLE);
|
||||
@@ -1468,9 +1610,9 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
|
||||
// Checkbox, if relevant
|
||||
if (item.checkState) {
|
||||
const uint16_t cbWH = fontSmall.lineHeight(); // Checkbox: width / height
|
||||
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
|
||||
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
|
||||
const uint16_t cbWH = menuItemFont.lineHeight(); // Checkbox: width / height
|
||||
const int16_t cbL = itemR - X(padding) - cbWH; // Checkbox: left
|
||||
const int16_t cbT = center - (cbWH / 2); // Checkbox : top
|
||||
// Checkbox ticked
|
||||
if (*(item.checkState)) {
|
||||
drawRect(cbL, cbT, cbWH, cbWH, BLACK);
|
||||
@@ -1499,13 +1641,102 @@ void InkHUD::MenuApplet::onRender(bool full)
|
||||
}
|
||||
}
|
||||
|
||||
bool InkHUD::MenuApplet::onTouchPoint(uint16_t x, uint16_t y, bool longPress)
|
||||
{
|
||||
(void)longPress;
|
||||
|
||||
if (freeTextMode || !getTile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t tileL = getTile()->getLeft();
|
||||
const uint16_t tileT = getTile()->getTop();
|
||||
const uint16_t tileR = tileL + getTile()->getWidth();
|
||||
const uint16_t tileB = tileT + getTile()->getHeight();
|
||||
if (x < tileL || x >= tileR || y < tileT || y >= tileB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (items.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Direct touch controls should act as activity and keep the menu open.
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// If button-driven selection is active on touch-first layouts, clear it as soon as
|
||||
// touch interaction resumes so touch behavior remains direct/tap-first.
|
||||
if (useTouchFriendlyMenuLayout(inkhud)) {
|
||||
cursorShown = false;
|
||||
hideTouchSelectionHighlight = true;
|
||||
}
|
||||
|
||||
const int16_t localY = (int16_t)y - (int16_t)tileT;
|
||||
|
||||
// Keep geometry in sync with onRender() so touch hit-testing matches what users see.
|
||||
const uint16_t itemH = getMenuItemHeightPx(inkhud);
|
||||
int16_t itemT = 0;
|
||||
uint8_t slotCount = (height() - itemT) / itemH;
|
||||
if (slotCount == 0) {
|
||||
slotCount = 1;
|
||||
}
|
||||
const uint16_t &siH = systemInfoPanelHeight;
|
||||
const uint8_t slotsObscured = ceilf(siH / (float)itemH);
|
||||
|
||||
if (currentPage == ROOT) {
|
||||
int16_t siT = 0;
|
||||
const int16_t scrollThreshold = (int16_t)slotCount - (int16_t)slotsObscured - 1;
|
||||
if (scrollThreshold >= 0 && (int16_t)cursor >= scrollThreshold) {
|
||||
siT = 0 - ((cursor - scrollThreshold) * itemH);
|
||||
}
|
||||
itemT = max((int16_t)(siT + siH), (int16_t)0);
|
||||
}
|
||||
|
||||
const uint8_t firstItem = (cursor < slotCount) ? 0 : (cursor - (slotCount - 1));
|
||||
uint16_t visibleEnd = (uint16_t)firstItem + (uint16_t)slotCount;
|
||||
const uint8_t maxIndex = (uint8_t)items.size() - 1;
|
||||
if (visibleEnd > maxIndex) {
|
||||
visibleEnd = maxIndex;
|
||||
}
|
||||
const uint8_t lastItem = (uint8_t)visibleEnd;
|
||||
|
||||
for (uint8_t i = firstItem; i <= lastItem; i++) {
|
||||
const int16_t rowTop = itemT;
|
||||
const int16_t rowBottom = itemT + itemH;
|
||||
|
||||
if (localY >= rowTop && localY < rowBottom) {
|
||||
if (items.at(i).isHeader) {
|
||||
// Consume taps on headers so they don't fall back to button semantics.
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor = i;
|
||||
cursorShown = true;
|
||||
execute(items.at(cursor));
|
||||
|
||||
if (!wantsToRender()) {
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
itemT += itemH;
|
||||
}
|
||||
|
||||
// Consume taps on menu whitespace so we don't trigger button-like fallback behavior.
|
||||
return true;
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onButtonShortPress()
|
||||
{
|
||||
if (!freeTextMode) {
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!settings->joystick.enabled) {
|
||||
// Touch-first nodes keep user-button short-press as "advance selection" in menus.
|
||||
// Any button-driven navigation should restore visible highlight.
|
||||
hideTouchSelectionHighlight = false;
|
||||
if (!settings->joystick.enabled || useTouchFriendlyMenuLayout(inkhud)) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
@@ -1566,6 +1797,32 @@ void InkHUD::MenuApplet::onNavUp()
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// Touch-first menus: swipe up/down should scroll only.
|
||||
// Keep cursor movement for scroll math, but selection box is hidden in onRender().
|
||||
if (useTouchFriendlyMenuLayout(inkhud)) {
|
||||
hideTouchSelectionHighlight = true;
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = items.size() - 1;
|
||||
while (items.at(cursor).isHeader) {
|
||||
if (cursor == 0) {
|
||||
cursorShown = false;
|
||||
break;
|
||||
}
|
||||
cursor--;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
cursor = items.size() - 1;
|
||||
else
|
||||
cursor--;
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the last item that isn't a header
|
||||
@@ -1595,6 +1852,29 @@ void InkHUD::MenuApplet::onNavDown()
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// Touch-first menus: swipe up/down should scroll only.
|
||||
// Keep cursor movement for scroll math, but selection box is hidden in onRender().
|
||||
if (useTouchFriendlyMenuLayout(inkhud)) {
|
||||
hideTouchSelectionHighlight = true;
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
while (cursor < items.size() && items.at(cursor).isHeader) {
|
||||
cursor++;
|
||||
}
|
||||
if (cursor >= items.size()) {
|
||||
cursorShown = false;
|
||||
cursor = 0;
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
// Select the first item that isn't a header
|
||||
@@ -1727,6 +2007,17 @@ void InkHUD::MenuApplet::populateRecentsPage()
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::populateDisplayTimeoutPage()
|
||||
{
|
||||
constexpr uint8_t optionCount = sizeof(DISPLAY_TIMEOUT_OPTIONS) / sizeof(DISPLAY_TIMEOUT_OPTIONS[0]);
|
||||
for (uint8_t i = 0; i < optionCount; i++) {
|
||||
displayTimeoutSelected[i] = (config.display.screen_on_secs == DISPLAY_TIMEOUT_OPTIONS[i].seconds);
|
||||
nodeConfigLabels.emplace_back(DISPLAY_TIMEOUT_OPTIONS[i].label);
|
||||
items.push_back(MenuItem(nodeConfigLabels.back().c_str(), MenuAction::SET_DISPLAY_TIMEOUT, MenuPage::NODE_CONFIG_DISPLAY,
|
||||
&displayTimeoutSelected[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// MenuItem entries for the "send" page
|
||||
// Dynamically creates menu items based on available canned messages
|
||||
void InkHUD::MenuApplet::populateSendPage()
|
||||
@@ -1734,8 +2025,8 @@ void InkHUD::MenuApplet::populateSendPage()
|
||||
// Position / NodeInfo packet
|
||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||
|
||||
// If joystick is available, include the Free Text option
|
||||
if (settings->joystick.enabled && !inkhud->twoWayRocker)
|
||||
// Show the Free Text option on any node that supports the on-screen keyboard.
|
||||
if (supportsFreeTextKeyboard(inkhud, settings))
|
||||
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
|
||||
|
||||
// One menu item for each canned message
|
||||
|
||||
@@ -35,6 +35,7 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void onFreeText(char c) override;
|
||||
void onFreeTextDone() override;
|
||||
void onFreeTextCancel() override;
|
||||
bool onTouchPoint(uint16_t x, uint16_t y, bool longPress) override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
@@ -48,11 +49,12 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||
void showPage(MenuPage page); // Load and display a MenuPage
|
||||
|
||||
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
void populateDisplayTimeoutPage(); // Create menu items for config.display.screen_on_secs
|
||||
|
||||
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||
const std::string &text); // Draw input field for free text
|
||||
@@ -65,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
MenuPage startPageOverride = MenuPage::ROOT;
|
||||
MenuPage currentPage = MenuPage::ROOT;
|
||||
MenuPage previousPage = MenuPage::EXIT;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
bool hideTouchSelectionHighlight = false; // Touch scrolling keeps cursor for paging math, but can hide highlight
|
||||
bool freeTextMode = false;
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
uint16_t menuTextLimit = 200;
|
||||
@@ -80,6 +83,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
// Recents menu checkbox state (derived from settings.recentlyActiveSeconds)
|
||||
static constexpr uint8_t RECENTS_COUNT = 6;
|
||||
bool recentsSelected[RECENTS_COUNT] = {};
|
||||
static constexpr uint8_t DISPLAY_TIMEOUT_COUNT = 7;
|
||||
bool displayTimeoutSelected[DISPLAY_TIMEOUT_COUNT] = {};
|
||||
|
||||
// Data for selecting and sending canned messages via the menu
|
||||
// Placed into a sub-class for organization only
|
||||
@@ -116,7 +121,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options
|
||||
bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options
|
||||
bool keepBacklightOn = false; // Helper to display current backlight latch state in InkHUD options
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -32,6 +32,7 @@ enum MenuPage : uint8_t {
|
||||
NODE_CONFIG_POWER_ADC_CAL,
|
||||
NODE_CONFIG_NETWORK,
|
||||
NODE_CONFIG_DISPLAY,
|
||||
NODE_CONFIG_DISPLAY_TIMEOUT,
|
||||
NODE_CONFIG_BLUETOOTH,
|
||||
NODE_CONFIG_POSITION,
|
||||
NODE_CONFIG_ADMIN_RESET,
|
||||
@@ -45,4 +46,4 @@ enum MenuPage : uint8_t {
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
#include "./TouchStatusApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::TouchStatusApplet::TouchStatusApplet()
|
||||
{
|
||||
alwaysRender = true;
|
||||
}
|
||||
|
||||
void InkHUD::TouchStatusApplet::onRender(bool full)
|
||||
{
|
||||
(void)full;
|
||||
|
||||
if (inkhud->isTouchEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFont(fontSmall);
|
||||
|
||||
const uint16_t barH = fontSmall.lineHeight() + 4;
|
||||
const int16_t top = height() - barH;
|
||||
|
||||
fillRect(0, top, width(), barH, WHITE);
|
||||
drawLine(0, top, width() - 1, top, BLACK);
|
||||
printAt(width() / 2, top + (barH / 2), "TOUCH OFF", CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
Low-profile bottom-edge touch status indicator.
|
||||
Shown only while touch input is disabled.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class TouchStatusApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
TouchStatusApplet();
|
||||
|
||||
void onRender(bool full) override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -47,6 +47,9 @@ InkHUD::TipsApplet::TipsApplet()
|
||||
|
||||
void InkHUD::TipsApplet::onRender(bool full)
|
||||
{
|
||||
const char *continuePrompt =
|
||||
(inkhud && inkhud->hasTouchEnabledProvider()) ? "Tap screen to continue" : "Press button to continue";
|
||||
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
renderWelcome();
|
||||
@@ -79,7 +82,7 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
cursorY += fontSmall.lineHeight() / 2;
|
||||
drawBullet("More info at meshtastic.org");
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::PICK_REGION: {
|
||||
@@ -109,7 +112,7 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
printWrapped(0, cursorY, width(), body);
|
||||
cursorY += bodyH + (fontSmall.lineHeight() / 2);
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::CUSTOMIZATION: {
|
||||
@@ -129,10 +132,13 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
printWrapped(0, cursorY, width(), body);
|
||||
cursorY += bodyH + (fontSmall.lineHeight() / 2);
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::BUTTONS: {
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
renderT5S3ButtonsTip();
|
||||
#else
|
||||
setFont(fontMedium);
|
||||
|
||||
const char *title = "Tip: Buttons";
|
||||
@@ -164,7 +170,8 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
drawBullet("- press: switch tile or close menu");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM);
|
||||
#endif
|
||||
} break;
|
||||
|
||||
case Tip::ROTATION: {
|
||||
@@ -189,7 +196,7 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
printAt(0, Y(1.0), continuePrompt, LEFT, BOTTOM);
|
||||
|
||||
// Revert the "flip screen" setting, preventing this message showing again
|
||||
config.display.flip_screen = false;
|
||||
@@ -198,6 +205,42 @@ void InkHUD::TipsApplet::onRender(bool full)
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
void InkHUD::TipsApplet::renderT5S3ButtonsTip()
|
||||
{
|
||||
setFont(fontMedium);
|
||||
|
||||
const char *title = "Tip: T5-S3 Buttons";
|
||||
uint16_t h = getWrappedTextHeight(0, width(), title);
|
||||
printWrapped(0, 0, width(), title);
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = h + fontSmall.lineHeight();
|
||||
|
||||
auto drawBullet = [&](const char *text) {
|
||||
uint16_t bh = getWrappedTextHeight(0, width(), text);
|
||||
printWrapped(0, cursorY, width(), text);
|
||||
cursorY += bh + (fontSmall.lineHeight() / 3);
|
||||
};
|
||||
|
||||
drawBullet("BOOT button");
|
||||
drawBullet("- short press: next");
|
||||
drawBullet("- long press: open menu or select");
|
||||
|
||||
drawBullet("IO48 button");
|
||||
drawBullet("- short press: toggle touch on/off");
|
||||
drawBullet("- long press: toggle backlight on/off");
|
||||
|
||||
drawBullet("PWR button");
|
||||
drawBullet("- Hold Press to wake after Shutdown");
|
||||
|
||||
drawBullet("HOME button");
|
||||
drawBullet("- press: back/exit in InkHUD and open App switcher");
|
||||
|
||||
printAt(0, Y(1.0), "Tap screen to continue", LEFT, BOTTOM);
|
||||
}
|
||||
#endif
|
||||
|
||||
// This tip has its own render method, only because it's a big block of code
|
||||
// Didn't want to clutter up the switch in onRender too much
|
||||
void InkHUD::TipsApplet::renderWelcome()
|
||||
@@ -244,7 +287,9 @@ void InkHUD::TipsApplet::renderWelcome()
|
||||
|
||||
// Block 3 - press to continue
|
||||
// ============================
|
||||
printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM);
|
||||
const char *continuePrompt =
|
||||
(inkhud && inkhud->hasTouchEnabledProvider()) ? "Tap screen to continue" : "Press button to continue";
|
||||
printAt(X(0.5), Y(1), continuePrompt, CENTER, BOTTOM);
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onForeground()
|
||||
|
||||
@@ -41,6 +41,9 @@ class TipsApplet : public SystemApplet
|
||||
|
||||
protected:
|
||||
void renderWelcome(); // Very first screen of tutorial
|
||||
#if defined(T5_S3_EPAPER_PRO)
|
||||
void renderT5S3ButtonsTip();
|
||||
#endif
|
||||
|
||||
std::deque<Tip> tipQueue; // List of tips to show, one after another
|
||||
|
||||
@@ -49,4 +52,4 @@ class TipsApplet : public SystemApplet
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "./Events.h"
|
||||
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
@@ -14,6 +15,19 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
namespace
|
||||
{
|
||||
// When touch long-press opens menu, some panels report a delayed release/tap
|
||||
// if the finger stays down briefly. Keep this long enough to cover that release.
|
||||
constexpr uint32_t TOUCH_MENU_OPEN_TAP_SUPPRESS_MS = 1200;
|
||||
|
||||
inline void noteInkHUDUserInteraction()
|
||||
{
|
||||
// Keep power state and screen-timeout behavior in sync with InkHUD input activity.
|
||||
powerFSM.trigger(EVENT_INPUT);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InkHUD::Events::Events()
|
||||
{
|
||||
// Get convenient references
|
||||
@@ -38,6 +52,8 @@ void InkHUD::Events::begin()
|
||||
|
||||
void InkHUD::Events::onButtonShort()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
@@ -74,6 +90,8 @@ void InkHUD::Events::onButtonShort()
|
||||
|
||||
void InkHUD::Events::onButtonLong()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
@@ -102,193 +120,295 @@ void InkHUD::Events::onButtonLong()
|
||||
|
||||
void InkHUD::Events::onExitShort()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
// Preserve legacy behavior on non-touch builds:
|
||||
// EXIT input is only active when joystick mode is enabled.
|
||||
// Touch-capable builds intentionally bypass this joystick gate.
|
||||
if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification module (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is change tiles
|
||||
if (consumer)
|
||||
consumer->onExitShort();
|
||||
else if (!dismissedExt) { // Don't change tile if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
// Always let active system applets consume EXIT/HOME input (menu close, keyboard cancel, etc),
|
||||
// including on touch-first nodes where joystick mode is disabled.
|
||||
if (consumer) {
|
||||
consumer->onExitShort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT))
|
||||
userConsumer->onExitShort();
|
||||
else
|
||||
inkhud->nextTile();
|
||||
// Touch-capable InkHUD nodes use EXIT/HOME as a quick app switcher launcher.
|
||||
if (!dismissedExt && inkhud->hasTouchEnabledProvider()) {
|
||||
inkhud->openAppSwitcher();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_SHORT)) {
|
||||
userConsumer->onExitShort();
|
||||
} else if (settings->joystick.enabled) {
|
||||
// Preserve existing joystick behavior when no applet handles EXIT.
|
||||
inkhud->nextTile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onExitLong()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
// Preserve legacy behavior on non-touch builds:
|
||||
// EXIT input is only active when joystick mode is enabled.
|
||||
// Touch-capable builds intentionally bypass this joystick gate.
|
||||
if (!settings->joystick.enabled && !inkhud->hasTouchEnabledProvider()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onExitLong();
|
||||
else {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
// Always allow system applets to consume EXIT/HOME long-press.
|
||||
if (consumer) {
|
||||
consumer->onExitLong();
|
||||
} else {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
|
||||
userConsumer->onExitLong();
|
||||
// Nothing uses exit long yet
|
||||
}
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::EXIT_LONG))
|
||||
userConsumer->onExitLong();
|
||||
// Nothing uses exit long yet
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavUp()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavUp();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
|
||||
userConsumer->onNavUp();
|
||||
}
|
||||
}
|
||||
if (settings->joystick.enabled)
|
||||
onTouchNavUp();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavDown()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavDown();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
|
||||
userConsumer->onNavDown();
|
||||
}
|
||||
}
|
||||
if (settings->joystick.enabled)
|
||||
onTouchNavDown();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavLeft()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavLeft();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
|
||||
userConsumer->onNavLeft();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
}
|
||||
if (settings->joystick.enabled)
|
||||
onTouchNavLeft();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavRight()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
if (settings->joystick.enabled)
|
||||
onTouchNavRight();
|
||||
}
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
void InkHUD::Events::onTouchNavUp()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavRight();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
|
||||
userConsumer->onNavRight();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavUp();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_UP))
|
||||
userConsumer->onNavUp();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onTouchNavDown()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavDown();
|
||||
else if (!dismissedExt) {
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_DOWN))
|
||||
userConsumer->onNavDown();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onTouchNavLeft()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavLeft();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_LEFT))
|
||||
userConsumer->onNavLeft();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onTouchNavRight()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavRight();
|
||||
else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
|
||||
if (userConsumer != nullptr && userConsumer->isInputSubscribed(Applet::NAV_RIGHT))
|
||||
userConsumer->onNavRight();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onTouchTap(uint16_t x, uint16_t y, bool longPress)
|
||||
{
|
||||
const bool touchEnabledBuild = inkhud->hasTouchEnabledProvider();
|
||||
|
||||
// A long-press used to open the menu can be followed by a synthetic/queued tap at release.
|
||||
// Ignore that brief follow-up window so touch-opened menus do not auto-select an item.
|
||||
if (touchEnabledBuild && !longPress && suppressTouchTapUntilMs != 0) {
|
||||
if ((int32_t)(millis() - suppressTouchTapUntilMs) < 0) {
|
||||
noteInkHUDUserInteraction();
|
||||
return;
|
||||
}
|
||||
suppressTouchTapUntilMs = 0;
|
||||
}
|
||||
|
||||
// Give system applets (menu, keyboard, etc) first chance to consume direct touch input.
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput && sa->onTouchPoint(x, y, longPress)) {
|
||||
noteInkHUDUserInteraction();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In split layouts, tapping a different tile changes focus.
|
||||
// Consume this tap so selection does not also trigger button fallback behavior.
|
||||
if (inkhud->selectTileAt(x, y)) {
|
||||
noteInkHUDUserInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow foreground user applet to consume direct touch input if it wants.
|
||||
Applet *userConsumer = inkhud->getActiveApplet();
|
||||
if (userConsumer != nullptr && userConsumer->onTouchPoint(x, y, longPress)) {
|
||||
noteInkHUDUserInteraction();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to existing button semantics so non-touch-aware applets keep working unchanged.
|
||||
if (longPress) {
|
||||
onButtonLong();
|
||||
|
||||
// Only arm suppression if the long-press actually opened menu foreground.
|
||||
SystemApplet *menu = inkhud->getSystemApplet("Menu");
|
||||
if (touchEnabledBuild && menu && menu->isForeground()) {
|
||||
suppressTouchTapUntilMs = millis() + TOUCH_MENU_OPEN_TAP_SUPPRESS_MS;
|
||||
}
|
||||
} else
|
||||
onButtonShort();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeText(char c)
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Trigger the first system applet that wants to handle the new character
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
@@ -300,6 +420,8 @@ void InkHUD::Events::onFreeText(char c)
|
||||
|
||||
void InkHUD::Events::onFreeTextDone()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
@@ -311,6 +433,8 @@ void InkHUD::Events::onFreeTextDone()
|
||||
|
||||
void InkHUD::Events::onFreeTextCancel()
|
||||
{
|
||||
noteInkHUDUserInteraction();
|
||||
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
@@ -487,4 +611,4 @@ bool InkHUD::Events::dismissExternalNotification()
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -17,6 +17,7 @@ however this class handles general events which concern InkHUD as a whole, e.g.
|
||||
|
||||
#include "./InkHUD.h"
|
||||
#include "./Persistence.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
@@ -30,12 +31,17 @@ class Events
|
||||
void onButtonShort(); // User button: short press
|
||||
void onButtonLong(); // User button: long press
|
||||
void applyingChanges();
|
||||
void onExitShort(); // Exit button: short press
|
||||
void onExitLong(); // Exit button: long press
|
||||
void onNavUp(); // Navigate up
|
||||
void onNavDown(); // Navigate down
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
void onExitShort(); // Exit button: short press
|
||||
void onExitLong(); // Exit button: long press
|
||||
void onNavUp(); // Navigate up
|
||||
void onNavDown(); // Navigate down
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
void onTouchNavUp(); // Navigate up from touch input
|
||||
void onTouchNavDown(); // Navigate down from touch input
|
||||
void onTouchNavLeft(); // Navigate left from touch input
|
||||
void onTouchNavRight(); // Navigate right from touch input
|
||||
void onTouchTap(uint16_t x, uint16_t y, bool longPress); // Touch tap/long-press with coordinates
|
||||
|
||||
// Free text typing events
|
||||
void onFreeText(char c); // New freetext character input
|
||||
@@ -79,8 +85,11 @@ class Events
|
||||
|
||||
// If set, InkHUD's data will be erased during onReboot
|
||||
bool eraseOnReboot = false;
|
||||
|
||||
// Suppress follow-up tap generated immediately after a touch long-press opens menu.
|
||||
uint32_t suppressTouchTapUntilMs = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -73,6 +73,24 @@ void InkHUD::InkHUD::begin()
|
||||
// LogoApplet shows boot screen here
|
||||
}
|
||||
|
||||
void InkHUD::InkHUD::setTouchEnabledProvider(TouchEnabledProvider provider)
|
||||
{
|
||||
touchEnabledProvider = provider;
|
||||
}
|
||||
|
||||
bool InkHUD::InkHUD::hasTouchEnabledProvider() const
|
||||
{
|
||||
return touchEnabledProvider != nullptr;
|
||||
}
|
||||
|
||||
bool InkHUD::InkHUD::isTouchEnabled() const
|
||||
{
|
||||
if (!touchEnabledProvider)
|
||||
return true;
|
||||
|
||||
return touchEnabledProvider();
|
||||
}
|
||||
|
||||
// Call this when your user button gets a short press
|
||||
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
|
||||
void InkHUD::InkHUD::shortpress()
|
||||
@@ -175,6 +193,92 @@ void InkHUD::InkHUD::navRight()
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when touch input needs joystick-like up navigation independent of joystick-enabled mode
|
||||
void InkHUD::InkHUD::touchNavUp()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onTouchNavLeft();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onTouchNavDown();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onTouchNavRight();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onTouchNavUp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when touch input needs joystick-like down navigation independent of joystick-enabled mode
|
||||
void InkHUD::InkHUD::touchNavDown()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onTouchNavRight();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onTouchNavUp();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onTouchNavLeft();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onTouchNavDown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when touch input needs joystick-like left navigation independent of joystick-enabled mode
|
||||
void InkHUD::InkHUD::touchNavLeft()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onTouchNavDown();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onTouchNavRight();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onTouchNavUp();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onTouchNavLeft();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when touch input needs joystick-like right navigation independent of joystick-enabled mode
|
||||
void InkHUD::InkHUD::touchNavRight()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onTouchNavUp();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onTouchNavLeft();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onTouchNavDown();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onTouchNavRight();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::InkHUD::touchTap(uint16_t x, uint16_t y)
|
||||
{
|
||||
events->onTouchTap(x, y, false);
|
||||
}
|
||||
|
||||
void InkHUD::InkHUD::touchLongPress(uint16_t x, uint16_t y)
|
||||
{
|
||||
events->onTouchTap(x, y, true);
|
||||
}
|
||||
|
||||
// Call this for keyboard input
|
||||
// The Keyboard Applet also calls this
|
||||
void InkHUD::InkHUD::freeText(char c)
|
||||
@@ -223,6 +327,12 @@ void InkHUD::InkHUD::openMenu()
|
||||
windowManager->openMenu();
|
||||
}
|
||||
|
||||
// Show touch-friendly app switcher (on the focused tile)
|
||||
void InkHUD::InkHUD::openAppSwitcher()
|
||||
{
|
||||
windowManager->openAppSwitcher();
|
||||
}
|
||||
|
||||
// Bring AlignStick applet to the foreground
|
||||
void InkHUD::InkHUD::openAlignStick()
|
||||
{
|
||||
@@ -255,6 +365,16 @@ void InkHUD::InkHUD::prevTile()
|
||||
windowManager->prevTile();
|
||||
}
|
||||
|
||||
bool InkHUD::InkHUD::showApplet(uint8_t appletIndex)
|
||||
{
|
||||
return windowManager->showApplet(appletIndex);
|
||||
}
|
||||
|
||||
bool InkHUD::InkHUD::selectTileAt(uint16_t x, uint16_t y)
|
||||
{
|
||||
return windowManager->selectTileAt(x, y);
|
||||
}
|
||||
|
||||
// Rotate the display image by 90 degrees
|
||||
void InkHUD::InkHUD::rotate()
|
||||
{
|
||||
@@ -376,4 +496,4 @@ void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c)
|
||||
renderer->handlePixel(x, y, c);
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,8 @@ class WindowManager;
|
||||
class InkHUD
|
||||
{
|
||||
public:
|
||||
using TouchEnabledProvider = bool (*)();
|
||||
|
||||
static InkHUD *getInstance(); // Access to this singleton class
|
||||
|
||||
// Configuration
|
||||
@@ -51,6 +53,11 @@ class InkHUD
|
||||
|
||||
void begin();
|
||||
|
||||
// Optional touch-state provider for reusable touch status indicators.
|
||||
void setTouchEnabledProvider(TouchEnabledProvider provider);
|
||||
bool hasTouchEnabledProvider() const;
|
||||
bool isTouchEnabled() const;
|
||||
|
||||
// Handle user-button press
|
||||
// - connected to an input source, in variant nicheGraphics.h
|
||||
|
||||
@@ -62,6 +69,12 @@ class InkHUD
|
||||
void navDown();
|
||||
void navLeft();
|
||||
void navRight();
|
||||
void touchNavUp();
|
||||
void touchNavDown();
|
||||
void touchNavLeft();
|
||||
void touchNavRight();
|
||||
void touchTap(uint16_t x, uint16_t y);
|
||||
void touchLongPress(uint16_t x, uint16_t y);
|
||||
|
||||
// Freetext handlers
|
||||
void freeText(char c);
|
||||
@@ -76,11 +89,14 @@ class InkHUD
|
||||
void prevApplet();
|
||||
NicheGraphics::InkHUD::Applet *getActiveApplet();
|
||||
void openMenu();
|
||||
void openAppSwitcher();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
bool showApplet(uint8_t appletIndex);
|
||||
bool selectTileAt(uint16_t x, uint16_t y);
|
||||
void rotate();
|
||||
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
|
||||
void toggleBatteryIcon();
|
||||
@@ -129,6 +145,7 @@ class InkHUD
|
||||
Events *events = nullptr; // Handle non-specific firmware events
|
||||
Renderer *renderer = nullptr; // Co-ordinate display updates
|
||||
WindowManager *windowManager = nullptr; // Multiplexing of applets
|
||||
TouchEnabledProvider touchEnabledProvider = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -142,4 +142,4 @@ class Persistence
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#include "./WindowManager.h"
|
||||
|
||||
#include "./Applets/System/AlignStick/AlignStickApplet.h"
|
||||
#include "./Applets/System/AppSwitcher/AppSwitcherApplet.h"
|
||||
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
|
||||
#include "./Applets/System/Keyboard/KeyboardApplet.h"
|
||||
#include "./Applets/System/Logo/LogoApplet.h"
|
||||
#include "./Applets/System/Menu/MenuApplet.h"
|
||||
#include "./Applets/System/Notification/NotificationApplet.h"
|
||||
#include "./Applets/System/Notification/TouchStatusApplet.h"
|
||||
#include "./Applets/System/Pairing/PairingApplet.h"
|
||||
#include "./Applets/System/Placeholder/PlaceholderApplet.h"
|
||||
#include "./Applets/System/Tips/TipsApplet.h"
|
||||
@@ -15,6 +17,14 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool supportsOnScreenKeyboard(const InkHUD::InkHUD *inkhud, const InkHUD::Persistence::Settings *settings)
|
||||
{
|
||||
return !inkhud->twoWayRocker && (settings->joystick.enabled || inkhud->hasTouchEnabledProvider());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InkHUD::WindowManager::WindowManager()
|
||||
{
|
||||
// Convenient references
|
||||
@@ -132,6 +142,38 @@ void InkHUD::WindowManager::prevTile()
|
||||
userTiles.at(settings->userTiles.focused)->requestHighlight();
|
||||
}
|
||||
|
||||
// Focus the user tile containing a touch coordinate.
|
||||
// Returns true only when the focused tile changes.
|
||||
bool InkHUD::WindowManager::selectTileAt(uint16_t x, uint16_t y)
|
||||
{
|
||||
if (userTiles.size() < 2)
|
||||
return false;
|
||||
|
||||
const int32_t tx = x;
|
||||
const int32_t ty = y;
|
||||
|
||||
for (uint8_t i = 0; i < userTiles.size(); i++) {
|
||||
Tile *tile = userTiles.at(i);
|
||||
|
||||
const int32_t left = tile->getLeft();
|
||||
const int32_t top = tile->getTop();
|
||||
const int32_t right = left + tile->getWidth();
|
||||
const int32_t bottom = top + tile->getHeight();
|
||||
|
||||
if (tx < left || tx >= right || ty < top || ty >= bottom)
|
||||
continue;
|
||||
|
||||
if (settings->userTiles.focused == i)
|
||||
return false;
|
||||
|
||||
settings->userTiles.focused = i;
|
||||
refocusTile();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show the menu (on the the focused tile)
|
||||
// The applet previously displayed there will be restored once the menu closes
|
||||
void InkHUD::WindowManager::openMenu()
|
||||
@@ -140,6 +182,18 @@ void InkHUD::WindowManager::openMenu()
|
||||
menu->show(userTiles.at(settings->userTiles.focused));
|
||||
}
|
||||
|
||||
// Show touch-only app switcher on the focused tile
|
||||
void InkHUD::WindowManager::openAppSwitcher()
|
||||
{
|
||||
if (!inkhud->hasTouchEnabledProvider())
|
||||
return;
|
||||
|
||||
AppSwitcherApplet *switcher = static_cast<AppSwitcherApplet *>(inkhud->getSystemApplet("AppSwitcher"));
|
||||
if (switcher) {
|
||||
switcher->show(userTiles.at(settings->userTiles.focused));
|
||||
}
|
||||
}
|
||||
|
||||
// Bring the AlignStick applet to the foreground
|
||||
void InkHUD::WindowManager::openAlignStick()
|
||||
{
|
||||
@@ -151,7 +205,7 @@ void InkHUD::WindowManager::openAlignStick()
|
||||
|
||||
void InkHUD::WindowManager::openKeyboard()
|
||||
{
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
if (!supportsOnScreenKeyboard(inkhud, settings))
|
||||
return;
|
||||
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
@@ -165,7 +219,7 @@ void InkHUD::WindowManager::openKeyboard()
|
||||
|
||||
void InkHUD::WindowManager::closeKeyboard()
|
||||
{
|
||||
if (!settings->joystick.enabled || inkhud->twoWayRocker)
|
||||
if (!supportsOnScreenKeyboard(inkhud, settings))
|
||||
return;
|
||||
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
@@ -279,6 +333,41 @@ void InkHUD::WindowManager::prevApplet()
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST); // bringToForeground already requested, but we're manually forcing FAST
|
||||
}
|
||||
|
||||
// Show a specific applet on the focused tile, or focus the tile where it is already shown.
|
||||
bool InkHUD::WindowManager::showApplet(uint8_t appletIndex)
|
||||
{
|
||||
if (appletIndex >= inkhud->userApplets.size())
|
||||
return false;
|
||||
|
||||
Applet *target = inkhud->userApplets.at(appletIndex);
|
||||
if (!target || !target->isActive())
|
||||
return false;
|
||||
|
||||
// If target is already visible on another tile, just focus that tile.
|
||||
for (uint8_t i = 0; i < userTiles.size(); i++) {
|
||||
if (userTiles.at(i)->getAssignedApplet() == target) {
|
||||
settings->userTiles.focused = i;
|
||||
refocusTile();
|
||||
if (!settings->optionalMenuItems.nextTile)
|
||||
userTiles.at(settings->userTiles.focused)->requestHighlight();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise replace the focused tile's applet.
|
||||
Tile *focused = userTiles.at(settings->userTiles.focused);
|
||||
Applet *current = focused->getAssignedApplet();
|
||||
if (current && current != target)
|
||||
current->sendToBackground();
|
||||
|
||||
focused->assignApplet(target);
|
||||
target->bringToForeground();
|
||||
settings->userTiles.displayedUserApplet[settings->userTiles.focused] = appletIndex;
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns active applet
|
||||
NicheGraphics::InkHUD::Applet *InkHUD::WindowManager::getActiveApplet()
|
||||
{
|
||||
@@ -485,14 +574,21 @@ void InkHUD::WindowManager::createSystemApplets()
|
||||
addSystemApplet("Tips", new TipsApplet, new Tile);
|
||||
if (settings->joystick.enabled && !inkhud->twoWayRocker) {
|
||||
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
||||
}
|
||||
if (supportsOnScreenKeyboard(inkhud, settings)) {
|
||||
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
|
||||
}
|
||||
if (inkhud->hasTouchEnabledProvider()) {
|
||||
addSystemApplet("AppSwitcher", new AppSwitcherApplet, nullptr);
|
||||
}
|
||||
|
||||
addSystemApplet("Menu", new MenuApplet, nullptr);
|
||||
|
||||
// Battery and notifications *behind* the menu
|
||||
addSystemApplet("Notification", new NotificationApplet, new Tile);
|
||||
addSystemApplet("BatteryIcon", new BatteryIconApplet, new Tile);
|
||||
if (inkhud->hasTouchEnabledProvider())
|
||||
addSystemApplet("TouchStatus", new TouchStatusApplet, new Tile);
|
||||
|
||||
// Special handling only, via Rendering::renderPlaceholders
|
||||
addSystemApplet("Placeholder", new PlaceholderApplet, nullptr);
|
||||
@@ -511,6 +607,8 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
if (settings->joystick.enabled && !inkhud->twoWayRocker) {
|
||||
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
}
|
||||
if (supportsOnScreenKeyboard(inkhud, settings)) {
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
inkhud->getSystemApplet("Keyboard")
|
||||
->getTile()
|
||||
@@ -527,6 +625,17 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
batteryIconWidth + 1, // width
|
||||
batteryIconHeight + 2); // height
|
||||
|
||||
if (inkhud->hasTouchEnabledProvider()) {
|
||||
const uint16_t touchStatusH = Applet::fontSmall.lineHeight() + 4;
|
||||
inkhud->getSystemApplet("TouchStatus")
|
||||
->getTile()
|
||||
->setRegion(0, inkhud->height() - touchStatusH, inkhud->width(), touchStatusH);
|
||||
if (inkhud->isTouchEnabled())
|
||||
inkhud->getSystemApplet("TouchStatus")->sendToBackground();
|
||||
else
|
||||
inkhud->getSystemApplet("TouchStatus")->bringToForeground();
|
||||
}
|
||||
|
||||
// Note: the tiles of placeholder and menu applets are manipulated specially
|
||||
// - menuApplet borrows user tiles
|
||||
// - placeholder applet is temporarily assigned to each user tile of WindowManager::getEmptyTiles
|
||||
|
||||
@@ -29,13 +29,16 @@ class WindowManager
|
||||
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
bool selectTileAt(uint16_t x, uint16_t y);
|
||||
Applet *getActiveApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openAppSwitcher();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
bool showApplet(uint8_t appletIndex);
|
||||
void rotate();
|
||||
void toggleBatteryIcon();
|
||||
|
||||
@@ -76,4 +79,4 @@ class WindowManager
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -143,6 +143,10 @@ int32_t ButtonThread::runOnce()
|
||||
leadUpSequenceActive = false;
|
||||
resetLeadUpSequence();
|
||||
}
|
||||
#ifdef INPUT_DEBUG
|
||||
if (buttonCurrentlyPressed)
|
||||
LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime);
|
||||
#endif
|
||||
|
||||
// Progressive lead-up sound system
|
||||
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
|
||||
@@ -311,7 +315,8 @@ int32_t ButtonThread::runOnce()
|
||||
void ButtonThread::attachButtonInterrupts()
|
||||
{
|
||||
// Interrupt for user button, during normal use. Improves responsiveness.
|
||||
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
||||
if (_intRoutine != nullptr)
|
||||
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -320,7 +325,8 @@ void ButtonThread::attachButtonInterrupts()
|
||||
*/
|
||||
void ButtonThread::detachButtonInterrupts()
|
||||
{
|
||||
detachInterrupt(_pinNum);
|
||||
if (_intRoutine != nullptr)
|
||||
detachInterrupt(_pinNum);
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
@@ -9,6 +9,35 @@
|
||||
#define TIME_LONG_PRESS 400
|
||||
#endif
|
||||
|
||||
// Touch sampling cadence (milliseconds).
|
||||
// Can be overridden by board variants for faster touch panels.
|
||||
#ifndef TOUCH_POLL_INTERVAL_IDLE
|
||||
#define TOUCH_POLL_INTERVAL_IDLE 100
|
||||
#endif
|
||||
|
||||
#ifndef TOUCH_POLL_INTERVAL_ACTIVE
|
||||
#define TOUCH_POLL_INTERVAL_ACTIVE 20
|
||||
#endif
|
||||
|
||||
#ifndef TOUCH_POLL_INTERVAL_RELEASE
|
||||
#define TOUCH_POLL_INTERVAL_RELEASE 50
|
||||
#endif
|
||||
|
||||
// Faster cadence used for keyboard-like tap-heavy UIs.
|
||||
#ifndef TOUCH_POLL_INTERVAL_ACTIVE_FAST
|
||||
#define TOUCH_POLL_INTERVAL_ACTIVE_FAST TOUCH_POLL_INTERVAL_ACTIVE
|
||||
#endif
|
||||
|
||||
#ifndef TOUCH_POLL_INTERVAL_RELEASE_FAST
|
||||
#define TOUCH_POLL_INTERVAL_RELEASE_FAST TOUCH_POLL_INTERVAL_RELEASE
|
||||
#endif
|
||||
|
||||
// Ignore very short "finger lifted" glitches from noisy touch controllers.
|
||||
// A release is only accepted once we've seen no-touch for at least this duration.
|
||||
#ifndef TOUCH_RELEASE_GRACE_MS
|
||||
#define TOUCH_RELEASE_GRACE_MS 35
|
||||
#endif
|
||||
|
||||
// move a minimum distance over the screen to detect a "swipe"
|
||||
#ifndef TOUCH_THRESHOLD_X
|
||||
#define TOUCH_THRESHOLD_X 30
|
||||
@@ -20,7 +49,7 @@
|
||||
|
||||
TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height)
|
||||
: concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0),
|
||||
_last_y(0), _start(0), _tapped(false), _originName(name)
|
||||
_last_y(0), _start(0), _lastTouchSeenMs(0), _tapped(false), _originName(name)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -28,7 +57,7 @@ void TouchScreenBase::init(bool hasTouch)
|
||||
{
|
||||
if (hasTouch) {
|
||||
LOG_INFO("TouchScreen initialized %d %d", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y);
|
||||
this->setInterval(100);
|
||||
this->setInterval(TOUCH_POLL_INTERVAL_IDLE);
|
||||
} else {
|
||||
disable();
|
||||
this->setInterval(UINT_MAX);
|
||||
@@ -39,6 +68,8 @@ int32_t TouchScreenBase::runOnce()
|
||||
{
|
||||
TouchEvent e;
|
||||
e.touchEvent = static_cast<char>(TOUCH_ACTION_NONE);
|
||||
const bool fastTapMode = fastTapModeEnabled();
|
||||
const bool allowLongPress = longPressEnabled();
|
||||
|
||||
// process touch events
|
||||
int16_t x, y;
|
||||
@@ -46,9 +77,13 @@ int32_t TouchScreenBase::runOnce()
|
||||
if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turning off the screen
|
||||
touched = false;
|
||||
if (touched) {
|
||||
this->setInterval(20);
|
||||
_lastTouchSeenMs = millis();
|
||||
this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_ACTIVE_FAST : TOUCH_POLL_INTERVAL_ACTIVE);
|
||||
_last_x = x;
|
||||
_last_y = y;
|
||||
} else if (_touchedOld && ((uint32_t)millis() - _lastTouchSeenMs) < TOUCH_RELEASE_GRACE_MS) {
|
||||
// Treat brief no-touch samples as continuous touch to preserve long-press detection.
|
||||
touched = true;
|
||||
}
|
||||
if (touched != _touchedOld) {
|
||||
if (touched) {
|
||||
@@ -62,7 +97,7 @@ int32_t TouchScreenBase::runOnce()
|
||||
time_t duration = millis() - _start;
|
||||
x = _last_x;
|
||||
y = _last_y;
|
||||
this->setInterval(50);
|
||||
this->setInterval(fastTapMode ? TOUCH_POLL_INTERVAL_RELEASE_FAST : TOUCH_POLL_INTERVAL_RELEASE);
|
||||
|
||||
// compute distance
|
||||
int16_t dx = x - _first_x;
|
||||
@@ -92,7 +127,7 @@ int32_t TouchScreenBase::runOnce()
|
||||
}
|
||||
// tap
|
||||
else {
|
||||
if (duration > 0 && duration < TIME_LONG_PRESS) {
|
||||
if (duration > 0 && (duration < TIME_LONG_PRESS || !allowLongPress)) {
|
||||
if (_tapped) {
|
||||
_tapped = false;
|
||||
} else {
|
||||
@@ -132,7 +167,7 @@ int32_t TouchScreenBase::runOnce()
|
||||
#endif
|
||||
|
||||
// fire LONG_PRESS event without the need for release
|
||||
if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) {
|
||||
if (allowLongPress && touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) {
|
||||
// tricky: prevent reoccurring events and another touch event when releasing
|
||||
_start = millis() + 30000;
|
||||
e.touchEvent = static_cast<char>(TOUCH_ACTION_LONG_PRESS);
|
||||
@@ -157,3 +192,13 @@ void TouchScreenBase::hapticFeedback()
|
||||
drv.go();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool TouchScreenBase::fastTapModeEnabled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TouchScreenBase::longPressEnabled() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
|
||||
virtual bool getTouch(int16_t &x, int16_t &y) = 0;
|
||||
virtual void onEvent(const TouchEvent &event) = 0;
|
||||
virtual bool fastTapModeEnabled() const;
|
||||
virtual bool longPressEnabled() const;
|
||||
|
||||
volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED;
|
||||
volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE;
|
||||
@@ -49,6 +51,7 @@ class TouchScreenBase : public Observable<const InputEvent *>, public concurrenc
|
||||
int16_t _first_x, _last_x; // horizontal swipe direction
|
||||
int16_t _first_y, _last_y; // vertical swipe direction
|
||||
time_t _start; // for LONG_PRESS
|
||||
uint32_t _lastTouchSeenMs; // helps suppress brief touch-controller dropouts
|
||||
bool _tapped; // for DOUBLE_TAP
|
||||
|
||||
const char *_originName;
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
#endif
|
||||
|
||||
#if ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
@@ -29,7 +35,8 @@ void TouchScreenImpl1::init()
|
||||
return;
|
||||
#else
|
||||
TouchScreenBase::init(true);
|
||||
inputBroker->registerSource(this);
|
||||
if (inputBroker)
|
||||
inputBroker->registerSource(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -38,6 +45,31 @@ bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y)
|
||||
return _getTouch(&x, &y);
|
||||
}
|
||||
|
||||
bool TouchScreenImpl1::fastTapModeEnabled() const
|
||||
{
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
const auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
if (!inkhud) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto *sa : inkhud->systemApplets) {
|
||||
if (!sa || !sa->name) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(sa->name, "Keyboard") == 0) {
|
||||
return sa->isForeground();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TouchScreenImpl1::longPressEnabled() const
|
||||
{
|
||||
return !fastTapModeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief forward touchscreen event
|
||||
*
|
||||
@@ -82,4 +114,4 @@ void TouchScreenImpl1::onEvent(const TouchEvent &event)
|
||||
return;
|
||||
}
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ class TouchScreenImpl1 : public TouchScreenBase
|
||||
protected:
|
||||
virtual bool getTouch(int16_t &x, int16_t &y);
|
||||
virtual void onEvent(const TouchEvent &event);
|
||||
bool fastTapModeEnabled() const override;
|
||||
bool longPressEnabled() const override;
|
||||
|
||||
bool (*_getTouch)(int16_t *, int16_t *);
|
||||
};
|
||||
|
||||
129
src/main.cpp
129
src/main.cpp
@@ -340,138 +340,9 @@ void setup()
|
||||
|
||||
#ifdef BLE_LED
|
||||
pinMode(BLE_LED, OUTPUT);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
#if HAS_SCREEN
|
||||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||||
|
||||
@@ -48,8 +48,11 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
|
||||
// and return false so callers know no extra mixing occurred.
|
||||
RadioLibInterface *radio = RadioLibInterface::instance;
|
||||
if (!radio) {
|
||||
// Intentionally silent: this path runs during portduinoSetup() before the
|
||||
// console/SerialConsole is initialized, so LOG_* here would dereference a null pointer.
|
||||
// This path can run during portduinoSetup() before the console is initialized,
|
||||
// both for unit-test binaries and the simulator's meshtasticd; LOG_* dereferences `console`.
|
||||
if (console) {
|
||||
LOG_ERROR("No radio instance available to provide entropy");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,9 +101,12 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
if (origTx) {
|
||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
||||
// directly from the destination
|
||||
bool wasAlreadyRelayer = wasRelayer(p->relay_node, p->decoded.request_id, p->to);
|
||||
// Single lookup for both relayer checks on the same (request_id, to) pair
|
||||
bool wasAlreadyRelayer = false;
|
||||
bool weWereSoleRelayer = false;
|
||||
bool weWereRelayer = wasRelayer(ourRelayID, p->decoded.request_id, p->to, &weWereSoleRelayer);
|
||||
bool weWereRelayer = false;
|
||||
checkRelayers(p->relay_node, ourRelayID, p->decoded.request_id, p->to, &wasAlreadyRelayer, &weWereRelayer,
|
||||
&weWereSoleRelayer);
|
||||
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
|
||||
if (origTx->next_hop != p->relay_node) { // Not already set
|
||||
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply (was relayer %d we were sole %d)", p->from,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "PacketHistory.h"
|
||||
#include "configuration.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
#ifdef ARCH_PORTDUINO
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
@@ -23,6 +24,14 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
|
||||
size = PACKETHISTORY_MAX; // Use default size if invalid
|
||||
}
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Ensure capacity fits in uint16_t hash index (HASH_EMPTY = 0xFFFF is the sentinel)
|
||||
if (size >= HASH_EMPTY) {
|
||||
LOG_WARN("Packet History - Clamping size %d to %d (hash index limit)", size, HASH_EMPTY - 1);
|
||||
size = HASH_EMPTY - 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Allocate memory for the recent packets array
|
||||
recentPacketsCapacity = size;
|
||||
recentPackets = new PacketRecord[recentPacketsCapacity];
|
||||
@@ -35,6 +44,20 @@ PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPa
|
||||
|
||||
// Initialize the recent packets array to zero
|
||||
memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity);
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Allocate hash index with load factor <= 0.5 for short probe chains
|
||||
hashCapacity = nextPowerOf2(recentPacketsCapacity * 2);
|
||||
hashMask = hashCapacity - 1;
|
||||
hashIndex = new uint16_t[hashCapacity];
|
||||
if (!hashIndex) {
|
||||
LOG_ERROR("Packet History - Hash index allocation failed for %d entries", hashCapacity);
|
||||
hashCapacity = 0;
|
||||
hashMask = 0;
|
||||
return;
|
||||
}
|
||||
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity); // Fill with HASH_EMPTY (0xFFFF)
|
||||
#endif
|
||||
}
|
||||
|
||||
PacketHistory::~PacketHistory()
|
||||
@@ -42,6 +65,12 @@ PacketHistory::~PacketHistory()
|
||||
recentPacketsCapacity = 0;
|
||||
delete[] recentPackets;
|
||||
recentPackets = NULL;
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
delete[] hashIndex;
|
||||
hashIndex = NULL;
|
||||
hashCapacity = 0;
|
||||
hashMask = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/** Update recentPackets and return true if we have already seen this packet */
|
||||
@@ -194,7 +223,78 @@ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpd
|
||||
return seenRecently;
|
||||
}
|
||||
|
||||
/** Find a packet record in history.
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Hash function for (sender, id) pairs. Uses xor-shift mixing for good distribution.
|
||||
uint32_t PacketHistory::hashSlot(NodeNum sender, PacketId id) const
|
||||
{
|
||||
uint32_t h = sender ^ (id * 0x9E3779B9); // Fibonacci hashing constant
|
||||
h ^= h >> 16;
|
||||
h *= 0x45d9f3b;
|
||||
h ^= h >> 16;
|
||||
return h & hashMask;
|
||||
}
|
||||
|
||||
void PacketHistory::hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx)
|
||||
{
|
||||
if (!hashIndex)
|
||||
return;
|
||||
uint32_t bucket = hashSlot(sender, id);
|
||||
// Guard against infinite loop if hash table is corrupted (no HASH_EMPTY slots)
|
||||
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||
if (hashIndex[bucket] == HASH_EMPTY) {
|
||||
hashIndex[bucket] = slotIdx;
|
||||
return;
|
||||
}
|
||||
bucket = (bucket + 1) & hashMask;
|
||||
}
|
||||
LOG_ERROR("Packet History - hashInsert: table full or corrupted, rebuilding");
|
||||
hashRebuild();
|
||||
}
|
||||
|
||||
void PacketHistory::hashRemove(NodeNum sender, PacketId id)
|
||||
{
|
||||
if (!hashIndex)
|
||||
return;
|
||||
uint32_t bucket = hashSlot(sender, id);
|
||||
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||
if (hashIndex[bucket] == HASH_EMPTY)
|
||||
return;
|
||||
uint16_t idx = hashIndex[bucket];
|
||||
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
|
||||
// Found it — delete and re-insert subsequent entries to maintain probe chain integrity
|
||||
hashIndex[bucket] = HASH_EMPTY;
|
||||
uint32_t next = (bucket + 1) & hashMask;
|
||||
for (uint32_t j = 0; j < hashCapacity; j++) {
|
||||
if (hashIndex[next] == HASH_EMPTY)
|
||||
break;
|
||||
uint16_t displaced = hashIndex[next];
|
||||
hashIndex[next] = HASH_EMPTY;
|
||||
if (displaced < recentPacketsCapacity) {
|
||||
const auto &rec = recentPackets[displaced];
|
||||
hashInsert(rec.sender, rec.id, displaced);
|
||||
}
|
||||
next = (next + 1) & hashMask;
|
||||
}
|
||||
return;
|
||||
}
|
||||
bucket = (bucket + 1) & hashMask;
|
||||
}
|
||||
}
|
||||
|
||||
void PacketHistory::hashRebuild()
|
||||
{
|
||||
if (!hashIndex)
|
||||
return;
|
||||
memset(hashIndex, 0xFF, sizeof(uint16_t) * hashCapacity);
|
||||
for (uint32_t i = 0; i < recentPacketsCapacity; i++) {
|
||||
if (recentPackets[i].rxTimeMsec != 0)
|
||||
hashInsert(recentPackets[i].sender, recentPackets[i].id, (uint16_t)i);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Find a packet record in history using the hash index for O(1) average lookup.
|
||||
* Falls back to linear scan if hash index is unavailable.
|
||||
* @return pointer to PacketRecord if found, NULL if not found */
|
||||
PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
||||
{
|
||||
@@ -205,23 +305,40 @@ PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PacketRecord *it = NULL;
|
||||
for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
||||
if (it->id == id && it->sender == sender) {
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Use hash index for O(1) lookup when available
|
||||
if (hashIndex) {
|
||||
uint32_t bucket = hashSlot(sender, id);
|
||||
for (uint32_t i = 0; i < hashCapacity; i++) {
|
||||
if (hashIndex[bucket] == HASH_EMPTY)
|
||||
break;
|
||||
uint16_t idx = hashIndex[bucket];
|
||||
if (idx < recentPacketsCapacity && recentPackets[idx].id == id && recentPackets[idx].sender == sender) {
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender,
|
||||
it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec),
|
||||
it - recentPackets, recentPacketsCapacity);
|
||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d",
|
||||
recentPackets[idx].sender, recentPackets[idx].id, recentPackets[idx].next_hop,
|
||||
recentPackets[idx].relayed_by[0], recentPackets[idx].relayed_by[1], recentPackets[idx].relayed_by[2],
|
||||
millis() - (recentPackets[idx].rxTimeMsec), idx, recentPacketsCapacity);
|
||||
#endif
|
||||
// only the first match is returned, so be careful not to create duplicate entries
|
||||
return it; // Return pointer to the found record
|
||||
return &recentPackets[idx];
|
||||
}
|
||||
bucket = (bucket + 1) & hashMask;
|
||||
}
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
|
||||
#endif
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Linear scan (sole path when hash excluded, fallback when hash allocation failed)
|
||||
for (PacketRecord *it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) {
|
||||
if (it->id == id && it->sender == sender) {
|
||||
return it;
|
||||
}
|
||||
}
|
||||
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id);
|
||||
#endif
|
||||
return NULL; // Not found
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** Insert/Replace oldest PacketRecord in recentPackets. */
|
||||
@@ -327,8 +444,22 @@ void PacketHistory::insert(const PacketRecord &r)
|
||||
return; // Return early if we can't update the history
|
||||
}
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Maintain hash index: remove old entry if evicting a different packet, then insert new entry
|
||||
bool isMatchingSlot = (tu->id == r.id && tu->sender == r.sender);
|
||||
if (!isMatchingSlot && tu->rxTimeMsec != 0) {
|
||||
hashRemove(tu->sender, tu->id);
|
||||
}
|
||||
|
||||
*tu = r; // store the packet
|
||||
|
||||
if (!isMatchingSlot) {
|
||||
hashInsert(r.sender, r.id, (uint16_t)(tu - recentPackets));
|
||||
}
|
||||
#else
|
||||
*tu = r; // store the packet
|
||||
#endif
|
||||
|
||||
#if VERBOSE_PACKET_HISTORY
|
||||
LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER",
|
||||
tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1],
|
||||
@@ -396,6 +527,31 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, boo
|
||||
return found;
|
||||
}
|
||||
|
||||
// Check two relayers against the same packet record with a single find() call,
|
||||
// avoiding redundant O(N) lookups when both are checked for the same (id, sender) pair.
|
||||
void PacketHistory::checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
|
||||
bool *r2WasSole)
|
||||
{
|
||||
*r1Result = false;
|
||||
*r2Result = false;
|
||||
if (r2WasSole)
|
||||
*r2WasSole = false;
|
||||
|
||||
if (!initOk()) {
|
||||
LOG_ERROR("PacketHistory - checkRelayers: NOT INITIALIZED!");
|
||||
return;
|
||||
}
|
||||
|
||||
const PacketRecord *found = find(sender, id);
|
||||
if (!found)
|
||||
return;
|
||||
|
||||
if (relayer1 != 0)
|
||||
*r1Result = wasRelayer(relayer1, *found);
|
||||
if (relayer2 != 0)
|
||||
*r2Result = wasRelayer(relayer2, *found, r2WasSole);
|
||||
}
|
||||
|
||||
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
||||
void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,22 @@ class PacketHistory
|
||||
0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
|
||||
PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat.
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
|
||||
// Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
|
||||
// Maps (sender, id) -> index into recentPackets[]. Uses linear probing with a load factor <= 0.5.
|
||||
// The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
|
||||
// and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
|
||||
static constexpr uint16_t HASH_EMPTY = 0xFFFF;
|
||||
uint16_t *hashIndex = NULL;
|
||||
uint32_t hashCapacity = 0; // Always a power of 2
|
||||
uint32_t hashMask = 0; // hashCapacity - 1, for fast modular indexing
|
||||
|
||||
uint32_t hashSlot(NodeNum sender, PacketId id) const;
|
||||
void hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx);
|
||||
void hashRemove(NodeNum sender, PacketId id);
|
||||
void hashRebuild();
|
||||
#endif
|
||||
|
||||
/** Find a packet record in history.
|
||||
* @param sender NodeNum
|
||||
* @param id PacketId
|
||||
@@ -70,6 +86,16 @@ class PacketHistory
|
||||
* @return true if node was indeed a relayer, false if not */
|
||||
bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);
|
||||
|
||||
/**
|
||||
* Check two relayers against the same packet record with a single lookup.
|
||||
* Avoids redundant find() calls when checking multiple relayers for the same (id, sender) pair.
|
||||
* @param r1Result set to true if relayer1 was a relayer
|
||||
* @param r2Result set to true if relayer2 was a relayer
|
||||
* @param r2WasSole if not nullptr, set to true if relayer2 was the sole relayer
|
||||
*/
|
||||
void checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
|
||||
bool *r2WasSole = nullptr);
|
||||
|
||||
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender
|
||||
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
|
||||
|
||||
|
||||
@@ -470,8 +470,13 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.traffic_management = moduleConfig.traffic_management;
|
||||
break;
|
||||
case meshtastic_ModuleConfig_tak_tag:
|
||||
LOG_DEBUG("Send module config: tak");
|
||||
fromRadioScratch.moduleConfig.which_payload_variant = meshtastic_ModuleConfig_tak_tag;
|
||||
fromRadioScratch.moduleConfig.payload_variant.tak = moduleConfig.tak;
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR("Unknown module config type %d", config_state);
|
||||
LOG_DEBUG("Unhandled module config type %d", config_state);
|
||||
}
|
||||
|
||||
config_state++;
|
||||
|
||||
@@ -46,6 +46,16 @@ RadioLibInterface::RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE c
|
||||
#endif
|
||||
}
|
||||
|
||||
RadioLibInterface::~RadioLibInterface()
|
||||
{
|
||||
// If the static `instance` pointer still references us, clear it.
|
||||
// A later successful init() may have replaced `instance` with a newer
|
||||
// interface — don't clobber that case.
|
||||
if (instance == this) {
|
||||
instance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// ESP32 doesn't use that flag
|
||||
#define YIELD_FROM_ISR(x) portYIELD_FROM_ISR()
|
||||
|
||||
@@ -136,6 +136,13 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
RadioLibInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst,
|
||||
RADIOLIB_PIN_TYPE busy, PhysicalLayer *iface = NULL);
|
||||
|
||||
/**
|
||||
* Clear the static `instance` pointer if it still points at us, so callers
|
||||
* that check `RadioLibInterface::instance != nullptr` don't dereference a
|
||||
* freed object after a failed init() + unique_ptr reset.
|
||||
*/
|
||||
virtual ~RadioLibInterface();
|
||||
|
||||
virtual ErrorCode send(meshtastic_MeshPacket *p) override;
|
||||
|
||||
/**
|
||||
|
||||
@@ -499,9 +499,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
meshtastic_Data decodedtmp;
|
||||
memset(&decodedtmp, 0, sizeof(decodedtmp));
|
||||
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
|
||||
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
|
||||
LOG_DEBUG("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)", p->id);
|
||||
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
|
||||
LOG_ERROR("Invalid portnum (bad psk?)!");
|
||||
LOG_DEBUG("Invalid portnum (bad psk?)");
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
} else if (!owner.is_licensed && isToUs(p) && decodedtmp.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) {
|
||||
LOG_WARN("Rejecting legacy DM");
|
||||
@@ -736,9 +736,13 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
// Also, we should set the time from the ISR and it should have msec level resolution
|
||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
||||
|
||||
// Store a copy of encrypted packet for MQTT
|
||||
// Store a copy of the encrypted packet for MQTT.
|
||||
// Local, not a class member: handleReceived re-enters itself when a module
|
||||
// reply broadcast goes through MeshService::sendToMesh -> Router::sendLocal,
|
||||
// and a member would be silently overwritten without release on the inner
|
||||
// call. Each invocation now owns its own copy (issue #9632, #10101, #8729).
|
||||
DEBUG_HEAP_BEFORE;
|
||||
p_encrypted = packetPool.allocCopy(*p);
|
||||
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
||||
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
||||
|
||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||
@@ -832,8 +836,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||
#endif
|
||||
}
|
||||
|
||||
packetPool.release(p_encrypted); // Release the encrypted packet
|
||||
p_encrypted = nullptr;
|
||||
packetPool.release(p_encrypted); // Release the encrypted packet (release() handles nullptr)
|
||||
}
|
||||
|
||||
void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
||||
|
||||
@@ -92,9 +92,6 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
before us */
|
||||
uint32_t rxDupe = 0, txRelayCanceled = 0;
|
||||
|
||||
// pointer to the encrypted packet
|
||||
meshtastic_MeshPacket *p_encrypted = nullptr;
|
||||
|
||||
protected:
|
||||
friend class RoutingModule;
|
||||
|
||||
|
||||
@@ -455,6 +455,15 @@ template <typename T> void SX126xInterface<T>::resetAGC()
|
||||
// RX boosted gain mode
|
||||
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
|
||||
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
|
||||
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
|
||||
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
|
||||
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
|
||||
// and never recovers until reboot. See empirical evidence in the PR description.
|
||||
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
|
||||
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
|
||||
}
|
||||
|
||||
// 6. Resume receiving
|
||||
startReceive();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TypeConversions.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
meshtastic_NodeInfo TypeConversions::ConvertToNodeInfo(const meshtastic_NodeInfoLite *lite)
|
||||
{
|
||||
@@ -81,7 +82,11 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
||||
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
||||
|
||||
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite.long_name, sizeof(lite.long_name));
|
||||
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(lite.short_name, sizeof(lite.short_name));
|
||||
lite.hw_model = user.hw_model;
|
||||
lite.role = user.role;
|
||||
lite.is_licensed = user.is_licensed;
|
||||
@@ -99,7 +104,11 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
|
||||
|
||||
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(user.long_name, sizeof(user.long_name));
|
||||
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(user.short_name, sizeof(user.short_name));
|
||||
user.hw_model = lite.hw_model;
|
||||
user.role = lite.role;
|
||||
user.is_licensed = lite.is_licensed;
|
||||
|
||||
@@ -285,7 +285,18 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
|
||||
/* Nepal 865MHz */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
|
||||
/* Brazil 902MHz */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
|
||||
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
|
||||
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
|
||||
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28,
|
||||
/* EU 866MHz band (Band no. 47b of 2006/771/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_866 = 29,
|
||||
/* EU 874MHz and 917MHz bands (Band no. 1 and 4 of 2022/172/EC and subsequent amendments) for Non-specific short-range devices (SRD) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_874 = 30,
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_917 = 31,
|
||||
/* EU 868MHz band, with narrow presets */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_EU_N_868 = 32
|
||||
} meshtastic_Config_LoRaConfig_RegionCode;
|
||||
|
||||
/* Standard predefined channel settings
|
||||
@@ -315,7 +326,24 @@ typedef enum _meshtastic_Config_LoRaConfig_ModemPreset {
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO = 8,
|
||||
/* Long Range - Turbo
|
||||
This preset performs similarly to LongFast, but with 500Khz bandwidth. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO = 9,
|
||||
/* Lite Fast
|
||||
Medium range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to MEDIUM_FAST but compliant with Band no. 47b of 2006/771/EC. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LITE_FAST = 10,
|
||||
/* Lite Slow
|
||||
Medium-to-moderate range preset optimized for EU 866MHz SRD band with 125kHz bandwidth.
|
||||
Comparable link budget to LONG_FAST but compliant with Band no. 47b of 2006/771/EC. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_LITE_SLOW = 11,
|
||||
/* Narrow Fast
|
||||
Medium-to-moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget to SHORT_SLOW, but with half the data rate.
|
||||
Intended to avoid interference with other devices. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_FAST = 12,
|
||||
/* Narrow Slow
|
||||
Moderate range preset optimized for EU 868MHz band with 62.5kHz bandwidth.
|
||||
Comparable link budget and data rate to LONG_FAST. */
|
||||
meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW = 13
|
||||
} meshtastic_Config_LoRaConfig_ModemPreset;
|
||||
|
||||
typedef enum _meshtastic_Config_LoRaConfig_FEM_LNA_Mode {
|
||||
@@ -702,12 +730,12 @@ extern "C" {
|
||||
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_EU_N_868
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_EU_N_868+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO+1))
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_ARRAYSIZE ((meshtastic_Config_LoRaConfig_ModemPreset)(meshtastic_Config_LoRaConfig_ModemPreset_NARROW_SLOW+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MIN meshtastic_Config_LoRaConfig_FEM_LNA_Mode_DISABLED
|
||||
#define _meshtastic_Config_LoRaConfig_FEM_LNA_Mode_MAX meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT
|
||||
|
||||
@@ -315,6 +315,8 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_THINKNODE_M7 = 129,
|
||||
meshtastic_HardwareModel_THINKNODE_M8 = 130,
|
||||
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
||||
/* The Heltec-V4-R8 uses an ESP32S3R8 chip, plus an SX1262. */
|
||||
meshtastic_HardwareModel_HELTEC_V4_R8 = 132,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
|
||||
@@ -117,4 +117,93 @@ size_t pb_string_length(const char *str, size_t max_len)
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
bool sanitizeUtf8(char *buf, size_t bufSize)
|
||||
{
|
||||
if (!buf || bufSize == 0)
|
||||
return false;
|
||||
|
||||
// Ensure null-terminated within buffer; report if we had to enforce it
|
||||
bool replaced = (buf[bufSize - 1] != '\0');
|
||||
buf[bufSize - 1] = '\0';
|
||||
|
||||
size_t i = 0;
|
||||
size_t len = strlen(buf);
|
||||
|
||||
while (i < len) {
|
||||
uint8_t b = (uint8_t)buf[i];
|
||||
|
||||
// Determine expected sequence length from lead byte
|
||||
size_t seqLen;
|
||||
uint32_t minCodepoint;
|
||||
if (b <= 0x7F) {
|
||||
// ASCII — valid single byte
|
||||
i++;
|
||||
continue;
|
||||
} else if ((b & 0xE0) == 0xC0) {
|
||||
seqLen = 2;
|
||||
minCodepoint = 0x80; // Reject overlong
|
||||
} else if ((b & 0xF0) == 0xE0) {
|
||||
seqLen = 3;
|
||||
minCodepoint = 0x800;
|
||||
} else if ((b & 0xF8) == 0xF0) {
|
||||
seqLen = 4;
|
||||
minCodepoint = 0x10000;
|
||||
} else {
|
||||
// Invalid lead byte (0x80-0xBF or 0xF8+)
|
||||
buf[i] = '?';
|
||||
replaced = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check that we have enough bytes remaining
|
||||
if (i + seqLen > len) {
|
||||
// Truncated sequence at end of string — replace remaining bytes
|
||||
for (size_t j = i; j < len; j++) {
|
||||
buf[j] = '?';
|
||||
}
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate continuation bytes (must be 10xxxxxx)
|
||||
bool valid = true;
|
||||
for (size_t j = 1; j < seqLen; j++) {
|
||||
if (((uint8_t)buf[i + j] & 0xC0) != 0x80) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
// Decode codepoint to check for overlong encodings and surrogates
|
||||
uint32_t cp = 0;
|
||||
if (seqLen == 2)
|
||||
cp = b & 0x1F;
|
||||
else if (seqLen == 3)
|
||||
cp = b & 0x0F;
|
||||
else
|
||||
cp = b & 0x07;
|
||||
for (size_t j = 1; j < seqLen; j++)
|
||||
cp = (cp << 6) | ((uint8_t)buf[i + j] & 0x3F);
|
||||
|
||||
if (cp < minCodepoint || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
||||
// Overlong encoding, out of Unicode range, or surrogate half
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
i += seqLen;
|
||||
} else {
|
||||
// Replace only the lead byte; continuation bytes will be caught on next iteration
|
||||
buf[i] = '?';
|
||||
replaced = true;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return replaced;
|
||||
}
|
||||
@@ -11,6 +11,24 @@ template <class T> constexpr const T &clamp(const T &v, const T &lo, const T &hi
|
||||
return (v < lo) ? lo : (hi < v) ? hi : v;
|
||||
}
|
||||
|
||||
/// Return the smallest power of 2 >= n (undefined for n > 2^31)
|
||||
static inline uint32_t nextPowerOf2(uint32_t n)
|
||||
{
|
||||
if (n <= 1)
|
||||
return 1;
|
||||
#if defined(__GNUC__)
|
||||
return 1U << (32 - __builtin_clz(n - 1));
|
||||
#else
|
||||
n--;
|
||||
n |= n >> 1;
|
||||
n |= n >> 2;
|
||||
n |= n >> 4;
|
||||
n |= n >> 8;
|
||||
n |= n >> 16;
|
||||
return n + 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
#define IF_SCREEN(X) \
|
||||
if (screen) { \
|
||||
@@ -38,6 +56,10 @@ const std::string vformat(const char *const zcFormat, ...);
|
||||
// Get actual string length for nanopb char array fields.
|
||||
size_t pb_string_length(const char *str, size_t max_len);
|
||||
|
||||
// Sanitize a fixed-size char buffer in-place by replacing invalid UTF-8 sequences with '?'.
|
||||
// Ensures the result is null-terminated within bufSize. Returns true if any bytes were replaced.
|
||||
bool sanitizeUtf8(char *buf, size_t bufSize);
|
||||
|
||||
/// Calculate 2^n without calling pow() - used for spreading factor and other calculations
|
||||
inline uint32_t pow_of_2(uint32_t n)
|
||||
{
|
||||
|
||||
@@ -626,10 +626,14 @@ void AdminModule::handleSetOwner(const meshtastic_User &o)
|
||||
if (*o.long_name) {
|
||||
changed |= strcmp(owner.long_name, o.long_name);
|
||||
strncpy(owner.long_name, o.long_name, sizeof(owner.long_name));
|
||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(owner.long_name, sizeof(owner.long_name));
|
||||
}
|
||||
if (*o.short_name) {
|
||||
changed |= strcmp(owner.short_name, o.short_name);
|
||||
strncpy(owner.short_name, o.short_name, sizeof(owner.short_name));
|
||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(owner.short_name, sizeof(owner.short_name));
|
||||
}
|
||||
snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum());
|
||||
|
||||
@@ -1430,7 +1434,11 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||
|
||||
// Set call sign and override lora limitations for licensed use
|
||||
strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name));
|
||||
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
||||
sanitizeUtf8(owner.long_name, sizeof(owner.long_name));
|
||||
strncpy(owner.short_name, p.short_name, sizeof(owner.short_name));
|
||||
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(owner.short_name, sizeof(owner.short_name));
|
||||
owner.is_licensed = true;
|
||||
config.lora.override_duty_cycle = true;
|
||||
config.lora.tx_power = p.tx_power;
|
||||
|
||||
@@ -492,15 +492,24 @@ void PositionModule::sendLostAndFoundText()
|
||||
{
|
||||
meshtastic_MeshPacket *p = allocDataPacket();
|
||||
p->to = NODENUM_BROADCAST;
|
||||
char *message = new char[60];
|
||||
sprintf(message, "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7), (lastGpsLongitude * 1e-7));
|
||||
char message[128];
|
||||
int written = snprintf(message, sizeof(message), "🚨I'm lost! Lat / Lon: %f, %f\a", (lastGpsLatitude * 1e-7),
|
||||
(lastGpsLongitude * 1e-7));
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
p->want_ack = false;
|
||||
p->decoded.payload.size = strlen(message);
|
||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||
if (written < 0) {
|
||||
// snprintf encoding error — send an empty payload rather than uninitialized bytes.
|
||||
p->decoded.payload.size = 0;
|
||||
} else {
|
||||
// Clamp to buffer capacity (snprintf returns "would-have-written" which can exceed the buffer).
|
||||
const size_t msg_len = std::min(static_cast<size_t>(written), sizeof(message) - 1);
|
||||
p->decoded.payload.size = msg_len;
|
||||
if (msg_len > 0) {
|
||||
memcpy(p->decoded.payload.bytes, message, msg_len);
|
||||
}
|
||||
}
|
||||
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
delete[] message;
|
||||
}
|
||||
|
||||
// Helper: return imprecise (truncated + centered) lat/lon as int32 using current precision
|
||||
@@ -580,4 +589,4 @@ void PositionModule::handleNewPosition()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -206,7 +206,7 @@ void StoreForwardModule::historyAdd(const meshtastic_MeshPacket &mp)
|
||||
this->packetHistory[this->packetHistoryTotalCount].hop_limit = mp.hop_limit;
|
||||
this->packetHistory[this->packetHistoryTotalCount].via_mqtt = mp.via_mqtt;
|
||||
this->packetHistory[this->packetHistoryTotalCount].transport_mechanism = mp.transport_mechanism;
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, meshtastic_Constants_DATA_PAYLOAD_LEN);
|
||||
memcpy(this->packetHistory[this->packetHistoryTotalCount].payload, p.payload.bytes, p.payload.size);
|
||||
|
||||
this->packetHistoryTotalCount++;
|
||||
}
|
||||
|
||||
0
src/motion/AccelerometerThread.h
Normal file → Executable file
0
src/motion/AccelerometerThread.h
Normal file → Executable file
0
src/motion/BMA423Sensor.cpp
Normal file → Executable file
0
src/motion/BMA423Sensor.cpp
Normal file → Executable file
0
src/motion/BMA423Sensor.h
Normal file → Executable file
0
src/motion/BMA423Sensor.h
Normal file → Executable file
0
src/motion/BMX160Sensor.cpp
Normal file → Executable file
0
src/motion/BMX160Sensor.cpp
Normal file → Executable file
0
src/motion/BMX160Sensor.h
Normal file → Executable file
0
src/motion/BMX160Sensor.h
Normal file → Executable file
0
src/motion/ICM20948Sensor.cpp
Normal file → Executable file
0
src/motion/ICM20948Sensor.cpp
Normal file → Executable file
0
src/motion/ICM20948Sensor.h
Normal file → Executable file
0
src/motion/ICM20948Sensor.h
Normal file → Executable file
0
src/motion/LIS3DHSensor.cpp
Normal file → Executable file
0
src/motion/LIS3DHSensor.cpp
Normal file → Executable file
0
src/motion/LIS3DHSensor.h
Normal file → Executable file
0
src/motion/LIS3DHSensor.h
Normal file → Executable file
0
src/motion/LSM6DS3Sensor.cpp
Normal file → Executable file
0
src/motion/LSM6DS3Sensor.cpp
Normal file → Executable file
0
src/motion/LSM6DS3Sensor.h
Normal file → Executable file
0
src/motion/LSM6DS3Sensor.h
Normal file → Executable file
0
src/motion/MPU6050Sensor.cpp
Normal file → Executable file
0
src/motion/MPU6050Sensor.cpp
Normal file → Executable file
0
src/motion/MPU6050Sensor.h
Normal file → Executable file
0
src/motion/MPU6050Sensor.h
Normal file → Executable file
0
src/motion/MotionSensor.cpp
Normal file → Executable file
0
src/motion/MotionSensor.cpp
Normal file → Executable file
0
src/motion/MotionSensor.h
Normal file → Executable file
0
src/motion/MotionSensor.h
Normal file → Executable file
0
src/motion/STK8XXXSensor.cpp
Normal file → Executable file
0
src/motion/STK8XXXSensor.cpp
Normal file → Executable file
0
src/motion/STK8XXXSensor.h
Normal file → Executable file
0
src/motion/STK8XXXSensor.h
Normal file → Executable file
@@ -323,7 +323,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
/**
|
||||
* Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies)
|
||||
*/
|
||||
virtual void onNowHasData(uint32_t fromRadioNum)
|
||||
virtual void onNowHasData(uint32_t fromRadioNum) override
|
||||
{
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
@@ -350,7 +350,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
}
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
virtual bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
|
||||
|
||||
void requestHighThroughputConnection(uint16_t conn_handle)
|
||||
{
|
||||
@@ -412,9 +412,9 @@ static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
|
||||
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic) override
|
||||
|
||||
#endif
|
||||
{
|
||||
@@ -464,9 +464,9 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic)
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic) override
|
||||
#endif
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
@@ -582,9 +582,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
private:
|
||||
NimbleBluetooth *ble;
|
||||
|
||||
virtual uint32_t onPassKeyDisplay()
|
||||
virtual uint32_t onPassKeyDisplay() override
|
||||
#else
|
||||
virtual uint32_t onPassKeyRequest()
|
||||
virtual uint32_t onPassKeyRequest() override
|
||||
#endif
|
||||
{
|
||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||
@@ -635,9 +635,9 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
|
||||
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo) override
|
||||
#else
|
||||
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
|
||||
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
#endif
|
||||
{
|
||||
LOG_INFO("BLE authentication complete");
|
||||
@@ -655,7 +655,7 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
|
||||
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override
|
||||
{
|
||||
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
||||
|
||||
@@ -683,11 +683,11 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
#endif
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
|
||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override
|
||||
{
|
||||
LOG_INFO("BLE disconnect reason: %d", reason);
|
||||
#else
|
||||
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
|
||||
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc) override
|
||||
{
|
||||
LOG_INFO("BLE disconnect");
|
||||
#endif
|
||||
@@ -989,11 +989,4 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
#endif
|
||||
}
|
||||
|
||||
void clearNVS()
|
||||
{
|
||||
NimBLEDevice::deleteAllBonds();
|
||||
#ifdef ARCH_ESP32
|
||||
ESP.restart();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -24,5 +24,4 @@ class NimbleBluetooth : BluetoothApi
|
||||
#endif
|
||||
};
|
||||
|
||||
void setBluetoothEnable(bool enable);
|
||||
void clearNVS();
|
||||
void setBluetoothEnable(bool enable);
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "AudioBoard.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef M5STACK_CARDPUTER_ADV
|
||||
|
||||
#include "AudioBoard.h"
|
||||
|
||||
DriverPins PinsAudioBoardES8311;
|
||||
AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
|
||||
|
||||
@@ -38,3 +41,5 @@ void lateInitVariant()
|
||||
es8311_write_reg(0x32, 0xBF); // DAC volume (0dB)
|
||||
es8311_write_reg(0x37, 0x08); // EQ bypass
|
||||
}
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user