Compare commits

..

17 Commits

Author SHA1 Message Date
whywilson
c95991cee2 trunk fmt 2025-08-23 16:17:13 +08:00
whywilson
f901166f16 Merge branch 'develop' of https://github.com/meshtastic/firmware into on-screen-keyboard 2025-08-23 16:14:27 +08:00
whywilson
ddc66c5e84 Add longPress event for RotaryEncoder Press. 2025-08-23 15:26:05 +08:00
whywilson
0544998c9b Add On-Screen Keyboard for UpDownInterrupt. Pls notice the new keyboard layout was inspired and adviced by https://github.com/csrutil 2025-08-23 15:24:40 +08:00
Ben Meadors
093a37a2b0 On screen keyboard (#7705)
* Add on-screen keyboard implementation on Trackball device.

* Update On-Screen Keyboard to new layout.

* The on-screen keyboard dynamically adjusts the key size based on the screen.

* Improve input box display on small screens.

* Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens.

* Optimize the text alignment of numeric keys on ssd1306.

---------

Co-authored-by: whywilson <m.tools@qq.com>
2025-08-21 06:31:27 -05:00
whywilson
dca615fe3d Optimize the text alignment of numeric keys on ssd1306. 2025-08-21 07:03:52 +08:00
whywilson
cca2ead2c1 Merge branch 'master' into on-screen-keyboard 2025-08-21 06:20:44 +08:00
whywilson
53afebae41 Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens. 2025-08-20 21:49:25 +08:00
whywilson
4f4ede9c8c Merge branch 'master' into on-screen-keyboard 2025-08-20 14:21:23 +08:00
whywilson
fbd4138d98 Merge branch 'master' of https://github.com/meshtastic/firmware into on-screen-keyboard 2025-08-19 13:46:22 +08:00
whywilson
f4bb2ec0f0 Improve input box display on small screens. 2025-08-19 13:42:15 +08:00
whywilson
995752e31d The on-screen keyboard dynamically adjusts the key size based on the screen. 2025-08-18 18:02:19 +08:00
whywilson
75b12d318d Update On-Screen Keyboard to new layout. 2025-08-18 14:06:54 +08:00
whywilson
eea4d734d2 Merge branch 'master' of https://github.com/meshtastic/firmware into on-screen-keyboard 2025-08-18 09:47:08 +08:00
whywilson
e98c6debb2 Merge branch 'master' of https://github.com/meshtastic/firmware into on-screen-keyboard 2025-08-16 13:37:02 +08:00
Ben Meadors
ace45c1236 Merge branch 'master' into on-screen-keyboard 2025-08-15 06:20:03 -05:00
whywilson
6c7da1e6b1 Add on-screen keyboard implementation on Trackball device. 2025-08-14 08:34:42 +08:00
178 changed files with 2051 additions and 3288 deletions

View File

@@ -11,6 +11,11 @@ runs:
ref: ${{github.event.pull_request.head.ref}} ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Uncomment build epoch
shell: bash
run: |
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: | run: |
@@ -18,7 +23,7 @@ runs:
sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release sudo apt-get install -y cppcheck libbluetooth-dev libgpiod-dev libyaml-cpp-dev lsb-release
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
cache: pip cache: pip

View File

@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-python@v6 - uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
cache: pip cache: pip
@@ -370,7 +370,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@@ -439,7 +439,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x
@@ -494,7 +494,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -31,7 +31,7 @@ jobs:
repository: ${{github.event.pull_request.head.repo.full_name}} repository: ${{github.event.pull_request.head.repo.full_name}}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Check for PR labels - name: Check for PR labels
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const labels = context.payload.pull_request.labels.map(label => label.name); const labels = context.payload.pull_request.labels.map(label => label.name);

View File

@@ -177,7 +177,7 @@ jobs:
- name: Comment test results on PR - name: Comment test results on PR
if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped' if: github.event_name == 'pull_request' && needs.native-tests.result != 'skipped'
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
script: | script: |
const fs = require('fs'); const fs = require('fs');

View File

@@ -63,7 +63,7 @@ jobs:
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v5
with: with:
python-version: 3.x python-version: 3.x

View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Stale PR+Issues - name: Stale PR+Issues
uses: actions/stale@v10.0.0 uses: actions/stale@v9.1.0
with: with:
days-before-stale: 45 days-before-stale: 45
exempt-issue-labels: pinned,3.0 exempt-issue-labels: pinned,3.0

View File

@@ -47,7 +47,7 @@ jobs:
pio upgrade pio upgrade
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v5 uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22

View File

@@ -39,7 +39,7 @@ jobs:
git push git push
- name: Comment on PR - name: Comment on PR
uses: actions/github-script@v8 uses: actions/github-script@v7
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
script: | script: |

View File

@@ -4,28 +4,28 @@ cli:
plugins: plugins:
sources: sources:
- id: trunk - id: trunk
ref: v1.7.2 ref: v1.7.1
uri: https://github.com/trunk-io/plugins uri: https://github.com/trunk-io/plugins
lint: lint:
enabled: enabled:
- checkov@3.2.469 - checkov@3.2.461
- renovate@41.94.0 - renovate@41.74.0
- prettier@3.6.2 - prettier@3.6.2
- trufflehog@3.90.5 - trufflehog@3.90.5
- yamllint@1.37.1 - yamllint@1.37.1
- bandit@1.8.6 - bandit@1.8.6
- trivy@0.66.0 - trivy@0.64.1
- taplo@0.10.0 - taplo@0.9.3
- ruff@0.12.11 - ruff@0.12.7
- isort@6.0.1 - isort@6.0.1
- markdownlint@0.45.0 - markdownlint@0.45.0
- oxipng@9.1.5 - oxipng@9.1.5
- svgo@4.0.0 - svgo@4.0.0
- actionlint@1.7.7 - actionlint@1.7.7
- flake8@7.3.0 - flake8@7.3.0
- hadolint@2.13.1 - hadolint@2.12.1-beta
- shfmt@3.6.0 - shfmt@3.6.0
- shellcheck@0.11.0 - shellcheck@0.10.0
- black@25.1.0 - black@25.1.0
- git-diff-check - git-diff-check
- gitleaks@8.28.0 - gitleaks@8.28.0

View File

@@ -2,7 +2,7 @@
[portduino_base] [portduino_base]
platform = platform =
# renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop
https://github.com/meshtastic/platform-native/archive/c490bcd019e0658404088a61b96e653c9da22c45.zip https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip
framework = arduino framework = arduino
build_src_filter = build_src_filter =
@@ -31,8 +31,6 @@ lib_deps =
https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip
# renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library
adafruit/Adafruit seesaw Library@1.7.9 adafruit/Adafruit seesaw Library@1.7.9
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
build_flags = build_flags =
${arduino_base.build_flags} ${arduino_base.build_flags}

View File

@@ -50,7 +50,7 @@ lib_deps =
${radiolib_base.lib_deps} ${radiolib_base.lib_deps}
# renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main
https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip
lib_ignore = lib_ignore =
OneButton OneButton

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
sed -i 's/#-DBUILD_EPOCH=$UNIX_TIME/-DBUILD_EPOCH=$UNIX_TIME/' platformio.ini
export PIP_BREAK_SYSTEM_PACKAGES=1 export PIP_BREAK_SYSTEM_PACKAGES=1
if (echo $2 | grep -q "esp32"); then if (echo $2 | grep -q "esp32"); then

View File

@@ -7,7 +7,6 @@ SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "TFT_BUILD=0" SET "TFT_BUILD=0"
SET "BIGDB8=0" SET "BIGDB8=0"
SET "MUIDB8=0"
SET "BIGDB16=0" SET "BIGDB16=0"
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
@@ -15,12 +14,11 @@ SET "LOGCOUNTER=0"
SET "BPS_RESET=0" SET "BPS_RESET=0"
@REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable.
SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone t-eth-elite tlora-pager mesh-tab dreamcatcher ESP32-S3-Pico seeed-sensecap-indicator heltec_capsule_sensor_v3 vision-master icarus tracksenger elecrow-adv" SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone"
SET "C3=esp32c3" SET "C3=esp32c3"
@REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable. @REM FIXME: Determine flash size from PlatformIO variant, this is unmaintainable.
SET "BIGDB_8MB=crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger" SET "BIGDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator crowpanel-esp32s3 heltec_capsule_sensor_v3 heltec-v3 heltec-vision-master-e213 heltec-vision-master-e290 heltec-vision-master-t190 heltec-wireless-paper heltec-wireless-tracker heltec-wsl-v3 icarus seeed-xiao-s3 tbeam-s3-core tracksenger"
SET "MUIDB_8MB=picomputer-s3 unphone seeed-sensecap-indicator" SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite t-watch-s3"
SET "BIGDB_16MB=t-deck mesh-tab t-energy-s3 dreamcatcher ESP32-S3-Pico m5stack-cores3 station-g2 t-eth-elite tlora-pager t-watch-s3 elecrow-adv"
GOTO getopts GOTO getopts
:help :help
@@ -102,6 +100,7 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" (
) )
:skip-filename :skip-filename
SET "ESPTOOL_BAUD=1200"
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" ( IF NOT "__%PYTHON%__"=="____" (
@@ -121,10 +120,11 @@ IF NOT "__%PYTHON%__"=="____" (
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1 !ESPTOOL_CMD! >nul 2>&1
IF %ERRORLEVEL% EQU 9009 ( IF %ERRORLEVEL% GEQ 2 (
@REM 9009 = command not found on Windows @REM esptool exits with code 1 if help is displayed.
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1 EXIT /B 1
GOTO eof
) )
IF %DEBUG% EQU 1 ( IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
@@ -142,7 +142,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
IF %BPS_RESET% EQU 1 ( IF %BPS_RESET% EQU 1 (
@REM Attempt to change mode via 1200bps Reset. @REM Attempt to change mode via 1200bps Reset.
CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
GOTO eof GOTO eof
) )
@@ -164,15 +164,6 @@ FOR %%a IN (%BIGDB_8MB%) DO (
) )
:end_loop_bigdb_8mb :end_loop_bigdb_8mb
FOR %%a IN (%MUIDB_8MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %MUIDB_8MB%.
SET "MUIDB8=1"
GOTO end_loop_muidb_8mb
)
)
:end_loop_muidb_8mb
FOR %%a IN (%BIGDB_16MB%) DO ( FOR %%a IN (%BIGDB_16MB%) DO (
IF NOT "!FILENAME:%%a=!"=="!FILENAME!" ( IF NOT "!FILENAME:%%a=!"=="!FILENAME!" (
@REM We are working with any of %BIGDB_16MB%. @REM We are working with any of %BIGDB_16MB%.
@@ -183,7 +174,6 @@ FOR %%a IN (%BIGDB_16MB%) DO (
:end_loop_bigdb_16mb :end_loop_bigdb_16mb
IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected." IF %BIGDB8% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 8mb partition selected."
IF %MUIDB8% EQU 1 CALL :LOG_MESSAGE INFO "MUIDB 8mb partition selected."
IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected." IF %BIGDB16% EQU 1 CALL :LOG_MESSAGE INFO "BigDB 16mb partition selected."
@REM Extract BASENAME from %FILENAME% for later use. @REM Extract BASENAME from %FILENAME% for later use.
@@ -228,12 +218,6 @@ IF %BIGDB8% EQU 1 (
SET "SPIFFS_OFFSET=0x670000" SET "SPIFFS_OFFSET=0x670000"
) )
@REM Offsets for MUIDB 8mb.
IF %MUIDB8% EQU 1 (
SET "OTA_OFFSET=0x5D0000"
SET "SPIFFS_OFFSET=0x670000"
)
@REM Offsets for BigDB 16mb. @REM Offsets for BigDB 16mb.
IF %BIGDB16% EQU 1 ( IF %BIGDB16% EQU 1 (
SET "OTA_OFFSET=0x650000" SET "OTA_OFFSET=0x650000"

View File

@@ -5,12 +5,11 @@ BPS_RESET=false
TFT_BUILD=false TFT_BUILD=false
MCU="" MCU=""
# Constants
RESET_BAUD=1200
FIRMWARE_OFFSET=0x00
# Variant groups # Variant groups
BIGDB_8MB=( BIGDB_8MB=(
"picomputer-s3"
"unphone"
"seeed-sensecap-indicator"
"crowpanel-esp32s3" "crowpanel-esp32s3"
"heltec_capsule_sensor_v3" "heltec_capsule_sensor_v3"
"heltec-v3" "heltec-v3"
@@ -25,11 +24,6 @@ BIGDB_8MB=(
"tbeam-s3-core" "tbeam-s3-core"
"tracksenger" "tracksenger"
) )
MUIDB_8MB=(
"picomputer-s3"
"unphone"
"seeed-sensecap-indicator"
)
BIGDB_16MB=( BIGDB_16MB=(
"t-deck" "t-deck"
"mesh-tab" "mesh-tab"
@@ -39,9 +33,10 @@ BIGDB_16MB=(
"m5stack-cores3" "m5stack-cores3"
"station-g2" "station-g2"
"t-eth-elite" "t-eth-elite"
"tlora-pager"
"t-watch-s3" "t-watch-s3"
"elecrow-adv" "elecrow-adv-35-tft"
"elecrow-adv-24-28-tft"
"elecrow-adv1-43-50-70-tft"
) )
S3_VARIANTS=( S3_VARIANTS=(
"s3" "s3"
@@ -52,7 +47,6 @@ S3_VARIANTS=(
"station-g2" "station-g2"
"unphone" "unphone"
"t-eth-elite" "t-eth-elite"
"tlora-pager"
"mesh-tab" "mesh-tab"
"dreamcatcher" "dreamcatcher"
"ESP32-S3-Pico" "ESP32-S3-Pico"
@@ -127,7 +121,7 @@ while [ $# -gt 0 ]; do
done done
if [[ $BPS_RESET == true ]]; then if [[ $BPS_RESET == true ]]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
exit 0 exit 0
fi fi
@@ -164,13 +158,6 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
fi fi
done done
for variant in "${MUIDB_8MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then
OFFSET=0x670000
OTA_OFFSET=0x5D0000
fi
done
# littlefs* offset for BigDB 16mb and OTA OFFSET. # littlefs* offset for BigDB 16mb and OTA OFFSET.
for variant in "${BIGDB_16MB[@]}"; do for variant in "${BIGDB_16MB[@]}"; do
if [ -z "${FILENAME##*"$variant"*}" ]; then if [ -z "${FILENAME##*"$variant"*}" ]; then
@@ -214,8 +201,8 @@ if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then
fi fi
echo "Trying to flash ${FILENAME}, but first erasing and writing system information" echo "Trying to flash ${FILENAME}, but first erasing and writing system information"
$ESPTOOL_CMD erase-flash $ESPTOOL_CMD erase_flash
$ESPTOOL_CMD write-flash $FIRMWARE_OFFSET "${FILENAME}" $ESPTOOL_CMD write_flash 0x00 "${FILENAME}"
echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}"
$ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}"
echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}"

View File

@@ -6,8 +6,6 @@ SET "SCRIPT_NAME=%~nx0"
SET "DEBUG=0" SET "DEBUG=0"
SET "PYTHON=" SET "PYTHON="
SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_BAUD=115200"
SET "RESET_BAUD=1200"
SET "UPDATE_OFFSET=0x10000"
SET "ESPTOOL_CMD=" SET "ESPTOOL_CMD="
SET "LOGCOUNTER=0" SET "LOGCOUNTER=0"
SET "CHANGE_MODE=0" SET "CHANGE_MODE=0"
@@ -87,13 +85,14 @@ IF "!FILENAME:update=!"=="!FILENAME!" (
) )
:skip-filename :skip-filename
SET "ESPTOOL_BAUD=1200"
CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..."
IF NOT "__%PYTHON%__"=="____" ( IF NOT "__%PYTHON%__"=="____" (
SET "ESPTOOL_CMD=""!PYTHON!"" -m esptool" SET "ESPTOOL_CMD=!PYTHON! -m esptool"
CALL :LOG_MESSAGE DEBUG "Python interpreter supplied." CALL :LOG_MESSAGE DEBUG "Python interpreter supplied."
) ELSE ( ) ELSE (
CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool..." CALL :LOG_MESSAGE DEBUG "Python interpreter NOT supplied. Looking for esptool...
WHERE esptool >nul 2>&1 WHERE esptool >nul 2>&1
IF %ERRORLEVEL% EQU 0 ( IF %ERRORLEVEL% EQU 0 (
@REM WHERE exits with code 0 if esptool is found. @REM WHERE exits with code 0 if esptool is found.
@@ -106,11 +105,11 @@ IF NOT "__%PYTHON%__"=="____" (
CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..." CALL :LOG_MESSAGE DEBUG "Checking esptool command !ESPTOOL_CMD!..."
!ESPTOOL_CMD! >nul 2>&1 !ESPTOOL_CMD! >nul 2>&1
CALL :LOG_MESSAGE DEBUG "esptool exit code: %ERRORLEVEL%" IF %ERRORLEVEL% GEQ 2 (
IF %ERRORLEVEL% EQU 9009 ( @REM esptool exits with code 1 if help is displayed.
@REM 9009 = command not found on Windows
CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!" CALL :LOG_MESSAGE ERROR "esptool not found: !ESPTOOL_CMD!"
EXIT /B 1 EXIT /B 1
GOTO eof
) )
IF %DEBUG% EQU 1 ( IF %DEBUG% EQU 1 (
CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps." CALL :LOG_MESSAGE DEBUG "Skipping ESPTOOL_CMD steps."
@@ -128,13 +127,13 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!."
IF %CHANGE_MODE% EQU 1 ( IF %CHANGE_MODE% EQU 1 (
@REM Attempt to change mode via 1200bps Reset. @REM Attempt to change mode via 1200bps Reset.
CALL :RUN_ESPTOOL !RESET_BAUD! --after no_reset read_flash_status CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status
GOTO eof GOTO eof
) )
@REM Flashing operations. @REM Flashing operations.
CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET !UPDATE_OFFSET!..." CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..."
CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write-flash !UPDATE_OFFSET! "!FILENAME!" || GOTO eof CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof
CALL :LOG_MESSAGE INFO "Script complete!." CALL :LOG_MESSAGE INFO "Script complete!."
@@ -146,9 +145,9 @@ EXIT /B %ERRORLEVEL%
:RUN_ESPTOOL :RUN_ESPTOOL
@REM Subroutine used to run ESPTOOL_CMD with arguments. @REM Subroutine used to run ESPTOOL_CMD with arguments.
@REM Also handles %ERRORLEVEL%. @REM Also handles %ERRORLEVEL%.
@REM CALL :RUN_ESPTOOL [Baud] [erase-flash|write-flash] [OFFSET] [Filename] @REM CALL :RUN_ESPTOOL [Baud] [erase_flash|write_flash] [OFFSET] [Filename]
@REM. @REM.
@REM Example:: CALL :RUN_ESPTOOL 115200 write-flash 0x10000 "firmwarefile.bin" @REM Example:: CALL :RUN_ESPTOOL 115200 write_flash 0x10000 "firmwarefile.bin"
IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4"
CALL :RESET_ERROR CALL :RESET_ERROR
!ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4

View File

@@ -3,11 +3,6 @@
PYTHON=${PYTHON:-$(which python3 python|head -n 1)} PYTHON=${PYTHON:-$(which python3 python|head -n 1)}
CHANGE_MODE=false CHANGE_MODE=false
# Constants
FLASH_BAUD=115200
RESET_BAUD=1200
UPDATE_OFFSET=0x10000
# Determine the correct esptool command to use # Determine the correct esptool command to use
if "$PYTHON" -m esptool version >/dev/null 2>&1; then if "$PYTHON" -m esptool version >/dev/null 2>&1; then
ESPTOOL_CMD="$PYTHON -m esptool" ESPTOOL_CMD="$PYTHON -m esptool"
@@ -69,7 +64,7 @@ done
shift "$((OPTIND-1))" shift "$((OPTIND-1))"
if [ "$CHANGE_MODE" = true ]; then if [ "$CHANGE_MODE" = true ]; then
$ESPTOOL_CMD --baud $RESET_BAUD --after no_reset read_flash_status $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status
exit 0 exit 0
fi fi
@@ -80,7 +75,7 @@ fi
if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then
echo "Trying to flash update ${FILENAME}" echo "Trying to flash update ${FILENAME}"
$ESPTOOL_CMD --baud $FLASH_BAUD write-flash $UPDATE_OFFSET "${FILENAME}" $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}"
else else
show_help show_help
echo "Invalid file: ${FILENAME}" echo "Invalid file: ${FILENAME}"

View File

@@ -87,15 +87,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="2.7.9" date="2025-09-03">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.9</url>
</release>
<release version="2.7.8" date="2025-08-30">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8</url>
</release>
<release version="2.7.7" date="2025-08-28">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7</url>
</release>
<release version="2.7.6" date="2025-08-12"> <release version="2.7.6" date="2025-08-12">
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url> <url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6</url>
</release> </release>

View File

@@ -6,8 +6,6 @@ from os.path import join
import subprocess import subprocess
import json import json
import re import re
import time
from datetime import datetime
from readprops import readProps from readprops import readProps
@@ -127,16 +125,11 @@ for pref in userPrefs:
pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "") pref_flags.append("-D" + pref + "=" + env.StringifyMacro(userPrefs[pref]) + "")
# General options that are passed to the C and C++ compilers # General options that are passed to the C and C++ compilers
# Calculate unix epoch for current day (midnight)
current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
build_epoch = int(current_date.timestamp())
flags = [ flags = [
"-DAPP_VERSION=" + verObj["long"], "-DAPP_VERSION=" + verObj["long"],
"-DAPP_VERSION_SHORT=" + verObj["short"], "-DAPP_VERSION_SHORT=" + verObj["short"],
"-DAPP_ENV=" + env.get("PIOENV"), "-DAPP_ENV=" + env.get("PIOENV"),
"-DAPP_REPO=" + repo_owner, "-DAPP_REPO=" + repo_owner,
"-DBUILD_EPOCH=" + str(build_epoch),
] + pref_flags ] + pref_flags
print ("Using flags:") print ("Using flags:")

View File

@@ -1,54 +0,0 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"],
["0x239A", "0x0071"]
],
"usb_product": "HT-n5262",
"mcu": "nrf52840",
"variant": "heltec_mesh_solar",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "Heltec nrf (Adafruit BSP)",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "https://heltec.org/project/meshsolar/",
"vendor": "Heltec"
}

View File

@@ -2,7 +2,7 @@
"build": { "build": {
"arduino": { "arduino": {
"ldscript": "esp32s3_out.ld", "ldscript": "esp32s3_out.ld",
"partitions": "partition-table-8MB.csv", "partitions": "default_8MB.csv",
"memory_type": "qio_opi" "memory_type": "qio_opi"
}, },
"core": "esp32", "core": "esp32",

View File

@@ -3,7 +3,7 @@
"arduino": { "arduino": {
"ldscript": "esp32s3_out.ld", "ldscript": "esp32s3_out.ld",
"memory_type": "qio_opi", "memory_type": "qio_opi",
"partitions": "partition-table-8MB.csv" "partitions": "default_8MB.csv"
}, },
"core": "esp32", "core": "esp32",
"extra_flags": [ "extra_flags": [

View File

@@ -5,7 +5,7 @@
}, },
"core": "stm32", "core": "stm32",
"cpu": "cortex-m4", "cpu": "cortex-m4",
"extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX",
"f_cpu": "48000000L", "f_cpu": "48000000L",
"mcu": "stm32wle5ccu", "mcu": "stm32wle5ccu",
"variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U",

11
debian/changelog vendored
View File

@@ -1,4 +1,4 @@
meshtasticd (2.7.9.0) UNRELEASED; urgency=medium meshtasticd (2.7.6.0) UNRELEASED; urgency=medium
[ Austin Lane ] [ Austin Lane ]
* Initial packaging * Initial packaging
@@ -39,12 +39,5 @@ meshtasticd (2.7.9.0) UNRELEASED; urgency=medium
[ ] [ ]
* GitHub Actions Automatic version bump * GitHub Actions Automatic version bump
* GitHub Actions Automatic version bump
[ ] -- <github-actions[bot]@users.noreply.github.com> Tue, 12 Aug 2025 23:48:48 +0000
* GitHub Actions Automatic version bump
[ ]
* GitHub Actions Automatic version bump
-- <github-actions[bot]@users.noreply.github.com> Wed, 03 Sep 2025 23:39:17 +0000

View File

@@ -1,7 +0,0 @@
# This is a layout for 8MB of flash for MUI devices
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x5C0000,
flashApp, app, ota_1, 0x5D0000,0x0A0000,
spiffs, data, spiffs, 0x670000,0x180000
1 # This is a layout for 8MB of flash for MUI devices
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x5000,
4 otadata, data, ota, 0xe000, 0x2000,
5 app0, app, ota_0, 0x10000, 0x5C0000,
6 flashApp, app, ota_1, 0x5D0000,0x0A0000,
7 spiffs, data, spiffs, 0x670000,0x180000

View File

@@ -53,14 +53,14 @@ build_flags = -Wno-missing-field-initializers
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
#-DBUILD_EPOCH=$UNIX_TIME ; set in platformio-custom.py now #-DBUILD_EPOCH=$UNIX_TIME
#-D OLED_PL=1 #-D OLED_PL=1
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = direct monitor_filters = direct
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master # 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/0cbc26b1f8f61957af0475f486b362eafe7cc4e2.zip https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip
# renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master
https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip 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 # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master
@@ -118,7 +118,7 @@ lib_deps =
[device-ui_base] [device-ui_base]
lib_deps = lib_deps =
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
https://github.com/meshtastic/device-ui/archive/3677476c8a823ee85056b5fb1d146a3e193f8276.zip https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip
; Common libs for environmental measurements in telemetry module ; Common libs for environmental measurements in telemetry module
[environmental_base] [environmental_base]
@@ -157,8 +157,8 @@ lib_deps =
emotibit/EmotiBit MLX90632@1.0.8 emotibit/EmotiBit MLX90632@1.0.8
# renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library # renovate: datasource=custom.pio depName=Adafruit MLX90614 packageName=adafruit/library/Adafruit MLX90614 Library
adafruit/Adafruit MLX90614 Library@2.1.5 adafruit/Adafruit MLX90614 Library@2.1.5
# renovate: datasource=github-tags depName=INA3221 packageName=sgtwilko/INA3221 # renovate: datasource=github-tags depName=INA3221 packageName=KodinLanewave/INA3221
https://github.com/sgtwilko/INA3221#bb03d7e9bfcc74fc798838a54f4f99738f29fc6a https://github.com/KodinLanewave/INA3221/archive/1.0.1.zip
# renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass
mprograms/QMC5883LCompass@1.2.3 mprograms/QMC5883LCompass@1.2.3
# renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU
@@ -177,8 +177,6 @@ lib_deps =
adafruit/Adafruit PCT2075@1.0.5 adafruit/Adafruit PCT2075@1.0.5
# renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150
dfrobot/DFRobot_BMM150@1.0.0 dfrobot/DFRobot_BMM150@1.0.0
# renovate: datasource=custom.pio depName=Adafruit_TSL2561 packageName=adafruit/library/Adafruit TSL2561
adafruit/Adafruit TSL2561@1.1.2
; (not included in native / portduino) ; (not included in native / portduino)
[environmental_extra] [environmental_extra]

View File

@@ -2,12 +2,6 @@
#include "configuration.h" #include "configuration.h"
// Forward declarations
#if defined(DEBUG_HEAP)
class MemGet;
extern MemGet memGet;
#endif
// DEBUG LED // DEBUG LED
#ifndef LED_STATE_ON #ifndef LED_STATE_ON
#define LED_STATE_ON 1 #define LED_STATE_ON 1
@@ -29,7 +23,6 @@ extern MemGet memGet;
#define MESHTASTIC_LOG_LEVEL_ERROR "ERROR" #define MESHTASTIC_LOG_LEVEL_ERROR "ERROR"
#define MESHTASTIC_LOG_LEVEL_CRIT "CRIT " #define MESHTASTIC_LOG_LEVEL_CRIT "CRIT "
#define MESHTASTIC_LOG_LEVEL_TRACE "TRACE" #define MESHTASTIC_LOG_LEVEL_TRACE "TRACE"
#define MESHTASTIC_LOG_LEVEL_HEAP "HEAP"
#include "SerialConsole.h" #include "SerialConsole.h"
@@ -69,25 +62,6 @@ extern MemGet memGet;
#endif #endif
#endif #endif
#if defined(DEBUG_HEAP)
#define LOG_HEAP(...) DEBUG_PORT.log(MESHTASTIC_LOG_LEVEL_HEAP, __VA_ARGS__)
// Macro-based heap debugging
#define DEBUG_HEAP_BEFORE auto heapBefore = memGet.getFreeHeap();
#define DEBUG_HEAP_AFTER(context, ptr) \
do { \
auto heapAfter = memGet.getFreeHeap(); \
if (heapBefore != heapAfter) { \
LOG_HEAP("Alloc in %s pointer 0x%x, size: %u, free: %u", context, ptr, heapBefore - heapAfter, heapAfter); \
} \
} while (0)
#else
#define LOG_HEAP(...)
#define DEBUG_HEAP_BEFORE
#define DEBUG_HEAP_AFTER(context, ptr)
#endif
/// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic /// A C wrapper for LOG_DEBUG that can be used from arduino C libs that don't know about C++ or meshtastic
extern "C" void logLegacy(const char *level, const char *fmt, ...); extern "C" void logLegacy(const char *level, const char *fmt, ...);

View File

@@ -1,14 +1,7 @@
#include "DisplayFormatters.h" #include "DisplayFormatters.h"
const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName)
bool usePreset)
{ {
// If use_preset is false, always return "Custom"
if (!usePreset) {
return "Custom";
}
switch (preset) { switch (preset) {
case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO:
return useShortName ? "ShortT" : "ShortTurbo"; return useShortName ? "ShortT" : "ShortTurbo";

View File

@@ -4,6 +4,5 @@
class DisplayFormatters class DisplayFormatters
{ {
public: public:
static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName);
bool usePreset);
}; };

View File

@@ -128,7 +128,6 @@ RAK9154Sensor rak9154Sensor;
#ifdef HAS_PPM #ifdef HAS_PPM
// note: XPOWERS_CHIP_XXX must be defined in variant.h // note: XPOWERS_CHIP_XXX must be defined in variant.h
#include <XPowersLib.h> #include <XPowersLib.h>
XPowersPPM *PPM = NULL;
#endif #endif
#ifdef HAS_BQ27220 #ifdef HAS_BQ27220
@@ -682,8 +681,6 @@ bool Power::setup()
found = true; found = true;
} else if (lipoChargerInit()) { } else if (lipoChargerInit()) {
found = true; found = true;
} else if (meshSolarInit()) {
found = true;
} else if (analogInit()) { } else if (analogInit()) {
found = true; found = true;
} }
@@ -746,11 +743,7 @@ void Power::shutdown()
#if HAS_SCREEN #if HAS_SCREEN
if (screen) { if (screen) {
#ifdef T_DECK_PRO
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button
#else
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
#endif
} }
#endif #endif
#if !defined(ARCH_STM32WL) #if !defined(ARCH_STM32WL)
@@ -768,7 +761,7 @@ void Power::shutdown()
#ifdef PIN_LED3 #ifdef PIN_LED3
ledOff(PIN_LED3); ledOff(PIN_LED3);
#endif #endif
doDeepSleep(DELAY_FOREVER, true, true); doDeepSleep(DELAY_FOREVER, false, true);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
#else #else
@@ -833,26 +826,17 @@ void Power::readPowerStatus()
newStatus.notifyObservers(&powerStatus2); newStatus.notifyObservers(&powerStatus2);
#ifdef DEBUG_HEAP #ifdef DEBUG_HEAP
if (lastheap != memGet.getFreeHeap()) { if (lastheap != memGet.getFreeHeap()) {
// Use stack-allocated buffer to avoid heap allocations in monitoring code std::string threadlist = "Threads running:";
char threadlist[256] = "Threads running:";
int threadlistLen = strlen(threadlist);
int running = 0; int running = 0;
for (int i = 0; i < MAX_THREADS; i++) { for (int i = 0; i < MAX_THREADS; i++) {
auto thread = concurrency::mainController.get(i); auto thread = concurrency::mainController.get(i);
if ((thread != nullptr) && (thread->enabled)) { if ((thread != nullptr) && (thread->enabled)) {
// Use snprintf to safely append to stack buffer without heap allocation threadlist += vformat(" %s", thread->ThreadName.c_str());
int remaining = sizeof(threadlist) - threadlistLen - 1;
if (remaining > 0) {
int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str());
if (written > 0 && written < remaining) {
threadlistLen += written;
}
}
running++; running++;
} }
} }
LOG_HEAP(threadlist); LOG_DEBUG(threadlist.c_str());
LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(), LOG_DEBUG("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false)); memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
lastheap = memGet.getFreeHeap(); lastheap = memGet.getFreeHeap();
} }
@@ -865,19 +849,15 @@ void Power::readPowerStatus()
sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]); sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]);
auto newHeap = memGet.getFreeHeap(); auto newHeap = memGet.getFreeHeap();
// Use stack-allocated buffers to avoid heap allocations in monitoring code std::string heapTopic =
char heapTopic[128]; (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/heap/") + std::string(mac);
snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); std::string heapString = std::to_string(newHeap);
char heapString[16]; mqtt->pubSub.publish(heapTopic.c_str(), heapString.c_str(), false);
snprintf(heapString, sizeof(heapString), "%u", newHeap);
mqtt->pubSub.publish(heapTopic, heapString, false);
auto wifiRSSI = WiFi.RSSI(); auto wifiRSSI = WiFi.RSSI();
char wifiTopic[128]; std::string wifiTopic =
snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac); (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh") + std::string("/2/wifi/") + std::string(mac);
char wifiString[16]; std::string wifiString = std::to_string(wifiRSSI);
snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI); mqtt->pubSub.publish(wifiTopic.c_str(), wifiString.c_str(), false);
mqtt->pubSub.publish(wifiTopic, wifiString, false);
} }
#endif #endif
@@ -1338,6 +1318,7 @@ bool Power::lipoInit()
class LipoCharger : public HasBatteryLevel class LipoCharger : public HasBatteryLevel
{ {
private: private:
XPowersPPM *ppm = nullptr;
BQ27220 *bq = nullptr; BQ27220 *bq = nullptr;
public: public:
@@ -1346,41 +1327,41 @@ class LipoCharger : public HasBatteryLevel
*/ */
bool runOnce() bool runOnce()
{ {
if (PPM == nullptr) { if (ppm == nullptr) {
PPM = new XPowersPPM; ppm = new XPowersPPM;
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
if (result) { if (result) {
LOG_INFO("PPM BQ25896 init succeeded"); LOG_INFO("PPM BQ25896 init succeeded");
// Set the minimum operating voltage. Below this voltage, the PPM will protect // Set the minimum operating voltage. Below this voltage, the PPM will protect
// PPM->setSysPowerDownVoltage(3100); // ppm->setSysPowerDownVoltage(3100);
// Set input current limit, default is 500mA // Set input current limit, default is 500mA
// PPM->setInputCurrentLimit(800); // ppm->setInputCurrentLimit(800);
// Disable current limit pin // Disable current limit pin
// PPM->disableCurrentLimitPin(); // ppm->disableCurrentLimitPin();
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
PPM->setChargeTargetVoltage(4288); ppm->setChargeTargetVoltage(4288);
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
// PPM->setPrechargeCurr(64); // ppm->setPrechargeCurr(64);
// The premise is that limit pin is disabled, or it will // The premise is that limit pin is disabled, or it will
// only follow the maximum charging current set by limit pin. // only follow the maximum charging current set by limit pin.
// Set the charging current , Range:0~5056mA ,step:64mA // Set the charging current , Range:0~5056mA ,step:64mA
PPM->setChargerConstantCurr(1024); ppm->setChargerConstantCurr(1024);
// To obtain voltage data, the ADC must be enabled first // To obtain voltage data, the ADC must be enabled first
PPM->enableMeasure(); ppm->enableMeasure();
// Turn on charging function // Turn on charging function
// If there is no battery connected, do not turn on the charging function // If there is no battery connected, do not turn on the charging function
PPM->enableCharge(); ppm->enableCharge();
} else { } else {
LOG_WARN("PPM BQ25896 init failed"); LOG_WARN("PPM BQ25896 init failed");
delete PPM; delete ppm;
PPM = nullptr; ppm = nullptr;
return false; return false;
} }
} }
@@ -1421,23 +1402,23 @@ class LipoCharger : public HasBatteryLevel
/** /**
* return true if there is a battery installed in this unit * return true if there is a battery installed in this unit
*/ */
virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; }
/** /**
* return true if there is an external power source detected * return true if there is an external power source detected
*/ */
virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; }
/** /**
* return true if the battery is currently charging * return true if the battery is currently charging
*/ */
virtual bool isCharging() override virtual bool isCharging() override
{ {
bool isCharging = PPM->isCharging(); bool isCharging = ppm->isCharging();
if (isCharging) { if (isCharging) {
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
} else { } else {
if (!PPM->isVbusIn()) { if (!ppm->isVbusIn()) {
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
} }
} }
@@ -1469,73 +1450,3 @@ bool Power::lipoChargerInit()
return false; return false;
} }
#endif #endif
#ifdef HELTEC_MESH_SOLAR
#include "meshSolarApp.h"
/**
* meshSolar class for an SMBUS battery sensor.
*/
class meshSolarBatteryLevel : public HasBatteryLevel
{
public:
/**
* Init the I2C meshSolar battery level sensor
*/
bool runOnce()
{
meshSolarStart();
return true;
}
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*/
virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }
/**
* return true if there is a battery installed in this unit
*/
virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }
/**
* return true if there is an external power source detected
*/
virtual bool isVbusIn() override { return meshSolarIsVbusIn(); }
/**
* return true if the battery is currently charging
*/
virtual bool isCharging() override { return meshSolarIsCharging(); }
};
meshSolarBatteryLevel meshSolarLevel;
/**
* Init the meshSolar battery level sensor
*/
bool Power::meshSolarInit()
{
bool result = meshSolarLevel.runOnce();
LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet");
if (!result)
return false;
batteryLevel = &meshSolarLevel;
return true;
}
#else
/**
* The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel
*/
bool Power::meshSolarInit()
{
return false;
}
#endif

View File

@@ -64,14 +64,6 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con
int32_t SerialConsole::runOnce() int32_t SerialConsole::runOnce()
{ {
#ifdef HELTEC_MESH_SOLAR
//After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module.
if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port
&& moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)
{
return 250;
}
#endif
return runOncePart(); return runOncePart();
} }

View File

@@ -28,11 +28,14 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_USER_PRESS:
case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_ALT_PRESS:
case INPUT_BROKER_SELECT: case INPUT_BROKER_SELECT:
case INPUT_BROKER_SELECT_LONG:
playBeep(); // Confirmation feedback playBeep(); // Confirmation feedback
break; break;
case INPUT_BROKER_UP: case INPUT_BROKER_UP:
case INPUT_BROKER_UP_LONG:
case INPUT_BROKER_DOWN: case INPUT_BROKER_DOWN:
case INPUT_BROKER_DOWN_LONG:
case INPUT_BROKER_LEFT: case INPUT_BROKER_LEFT:
case INPUT_BROKER_RIGHT: case INPUT_BROKER_RIGHT:
playChirp(); // Navigation feedback playChirp(); // Navigation feedback

View File

@@ -86,9 +86,9 @@ void OSThread::run()
#ifdef DEBUG_HEAP #ifdef DEBUG_HEAP
auto newHeap = memGet.getFreeHeap(); auto newHeap = memGet.getFreeHeap();
if (newHeap < heap) if (newHeap < heap)
LOG_HEAP("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap); LOG_DEBUG("------ Thread %s leaked heap %d -> %d (%d) ------", ThreadName.c_str(), heap, newHeap, newHeap - heap);
if (heap < newHeap) if (heap < newHeap)
LOG_HEAP("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap); LOG_DEBUG("++++++ Thread %s freed heap %d -> %d (%d) ++++++", ThreadName.c_str(), heap, newHeap, newHeap - heap);
#endif #endif
runned(); runned();

View File

@@ -26,10 +26,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include <Arduino.h> #include <Arduino.h>
#if __has_include("Melopero_RV3028.h") #ifdef RV3028_RTC
#include "Melopero_RV3028.h" #include "Melopero_RV3028.h"
#endif #endif
#if __has_include("pcf8563.h") #ifdef PCF8563_RTC
#include "pcf8563.h" #include "pcf8563.h"
#endif #endif
@@ -135,7 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// OLED & Input // OLED & Input
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #if defined(SEEED_WIO_TRACKER_L1)
#define SSD1306_ADDRESS 0x3D #define SSD1306_ADDRESS 0x3D
#define USE_SH1106 #define USE_SH1106
#else #else

View File

@@ -79,9 +79,7 @@ class ScanI2C
BQ27220, BQ27220,
LTR553ALS, LTR553ALS,
BHI260AP, BHI260AP,
BMM150, BMM150
TSL2561,
DRV2605
} DeviceType; } DeviceType;
// typedef uint8_t DeviceAddress; // typedef uint8_t DeviceAddress;

View File

@@ -461,17 +461,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address); SCAN_SIMPLE_CASE(TCA9555_ADDR, TCA9555, "TCA9555", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address); SCAN_SIMPLE_CASE(VEML7700_ADDR, VEML7700, "VEML7700", (uint8_t)addr.address);
case TSL25911_ADDR: SCAN_SIMPLE_CASE(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address);
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x12), 1);
if (registerValue == 0x50) {
type = TSL2591;
logFoundDevice("TSL25911", (uint8_t)addr.address);
} else {
type = TSL2561;
logFoundDevice("TSL2561", (uint8_t)addr.address);
}
break;
SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MLX90632_ADDR, MLX90632, "MLX90632", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address); SCAN_SIMPLE_CASE(NAU7802_ADDR, NAU7802, "NAU7802", (uint8_t)addr.address);
SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address);
@@ -492,16 +482,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (registerValue == 0x5a) { if (registerValue == 0x5a) {
type = MLX90614; type = MLX90614;
logFoundDevice("MLX90614", (uint8_t)addr.address); logFoundDevice("MLX90614", (uint8_t)addr.address);
} else {
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS
if (registerValue == 0xe0) {
type = DRV2605;
logFoundDevice("DRV2605", (uint8_t)addr.address);
} else { } else {
type = MPR121KB; type = MPR121KB;
logFoundDevice("MPR121KB", (uint8_t)addr.address); logFoundDevice("MPR121KB", (uint8_t)addr.address);
} }
}
break; break;
case ICM20948_ADDR: // same as BMX160_ADDR case ICM20948_ADDR: // same as BMX160_ADDR

