mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-05 03:38:32 +10:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee293c209d | ||
|
|
bfadf0c36a | ||
|
|
80ce9c4a75 | ||
|
|
1d5b9361d8 | ||
|
|
12285088d1 | ||
|
|
0e05e715c0 | ||
|
|
24c4162a75 | ||
|
|
b148fac340 | ||
|
|
8dde4eeee1 | ||
|
|
5b5ebe6fb4 | ||
|
|
4d4e14600c | ||
|
|
aec0805a27 | ||
|
|
0bd8dee346 | ||
|
|
7800dc3c8d | ||
|
|
554188e90e | ||
|
|
fb678b9337 | ||
|
|
2828dbe4ca | ||
|
|
a8c8fd7002 | ||
|
|
e7c02da24b | ||
|
|
7347091055 | ||
|
|
55f15076ca | ||
|
|
7421953e8f | ||
|
|
9306e66067 | ||
|
|
439b87b860 | ||
|
|
d47301defc | ||
|
|
04b819a7b5 | ||
|
|
8e653122c7 | ||
|
|
5ea3d143da | ||
|
|
56c897e826 | ||
|
|
2cc13a1132 | ||
|
|
031f332ec1 |
+1
-1
@@ -9,7 +9,7 @@ plugins:
|
||||
lint:
|
||||
enabled:
|
||||
- checkov@3.2.524
|
||||
- renovate@43.139.6
|
||||
- renovate@43.141.0
|
||||
- prettier@3.8.3
|
||||
- trufflehog@3.95.2
|
||||
- yamllint@1.38.0
|
||||
|
||||
+2
-1
@@ -31,5 +31,6 @@ basename=meshtasticd-$1-$VERSION
|
||||
pio pkg install --environment "$PIO_ENV" || platformioFailed
|
||||
pio run --environment "$PIO_ENV" || platformioFailed
|
||||
|
||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_linux_$(uname -m)"
|
||||
os_name=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
cp "$BUILDDIR/meshtasticd" "$OUTDIR/meshtasticd_${os_name}_$(uname -m)"
|
||||
cp bin/native-install.* $OUTDIR/
|
||||
|
||||
Executable
+118
@@ -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
|
||||
+2
-1
@@ -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
|
||||
|
||||
+1
-1
Submodule protobufs updated: d004f503bb...249a80855a
+100
-12
@@ -68,7 +68,11 @@
|
||||
#define ETH ETH2
|
||||
#endif // HAS_ETHERNET
|
||||
|
||||
#endif // MQTT
|
||||
#endif
|
||||
|
||||
#ifndef DELAY_FOREVER
|
||||
#define DELAY_FOREVER portMAX_DELAY
|
||||
#endif
|
||||
|
||||
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
|
||||
|
||||
@@ -90,6 +94,34 @@ 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
|
||||
#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
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||||
#if __has_include(<Adafruit_INA219.h>)
|
||||
INA219Sensor ina219Sensor;
|
||||
@@ -128,7 +160,7 @@ MAX17048Sensor max17048Sensor;
|
||||
NullSensor max17048Sensor;
|
||||
#endif
|
||||
#endif
|
||||
#endif // !MESHTASTIC_EXCLUDE_I2C
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
|
||||
RAK9154Sensor rak9154Sensor;
|
||||
@@ -146,12 +178,46 @@ XPowersPPM *PPM = NULL;
|
||||
|
||||
#ifdef HAS_PMU
|
||||
XPowersLibInterface *PMU = NULL;
|
||||
#else
|
||||
|
||||
// Copy of the base class defined in axp20x.h.
|
||||
// I'd rather not include axp20x.h as it brings Wire dependency.
|
||||
class HasBatteryLevel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() { return -1; }
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() { return 0; }
|
||||
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
virtual bool isBatteryConnect() { return false; }
|
||||
|
||||
virtual bool isVbusIn() { return false; }
|
||||
virtual bool isCharging() { return false; }
|
||||
};
|
||||
#endif
|
||||
|
||||
bool pmu_irq = false;
|
||||
|
||||
Power *power;
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||||
// make sure it's included. If something is wrong with NRF52
|
||||
// definition - compilation will fail on missing definition
|
||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||||
#define AREF_VOLTAGE 3.3
|
||||
#endif
|
||||
|
||||
/**
|
||||
* If this board has a battery level sensor, set this to a valid implementation
|
||||
*/
|
||||
@@ -194,7 +260,7 @@ static void battery_adcDisable()
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif // BATTERY_PIN
|
||||
#endif
|
||||
|
||||
/**
|
||||
* A simple battery level sensor that assumes the battery voltage is attached
|
||||
@@ -253,7 +319,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery in millivolts or NAN if unknown
|
||||
* The raw voltage of the batteryin millivolts or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() override
|
||||
{
|
||||
@@ -270,6 +336,16 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef ADC_MULTIPLIER
|
||||
#define ADC_MULTIPLIER 2.0
|
||||
#endif
|
||||
|
||||
#ifndef BATTERY_SENSE_SAMPLES
|
||||
#define BATTERY_SENSE_SAMPLES \
|
||||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
||||
// complex electromagnetic environment.
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
// Override variant or default ADC_MULTIPLIER if we have the override pref
|
||||
float operativeAdcMultiplier =
|
||||
@@ -705,8 +781,10 @@ void Power::reboot()
|
||||
rp2040.reboot();
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
deInitApiServer();
|
||||
#ifdef __linux__
|
||||
if (aLinuxInputImpl)
|
||||
aLinuxInputImpl->deInit();
|
||||
#endif
|
||||
SPI.end();
|
||||
Wire.end();
|
||||
Serial1.end();
|
||||
@@ -1017,7 +1095,7 @@ void Power::attachPowerInterrupts()
|
||||
attachInterrupt(
|
||||
PMU_IRQ,
|
||||
[]() {
|
||||
power->pmu_irq = true;
|
||||
pmu_irq = true;
|
||||
power->setIntervalFromNow(0);
|
||||
runASAP = true;
|
||||
},
|
||||
@@ -1636,7 +1714,7 @@ bool Power::lipoChargerInit()
|
||||
return true;
|
||||
}
|
||||
|
||||
#else // HAS_PPM
|
||||
#else
|
||||
/**
|
||||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||||
*/
|
||||
@@ -1644,7 +1722,7 @@ bool Power::lipoChargerInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif // HAS_PPM
|
||||
#endif
|
||||
|
||||
#ifdef HELTEC_MESH_SOLAR
|
||||
#include "meshSolarApp.h"
|
||||
@@ -1706,7 +1784,7 @@ bool Power::meshSolarInit()
|
||||
return true;
|
||||
}
|
||||
|
||||
#else // HELTEC_MESH_SOLAR
|
||||
#else
|
||||
/**
|
||||
* The meshSolar battery level sensor is unavailable - default to
|
||||
* AnalogBatteryLevel
|
||||
@@ -1715,7 +1793,7 @@ bool Power::meshSolarInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif // HELTEC_MESH_SOLAR
|
||||
#endif
|
||||
|
||||
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
||||
#include <SoftwareSerial.h>
|
||||
@@ -1723,7 +1801,7 @@ bool Power::meshSolarInit()
|
||||
/**
|
||||
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
||||
*/
|
||||
class SerialBatteryLevel : public AnalogBatteryLevel
|
||||
class SerialBatteryLevel : public HasBatteryLevel
|
||||
{
|
||||
|
||||
public:
|
||||
@@ -1800,6 +1878,16 @@ class SerialBatteryLevel : public AnalogBatteryLevel
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool isCharging() override
|
||||
{
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||||
|
||||
#endif
|
||||
// by default, we check the battery voltage only
|
||||
return isVbusIn();
|
||||
}
|
||||
|
||||
private:
|
||||
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
||||
uint8_t Data[6] = {0};
|
||||
@@ -1829,7 +1917,7 @@ bool Power::serialBatteryInit()
|
||||
return true;
|
||||
}
|
||||
|
||||
#else // HAS_SERIAL_BATTERY_LEVEL
|
||||
#else
|
||||
/**
|
||||
* If this device has no serial battery level sensor, don't try to use it.
|
||||
*/
|
||||
@@ -1837,4 +1925,4 @@ bool Power::serialBatteryInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif // HAS_SERIAL_BATTERY_LEVEL
|
||||
#endif
|
||||
|
||||
+48
-11
@@ -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");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "../freertosinc.h"
|
||||
#include "Print.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <Print.h>
|
||||
#include <stdarg.h>
|
||||
#include <string>
|
||||
|
||||
|
||||
+59
-53
@@ -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);
|
||||
@@ -474,9 +467,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);
|
||||
@@ -663,16 +660,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
|
||||
@@ -718,7 +715,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);
|
||||
@@ -757,9 +754,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();
|
||||
|
||||
@@ -832,7 +829,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
|
||||
@@ -903,9 +900,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;
|
||||
}
|
||||
@@ -996,7 +993,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
|
||||
@@ -1058,7 +1055,7 @@ void Screen::setSSLFrames()
|
||||
// LOG_DEBUG("Show SSL frames");
|
||||
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
||||
ui->setFrames(sslFrames, 1);
|
||||
ui->update();
|
||||
updateUiFrame(ui);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,7 +1091,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)
|
||||
@@ -1469,9 +1466,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;
|
||||
@@ -1605,6 +1608,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
|
||||
@@ -1816,7 +1822,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;
|
||||
}
|
||||
|
||||
@@ -1831,7 +1837,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+142
-43
@@ -16,12 +16,6 @@
|
||||
extern SX1509 gpioExtender;
|
||||
#endif
|
||||
|
||||
#ifdef TFT_MESH_OVERRIDE
|
||||
uint16_t TFT_MESH = TFT_MESH_OVERRIDE;
|
||||
#else
|
||||
uint16_t TFT_MESH = COLOR565(0x67, 0xEA, 0x94);
|
||||
#endif
|
||||
|
||||
#if defined(ST7735S)
|
||||
#include <LovyanGFX.hpp> // Graphics and font library for ST7735 driver chip
|
||||
|
||||
@@ -1140,7 +1134,9 @@ static LGFX *tft = nullptr;
|
||||
#endif
|
||||
|
||||
#include "SPILock.h"
|
||||
#include "TFTColorRegions.h"
|
||||
#include "TFTDisplay.h"
|
||||
#include "TFTPalette.h"
|
||||
#include <SPI.h>
|
||||
|
||||
#ifdef UNPHONE
|
||||
@@ -1150,6 +1146,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!");
|
||||
@@ -1189,14 +1204,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;
|
||||
@@ -1205,12 +1221,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) {
|
||||
@@ -1219,7 +1293,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;
|
||||
@@ -1237,13 +1311,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;
|
||||
}
|
||||
@@ -1253,43 +1328,51 @@ 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;
|
||||
}
|
||||
|
||||
// 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(HACKADAY_COMMUNICATOR)
|
||||
tft->draw16bitBeRGBBitmap(x_FirstPixelUpdate, y, &linePixelBuffer[x_FirstPixelUpdate],
|
||||
(x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1);
|
||||
#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, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
tft->pushImage(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1,
|
||||
&linePixelBuffer[x_FirstPixelUpdate]);
|
||||
#endif
|
||||
somethingChanged = true;
|
||||
}
|
||||
@@ -1298,6 +1381,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()
|
||||
@@ -1511,7 +1602,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) {
|
||||
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
|
||||
@@ -1521,6 +1612,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;
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
@@ -286,6 +318,9 @@ void drawEntryHopSignal(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;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
@@ -315,6 +350,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
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);
|
||||
@@ -350,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] = "";
|
||||
|
||||
@@ -455,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);
|
||||
@@ -710,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);
|
||||
|
||||
+638
-248
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);
|
||||
|
||||
|
||||
@@ -25,6 +25,116 @@
|
||||
|
||||
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
|
||||
@@ -34,24 +144,48 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_
|
||||
(void)pin_busy;
|
||||
(void)pin_rst;
|
||||
|
||||
epaper = new FASTEPD;
|
||||
SafeFastEPD *safeEpaper = new SafeFastEPD;
|
||||
epaper = safeEpaper;
|
||||
|
||||
int initRc = BBEP_ERROR_BAD_PARAMETER;
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
initRc = epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
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
|
||||
|
||||
epaper->setMode(BB_MODE_1BPP);
|
||||
epaper->clearWhite();
|
||||
epaper->fullUpdate(true); // Blocking initial clear
|
||||
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)
|
||||
@@ -111,9 +245,8 @@ void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
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
|
||||
// the update waveform to rows that actually changed. Unchanged rows get a neutral
|
||||
// signal (no visible effect). partialUpdate() updates pPrevious internally.
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -390,8 +390,11 @@ void InputBroker::Init()
|
||||
seesawRotary = nullptr;
|
||||
}
|
||||
}
|
||||
#ifdef __linux__
|
||||
// Linux evdev keyboard input only — macOS has no <linux/input.h>.
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER && HAS_TRACKBALL
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
#if ARCH_PORTDUINO
|
||||
// Linux evdev keyboard input. Only compiled on Linux portduino targets;
|
||||
// macOS / non-Linux builds have no <linux/input.h> or epoll, and the
|
||||
// headless build doesn't need real keyboards anyway.
|
||||
#if ARCH_PORTDUINO && defined(__linux__)
|
||||
#include "InputBroker.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <assert.h>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#ifdef ARCH_PORTDUINO
|
||||
// Linux evdev impl. Same Linux-only gating as LinuxInput.h.
|
||||
#if defined(ARCH_PORTDUINO) && defined(__linux__)
|
||||
#pragma once
|
||||
#include "LinuxInput.h"
|
||||
#include "main.h"
|
||||
|
||||
@@ -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"
|
||||
@@ -39,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
|
||||
*
|
||||
@@ -83,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 *);
|
||||
};
|
||||
|
||||
@@ -19,8 +19,12 @@ extern Adafruit_nRFCrypto nRFCrypto;
|
||||
#include <Arduino.h>
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
#include <random>
|
||||
#include <sys/random.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <sys/random.h> // getrandom()
|
||||
#else
|
||||
#include <stdlib.h> // arc4random_buf() on Darwin/BSD
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace HardwareRNG
|
||||
@@ -48,8 +52,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;
|
||||
}
|
||||
|
||||
@@ -116,10 +123,16 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
filled = true;
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
// Prefer the host OS RNG first when running under Portduino.
|
||||
#ifdef __linux__
|
||||
ssize_t generated = ::getrandom(buffer, length, 0);
|
||||
if (generated == static_cast<ssize_t>(length)) {
|
||||
filled = true;
|
||||
}
|
||||
#else
|
||||
// arc4random_buf is available on Darwin/BSD and cannot fail.
|
||||
::arc4random_buf(buffer, length);
|
||||
filled = true;
|
||||
#endif
|
||||
|
||||
if (!filled) {
|
||||
fillWithRandomDevice(buffer, length);
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
#include "configuration.h"
|
||||
#include "detect/LoRaRadioType.h"
|
||||
|
||||
// Sentinel marking the end of a modem preset array
|
||||
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
// Sentinel marking the end of a modem preset array. Declared `const` rather
|
||||
// than `constexpr` because the cast from 0xFF to the enum is out-of-range and
|
||||
// therefore not a valid constant expression on Clang 16+ (Apple Clang on
|
||||
// macOS). The value is only ever compared at runtime, so static-init is fine.
|
||||
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);
|
||||
|
||||
// Region profile: bundles the preset list with regulatory parameters shared across regions
|
||||
|
||||
@@ -119,7 +119,7 @@ bool RadioLibInterface::canSendImmediately()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RadioLibInterface::receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag)
|
||||
bool RadioLibInterface::receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag)
|
||||
{
|
||||
bool detected = (irq & (syncWordHeaderValidFlag | preambleDetectedFlag));
|
||||
// Handle false detections
|
||||
|
||||
@@ -220,7 +220,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
protected:
|
||||
uint32_t activeReceiveStart = 0;
|
||||
|
||||
bool receiveDetected(uint16_t irq, ulong syncWordHeaderValidFlag, ulong preambleDetectedFlag);
|
||||
bool receiveDetected(uint16_t irq, unsigned long syncWordHeaderValidFlag, unsigned long preambleDetectedFlag);
|
||||
|
||||
/** Do any hardware setup needed on entry into send configuration for the radio.
|
||||
* Subclasses can customize, but must also call this base method */
|
||||
|
||||
+7
-4
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -82,8 +83,10 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
||||
|
||||
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;
|
||||
@@ -102,8 +105,10 @@ 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;
|
||||
|
||||
@@ -289,7 +289,14 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
|
||||
/* 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
|
||||
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
|
||||
@@ -319,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 {
|
||||
@@ -706,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_ITU23_2M
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+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;
|
||||
}
|
||||
@@ -56,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;
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+1
-1
@@ -32,7 +32,7 @@
|
||||
#include <assert.h>
|
||||
#include <utility>
|
||||
|
||||
#include <IPAddress.h>
|
||||
#include "IPAddress.h"
|
||||
#if defined(ARCH_PORTDUINO)
|
||||
#include <netinet/in.h>
|
||||
#elif !defined(ntohl)
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
#include "Observer.h"
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include "Wire.h"
|
||||
#include "buzz.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include "main.h"
|
||||
#include "sleep.h"
|
||||
#include <cstring>
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/Persistence.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
|
||||
@@ -18,8 +23,7 @@
|
||||
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
|
||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
|
||||
// - Nav up/down: navUp/navDown always (menu scroll)
|
||||
// - Tap: shortpress (cycle applets / confirm in menu)
|
||||
// - Long press: longpress (open menu / back)
|
||||
// - Tap/long-press: direct touch point dispatch (with fallback to short/long button semantics)
|
||||
class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
{
|
||||
int onNotify(const InputEvent *e) override
|
||||
@@ -41,28 +45,28 @@ class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
|
||||
switch (e->inputEvent) {
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
inkhud->shortpress();
|
||||
inkhud->touchTap(e->touchX, e->touchY);
|
||||
break;
|
||||
case INPUT_BROKER_SELECT:
|
||||
inkhud->longpress();
|
||||
inkhud->touchLongPress(e->touchX, e->touchY);
|
||||
break;
|
||||
case INPUT_BROKER_LEFT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navUp();
|
||||
inkhud->touchNavUp();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
break;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navDown();
|
||||
inkhud->touchNavDown();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
break;
|
||||
case INPUT_BROKER_UP:
|
||||
inkhud->navUp();
|
||||
inkhud->touchNavUp();
|
||||
break;
|
||||
case INPUT_BROKER_DOWN:
|
||||
inkhud->navDown();
|
||||
inkhud->touchNavDown();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -76,6 +80,430 @@ static TouchInkHUDBridge touchBridge;
|
||||
|
||||
TouchDrvGT911 touch;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint8_t BACKLIGHT_ON_LEVEL = HIGH;
|
||||
constexpr uint8_t BACKLIGHT_OFF_LEVEL = LOW;
|
||||
volatile bool backlightUserEnabled = true;
|
||||
volatile bool backlightForcedByTimeout = false;
|
||||
volatile bool backlightForcedBySleep = false;
|
||||
|
||||
void applyBacklightState()
|
||||
{
|
||||
const bool shouldOn = backlightUserEnabled && !backlightForcedByTimeout && !backlightForcedBySleep;
|
||||
digitalWrite(BOARD_BL_EN, shouldOn ? BACKLIGHT_ON_LEVEL : BACKLIGHT_OFF_LEVEL);
|
||||
}
|
||||
|
||||
volatile bool touchInputEnabled = true;
|
||||
volatile bool touchForcedByTimeout = false;
|
||||
volatile bool touchControllerReady = false;
|
||||
volatile bool touchLightSleepActive = false;
|
||||
volatile bool touchNeedsWake = false;
|
||||
volatile bool touchIndicatorRefreshPending = false;
|
||||
volatile uint32_t touchResumeBlockUntilMs = 0;
|
||||
volatile uint32_t touchStateEpoch = 1;
|
||||
volatile bool homeCapButtonEventsEnabled = false;
|
||||
#if HAS_SCREEN
|
||||
uint32_t lastTouchIndicatorMs = 0;
|
||||
#endif
|
||||
|
||||
void showTouchIndicator(const char *text)
|
||||
{
|
||||
#if HAS_SCREEN
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// InkHUD builds render a dedicated bottom-edge "TOUCH OFF" overlay instead of popup banners.
|
||||
(void)text;
|
||||
return;
|
||||
#else
|
||||
// Keep repeated notifications low profile and non-spammy.
|
||||
if ((millis() - lastTouchIndicatorMs) < 500) {
|
||||
return;
|
||||
}
|
||||
lastTouchIndicatorMs = millis();
|
||||
if (screen) {
|
||||
screen->showSimpleBanner(text, 1400);
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
(void)text;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(BOARD_PCA9535_ADDR) && defined(BOARD_PCA9535_BUTTON_MASK)
|
||||
bool readPca9535Port1(uint8_t *value)
|
||||
{
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Wire.beginTransmission(BOARD_PCA9535_ADDR);
|
||||
Wire.write((uint8_t)0x01); // input port 1
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (Wire.requestFrom((uint8_t)BOARD_PCA9535_ADDR, (uint8_t)1) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*value = Wire.read();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isPca9535SideKeyPressed()
|
||||
{
|
||||
uint8_t port1 = 0xFF;
|
||||
if (!readPca9535Port1(&port1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (port1 & BOARD_PCA9535_BUTTON_MASK) == 0;
|
||||
}
|
||||
|
||||
class SideKeyInterruptThread : public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
SideKeyInterruptThread() : concurrency::OSThread("t5s3SideKeyInt", SAMPLE_MS)
|
||||
{
|
||||
// Do not run unless an edge arrives.
|
||||
OSThread::disable();
|
||||
instance = this;
|
||||
#ifdef ARCH_ESP32
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
}
|
||||
|
||||
void begin()
|
||||
{
|
||||
pinMode(BOARD_PCA9535_INT, INPUT_PULLUP);
|
||||
attachInterrupt(BOARD_PCA9535_INT, SideKeyInterruptThread::isr, FALLING);
|
||||
}
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
|
||||
if (now < touchResumeBlockUntilMs) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
if (touchLightSleepActive) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Ignore side-key handling while BOOT/user button is held.
|
||||
if (digitalRead(BUTTON_PIN) == LOW) {
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case State::IRQ_PENDING:
|
||||
// Initial debounce after expander interrupt edge.
|
||||
if ((uint32_t)(now - irqAtMs) < DEBOUNCE_MS) {
|
||||
return SAMPLE_MS;
|
||||
}
|
||||
|
||||
if (isPca9535SideKeyPressed()) {
|
||||
state = State::PRESSED;
|
||||
pressStartMs = now;
|
||||
return SAMPLE_MS;
|
||||
}
|
||||
|
||||
// Spurious/cleared edge.
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
|
||||
case State::PRESSED: {
|
||||
if (isPca9535SideKeyPressed()) {
|
||||
// Fire long-press action as soon as threshold is reached, without waiting for release.
|
||||
if (!longPressFired && (uint32_t)(now - pressStartMs) >= LONG_PRESS_MIN_MS &&
|
||||
(uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) {
|
||||
t5BacklightToggleUser();
|
||||
longPressFired = true;
|
||||
lastActionMs = now;
|
||||
}
|
||||
return SAMPLE_MS;
|
||||
}
|
||||
|
||||
// Released: if long-press already fired, do nothing. Otherwise classify short press.
|
||||
const uint32_t heldMs = now - pressStartMs;
|
||||
if (!longPressFired && heldMs >= SHORT_PRESS_MIN_MS && (uint32_t)(now - lastActionMs) >= ACTION_COOLDOWN_MS) {
|
||||
// If timeout forced touch/backlight off, short-press acts as a wake action first.
|
||||
if (t5TouchIsForcedByTimeout()) {
|
||||
t5TouchHandleUserInput();
|
||||
t5BacklightHandleUserInput();
|
||||
} else {
|
||||
toggleTouchInputEnabled();
|
||||
}
|
||||
lastActionMs = now;
|
||||
}
|
||||
|
||||
resetStateAndStop();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
case State::REST:
|
||||
default:
|
||||
return OSThread::disable();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
enum class State : uint8_t {
|
||||
REST,
|
||||
IRQ_PENDING,
|
||||
PRESSED,
|
||||
};
|
||||
|
||||
static constexpr uint32_t SAMPLE_MS = 15;
|
||||
static constexpr uint32_t DEBOUNCE_MS = 25;
|
||||
static constexpr uint32_t SHORT_PRESS_MIN_MS = 30;
|
||||
static constexpr uint32_t LONG_PRESS_MIN_MS = 450;
|
||||
static constexpr uint32_t ACTION_COOLDOWN_MS = 180;
|
||||
|
||||
static SideKeyInterruptThread *instance;
|
||||
|
||||
static void isr()
|
||||
{
|
||||
if (instance) {
|
||||
instance->onInterruptEdge();
|
||||
}
|
||||
}
|
||||
|
||||
void onInterruptEdge()
|
||||
{
|
||||
if (touchLightSleepActive) {
|
||||
return;
|
||||
}
|
||||
const uint32_t now = millis();
|
||||
if (now < touchResumeBlockUntilMs) {
|
||||
return;
|
||||
}
|
||||
if (state != State::REST) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = State::IRQ_PENDING;
|
||||
irqAtMs = millis();
|
||||
startThread();
|
||||
}
|
||||
|
||||
void startThread()
|
||||
{
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
}
|
||||
|
||||
void resetStateAndStop()
|
||||
{
|
||||
state = State::REST;
|
||||
longPressFired = false;
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
int onLightSleep(void *)
|
||||
{
|
||||
detachInterrupt(BOARD_PCA9535_INT);
|
||||
// Clear any latched PCA9535 interrupt before enabling GPIO wake.
|
||||
// If INT is left asserted low, light sleep exits immediately.
|
||||
uint8_t ignored = 0xFF;
|
||||
(void)readPca9535Port1(&ignored);
|
||||
resetStateAndStop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int onLightSleepEnd(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
(void)cause;
|
||||
// Consume any pending interrupt source before reattaching ISR.
|
||||
uint8_t ignored = 0xFF;
|
||||
(void)readPca9535Port1(&ignored);
|
||||
pinMode(BOARD_PCA9535_INT, INPUT_PULLUP);
|
||||
attachInterrupt(BOARD_PCA9535_INT, SideKeyInterruptThread::isr, FALLING);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
CallbackObserver<SideKeyInterruptThread, void *> lsObserver{this, &SideKeyInterruptThread::onLightSleep};
|
||||
CallbackObserver<SideKeyInterruptThread, esp_sleep_wakeup_cause_t> lsEndObserver{this,
|
||||
&SideKeyInterruptThread::onLightSleepEnd};
|
||||
#endif
|
||||
|
||||
volatile State state = State::REST;
|
||||
volatile uint32_t irqAtMs = 0;
|
||||
uint32_t pressStartMs = 0;
|
||||
bool longPressFired = false;
|
||||
uint32_t lastActionMs = 0;
|
||||
};
|
||||
|
||||
SideKeyInterruptThread *SideKeyInterruptThread::instance = nullptr;
|
||||
SideKeyInterruptThread *sideKeyThread = nullptr;
|
||||
#endif
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
void refreshTouchIndicatorInInkHUD(bool async = true)
|
||||
{
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
NicheGraphics::InkHUD::SystemApplet *touchStatus = nullptr;
|
||||
for (auto *sa : inkhud->systemApplets) {
|
||||
if (sa && sa->name && strcmp(sa->name, "TouchStatus") == 0) {
|
||||
touchStatus = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (touchStatus) {
|
||||
if (inkhud->isTouchEnabled())
|
||||
touchStatus->sendToBackground();
|
||||
else
|
||||
touchStatus->bringToForeground();
|
||||
}
|
||||
|
||||
// Re-render all applets so touch-status visibility changes are immediately reflected.
|
||||
inkhud->forceUpdate(NicheGraphics::Drivers::EInk::UpdateTypes::FAST, true, async);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void t5BacklightSetUserEnabled(bool enabled)
|
||||
{
|
||||
backlightUserEnabled = enabled;
|
||||
if (enabled) {
|
||||
// Manual ON should release auto-off gates.
|
||||
backlightForcedByTimeout = false;
|
||||
backlightForcedBySleep = false;
|
||||
}
|
||||
applyBacklightState();
|
||||
}
|
||||
|
||||
bool t5BacklightIsUserEnabled()
|
||||
{
|
||||
return backlightUserEnabled;
|
||||
}
|
||||
|
||||
void t5BacklightToggleUser()
|
||||
{
|
||||
t5BacklightSetUserEnabled(!backlightUserEnabled);
|
||||
}
|
||||
|
||||
void t5BacklightSetForcedByTimeout(bool forced)
|
||||
{
|
||||
backlightForcedByTimeout = forced;
|
||||
applyBacklightState();
|
||||
}
|
||||
|
||||
void t5BacklightSetForcedBySleep(bool forced)
|
||||
{
|
||||
backlightForcedBySleep = forced;
|
||||
applyBacklightState();
|
||||
}
|
||||
|
||||
void t5BacklightHandleUserInput()
|
||||
{
|
||||
// Screen-timeout should be lifted by direct user interaction.
|
||||
backlightForcedByTimeout = false;
|
||||
applyBacklightState();
|
||||
}
|
||||
|
||||
void t5TouchSetForcedByTimeout(bool forced)
|
||||
{
|
||||
touchForcedByTimeout = forced;
|
||||
touchStateEpoch++;
|
||||
touchIndicatorRefreshPending = true;
|
||||
|
||||
if (forced) {
|
||||
// While timeout-forced, keep controller asleep to avoid stale IRQ chatter.
|
||||
touchNeedsWake = false;
|
||||
if (touchControllerReady && !touchLightSleepActive) {
|
||||
touch.sleep();
|
||||
}
|
||||
} else if (touchInputEnabled && touchControllerReady && !touchLightSleepActive) {
|
||||
// Defer wake until readTouch() so I2C settles post-state transition.
|
||||
touchNeedsWake = true;
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
if (!touchLightSleepActive) {
|
||||
refreshTouchIndicatorInInkHUD();
|
||||
touchIndicatorRefreshPending = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool t5TouchIsForcedByTimeout()
|
||||
{
|
||||
return touchForcedByTimeout;
|
||||
}
|
||||
|
||||
void t5TouchHandleUserInput()
|
||||
{
|
||||
t5TouchSetForcedByTimeout(false);
|
||||
}
|
||||
|
||||
void t5SetHomeCapButtonEventsEnabled(bool enabled)
|
||||
{
|
||||
homeCapButtonEventsEnabled = enabled;
|
||||
}
|
||||
|
||||
bool isTouchInputEnabled()
|
||||
{
|
||||
return touchInputEnabled && !touchForcedByTimeout && !touchLightSleepActive;
|
||||
}
|
||||
|
||||
void setTouchInputEnabled(bool enabled, bool showIndicator)
|
||||
{
|
||||
if (touchInputEnabled == enabled) {
|
||||
LOG_DEBUG("touchscreen1: setTouchInputEnabled no-op en=%d", enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("touchscreen1: setTouchInputEnabled %d -> %d (showIndicator=%d)", touchInputEnabled, enabled, showIndicator);
|
||||
touchInputEnabled = enabled;
|
||||
touchStateEpoch++;
|
||||
|
||||
if (enabled) {
|
||||
touchNeedsWake = touchControllerReady;
|
||||
if (touchControllerReady && !touchLightSleepActive) {
|
||||
LOG_DEBUG("touchscreen1: wakeup() on enable");
|
||||
touch.wakeup();
|
||||
touchNeedsWake = false;
|
||||
}
|
||||
} else {
|
||||
touchNeedsWake = false;
|
||||
if (touchControllerReady && !touchLightSleepActive) {
|
||||
LOG_DEBUG("touchscreen1: sleep() on disable");
|
||||
touch.sleep();
|
||||
}
|
||||
if (showIndicator) {
|
||||
showTouchIndicator("Touch OFF");
|
||||
touchIndicatorRefreshPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
if (showIndicator && !touchLightSleepActive) {
|
||||
refreshTouchIndicatorInInkHUD();
|
||||
touchIndicatorRefreshPending = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void toggleTouchInputEnabled()
|
||||
{
|
||||
setTouchInputEnabled(!touchInputEnabled, true);
|
||||
}
|
||||
|
||||
// Commands the GT911 into standby before the Wire bus is torn down.
|
||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
|
||||
struct TouchDeepSleepObserver {
|
||||
@@ -87,8 +515,96 @@ struct TouchDeepSleepObserver {
|
||||
CallbackObserver<TouchDeepSleepObserver, void *> observer{this, &TouchDeepSleepObserver::onDeepSleep};
|
||||
} static touchDeepSleepObserver;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
struct TouchLightSleepObserver {
|
||||
int onLightSleep(void *)
|
||||
{
|
||||
touchLightSleepActive = true;
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Render touch-off overlay before sleeping so user sees touch is unavailable.
|
||||
touchIndicatorRefreshPending = true;
|
||||
refreshTouchIndicatorInInkHUD(false);
|
||||
touchIndicatorRefreshPending = false;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
CallbackObserver<TouchLightSleepObserver, void *> observer{this, &TouchLightSleepObserver::onLightSleep};
|
||||
} static touchLightSleepObserver;
|
||||
|
||||
struct TouchLightSleepEndObserver {
|
||||
int onLightSleepEnd(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
(void)cause;
|
||||
touchLightSleepActive = false;
|
||||
|
||||
if (!touchControllerReady) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (touchInputEnabled && !touchForcedByTimeout) {
|
||||
touchNeedsWake = true;
|
||||
} else {
|
||||
touchNeedsWake = false;
|
||||
}
|
||||
|
||||
touchStateEpoch++;
|
||||
touchResumeBlockUntilMs = millis() + 150;
|
||||
touchIndicatorRefreshPending = !isTouchInputEnabled();
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Clear sleep-time touch overlay after wake.
|
||||
touchIndicatorRefreshPending = true;
|
||||
refreshTouchIndicatorInInkHUD();
|
||||
touchIndicatorRefreshPending = false;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
CallbackObserver<TouchLightSleepEndObserver, esp_sleep_wakeup_cause_t> observer{this,
|
||||
&TouchLightSleepEndObserver::onLightSleepEnd};
|
||||
} static touchLightSleepEndObserver;
|
||||
#endif
|
||||
|
||||
bool readTouch(int16_t *x, int16_t *y)
|
||||
{
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
static uint32_t suppressUntilMs = 0;
|
||||
static uint32_t seenTouchStateEpoch = 0;
|
||||
|
||||
// Reset transient gesture helpers whenever touch mode changes.
|
||||
if (seenTouchStateEpoch != touchStateEpoch) {
|
||||
seenTouchStateEpoch = touchStateEpoch;
|
||||
suppressUntilMs = 0;
|
||||
}
|
||||
|
||||
// Let buses and peripherals settle briefly after light-sleep wake.
|
||||
if (millis() < touchResumeBlockUntilMs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (touchIndicatorRefreshPending) {
|
||||
refreshTouchIndicatorInInkHUD();
|
||||
touchIndicatorRefreshPending = false;
|
||||
}
|
||||
|
||||
if (!isTouchInputEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (touchNeedsWake && touchControllerReady) {
|
||||
LOG_DEBUG("touchscreen1: wakeup() on deferred resume");
|
||||
touch.wakeup();
|
||||
touchNeedsWake = false;
|
||||
suppressUntilMs = millis() + 60;
|
||||
return false;
|
||||
}
|
||||
|
||||
// After a recovery pulse, emit a brief "released" window so gesture state can reset.
|
||||
if (suppressUntilMs != 0 && millis() < suppressUntilMs) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!digitalRead(GT911_PIN_INT)) {
|
||||
int16_t raw_x;
|
||||
int16_t raw_y;
|
||||
@@ -123,22 +639,71 @@ bool readTouch(int16_t *x, int16_t *y)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// T5-S3-ePaper Pro specific (late-) init
|
||||
void lateInitVariant(void)
|
||||
void variant_shutdown()
|
||||
{
|
||||
// Ensure backlight is off during deep sleep.
|
||||
t5BacklightSetForcedBySleep(true);
|
||||
}
|
||||
|
||||
void lateInitVariant()
|
||||
{
|
||||
touch.setPins(GT911_PIN_RST, GT911_PIN_INT);
|
||||
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) {
|
||||
// Match LilyGO sample behavior: GT911 center/home capacitive key callback.
|
||||
touch.setHomeButtonCallback(
|
||||
[](void *user_data) {
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
if (!homeCapButtonEventsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t lastHomeMs = 0;
|
||||
const uint32_t now = millis();
|
||||
if ((uint32_t)(now - lastHomeMs) < 220) {
|
||||
return; // debounce repeated key reports while still touched
|
||||
}
|
||||
lastHomeMs = now;
|
||||
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
if (inkhud) {
|
||||
// Route through InkHUD EXIT/HOME path (menu close, etc).
|
||||
inkhud->exitShort();
|
||||
}
|
||||
#else
|
||||
(void)user_data;
|
||||
#endif
|
||||
},
|
||||
nullptr);
|
||||
touchControllerReady = true;
|
||||
touchInputEnabled = true;
|
||||
touchForcedByTimeout = false;
|
||||
touchLightSleepActive = false;
|
||||
touchStateEpoch++;
|
||||
touchDeepSleepObserver.observer.observe(¬ifyDeepSleep);
|
||||
#ifdef ARCH_ESP32
|
||||
touchLightSleepObserver.observer.observe(¬ifyLightSleep);
|
||||
touchLightSleepEndObserver.observer.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
touchBridge.observe(touchScreenImpl1);
|
||||
#endif
|
||||
} else {
|
||||
touchControllerReady = false;
|
||||
LOG_ERROR("Failed to find touch controller!");
|
||||
}
|
||||
|
||||
#if defined(BOARD_PCA9535_ADDR) && defined(BOARD_PCA9535_BUTTON_MASK)
|
||||
// Start side-key interrupt handling after touch init is complete.
|
||||
if (!sideKeyThread) {
|
||||
sideKeyThread = new SideKeyInterruptThread();
|
||||
sideKeyThread->begin();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "NRF52Bluetooth.h"
|
||||
#include "BLEDfuSecure.h"
|
||||
#include "BluetoothCommon.h"
|
||||
#include "HardwareRNG.h"
|
||||
#include "PowerFSM.h"
|
||||
#include "configuration.h"
|
||||
#include "main.h"
|
||||
@@ -272,9 +273,13 @@ void NRF52Bluetooth::setup()
|
||||
Bluefruit.setTxPower(NRF52_BLE_TX_POWER);
|
||||
#endif
|
||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
configuredPasskey = config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN
|
||||
? config.bluetooth.fixed_pin
|
||||
: random(100000, 999999);
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN) {
|
||||
configuredPasskey = config.bluetooth.fixed_pin;
|
||||
} else {
|
||||
uint32_t hwrand = 0;
|
||||
HardwareRNG::fill(reinterpret_cast<uint8_t *>(&hwrand), sizeof(hwrand));
|
||||
configuredPasskey = hwrand % 900000u + 100000u;
|
||||
}
|
||||
auto pinString = std::to_string(configuredPasskey);
|
||||
LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey);
|
||||
Bluefruit.Security.setPIN(pinString.c_str());
|
||||
|
||||
@@ -9,13 +9,10 @@
|
||||
#include "PortduinoGlue.h"
|
||||
#include "SHA256.h"
|
||||
#include "api/ServerAPI.h"
|
||||
#include "linux/gpio/LinuxGPIOPin.h"
|
||||
#include "meshUtils.h"
|
||||
#include <ErriezCRC32.h>
|
||||
#include <Utility.h>
|
||||
#include <assert.h>
|
||||
#include <bluetooth/bluetooth.h>
|
||||
#include <bluetooth/hci.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -25,6 +22,12 @@
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||
#include "linux/gpio/LinuxGPIOPin.h"
|
||||
#include <bluetooth/bluetooth.h>
|
||||
#include <bluetooth/hci.h>
|
||||
#endif
|
||||
|
||||
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||
#include <cxxabi.h>
|
||||
#endif
|
||||
@@ -130,9 +133,9 @@ void getMacAddr(uint8_t *dmac)
|
||||
}
|
||||
} else if (portduino_config.mac_address.length() > 11) {
|
||||
MAC_from_string(portduino_config.mac_address, dmac);
|
||||
exit;
|
||||
return;
|
||||
} else {
|
||||
|
||||
#ifdef PORTDUINO_LINUX_HARDWARE
|
||||
struct hci_dev_info di = {0};
|
||||
di.dev_id = 0;
|
||||
bdaddr_t bdaddr;
|
||||
@@ -152,6 +155,11 @@ void getMacAddr(uint8_t *dmac)
|
||||
dmac[3] = di.bdaddr.b[2];
|
||||
dmac[4] = di.bdaddr.b[1];
|
||||
dmac[5] = di.bdaddr.b[0];
|
||||
#else
|
||||
// No BlueZ on non-Linux hosts (e.g. macOS). Leave dmac at its default;
|
||||
// the caller can override via the --hwid CLI flag or the YAML config.
|
||||
(void)dmac;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "platform/portduino/PortduinoGlue.h"
|
||||
#include <RadioLib.h>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <libpinedio-usb.h>
|
||||
#include <unistd.h>
|
||||
@@ -34,7 +35,7 @@ class Ch341Hal : public RadioLibHal
|
||||
: RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING)
|
||||
{
|
||||
if (serial != "") {
|
||||
strncpy(pinedio.serial_number, serial.c_str(), 8);
|
||||
std::strncpy(pinedio.serial_number, serial.c_str(), 8);
|
||||
pinedio_set_option(&pinedio, PINEDIO_OPTION_SEARCH_SERIAL, 1);
|
||||
}
|
||||
// LOG_INFO("USB Serial: %s", pinedio.serial_number);
|
||||
@@ -59,8 +60,11 @@ class Ch341Hal : public RadioLibHal
|
||||
|
||||
void getSerialString(char *_serial, size_t len)
|
||||
{
|
||||
len = len > 8 ? 8 : len;
|
||||
strncpy(_serial, pinedio.serial_number, len);
|
||||
if (len == 0)
|
||||
return;
|
||||
size_t bytesCopied = (len - 1) < 8 ? (len - 1) : 8;
|
||||
std::strncpy(_serial, pinedio.serial_number, bytesCopied);
|
||||
_serial[bytesCopied] = '\0';
|
||||
}
|
||||
|
||||
void getProductString(char *_product_string, size_t len)
|
||||
|
||||
-75
@@ -28,54 +28,6 @@
|
||||
#define NUM_CELLS 1
|
||||
#endif
|
||||
|
||||
// Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
|
||||
#ifndef BATTERY_SENSE_SAMPLES
|
||||
#define BATTERY_SENSE_SAMPLES 15
|
||||
#endif
|
||||
|
||||
#ifndef ADC_MULTIPLIER
|
||||
#define ADC_MULTIPLIER 2.0
|
||||
#endif
|
||||
|
||||
#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
|
||||
#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
|
||||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef DELAY_FOREVER
|
||||
#define DELAY_FOREVER portMAX_DELAY
|
||||
#endif
|
||||
|
||||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||||
// make sure it's included. If something is wrong with NRF52
|
||||
// definition - compilation will fail on missing definition
|
||||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||||
#define AREF_VOLTAGE 3.3
|
||||
#endif
|
||||
|
||||
#ifdef BAT_MEASURE_ADC_UNIT
|
||||
extern RTC_NOINIT_ATTR uint64_t RTC_reg_b;
|
||||
#include "soc/sens_reg.h" // needed for adc pin reset
|
||||
@@ -134,31 +86,6 @@ extern RAK9154Sensor rak9154Sensor;
|
||||
extern XPowersLibInterface *PMU;
|
||||
#endif
|
||||
|
||||
#ifndef HAS_PMU
|
||||
// Copy of the base class defined in axp20x.h, to prevent an automatic wire.h dependency
|
||||
class HasBatteryLevel
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||||
*/
|
||||
virtual int getBatteryPercent() { return -1; }
|
||||
|
||||
/**
|
||||
* The raw voltage of the battery or NAN if unknown
|
||||
*/
|
||||
virtual uint16_t getBattVoltage() { return 0; }
|
||||
|
||||
/**
|
||||
* return true if there is a battery installed in this unit
|
||||
*/
|
||||
virtual bool isBatteryConnect() { return false; }
|
||||
|
||||
virtual bool isVbusIn() { return false; }
|
||||
virtual bool isCharging() { return false; }
|
||||
};
|
||||
#endif
|
||||
|
||||
class Power : public concurrency::OSThread
|
||||
{
|
||||
|
||||
@@ -183,8 +110,6 @@ class Power : public concurrency::OSThread
|
||||
void attachPowerInterrupts();
|
||||
void detachPowerInterrupts();
|
||||
|
||||
volatile bool pmu_irq = false;
|
||||
|
||||
protected:
|
||||
meshtastic::PowerStatus *statusHandler;
|
||||
|
||||
|
||||
+16
-4
@@ -79,7 +79,7 @@ RTC_DATA_ATTR int bootCount = 0;
|
||||
*/
|
||||
void setCPUFast(bool on)
|
||||
{
|
||||
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT
|
||||
#if defined(ARCH_ESP32) && HAS_WIFI && !HAS_TFT && !defined(T_LORA_PAGER) && !defined(T_DECK)
|
||||
|
||||
if (isWifiAvailable()) {
|
||||
/*
|
||||
@@ -316,9 +316,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN
|
||||
|
||||
#ifdef HAS_PPM
|
||||
if (PPM) {
|
||||
LOG_INFO("PMM shutdown");
|
||||
console->flush();
|
||||
PPM->shutdown();
|
||||
// BQ25896 PMIC shutdown is a hard power-off state.
|
||||
// Only use it for "sleep forever" / explicit shutdown, because timed deep sleep
|
||||
// must remain wakeable by RTC timer.
|
||||
if (msecToWake == portMAX_DELAY) {
|
||||
LOG_INFO("PPM shutdown");
|
||||
console->flush();
|
||||
PPM->shutdown();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -425,6 +430,10 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
||||
#ifdef KB_INT
|
||||
gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL);
|
||||
#endif
|
||||
#ifdef BOARD_PCA9535_INT
|
||||
// Side-key interrupt line from PCA9535 expander (active low).
|
||||
gpio_wakeup_enable((gpio_num_t)BOARD_PCA9535_INT, GPIO_INTR_LOW_LEVEL);
|
||||
#endif
|
||||
#ifdef BUTTON_PIN
|
||||
gpio_num_t pin = (gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN);
|
||||
gpio_wakeup_enable(pin, GPIO_INTR_LOW_LEVEL);
|
||||
@@ -472,6 +481,9 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r
|
||||
#ifdef KB_INT
|
||||
gpio_wakeup_disable((gpio_num_t)KB_INT);
|
||||
#endif
|
||||
#ifdef BOARD_PCA9535_INT
|
||||
gpio_wakeup_disable((gpio_num_t)BOARD_PCA9535_INT);
|
||||
#endif
|
||||
#ifdef BUTTON_PIN
|
||||
// Disable wake-on-button interrupt. Re-attach normal button-interrupts
|
||||
gpio_wakeup_disable(pin);
|
||||
|
||||
@@ -303,6 +303,10 @@ void setup()
|
||||
{
|
||||
initializeTestEnvironment();
|
||||
|
||||
// Wait for portduino's millis() clock to start ticking before tests run
|
||||
testDelay(10);
|
||||
testDelay(2000);
|
||||
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_setLastSentToMesh_stores_millis);
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#include "meshUtils.h"
|
||||
#include <cstring>
|
||||
#include <unity.h>
|
||||
|
||||
void setUp(void) {}
|
||||
void tearDown(void) {}
|
||||
|
||||
// --- Valid UTF-8 should pass through unchanged ---
|
||||
|
||||
void test_ascii_unchanged()
|
||||
{
|
||||
char buf[32] = "Hello World";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("Hello World", buf);
|
||||
}
|
||||
|
||||
void test_valid_2byte_unchanged()
|
||||
{
|
||||
// "café" — é is C3 A9
|
||||
char buf[16] = "caf\xC3\xA9";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("caf\xC3\xA9", buf);
|
||||
}
|
||||
|
||||
void test_valid_3byte_unchanged()
|
||||
{
|
||||
// "€" is E2 82 AC
|
||||
char buf[16] = "\xE2\x82\xAC";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("\xE2\x82\xAC", buf);
|
||||
}
|
||||
|
||||
void test_valid_4byte_emoji_unchanged()
|
||||
{
|
||||
// 🌙 is F0 9F 8C 99
|
||||
char buf[16] = "\xF0\x9F\x8C\x99";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("\xF0\x9F\x8C\x99", buf);
|
||||
}
|
||||
|
||||
void test_valid_mixed_unchanged()
|
||||
{
|
||||
// "Hi 🌙!" — mix of ASCII and 4-byte
|
||||
char buf[16] = "Hi \xF0\x9F\x8C\x99!";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("Hi \xF0\x9F\x8C\x99!", buf);
|
||||
}
|
||||
|
||||
void test_empty_string()
|
||||
{
|
||||
char buf[8] = "";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("", buf);
|
||||
}
|
||||
|
||||
// --- Invalid sequences observed in the wild ---
|
||||
|
||||
void test_truncated_4byte_at_end()
|
||||
{
|
||||
// Name with valid emoji 🌙 followed by a truncated 4-byte sequence + ASCII
|
||||
char buf[32] = "Lunar Tower \xF0\x9F\x8C\x99\xF0\x9F\x97"
|
||||
"4";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
// The 🌙 should be preserved; F0 9F 97 is an incomplete 4-byte sequence,
|
||||
// '4' (0x34) is not a valid continuation byte
|
||||
TEST_ASSERT_EQUAL_STRING("Lunar Tower \xF0\x9F\x8C\x99???4", buf);
|
||||
}
|
||||
|
||||
void test_lone_lead_bytes_without_continuations()
|
||||
{
|
||||
// Mixed ASCII with stray multibyte lead bytes (E1, F3) lacking proper continuations
|
||||
char buf[32] = "Mesht\xE1\xF3tic 37e2";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
// E1 expects 2 continuation bytes, but F3 is not a continuation → E1 replaced
|
||||
// F3 expects 3 continuation bytes, 't','i','c' are not continuations → F3 replaced
|
||||
TEST_ASSERT_EQUAL_STRING("Mesht??tic 37e2", buf);
|
||||
}
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
void test_bare_continuation_byte()
|
||||
{
|
||||
// 0x80 alone is invalid (continuation byte with no lead)
|
||||
char buf[8] = "\x80";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("?", buf);
|
||||
}
|
||||
|
||||
void test_overlong_2byte()
|
||||
{
|
||||
// C0 AF is an overlong encoding of U+002F '/'
|
||||
char buf[8] = "\xC0\xAF";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
// C0 is a 2-byte lead, AF is valid continuation, but codepoint 0x2F < 0x80 → overlong
|
||||
// C0 replaced, AF (now bare continuation) also replaced
|
||||
TEST_ASSERT_EQUAL_STRING("??", buf);
|
||||
}
|
||||
|
||||
void test_surrogate_half()
|
||||
{
|
||||
// ED A0 80 encodes U+D800 (surrogate half — invalid in UTF-8)
|
||||
char buf[8] = "\xED\xA0\x80";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("???", buf);
|
||||
}
|
||||
|
||||
void test_5byte_sequence_rejected()
|
||||
{
|
||||
// F8 80 80 80 80 — 5-byte sequence, not valid UTF-8
|
||||
char buf[8] = "\xF8\x80\x80\x80\x80";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
// F8 is invalid lead (>= 0xF8), each 0x80 is bare continuation
|
||||
TEST_ASSERT_EQUAL_STRING("?????", buf);
|
||||
}
|
||||
|
||||
void test_truncated_3byte_at_buffer_end()
|
||||
{
|
||||
// Buffer is exactly 4 bytes: E2 82 then forced null at [3]
|
||||
char buf[4];
|
||||
buf[0] = '\xE2';
|
||||
buf[1] = '\x82';
|
||||
buf[2] = '\0'; // String ends before the 3-byte sequence completes
|
||||
buf[3] = '\0';
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("??", buf);
|
||||
}
|
||||
|
||||
void test_null_termination_enforced()
|
||||
{
|
||||
// Fill buffer completely with no null terminator
|
||||
char buf[5];
|
||||
memset(buf, 'A', sizeof(buf));
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
// Should be null-terminated and content preserved (all ASCII)
|
||||
TEST_ASSERT_EQUAL_STRING("AAAA", buf);
|
||||
}
|
||||
|
||||
void test_null_buffer()
|
||||
{
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(nullptr, 10));
|
||||
}
|
||||
|
||||
void test_zero_size()
|
||||
{
|
||||
char buf[4] = "Hi";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, 0));
|
||||
// Buffer should be untouched
|
||||
TEST_ASSERT_EQUAL_STRING("Hi", buf);
|
||||
}
|
||||
|
||||
void test_valid_max_codepoint()
|
||||
{
|
||||
// U+10FFFF = F4 8F BF BF (maximum valid Unicode codepoint)
|
||||
char buf[8] = "\xF4\x8F\xBF\xBF";
|
||||
TEST_ASSERT_FALSE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
TEST_ASSERT_EQUAL_STRING("\xF4\x8F\xBF\xBF", buf);
|
||||
}
|
||||
|
||||
void test_above_max_codepoint()
|
||||
{
|
||||
// U+110000 = F4 90 80 80 (just above maximum valid Unicode)
|
||||
char buf[8] = "\xF4\x90\x80\x80";
|
||||
TEST_ASSERT_TRUE(sanitizeUtf8(buf, sizeof(buf)));
|
||||
}
|
||||
|
||||
void setup()
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
||||
// Valid UTF-8 passthrough
|
||||
RUN_TEST(test_ascii_unchanged);
|
||||
RUN_TEST(test_valid_2byte_unchanged);
|
||||
RUN_TEST(test_valid_3byte_unchanged);
|
||||
RUN_TEST(test_valid_4byte_emoji_unchanged);
|
||||
RUN_TEST(test_valid_mixed_unchanged);
|
||||
RUN_TEST(test_empty_string);
|
||||
|
||||
// Invalid sequences observed in the wild
|
||||
RUN_TEST(test_truncated_4byte_at_end);
|
||||
RUN_TEST(test_lone_lead_bytes_without_continuations);
|
||||
|
||||
// Edge cases
|
||||
RUN_TEST(test_bare_continuation_byte);
|
||||
RUN_TEST(test_overlong_2byte);
|
||||
RUN_TEST(test_surrogate_half);
|
||||
RUN_TEST(test_5byte_sequence_rejected);
|
||||
RUN_TEST(test_truncated_3byte_at_buffer_end);
|
||||
RUN_TEST(test_null_termination_enforced);
|
||||
RUN_TEST(test_null_buffer);
|
||||
RUN_TEST(test_zero_size);
|
||||
RUN_TEST(test_valid_max_codepoint);
|
||||
RUN_TEST(test_above_max_codepoint);
|
||||
|
||||
exit(UNITY_END());
|
||||
}
|
||||
|
||||
void loop() {}
|
||||
@@ -35,9 +35,10 @@
|
||||
// code)
|
||||
#endif
|
||||
|
||||
// Leave undefined to disable our PMU IRQ handler. DO NOT ENABLE THIS because the pmuirq can cause sperious interrupts
|
||||
// and waking from light sleep
|
||||
// #define PMU_IRQ 35
|
||||
// Voiding more warranties.
|
||||
#define PMU_IRQ 35
|
||||
#define PMU_POWER_BUTTON_IS_CANCEL // maps a short click of the power button to a cancel action (turning off the screen)
|
||||
|
||||
#define HAS_AXP192
|
||||
#define GPS_UBLOX
|
||||
#define GPS_RX_PIN 34
|
||||
|
||||
@@ -120,7 +120,7 @@ build_flags =
|
||||
-D TFT_OFFSET_Y=0
|
||||
-D TFT_OFFSET_ROTATION=0
|
||||
-D SCREEN_ROTATE
|
||||
-D SCREEN_TRANSITION_FRAMERATE=5
|
||||
-D SCREEN_TRANSITION_FRAMERATE=30
|
||||
-D BRIGHTNESS_DEFAULT=130 ; Medium Low Brightness
|
||||
-D HAS_TOUCHSCREEN=1
|
||||
-D TOUCH_I2C_PORT=0
|
||||
@@ -133,4 +133,4 @@ lib_deps = ${heltec_v4_base.lib_deps}
|
||||
# renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX
|
||||
lovyan03/LovyanGFX@1.2.19
|
||||
# renovate: datasource=git-refs depName=Quency-D_chsc6x packageName=https://github.com/Quency-D/chsc6x gitBranch=master
|
||||
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
||||
https://github.com/Quency-D/chsc6x/archive/5cbead829d6b432a8d621ed1aafd4eb474fd4f27.zip
|
||||
|
||||
@@ -20,5 +20,5 @@ build_flags =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
||||
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||
upload_speed = 921600
|
||||
|
||||
@@ -13,7 +13,7 @@ build_flags =
|
||||
lib_deps =
|
||||
${esp32s3_base.lib_deps}
|
||||
# renovate: datasource=git-refs depName=meshtastic-st7789 packageName=https://github.com/meshtastic/st7789 gitBranch=main
|
||||
https://github.com/meshtastic/st7789/archive/92bae2e4a307afb430c3b0bc3d661c55ee1565f0.zip
|
||||
https://github.com/meshtastic/st7789/archive/5180423ae2dbf5885168a8bfb308c7fb7eff6930.zip
|
||||
# renovate: datasource=github-tags depName=pschatzmann_arduino-audio-driver packageName=pschatzmann/arduino-audio-driver
|
||||
https://github.com/pschatzmann/arduino-audio-driver/archive/v0.2.1.zip
|
||||
# renovate: datasource=git-refs depName=ESP8266Audio packageName=https://github.com/meshtastic/ESP8266Audio gitBranch=meshtastic-2.0.0-dacfix
|
||||
|
||||
@@ -47,10 +47,9 @@
|
||||
#define TFT_OFFSET_Y 0
|
||||
#define TFT_OFFSET_ROTATION 0
|
||||
#define SCREEN_ROTATE
|
||||
#define SCREEN_TRANSITION_FRAMERATE 5
|
||||
#define SCREEN_TRANSITION_FRAMERATE 30
|
||||
|
||||
// Picomputer gets a white on black display
|
||||
#define TFT_MESH_OVERRIDE COLOR565(255, 255, 255)
|
||||
|
||||
#define INPUTBROKER_MATRIX_TYPE 1
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#define TFT_OFFSET_ROTATION 0
|
||||
#define TFT_BL 45
|
||||
#define SCREEN_ROTATE
|
||||
#define SCREEN_TRANSITION_FRAMERATE 5 // fps
|
||||
#define SCREEN_TRANSITION_FRAMERATE 30 // fps
|
||||
#define USE_TFTDISPLAY 1
|
||||
|
||||
#define HAS_TOUCHSCREEN 1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user