View File

@@ -1,4 +1,5 @@
#include <cstring> // Include for strstr #include <cstring> // Include for strstr
#include <string>
#include <vector> #include <vector>
#include "configuration.h" #include "configuration.h"
@@ -842,6 +843,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
setPowerPMU(true); // Power (PMU): on setPowerPMU(true); // Power (PMU): on
writePinStandby(false); // Standby (pin): awake (not standby) writePinStandby(false); // Standby (pin): awake (not standby)
setPowerUBLOX(true); // Standby (UBLOX): awake setPowerUBLOX(true); // Standby (UBLOX): awake
#ifdef GNSS_AIROHA
lastFixStartMsec = 0;
#endif
break; break;
case GPS_SOFTSLEEP: case GPS_SOFTSLEEP:
@@ -859,7 +863,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
writePinStandby(true); // Standby (pin): asleep (not awake) writePinStandby(true); // Standby (pin): asleep (not awake)
setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed
#ifdef GNSS_AIROHA #ifdef GNSS_AIROHA
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW); digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
@@ -871,7 +877,9 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime)
writePinStandby(true); // Standby (pin): asleep writePinStandby(true); // Standby (pin): asleep
setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely
#ifdef GNSS_AIROHA #ifdef GNSS_AIROHA
if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) {
digitalWrite(PIN_GPS_EN, LOW); digitalWrite(PIN_GPS_EN, LOW);
}
#endif #endif
break; break;
} }
@@ -1054,8 +1062,6 @@ void GPS::down()
} }
// If update interval long enough (or softsleep unsupported): hardsleep instead // If update interval long enough (or softsleep unsupported): hardsleep instead
setPowerState(GPS_HARDSLEEP, sleepTime); setPowerState(GPS_HARDSLEEP, sleepTime);
// Reset the fix quality to 0, since we're off.
fixQual = 0;
} }
} }
@@ -1115,19 +1121,11 @@ int32_t GPS::runOnce()
shouldPublish = true; shouldPublish = true;
} }
uint8_t prev_fixQual = fixQual;
bool gotLoc = lookForLocation(); bool gotLoc = lookForLocation();
if (gotLoc && !hasValidLocation) { // declare that we have location ASAP if (gotLoc && !hasValidLocation) { // declare that we have location ASAP
LOG_DEBUG("hasValidLocation RISING EDGE"); LOG_DEBUG("hasValidLocation RISING EDGE");
hasValidLocation = true; hasValidLocation = true;
shouldPublish = true; shouldPublish = true;
// Hold for 20secs after getting a lock to download ephemeris etc
fixHoldEnds = millis() + 20000;
}
if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on.
fixHoldEnds = millis() + 20000;
shouldPublish = true; // Publish immediately, since next publish is at end of hold
} }
bool tooLong = scheduling.searchedTooLong(); bool tooLong = scheduling.searchedTooLong();
@@ -1136,7 +1134,8 @@ int32_t GPS::runOnce()
// Once we get a location we no longer desperately want an update // Once we get a location we no longer desperately want an update
if ((gotLoc && gotTime) || tooLong) { if ((gotLoc && gotTime) || tooLong) {
if (tooLong && !gotLoc) {
if (tooLong) {
// we didn't get a location during this ack window, therefore declare loss of lock // we didn't get a location during this ack window, therefore declare loss of lock
if (hasValidLocation) { if (hasValidLocation) {
LOG_DEBUG("hasValidLocation FALLING EDGE"); LOG_DEBUG("hasValidLocation FALLING EDGE");
@@ -1144,15 +1143,9 @@ int32_t GPS::runOnce()
p = meshtastic_Position_init_default; p = meshtastic_Position_init_default;
hasValidLocation = false; hasValidLocation = false;
} }
if (millis() > fixHoldEnds) {
shouldPublish = true; // publish our update at the end of the lock hold
publishUpdate();
down(); down();
#ifdef GPS_DEBUG shouldPublish = true; // publish our update for this just finished acquisition window
} else {
LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view);
#endif
}
} }
// If state has changed do a publish // If state has changed do a publish
@@ -1205,7 +1198,7 @@ static const char *DETECTED_MESSAGE = "%s detected";
LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \ LOG_DEBUG(PROBE_MESSAGE, COMMAND, FAMILY_NAME); \
clearBuffer(); \ clearBuffer(); \
_serial_gps->write(COMMAND "\r\n"); \ _serial_gps->write(COMMAND "\r\n"); \
GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP, serialSpeed); \ GnssModel_t detectedDriver = getProbeResponse(TIMEOUT, RESPONSE_MAP); \
if (detectedDriver != GNSS_MODEL_UNKNOWN) { \ if (detectedDriver != GNSS_MODEL_UNKNOWN) { \
return detectedDriver; \ return detectedDriver; \
} \ } \
@@ -1367,55 +1360,36 @@ GnssModel_t GPS::probe(int serialSpeed)
return GNSS_MODEL_UNKNOWN; return GNSS_MODEL_UNKNOWN;
} }
GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed) GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap)
{ {
// Calculate buffer size based on baud rate - 256 bytes for 9600 baud as baseline String response = "";
// Higher baud rates get proportionally larger buffers to handle more data
int bufferSize = (serialSpeed * 256) / 9600;
// Clamp buffer size between reasonable limits
if (bufferSize < 128)
bufferSize = 128;
if (bufferSize > 2048)
bufferSize = 2048;
char *response = new char[bufferSize](); // Dynamically allocate based on baud rate
uint16_t responseLen = 0;
unsigned long start = millis(); unsigned long start = millis();
while (millis() - start < timeout) { while (millis() - start < timeout) {
if (_serial_gps->available()) { if (_serial_gps->available()) {
char c = _serial_gps->read(); response += (char)_serial_gps->read();
// Add char to buffer if there's space if (response.endsWith(",") || response.endsWith("\r\n")) {
if (responseLen < bufferSize - 1) {
response[responseLen++] = c;
response[responseLen] = '\0';
}
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
LOG_DEBUG(response); LOG_DEBUG(response.c_str());
#endif #endif
// check if we can see our chips // check if we can see our chips
for (const auto &chipInfo : responseMap) { for (const auto &chipInfo : responseMap) {
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) { if (strstr(response.c_str(), chipInfo.detectionString.c_str()) != nullptr) {
LOG_INFO("%s detected", chipInfo.chipName.c_str()); LOG_INFO("%s detected", chipInfo.chipName.c_str());
delete[] response; // Cleanup before return
return chipInfo.driver; return chipInfo.driver;
} }
} }
} }
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') { if (response.endsWith("\r\n")) {
// Reset the response buffer for the next potential message response.trim();
responseLen = 0; response = ""; // Reset the response string for the next potential message
response[0] = '\0';
} }
} }
} }
#ifdef GPS_DEBUG #ifdef GPS_DEBUG
LOG_DEBUG(response); LOG_DEBUG(response.c_str());
#endif #endif
delete[] response; // Cleanup before return return GNSS_MODEL_UNKNOWN; // Return empty string on timeout
return GNSS_MODEL_UNKNOWN; // Return unknown on timeout
} }
GPS *GPS::createGps() GPS *GPS::createGps()
@@ -1530,10 +1504,28 @@ static int32_t toDegInt(RawDegrees d)
* Perform any processing that should be done only while the GPS is awake and looking for a fix. * Perform any processing that should be done only while the GPS is awake and looking for a fix.
* Override this method to check for new locations * Override this method to check for new locations
* *
* @return true if we've set a new time * @return true if we've acquired a new location
*/ */
bool GPS::lookForTime() bool GPS::lookForTime()
{ {
#ifdef GNSS_AIROHA
uint8_t fix = reader.fixQuality();
if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
} else {
clearBuffer();
}
} else {
lastFixStartMsec = millis();
return false;
}
} else {
return false;
}
#endif
auto ti = reader.time; auto ti = reader.time;
auto d = reader.date; auto d = reader.date;
if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed
@@ -1550,13 +1542,13 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
t.tm_year = d.year() - 1900; t.tm_year = d.year() - 1900;
t.tm_isdst = false; t.tm_isdst = false;
if (t.tm_mon > -1) { if (t.tm_mon > -1) {
if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min,
LOG_DEBUG("NMEA GPS time set %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_sec, ti.age());
t.tm_min, t.tm_sec, ti.age()); if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) {
return true; // Clear the GPS buffer if we got an invalid time
} else { clearBuffer();
return false;
} }
return true;
} else } else
return false; return false;
} else } else
@@ -1571,6 +1563,25 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s
*/ */
bool GPS::lookForLocation() bool GPS::lookForLocation()
{ {
#ifdef GNSS_AIROHA
if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) {
uint8_t fix = reader.fixQuality();
if (fix >= 1 && fix <= 5) {
if (lastFixStartMsec > 0) {
if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) {
return false;
} else {
clearBuffer();
}
} else {
lastFixStartMsec = millis();
return false;
}
} else {
return false;
}
}
#endif
// By default, TinyGPS++ does not parse GPGSA lines, which give us // By default, TinyGPS++ does not parse GPGSA lines, which give us
// the 2D/3D fixType (see NMEAGPS.h) // the 2D/3D fixType (see NMEAGPS.h)
// At a minimum, use the fixQuality indicator in GPGGA (FIXME?) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?)

View File

@@ -159,7 +159,7 @@ class GPS : private concurrency::OSThread
uint8_t fixType = 0; // fix type from GPGSA uint8_t fixType = 0; // fix type from GPGSA
#endif #endif
uint32_t fixHoldEnds = 0; uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0;
uint32_t rx_gpio = 0; uint32_t rx_gpio = 0;
uint32_t tx_gpio = 0; uint32_t tx_gpio = 0;
@@ -236,7 +236,7 @@ class GPS : private concurrency::OSThread
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap, int serialSpeed); GnssModel_t getProbeResponse(unsigned long timeout, const std::vector<ChipInfo> &responseMap);
// Get GNSS model // Get GNSS model
GnssModel_t probe(int serialSpeed); GnssModel_t probe(int serialSpeed);

View File

@@ -9,9 +9,6 @@
static RTCQuality currentQuality = RTCQualityNone; static RTCQuality currentQuality = RTCQualityNone;
uint32_t lastSetFromPhoneNtpOrGps = 0; uint32_t lastSetFromPhoneNtpOrGps = 0;
static uint32_t lastTimeValidationWarning = 0;
static const uint32_t TIME_VALIDATION_WARNING_INTERVAL_MS = 15000; // 15 seconds
RTCQuality getRTCQuality() RTCQuality getRTCQuality()
{ {
return currentQuality; return currentQuality;
@@ -26,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda
* Reads the current date and time from the RTC module and updates the system time. * Reads the current date and time from the RTC module and updates the system time.
* @return True if the RTC was successfully read and the system time was updated, false otherwise. * @return True if the RTC was successfully read and the system time was updated, false otherwise.
*/ */
RTCSetResult readFromRTC() void readFromRTC()
{ {
struct timeval tv; /* btw settimeofday() is helpful here too*/ struct timeval tv; /* btw settimeofday() is helpful here too*/
#ifdef RV3028_RTC #ifdef RV3028_RTC
@@ -47,25 +44,15 @@ RTCSetResult readFromRTC()
t.tm_sec = rtc.getSecond(); t.tm_sec = rtc.getSecond();
tv.tv_sec = gm_mktime(&t); tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0; tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
}
return RTCSetResultInvalidTime;
}
#endif
LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) {
currentQuality = RTCQualityDevice; currentQuality = RTCQualityDevice;
} }
return RTCSetResultSuccess;
} }
#elif defined(PCF8563_RTC) #elif defined(PCF8563_RTC)
if (rtc_found.address == PCF8563_RTC) { if (rtc_found.address == PCF8563_RTC) {
@@ -88,26 +75,15 @@ RTCSetResult readFromRTC()
t.tm_sec = tc.second; t.tm_sec = tc.second;
tv.tv_sec = gm_mktime(&t); tv.tv_sec = gm_mktime(&t);
tv.tv_usec = 0; tv.tv_usec = 0;
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
}
#endif
LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch);
if (currentQuality == RTCQualityNone) {
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
if (currentQuality == RTCQualityNone) {
currentQuality = RTCQualityDevice; currentQuality = RTCQualityDevice;
} }
return RTCSetResultSuccess;
} }
#else #else
if (!gettimeofday(&tv, NULL)) { if (!gettimeofday(&tv, NULL)) {
@@ -116,10 +92,8 @@ RTCSetResult readFromRTC()
LOG_DEBUG("Read RTC time as %ld", printableEpoch); LOG_DEBUG("Read RTC time as %ld", printableEpoch);
timeStartMsec = now; timeStartMsec = now;
zeroOffsetSecs = tv.tv_sec; zeroOffsetSecs = tv.tv_sec;
return RTCSetResultSuccess;
} }
#endif #endif
return RTCSetResultNotSet;
} }
/** /**
@@ -127,7 +101,7 @@ RTCSetResult readFromRTC()
* *
* @param q The quality of the provided time. * @param q The quality of the provided time.
* @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to.
* @return RTCSetResult * @return True if the RTC was set, false otherwise.
* *
* If we haven't yet set our RTC this boot, set it from a GPS derived time * If we haven't yet set our RTC this boot, set it from a GPS derived time
*/ */
@@ -138,20 +112,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd
uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv->tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH #ifdef BUILD_EPOCH
if (tv->tv_sec < BUILD_EPOCH) { if (tv->tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
} else if (tv->tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
// Calculate max allowed time safely to avoid overflow in logging
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime; return RTCSetResultInvalidTime;
} }
#endif #endif
@@ -269,20 +230,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms
#ifdef BUILD_EPOCH #ifdef BUILD_EPOCH
if (tv.tv_sec < BUILD_EPOCH) { if (tv.tv_sec < BUILD_EPOCH) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime;
} else if (tv.tv_sec > (time_t)(BUILD_EPOCH + FORTY_YEARS)) {
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
// Calculate max allowed time safely to avoid overflow in logging
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
lastTimeValidationWarning = millis();
}
return RTCSetResultInvalidTime; return RTCSetResultInvalidTime;
} }
#endif #endif

View File

@@ -48,13 +48,10 @@ uint32_t getTime(bool local = false);
/// Return time since 1970 in secs. If quality is RTCQualityNone return zero /// Return time since 1970 in secs. If quality is RTCQualityNone return zero
uint32_t getValidTime(RTCQuality minQuality, bool local = false); uint32_t getValidTime(RTCQuality minQuality, bool local = false);
RTCSetResult readFromRTC(); void readFromRTC();
time_t gm_mktime(struct tm *tm); time_t gm_mktime(struct tm *tm);
#define SEC_PER_DAY 86400 #define SEC_PER_DAY 86400
#define SEC_PER_HOUR 3600 #define SEC_PER_HOUR 3600
#define SEC_PER_MIN 60 #define SEC_PER_MIN 60
#ifdef BUILD_EPOCH
#define FORTY_YEARS (40ULL * 365 * SEC_PER_DAY) // Use 64-bit arithmetic to prevent overflow
#endif

View File

@@ -67,28 +67,20 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
// FIXME - only draw bits have changed (use backbuf similar to the other displays) // FIXME - only draw bits have changed (use backbuf similar to the other displays)
const bool flipped = config.display.flip_screen; const bool flipped = config.display.flip_screen;
// HACK for L1 EInk
#if defined(SEEED_WIO_TRACKER_L1_EINK)
// For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes
for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) {
auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7));
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
}
}
#else
for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) { for (uint32_t x = 0; x < displayWidth; x++) {
// get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient
auto b = buffer[x + (y / 8) * displayWidth]; auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7)); auto isset = b & (1 << (y & 7));
// Handle flip here, rather than with setRotation(),
// Avoids issues when display width is not a multiple of 8
if (flipped) if (flipped)
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
else else
adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE);
} }
} }
#endif
// Trigger the refresh in GxEPD2 // Trigger the refresh in GxEPD2
LOG_DEBUG("Update E-Paper"); LOG_DEBUG("Update E-Paper");
@@ -243,7 +235,7 @@ bool EInkDisplay::connect()
adafruitDisplay->setRotation(1); adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) #elif defined(HELTEC_MESH_POCKET)
{ {
spi1 = &SPI1; spi1 = &SPI1;
spi1->begin(); spi1->begin();
@@ -257,7 +249,6 @@ bool EInkDisplay::connect()
// Init GxEPD2 // Init GxEPD2
adafruitDisplay->init(); adafruitDisplay->init();
adafruitDisplay->setRotation(3); adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
} }
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)

View File

@@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay
SPIClass *hspi = NULL; SPIClass *hspi = NULL;
#endif #endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) #if defined(HELTEC_MESH_POCKET)
SPIClass *spi1 = NULL; SPIClass *spi1 = NULL;
#endif #endif

View File

@@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
ui->update(); ui->update();
} }
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback)
{
LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs);
if (NotificationRenderer::virtualKeyboard) {
delete NotificationRenderer::virtualKeyboard;
NotificationRenderer::virtualKeyboard = nullptr;
}
NotificationRenderer::textInputCallback = nullptr;
NotificationRenderer::virtualKeyboard = new VirtualKeyboard();
if (header) {
NotificationRenderer::virtualKeyboard->setHeader(header);
}
if (initialText) {
NotificationRenderer::virtualKeyboard->setInputText(initialText);
}
// Set up callback with safer cleanup mechanism
NotificationRenderer::textInputCallback = textCallback;
NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); });
// Store the message and set the expiration timestamp (use same pattern as other notifications)
strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255);
NotificationRenderer::alertBannerMessage[255] = '\0';
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
NotificationRenderer::pauseBanner = false;
NotificationRenderer::current_notification_type = notificationTypeEnum::text_input;
// Set the overlay using the same pattern as other notification types
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
ui->setTargetFPS(60);
ui->update();
}
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
uint8_t module_frame; uint8_t module_frame;
@@ -318,7 +356,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
dispdev = new SSD1306Wire(address.address, -1, -1, geometry, dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
dispdev = new TFTDisplay(address.address, -1, -1, geometry, dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
@@ -550,7 +588,7 @@ void Screen::setup()
#else #else
if (!config.display.flip_screen) { if (!config.display.flip_screen) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically(); static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
#elif defined(USE_ST7789) #elif defined(USE_ST7789)
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically(); static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
@@ -713,13 +751,19 @@ int32_t Screen::runOnce()
handleSetOn(false); handleSetOn(false);
break; break;
case Cmd::ON_PRESS: case Cmd::ON_PRESS:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleOnPress(); handleOnPress();
}
break; break;
case Cmd::SHOW_PREV_FRAME: case Cmd::SHOW_PREV_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowPrevFrame(); handleShowPrevFrame();
}
break; break;
case Cmd::SHOW_NEXT_FRAME: case Cmd::SHOW_NEXT_FRAME:
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
handleShowNextFrame(); handleShowNextFrame();
}
break; break;
case Cmd::START_ALERT_FRAME: { case Cmd::START_ALERT_FRAME: {
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
@@ -741,7 +785,9 @@ int32_t Screen::runOnce()
NotificationRenderer::pauseBanner = false; NotificationRenderer::pauseBanner = false;
case Cmd::STOP_BOOT_SCREEN: case Cmd::STOP_BOOT_SCREEN:
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) {
setFrames(); setFrames();
}
break; break;
case Cmd::NOOP: case Cmd::NOOP:
break; break;
@@ -777,6 +823,7 @@ int32_t Screen::runOnce()
if (showingNormalScreen) { if (showingNormalScreen) {
// standard screen loop handling here // standard screen loop handling here
if (config.display.auto_screen_carousel_secs > 0 && if (config.display.auto_screen_carousel_secs > 0 &&
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
@@ -867,6 +914,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
// Called when a frame should be added / removed, or custom frames should be cleared // Called when a frame should be added / removed, or custom frames should be cleared
void Screen::setFrames(FrameFocus focus) void Screen::setFrames(FrameFocus focus)
{ {
// Block setFrames calls when virtual keyboard is active to prevent overlay interference
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return;
}
uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t originalPosition = ui->getUiState()->currentFrame;
uint8_t previousFrameCount = framesetInfo.frameCount; uint8_t previousFrameCount = framesetInfo.frameCount;
FramesetInfo fsi; // Location of specific frames, for applying focus parameter FramesetInfo fsi; // Location of specific frames, for applying focus parameter
@@ -1313,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Triggered by MeshModules // Triggered by MeshModules
int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleUIFrameEvent(const UIFrameEvent *event)
{ {
// Block UI frame events when virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return 0;
}
if (showingNormalScreen) { if (showingNormalScreen) {
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call // Regenerate the frameset, potentially honoring a module's internal requestFocus() call
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
@@ -1335,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event)
if (!screenOn) if (!screenOn)
return 0; return 0;
// Handle text input notifications specially - pass input to virtual keyboard
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
NotificationRenderer::inEvent = *event;
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
setFastFramerate(); // Draw ASAP
ui->update();
return 0;
}
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update

View File

@@ -12,7 +12,7 @@
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
namespace graphics namespace graphics
{ {
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
struct BannerOverlayOptions { struct BannerOverlayOptions {
const char *message; const char *message;
@@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback); void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
std::function<void(const std::string &)> textCallback);
void requestMenu(graphics::menuHandler::screenMenus menuToShow) void requestMenu(graphics::menuHandler::screenMenus menuToShow)
{ {

View File

@@ -73,7 +73,7 @@
#endif #endif
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
// The screen is bigger so use bigger fonts // The screen is bigger so use bigger fonts
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19

View File

@@ -562,91 +562,6 @@ class LGFX : public lgfx::LGFX_Device
static LGFX *tft = nullptr; static LGFX *tft = nullptr;
#elif defined(ST7796_CS)
#include <LovyanGFX.hpp> // Graphics and font library for ST7796 driver chip
class LGFX : public lgfx::LGFX_Device
{
lgfx::Panel_ST7796 _panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::Light_PWM _light_instance;
public:
LGFX(void)
{
{
auto cfg = _bus_instance.config();
// SPI
cfg.spi_host = ST7796_SPI_HOST;
cfg.spi_mode = 0;
cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing
// 80MHz by an integer)
cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving
cfg.spi_3wire = false;
cfg.use_lock = true; // Set to true to use transaction locking
cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch /
// SPI_DMA_CH_AUTO=auto setting)
cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number
cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number
cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable)
cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable)
_bus_instance.config(cfg); // applies the set value to the bus.
_panel_instance.setBus(&_bus_instance); // set the bus on the panel.
}
{ // Set the display panel control.
auto cfg = _panel_instance.config(); // Gets a structure for display panel settings.
cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable)
cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable)
cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable)
// cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
// cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC
cfg.panel_width = TFT_WIDTH; // actual displayable width
cfg.panel_height = TFT_HEIGHT; // actual displayable height
cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction
cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction
cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored)
#ifdef TFT_DUMMY_READ_PIXELS
cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout
#else
cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout
#endif
cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read
cfg.readable = true; // Set to true if data can be read
cfg.invert = true; // Set to true if the light/darkness of the panel is reversed
cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped
cfg.dlen_16bit =
false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI
cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.)
_panel_instance.config(cfg);
}
#ifdef ST7796_BL
// Set the backlight control. (delete if not necessary)
{
auto cfg = _light_instance.config(); // Gets a structure for backlight settings.
cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected
cfg.invert = false; // true to invert the brightness of the backlight
cfg.freq = 44100;
cfg.pwm_channel = 7;
_light_instance.config(cfg);
_panel_instance.setLight(&_light_instance); // Set the backlight on the panel.
}
#endif
setPanel(&_panel_instance); // Sets the panel to use.
}
};
static LGFX *tft = nullptr;
#elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER)
#include <LovyanGFX.hpp> // Graphics and font library for ILI9341/ILI9342 driver chip #include <LovyanGFX.hpp> // Graphics and font library for ILI9341/ILI9342 driver chip
@@ -1082,9 +997,8 @@ static LGFX *tft = nullptr;
#endif #endif
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0)
(ARCH_PORTDUINO && HAS_SCREEN != 0)
#include "SPILock.h" #include "SPILock.h"
#include "TFTDisplay.h" #include "TFTDisplay.h"
#include <SPI.h> #include <SPI.h>
@@ -1128,111 +1042,37 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g
#endif #endif
} }
TFTDisplay::~TFTDisplay()
{
// Clean up allocated line pixel buffer to prevent memory leak
if (linePixelBuffer != nullptr) {
free(linePixelBuffer);
linePixelBuffer = nullptr;
}
}
// Write the buffer to the display memory // Write the buffer to the display memory
void TFTDisplay::display(bool fromBlank) void TFTDisplay::display(bool fromBlank)
{ {
if (fromBlank) if (fromBlank)
tft->fillScreen(TFT_BLACK); tft->fillScreen(TFT_BLACK);
// tft->clear();
concurrency::LockGuard g(spiLock); concurrency::LockGuard g(spiLock);
uint32_t x, y; uint16_t x, y;
uint32_t y_byteIndex;
uint8_t y_byteMask;
uint32_t x_FirstPixelUpdate;
uint32_t x_LastPixelUpdate;
bool isset, dblbuf_isset;
uint16_t colorTftMesh, colorTftBlack;
bool somethingChanged = false;
// Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step for (y = 0; y < displayHeight; y++) {
colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8);
colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8);
y = 0;
while (y < displayHeight) {
y_byteIndex = (y / 8) * displayWidth;
y_byteMask = (1 << (y & 7));
// Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas.
if (y_byteMask == 1) {
if (!fromBlank) {
for (x = 0; x < displayWidth; x++) { for (x = 0; x < displayWidth; x++) {
if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7));
break;
}
} else {
for (x = 0; x < displayWidth; x++) {
if (buffer[x + y_byteIndex] != 0)
break;
}
}
if (x >= displayWidth) {
// No changed pixels found in these 8 rows, fast-forward to the next 8
y = y + 8;
continue;
}
}
// 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;
if (!fromBlank) { if (!fromBlank) {
// get src pixel in the page based ordering the OLED lib uses // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent
dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7));
if (isset != dblbuf_isset) { if (isset != dblbuf_isset) {
break; tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK);
} }
} else if (isset) { } else if (isset) {
break; tft->drawPixel(x, y, TFT_MESH);
} }
} }
// Did we find a pixel that needs updating on this row?
if (x_FirstPixelUpdate < displayWidth) {
// Quickly write out the first changed pixel (saves another array lookup)
linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack;
x_LastPixelUpdate = x_FirstPixelUpdate;
// Step 3: copy all remaining pixels in this row into the pixel line buffer,
// while also recording the last pixel in the row that needs updating
for (x = x_FirstPixelUpdate + 1; 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 (isset != dblbuf_isset) {
x_LastPixelUpdate = x;
}
} else if (isset) {
x_LastPixelUpdate = x;
}
}
// 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]);
somethingChanged = true;
}
y++;
} }
// Copy the Buffer to the Back Buffer // Copy the Buffer to the Back Buffer
if (somethingChanged) for (y = 0; y < (displayHeight / 8); y++) {
memcpy(buffer_back, buffer, displayBufferSize); for (x = 0; x < displayWidth; x++) {
uint16_t pos = x + y * displayWidth;
buffer_back[pos] = buffer[pos];
}
}
} }
void TFTDisplay::sdlLoop() void TFTDisplay::sdlLoop()
@@ -1424,21 +1264,13 @@ bool TFTDisplay::connect()
tft->setRotation(1); // T-Deck has the TFT in landscape tft->setRotation(1); // T-Deck has the TFT in landscape
#elif defined(T_WATCH_S3) #elif defined(T_WATCH_S3)
tft->setRotation(2); // T-Watch S3 left-handed orientation tft->setRotation(2); // T-Watch S3 left-handed orientation
#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) #elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR)
tft->setRotation(0); // use config.yaml to set rotation tft->setRotation(0); // use config.yaml to set rotation
#else #else
tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label
#endif #endif
tft->fillScreen(TFT_BLACK); tft->fillScreen(TFT_BLACK);
if (this->linePixelBuffer == NULL) {
this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth);
if (!this->linePixelBuffer) {
LOG_ERROR("Not enough memory to create TFT line buffer\n");
return false;
}
}
return true; return true;
} }

View File

@@ -20,9 +20,6 @@ class TFTDisplay : public OLEDDisplay
*/ */
TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C); TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
// Destructor to clean up allocated memory
~TFTDisplay();
// Write the buffer to the display memory // Write the buffer to the display memory
virtual void display() override { display(false); }; virtual void display() override { display(false); };
virtual void display(bool fromBlank); virtual void display(bool fromBlank);
@@ -61,6 +58,4 @@ class TFTDisplay : public OLEDDisplay
// Connect to the display // Connect to the display
virtual bool connect() override; virtual bool connect() override;
uint16_t *linePixelBuffer = nullptr;
}; };

View File

@@ -0,0 +1,738 @@
#include "VirtualKeyboard.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "main.h"
#include <Arduino.h>
#include <vector>
namespace graphics
{
VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis())
{
initializeKeyboard();
// Set cursor to H(2, 5)
cursorRow = 2;
cursorCol = 5;
}
VirtualKeyboard::~VirtualKeyboard() {}
void VirtualKeyboard::initializeKeyboard()
{
// New 4 row, 11 column keyboard layout:
static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'},
{'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}};
// Derive layout dimensions and assert they match the configured keyboard grid
constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0]));
constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0]));
static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS");
static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS");
// Initialize all keys to empty first
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0};
}
}
// Fill keyboard from the 2D layout
for (int row = 0; row < LAYOUT_ROWS; row++) {
for (int col = 0; col < LAYOUT_COLS; col++) {
char ch = LAYOUT[row][col];
// No empty slots in the simplified layout
VirtualKeyType type = VK_CHAR;
if (ch == '\b') {
type = VK_BACKSPACE;
} else if (ch == '\n') {
type = VK_ENTER;
} else if (ch == '\x1b') { // ESC
type = VK_ESC;
} else if (ch == ' ') {
type = VK_SPACE;
}
// Make action keys wider to fit text while keeping the last column aligned
uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH;
keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT};
}
}
}
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
{
// Repeat ticking is driven by NotificationRenderer once per frame
// Base styles
display->setColor(WHITE);
display->setFont(FONT_SMALL);
// Screen geometry
const int screenW = display->getWidth();
const int screenH = display->getHeight();
// Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels
// Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide
const bool isWide = screenW >= 200;
// Determine last-column label max width
display->setFont(FONT_SMALL);
const int wENTER = display->getStringWidth("ENTER");
int lastColLabelW = wENTER; // ENTER is usually the widest
// Smaller padding on very small screens to avoid excessive whitespace
const int lastColPad = (screenW <= 128 ? 2 : 6);
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
// Always reserve width for the rightmost text column to avoid overlap on small screens
int cellW = 0;
int leftoverW = 0;
{
const int leftCols = KEYBOARD_COLS - 1; // 10 input characters
int usableW = screenW - reservedLastColW;
if (usableW < leftCols) {
// Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely)
usableW = leftCols;
}
cellW = usableW / leftCols;
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
}
// Dynamic key geometry
int cellH = KEY_HEIGHT;
int keyboardStartY = 0;
if (screenH <= 64) {
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2);
const int gapBelowHeader = 0;
const int singleLineBoxHeight = FONT_HEIGHT_SMALL;
const int gapAboveKeyboard = 0;
keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard;
if (keyboardStartY < 0)
keyboardStartY = 0;
if (keyboardStartY > screenH)
keyboardStartY = screenH;
int keyboardHeight = screenH - keyboardStartY;
cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS);
} else if (isWide) {
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
cellH = std::max((int)KEY_HEIGHT, cellW);
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
display->setFont(FONT_SMALL);
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
const int headerToBoxGap = 1;
const int gapAboveKb = 1;
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
if (maxCellHAllowed < (int)KEY_HEIGHT)
maxCellHAllowed = KEY_HEIGHT;
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
cellH = maxCellHAllowed;
}
// Keyboard placement from bottom for wide screens
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
} else {
// Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom
cellH = KEY_HEIGHT;
int keyboardHeight = KEYBOARD_ROWS * cellH;
keyboardStartY = screenH - keyboardHeight;
if (keyboardStartY < 0)
keyboardStartY = 0;
}
// Draw input area above keyboard
drawInputArea(display, offsetX, offsetY, keyboardStartY);
// Precompute per-column x and width with leftover distributed over left columns for even spacing
int colX[KEYBOARD_COLS];
int colW[KEYBOARD_COLS];
int runningX = offsetX;
for (int col = 0; col < KEYBOARD_COLS - 1; ++col) {
int wcol = cellW + (col < leftoverW ? 1 : 0);
colX[col] = runningX;
colW[col] = wcol;
runningX += wcol;
}
// Last column
colX[KEYBOARD_COLS - 1] = runningX;
colW[KEYBOARD_COLS - 1] = reservedLastColW;
// Draw keyboard grid
for (int row = 0; row < KEYBOARD_ROWS; row++) {
for (int col = 0; col < KEYBOARD_COLS; col++) {
const VirtualKey &k = keyboard[row][col];
if (k.character != 0 || k.type != VK_CHAR) {
const bool isLastCol = (col == KEYBOARD_COLS - 1);
int x = colX[col];
int w = colW[col];
int y = offsetY + keyboardStartY + row * cellH;
int h = cellH;
bool selected = (row == cursorRow && col == cursorCol);
drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol);
}
}
}
}
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
{
display->setColor(WHITE);
const int screenWidth = display->getWidth();
const int screenHeight = display->getHeight();
// Use the standard small font metrics for input box sizing (restore original size)
const int inputLineH = FONT_HEIGHT_SMALL;
// Header uses the standard small (which may be larger on big screens)
display->setFont(FONT_SMALL);
int headerHeight = 0;
if (!headerText.empty()) {
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
display->drawString(offsetX + 2, offsetY, headerText.c_str());
if (screenHeight <= 64) {
headerHeight = FONT_HEIGHT_SMALL - 2; // 11px
} else {
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
}
}
const int boxX = offsetX;
const int boxWidth = screenWidth;
int boxY;
int boxHeight;
if (screenHeight <= 64) {
const int gapBelowHeader = 0;
const int fixedBoxHeight = inputLineH;
const int gapAboveKeyboard = 0;
boxY = offsetY + headerHeight + gapBelowHeader;
boxHeight = fixedBoxHeight;
if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) {
int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY;
boxHeight = std::max(1, fixedBoxHeight - over);
}
} else {
const int gapBelowHeader = 1;
int gapAboveKeyboard = 1;
int tmpBoxY = offsetY + headerHeight + gapBelowHeader;
const int minBoxHeight = inputLineH + 2;
int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard;
if (availableH < minBoxHeight)
availableH = minBoxHeight;
boxY = tmpBoxY;
boxHeight = availableH;
}
// Draw box border
display->drawRect(boxX, boxY, boxWidth, boxHeight);
display->setFont(FONT_SMALL);
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
const int textX = boxX + 2;
const int maxTextWidth = boxWidth - 4;
const int maxLines = (boxHeight - 2) / inputLineH;
if (maxLines >= 2) {
// Inner bounds for caret clamping
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Wrap text greedily into lines that fit maxTextWidth
std::vector<std::string> lines;
{
std::string remaining = inputText;
while (!remaining.empty()) {
int bestLen = 0;
for (int len = 1; len <= (int)remaining.size(); ++len) {
int w = display->getStringWidth(remaining.substr(0, len).c_str());
if (w <= maxTextWidth)
bestLen = len;
else
break;
}
if (bestLen == 0) {
// At least show one character to make progress
bestLen = 1;
}
lines.emplace_back(remaining.substr(0, bestLen));
remaining.erase(0, bestLen);
}
}
const bool scrolledUp = ((int)lines.size() > maxLines);
int caretX = textX;
int caretY = innerTop;
// Leave a small top gap to render '...' without replacing the first line
const int topInset = 2;
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
int lineY = innerTop + topInset;
if (scrolledUp) {
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
const int firstLineTop = lineY; // baseline top for the first visible line
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
const int centerX = boxX + boxWidth / 2;
const int dotSpacing = 3; // px between dots
const int dotSize = 1; // small square dot
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
display->fillRect(centerX, gapMidY, dotSize, dotSize);
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
}
// How many lines fit with our top inset and tighter step
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
const int linesToShow = std::min((int)lines.size(), linesCapacity);
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
for (int i = 0; i < linesToShow; ++i) {
const std::string &chunk = lines[startIndex + i];
display->drawString(textX, lineY, chunk.c_str());
caretX = textX + display->getStringWidth(chunk.c_str());
caretY = lineY;
lineY += lineStep;
}
// Draw caret at end of the last visible line
int caretPadY = 2;
if (boxHeight >= inputLineH + 4)
caretPadY = 3;
int cursorTop = caretY + caretPadY;
// Use lineStep so caret height matches the row spacing
int cursorH = lineStep - caretPadY * 2;
if (cursorH < 1)
cursorH = 1;
// Clamp vertical bounds to stay inside the inner rect
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
// Only draw if cursor is inside inner bounds
if (caretX >= innerLeft && caretX <= innerRight) {
display->drawVerticalLine(caretX, cursorTop, cursorH);
}
} else {
std::string displayText = inputText;
int textW = display->getStringWidth(displayText.c_str());
std::string scrolled = displayText;
if (textW > maxTextWidth) {
// Trim from the left until it fits
while (textW > maxTextWidth && !scrolled.empty()) {
scrolled.erase(0, 1);
textW = display->getStringWidth(scrolled.c_str());
}
// Add leading ellipsis and ensure it still fits
if (scrolled != displayText) {
scrolled = "..." + scrolled;
textW = display->getStringWidth(scrolled.c_str());
// If adding ellipsis causes overflow, trim more after the ellipsis
while (textW > maxTextWidth && scrolled.size() > 3) {
scrolled.erase(3, 1); // remove chars after the ellipsis
textW = display->getStringWidth(scrolled.c_str());
}
}
} else {
// Keep textW in sync with what we draw
textW = display->getStringWidth(scrolled.c_str());
}
int textY;
if (screenHeight <= 64) {
textY = boxY + (boxHeight - inputLineH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
// Center text vertically within inner box for single-line, then clamp so it never overlaps borders
int innerH = innerBottom - innerTop + 1;
textY = innerTop + std::max(0, (innerH - inputLineH) / 2);
// Clamp fully inside the inner rect
if (textY < innerTop)
textY = innerTop;
int maxTop = innerBottom - inputLineH + 1;
if (textY > maxTop)
textY = maxTop;
}
if (!scrolled.empty()) {
display->drawString(textX, textY, scrolled.c_str());
}
int cursorX = textX + textW;
if (screenHeight > 64) {
const int innerRight = boxX + boxWidth - 2;
if (cursorX > innerRight)
cursorX = innerRight;
}
int cursorTop, cursorH;
if (screenHeight <= 64) {
cursorH = 10;
cursorTop = boxY + (boxHeight - cursorH) / 2;
} else {
const int innerLeft = boxX + 1;
const int innerRight = boxX + boxWidth - 2;
const int innerTop = boxY + 1;
const int innerBottom = boxY + boxHeight - 2;
cursorTop = boxY + 2;
cursorH = boxHeight - 4;
if (cursorH < 1)
cursorH = 1;
if (cursorTop < innerTop)
cursorTop = innerTop;
if (cursorTop + cursorH - 1 > innerBottom)
cursorH = innerBottom - cursorTop + 1;
if (cursorH < 1)
cursorH = 1;
if (cursorX < innerLeft || cursorX > innerRight)
return;
}
display->drawVerticalLine(cursorX, cursorTop, cursorH);
}
}
void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width,
uint8_t height, bool isLastCol)
{
// Draw key content
display->setFont(FONT_SMALL);
const int fontH = FONT_HEIGHT_SMALL;
// Build label and metrics first
std::string keyText;
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
// Keep literal text labels for the action keys on the rightmost column
keyText = (key.type == VK_BACKSPACE) ? "BACK"
: (key.type == VK_ENTER) ? "ENTER"
: (key.type == VK_SPACE) ? "SPACE"
: (key.type == VK_ESC) ? "ESC"
: "";
} else {
char c = getCharForKey(key, false);
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c);
}
int textWidth = display->getStringWidth(keyText.c_str());
// Label alignment
// - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly.
// - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths.
int textX;
if (isLastCol) {
const int rightPad = 1;
textX = x + width - textWidth - rightPad;
if (textX < x)
textX = x; // guard
} else {
if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) {
textX = x + (width - textWidth + 1) / 2;
} else {
textX = x + (width - textWidth) / 2;
}
}
int contentTop = y;
int contentH = height;
if (selected) {
display->setColor(WHITE);
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
if (display->getHeight() <= 64 && !isAction) {
display->fillRect(x, y, width, height);
} else if (isAction) {
const int padX = 1;
const int padY = 2;
int hlW = textWidth + padX * 2;
int hlX = textX - padX;
if (hlX < x) {
hlW -= (x - hlX);
hlX = x;
}
int maxW = (x + width) - hlX;
if (hlW > maxW)
hlW = maxW;
if (hlW < 1)
hlW = 1;
int hlH = std::min(fontH + padY * 2, (int)height);
int hlY = y + (height - hlH) / 2;
display->fillRect(hlX, hlY, hlW, hlH);
contentTop = hlY;
contentH = hlH;
} else {
display->fillRect(x, y, width, height);
}
display->setColor(BLACK);
} else {
display->setColor(WHITE);
}
int centeredTextY;
if (display->getHeight() <= 64) {
centeredTextY = y + (height - fontH) / 2;
} else {
centeredTextY = contentTop + (contentH - fontH) / 2;
}
if (display->getHeight() > 64) {
if (centeredTextY < contentTop)
centeredTextY = contentTop;
if (centeredTextY + fontH > contentTop + contentH)
centeredTextY = std::max(contentTop, contentTop + contentH - fontH);
}
if (display->getHeight() <= 64 && keyText.size() == 1) {
char ch = keyText[0];
if (ch == '.' || ch == ',' || ch == ';') {
centeredTextY -= 1;
}
}
display->drawString(textX, centeredTextY, keyText.c_str());
}
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
{
if (key.type != VK_CHAR) {
return key.character;
}
char c = key.character;
// Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings
if (isLongPress && c >= 'a' && c <= 'z') {
c = (char)(c - 'a' + 'A');
}
return c;
}
void VirtualKeyboard::moveCursorDelta(int dRow, int dCol)
{
resetTimeout();
// wrap around rows and cols in the 4x11 grid
int r = (int)cursorRow + dRow;
int c = (int)cursorCol + dCol;
if (r < 0)
r = KEYBOARD_ROWS - 1;
else if (r >= KEYBOARD_ROWS)
r = 0;
if (c < 0)
c = KEYBOARD_COLS - 1;
else if (c >= KEYBOARD_COLS)
c = 0;
cursorRow = (uint8_t)r;
cursorCol = (uint8_t)c;
}
void VirtualKeyboard::moveCursorUp()
{
moveCursorDelta(-1, 0);
}
void VirtualKeyboard::moveCursorDown()
{
moveCursorDelta(1, 0);
}
void VirtualKeyboard::moveCursorLeft()
{
resetTimeout();
if (cursorCol > 0) {
cursorCol--;
} else {
if (cursorRow > 0) {
cursorRow--;
cursorCol = KEYBOARD_COLS - 1;
} else {
cursorRow = KEYBOARD_ROWS - 1;
cursorCol = KEYBOARD_COLS - 1;
}
}
}
void VirtualKeyboard::moveCursorRight()
{
resetTimeout();
if (cursorCol < KEYBOARD_COLS - 1) {
cursorCol++;
} else {
if (cursorRow < KEYBOARD_ROWS - 1) {
cursorRow++;
cursorCol = 0;
} else {
cursorRow = 0;
cursorCol = 0;
}
}
}
void VirtualKeyboard::handlePress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert lowercase character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char
return;
}
// Handle non-character keys immediately
switch (key.type) {
case VK_BACKSPACE:
deleteCharacter();
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
std::function<void(const std::string &)> callback = onTextEntered;
onTextEntered = nullptr;
inputText = "";
callback("");
}
return;
default:
break;
}
}
void VirtualKeyboard::handleLongPress()
{
resetTimeout(); // Reset timeout on any input activity
const VirtualKey &key = keyboard[cursorRow][cursorCol];
// Don't handle press if the key is empty (but allow special keys)
if (key.character == 0 && key.type == VK_CHAR) {
return;
}
// For character keys, insert uppercase/alternate character
if (key.type == VK_CHAR) {
insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char
return;
}
switch (key.type) {
case VK_BACKSPACE:
// One-shot: delete up to 5 characters on long press
for (int i = 0; i < 5; ++i) {
if (inputText.empty())
break;
deleteCharacter();
}
break;
case VK_ENTER:
submitText();
break;
case VK_SPACE:
insertCharacter(' ');
break;
case VK_ESC:
if (onTextEntered) {
onTextEntered("");
}
break;
default:
break;
}
}
void VirtualKeyboard::insertCharacter(char c)
{
if (inputText.length() < 160) { // Reasonable text length limit
inputText += c;
}
}
void VirtualKeyboard::deleteCharacter()
{
if (!inputText.empty()) {
inputText.pop_back();
}
}
void VirtualKeyboard::submitText()
{
LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str());
// Only submit if text is not empty
if (!inputText.empty() && onTextEntered) {
// Store callback and text to submit before clearing callback
std::function<void(const std::string &)> callback = onTextEntered;
std::string textToSubmit = inputText;
onTextEntered = nullptr;
// Don't clear inputText here - let the calling module handle cleanup
// inputText = ""; // Removed: keep text visible until module cleans up
callback(textToSubmit);
} else if (inputText.empty()) {
// For empty text, just ignore the submission - don't clear callback
// This keeps the virtual keyboard responsive for further input
LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active");
} else {
// No callback available
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
}
}
void VirtualKeyboard::setInputText(const std::string &text)
{
inputText = text;
}
std::string VirtualKeyboard::getInputText() const
{
return inputText;
}
void VirtualKeyboard::setHeader(const std::string &header)
{
headerText = header;
}
void VirtualKeyboard::setCallback(std::function<void(const std::string &)> callback)
{
onTextEntered = callback;
}
void VirtualKeyboard::resetTimeout()
{
lastActivityTime = millis();
}
bool VirtualKeyboard::isTimedOut() const
{
return (millis() - lastActivityTime) > TIMEOUT_MS;
}
} // namespace graphics

View File

@@ -0,0 +1,80 @@
#pragma once
#include "configuration.h"
#include <OLEDDisplay.h>
#include <functional>
#include <string>
namespace graphics
{
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
struct VirtualKey {
char character;
VirtualKeyType type;
uint8_t x;
uint8_t y;
uint8_t width;
uint8_t height;
};
class VirtualKeyboard
{
public:
VirtualKeyboard();
~VirtualKeyboard();
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
void setInputText(const std::string &text);
std::string getInputText() const;
void setHeader(const std::string &header);
void setCallback(std::function<void(const std::string &)> callback);
// Navigation methods for encoder input
void moveCursorUp();
void moveCursorDown();
void moveCursorLeft();
void moveCursorRight();
void handlePress();
void handleLongPress();
// Timeout management
void resetTimeout();
bool isTimedOut() const;
private:
static const uint8_t KEYBOARD_ROWS = 4;
static const uint8_t KEYBOARD_COLS = 11;
static const uint8_t KEY_WIDTH = 9;
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
std::string inputText;
std::string headerText;
std::function<void(const std::string &)> onTextEntered;
uint8_t cursorRow;
uint8_t cursorCol;
// Timeout management for auto-exit
uint32_t lastActivityTime;
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
void initializeKeyboard();
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
bool isLastCol);
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
// Unified cursor movement helper
void moveCursorDelta(int dRow, int dCol);
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
void insertCharacter(char c);
void deleteCharacter();
void submitText();
};
} // namespace graphics

View File

@@ -94,8 +94,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
8, imgQuestionL1); 8, imgQuestionL1);
@@ -107,7 +106,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
#endif #endif
} else { } else {
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
8, imgSFL1); 8, imgSFL1);
@@ -122,8 +121,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
} else { } else {
// TODO: Raspberry Pi supports more than just the one screen size // TODO: Raspberry Pi supports more than just the one screen size
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
imgInfoL1); imgInfoL1);
@@ -263,6 +261,12 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
display->drawString(x + 1, y, "USB"); display->drawString(x + 1, y, "USB");
} }
// auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true);
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode);
// if (config.display.heading_bold)
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode);
uint32_t currentMillis = millis(); uint32_t currentMillis = millis();
uint32_t seconds = currentMillis / 1000; uint32_t seconds = currentMillis / 1000;
uint32_t minutes = seconds / 60; uint32_t minutes = seconds / 60;
@@ -392,7 +396,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->drawString(nameX, getTextPositions(display)[line++], shortnameble); display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
// === Second Row: Radio Preset === // === Second Row: Radio Preset ===
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
char regionradiopreset[25]; char regionradiopreset[25];
const char *region = myRegion ? myRegion->name : NULL; const char *region = myRegion ? myRegion->name : NULL;
if (region != nullptr) { if (region != nullptr) {

View File

@@ -10,7 +10,10 @@
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/draw/UIRenderer.h" #include "graphics/draw/UIRenderer.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#include "main.h" #include "main.h"
#include "mesh/MeshTypes.h"
#include "modules/AdminModule.h" #include "modules/AdminModule.h"
#include "modules/CannedMessageModule.h" #include "modules/CannedMessageModule.h"
#include "modules/KeyVerificationModule.h" #include "modules/KeyVerificationModule.h"
@@ -434,8 +437,8 @@ void menuHandler::systemBaseMenu()
optionsArray[options] = "Notifications"; optionsArray[options] = "Notifications";
optionsEnumArray[options++] = Notifications; optionsEnumArray[options++] = Notifications;
#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ #if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \
defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
optionsArray[options] = "Screen Options"; optionsArray[options] = "Screen Options";
optionsEnumArray[options++] = ScreenOptions; optionsEnumArray[options++] = ScreenOptions;
#endif #endif
@@ -725,7 +728,7 @@ void menuHandler::BrightnessPickerMenu()
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190)
// For HELTEC devices, use analogWrite to control backlight // For HELTEC devices, use analogWrite to control backlight
analogWrite(VTFT_LEDA, uiconfig.screen_brightness); analogWrite(VTFT_LEDA, uiconfig.screen_brightness);
#elif defined(ST7789_CS) || defined(ST7796_CS) #elif defined(ST7789_CS)
static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); static_cast<TFTDisplay *>(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness);
#elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107)
screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness);
@@ -768,7 +771,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display)
bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsArrayPtr = optionsArray;
bannerOptions.optionsCount = 10; bannerOptions.optionsCount = 10;
bannerOptions.bannerCallback = [display](int selected) -> void { bannerOptions.bannerCallback = [display](int selected) -> void {
#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_r = 0;
uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_g = 0;
uint8_t TFT_MESH_b = 0; uint8_t TFT_MESH_b = 0;
@@ -1045,7 +1048,7 @@ void menuHandler::screenOptionsMenu()
} }
// Only show screen color for TFT displays // 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 #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT
optionsArray[options] = "Screen Color"; optionsArray[options] = "Screen Color";
optionsEnumArray[options++] = ScreenColor; optionsEnumArray[options++] = ScreenColor;
#endif #endif

View File

@@ -7,10 +7,18 @@
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/images.h" #include "graphics/images.h"
#include "input/RotaryEncoderInterruptImpl1.h"
#include "input/UpDownInterruptImpl1.h"
#if HAS_BUTTON
#include "input/ButtonThread.h"
#endif
#include "main.h" #include "main.h"
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include <vector> #include <vector>
#if HAS_TRACKBALL
#include "input/TrackballInterruptImpl1.h"
#endif
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
#include "esp_task_wdt.h" #include "esp_task_wdt.h"
@@ -18,6 +26,11 @@
using namespace meshtastic; using namespace meshtastic;
#if HAS_BUTTON
// Global button thread pointer defined in main.cpp
extern ::ButtonThread *UserButtonThread;
#endif
// External references to global variables from Screen.cpp // External references to global variables from Screen.cpp
extern std::vector<std::string> functionSymbol; extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString; extern std::string functionSymbolString;
@@ -38,6 +51,8 @@ bool NotificationRenderer::pauseBanner = false;
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::numDigits = 0;
uint32_t NotificationRenderer::currentNumber = 0; uint32_t NotificationRenderer::currentNumber = 0;
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
uint32_t pow_of_10(uint32_t n) uint32_t pow_of_10(uint32_t n)
{ {
@@ -89,14 +104,33 @@ void NotificationRenderer::resetBanner()
void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state)
{ {
if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') // Handle text_input notifications first - they have their own timeout/banner logic
if (current_notification_type == notificationTypeEnum::text_input) {
// Check for timeout and reset if needed for text input
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner(); resetBanner();
if (!isOverlayBannerShowing() || pauseBanner)
return; return;
}
drawTextInput(display, state);
return;
}
if (millis() > alertBannerUntil && alertBannerUntil > 0) {
resetBanner();
}
// Exit if no banner is showing or banner is paused
if (!isOverlayBannerShowing() || pauseBanner) {
return;
}
switch (current_notification_type) { switch (current_notification_type) {
case notificationTypeEnum::none: case notificationTypeEnum::none:
// Do nothing - no notification to display // Do nothing - no notification to display
break; break;
case notificationTypeEnum::text_input:
// Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch.
break;
case notificationTypeEnum::text_banner: case notificationTypeEnum::text_banner:
case notificationTypeEnum::selection_picker: case notificationTypeEnum::selection_picker:
drawAlertBannerOverlay(display, state); drawAlertBannerOverlay(display, state);
@@ -267,12 +301,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) {
std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name);
strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1);
} else { } else {
snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF));
} }
// make temp buffer for name
// fi
if (i == curSelected) { if (i == curSelected) {
selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num;
if (isHighResolution) { if (isHighResolution) {
@@ -286,7 +317,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
} }
scratchLineBuffer[scratchLineNum][39] = '\0'; scratchLineBuffer[scratchLineNum][39] = '\0';
} else { } else {
strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36); strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39);
scratchLineBuffer[scratchLineNum][39] = '\0';
} }
linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; linePointers[linesShown] = scratchLineBuffer[scratchLineNum++];
} }
@@ -575,6 +607,100 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi
"Please be patient and do not power off."); "Please be patient and do not power off.");
} }
void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state)
{
if (virtualKeyboard) {
// Check for timeout and auto-exit if needed
if (virtualKeyboard->isTimedOut()) {
LOG_INFO("Virtual keyboard timeout - auto-exiting");
// Cancel virtual keyboard - call callback with empty string to indicate timeout
auto callback = textInputCallback; // Store callback before clearing
// Clean up first to prevent re-entry
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
// Call callback after cleanup
if (callback) {
callback("");
}
// Restore normal overlays
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
if (inEvent.inputEvent != INPUT_BROKER_NONE) {
if (inEvent.inputEvent == INPUT_BROKER_UP) {
// high frequency for move cursor left/right than up/down with encoders
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorLeft();
} else {
virtualKeyboard->moveCursorUp();
}
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN) {
extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
extern ::UpDownInterruptImpl1 *upDownInterruptImpl1;
if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) {
virtualKeyboard->moveCursorRight();
} else {
virtualKeyboard->moveCursorDown();
}
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) {
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
virtualKeyboard->moveCursorUp();
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
virtualKeyboard->moveCursorDown();
} else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) {
// Long press UP = move left
virtualKeyboard->moveCursorLeft();
} else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) {
// Long press DOWN = move right
virtualKeyboard->moveCursorRight();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT) {
virtualKeyboard->handlePress();
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) {
virtualKeyboard->handleLongPress();
} else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) {
auto callback = textInputCallback;
delete virtualKeyboard;
virtualKeyboard = nullptr;
textInputCallback = nullptr;
resetBanner();
if (callback) {
callback("");
}
if (screen) {
screen->setFrames(graphics::Screen::FOCUS_PRESERVE);
}
return;
}
// Consume the event after processing for virtual keyboard
inEvent.inputEvent = INPUT_BROKER_NONE;
}
// Clear the screen to avoid overlapping with underlying frames or overlays
display->setColor(BLACK);
display->fillRect(0, 0, display->getWidth(), display->getHeight());
display->setColor(WHITE);
virtualKeyboard->draw(display, 0, 0);
} else {
// If virtualKeyboard is null, reset the banner to avoid getting stuck
LOG_INFO("Virtual keyboard is null - resetting banner");
resetBanner();
}
}
bool NotificationRenderer::isOverlayBannerShowing() bool NotificationRenderer::isOverlayBannerShowing()
{ {
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);

View File

@@ -3,6 +3,9 @@
#include "OLEDDisplay.h" #include "OLEDDisplay.h"
#include "OLEDDisplayUi.h" #include "OLEDDisplayUi.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/VirtualKeyboard.h"
#include <functional>
#include <string>
#define MAX_LINES 5 #define MAX_LINES 5
namespace graphics namespace graphics
@@ -22,6 +25,8 @@ class NotificationRenderer
static std::function<void(int)> alertBannerCallback; static std::function<void(int)> alertBannerCallback;
static uint32_t numDigits; static uint32_t numDigits;
static uint32_t currentNumber; static uint32_t currentNumber;
static VirtualKeyboard *virtualKeyboard;
static std::function<void(const std::string &)> textInputCallback;
static bool pauseBanner; static bool pauseBanner;
@@ -30,6 +35,7 @@ class NotificationRenderer
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);

View File

@@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
} }
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
if (isHighResolution) { if (isHighResolution) {

View File

@@ -27,8 +27,7 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
ARCH_PORTDUINO) && \
!defined(DISPLAY_FORCE_SMALL_FONTS) !defined(DISPLAY_FORCE_SMALL_FONTS)
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};

View File

@@ -1,68 +0,0 @@
#include "./ZJY122250_0213BAAMFGN.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
// Map the display controller IC's output to the connected panel
void ZJY122250_0213BAAMFGN::configScanning()
{
// "Driver output control"
// Scan gates from 0 to 249 (vertical resolution 250px)
sendCommand(0x01);
sendData(0xF9);
sendData(0x00);
sendData(0x00);
}
// Specify which information is used to control the sequence of voltages applied to move the pixels
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
// the controller IC's OTP memory, when the update procedure begins.
void ZJY122250_0213BAAMFGN::configWaveform()
{
switch (updateType) {
case FAST:
sendCommand(0x3C); // Border waveform:
sendData(0x80); // VCOM
break;
case FULL:
default:
sendCommand(0x3C); // Border waveform:
sendData(0x01); // Follow LUT 1 (blink same as white pixels)
break;
}
sendCommand(0x18); // Temperature sensor:
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
}
void ZJY122250_0213BAAMFGN::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22); // Set "update sequence"
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
break;
case FULL:
default:
sendCommand(0x22); // Set "update sequence"
sendData(0xF7); // Will load LUT from OTP memory
break;
}
}
// Once the refresh operation has been started,
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
// Only used when refresh is "async"
void ZJY122250_0213BAAMFGN::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 500); // At least 500ms for fast refresh
case FULL:
default:
return beginPolling(100, 2000); // At least 2 seconds for full refresh
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -1,42 +0,0 @@
/*
E-Ink display driver
- ZJY122250_0213BAAMFGN
- Manufacturer: Zhongjingyuan
- Size: 2.13 inch
- Resolution: 250px x 122px
- Flex connector marking (not a unique identifier): FPC-A002
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class ZJY122250_0213BAAMFGN : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 122;
static constexpr uint32_t height = 250;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {}
protected:
virtual void configScanning() override;
virtual void configWaveform() override;
virtual void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@@ -7,7 +7,12 @@ using namespace NicheGraphics;
// Timing for "maintenance" // Timing for "maintenance"
// Paying off full-refresh debt with unprovoked updates, if the display is not very active // Paying off full-refresh debt with unprovoked updates, if the display is not very active
#ifdef SEEED_WIO_TRACKER_L1
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL;
#else
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
#endif
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator")

View File

@@ -76,6 +76,9 @@ class ButtonThread : public Observable<const InputEvent *>, public concurrency::
return digitalRead(buttonPin); // Most buttons are active low by default return digitalRead(buttonPin); // Most buttons are active low by default
} }
// Returns true while this thread's button is physically held down
bool isHeld() { return isButtonPressed(_pinNum); }
// Disconnect and reconnect interrupts for light sleep // Disconnect and reconnect interrupts for light sleep
#ifdef ARCH_ESP32 #ifdef ARCH_ESP32
int beforeLightSleep(void *unused); int beforeLightSleep(void *unused);

View File

@@ -4,6 +4,9 @@
enum input_broker_event { enum input_broker_event {
INPUT_BROKER_NONE = 0, INPUT_BROKER_NONE = 0,
INPUT_BROKER_SELECT = 10, INPUT_BROKER_SELECT = 10,
INPUT_BROKER_SELECT_LONG = 11,
INPUT_BROKER_UP_LONG = 12,
INPUT_BROKER_DOWN_LONG = 13,
INPUT_BROKER_UP = 17, INPUT_BROKER_UP = 17,
INPUT_BROKER_DOWN = 18, INPUT_BROKER_DOWN = 18,
INPUT_BROKER_LEFT = 19, INPUT_BROKER_LEFT = 19,

View File

@@ -1,73 +0,0 @@
#ifdef T_LORA_PAGER
#include "RotaryEncoderImpl.h"
#include "InputBroker.h"
#include "RotaryEncoder.h"
#define ORIGIN_NAME "RotaryEncoder"
RotaryEncoderImpl *rotaryEncoderImpl;
RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME)
{
rotary = nullptr;
}
bool RotaryEncoderImpl::init()
{
if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 ||
moduleConfig.canned_message.inputbroker_pin_b == 0) {
// Input device is disabled.
disable();
return false;
}
eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b,
moduleConfig.canned_message.inputbroker_pin_press);
rotary->resetButton();
inputBroker->registerSource(this);
LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a,
moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw,
eventPressed);
return true;
}
int32_t RotaryEncoderImpl::runOnce()
{
InputEvent e{originName, INPUT_BROKER_NONE, 0, 0, 0};
static uint32_t lastPressed = millis();
if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) {
if (lastPressed + 200 < millis()) {
LOG_DEBUG("Rotary event Press");
lastPressed = millis();
e.inputEvent = this->eventPressed;
}
} else {
switch (rotary->process()) {
case RotaryEncoder::DIRECTION_CW:
LOG_DEBUG("Rotary event CW");
e.inputEvent = this->eventCw;
break;
case RotaryEncoder::DIRECTION_CCW:
LOG_DEBUG("Rotary event CCW");
e.inputEvent = this->eventCcw;
break;
default:
break;
}
}
if (e.inputEvent != INPUT_BROKER_NONE) {
this->notifyObservers(&e);
}
return 10;
}
#endif

View File

@@ -1,28 +0,0 @@
#pragma once
// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library)
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include "mesh/NodeDB.h"
class RotaryEncoder;
class RotaryEncoderImpl : public Observable<const InputEvent *>, public concurrency::OSThread
{
public:
RotaryEncoderImpl();
bool init(void);
protected:
virtual int32_t runOnce() override;
input_broker_event eventCw = INPUT_BROKER_NONE;
input_broker_event eventCcw = INPUT_BROKER_NONE;
input_broker_event eventPressed = INPUT_BROKER_NONE;
RotaryEncoder *rotary;
const char *originName;
};
extern RotaryEncoderImpl *rotaryEncoderImpl;

View File

@@ -8,33 +8,26 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu
void RotaryEncoderInterruptBase::init( void RotaryEncoderInterruptBase::init(
uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed, input_broker_event eventPressed, input_broker_event eventPressedLong,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) : // std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress) :
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) void (*onIntA)(), void (*onIntB)(), void (*onIntPress)())
{ {
this->_pinA = pinA; this->_pinA = pinA;
this->_pinB = pinB; this->_pinB = pinB;
this->_pinPress = pinPress;
this->_eventCw = eventCw; this->_eventCw = eventCw;
this->_eventCcw = eventCcw; this->_eventCcw = eventCcw;
this->_eventPressed = eventPressed; this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
bool isRAK = false;
#ifdef RAK_4631
isRAK = true;
#endif
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP); pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, RISING);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinA, INPUT_PULLUP); pinMode(this->_pinA, INPUT_PULLUP);
attachInterrupt(this->_pinA, onIntA, CHANGE);
}
if (!isRAK || this->_pinA != 0) {
pinMode(this->_pinB, INPUT_PULLUP); pinMode(this->_pinB, INPUT_PULLUP);
// Use FALLING edge for active-low press button to start long-press timing immediately
attachInterrupt(pinPress, onIntPress, FALLING);
attachInterrupt(this->_pinA, onIntA, CHANGE);
attachInterrupt(this->_pinB, onIntB, CHANGE); attachInterrupt(this->_pinB, onIntB, CHANGE);
}
this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelA = digitalRead(this->_pinA);
this->rotaryLevelB = digitalRead(this->_pinB); this->rotaryLevelB = digitalRead(this->_pinB);
@@ -46,10 +39,37 @@ int32_t RotaryEncoderInterruptBase::runOnce()
InputEvent e; InputEvent e;
e.inputEvent = INPUT_BROKER_NONE; e.inputEvent = INPUT_BROKER_NONE;
e.source = this->_originName; e.source = this->_originName;
unsigned long now = millis();
// Handle press long/short detection
if (this->action == ROTARY_ACTION_PRESSED) { if (this->action == ROTARY_ACTION_PRESSED) {
LOG_DEBUG("Rotary event Press"); bool buttonPressed = !digitalRead(_pinPress);
if (!pressDetected && buttonPressed) {
pressDetected = true;
pressStartTime = now;
}
if (pressDetected) {
uint32_t duration = now - pressStartTime;
if (!buttonPressed) {
// released -> if short press, send short, else already sent long
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short");
e.inputEvent = this->_eventPressed; e.inputEvent = this->_eventPressed;
}
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
this->action = ROTARY_ACTION_NONE;
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
lastPressLongEventTime == 0) {
// fire single-shot long press
lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
}
} else if (this->action == ROTARY_ACTION_CW) { } else if (this->action == ROTARY_ACTION_CW) {
LOG_DEBUG("Rotary event CW"); LOG_DEBUG("Rotary event CW");
e.inputEvent = this->_eventCw; e.inputEvent = this->_eventCw;
@@ -62,7 +82,9 @@ int32_t RotaryEncoderInterruptBase::runOnce()
this->notifyObservers(&e); this->notifyObservers(&e);
} }
if (!pressDetected) {
this->action = ROTARY_ACTION_NONE; this->action = ROTARY_ACTION_NONE;
}
return INT32_MAX; return INT32_MAX;
} }
@@ -70,7 +92,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
void RotaryEncoderInterruptBase::intPressHandler() void RotaryEncoderInterruptBase::intPressHandler()
{ {
this->action = ROTARY_ACTION_PRESSED; this->action = ROTARY_ACTION_PRESSED;
setIntervalFromNow(20); // TODO: this modifies a non-volatile variable! setIntervalFromNow(20); // start checking for long/short
} }
void RotaryEncoderInterruptBase::intAHandler() void RotaryEncoderInterruptBase::intAHandler()

View File

@@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
public: public:
explicit RotaryEncoderInterruptBase(const char *name); explicit RotaryEncoderInterruptBase(const char *name);
void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw,
input_broker_event eventPressed, input_broker_event eventPressed, input_broker_event eventPressedLong,
// std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress); // std::function<void(void)> onIntA, std::function<void(void)> onIntB, std::function<void(void)> onIntPress);
void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); void (*onIntA)(), void (*onIntB)(), void (*onIntPress)());
void intPressHandler(); void intPressHandler();
@@ -33,10 +33,22 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
private: private:
// pins and events
uint8_t _pinA = 0; uint8_t _pinA = 0;
uint8_t _pinB = 0; uint8_t _pinB = 0;
uint8_t _pinPress = 0;
input_broker_event _eventCw = INPUT_BROKER_NONE; input_broker_event _eventCw = INPUT_BROKER_NONE;
input_broker_event _eventCcw = INPUT_BROKER_NONE; input_broker_event _eventCcw = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName; const char *_originName;
// Long press detection variables
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastPressLongEventTime = 0;
unsigned long lastPressKeyTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select
const unsigned long pressDebounceMs = 200; // ms
}; };

View File

@@ -1,5 +1,6 @@
#include "RotaryEncoderInterruptImpl1.h" #include "RotaryEncoderInterruptImpl1.h"
#include "InputBroker.h" #include "InputBroker.h"
extern bool osk_found;
RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1;
@@ -19,12 +20,14 @@ bool RotaryEncoderInterruptImpl1::init()
input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw); input_broker_event eventCw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_cw);
input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw); input_broker_event eventCcw = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_ccw);
input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press); input_broker_event eventPressed = static_cast<input_broker_event>(moduleConfig.canned_message.inputbroker_event_press);
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
// moduleConfig.canned_message.ext_notification_module_output // moduleConfig.canned_message.ext_notification_module_output
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed); RotaryEncoderInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this); inputBroker->registerSource(this);
osk_found = true;
return true; return true;
} }

View File

@@ -1,230 +0,0 @@
#if defined(T_LORA_PAGER)
#include "TLoraPagerKeyboard.h"
#include "main.h"
#ifndef LEDC_BACKLIGHT_CHANNEL
#define LEDC_BACKLIGHT_CHANNEL 4
#endif
#ifndef LEDC_BACKLIGHT_BIT_WIDTH
#define LEDC_BACKLIGHT_BIT_WIDTH 8
#endif
#ifndef LEDC_BACKLIGHT_FREQ
#define LEDC_BACKLIGHT_FREQ 1000 // Hz
#endif
#define _TCA8418_COLS 10
#define _TCA8418_ROWS 4
#define _TCA8418_NUM_KEYS 31
#define _TCA8418_MULTI_TAP_THRESHOLD 1500
using Key = TCA8418KeyboardBase::TCA8418Key;
constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1
constexpr uint8_t modifierRightShift = 0b0001;
constexpr uint8_t modifierSymKey = 21 - 1;
constexpr uint8_t modifierSym = 0b0010;
// Num chars per key, Modulus for rotating through characters
static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3};
static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
{'w', 'W', '2'},
{'e', 'E', '3'},
{'r', 'R', '4'},
{'t', 'T', '5'},
{'y', 'Y', '6'},
{'u', 'U', '7'},
{'i', 'I', '8'},
{'o', 'O', '9'},
{'p', 'P', '0'},
{'a', 'A', '*'},
{'s', 'S', '/'},
{'d', 'D', '+'},
{'f', 'F', '-'},
{'g', 'G', '='},
{'h', 'H', ':'},
{'j', 'J', '\''},
{'k', 'K', '"'},
{'l', 'L', '@'},
{Key::SELECT, 0x00, Key::TAB},
{0x00, 0x00, 0x00},
{'z', 'Z', '_'},
{'x', 'X', '$'},
{'c', 'C', ';'},
{'v', 'V', '?'},
{'b', 'B', '!'},
{'n', 'N', ','},
{'m', 'M', '.'},
{0x00, 0x00, 0x00},
{Key::BSP, 0x00, Key::ESC},
{' ', 0x00, Key::BL_TOGGLE}};
TLoraPagerKeyboard::TLoraPagerKeyboard()
: TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1),
last_tap(0L), char_idx(0), tap_interval(0)
{
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
#else
ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH);
ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL);
#endif
reset();
}
void TLoraPagerKeyboard::reset(void)
{
TCA8418KeyboardBase::reset();
pinMode(KB_BL_PIN, OUTPUT);
digitalWrite(KB_BL_PIN, LOW);
setBacklight(false);
}
// handle multi-key presses (shift and alt)
void TLoraPagerKeyboard::trigger()
{
uint8_t count = keyCount();
if (count == 0)
return;
for (uint8_t i = 0; i < count; ++i) {
uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i);
uint8_t key = k & 0x7F;
if (k & 0x80) {
pressed(key);
} else {
released();
state = Idle;
}
}
}
void TLoraPagerKeyboard::setBacklight(bool on)
{
toggleBacklight(!on);
}
void TLoraPagerKeyboard::pressed(uint8_t key)
{
if (state == Init || state == Busy) {
return;
}
if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED ||
config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) {
hapticFeedback();
}
if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) {
modifierFlag = 0;
}
uint8_t next_key = 0;
int row = (key - 1) / 10;
int col = (key - 1) % 10;
if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) {
return; // Invalid key
}
next_key = row * _TCA8418_COLS + col;
state = Held;
uint32_t now = millis();
tap_interval = now - last_tap;
updateModifierFlag(next_key);
if (isModifierKey(next_key)) {
last_modifier_time = now;
}
if (tap_interval < 0) {
last_tap = 0;
state = Busy;
return;
}
if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) {
char_idx = 0;
} else {
char_idx += 1;
}
last_key = next_key;
last_tap = now;
}
void TLoraPagerKeyboard::released()
{
if (state != Held) {
return;
}
if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) {
last_key = -1;
state = Idle;
return;
}
uint32_t now = millis();
last_tap = now;
if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) {
toggleBacklight();
return;
}
queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]);
if (isModifierKey(last_key) == false)
modifierFlag = 0;
}
void TLoraPagerKeyboard::hapticFeedback()
{
drv.setWaveform(0, 14); // strong buzz 100%
drv.setWaveform(1, 0); // end waveform
drv.go();
}
// toggle brightness of the backlight in three steps
void TLoraPagerKeyboard::toggleBacklight(bool off)
{
static uint32_t brightness = 0;
if (off) {
brightness = 0;
} else {
if (brightness == 0) {
brightness = 40;
} else if (brightness == 40) {
brightness = 127;
} else if (brightness >= 127) {
brightness = 0;
}
}
LOG_DEBUG("Toggle backlight: %d", brightness);
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
ledcWrite(KB_BL_PIN, brightness);
#else
ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness);
#endif
}
void TLoraPagerKeyboard::updateModifierFlag(uint8_t key)
{
if (key == modifierRightShiftKey) {
modifierFlag ^= modifierRightShift;
} else if (key == modifierSymKey) {
modifierFlag ^= modifierSym;
}
}
bool TLoraPagerKeyboard::isModifierKey(uint8_t key)
{
return (key == modifierRightShiftKey || key == modifierSymKey);
}
#endif

View File

@@ -4,26 +4,9 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase
{ {
public: public:
TLoraPagerKeyboard(); TLoraPagerKeyboard();
void reset(void); void setBacklight(bool on) override{};
void trigger(void) override;
void setBacklight(bool on) override;
virtual ~TLoraPagerKeyboard() {}
protected: protected:
void pressed(uint8_t key) override; void pressed(uint8_t key) override{};
void released(void) override; void released(void) override{};
void hapticFeedback(void);
void updateModifierFlag(uint8_t key);
bool isModifierKey(uint8_t key);
void toggleBacklight(bool off = false);
private:
uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed
uint32_t last_modifier_time; // Timestamp of the last modifier key press
int8_t last_key;
int8_t next_key;
uint32_t last_tap;
uint8_t char_idx;
int32_t tap_interval;
}; };

View File

@@ -1,12 +1,14 @@
#include "TrackballInterruptBase.h" #include "TrackballInterruptBase.h"
#include "configuration.h" #include "configuration.h"
extern bool osk_found;
TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {}
void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress,
input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft,
input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), input_broker_event eventRight, input_broker_event eventPressed,
void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)())
{ {
this->_pinDown = pinDown; this->_pinDown = pinDown;
this->_pinUp = pinUp; this->_pinUp = pinUp;
@@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
this->_eventLeft = eventLeft; this->_eventLeft = eventLeft;
this->_eventRight = eventRight; this->_eventRight = eventRight;
this->_eventPressed = eventPressed; this->_eventPressed = eventPressed;
this->_eventPressedLong = eventPressedLong;
if (pinPress != 255) { if (pinPress != 255) {
pinMode(pinPress, INPUT_PULLUP); pinMode(pinPress, INPUT_PULLUP);
@@ -40,9 +43,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
} }
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown,
pinPress); this->_pinLeft, this->_pinRight, pinPress);
osk_found = true;
this->setInterval(100); this->setInterval(100);
} }
@@ -50,10 +53,47 @@ int32_t TrackballInterruptBase::runOnce()
{ {
InputEvent e; InputEvent e;
e.inputEvent = INPUT_BROKER_NONE; e.inputEvent = INPUT_BROKER_NONE;
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED) { // Handle long press detection for press button
// LOG_DEBUG("Trackball event Press"); if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = millis() - pressStartTime;
bool buttonStillPressed = false;
#if defined(T_DECK)
buttonStillPressed = (this->action == TB_ACTION_PRESSED);
#else
buttonStillPressed = !digitalRead(_pinPress);
#endif
if (!buttonStillPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION) {
// Short press
e.inputEvent = this->_eventPressed; e.inputEvent = this->_eventPressed;
}
// Reset state
pressDetected = false;
pressStartTime = 0;
lastLongPressEventTime = 0;
this->action = TB_ACTION_NONE;
} else if (pressDuration >= LONG_PRESS_DURATION) {
// Long press detected
uint32_t currentTime = millis();
// Only trigger long press event if enough time has passed since the last one
if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventPressedLong;
lastLongPressEventTime = currentTime;
}
this->action = TB_ACTION_PRESSED_LONG;
}
}
#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball
if (this->action == TB_ACTION_PRESSED && !pressDetected) {
// Start long press detection
pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
} else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) {
// LOG_DEBUG("Trackball event UP"); // LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp; e.inputEvent = this->_eventUp;
@@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce()
e.inputEvent = this->_eventRight; e.inputEvent = this->_eventRight;
} }
#else #else
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) {
// LOG_DEBUG("Trackball event Press"); // Start long press detection
e.inputEvent = this->_eventPressed; pressDetected = true;
pressStartTime = millis();
// Don't send event yet, wait to see if it's a long press
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
// LOG_DEBUG("Trackball event UP"); // LOG_DEBUG("Trackball event UP");
e.inputEvent = this->_eventUp; e.inputEvent = this->_eventUp;
@@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce()
e.kbchar = 0x00; e.kbchar = 0x00;
this->notifyObservers(&e); this->notifyObservers(&e);
} }
lastEvent = action;
this->action = TB_ACTION_NONE;
return 100; // Only update lastEvent for non-press actions or completed press actions
if (this->action != TB_ACTION_PRESSED || !pressDetected) {
lastEvent = action;
if (!pressDetected) {
this->action = TB_ACTION_NONE;
}
}
return 50; // Check more frequently for better long press detection
} }
void TrackballInterruptBase::intPressHandler() void TrackballInterruptBase::intPressHandler()

View File

@@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
explicit TrackballInterruptBase(const char *name); explicit TrackballInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight,
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(),
void (*onIntPress)()); void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)());
void intPressHandler(); void intPressHandler();
void intDownHandler(); void intDownHandler();
void intUpHandler(); void intUpHandler();
@@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
enum TrackballInterruptBaseActionType { enum TrackballInterruptBaseActionType {
TB_ACTION_NONE, TB_ACTION_NONE,
TB_ACTION_PRESSED, TB_ACTION_PRESSED,
TB_ACTION_PRESSED_LONG,
TB_ACTION_UP, TB_ACTION_UP,
TB_ACTION_DOWN, TB_ACTION_DOWN,
TB_ACTION_LEFT, TB_ACTION_LEFT,
@@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
// Long press detection for press button
uint32_t pressStartTime = 0;
bool pressDetected = false;
uint32_t lastLongPressEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 500; // ms
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events
private: private:
input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE;
input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName; const char *_originName;
TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE;
}; };

View File

@@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventLeft = INPUT_BROKER_LEFT;
input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventRight = INPUT_BROKER_RIGHT;
input_broker_event eventPressed = INPUT_BROKER_SELECT; input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight,
eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown,
TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft,
TrackballInterruptImpl1::handleIntPressed); TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this); inputBroker->registerSource(this);
} }

View File

@@ -7,35 +7,36 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre
} }
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(), input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong,
input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(),
void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs)
{ {
this->_pinDown = pinDown; this->_pinDown = pinDown;
this->_pinUp = pinUp; this->_pinUp = pinUp;
this->_pinPress = pinPress;
this->_eventDown = eventDown; this->_eventDown = eventDown;
this->_eventUp = eventUp; this->_eventUp = eventUp;
this->_eventPressed = eventPressed; this->_eventPressed = eventPressed;
bool isRAK = false; this->_eventPressedLong = eventPressedLong;
#ifdef RAK_4631 this->_eventUpLong = eventUpLong;
isRAK = true; this->_eventDownLong = eventDownLong;
#endif
// Store debounce configuration passed by caller
this->updownDebounceMs = updownDebounceMs;
if (!isRAK || pinPress != 0) {
pinMode(pinPress, INPUT_PULLUP); pinMode(pinPress, INPUT_PULLUP);
attachInterrupt(pinPress, onIntPress, RISING);
}
if (!isRAK || this->_pinDown != 0) {
pinMode(this->_pinDown, INPUT_PULLUP); pinMode(this->_pinDown, INPUT_PULLUP);
attachInterrupt(this->_pinDown, onIntDown, RISING);
}
if (!isRAK || this->_pinUp != 0) {
pinMode(this->_pinUp, INPUT_PULLUP); pinMode(this->_pinUp, INPUT_PULLUP);
attachInterrupt(this->_pinUp, onIntUp, RISING);
} // Use FALLING edge for active-low buttons so we detect press at the moment of pressing
// This enables long-press timing to start immediately instead of waiting for release.
attachInterrupt(pinPress, onIntPress, FALLING);
attachInterrupt(this->_pinDown, onIntDown, FALLING);
attachInterrupt(this->_pinUp, onIntUp, FALLING);
LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress);
this->setInterval(100); this->setInterval(20);
} }
int32_t UpDownInterruptBase::runOnce() int32_t UpDownInterruptBase::runOnce()
@@ -43,24 +44,89 @@ int32_t UpDownInterruptBase::runOnce()
InputEvent e; InputEvent e;
e.inputEvent = INPUT_BROKER_NONE; e.inputEvent = INPUT_BROKER_NONE;
unsigned long now = millis(); unsigned long now = millis();
if (this->action == UPDOWN_ACTION_PRESSED) {
if (now - lastPressKeyTime >= pressDebounceMs) { // Read all button states once at the beginning
bool pressButtonPressed = !digitalRead(_pinPress);
bool upButtonPressed = !digitalRead(_pinUp);
bool downButtonPressed = !digitalRead(_pinDown);
// Handle initial button press detection - only if not already detected
if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) {
pressDetected = true;
pressStartTime = now;
} else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) {
upDetected = true;
upStartTime = now;
} else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) {
downDetected = true;
downStartTime = now;
}
// Handle long press detection for press button
if (pressDetected && pressStartTime > 0) {
uint32_t pressDuration = now - pressStartTime;
if (!pressButtonPressed) {
// Button released
if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now; lastPressKeyTime = now;
LOG_DEBUG("GPIO event Press");
e.inputEvent = this->_eventPressed; e.inputEvent = this->_eventPressed;
} }
} else if (this->action == UPDOWN_ACTION_UP) { // Reset state
if (now - lastUpKeyTime >= updownDebounceMs) { pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
} else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) {
// First long press event only - avoid repeated events causing lag
e.inputEvent = this->_eventPressedLong;
lastPressLongEventTime = now;
}
}
// Handle long press detection for up button
if (upDetected && upStartTime > 0) {
uint32_t upDuration = now - upStartTime;
if (!upButtonPressed) {
// Button released
if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) {
lastUpKeyTime = now; lastUpKeyTime = now;
LOG_DEBUG("GPIO event Up");
e.inputEvent = this->_eventUp; e.inputEvent = this->_eventUp;
} }
} else if (this->action == UPDOWN_ACTION_DOWN) { // Reset state
if (now - lastDownKeyTime >= updownDebounceMs) { upDetected = false;
upStartTime = 0;
lastUpLongEventTime = 0;
} else if (upDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventUpLong;
lastUpLongEventTime = now;
}
}
}
// Handle long press detection for down button
if (downDetected && downStartTime > 0) {
uint32_t downDuration = now - downStartTime;
if (!downButtonPressed) {
// Button released
if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) {
lastDownKeyTime = now; lastDownKeyTime = now;
LOG_DEBUG("GPIO event Down");
e.inputEvent = this->_eventDown; e.inputEvent = this->_eventDown;
} }
// Reset state
downDetected = false;
downStartTime = 0;
lastDownLongEventTime = 0;
} else if (downDuration >= LONG_PRESS_DURATION) {
// Auto-repeat long press events
if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) {
e.inputEvent = this->_eventDownLong;
lastDownLongEventTime = now;
}
}
} }
if (e.inputEvent != INPUT_BROKER_NONE) { if (e.inputEvent != INPUT_BROKER_NONE) {
@@ -69,8 +135,11 @@ int32_t UpDownInterruptBase::runOnce()
this->notifyObservers(&e); this->notifyObservers(&e);
} }
if (!pressDetected && !upDetected && !downDetected) {
this->action = UPDOWN_ACTION_NONE; this->action = UPDOWN_ACTION_NONE;
return 100; }
return 20; // This will control how the input frequency
} }
void UpDownInterruptBase::intPressHandler() void UpDownInterruptBase::intPressHandler()

View File

@@ -8,7 +8,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
public: public:
explicit UpDownInterruptBase(const char *name); explicit UpDownInterruptBase(const char *name);
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong,
input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
unsigned long updownDebounceMs = 50); unsigned long updownDebounceMs = 50);
void intPressHandler(); void intPressHandler();
void intDownHandler(); void intDownHandler();
@@ -17,16 +18,41 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
int32_t runOnce() override; int32_t runOnce() override;
protected: protected:
enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN }; enum UpDownInterruptBaseActionType {
UPDOWN_ACTION_NONE,
UPDOWN_ACTION_PRESSED,
UPDOWN_ACTION_PRESSED_LONG,
UPDOWN_ACTION_UP,
UPDOWN_ACTION_UP_LONG,
UPDOWN_ACTION_DOWN,
UPDOWN_ACTION_DOWN_LONG
};
volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE;
// Long press detection variables
uint32_t pressStartTime = 0;
uint32_t upStartTime = 0;
uint32_t downStartTime = 0;
bool pressDetected = false;
bool upDetected = false;
bool downDetected = false;
uint32_t lastPressLongEventTime = 0;
uint32_t lastUpLongEventTime = 0;
uint32_t lastDownLongEventTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300;
static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300;
private: private:
uint8_t _pinDown = 0; uint8_t _pinDown = 0;
uint8_t _pinUp = 0; uint8_t _pinUp = 0;
uint8_t _pinPress = 0;
input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventDown = INPUT_BROKER_NONE;
input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE;
input_broker_event _eventPressed = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE;
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
input_broker_event _eventUpLong = INPUT_BROKER_NONE;
input_broker_event _eventDownLong = INPUT_BROKER_NONE;
const char *_originName; const char *_originName;
unsigned long lastUpKeyTime = 0; unsigned long lastUpKeyTime = 0;

View File

@@ -1,5 +1,6 @@
#include "UpDownInterruptImpl1.h" #include "UpDownInterruptImpl1.h"
#include "InputBroker.h" #include "InputBroker.h"
extern bool osk_found;
UpDownInterruptImpl1 *upDownInterruptImpl1; UpDownInterruptImpl1 *upDownInterruptImpl1;
@@ -20,10 +21,15 @@ bool UpDownInterruptImpl1::init()
input_broker_event eventDown = INPUT_BROKER_DOWN; input_broker_event eventDown = INPUT_BROKER_DOWN;
input_broker_event eventUp = INPUT_BROKER_UP; input_broker_event eventUp = INPUT_BROKER_UP;
input_broker_event eventPressed = INPUT_BROKER_SELECT; input_broker_event eventPressed = INPUT_BROKER_SELECT;
input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG;
input_broker_event eventUpLong = INPUT_BROKER_UP_LONG;
input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG;
UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown, UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong,
UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp,
UpDownInterruptImpl1::handleIntPressed);
inputBroker->registerSource(this); inputBroker->registerSource(this);
osk_found = true;
return true; return true;
} }

View File

@@ -12,12 +12,8 @@ void CardKbI2cImpl::init()
#if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN)
if (cardkb_found.address == 0x00) { if (cardkb_found.address == 0x00) {
LOG_DEBUG("Rescan for I2C keyboard"); LOG_DEBUG("Rescan for I2C keyboard");
uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS};
#if defined(T_LORA_PAGER)
uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]);
#else
uint8_t i2caddr_asize = 5; uint8_t i2caddr_asize = 5;
#endif
auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire()); auto i2cScanner = std::unique_ptr<ScanI2CTwoWire>(new ScanI2CTwoWire());
#if WIRE_INTERFACES_COUNT == 2 #if WIRE_INTERFACES_COUNT == 2

View File

@@ -135,9 +135,8 @@ AccelerometerThread *accelerometerThread = nullptr;
AudioThread *audioThread = nullptr; AudioThread *audioThread = nullptr;
#endif #endif
#ifdef USE_XL9555 #ifdef USE_PCA9557
#include "ExtensionIOXL9555.hpp" PCA9557 IOEXP;
ExtensionIOXL9555 io;
#endif #endif
#if HAS_TFT #if HAS_TFT
@@ -192,6 +191,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE;
uint8_t kb_model; uint8_t kb_model;
// global bool to record that a kb is present // global bool to record that a kb is present
bool kb_found = false; bool kb_found = false;
// global bool to record that on-screen keyboard (OSK) is present
bool osk_found = false;
// The I2C address of the RTC Module (if found) // The I2C address of the RTC Module (if found)
ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE;
@@ -202,7 +203,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE,
/// The I2C address of our Air Quality Indicator (if found) /// The I2C address of our Air Quality Indicator (if found)
ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE;
#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #ifdef T_WATCH_S3
Adafruit_DRV2605 drv; Adafruit_DRV2605 drv;
#endif #endif
@@ -360,30 +361,6 @@ void setup()
digitalWrite(SDCARD_CS, HIGH); digitalWrite(SDCARD_CS, HIGH);
pinMode(PIN_EINK_CS, OUTPUT); pinMode(PIN_EINK_CS, OUTPUT);
digitalWrite(PIN_EINK_CS, HIGH); digitalWrite(PIN_EINK_CS, HIGH);
#elif defined(T_LORA_PAGER)
pinMode(LORA_CS, OUTPUT);
digitalWrite(LORA_CS, HIGH);
pinMode(SDCARD_CS, OUTPUT);
digitalWrite(SDCARD_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
// io expander
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
io.digitalWrite(EXPANDS_AMP_EN, HIGH);
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
io.pinMode(EXPANDS_KB_EN, OUTPUT);
io.digitalWrite(EXPANDS_KB_EN, HIGH);
io.pinMode(EXPANDS_SD_EN, OUTPUT);
io.digitalWrite(EXPANDS_SD_EN, HIGH);
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
#endif #endif
concurrency::hasBeenSetup = true; concurrency::hasBeenSetup = true;
@@ -419,7 +396,7 @@ void setup()
struct timeval tv; struct timeval tv;
tv.tv_sec = time(NULL); tv.tv_sec = time(NULL);
tv.tv_usec = 0; tv.tv_usec = 0;
perhapsSetRTC(RTCQualityDevice, &tv); perhapsSetRTC(RTCQualityNTP, &tv);
#endif #endif
powerMonInit(); powerMonInit();
@@ -429,16 +406,6 @@ void setup()
initDeepSleep(); initDeepSleep();
#if defined(MODEM_POWER_EN)
pinMode(MODEM_POWER_EN, OUTPUT);
digitalWrite(MODEM_POWER_EN, LOW);
#endif
#if defined(MODEM_PWRKEY)
pinMode(MODEM_PWRKEY, OUTPUT);
digitalWrite(MODEM_PWRKEY, LOW);
#endif
#if defined(LORA_TCXO_GPIO) #if defined(LORA_TCXO_GPIO)
pinMode(LORA_TCXO_GPIO, OUTPUT); pinMode(LORA_TCXO_GPIO, OUTPUT);
digitalWrite(LORA_TCXO_GPIO, HIGH); digitalWrite(LORA_TCXO_GPIO, HIGH);
@@ -741,7 +708,6 @@ void setup()
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::SCD4X, meshtastic_TelemetrySensorType_SCD4X);
scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::TSL2561, meshtastic_TelemetrySensorType_TSL2561);
i2cScanner.reset(); i2cScanner.reset();
#endif #endif
@@ -831,7 +797,7 @@ void setup()
#endif #endif
#endif #endif
#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #ifdef T_WATCH_S3
drv.begin(); drv.begin();
drv.selectLibrary(1); drv.selectLibrary(1);
// I2C trigger by sending 'go' command // I2C trigger by sending 'go' command
@@ -877,7 +843,7 @@ void setup()
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS)
screen = new graphics::Screen(screen_found, screen_model, screen_geometry); screen = new graphics::Screen(screen_found, screen_model, screen_geometry);
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) &&
@@ -1140,7 +1106,7 @@ void setup()
// Don't call screen setup until after nodedb is setup (because we need // Don't call screen setup until after nodedb is setup (because we need
// the current region name) // the current region name)
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \
defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS)
if (screen) if (screen)
screen->setup(); screen->setup();
#elif defined(ARCH_PORTDUINO) #elif defined(ARCH_PORTDUINO)
@@ -1448,6 +1414,10 @@ void setup()
#endif #endif
#endif #endif
#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2)
osk_found = true;
#endif
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER
// Start web server thread. // Start web server thread.
webServerThread = new WebServerThread(); webServerThread = new WebServerThread();
@@ -1526,7 +1496,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG;
#endif #endif
#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION #if NO_EXT_GPIO
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG;
#endif #endif
// Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts

View File

@@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found;
extern ScanI2C::DeviceAddress cardkb_found; extern ScanI2C::DeviceAddress cardkb_found;
extern uint8_t kb_model; extern uint8_t kb_model;
extern bool kb_found; extern bool kb_found;
extern bool osk_found;
extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress rtc_found;
extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::DeviceAddress accelerometer_found;
extern ScanI2C::FoundDevice rgb_found; extern ScanI2C::FoundDevice rgb_found;
@@ -41,7 +42,7 @@ extern bool eink_found;
extern bool pmu_found; extern bool pmu_found;
extern bool isUSBPowered; extern bool isUSBPowered;
#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #ifdef T_WATCH_S3
#include <Adafruit_DRV2605.h> #include <Adafruit_DRV2605.h>
extern Adafruit_DRV2605 drv; extern Adafruit_DRV2605 drv;
#endif #endif

View File

@@ -89,15 +89,3 @@ uint32_t MemGet::getPsramSize()
return 0; return 0;
#endif #endif
} }
void displayPercentHeapFree()
{
uint32_t freeHeap = memGet.getFreeHeap();
uint32_t totalHeap = memGet.getHeapSize();
if (totalHeap == 0 || totalHeap == UINT32_MAX) {
LOG_INFO("Heap size unavailable");
return;
}
int percent = (int)((freeHeap * 100) / totalHeap);
LOG_INFO("Heap free: %d%% (%u/%u bytes)", percent, freeHeap, totalHeap);
}

View File

@@ -368,7 +368,7 @@ const char *Channels::getName(size_t chIndex)
// Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case
// the app effed up and forgot to set channelSettings.name // the app effed up and forgot to set channelSettings.name
if (config.lora.use_preset) { if (config.lora.use_preset) {
channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
} else { } else {
channelName = "Custom"; channelName = "Custom";
} }
@@ -382,8 +382,7 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex)
const auto &ch = getByIndex(chIndex); const auto &ch = getByIndex(chIndex);
if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) {
const char *name = getName(chIndex); const char *name = getName(chIndex);
const char *presetName = const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
// Check if the name is the default derived from the modem preset // Check if the name is the default derived from the modem preset
if (strcmp(name, presetName) == 0) if (strcmp(name, presetName) == 0)
return true; return true;

View File

@@ -6,7 +6,6 @@
#include <memory> #include <memory>
#include "PointerQueue.h" #include "PointerQueue.h"
#include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP
template <class T> class Allocator template <class T> class Allocator
{ {
@@ -15,14 +14,13 @@ template <class T> class Allocator
Allocator() : deleter([this](T *p) { this->release(p); }) {} Allocator() : deleter([this](T *p) { this->release(p); }) {}
virtual ~Allocator() {} virtual ~Allocator() {}
/// Return a queable object which has been prefilled with zeros. Return nullptr if no buffer is available /// Return a queable object which has been prefilled with zeros. Panic if no buffer is available
/// Note: this method is safe to call from regular OR ISR code /// Note: this method is safe to call from regular OR ISR code
T *allocZeroed() T *allocZeroed()
{ {
T *p = allocZeroed(0); T *p = allocZeroed(0);
if (!p) {
LOG_WARN("Failed to allocate zeroed memory"); assert(p); // FIXME panic instead
}
return p; return p;
} }
@@ -41,11 +39,9 @@ template <class T> class Allocator
T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY)
{ {
T *p = alloc(maxWait); T *p = alloc(maxWait);
if (!p) { assert(p);
LOG_WARN("Failed to allocate memory for copy");
return nullptr;
}
if (p)
*p = src; *p = src;
return p; return p;
} }
@@ -87,11 +83,7 @@ template <class T> class MemoryDynamic : public Allocator<T>
/// Return a buffer for use by others /// Return a buffer for use by others
virtual void release(T *p) override virtual void release(T *p) override
{ {
if (p == nullptr) assert(p);
return;
LOG_HEAP("Freeing 0x%x", p);
free(p); free(p);
} }
@@ -104,58 +96,3 @@ template <class T> class MemoryDynamic : public Allocator<T>
return p; return p;
} }
}; };
/**
* A static memory pool that uses a fixed buffer instead of heap allocation
*/
template <class T, int MaxSize> class MemoryPool : public Allocator<T>
{
private:
T pool[MaxSize];
bool used[MaxSize];
public:
MemoryPool() : pool{}, used{}
{
// Arrays are now zero-initialized by member initializer list
// pool array: all elements are default-constructed (zero for POD types)
// used array: all elements are false (zero-initialized)
}
/// Return a buffer for use by others
virtual void release(T *p) override
{
if (!p) {
LOG_DEBUG("Failed to release memory, pointer is null");
return;
}
// Find the index of this pointer in our pool
int index = p - pool;
if (index >= 0 && index < MaxSize) {
assert(used[index]); // Should be marked as used
used[index] = false;
LOG_HEAP("Released static pool item %d at 0x%x", index, p);
} else {
LOG_WARN("Pointer 0x%x not from our pool!", p);
}
}
protected:
// Alloc some storage from our static pool
virtual T *alloc(TickType_t maxWait) override
{
// Find first free slot
for (int i = 0; i < MaxSize; i++) {
if (!used[i]) {
used[i] = true;
LOG_HEAP("Allocated static pool item %d at 0x%x", i, &pool[i]);
return &pool[i];
}
}
// No free slots available - return nullptr instead of asserting
LOG_WARN("No free slots available in static memory pool!");
return nullptr;
}
};

View File

@@ -85,8 +85,11 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e
return r; return r;
} }
void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char *specificModule)
{ {
if (specificModule) {
LOG_DEBUG("Calling specific module: %s", specificModule);
}
// LOG_DEBUG("In call modules"); // LOG_DEBUG("In call modules");
bool moduleFound = false; bool moduleFound = false;
@@ -104,6 +107,11 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src)
for (auto i = modules->begin(); i != modules->end(); ++i) { for (auto i = modules->begin(); i != modules->end(); ++i) {
auto &pi = **i; auto &pi = **i;
// If specificModule is provided, only call that specific module
if (specificModule && (!pi.name || strcmp(pi.name, specificModule) != 0)) {
continue;
}
pi.currentRequest = &mp; pi.currentRequest = &mp;
/// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious)

View File

@@ -73,7 +73,7 @@ class MeshModule
/** For use only by MeshService /** For use only by MeshService
*/ */
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO, const char *specificModule = nullptr);
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex); static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
static void observeUIEvents(Observer<const UIFrameEvent *> *observer); static void observeUIEvents(Observer<const UIFrameEvent *> *observer);

View File

@@ -46,14 +46,11 @@ the new node can build its node db)
MeshService *service; MeshService *service;
#define MAX_MQTT_PROXY_MESSAGES 16 static MemoryDynamic<meshtastic_MqttClientProxyMessage> staticMqttClientProxyMessagePool;
static MemoryPool<meshtastic_MqttClientProxyMessage, MAX_MQTT_PROXY_MESSAGES> staticMqttClientProxyMessagePool;
#define MAX_QUEUE_STATUS 4 static MemoryDynamic<meshtastic_QueueStatus> staticQueueStatusPool;
static MemoryPool<meshtastic_QueueStatus, MAX_QUEUE_STATUS> staticQueueStatusPool;
#define MAX_CLIENT_NOTIFICATIONS 4 static MemoryDynamic<meshtastic_ClientNotification> staticClientNotificationPool;
static MemoryPool<meshtastic_ClientNotification, MAX_CLIENT_NOTIFICATIONS> staticClientNotificationPool;
Allocator<meshtastic_MqttClientProxyMessage> &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; Allocator<meshtastic_MqttClientProxyMessage> &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool;
@@ -64,10 +61,8 @@ Allocator<meshtastic_QueueStatus> &queueStatusPool = staticQueueStatusPool;
#include "Router.h" #include "Router.h"
MeshService::MeshService() MeshService::MeshService()
#ifdef ARCH_PORTDUINO : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE),
: toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_QUEUESTATUS_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_TOPHONE / 2)
toPhoneMqttProxyQueue(MAX_RX_MQTTPROXY_TOPHONE), toPhoneClientNotificationQueue(MAX_RX_NOTIFICATION_TOPHONE)
#endif
{ {
lastQueueStatus = {0, 0, 16, 0}; lastQueueStatus = {0, 0, 16, 0};
} }
@@ -196,10 +191,8 @@ void MeshService::handleToRadio(meshtastic_MeshPacket &p)
// (so we update our nodedb for the local node) // (so we update our nodedb for the local node)
// Send the packet into the mesh // Send the packet into the mesh
DEBUG_HEAP_BEFORE;
auto a = packetPool.allocCopy(p); sendToMesh(packetPool.allocCopy(p), RX_SRC_USER);
DEBUG_HEAP_AFTER("MeshService::handleToRadio", a);
sendToMesh(a, RX_SRC_USER);
bool loopback = false; // if true send any packet the phone sends back itself (for testing) bool loopback = false; // if true send any packet the phone sends back itself (for testing)
if (loopback) { if (loopback) {
@@ -255,11 +248,7 @@ void MeshService::sendToMesh(meshtastic_MeshPacket *p, RxSource src, bool ccToPh
} }
if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent if ((res == ERRNO_OK || res == ERRNO_SHOULD_RELEASE) && ccToPhone) { // Check if p is not released in case it couldn't be sent
DEBUG_HEAP_BEFORE; sendToPhone(packetPool.allocCopy(*p));
auto a = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("MeshService::sendToMesh", a);
sendToPhone(a);
} }
// Router may ask us to release the packet if it wasn't sent // Router may ask us to release the packet if it wasn't sent

View File

@@ -9,12 +9,7 @@
#include "MeshRadio.h" #include "MeshRadio.h"
#include "MeshTypes.h" #include "MeshTypes.h"
#include "Observer.h" #include "Observer.h"
#ifdef ARCH_PORTDUINO
#include "PointerQueue.h" #include "PointerQueue.h"
#else
#include "StaticPointerQueue.h"
#endif
#include "mesh-pb-constants.h"
#if defined(ARCH_PORTDUINO) #if defined(ARCH_PORTDUINO)
#include "../platform/portduino/SimRadio.h" #include "../platform/portduino/SimRadio.h"
#endif #endif
@@ -42,32 +37,16 @@ class MeshService
/// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure /// FIXME, change to a DropOldestQueue and keep a count of the number of dropped packets to ensure
/// we never hang because android hasn't been there in a while /// we never hang because android hasn't been there in a while
/// FIXME - save this to flash on deep sleep /// FIXME - save this to flash on deep sleep
#ifdef ARCH_PORTDUINO
PointerQueue<meshtastic_MeshPacket> toPhoneQueue; PointerQueue<meshtastic_MeshPacket> toPhoneQueue;
#else
StaticPointerQueue<meshtastic_MeshPacket, MAX_RX_TOPHONE> toPhoneQueue;
#endif
// keep list of QueueStatus packets to be send to the phone // keep list of QueueStatus packets to be send to the phone
#ifdef ARCH_PORTDUINO
PointerQueue<meshtastic_QueueStatus> toPhoneQueueStatusQueue; PointerQueue<meshtastic_QueueStatus> toPhoneQueueStatusQueue;
#else
StaticPointerQueue<meshtastic_QueueStatus, MAX_RX_QUEUESTATUS_TOPHONE> toPhoneQueueStatusQueue;
#endif
// keep list of MqttClientProxyMessages to be send to the client for delivery // keep list of MqttClientProxyMessages to be send to the client for delivery
#ifdef ARCH_PORTDUINO
PointerQueue<meshtastic_MqttClientProxyMessage> toPhoneMqttProxyQueue; PointerQueue<meshtastic_MqttClientProxyMessage> toPhoneMqttProxyQueue;
#else
StaticPointerQueue<meshtastic_MqttClientProxyMessage, MAX_RX_MQTTPROXY_TOPHONE> toPhoneMqttProxyQueue;
#endif
// keep list of ClientNotifications to be send to the client (phone) // keep list of ClientNotifications to be send to the client (phone)
#ifdef ARCH_PORTDUINO
PointerQueue<meshtastic_ClientNotification> toPhoneClientNotificationQueue; PointerQueue<meshtastic_ClientNotification> toPhoneClientNotificationQueue;
#else
StaticPointerQueue<meshtastic_ClientNotification, MAX_RX_NOTIFICATION_TOPHONE> toPhoneClientNotificationQueue;
#endif
// This holds the last QueueStatus send // This holds the last QueueStatus send
meshtastic_QueueStatus lastQueueStatus; meshtastic_QueueStatus lastQueueStatus;

View File

@@ -34,11 +34,8 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
bool weWereNextHop = false; bool weWereNextHop = false;
if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record if (wasSeenRecently(p, true, &wasFallback, &weWereNextHop)) { // Note: this will also add a recent packet record
printPacket("Ignore dupe incoming msg", p); printPacket("Ignore dupe incoming msg", p);
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
rxDupe++; rxDupe++;
stopRetransmission(p->from, p->id); stopRetransmission(p->from, p->id);
}
// If it was a fallback to flooding, try to relay again // If it was a fallback to flooding, try to relay again
if (wasFallback) { if (wasFallback) {
@@ -74,11 +71,10 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
if (p->from != 0) { if (p->from != 0) {
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from); meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
if (origTx) { if (origTx) {
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came directly // Either relayer of ACK was also a relayer of the packet, or we were the relayer and the ACK came directly from
// from the destination // the destination
if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) || if (wasRelayer(p->relay_node, p->decoded.request_id, p->to) ||
(p->hop_start != 0 && p->hop_start == p->hop_limit && (wasRelayer(ourRelayID, p->decoded.request_id, p->to) && p->hop_start != 0 && p->hop_start == p->hop_limit)) {
wasSoleRelayer(ourRelayID, p->decoded.request_id, p->to))) {
if (origTx->next_hop != p->relay_node) { // Not already set if (origTx->next_hop != p->relay_node) { // Not already set
LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node); LOG_INFO("Update next hop of 0x%x to 0x%x based on ACK/reply", p->from, p->relay_node);
origTx->next_hop = p->relay_node; origTx->next_hop = p->relay_node;
@@ -175,18 +171,12 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key)
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
// remove the 'original' (identified by originator and packet->id) from the txqueue and free it // remove the 'original' (identified by originator and packet->id) from the txqueue and free it
cancelSending(getFrom(p), p->id); cancelSending(getFrom(p), p->id);
// now free the pooled copy for retransmission too
packetPool.release(p);
} }
} }
// Regardless of whether or not we canceled this packet from the txQueue, remove it from our pending list so it doesn't
// get scheduled again. (This is the core of stopRetransmission.)
auto numErased = pending.erase(key); auto numErased = pending.erase(key);
assert(numErased == 1); assert(numErased == 1);
// When we remove an entry from pending, always be sure to release the copy of the packet that was allocated in the call
// to startRetransmission.
packetPool.release(p);
return true; return true;
} else } else
return false; return false;

View File

@@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false)
config.bluetooth.fixed_pin = defaultBLEPin; config.bluetooth.fixed_pin = defaultBLEPin;
#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS)
bool hasScreen = true; bool hasScreen = true;
#ifdef HELTEC_MESH_NODE_T114 #ifdef HELTEC_MESH_NODE_T114
uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET);
@@ -830,15 +830,6 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.alert_message = true;
moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.output_ms = 1000;
moduleConfig.external_notification.nag_timeout = 60; moduleConfig.external_notification.nag_timeout = 60;
#endif
#ifdef T_LORA_PAGER
moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B;
moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS;
moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28);
moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29);
moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
#endif #endif
moduleConfig.has_canned_message = true; moduleConfig.has_canned_message = true;
#if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT
@@ -1711,10 +1702,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde
/// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw
void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
{ {
if (mp.from == getNodeNum()) { // if (mp.from == getNodeNum()) {
LOG_DEBUG("Ignore update from self"); // LOG_DEBUG("Ignore update from self");
return; // return;
} // }
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time);

View File

@@ -294,7 +294,7 @@ void PacketHistory::insert(const PacketRecord &r)
/* Check if a certain node was a relayer of a packet in the history given an ID and sender /* Check if a certain node was a relayer of a packet in the history given an ID and sender
* @return true if node was indeed a relayer, false if not */ * @return true if node was indeed a relayer, false if not */
bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole) bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
{ {
if (!initOk()) { if (!initOk()) {
LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!");
@@ -322,42 +322,27 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1],
found->relayed_by[2], relayer); found->relayed_by[2], relayer);
#endif #endif
return wasRelayer(relayer, *found, wasSole); return wasRelayer(relayer, *found);
} }
/* Check if a certain node was a relayer of a packet in the history given iterator /* Check if a certain node was a relayer of a packet in the history given iterator
* @return true if node was indeed a relayer, false if not */ * @return true if node was indeed a relayer, false if not */
bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole) bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r)
{ {
bool found = false; for (uint8_t i = 0; i < NUM_RELAYERS; i++) {
bool other_present = false;
for (uint8_t i = 0; i < NUM_RELAYERS; ++i) {
if (r.relayed_by[i] == relayer) { if (r.relayed_by[i] == relayer) {
found = true; #if VERBOSE_PACKET_HISTORY
} else if (r.relayed_by[i] != 0) { LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id,
other_present = true; r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer);
#endif
return true;
} }
} }
if (wasSole) {
*wasSole = (found && !other_present);
}
#if VERBOSE_PACKET_HISTORY #if VERBOSE_PACKET_HISTORY
LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0], LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? NO", r.sender, r.id, r.relayed_by[0],
r.relayed_by[1], r.relayed_by[2], relayer); r.relayed_by[1], r.relayed_by[2], relayer);
#endif #endif
return false;
return found;
}
// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender
bool PacketHistory::wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender)
{
bool wasSole = false;
wasRelayer(relayer, id, sender, &wasSole);
return wasSole;
} }
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender // Remove a relayer from the list of relayers of a packet in the history given an ID and sender

View File

@@ -34,9 +34,8 @@ class PacketHistory
void insert(const PacketRecord &r); // Insert or replace a packet record in the history void insert(const PacketRecord &r); // Insert or replace a packet record in the history
/* Check if a certain node was a relayer of a packet in the history given iterator /* Check if a certain node was a relayer of a packet in the history given iterator
* If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
* @return true if node was indeed a relayer, false if not */ * @return true if node was indeed a relayer, false if not */
bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr); bool wasRelayer(const uint8_t relayer, const PacketRecord &r);
PacketHistory(const PacketHistory &); // non construction-copyable PacketHistory(const PacketHistory &); // non construction-copyable
PacketHistory &operator=(const PacketHistory &); // non copyable PacketHistory &operator=(const PacketHistory &); // non copyable
@@ -55,12 +54,8 @@ class PacketHistory
bool *weWereNextHop = nullptr); bool *weWereNextHop = nullptr);
/* Check if a certain node was a relayer of a packet in the history given an ID and sender /* Check if a certain node was a relayer of a packet in the history given an ID and sender
* If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
* @return true if node was indeed a relayer, false if not */ * @return true if node was indeed a relayer, false if not */
bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr); bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
// Check if a certain node was the *only* relayer of a packet in the history given an ID and sender
bool wasSoleRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);
// Remove a relayer from the list of relayers of a packet in the history given an ID and sender // Remove a relayer from the list of relayers of a packet in the history given an ID and sender
void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);

View File

@@ -100,7 +100,6 @@ void PhoneAPI::close()
config_nonce = 0; config_nonce = 0;
config_state = 0; config_state = 0;
pauseBluetoothLogging = false; pauseBluetoothLogging = false;
heartbeatReceived = false;
} }
} }

View File

@@ -586,8 +586,7 @@ void RadioInterface::applyModemConfig()
// Check if we use the default frequency slot // Check if we use the default frequency slot
RadioInterface::uses_default_frequency_slot = RadioInterface::uses_default_frequency_slot =
channel_num == channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels;
hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels;
// Old frequency selection formula // Old frequency selection formula
// float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num);

View File

@@ -2,7 +2,6 @@
#include "Default.h" #include "Default.h"
#include "MeshTypes.h" #include "MeshTypes.h"
#include "configuration.h" #include "configuration.h"
#include "memGet.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "modules/NodeInfoModule.h" #include "modules/NodeInfoModule.h"
#include "modules/RoutingModule.h" #include "modules/RoutingModule.h"
@@ -22,10 +21,8 @@ ErrorCode ReliableRouter::send(meshtastic_MeshPacket *p)
if (p->hop_limit == 0) { if (p->hop_limit == 0) {
p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); p->hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
} }
DEBUG_HEAP_BEFORE;
auto copy = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("ReliableRouter::send", copy);
auto copy = packetPool.allocCopy(*p);
startRetransmission(copy, NUM_RELIABLE_RETX); startRetransmission(copy, NUM_RELIABLE_RETX);
} }
@@ -61,10 +58,7 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
// marked as wantAck // marked as wantAck
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel); sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, old->packet->channel);
// Only stop retransmissions if the rebroadcast came via LoRa
if (p->transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA) {
stopRetransmission(key); stopRetransmission(key);
}
} else { } else {
LOG_DEBUG("Didn't find pending packet"); LOG_DEBUG("Didn't find pending packet");
} }

View File

@@ -5,7 +5,6 @@
#include "MeshService.h" #include "MeshService.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "RTC.h" #include "RTC.h"
#include "configuration.h" #include "configuration.h"
#include "detect/LoRaRadioType.h" #include "detect/LoRaRadioType.h"
#include "main.h" #include "main.h"
@@ -28,24 +27,14 @@
// I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX // I think this is right, one packet for each of the three fifos + one packet being currently assembled for TX or RX
// And every TX packet might have a retransmission packet or an ack alive at any moment // And every TX packet might have a retransmission packet or an ack alive at any moment
#ifdef ARCH_PORTDUINO
// Portduino (native) targets can use dynamic memory pools with runtime-configurable sizes
#define MAX_PACKETS \ #define MAX_PACKETS \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending) 2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryDynamic<meshtastic_MeshPacket> dynamicPool; // static MemoryPool<MeshPacket> staticPool(MAX_PACKETS);
Allocator<meshtastic_MeshPacket> &packetPool = dynamicPool; static MemoryDynamic<meshtastic_MeshPacket> staticPool;
#else
// Embedded targets use static memory pools with compile-time constants
#define MAX_PACKETS_STATIC \
(MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \
2) // max number of packets which can be in flight (either queued from reception or queued for sending)
static MemoryPool<meshtastic_MeshPacket, MAX_PACKETS_STATIC> staticPool;
Allocator<meshtastic_MeshPacket> &packetPool = staticPool; Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
#endif
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
@@ -286,10 +275,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p)
// If the packet is not yet encrypted, do so now // If the packet is not yet encrypted, do so now
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it
DEBUG_HEAP_BEFORE;
meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p); meshtastic_MeshPacket *p_decoded = packetPool.allocCopy(*p);
DEBUG_HEAP_AFTER("Router::send", p_decoded);
auto encodeResult = perhapsEncode(p); auto encodeResult = perhapsEncode(p);
if (encodeResult != meshtastic_Routing_Error_NONE) { if (encodeResult != meshtastic_Routing_Error_NONE) {
@@ -543,9 +529,8 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
#endif #endif
// Don't use PKC with Ham mode // Don't use PKC with Ham mode
!owner.is_licensed && !owner.is_licensed &&
// Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested // Don't use PKC if it's not explicitly requested and a non-primary channel is requested
!(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || !(p->pki_encrypted != true && p->channel > 0) &&
strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) &&
// Check for valid keys and single node destination // Check for valid keys and single node destination
config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr &&
// Check for a known public key for the destination // Check for a known public key for the destination
@@ -576,7 +561,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Now that we are encrypting the packet channel should be the hash (no longer the index) // Now that we are encrypting the packet channel should be the hash (no longer the index)
p->channel = hash; p->channel = hash;
if (hash < 0) { if (hash < 0) {
// No suitable channel could be found for // No suitable channel could be found for sending
return meshtastic_Routing_Error_NO_CHANNEL; return meshtastic_Routing_Error_NO_CHANNEL;
} }
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
@@ -592,7 +577,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// Now that we are encrypting the packet channel should be the hash (no longer the index) // Now that we are encrypting the packet channel should be the hash (no longer the index)
p->channel = hash; p->channel = hash;
if (hash < 0) { if (hash < 0) {
// No suitable channel could be found for // No suitable channel could be found for sending
return meshtastic_Routing_Error_NO_CHANNEL; return meshtastic_Routing_Error_NO_CHANNEL;
} }
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
@@ -621,11 +606,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
bool skipHandle = false; bool skipHandle = false;
// Also, we should set the time from the ISR and it should have msec level resolution // 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 p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
// Store a copy of encrypted packet for MQTT // Store a copy of encrypted packet for MQTT
DEBUG_HEAP_BEFORE;
meshtastic_MeshPacket *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 // Take those raw bytes and convert them back into a well structured protobuf we can understand
auto decodedState = perhapsDecode(p); auto decodedState = perhapsDecode(p);
@@ -673,7 +655,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
// call modules here // call modules here
// If this could be a spoofed packet, don't let the modules see it. // If this could be a spoofed packet, don't let the modules see it.
if (!skipHandle) { if (!skipHandle && p->from != nodeDB->getNodeNum()) {
MeshModule::callModules(*p, src); MeshModule::callModules(*p, src);
#if !MESHTASTIC_EXCLUDE_MQTT #if !MESHTASTIC_EXCLUDE_MQTT
@@ -687,6 +669,8 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
!isFromUs(p) && mqtt) !isFromUs(p) && mqtt)
mqtt->onSend(*p_encrypted, *p, p->channel); mqtt->onSend(*p_encrypted, *p, p->channel);
#endif #endif
} else if (p->from == nodeDB->getNodeNum() && !skipHandle) {
MeshModule::callModules(*p, src, ROUTING_MODULE);
} }
packetPool.release(p_encrypted); // Release the encrypted packet packetPool.release(p_encrypted); // Release the encrypted packet

View File

@@ -1,13 +1,13 @@
#include "configuration.h"
#ifdef ARCH_STM32WL
#include "STM32WLE5JCInterface.h" #include "STM32WLE5JCInterface.h"
#include "configuration.h"
#include "error.h" #include "error.h"
#ifndef STM32WLx_MAX_POWER #ifndef STM32WLx_MAX_POWER
#define STM32WLx_MAX_POWER 22 #define STM32WLx_MAX_POWER 22
#endif #endif
#ifdef ARCH_STM32WL
STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, STM32WLE5JCInterface::STM32WLE5JCInterface(LockingArduinoHal *hal, RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq,
RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy) RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy)
: SX126xInterface(hal, cs, irq, rst, busy) : SX126xInterface(hal, cs, irq, rst, busy)

View File

@@ -1,8 +1,8 @@
#pragma once #pragma once
#ifdef ARCH_STM32WL
#include "SX126xInterface.h" #include "SX126xInterface.h"
#include "rfswitch.h"
#ifdef ARCH_STM32WL
/** /**
* Our adapter for STM32WLE5JC radios * Our adapter for STM32WLE5JC radios
@@ -16,4 +16,13 @@ class STM32WLE5JCInterface : public SX126xInterface<STM32WLx>
virtual bool init() override; virtual bool init() override;
}; };
/* https://wiki.seeedstudio.com/LoRa-E5_STM32WLE5JC_Module/
* Wio-E5 module ONLY transmits through RFO_HP
* Receive: PA4=1, PA5=0
* Transmit(high output power, SMPS mode): PA4=0, PA5=1 */
static const RADIOLIB_PIN_TYPE rfswitch_pins[5] = {PA4, PA5, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC};
static const Module::RfSwitchMode_t rfswitch_table[4] = {
{STM32WLx::MODE_IDLE, {LOW, LOW}}, {STM32WLx::MODE_RX, {HIGH, LOW}}, {STM32WLx::MODE_TX_HP, {LOW, HIGH}}, END_OF_MODE_TABLE};
#endif // ARCH_STM32WL #endif // ARCH_STM32WL

View File

@@ -1,77 +0,0 @@
#pragma once
#include "concurrency/OSThread.h"
#include "freertosinc.h"
#include <cassert>
/**
* A static circular buffer queue for pointers.
* This provides the same interface as PointerQueue but uses a statically allocated
* buffer instead of dynamic allocation.
*/
template <class T, int MaxElements> class StaticPointerQueue
{
static_assert(MaxElements > 0, "MaxElements must be greater than 0");
T *buffer[MaxElements];
int head = 0;
int tail = 0;
int count = 0;
concurrency::OSThread *reader = nullptr;
public:
StaticPointerQueue()
{
// Initialize all buffer elements to nullptr to silence warnings and ensure clean state
for (int i = 0; i < MaxElements; i++) {
buffer[i] = nullptr;
}
}
int numFree() const { return MaxElements - count; }
bool isEmpty() const { return count == 0; }
int numUsed() const { return count; }
bool enqueue(T *x, TickType_t maxWait = portMAX_DELAY)
{
if (count >= MaxElements) {
return false; // Queue is full
}
if (reader) {
reader->setInterval(0);
concurrency::mainDelay.interrupt();
}
buffer[tail] = x;
tail = (tail + 1) % MaxElements;
count++;
return true;
}
bool dequeue(T **p, TickType_t maxWait = portMAX_DELAY)
{
if (count == 0) {
return false; // Queue is empty
}
*p = buffer[head];
head = (head + 1) % MaxElements;
count--;
return true;
}
// returns a ptr or null if the queue was empty
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY)
{
T *p;
return dequeue(&p, maxWait) ? p : nullptr;
}
void setReader(concurrency::OSThread *t) { reader = t; }
// For compatibility with PointerQueue interface
int getMaxLen() const { return MaxElements; }
};

View File

@@ -16,95 +16,6 @@ int32_t StreamAPI::runOncePart()
return result; return result;
} }
int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen)
{
auto result = readStream(buf, bufLen);
writeStream();
checkConnectionTimeout();
return result;
}
/**
* Read any rx chars from the link and call handleRecStream
*/
int32_t StreamAPI::readStream(char *buf, uint16_t bufLen)
{
if (bufLen < 1) {
// Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time
bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000);
return recentRx ? 5 : 250;
} else {
handleRecStream(buf, bufLen);
// we had bytes available this time, so assume we might have them next time also
lastRxMsec = millis();
return 0;
}
}
/**
* call getFromRadio() and deliver encapsulated packets to the Stream
*/
void StreamAPI::writeStream()
{
if (canWrite) {
uint32_t len;
do {
// Send every packet we can
len = getFromRadio(txBuf + HEADER_LEN);
emitTxBuffer(len);
} while (len);
}
}
int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen)
{
uint16_t index = 0;
while (bufLen > index) { // Currently we never want to block
int cInt = buf[index++];
if (cInt < 0)
break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit
// arduino
uint8_t c = (uint8_t)cInt;
// Use the read pointer for a little state machine, first look for framing, then length bytes, then payload
size_t ptr = rxPtr;
rxPtr++; // assume we will probably advance the rxPtr
rxBuf[ptr] = c; // store all bytes (including framing)
// console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c);
if (ptr == 0) { // looking for START1
if (c != START1)
rxPtr = 0; // failed to find framing
} else if (ptr == 1) { // looking for START2
if (c != START2)
rxPtr = 0; // failed to find framing
} else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing
uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing
// console->printf("len %d\n", len);
if (ptr == HEADER_LEN - 1) {
// we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid
// protobuf also)
if (len > MAX_TO_FROM_RADIO_SIZE)
rxPtr = 0; // length is bogus, restart search for framing
}
if (rxPtr != 0) // Is packet still considered 'good'?
if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload?
rxPtr = 0; // start over again on the next packet
// If we didn't just fail the packet and we now have the right # of bytes, parse it
handleToRadio(rxBuf + HEADER_LEN, len);
}
}
}
return 0;
}
/** /**
* Read any rx chars from the link and call handleToRadio * Read any rx chars from the link and call handleToRadio
*/ */
@@ -165,6 +76,21 @@ int32_t StreamAPI::readStream()
} }
} }
/**
* call getFromRadio() and deliver encapsulated packets to the Stream
*/
void StreamAPI::writeStream()
{
if (canWrite) {
uint32_t len;
do {
// Send every packet we can
len = getFromRadio(txBuf + HEADER_LEN);
emitTxBuffer(len);
} while (len);
}
}
/** /**
* Send the current txBuffer over our stream * Send the current txBuffer over our stream
*/ */

View File

@@ -50,15 +50,12 @@ class StreamAPI : public PhoneAPI
* phone. * phone.
*/ */
virtual int32_t runOncePart(); virtual int32_t runOncePart();
virtual int32_t runOncePart(char *buf,uint16_t bufLen);
private: private:
/** /**
* Read any rx chars from the link and call handleToRadio * Read any rx chars from the link and call handleToRadio
*/ */
int32_t readStream(); int32_t readStream();
int32_t readStream(char *buf,uint16_t bufLen);
int32_t handleRecStream(char *buf,uint16_t bufLen);
/** /**
* call getFromRadio() and deliver encapsulated packets to the Stream * call getFromRadio() and deliver encapsulated packets to the Stream

View File

@@ -64,12 +64,7 @@ typedef enum _meshtastic_Config_DeviceConfig_Role {
in areas not already covered by other routers, or to bridge around problematic terrain, in areas not already covered by other routers, or to bridge around problematic terrain,
but should not be given priority over other routers in order to avoid unnecessaraily but should not be given priority over other routers in order to avoid unnecessaraily
consuming hops. */ consuming hops. */
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE = 11
/* Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT.
Technical Details: Used for stronger attic/roof nodes to distribute messages more widely
from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes
where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. */
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE = 12
} meshtastic_Config_DeviceConfig_Role; } meshtastic_Config_DeviceConfig_Role;
/* Defines the device's behavior for how messages are rebroadcast */ /* Defines the device's behavior for how messages are rebroadcast */
@@ -651,8 +646,8 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT #define _meshtastic_Config_DeviceConfig_Role_MIN meshtastic_Config_DeviceConfig_Role_CLIENT
#define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_CLIENT_BASE #define _meshtastic_Config_DeviceConfig_Role_MAX meshtastic_Config_DeviceConfig_Role_ROUTER_LATE
#define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_CLIENT_BASE+1)) #define _meshtastic_Config_DeviceConfig_Role_ARRAYSIZE ((meshtastic_Config_DeviceConfig_Role)(meshtastic_Config_DeviceConfig_Role_ROUTER_LATE+1))
#define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN meshtastic_Config_DeviceConfig_RebroadcastMode_ALL
#define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY

View File

@@ -66,8 +66,6 @@ typedef enum _meshtastic_Language {
meshtastic_Language_UKRAINIAN = 16, meshtastic_Language_UKRAINIAN = 16,
/* Bulgarian */ /* Bulgarian */
meshtastic_Language_BULGARIAN = 17, meshtastic_Language_BULGARIAN = 17,
/* Czech */
meshtastic_Language_CZECH = 18,
/* Simplified Chinese (experimental) */ /* Simplified Chinese (experimental) */
meshtastic_Language_SIMPLIFIED_CHINESE = 30, meshtastic_Language_SIMPLIFIED_CHINESE = 30,
/* Traditional Chinese (experimental) */ /* Traditional Chinese (experimental) */

View File

@@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg;
/* Maximum encoded size of messages (where known) */ /* Maximum encoded size of messages (where known) */
/* meshtastic_NodeDatabase_size depends on runtime parameters */ /* meshtastic_NodeDatabase_size depends on runtime parameters */
#define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size
#define meshtastic_BackupPreferences_size 2273 #define meshtastic_BackupPreferences_size 2271
#define meshtastic_ChannelFile_size 718 #define meshtastic_ChannelFile_size 718
#define meshtastic_DeviceState_size 1737 #define meshtastic_DeviceState_size 1737
#define meshtastic_NodeInfoLite_size 196 #define meshtastic_NodeInfoLite_size 196

Some files were not shown because too many files have changed in this diff Show More