diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..8529eb7fc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +open_collective: meshtastic diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a15b34aae..0142c57a2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,7 @@ ## 🙏 Thank you for sending in a pull request, here's some tips to get started! ### ❌ (Please delete all these tips and replace them with your text) ❌ + - Before starting on some new big chunk of code, it it is optional but highly recommended to open an issue first to say "Hey, I think this idea X should be implemented and I'm starting work on it. My general plan is Y, any feedback is appreciated." This will allow other devs to potentially save you time by not accidentially duplicating work etc... @@ -15,12 +16,12 @@ - If you do not have the affected hardware to test your code changes adequately against regressions, please indicate this, so that contributors and commnunity members can help test your changes. - If your PR gets accepted you can request a "Contributor" role in the Meshtastic Discord - ## 🤝 Attestations + - [ ] I have tested that my proposed changes behave as described. - [ ] I have tested that my proposed changes do not cause any obvious regressions on the following devices: - [ ] Heltec (Lora32) V3 - - [ ] LilyGo T-Deck + - [ ] LilyGo T-Deck - [ ] LilyGo T-Beam - [ ] RAK WisBlock 4631 - [ ] Seeed Studio T-1000E tracker card diff --git a/.github/workflows/daily_packaging.yml b/.github/workflows/daily_packaging.yml index 11fe2043a..18939d567 100644 --- a/.github/workflows/daily_packaging.yml +++ b/.github/workflows/daily_packaging.yml @@ -1,7 +1,7 @@ name: Daily Packaging on: schedule: - - cron: 0 9 * * * + - cron: 0 2 * * * workflow_dispatch: push: branches: diff --git a/.github/workflows/release_channels.yml b/.github/workflows/release_channels.yml index 6f216b411..aac57fcbf 100644 --- a/.github/workflows/release_channels.yml +++ b/.github/workflows/release_channels.yml @@ -98,6 +98,7 @@ jobs: uses: peter-evans/create-pull-request@v7 with: base: ${{ github.event.repository.default_branch }} + branch: create-pull-request/bump-version title: Bump release version commit-message: automated bumps add-paths: | diff --git a/.github/workflows/update_protobufs.yml b/.github/workflows/update_protobufs.yml index 5aa295b89..ccdcc19ae 100644 --- a/.github/workflows/update_protobufs.yml +++ b/.github/workflows/update_protobufs.yml @@ -33,6 +33,7 @@ jobs: - name: Create pull request uses: peter-evans/create-pull-request@v7 with: + branch: create-pull-request/update-protobufs title: Update protobufs and classes add-paths: | protobufs diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 79bdf4778..b40f9458b 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -1,23 +1,24 @@ version: 0.1 cli: - version: 1.22.15 + version: 1.24.0 plugins: sources: - id: trunk - ref: v1.6.8 + ref: v1.7.0 uri: https://github.com/trunk-io/plugins lint: enabled: - - renovate@40.0.6 + - checkov@3.2.442 + - renovate@40.60.3 - prettier@3.5.3 - - trufflehog@3.88.29 + - trufflehog@3.89.2 - yamllint@1.37.1 - - bandit@1.8.3 - - trivy@0.62.1 + - bandit@1.8.5 + - trivy@0.63.0 - taplo@0.9.3 - - ruff@0.11.9 + - ruff@0.12.0 - isort@6.0.1 - - markdownlint@0.44.0 + - markdownlint@0.45.0 - oxipng@9.1.5 - svgo@3.3.2 - actionlint@1.7.7 @@ -27,7 +28,7 @@ lint: - shellcheck@0.10.0 - black@25.1.0 - git-diff-check - - gitleaks@8.26.0 + - gitleaks@8.27.2 - clang-format@16.0.3 ignore: - linters: [ALL] @@ -37,7 +38,7 @@ runtimes: enabled: - python@3.10.8 - go@1.21.0 - - node@18.20.5 + - node@22.16.0 actions: disabled: - trunk-announce diff --git a/README.md b/README.md index f34bf1839..a53fe9646 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,4 @@ Join our community and help improve Meshtastic! 🚀 ## Stats ![Alt](https://repobeats.axiom.co/api/embed/8025e56c482ec63541593cc5bd322c19d5c0bdcf.svg "Repobeats analytics image") + diff --git a/alpine.Dockerfile b/alpine.Dockerfile index bf7cad6d4..670736241 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -3,7 +3,7 @@ # trunk-ignore-all(hadolint/DL3018): Do not pin apk package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions -FROM python:3.13-alpine3.21 AS builder +FROM python:3.13-alpine3.22 AS builder ARG PIO_ENV=native ENV PIP_ROOT_USER_ACTION=ignore @@ -27,7 +27,7 @@ RUN bash ./bin/build-native.sh "$PIO_ENV" && \ # ##### PRODUCTION BUILD ############# -FROM alpine:3.21 +FROM alpine:3.22 LABEL org.opencontainers.image.title="Meshtastic" \ org.opencontainers.image.description="Alpine Meshtastic daemon" \ org.opencontainers.image.url="https://meshtastic.org" \ diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index a6eff7bf9..cba84181b 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -4,7 +4,7 @@ extends = arduino_base custom_esp32_kind = esp32 platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.10.0 + platformio/espressif32@6.11.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index a19c50319..429e010f5 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/622341c6de8a239704318b10c3dbb00c21a3eab3.zip + https://github.com/meshtastic/platform-native/archive/681ee029207e9fd040afa223df6e54074cbbe084.zip framework = arduino build_src_filter = diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index dd190c9d4..e7a340f92 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -2,7 +2,7 @@ extends = arduino_base platform = # renovate: datasource=custom.pio depName=platformio/ststm32 packageName=platformio/platform/ststm32 - platformio/ststm32@19.1.0 + platformio/ststm32@19.2.0 platform_packages = # TODO renovate platformio/framework-arduinoststm32@https://github.com/stm32duino/Arduino_Core_STM32/archive/2.10.1.zip diff --git a/bin/99-meshtasticd-udev.rules b/bin/99-meshtasticd-udev.rules index 69a468d7a..1151efafd 100644 --- a/bin/99-meshtasticd-udev.rules +++ b/bin/99-meshtasticd-udev.rules @@ -1,4 +1,7 @@ -# Set spidev ownership to 'spi' group. +# Set spidev ownership to 'spi' group SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" # Allow access to USB CH341 devices SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" +# Set gpio ownership to 'gpio' group +SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" +SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" diff --git a/bin/check-all.sh b/bin/check-all.sh index d1b50a8aa..29d6b5532 100755 --- a/bin/check-all.sh +++ b/bin/check-all.sh @@ -23,4 +23,4 @@ for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done -pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high +pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high diff --git a/bin/config-dist.yaml b/bin/config-dist.yaml index 55e8648d9..b40fb85a5 100644 --- a/bin/config-dist.yaml +++ b/bin/config-dist.yaml @@ -96,9 +96,9 @@ Lora: ### Some devices, like the pinedio, may require spidev0.1 as a workaround. # spidev: spidev0.0 -### Define GPIO buttons here: +### Deprecated location for User Button: -GPIO: +#GPIO: # User: 6 ### Define GPS @@ -115,17 +115,6 @@ I2C: Display: -### Waveshare 1.44inch LCD HAT -# Panel: ST7735S -# CS: 8 #Chip Select -# DC: 25 # Data/Command pin -# Backlight: 24 -# Width: 128 -# Height: 128 -# Reset: 27 -# OffsetX: 0 -# OffsetY: 0 - ### Adafruit PiTFT 2.8 TFT+Touchscreen # Panel: ILI9341 # CS: 8 @@ -180,6 +169,16 @@ Input: # KeyboardDevice: /dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd +### Standard User Button Config +# UserButton: 6 + +### Trackball/Joystick input +# TrackballUp: 6 +# TrackballDown: 19 +# TrackballLeft: 5 +# TrackballRight: 26 +# TrackballPress: 13 + ### Logging: diff --git a/bin/config.d/display-waveshare-1-44.yaml b/bin/config.d/display-waveshare-1-44.yaml new file mode 100644 index 000000000..1d85a4a3b --- /dev/null +++ b/bin/config.d/display-waveshare-1-44.yaml @@ -0,0 +1,26 @@ +### Waveshare 1.44inch LCD HAT +Display: + Panel: ST7735S + spidev: spidev0.0 # Specify either the spidev here, or the CS below +# CS: 8 #Chip Select # Optional, as this is the default pin for spidev0.0 + DC: 25 # Data/Command pin + Backlight: 24 + Width: 128 + Height: 128 + Reset: 27 + OffsetX: 2 + OffsetY: 1 + + +# OffsetY: 31 # These two options are used to properly flip the screen 180 degrees +# OffsetRotate: 3 + + +Input: + TrackballUp: 6 + TrackballDown: 19 + TrackballLeft: 5 + TrackballRight: 26 + TrackballPress: 13 + +# User: 21 diff --git a/bin/config.d/lora-RAK6421.yaml b/bin/config.d/lora-RAK6421.yaml new file mode 100644 index 000000000..bbf38a474 --- /dev/null +++ b/bin/config.d/lora-RAK6421.yaml @@ -0,0 +1,21 @@ +Lora: + + ### RAK13300in Slot 1 + Module: sx1262 + IRQ: 22 #IO6 + Reset: 16 # IO4 + Busy: 24 # IO5 + # Ant_sw: 13 # IO3 + DIO3_TCXO_VOLTAGE: true + DIO2_AS_RF_SWITCH: true + spidev: spidev0.0 + # CS: 8 + + + ### RAK13300in Slot 2 pins +# IRQ: 18 #IO6 +# Reset: 24 # IO4 +# Busy: 19 # IO5 +# # Ant_sw: 23 # IO3 +# spidev: spidev0.1 +# # CS: 7 \ No newline at end of file diff --git a/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml new file mode 100644 index 000000000..2fd128ce8 --- /dev/null +++ b/bin/config.d/lora-lyra-picocalc-wio-sx1262.yaml @@ -0,0 +1,18 @@ +Lora: + Module: sx1262 + DIO2_AS_RF_SWITCH: true + DIO3_TCXO_VOLTAGE: true + gpiochip: 0 + MOSI: 12 + MISO: 13 + IRQ: 1 + Busy: 23 + Reset: 22 + RXen: 0 + gpiochip: 1 + CS: 9 + SCK: 11 +# TXen: bridge to DIO2 on E22 module + SX126X_MAX_POWER: 22 + spidev: spidev1.0 + spiSpeed: 2000000 diff --git a/bin/device-install.bat b/bin/device-install.bat index 3ffca0b63..816d2fbba 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -12,6 +12,7 @@ SET "BIGDB16=0" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" +SET "BPS_RESET=0" @REM FIXME: Determine mcu from PlatformIO variant, this is unmaintainable. SET "S3=s3 v3 t-deck wireless-paper wireless-tracker station-g2 unphone" @@ -24,7 +25,7 @@ GOTO getopts :help ECHO Flash image file to device, but first erasing and writing system information. ECHO. -ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) +ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) [--1200bps-reset] ECHO. ECHO Options: ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required) @@ -35,13 +36,16 @@ ECHO -P python Specify alternate python interpreter to use to invoke ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. ECHO --web Enable WebUI. (default: false) +ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset) +ECHO Some hardware requires this twice. ECHO. +ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web GOTO eof :version -ECHO %SCRIPT_NAME% [Version 2.6.1] +ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO Meshtastic GOTO eof @@ -58,10 +62,13 @@ IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT IF /I "%~1"=="--web" SET "WEB_APP=1" +IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1" SHIFT GOTO getopts :endopts +IF %BPS_RESET% EQU 1 GOTO skip-filename + CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." @@ -95,6 +102,9 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are NOT working with a *update* file. !FILENAME!" ) +:skip-filename +SET "ESPTOOL_BAUD=1200" + CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" @@ -133,6 +143,12 @@ IF "__!ESPTOOL_PORT!__" == "____" ( ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." +IF %BPS_RESET% EQU 1 ( + @REM Attempt to change mode via 1200bps Reset. + CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + GOTO eof +) + @REM Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. @REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3 IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" ( @@ -254,6 +270,7 @@ EXIT /B %ERRORLEVEL% IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 +IF %BPS_RESET% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% diff --git a/bin/device-install.sh b/bin/device-install.sh index 7fa5ffdbb..613696d2f 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -2,6 +2,7 @@ PYTHON=${PYTHON:-$(which python3 python | head -n 1)} WEB_APP=false +BPS_RESET=false TFT_BUILD=false MCU="" @@ -32,47 +33,50 @@ BIGDB_16MB=( "ESP32-S3-Pico" "m5stack-cores3" "station-g2" - "t-eth-elite" - "t-watch-s3" + "t-eth-elite" + "t-watch-s3" + "elecrow-adv-35-tft" + "elecrow-adv-24-28-tft" + "elecrow-adv1-43-50-70-tft" ) S3_VARIANTS=( - "s3" - "-v3" - "t-deck" - "wireless-paper" - "wireless-tracker" - "station-g2" - "unphone" - "t-eth-elite" - "mesh-tab" - "dreamcatcher" - "ESP32-S3-Pico" - "seeed-sensecap-indicator" - "heltec_capsule_sensor_v3" - "vision-master" - "icarus" - "tracksenger" - "elecrow-adv" + "s3" + "-v3" + "t-deck" + "wireless-paper" + "wireless-tracker" + "station-g2" + "unphone" + "t-eth-elite" + "mesh-tab" + "dreamcatcher" + "ESP32-S3-Pico" + "seeed-sensecap-indicator" + "heltec_capsule_sensor_v3" + "vision-master" + "icarus" + "tracksenger" + "elecrow-adv" ) # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then - ESPTOOL_CMD="$PYTHON -m esptool" + ESPTOOL_CMD="$PYTHON -m esptool" elif command -v esptool >/dev/null 2>&1; then - ESPTOOL_CMD="esptool" + ESPTOOL_CMD="esptool" elif command -v esptool.py >/dev/null 2>&1; then - ESPTOOL_CMD="esptool.py" + ESPTOOL_CMD="esptool.py" else - echo "Error: esptool not found" - exit 1 + echo "Error: esptool not found" + exit 1 fi set -e # Usage info show_help() { - cat <&2 - exit 1 - ;; - esac - shift # Move to the next argument + case "$1" in + -h | --help) + show_help + exit 0 + ;; + -p) + ESPTOOL_CMD="$ESPTOOL_CMD --port $2" + shift + ;; + -P) + PYTHON="$2" + shift + ;; + -f) + FILENAME="$2" + shift + ;; + --web) + WEB_APP=true + ;; + --1200bps-reset) + BPS_RESET=true + ;; + --) # Stop parsing options + shift + break + ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac + shift # Move to the next argument done -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 - shift +if [[ $BPS_RESET == true ]]; then + $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + exit 0 +fi + +[ -z "$FILENAME" ] && [ -n "$1" ] && { + FILENAME="$1" + shift } -if [[ $FILENAME != firmware-* ]]; then +if [[ "$FILENAME" != firmware-* ]]; then echo "Filename must be a firmware-* file." exit 1 fi # Check if FILENAME contains "-tft-" and prevent web/mui comingling. -if [[ ${FILENAME//-tft-/} != "$FILENAME" ]]; then - TFT_BUILD=true - if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then - echo "Cannot enable WebUI (--web) and MUI." - exit 1 - fi +if [[ "${FILENAME//-tft-/}" != "$FILENAME" ]]; then + TFT_BUILD=true + if [[ $WEB_APP == true ]] && [[ $TFT_BUILD == true ]]; then + echo "Cannot enable WebUI (--web) and MUI." + exit 1 + fi fi # Extract BASENAME from %FILENAME% for later use. BASENAME="${FILENAME/firmware-/}" if [ -f "${FILENAME}" ] && [ -n "${FILENAME##*"update"*}" ]; then - # Default littlefs* offset (--web). - OFFSET=0x300000 + # Default littlefs* offset (--web). + OFFSET=0x300000 - # Default OTA Offset - OTA_OFFSET=0x260000 + # Default OTA Offset + OTA_OFFSET=0x260000 - # littlefs* offset for BigDB 8mb and OTA OFFSET. - for variant in "${BIGDB_8MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0x670000 - OTA_OFFSET=0x340000 - fi - done + # littlefs* offset for BigDB 8mb and OTA OFFSET. + for variant in "${BIGDB_8MB[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + OFFSET=0x670000 + OTA_OFFSET=0x340000 + fi + done - # littlefs* offset for BigDB 16mb and OTA OFFSET. - for variant in "${BIGDB_16MB[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - OFFSET=0xc90000 - OTA_OFFSET=0x650000 - fi - done + # littlefs* offset for BigDB 16mb and OTA OFFSET. + for variant in "${BIGDB_16MB[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + OFFSET=0xc90000 + OTA_OFFSET=0x650000 + fi + done - # Account for S3 board's different OTA partition - # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable - for variant in "${S3_VARIANTS[@]}"; do - if [ -z "${FILENAME##*"$variant"*}" ]; then - MCU="esp32s3" - fi - done + # Account for S3 board's different OTA partition + # FIXME: Use PlatformIO info to determine MCU type, this is unmaintainable + for variant in "${S3_VARIANTS[@]}"; do + if [ -z "${FILENAME##*"$variant"*}" ]; then + MCU="esp32s3" + fi + done - if [ "$MCU" != "esp32s3" ]; then - if [ -n "${FILENAME##*"esp32c3"*}" ]; then - OTAFILE=bleota.bin - else - OTAFILE=bleota-c3.bin - fi - else - OTAFILE=bleota-s3.bin - fi + if [ "$MCU" != "esp32s3" ]; then + if [ -n "${FILENAME##*"esp32c3"*}" ]; then + OTAFILE=bleota.bin + else + OTAFILE=bleota-c3.bin + fi + else + OTAFILE=bleota-s3.bin + fi - # Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-". - if [ "$WEB_APP" = true ]; then - SPIFFSFILE=littlefswebui-${BASENAME} - else - SPIFFSFILE=littlefs-${BASENAME} - fi + # Check if WEB_APP (--web) is enabled and add "littlefswebui-" to BASENAME else "littlefs-". + if [ "$WEB_APP" = true ]; then + SPIFFSFILE=littlefswebui-${BASENAME} + else + SPIFFSFILE=littlefs-${BASENAME} + fi - if [[ ! -f $FILENAME ]]; then - echo "Error: file ${FILENAME} wasn't found. Terminating." - exit 1 - fi - if [[ ! -f $OTAFILE ]]; then - echo "Error: file ${OTAFILE} wasn't found. Terminating." - exit 1 - fi - if [[ ! -f $SPIFFSFILE ]]; then - echo "Error: file ${SPIFFSFILE} wasn't found. Terminating." - exit 1 - fi + if [[ ! -f "$FILENAME" ]]; then + echo "Error: file ${FILENAME} wasn't found. Terminating." + exit 1 + fi + if [[ ! -f "$OTAFILE" ]]; then + echo "Error: file ${OTAFILE} wasn't found. Terminating." + exit 1 + fi + if [[ ! -f "$SPIFFSFILE" ]]; then + echo "Error: file ${SPIFFSFILE} wasn't found. Terminating." + exit 1 + fi - echo "Trying to flash ${FILENAME}, but first erasing and writing system information" - $ESPTOOL_CMD erase_flash - $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" - echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" - $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" - echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" - $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" + echo "Trying to flash ${FILENAME}, but first erasing and writing system information" + $ESPTOOL_CMD erase_flash + $ESPTOOL_CMD write_flash 0x00 "${FILENAME}" + echo "Trying to flash ${OTAFILE} at offset ${OTA_OFFSET}" + $ESPTOOL_CMD write_flash $OTA_OFFSET "${OTAFILE}" + echo "Trying to flash ${SPIFFSFILE}, at offset ${OFFSET}" + $ESPTOOL_CMD write_flash $OFFSET "${SPIFFSFILE}" else - show_help - echo "Invalid file: ${FILENAME}" + show_help + echo "Invalid file: ${FILENAME}" fi exit 0 diff --git a/bin/device-update.bat b/bin/device-update.bat index d9a4bd19a..6d55294a7 100755 --- a/bin/device-update.bat +++ b/bin/device-update.bat @@ -8,12 +8,13 @@ SET "PYTHON=" SET "ESPTOOL_BAUD=115200" SET "ESPTOOL_CMD=" SET "LOGCOUNTER=0" +SET "CHANGE_MODE=0" GOTO getopts :help ECHO Flash image file to device, but leave existing system intact. ECHO. -ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] +ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--change-mode] ECHO. ECHO Options: ECHO -f filename The update .bin file to flash. Custom to your device type and region. (required) @@ -23,12 +24,15 @@ ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python) ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. +ECHO --change-mode Attempt to place the device in correct mode. (1200bps Reset) +ECHO Some hardware requires this twice. ECHO. +ECHO Example: %SCRIPT_NAME% -p COM17 --change-mode ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4-update.bin -p COM11 GOTO eof :version -ECHO %SCRIPT_NAME% [Version 2.6.1] +ECHO %SCRIPT_NAME% [Version 2.6.2] ECHO Meshtastic GOTO eof @@ -44,10 +48,13 @@ IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT +IF /I "%~1"=="--change-mode" SET "CHANGE_MODE=1" SHIFT GOTO getopts :endopts +IF %CHANGE_MODE% EQU 1 GOTO skip-filename + CALL :LOG_MESSAGE DEBUG "Checking FILENAME parameter..." IF "__!FILENAME!__"=="____" ( CALL :LOG_MESSAGE DEBUG "Missing -f filename input." @@ -77,6 +84,9 @@ IF "!FILENAME:update=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are working with a *update* file. !FILENAME!" ) +:skip-filename +SET "ESPTOOL_BAUD=1200" + CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( SET "ESPTOOL_CMD=!PYTHON! -m esptool" @@ -115,6 +125,12 @@ IF "__!ESPTOOL_PORT!__" == "____" ( ) CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." +IF %CHANGE_MODE% EQU 1 ( + @REM Attempt to change mode via 1200bps Reset. + CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + GOTO eof +) + @REM Flashing operations. CALL :LOG_MESSAGE INFO "Trying to flash update "!FILENAME!" at OFFSET 0x10000..." CALL :RUN_ESPTOOL !ESPTOOL_BAUD! write_flash 0x10000 "!FILENAME!" || GOTO eof @@ -135,6 +151,7 @@ EXIT /B %ERRORLEVEL% IF %DEBUG% EQU 1 CALL :LOG_MESSAGE DEBUG "About to run command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" CALL :RESET_ERROR !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4 +IF %CHANGE_MODE% EQU 1 GOTO :eof IF %ERRORLEVEL% NEQ 0 ( CALL :LOG_MESSAGE ERROR "Error running command: !ESPTOOL_CMD! --baud %~1 %~2 %~3 %~4" EXIT /B %ERRORLEVEL% diff --git a/bin/device-update.sh b/bin/device-update.sh index ae7b52ea2..6adfe4e0e 100755 --- a/bin/device-update.sh +++ b/bin/device-update.sh @@ -1,6 +1,7 @@ #!/bin/sh PYTHON=${PYTHON:-$(which python3 python|head -n 1)} +CHANGE_MODE=false # Determine the correct esptool command to use if "$PYTHON" -m esptool version >/dev/null 2>&1; then @@ -17,14 +18,15 @@ fi # Usage info show_help() { cat << EOF -Usage: $(basename $0) [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] +Usage: $(basename "$0") [-h] [-p ESPTOOL_PORT] [-P PYTHON] [-f FILENAME|FILENAME] [--change-mode] Flash image file to device, leave existing system intact." -h Display this help and exit -p ESPTOOL_PORT Set the environment variable for ESPTOOL_PORT. If not set, ESPTOOL iterates all ports (Dangerous). -P PYTHON Specify alternate python interpreter to use to invoke esptool. (Default: "$PYTHON") -f FILENAME The *update.bin file to flash. Custom to your device type. - + --change-mode Attempt to place the device in correct mode. Some hardware requires this twice. (1200bps Reset) + EOF } @@ -36,13 +38,16 @@ while getopts ":hp:P:f:" opt; do exit 0 ;; p) ESPTOOL_CMD="$ESPTOOL_CMD --port ${OPTARG}" - ;; + ;; P) PYTHON=${OPTARG} ;; f) FILENAME=${OPTARG} ;; + --change-mode) + CHANGE_MODE=true + ;; *) - echo "Invalid flag." + echo "Invalid flag." show_help >&2 exit 1 ;; @@ -50,17 +55,22 @@ while getopts ":hp:P:f:" opt; do done shift "$((OPTIND-1))" -[ -z "$FILENAME" -a -n "$1" ] && { - FILENAME=$1 +if [[ $CHANGE_MODE == true ]]; then + $ESPTOOL_CMD --baud 1200 --after no_reset read_flash_status + exit 0 +fi + +[ -z "$FILENAME" ] && [ -n "$1" ] && { + FILENAME="$1" shift } if [ -f "${FILENAME}" ] && [ -z "${FILENAME##*"update"*}" ]; then - printf "Trying to flash update ${FILENAME}" - $ESPTOOL_CMD --baud 115200 write_flash 0x10000 ${FILENAME} + echo "Trying to flash update ${FILENAME}" + $ESPTOOL_CMD --baud 115200 write_flash 0x10000 "${FILENAME}" else - show_help - echo "Invalid file: ${FILENAME}" + show_help + echo "Invalid file: ${FILENAME}" fi exit 0 diff --git a/bin/generate_ci_matrix.py b/bin/generate_ci_matrix.py index 7513ccff5..0ce6b0f6b 100755 --- a/bin/generate_ci_matrix.py +++ b/bin/generate_ci_matrix.py @@ -27,7 +27,7 @@ for subdir, dirs, files in os.walk(rootdir): if c.startswith("env:"): section = config[c].name[4:] if "extends" in config[config[c].name]: - if config[config[c].name]["extends"] == options[0] + "_base": + if options[0] + "_base" in config[config[c].name]["extends"]: if "board_level" in config[config[c].name]: if ( config[config[c].name]["board_level"] == "extra" diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 1a7ad284d..f9f647dae 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,24 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.13 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.12 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.11 + + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.10 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.6.9 diff --git a/boards/ThinkNode-M1.json b/boards/ThinkNode-M1.json index e55da3ec7..2d6dbc352 100644 --- a/boards/ThinkNode-M1.json +++ b/boards/ThinkNode-M1.json @@ -48,6 +48,6 @@ "require_upload_port": true, "wait_for_upload_port": true }, - "url": "FIXME", + "url": "https://www.elecrow.com/thinknode-m1-meshtastic-lora-signal-transceiver-powered-by-nrf52840-with-154-screen-support-gps.html", "vendor": "ELECROW" } diff --git a/boards/gat562_mesh_trial_tracker.json b/boards/gat562_mesh_trial_tracker.json new file mode 100644 index 000000000..a3fb8a264 --- /dev/null +++ b/boards/gat562_mesh_trial_tracker.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "GAT562 Mesh Trial Tracker", + "mcu": "nrf52840", + "variant": "gat562_mesh_trial_tracker", + "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", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "GAT562 Mesh Trial Tracker", + "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": "http://www.gat-iot.com/", + "vendor": "GAT-IOT" +} diff --git a/boards/heltec_mesh_node_t114.json b/boards/heltec_mesh_node_t114.json index 2bd306eb9..d516c9701 100644 --- a/boards/heltec_mesh_node_t114.json +++ b/boards/heltec_mesh_node_t114.json @@ -48,6 +48,6 @@ "require_upload_port": true, "wait_for_upload_port": true }, - "url": "FIXME", + "url": "https://heltec.org/project/mesh-node-t114/", "vendor": "Heltec" } diff --git a/boards/seeed_wio_tracker_L1.json b/boards/seeed_wio_tracker_L1.json new file mode 100644 index 000000000..7c7bc62fa --- /dev/null +++ b/boards/seeed_wio_tracker_L1.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [["0x2886", "0x1668"]], + "usb_product": "TRACKER L1", + "mcu": "nrf52840", + "variant": "seeed_wio_tracker_L1", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "seeed_wio_tracker_L1", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Wio-Tracker-L1-p-6477.html", + "vendor": "Seeed Studio" +} diff --git a/boards/seeed_xiao_nrf52840_kit.json b/boards/seeed_xiao_nrf52840_kit.json index 4c5fdbeda..676733874 100644 --- a/boards/seeed_xiao_nrf52840_kit.json +++ b/boards/seeed_xiao_nrf52840_kit.json @@ -7,9 +7,7 @@ "cpu": "cortex-m4", "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", "f_cpu": "64000000L", - "hwids": [ - ["0x2886", "0x0166"] - ], + "hwids": [["0x2886", "0x0166"]], "usb_product": "XIAO-BOOT", "mcu": "nrf52840", "variant": "seeed_xiao_nrf52840_kit", diff --git a/debian/changelog b/debian/changelog index ae27bc3e9..4629e8c3a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.6.9.0) UNRELEASED; urgency=medium +meshtasticd (2.7.1.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -13,4 +13,16 @@ meshtasticd (2.6.9.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Thu, 15 May 2025 11:13:30 +0000 + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + [ ] + * GitHub Actions Automatic version bump + + -- Sat, 21 Jun 2025 15:51:49 +0000 diff --git a/debian/meshtasticd.postinst b/debian/meshtasticd.postinst index 324865718..fe0dbc332 100755 --- a/debian/meshtasticd.postinst +++ b/debian/meshtasticd.postinst @@ -20,16 +20,17 @@ set -e case "$1" in configure|reconfigure) - # create spi group (for udev rules) - # this group already exists on Raspberry Pi OS + # create spi, gpio groups (for udev rules) + # these groups already exist on Raspberry Pi OS getent group spi >/dev/null 2>/dev/null || addgroup --system spi + getent group gpio >/dev/null 2>/dev/null || addgroup --system gpio # create a meshtasticd group and user getent passwd meshtasticd >/dev/null 2>/dev/null || adduser --system --home /var/lib/meshtasticd --no-create-home meshtasticd getent group meshtasticd >/dev/null 2>/dev/null || addgroup --system meshtasticd adduser meshtasticd meshtasticd >/dev/null 2>/dev/null adduser meshtasticd spi >/dev/null 2>/dev/null + adduser meshtasticd gpio >/dev/null 2>/dev/null # add meshtasticd user to appropriate groups (if they exist) - getent group gpio >/dev/null 2>/dev/null && adduser meshtasticd gpio >/dev/null 2>/dev/null getent group plugdev >/dev/null 2>/dev/null && adduser meshtasticd plugdev >/dev/null 2>/dev/null getent group dialout >/dev/null 2>/dev/null && adduser meshtasticd dialout >/dev/null 2>/dev/null getent group i2c >/dev/null 2>/dev/null && adduser meshtasticd i2c >/dev/null 2>/dev/null diff --git a/debian/meshtasticd.udev b/debian/meshtasticd.udev index 69a468d7a..1151efafd 100644 --- a/debian/meshtasticd.udev +++ b/debian/meshtasticd.udev @@ -1,4 +1,7 @@ -# Set spidev ownership to 'spi' group. +# Set spidev ownership to 'spi' group SUBSYSTEM=="spidev", KERNEL=="spidev*", GROUP="spi", MODE="0660" # Allow access to USB CH341 devices SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="5512", MODE="0666" +# Set gpio ownership to 'gpio' group +SUBSYSTEM=="*gpiomem*", GROUP="gpio", MODE="0660" +SUBSYSTEM=="gpio", GROUP="gpio", MODE="0660" diff --git a/platformio.ini b/platformio.ini index 836b723af..693fdc9c3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -51,6 +51,7 @@ build_flags = -Wno-missing-field-initializers -DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1 -DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware -DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1 + -D MAX_THREADS=40 ; As we've split modules, we have more threads to manage #-DBUILD_EPOCH=$UNIX_TIME #-D OLED_PL=1 @@ -103,12 +104,12 @@ lib_deps = [radiolib_base] lib_deps = # renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib - jgromes/RadioLib@7.1.2 + jgromes/RadioLib@7.2.0 [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/0e9bb792bb4b015b487397427781eda2767c87e6.zip + https://github.com/meshtastic/device-ui/archive/cdc6e5bdeedb8293d10e4a02be6ca64e95a7c515.zip ; Common libs for environmental measurements in telemetry module [environmental_base] @@ -161,6 +162,12 @@ lib_deps = sparkfun/SparkFun MAX3010x Pulse and Proximity Sensor Library@1.1.2 # renovate: datasource=custom.pio depName=SparkFun 9DoF IMU Breakout ICM 20948 packageName=sparkfun/library/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@1.3.2 + # renovate: datasource=custom.pio depName=Adafruit LTR390 Library packageName=adafruit/library/Adafruit LTR390 Library + adafruit/Adafruit LTR390 Library@1.1.2 + # renovate: datasource=custom.pio depName=Adafruit PCT2075 packageName=adafruit/library/Adafruit PCT2075 + adafruit/Adafruit PCT2075@1.0.5 + # renovate: datasource=custom.pio depName=DFRobot_BMM150 packageName=dfrobot/library/DFRobot_BMM150 + dfrobot/DFRobot_BMM150@1.0.0 ; (not included in native / portduino) [environmental_extra] @@ -188,4 +195,4 @@ lib_deps = # renovate: datasource=custom.pio depName=Bosch BME68x packageName=boschsensortec/library/BME68x Sensor Library boschsensortec/BME68x Sensor Library@1.3.40408 # renovate: datasource=git-refs depName=meshtastic-DFRobot_LarkWeatherStation packageName=https://github.com/meshtastic/DFRobot_LarkWeatherStation gitBranch=master - https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip + https://github.com/meshtastic/DFRobot_LarkWeatherStation/archive/4de3a9cadef0f6a5220a8a906cf9775b02b0040d.zip \ No newline at end of file diff --git a/protobufs b/protobufs index 0b32ce24f..6791138f0 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 0b32ce24f029f69635026aec9428b5c8176e2ce1 +Subproject commit 6791138f0ba2b7c471072bd4bba6cbb8bacffe2d diff --git a/src/AmbientLightingThread.h b/src/AmbientLightingThread.h index bff8846d6..e4ef3b443 100644 --- a/src/AmbientLightingThread.h +++ b/src/AmbientLightingThread.h @@ -59,82 +59,82 @@ class AmbientLightingThread : public concurrency::OSThread return; } LOG_DEBUG("AmbientLighting init"); -#if defined(HAS_NCP5623) || defined(HAS_LP5562) +#ifdef HAS_NCP5623 if (_type == ScanI2C::NCP5623) { rgb.begin(); #endif #ifdef HAS_LP5562 - } else if (_type == ScanI2C::LP5562) { - rgbw.begin(); + if (_type == ScanI2C::LP5562) { + rgbw.begin(); #endif #ifdef RGBLED_RED - pinMode(RGBLED_RED, OUTPUT); - pinMode(RGBLED_GREEN, OUTPUT); - pinMode(RGBLED_BLUE, OUTPUT); + pinMode(RGBLED_RED, OUTPUT); + pinMode(RGBLED_GREEN, OUTPUT); + pinMode(RGBLED_BLUE, OUTPUT); #endif #ifdef HAS_NEOPIXEL - pixels.begin(); // Initialise the pixel(s) - pixels.clear(); // Set all pixel colors to 'off' - pixels.setBrightness(moduleConfig.ambient_lighting.current); + pixels.begin(); // Initialise the pixel(s) + pixels.clear(); // Set all pixel colors to 'off' + pixels.setBrightness(moduleConfig.ambient_lighting.current); #endif - setLighting(); + setLighting(); #endif #if defined(HAS_NCP5623) || defined(HAS_LP5562) - } + } #endif - } + } - protected: - int32_t runOnce() override - { + protected: + int32_t runOnce() override + { #ifdef HAS_RGB_LED #if defined(HAS_NCP5623) || defined(HAS_LP5562) - if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { + if ((_type == ScanI2C::NCP5623 || _type == ScanI2C::LP5562) && moduleConfig.ambient_lighting.led_state) { #endif - setLighting(); - return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification + setLighting(); + return 30000; // 30 seconds to reset from any animations that may have been running from Ext. Notification #if defined(HAS_NCP5623) || defined(HAS_LP5562) + } +#endif +#endif + return disable(); } -#endif -#endif - return disable(); - } - // When shutdown() is issued, setLightingOff will be called. - CallbackObserver notifyDeepSleepObserver = - CallbackObserver(this, &AmbientLightingThread::setLightingOff); + // When shutdown() is issued, setLightingOff will be called. + CallbackObserver notifyDeepSleepObserver = + CallbackObserver(this, &AmbientLightingThread::setLightingOff); - private: - ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; + private: + ScanI2C::DeviceType _type = ScanI2C::DeviceType::NONE; - // Turn RGB lighting off, is used in junction to shutdown() - int setLightingOff(void *unused) - { + // Turn RGB lighting off, is used in junction to shutdown() + int setLightingOff(void *unused) + { #ifdef HAS_NCP5623 - rgb.setCurrent(0); - rgb.setRed(0); - rgb.setGreen(0); - rgb.setBlue(0); - LOG_INFO("OFF: NCP5623 Ambient lighting"); + rgb.setCurrent(0); + rgb.setRed(0); + rgb.setGreen(0); + rgb.setBlue(0); + LOG_INFO("OFF: NCP5623 Ambient lighting"); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(0); - rgbw.setRed(0); - rgbw.setGreen(0); - rgbw.setBlue(0); - rgbw.setWhite(0); - LOG_INFO("OFF: LP5562 Ambient lighting"); + rgbw.setCurrent(0); + rgbw.setRed(0); + rgbw.setGreen(0); + rgbw.setBlue(0); + rgbw.setWhite(0); + LOG_INFO("OFF: LP5562 Ambient lighting"); #endif #ifdef HAS_NEOPIXEL - pixels.clear(); - pixels.show(); - LOG_INFO("OFF: NeoPixel Ambient lighting"); + pixels.clear(); + pixels.show(); + LOG_INFO("OFF: NeoPixel Ambient lighting"); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - 0); - analogWrite(RGBLED_GREEN, 255 - 0); - analogWrite(RGBLED_BLUE, 255 - 0); - LOG_INFO("OFF: Ambient light RGB Common Anode"); + analogWrite(RGBLED_RED, 255 - 0); + analogWrite(RGBLED_GREEN, 255 - 0); + analogWrite(RGBLED_BLUE, 255 - 0); + LOG_INFO("OFF: Ambient light RGB Common Anode"); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, 0); analogWrite(RGBLED_GREEN, 0); @@ -142,56 +142,57 @@ class AmbientLightingThread : public concurrency::OSThread LOG_INFO("OFF: Ambient light RGB Common Cathode"); #endif #ifdef UNPHONE - unphone.rgb(0, 0, 0); - LOG_INFO("OFF: unPhone Ambient lighting"); + unphone.rgb(0, 0, 0); + LOG_INFO("OFF: unPhone Ambient lighting"); #endif - return 0; - } + return 0; + } - void setLighting() - { + void setLighting() + { #ifdef HAS_NCP5623 - rgb.setCurrent(moduleConfig.ambient_lighting.current); - rgb.setRed(moduleConfig.ambient_lighting.red); - rgb.setGreen(moduleConfig.ambient_lighting.green); - rgb.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgb.setCurrent(moduleConfig.ambient_lighting.current); + rgb.setRed(moduleConfig.ambient_lighting.red); + rgb.setGreen(moduleConfig.ambient_lighting.green); + rgb.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init NCP5623 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", + moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_LP5562 - rgbw.setCurrent(moduleConfig.ambient_lighting.current); - rgbw.setRed(moduleConfig.ambient_lighting.red); - rgbw.setGreen(moduleConfig.ambient_lighting.green); - rgbw.setBlue(moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, - moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + rgbw.setCurrent(moduleConfig.ambient_lighting.current); + rgbw.setRed(moduleConfig.ambient_lighting.red); + rgbw.setGreen(moduleConfig.ambient_lighting.green); + rgbw.setBlue(moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init LP5562 Ambient light w/ current=%d, red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.current, + moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef HAS_NEOPIXEL - pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue), - 0, NEOPIXEL_COUNT); + pixels.fill(pixels.Color(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue), + 0, NEOPIXEL_COUNT); // RadioMaster Bandit has addressable LED at the two buttons // this allow us to set different lighting for them in variant.h file. #ifdef RADIOMASTER_900_BANDIT #if defined(BUTTON1_COLOR) && defined(BUTTON1_COLOR_INDEX) - pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); + pixels.fill(BUTTON1_COLOR, BUTTON1_COLOR_INDEX, 1); #endif #if defined(BUTTON2_COLOR) && defined(BUTTON2_COLOR_INDEX) - pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); + pixels.fill(BUTTON2_COLOR, BUTTON2_COLOR_INDEX, 1); #endif #endif - pixels.show(); - LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", - moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, - moduleConfig.ambient_lighting.blue); + pixels.show(); + LOG_DEBUG("Init NeoPixel Ambient light w/ brightness(current)=%d, red=%d, green=%d, blue=%d", + moduleConfig.ambient_lighting.current, moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef RGBLED_CA - analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); - analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); - analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + analogWrite(RGBLED_RED, 255 - moduleConfig.ambient_lighting.red); + analogWrite(RGBLED_GREEN, 255 - moduleConfig.ambient_lighting.green); + analogWrite(RGBLED_BLUE, 255 - moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init Ambient light RGB Common Anode w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #elif defined(RGBLED_RED) analogWrite(RGBLED_RED, moduleConfig.ambient_lighting.red); analogWrite(RGBLED_GREEN, moduleConfig.ambient_lighting.green); @@ -200,11 +201,12 @@ class AmbientLightingThread : public concurrency::OSThread moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif #ifdef UNPHONE - unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); - LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, - moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); + unphone.rgb(moduleConfig.ambient_lighting.red, moduleConfig.ambient_lighting.green, + moduleConfig.ambient_lighting.blue); + LOG_DEBUG("Init unPhone Ambient light w/ red=%d, green=%d, blue=%d", moduleConfig.ambient_lighting.red, + moduleConfig.ambient_lighting.green, moduleConfig.ambient_lighting.blue); #endif - } -}; + } + }; } // namespace concurrency diff --git a/src/AudioThread.h b/src/AudioThread.h index 04ff64a6e..286729909 100644 --- a/src/AudioThread.h +++ b/src/AudioThread.h @@ -47,6 +47,20 @@ class AudioThread : public concurrency::OSThread setCPUFast(false); } + void readAloud(const char *text) + { + if (i2sRtttl != nullptr) { + i2sRtttl->stop(); + delete i2sRtttl; + i2sRtttl = nullptr; + } + + ESP8266SAM *sam = new ESP8266SAM; + sam->Say(audioOut, text); + delete sam; + setCPUFast(false); + } + protected: int32_t runOnce() override { diff --git a/src/BluetoothStatus.h b/src/BluetoothStatus.h index 526b6f243..f6bb43cc2 100644 --- a/src/BluetoothStatus.h +++ b/src/BluetoothStatus.h @@ -88,10 +88,16 @@ class BluetoothStatus : public Status break; case ConnectionState::CONNECTED: LOG_DEBUG("BluetoothStatus CONNECTED"); +#ifdef BLE_LED + digitalWrite(BLE_LED, HIGH); +#endif break; case ConnectionState::DISCONNECTED: LOG_DEBUG("BluetoothStatus DISCONNECTED"); +#ifdef BLE_LED + digitalWrite(BLE_LED, LOW); +#endif break; } } @@ -102,4 +108,4 @@ class BluetoothStatus : public Status } // namespace meshtastic -extern meshtastic::BluetoothStatus *bluetoothStatus; \ No newline at end of file +extern meshtastic::BluetoothStatus *bluetoothStatus; diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp deleted file mode 100644 index 8db52c074..000000000 --- a/src/ButtonThread.cpp +++ /dev/null @@ -1,467 +0,0 @@ -#include "ButtonThread.h" - -#include "configuration.h" -#if !MESHTASTIC_EXCLUDE_GPS -#include "GPS.h" -#endif -#include "MeshService.h" -#include "PowerFSM.h" -#include "RadioLibInterface.h" -#include "buzz.h" -#include "main.h" -#include "modules/ExternalNotificationModule.h" -#include "power.h" -#include "sleep.h" -#ifdef ARCH_PORTDUINO -#include "platform/portduino/PortduinoGlue.h" -#endif - -#define DEBUG_BUTTONS 0 -#if DEBUG_BUTTONS -#define LOG_BUTTON(...) LOG_DEBUG(__VA_ARGS__) -#else -#define LOG_BUTTON(...) -#endif - -using namespace concurrency; - -ButtonThread *buttonThread; // Declared extern in header -volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) -OneButton ButtonThread::userButton; // Get reference to static member -#endif -ButtonThread::ButtonThread() : OSThread("Button") -{ -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { - this->userButton = OneButton(settingsMap[user], true, true); - LOG_DEBUG("Use GPIO%02d for button", settingsMap[user]); - } -#elif defined(BUTTON_PIN) -#if !defined(USERPREFS_BUTTON_PIN) - int pin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; // Resolved button pin -#endif -#ifdef USERPREFS_BUTTON_PIN - int pin = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; // Resolved button pin -#endif -#if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) - this->userButton = OneButton(pin, false, false); -#elif defined(BUTTON_ACTIVE_LOW) - this->userButton = OneButton(pin, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP); -#else - this->userButton = OneButton(pin, true, true); -#endif - LOG_DEBUG("Use GPIO%02d for button", pin); -#endif - -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did -#ifdef BUTTON_SENSE_TYPE - pinMode(pin, BUTTON_SENSE_TYPE); -#else - pinMode(pin, INPUT_PULLUP_SENSE); -#endif -#endif - -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - userButton.attachClick(userButtonPressed); - userButton.setClickMs(BUTTON_CLICK_MS); - userButton.setPressMs(BUTTON_LONGPRESS_MS); - userButton.setDebounceMs(1); - userButton.attachDoubleClick(userButtonDoublePressed); - userButton.attachMultiClick(userButtonMultiPressed, this); // Reference to instance: get click count from non-static OneButton -#if !defined(T_DECK) && \ - !defined( \ - ELECROW_ThinkNode_M2) // T-Deck immediately wakes up after shutdown, Thinknode M2 has this on the smaller ALT button - userButton.attachLongPressStart(userButtonPressedLongStart); - userButton.attachLongPressStop(userButtonPressedLongStop); -#endif -#endif - -#ifdef BUTTON_PIN_ALT -#if defined(ELECROW_ThinkNode_M2) - this->userButtonAlt = OneButton(BUTTON_PIN_ALT, false, false); -#else - this->userButtonAlt = OneButton(BUTTON_PIN_ALT, true, true); -#endif -#ifdef INPUT_PULLUP_SENSE - // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did - pinMode(BUTTON_PIN_ALT, INPUT_PULLUP_SENSE); -#endif - userButtonAlt.attachClick(userButtonPressedScreen); - userButtonAlt.setClickMs(BUTTON_CLICK_MS); - userButtonAlt.setPressMs(BUTTON_LONGPRESS_MS); - userButtonAlt.setDebounceMs(1); - userButtonAlt.attachLongPressStart(userButtonPressedLongStart); - userButtonAlt.attachLongPressStop(userButtonPressedLongStop); -#endif - -#ifdef BUTTON_PIN_TOUCH - userButtonTouch = OneButton(BUTTON_PIN_TOUCH, true, true); - userButtonTouch.setPressMs(BUTTON_TOUCH_MS); - userButtonTouch.attachLongPressStart(touchPressedLongStart); // Better handling with longpress than click? -#endif - -#ifdef ARCH_ESP32 - // Register callbacks for before and after lightsleep - // Used to detach and reattach interrupts - lsObserver.observe(¬ifyLightSleep); - lsEndObserver.observe(¬ifyLightSleepEnd); -#endif - - attachButtonInterrupts(); -#endif -} - -void ButtonThread::switchPage() -{ -#ifdef BUTTON_PIN -#if !defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) != - moduleConfig.canned_message.inputbroker_pin_press) || - !(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif -#if defined(USERPREFS_BUTTON_PIN) - if (((config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN) != - moduleConfig.canned_message.inputbroker_pin_press) || - !(moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.rotary1_enabled) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif - -#endif -#if defined(ARCH_PORTDUINO) - if ((settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) && - (settingsMap[user] != moduleConfig.canned_message.inputbroker_pin_press) || - !moduleConfig.canned_message.enabled) { - powerFSM.trigger(EVENT_PRESS); - } -#endif -} - -void ButtonThread::sendAdHocPosition() -{ - service->refreshLocalMeshNode(); - auto sentPosition = service->trySendPosition(NODENUM_BROADCAST, true); - if (screen) { - if (sentPosition) - screen->print("Sent ad-hoc position\n"); - else - screen->print("Sent ad-hoc nodeinfo\n"); - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update - } -} - -int32_t ButtonThread::runOnce() -{ - // If the button is pressed we suppress CPU sleep until release - canSleep = true; // Assume we should not keep the board awake - -#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) - userButton.tick(); - canSleep &= userButton.isIdle(); -#elif defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { - userButton.tick(); - canSleep &= userButton.isIdle(); - } -#endif -#ifdef BUTTON_PIN_ALT - userButtonAlt.tick(); - canSleep &= userButtonAlt.isIdle(); -#endif -#ifdef BUTTON_PIN_TOUCH - userButtonTouch.tick(); - canSleep &= userButtonTouch.isIdle(); -#endif - - if (btnEvent != BUTTON_EVENT_NONE) { - switch (btnEvent) { - case BUTTON_EVENT_PRESSED: { - LOG_BUTTON("press!"); - // If a nag notification is running, stop it and prevent other actions - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - break; - } -#ifdef ELECROW_ThinkNode_M1 - sendAdHocPosition(); - break; -#endif - switchPage(); - break; - } - - case BUTTON_EVENT_PRESSED_SCREEN: { - LOG_BUTTON("AltPress!"); -#ifdef ELECROW_ThinkNode_M1 - // If a nag notification is running, stop it and prevent other actions - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - break; - } - switchPage(); - break; -#endif - // turn screen on or off - screen_flag = !screen_flag; - if (screen) - screen->setOn(screen_flag); - break; - } - - case BUTTON_EVENT_DOUBLE_PRESSED: { - LOG_BUTTON("Double press!"); -#ifdef ELECROW_ThinkNode_M1 - digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); - break; -#endif - sendAdHocPosition(); - break; - } - - case BUTTON_EVENT_MULTI_PRESSED: { - LOG_BUTTON("Mulitipress! %hux", multipressClickCount); - switch (multipressClickCount) { -#if HAS_GPS && !defined(ELECROW_ThinkNode_M1) - // 3 clicks: toggle GPS - case 3: - if (!config.device.disable_triple_click && (gps != nullptr)) { - gps->toggleGpsMode(); - if (screen) - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update - } - break; -#elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) - case 3: - LOG_INFO("3 clicks: toggle buzzer"); - buzzer_flag = !buzzer_flag; - if (!buzzer_flag) - noTone(PIN_BUZZER); - break; - -#endif - -#if defined(USE_EINK) && defined(PIN_EINK_EN) && !defined(ELECROW_ThinkNode_M1) // i.e. T-Echo - // 4 clicks: toggle backlight - case 4: - digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); - break; -#endif -#if !MESHTASTIC_EXCLUDE_SCREEN && HAS_SCREEN - // 5 clicks: start accelerometer/magenetometer calibration for 30 seconds - case 5: - if (accelerometerThread) { - accelerometerThread->calibrate(30); - } - break; - // 6 clicks: start accelerometer/magenetometer calibration for 60 seconds - case 6: - if (accelerometerThread) { - accelerometerThread->calibrate(60); - } - break; -#endif - // No valid multipress action - default: - break; - } // end switch: click count - - break; - } // end multipress event - - case BUTTON_EVENT_LONG_PRESSED: { - LOG_BUTTON("Long press!"); - powerFSM.trigger(EVENT_PRESS); - if (screen) { - screen->startAlert("Shutting down..."); - } - playBeep(); - break; - } - - // Do actual shutdown when button released, otherwise the button release - // may wake the board immediatedly. - case BUTTON_EVENT_LONG_RELEASED: { - LOG_INFO("Shutdown from long press"); - playShutdownMelody(); - delay(3000); - power->shutdown(); - break; - } - -#ifdef BUTTON_PIN_TOUCH - case BUTTON_EVENT_TOUCH_LONG_PRESSED: { - LOG_BUTTON("Touch press!"); - // Ignore if: no screen - if (!screen) - break; - -#ifdef TTGO_T_ECHO - // Ignore if: TX in progress - // Uncommon T-Echo hardware bug, LoRa TX triggers touch button - if (!RadioLibInterface::instance || RadioLibInterface::instance->isSending()) - break; -#endif - - // Wake if asleep - if (powerFSM.getState() == &stateDARK) - powerFSM.trigger(EVENT_PRESS); - - // Update display (legacy behaviour) - screen->forceDisplay(); - break; - } -#endif // BUTTON_PIN_TOUCH - - default: - break; - } - btnEvent = BUTTON_EVENT_NONE; - } - - return 50; -} - -/* - * Attach (or re-attach) hardware interrupts for buttons - * Public method. Used outside class when waking from MCU sleep - */ -void ButtonThread::attachButtonInterrupts() -{ -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) - wakeOnIrq(settingsMap[user], FALLING); -#elif defined(BUTTON_PIN) - // Interrupt for user button, during normal use. Improves responsiveness. - attachInterrupt( -#if !defined(USERPREFS_BUTTON_PIN) - config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN, -#endif -#if defined(USERPREFS_BUTTON_PIN) - config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN, -#endif - []() { - ButtonThread::userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - CHANGE); -#endif - -#ifdef BUTTON_PIN_ALT -#ifdef ELECROW_ThinkNode_M2 - wakeOnIrq(BUTTON_PIN_ALT, RISING); -#else - wakeOnIrq(BUTTON_PIN_ALT, FALLING); -#endif -#endif - -#ifdef BUTTON_PIN_TOUCH - wakeOnIrq(BUTTON_PIN_TOUCH, FALLING); -#endif -} - -/* - * Detach the "normal" button interrupts. - * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep - */ -void ButtonThread::detachButtonInterrupts() -{ -#if defined(ARCH_PORTDUINO) - if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) - detachInterrupt(settingsMap[user]); -#elif defined(BUTTON_PIN) -#if !defined(USERPREFS_BUTTON_PIN) - detachInterrupt(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); -#endif -#if defined(USERPREFS_BUTTON_PIN) - detachInterrupt(config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN); -#endif -#endif - -#ifdef BUTTON_PIN_ALT - detachInterrupt(BUTTON_PIN_ALT); -#endif - -#ifdef BUTTON_PIN_TOUCH - detachInterrupt(BUTTON_PIN_TOUCH); -#endif -} - -#ifdef ARCH_ESP32 - -// Detach our class' interrupts before lightsleep -// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press -int ButtonThread::beforeLightSleep(void *unused) -{ - detachButtonInterrupts(); - return 0; // Indicates success -} - -// Reconfigure our interrupts -// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep -int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) -{ - attachButtonInterrupts(); - return 0; // Indicates success -} - -#endif - -/** - * Watch a GPIO and if we get an IRQ, wake the main thread. - * Use to add wake on button press - */ -void ButtonThread::wakeOnIrq(int irq, int mode) -{ - attachInterrupt( - irq, - [] { - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - runASAP = true; - }, - FALLING); -} - -// Static callback -void ButtonThread::userButtonMultiPressed(void *callerThread) -{ - // Grab click count from non-static button, while the info is still valid - ButtonThread *thread = (ButtonThread *)callerThread; - thread->storeClickCount(); - - // Then handle later, in the usual way - btnEvent = BUTTON_EVENT_MULTI_PRESSED; -} - -// Non-static method, runs during callback. Grabs info while still valid -void ButtonThread::storeClickCount() -{ -#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) - multipressClickCount = userButton.getNumberClicks(); -#endif -} - -void ButtonThread::userButtonPressedLongStart() -{ - if (millis() > c_holdOffTime) { - btnEvent = BUTTON_EVENT_LONG_PRESSED; - } -} - -void ButtonThread::userButtonPressedLongStop() -{ - if (millis() > c_holdOffTime) { - btnEvent = BUTTON_EVENT_LONG_RELEASED; - } -} \ No newline at end of file diff --git a/src/ButtonThread.h b/src/ButtonThread.h deleted file mode 100644 index 3af700dd0..000000000 --- a/src/ButtonThread.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "OneButton.h" -#include "concurrency/OSThread.h" -#include "configuration.h" - -#ifndef BUTTON_CLICK_MS -#define BUTTON_CLICK_MS 250 -#endif - -#ifndef BUTTON_LONGPRESS_MS -#define BUTTON_LONGPRESS_MS 5000 -#endif - -#ifndef BUTTON_TOUCH_MS -#define BUTTON_TOUCH_MS 400 -#endif - -class ButtonThread : public concurrency::OSThread -{ - public: - static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - - enum ButtonEventType { - BUTTON_EVENT_NONE, - BUTTON_EVENT_PRESSED, - BUTTON_EVENT_PRESSED_SCREEN, - BUTTON_EVENT_DOUBLE_PRESSED, - BUTTON_EVENT_MULTI_PRESSED, - BUTTON_EVENT_LONG_PRESSED, - BUTTON_EVENT_LONG_RELEASED, - BUTTON_EVENT_TOUCH_LONG_PRESSED, - }; - - ButtonThread(); - int32_t runOnce() override; - void attachButtonInterrupts(); - void detachButtonInterrupts(); - void storeClickCount(); - bool isBuzzing() { return buzzer_flag; } - void setScreenFlag(bool flag) { screen_flag = flag; } - bool getScreenFlag() { return screen_flag; } - - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - private: -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - static OneButton userButton; // Static - accessed from an interrupt -#endif -#ifdef BUTTON_PIN_ALT - OneButton userButtonAlt; -#endif -#ifdef BUTTON_PIN_TOUCH - OneButton userButtonTouch; -#endif - -#ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &ButtonThread::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &ButtonThread::afterLightSleep); -#endif - - // set during IRQ - static volatile ButtonEventType btnEvent; - bool buzzer_flag = false; - bool screen_flag = true; - - // Store click count during callback, for later use - volatile int multipressClickCount = 0; - - static void wakeOnIrq(int irq, int mode); - - static void sendAdHocPosition(); - static void switchPage(); - - // IRQ callbacks - static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } - static void userButtonPressedScreen() { btnEvent = BUTTON_EVENT_PRESSED_SCREEN; } - static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; } - static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid - static void userButtonPressedLongStart(); - static void userButtonPressedLongStop(); - static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; } -}; - -extern ButtonThread *buttonThread; diff --git a/src/Power.cpp b/src/Power.cpp index a9ed6360e..400b6c6eb 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -661,12 +661,14 @@ bool Power::analogInit() */ bool Power::setup() { - // initialise one power sensor (only) - bool found = axpChipInit(); - if (!found) - found = lipoInit(); - if (!found) - found = analogInit(); + bool found = false; + if (axpChipInit()) { + found = true; + } else if (lipoInit()) { + found = true; + } else if (analogInit()) { + found = true; + } #ifdef NRF_APM found = true; @@ -853,7 +855,8 @@ int32_t Power::runOnce() #ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? if (PMU->isPekeyLongPressIrq()) { LOG_DEBUG("PEK long button press"); - screen->setOn(false); + if (screen) + screen->setOn(false); } #endif diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index dbe4796cf..3b3f8080d 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -26,7 +26,7 @@ #ifndef SLEEP_TIME #define SLEEP_TIME 30 #endif -#if EXCLUDE_POWER_FSM +#if MESHTASTIC_EXCLUDE_POWER_FSM FakeFsm powerFSM; void PowerFSM_setup(){}; #else @@ -82,7 +82,8 @@ static uint32_t secsSlept; static void lsEnter() { LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); - screen->setOn(false); + if (screen) + screen->setOn(false); secsSlept = 0; // How long have we been sleeping this time // LOG_INFO("lsEnter end"); @@ -160,7 +161,8 @@ static void lsExit() static void nbEnter() { LOG_DEBUG("State: NB"); - screen->setOn(false); + if (screen) + screen->setOn(false); #ifdef ARCH_ESP32 // Only ESP32 should turn off bluetooth setBluetoothEnable(false); @@ -172,22 +174,23 @@ static void nbEnter() static void darkEnter() { setBluetoothEnable(true); - screen->setOn(false); + if (screen) + screen->setOn(false); } static void serialEnter() { LOG_DEBUG("State: SERIAL"); setBluetoothEnable(false); - screen->setOn(true); - screen->print("Serial connected\n"); + if (screen) { + screen->setOn(true); + } } static void serialExit() { // Turn bluetooth back on when we leave serial stream API setBluetoothEnable(true); - screen->print("Serial disconnected\n"); } static void powerEnter() @@ -198,15 +201,10 @@ static void powerEnter() LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from - - // Mothballed: print change of power-state to device screen - /* if (strcmp(powerFSM.getState()->name, "BOOT") != 0 && strcmp(powerFSM.getState()->name, "POWER") != 0 && - strcmp(powerFSM.getState()->name, "DARK") != 0) { - screen->print("Powered...\n"); - }*/ } } @@ -221,18 +219,16 @@ static void powerIdle() static void powerExit() { - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); - - // Mothballed: print change of power-state to device screen - /*if (!isPowered()) - screen->print("Unpowered...\n");*/ } static void onEnter() { LOG_DEBUG("State: ON"); - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); } @@ -244,11 +240,6 @@ static void onIdle() } } -static void screenPress() -{ - screen->onPress(); -} - static void bootEnter() { LOG_DEBUG("State: BOOT"); @@ -292,9 +283,9 @@ void PowerFSM_setup() powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, screenPress, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress, + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep @@ -328,10 +319,10 @@ void PowerFSM_setup() // if any packet destined for phone arrives, turn on bluetooth at least powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // show the latest node when we get a new node db update - powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // Removed 2.7: we don't show the nodes individually for every node on the screen anymore + // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // Show the received text message powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 13dfdc4cc..6330a5fc6 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -11,7 +11,7 @@ #define EVENT_RECEIVED_MSG 5 // #define EVENT_BOOT 6 // now done with a timed transition #define EVENT_BLUETOOTH_PAIR 7 -#define EVENT_NODEDB_UPDATED 8 // NodeDB has a big enough change that we think you should turn on the screen +// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 @@ -22,7 +22,7 @@ #define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep) #define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen -#if EXCLUDE_POWER_FSM +#if MESHTASTIC_EXCLUDE_POWER_FSM class FakeFsm { public: diff --git a/src/PowerFSMThread.h b/src/PowerFSMThread.h index c842f4515..135f53298 100644 --- a/src/PowerFSMThread.h +++ b/src/PowerFSMThread.h @@ -18,7 +18,7 @@ class PowerFSMThread : public OSThread protected: int32_t runOnce() override { -#if !EXCLUDE_POWER_FSM +#if !MESHTASTIC_EXCLUDE_POWER_FSM powerFSM.run_machine(); /// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 07f873864..7c8d77651 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -352,8 +352,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 for (uint16_t i = 0; i < len; i += 16) { if (i % 128 == 0) log(logLevel, " +------------------------------------------------+ +----------------+"); - char s[] = "| | | |\n"; - uint8_t ix = 1, iy = 52; + char s[] = " | | | |\n"; + uint8_t ix = 5, iy = 56; for (uint8_t j = 0; j < 16; j++) { if (i + j < len) { uint8_t c = buf[i + j]; @@ -367,10 +367,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 } } uint8_t index = i / 16; - if (i < 256) - log(logLevel, " "); - log(logLevel, "%02x", index); - log(logLevel, "."); + sprintf(s, "%03x", index); + s[3] = '.'; log(logLevel, s); } log(logLevel, " +------------------------------------------------+ +----------------+"); @@ -393,4 +391,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) break; } return std::string(formatted.get()); -} \ No newline at end of file +} diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp new file mode 100644 index 000000000..2bd3158a3 --- /dev/null +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -0,0 +1,79 @@ +#include "BuzzerFeedbackThread.h" +#include "NodeDB.h" +#include "buzz.h" +#include "configuration.h" + +BuzzerFeedbackThread *buzzerFeedbackThread; + +BuzzerFeedbackThread::BuzzerFeedbackThread() : OSThread("BuzzerFeedback") +{ + if (inputBroker) + inputObserver.observe(inputBroker); +} + +int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) +{ + // Only provide feedback if buzzer is enabled for notifications + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + return 0; // Let other handlers process the event + } + + // Track last event time for potential future use + lastEventTime = millis(); + needsUpdate = true; + + // Handle different input events with appropriate buzzer feedback + switch (event->inputEvent) { + case INPUT_BROKER_USER_PRESS: + case INPUT_BROKER_ALT_PRESS: + case INPUT_BROKER_SELECT: + playBeep(); // Confirmation feedback + break; + + case INPUT_BROKER_UP: + case INPUT_BROKER_DOWN: + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: + playChirp(); // Navigation feedback + break; + + case INPUT_BROKER_CANCEL: + case INPUT_BROKER_BACK: + playBoop(); // Cancel/back feedback + break; + + case INPUT_BROKER_SEND_PING: + playComboTune(); // Ping sent feedback + break; + + case INPUT_BROKER_SHUTDOWN: + playShutdownMelody(); // Shutdown feedback + break; + + default: + // For other events, check if it's a printable character + if (event->kbchar >= 32 && event->kbchar <= 126) { + // Typing feedback - very short boop + // Removing this for now, too chatty + // playChirp(); + } + break; + } + + return 0; // Allow other handlers to process the event +} + +int32_t BuzzerFeedbackThread::runOnce() +{ + // This thread is primarily event-driven, but we can use runOnce + // for any periodic tasks if needed in the future + + if (needsUpdate) { + needsUpdate = false; + // Could add any periodic processing here + } + + // Run every 100ms when active, less frequently when idle + return needsUpdate ? 100 : 1000; +} diff --git a/src/buzz/BuzzerFeedbackThread.h b/src/buzz/BuzzerFeedbackThread.h new file mode 100644 index 000000000..dedea9860 --- /dev/null +++ b/src/buzz/BuzzerFeedbackThread.h @@ -0,0 +1,24 @@ +#pragma once + +#include "Observer.h" +#include "concurrency/OSThread.h" +#include "input/InputBroker.h" + +class BuzzerFeedbackThread : public concurrency::OSThread +{ + CallbackObserver inputObserver = + CallbackObserver(this, &BuzzerFeedbackThread::handleInputEvent); + + public: + BuzzerFeedbackThread(); + int handleInputEvent(const InputEvent *event); + + protected: + virtual int32_t runOnce() override; + + private: + uint32_t lastEventTime = 0; + bool needsUpdate = false; +}; + +extern BuzzerFeedbackThread *buzzerFeedbackThread; diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6ba2f4140..b09d7a82c 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -38,6 +38,11 @@ const int DURATION_1_1 = 1000; // 1/1 note void playTones(const ToneDuration *tone_durations, int size) { + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + // Buzzer is disabled or not set to system tones + return; + } #ifdef PIN_BUZZER if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; @@ -54,7 +59,7 @@ void playTones(const ToneDuration *tone_durations, int size) void playBeep() { - ToneDuration melody[] = {{NOTE_B3, DURATION_1_4}}; + ToneDuration melody[] = {{NOTE_B3, DURATION_1_8}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } @@ -87,3 +92,72 @@ void playShutdownMelody() ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } + +void playChirp() +{ + // A short, friendly "chirp" sound for key presses + ToneDuration melody[] = {{NOTE_AS3, 20}}; // Very short AS3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playBoop() +{ + // A short, friendly "boop" sound for button presses + ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playLongPressLeadUp() +{ + // An ascending lead-up sequence for long press - builds anticipation + ToneDuration melody[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +// Static state for progressive lead-up notes +static int leadUpNoteIndex = 0; +static const ToneDuration leadUpNotes[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis +}; +static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); + +bool playNextLeadUpNote() +{ + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // All notes have been played + } + + // Use playTones to handle buzzer logic consistently + const auto ¬e = leadUpNotes[leadUpNoteIndex]; + playTones(¬e, 1); // Play single note using existing playTones function + + leadUpNoteIndex++; + return true; // Note was played (playTones handles buzzer availability internally) +} + +void resetLeadUpSequence() +{ + leadUpNoteIndex = 0; +} + +void playComboTune() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = { + {NOTE_G3, 80}, // Quick chirp + {NOTE_B3, 60}, // Higher chirp + {NOTE_CS4, 80}, // Even higher + {NOTE_G3, 60}, // Quick trill down + {NOTE_CS4, 60}, // Quick trill up + {NOTE_B3, 120} // Ending chirp + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index adeaca73d..c25a54a5b 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -5,4 +5,10 @@ void playLongBeep(); void playStartMelody(); void playShutdownMelody(); void playGPSEnableBeep(); -void playGPSDisableBeep(); \ No newline at end of file +void playGPSDisableBeep(); +void playComboTune(); +void playBoop(); +void playChirp(); +void playLongPressLeadUp(); +bool playNextLeadUpNote(); // Play the next note in the lead-up sequence +void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning \ No newline at end of file diff --git a/src/commands.h b/src/commands.h index f2b783010..e0bfab330 100644 --- a/src/commands.h +++ b/src/commands.h @@ -12,7 +12,6 @@ enum class Cmd { STOP_ALERT_FRAME, START_FIRMWARE_UPDATE_SCREEN, STOP_BOOT_SCREEN, - PRINT, SHOW_PREV_FRAME, SHOW_NEXT_FRAME }; \ No newline at end of file diff --git a/src/concurrency/Lock.cpp b/src/concurrency/Lock.cpp index 11501359b..0fe80e455 100644 --- a/src/concurrency/Lock.cpp +++ b/src/concurrency/Lock.cpp @@ -9,17 +9,23 @@ namespace concurrency Lock::Lock() : handle(xSemaphoreCreateBinary()) { assert(handle); - assert(xSemaphoreGive(handle)); + if (xSemaphoreGive(handle) == false) { + abort(); + } } void Lock::lock() { - assert(xSemaphoreTake(handle, portMAX_DELAY)); + if (xSemaphoreTake(handle, portMAX_DELAY) == false) { + abort(); + } } void Lock::unlock() { - assert(xSemaphoreGive(handle)); + if (xSemaphoreGive(handle) == false) { + abort(); + } } #else Lock::Lock() {} diff --git a/src/configuration.h b/src/configuration.h index d319ddb0a..89257ff2f 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -80,10 +80,46 @@ along with this program. If not, see . // Override user saved region, for producing region-locked builds // #define REGULATORY_LORA_REGIONCODE meshtastic_Config_LoRaConfig_RegionCode_SG_923 -// Total system gain in dBm to subtract from Tx power to remain within regulatory ERP limit for non-licensed operators -// This value should be set in variant.h and is PA gain + antenna gain (if system ships with an antenna) -#ifndef REGULATORY_GAIN_LORA -#define REGULATORY_GAIN_LORA 0 +// Total system gain in dBm to subtract from Tx power to remain within regulatory and Tx PA limits +// The value consists of PA gain + antenna gain (if variant has a non-removable antenna) +// TX_GAIN_LORA should be set with definitions below for common modules, or in variant.h. + +// Gain for common modules with transmit PAs +#ifdef EBYTE_E22_900M30S +// 10dB PA gain and 30dB rated output; based on measurements from +// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt +#define TX_GAIN_LORA 7 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef EBYTE_E22_900M33S +// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf +#define TX_GAIN_LORA 25 +#define SX126X_MAX_POWER 8 +#endif + +#ifdef NICERF_MINIF27 +// Note that datasheet power level of 9 corresponds with SX1262 at 22dBm +// Maximum output power of 29dBm with VCC_PA = 5V +#define TX_GAIN_LORA 7 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef NICERF_F30_HF +// Maximum output power of 29.6dBm with VCC = 5V and SX1262 at 22dBm +#define TX_GAIN_LORA 8 +#define SX126X_MAX_POWER 22 +#endif + +#ifdef NICERF_F30_LF +// Maximum output power of 32.0dBm with VCC = 5V and SX1262 at 22dBm +#define TX_GAIN_LORA 10 +#define SX126X_MAX_POWER 22 +#endif + +// Default system gain to 0 if not defined +#ifndef TX_GAIN_LORA +#define TX_GAIN_LORA 0 #endif // ----------------------------------------------------------------------------- @@ -99,8 +135,12 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- - +#if defined(SEEED_WIO_TRACKER_L1) +#define SSD1306_ADDRESS 0x3D +#define USE_SH1106 +#else #define SSD1306_ADDRESS 0x3C +#endif #define ST7567_ADDRESS 0x3F // The SH1106 controller is almost, but not quite, the same as SSD1306 @@ -153,6 +193,7 @@ along with this program. If not, see . #define CGRADSENS_ADDR 0x66 #define LTR390UV_ADDR 0x53 #define XPOWERS_AXP192_AXP2101_ADDRESS 0x34 // same adress as TCA8418 +#define PCT2075_ADDR 0x37 // ----------------------------------------------------------------------------- // ACCELEROMETER @@ -166,6 +207,7 @@ along with this program. If not, see . #define BMX160_ADDR 0x69 #define ICM20948_ADDR 0x69 #define ICM20948_ADDR_ALT 0x68 +#define BMM150_ADDR 0x13 // ----------------------------------------------------------------------------- // LED @@ -188,6 +230,15 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- #define FT6336U_ADDR 0x48 +// ----------------------------------------------------------------------------- +// RAK12035VB Soil Monitor (using RAK12023 up to 3 RAK12035 monitors can be connected) +// - the default i2c address for this sensor is 0x20, and users are instructed to +// set 0x21 and 0x22 for the second and third sensor if present. +// ----------------------------------------------------------------------------- +#define RAK120351_ADDR 0x20 +#define RAK120352_ADDR 0x21 +#define RAK120353_ADDR 0x22 + // ----------------------------------------------------------------------------- // BIAS-T Generator // ----------------------------------------------------------------------------- @@ -297,11 +348,41 @@ along with this program. If not, see . #error HW_VENDOR must be defined #endif +#ifndef TB_DOWN +#define TB_DOWN 255 +#endif +#ifndef TB_UP +#define TB_UP 255 +#endif +#ifndef TB_LEFT +#define TB_LEFT 255 +#endif +#ifndef TB_RIGHT +#define TB_RIGHT 255 +#endif +#ifndef TB_PRESS +#define TB_PRESS 255 +#endif + // Support multiple RGB LED configuration #if defined(HAS_NCP5623) || defined(HAS_LP5562) || defined(RGBLED_RED) || defined(HAS_NEOPIXEL) || defined(UNPHONE) #define HAS_RGB_LED #endif +// default mapping of pins +#if defined(PIN_BUTTON2) && !defined(CANCEL_BUTTON_PIN) +#define ALT_BUTTON_PIN PIN_BUTTON2 +#endif +#if defined ALT_BUTTON_PIN + +#ifndef ALT_BUTTON_ACTIVE_LOW +#define ALT_BUTTON_ACTIVE_LOW true +#endif +#ifndef ALT_BUTTON_ACTIVE_PULLUP +#define ALT_BUTTON_ACTIVE_PULLUP true +#endif +#endif + // ----------------------------------------------------------------------------- // Global switches to turn off features for a minimized build // ----------------------------------------------------------------------------- diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 5bd5c0d12..e6236251c 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -37,8 +37,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P}; - return firstOfOrNONE(8, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ICM20948, QMA6100P, BMM150}; + return firstOfOrNONE(9, types); } ScanI2C::FoundDevice ScanI2C::firstRGBLED() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c363db1b5..90467abd0 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -70,7 +70,10 @@ class ScanI2C DFROBOT_RAIN, DPS310, LTR390UV, + RAK12035, TCA8418KB, + PCT2075, + BMM150, } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 9781cbf56..fd3d1c80b 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -358,7 +358,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) case SHT31_4x_ADDR: // same as OPT3001_ADDR_ALT case SHT31_4x_ADDR_ALT: // same as OPT3001_ADDR registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x89), 2); - if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c) { + if (registerValue == 0x11a2 || registerValue == 0x11da || registerValue == 0xe9c || registerValue == 0xc8d) { type = SHT4X; logFoundDevice("SHT4X", (uint8_t)addr.address); } else if (getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x7E), 2) == 0x5449) { @@ -423,9 +423,21 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) logFoundDevice("BMA423", (uint8_t)addr.address); } break; + case TCA9535_ADDR: + case RAK120352_ADDR: + case RAK120353_ADDR: + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x02), 1); + if (registerValue == addr.address) { // RAK12035 returns its I2C address at 0x02 (eg 0x20) + type = RAK12035; + logFoundDevice("RAK12035", (uint8_t)addr.address); + } else { + type = TCA9535; + logFoundDevice("TCA9535", (uint8_t)addr.address); + } + + break; SCAN_SIMPLE_CASE(LSM6DS3_ADDR, LSM6DS3, "LSM6DS3", (uint8_t)addr.address); - SCAN_SIMPLE_CASE(TCA9535_ADDR, TCA9535, "TCA9535", (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(TSL25911_ADDR, TSL2591, "TSL2591", (uint8_t)addr.address); @@ -434,6 +446,8 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) SCAN_SIMPLE_CASE(MAX1704X_ADDR, MAX17048, "MAX17048", (uint8_t)addr.address); SCAN_SIMPLE_CASE(DFROBOT_RAIN_ADDR, DFROBOT_RAIN, "DFRobot Rain Gauge", (uint8_t)addr.address); SCAN_SIMPLE_CASE(LTR390UV_ADDR, LTR390UV, "LTR390UV", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address); + SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fba3e75e9..9c1b34e5a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1,9 +1,10 @@ /* +BaseUI -SSD1306 - Screen module - -Copyright (C) 2018 by Xose Pérez - +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) – Lead development and implementation. +- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) – Project management, structural planning, and testing This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,25 +28,36 @@ along with this program. If not, see . #include #include "DisplayFormatters.h" +#include "TimeFormatters.h" +#include "draw/ClockRenderer.h" +#include "draw/DebugRenderer.h" +#include "draw/MessageRenderer.h" +#include "draw/NodeListRenderer.h" +#include "draw/NotificationRenderer.h" +#include "draw/UIRenderer.h" +#include "modules/CannedMessageModule.h" + #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" +#include "buzz.h" #endif -#include "ButtonThread.h" +#include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" +#include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" #include "graphics/images.h" -#include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "meshUtils.h" -#include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" @@ -55,12 +67,19 @@ along with this program. If not, see . #include "sleep.h" #include "target_specific.h" +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif #ifdef ARCH_ESP32 +<<<<<<< store-and-forward #include "esp_task_wdt.h" +======= +>>>>>>> master #endif #if ARCH_PORTDUINO @@ -83,14 +102,10 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; +// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization uint32_t logo_timeout = 5000; // 4 seconds for EACH logo -uint32_t hours_in_month = 730; - -// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function -uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; - // Threshold values for the GPS lock accuracy bar display uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; @@ -98,13 +113,9 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; // we'll need to hold onto pointers for the modules that can draw a frame. std::vector moduleFrames; -// Stores the last 4 of our hardware ID, to make finding the device for pairing easier -static char ourId[5]; - -// vector where symbols (string) are displayed in bottom corner of display. +// Global variables for screen function overlay symbols std::vector functionSymbol; -// string displayed in bottom right corner of display. Created from elements in functionSymbol vector -std::string functionSymbolString = ""; +std::string functionSymbolString; #if HAS_GPS // GeoCoord object for the screen @@ -115,258 +126,38 @@ GeoCoord geoCoord; static bool heartbeat = false; #endif -// Quick access to screen dimensions from static drawing functions -// DEPRECATED. To-do: move static functions inside Screen class -#define SCREEN_WIDTH display->getWidth() -#define SCREEN_HEIGHT display->getHeight() - #include "graphics/ScreenFonts.h" #include -#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) +// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); +// End Functions to write date/time to the screen -// Check if the display can render a string (detect special chars; emoji) -static bool haveGlyphs(const char *str) +extern bool hasUnreadMessage; + +// ============================== +// Overlay Alert Banner Renderer +// ============================== +// Displays a temporary centered banner message (e.g., warning, status, etc.) +// The banner appears in the center of the screen and disappears after the specified duration + +// Called to trigger a banner with custom message and duration +void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function bannerCallback, + int8_t InitialSelected) { -#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) - // Don't want to make any assumptions about custom language support - return true; -#endif - - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ¿ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; - } - } - - LOG_DEBUG("haveGlyphs=%d", have); - return have; + // Store the message and set the expiration timestamp + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerOptions = options; + NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::curSelected = InitialSelected; + NotificationRenderer::pauseBanner = false; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); } -/** - * Draw the icon with extra info printed around the corners - */ -static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // draw an xbm image. - // Please note that everything that should be transitioned - // needs to be drawn relative to x and y - - // draw centered icon left to right and centered above the one line of app text - display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, - icon_width, icon_height, icon_bits); - - display->setFont(FONT_MEDIUM); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - // Draw version and short name in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code -} - -#ifdef USERPREFS_OEM_TEXT - -static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - - switch (USERPREFS_OEM_FONT_SIZE) { - case 0: - display->setFont(FONT_SMALL); - break; - case 2: - display->setFont(FONT_LARGE); - break; - default: - display->setFont(FONT_MEDIUM); - break; - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = USERPREFS_OEM_TEXT; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code -} - -static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawOEMIconScreen(region, display, state, x, y); -} - -#endif - -void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) -{ - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, message); -} - -// Used on boot when a certificate is being created -static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(64 + x, y, "Creating SSL certificate"); - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif - - display->setFont(FONT_SMALL); - if ((millis() / 1000) % 2) { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); - } else { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); - } -} - -// Used when booting without a region set -static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); - display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if ((millis() / 10000) % 2) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); - } else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); - } - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif -} - -// draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active -static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // LOG_DEBUG("Draw function overlay"); - if (functionSymbol.begin() != functionSymbol.end()) { - char buf[64]; - display->setFont(FONT_SMALL); - snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); - display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); - } -} - -#ifdef USE_EINK -/// Used on eink displays while in deep sleep -static void drawDeepSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - - // Next frame should use full-refresh, and block while running, else device will sleep before async callback - EINK_ADD_FRAMEFLAG(display, COSMETIC); - EINK_ADD_FRAMEFLAG(display, BLOCKING); - - LOG_DEBUG("Draw deep sleep screen"); - - // Display displayStr on the screen - drawIconScreen("Sleeping", display, state, x, y); -} - -/// Used on eink displays when screen updates are paused -static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - LOG_DEBUG("Draw screensaver overlay"); - - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh - - // Config - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *pauseText = "Screen Paused"; - const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name - constexpr uint16_t padding = 5; - constexpr uint8_t dividerGap = 1; - constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. - - // Dimensions - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars - const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; - - // Position - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); - // const int16_t boxRight = boxLeft + boxWidth - 1; - const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); - const int16_t boxBottom = boxTop + boxHeight - 1; - const int16_t idTextLeft = boxLeft + padding; - const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; - const int16_t pauseTextTop = boxTop + padding; - const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + 1 + dividerGap; - const int16_t dividerBottom = boxBottom - 1 - dividerGap; - - // Draw: box - display->setColor(EINK_WHITE); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box - display->setColor(EINK_BLACK); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - - // Draw: Text - if (useId) - display->drawString(idTextLeft, idTextTop, idText); - display->drawString(pauseTextLeft, pauseTextTop, pauseText); - display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold - - // Draw: divider - if (useId) - display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); -} -#endif - static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -389,875 +180,12 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int pi.drawFrame(display, state, x, y); } -static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, y, "Updating"); - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), - "Please be patient and do not power off."); -} - -/// Draw the last text message we received -static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - - char tempBuf[24]; - snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); - display->drawString(0 + x, 0 + y, tempBuf); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); -} - // Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) { return packet->from != 0 && !moduleConfig.store_forward.enabled; } -// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. -static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) -{ - static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; - static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - // Clear the bar area on the battery image - for (int i = 1; i < 14; i++) { - imgBuffer[i] = 0x81; - } - // If charging, draw a charging indicator - if (powerStatus->getIsCharging()) { - memcpy(imgBuffer + 3, lightning, 8); - // If not charging, Draw power bars - } else { - for (int i = 0; i < 4; i++) { - if (powerStatus->getBatteryChargePercent() >= 25 * i) - memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); - } - } - display->drawFastImage(x, y, 16, 8, imgBuffer); -} - -#if defined(DISPLAY_CLOCK_FRAME) - -void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - if (digitalMode) { - uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; - uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); - uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); - - display->drawCircle(centerX, centerY, radius); - display->drawCircle(centerX, centerY, radius + 1); - display->drawLine(centerX, centerY, centerX, centerY - radius + 3); - display->drawLine(centerX, centerY, centerX + radius - 3, centerY); - } else { - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentOneX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; - - uint16_t segmentFourX = x; - uint16_t segmentFourY = y + segmentHeight + 2; - - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } -} - -// Draw a digital clock -void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - if (hour == 0) { - hour = 12; - } - - // hours string - String hourString = String(hour); - - // minutes string - String minuteString = minute < 10 ? "0" + String(minute) : String(minute); - - String timeString = hourString + ":" + minuteString; - - // seconds string - String secondString = second < 10 ? "0" + String(second) : String(second); - - float scale = 1.5; - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // calculate hours:minutes string width - uint16_t timeStringWidth = timeString.length() * 5; - - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); - - if (character == ":") { - timeStringWidth += segmentHeight; - } else { - timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; - } - } - - // calculate seconds string width - uint16_t secondStringWidth = (secondString.length() * 12) + 4; - - // sum these to get total string width - uint16_t totalWidth = timeStringWidth + secondStringWidth; - - uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2); - - uint16_t startingHourMinuteTextX = hourMinuteTextX; - - uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); - - // iterate over characters in hours:minutes string and draw segmented characters - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); - - if (character == ":") { - drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); - - hourMinuteTextX += segmentHeight + 6; - } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character.toInt(), scale); - - hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; - } - - hourMinuteTextX += 5; - } - - // draw seconds string - display->setFont(FONT_MEDIUM); - display->drawString(startingHourMinuteTextX + timeStringWidth + 4, - (display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString); - } -} - -void Screen::drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - - uint16_t topAndBottomX = x + (4 * scale); - - uint16_t quarterCellHeight = cellHeight / 4; - - uint16_t topY = y + quarterCellHeight; - uint16_t bottomY = y + (quarterCellHeight * 3); - - display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); - display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); -} - -void Screen::drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) -{ - // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of - // segment {innerIndex + 1} - // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. - uint8_t numbers[10][7] = { - {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key - {0, 1, 1, 0, 0, 0, 0}, // 1 1 - {1, 1, 0, 1, 1, 0, 1}, // 2 ___ - {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 - {0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_| - {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 - {1, 0, 1, 1, 1, 1, 1}, // 6 |___| - {1, 1, 1, 0, 0, 1, 0}, // 7 - {1, 1, 1, 1, 1, 1, 1}, // 8 4 - {1, 1, 1, 1, 0, 1, 1}, // 9 - }; - - // the width and height of each segment's central rectangle: - // _____________________ - // ⋰| (only this part, |⋱ - // ⋰ | not including | ⋱ - // ⋱ | the triangles | ⋰ - // ⋱| on the ends) |⋰ - // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // segment x and y coordinates - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentTwoX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; - - uint16_t segmentFourX = segmentOneX; - uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; - - uint16_t segmentFiveX = x; - uint16_t segmentFiveY = segmentThreeY; - - uint16_t segmentSixX = x; - uint16_t segmentSixY = segmentTwoY; - - uint16_t segmentSevenX = segmentOneX; - uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - - if (numbers[number][0]) { - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - } - - if (numbers[number][1]) { - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - } - - if (numbers[number][2]) { - drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - } - - if (numbers[number][3]) { - drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } - - if (numbers[number][4]) { - drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - } - - if (numbers[number][5]) { - drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - } - - if (numbers[number][6]) { - drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); - } -} - -void Screen::drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, width, height); - - // draw end triangles - display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); - - display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); -} - -void Screen::drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, height, width); - - // draw end triangles - display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); - - display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); -} - -void Screen::drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) -{ - display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); -} - -// Draw an analog clock -void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - // clock face center coordinates - int16_t centerX = display->getWidth() / 2; - int16_t centerY = display->getHeight() / 2; - - // clock face radius - int16_t radius = (display->getWidth() / 2) * 0.8; - - // noon (0 deg) coordinates (outermost circle) - int16_t noonX = centerX; - int16_t noonY = centerY - radius; - - // second hand radius and y coordinate (outermost circle) - int16_t secondHandNoonY = noonY + 1; - - // tick mark outer y coordinate; (first nested circle) - int16_t tickMarkOuterNoonY = secondHandNoonY; - - // seconds tick mark inner y coordinate; (second nested circle) - double secondsTickMarkInnerNoonY = (double)noonY + 8; - - // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 16; - - // minute hand y coordinate - int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; - - // hour string y coordinate - int16_t hourStringNoonY = minuteHandNoonY + 18; - - // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.55; - int16_t hourHandNoonY = centerY - hourHandRadius; - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawCircle(centerX, centerY, radius); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - int16_t degreesPerHour = 30; - int16_t degreesPerMinuteOrSecond = 6; - - double hourBaseAngle = hour * degreesPerHour; - double hourAngleOffset = ((double)minute / 60) * degreesPerHour; - double hourAngle = radians(hourBaseAngle + hourAngleOffset); - - double minuteBaseAngle = minute * degreesPerMinuteOrSecond; - double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; - double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); - - double secondAngle = radians(second * degreesPerMinuteOrSecond); - - double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; - double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; - - double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; - double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; - - double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; - double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; - - display->setFont(FONT_MEDIUM); - - // draw minute and hour tick marks and hour numbers - for (uint16_t angle = 0; angle < 360; angle += 6) { - double angleInRadians = radians(angle); - - double sineAngleInRadians = sin(-angleInRadians); - double cosineAngleInRadians = cos(-angleInRadians); - - double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; - double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; - - if (angle % degreesPerHour == 0) { - double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; - - // draw hour tick mark - display->drawLine(startX, startY, endX, endY); - - static char buffer[2]; - - uint8_t hourInt = (angle / 30); - - if (hourInt == 0) { - hourInt = 12; - } - - // hour number x offset needs to be adjusted for some cases - int8_t hourStringXOffset; - int8_t hourStringYOffset = 13; - - switch (hourInt) { - case 3: - hourStringXOffset = 5; - break; - case 9: - hourStringXOffset = 7; - break; - case 10: - case 11: - hourStringXOffset = 8; - break; - case 12: - hourStringXOffset = 13; - break; - default: - hourStringXOffset = 6; - break; - } - - double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; - double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; - - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } - - if (angle % degreesPerMinuteOrSecond == 0) { - double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); - } - } - - // draw hour hand - display->drawLine(centerX, centerY, hourX, hourY); - - // draw minute hand - display->drawLine(centerX, centerY, minuteX, minuteY); - - // draw second hand - display->drawLine(centerX, centerY, secondX, secondY); - } -} - -#endif - -// Get an absolute time from "seconds ago" info. Returns false if no valid timestamp possible -bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) -{ - // Cache the result - avoid frequent recalculation - static uint8_t hoursCached = 0, minutesCached = 0; - static uint32_t daysAgoCached = 0; - static uint32_t secondsAgoCached = 0; - static bool validCached = false; - - // Abort: if timezone not set - if (strlen(config.device.tzdef) == 0) { - validCached = false; - return validCached; - } - - // Abort: if invalid pointers passed - if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { - validCached = false; - return validCached; - } - - // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) - if (secondsAgo > SEC_PER_DAY * 30UL * 6) { - validCached = false; - return validCached; - } - - // If repeated request, don't bother recalculating - if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { - if (validCached) { - *hours = hoursCached; - *minutes = minutesCached; - *daysAgo = daysAgoCached; - } - return validCached; - } - - // Get local time - uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time - - // Abort: if RTC not set - if (!secondsRTC) { - validCached = false; - return validCached; - } - - // Get absolute time when last seen - uint32_t secondsSeenAt = secondsRTC - secondsAgo; - - // Calculate daysAgo - *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed - - // Get seconds since midnight - uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into hours and minutes - *hours = hms / SEC_PER_HOUR; - *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - // Cache the result - daysAgoCached = *daysAgo; - hoursCached = *hours; - minutesCached = *minutes; - secondsAgoCached = secondsAgo; - - validCached = true; - return validCached; -} - -/// Draw the last text message we received -static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // the max length of this buffer is much longer than we can possibly print - static char tempBuf[237]; - - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - // LOG_DEBUG("Draw text message from 0x%x: %s", mp.from, - // mp.decoded.variant.data.decoded.bytes); - - // Demo for drawStringMaxWidth: - // with the third parameter you can define the width after which words will - // be wrapped. Currently only spaces and "-" are allowed for wrapping - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - // For time delta - uint32_t seconds = sinceReceived(&mp); - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - - // For timestamp - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); - - // If bold, draw twice, shifting right by one pixel - for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { - // Show a timestamp if received today, but longer than 15 minutes ago - if (useTimestamp && minutes >= 15 && daysAgo == 0) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "At %02hu:%02hu from %s", timestampHours, timestampMinutes, - (node && node->has_user) ? node->user.short_name : "???"); - } - // Timestamp yesterday (if display is wide enough) - else if (useTimestamp && daysAgo == 1 && display->width() >= 200) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "Yesterday %02hu:%02hu from %s", timestampHours, timestampMinutes, - (node && node->has_user) ? node->user.short_name : "???"); - } - // Otherwise, show a time delta - else { - display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", - screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); - } - } - - display->setColor(WHITE); -#ifndef EXCLUDE_EMOJI - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - if (strcmp(msg, "\U0001F44D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbup); - } else if (strcmp(msg, "\U0001F44E") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbdown); - } else if (strcmp(msg, "\U0001F60A") == 0 || strcmp(msg, "\U0001F600") == 0 || strcmp(msg, "\U0001F642") == 0 || - strcmp(msg, "\U0001F609") == 0 || - strcmp(msg, "\U0001F601") == 0) { // matches 5 different common smileys, so that the phone user doesn't have to - // remember which one is compatible - display->drawXbm(x + (SCREEN_WIDTH - smiley_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - smiley_height) / 2 + 2 + 5, smiley_width, smiley_height, - smiley); - } else if (strcmp(msg, "❓") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height, - question); - } else if (strcmp(msg, "‼️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5, - bang_width, bang_height, bang); - } else if (strcmp(msg, "\U0001F4A9") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5, - poo_width, poo_height, poo); - } else if (strcmp(msg, "\U0001F923") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5, - haha_width, haha_height, haha); - } else if (strcmp(msg, "\U0001F44B") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width, - wave_icon_height, wave_icon); - } else if (strcmp(msg, "\U0001F920") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height, - cowboy); - } else if (strcmp(msg, "\U0001F42D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height, - deadmau5); - } else if (strcmp(msg, "\xE2\x98\x80\xEF\xB8\x8F") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5, - sun_width, sun_height, sun); - } else if (strcmp(msg, "\u2614") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10, - rain_width, rain_height, rain); - } else if (strcmp(msg, "☁️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud); - } else if (strcmp(msg, "🌫️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5, - fog_width, fog_height, fog); - } else if (strcmp(msg, "\U0001F608") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil); - } else if (strcmp(msg, "♥️") == 0 || strcmp(msg, "\U0001F9E1") == 0 || strcmp(msg, "\U00002763") == 0 || - strcmp(msg, "\U00002764") == 0 || strcmp(msg, "\U0001F495") == 0 || strcmp(msg, "\U0001F496") == 0 || - strcmp(msg, "\U0001F497") == 0 || strcmp(msg, "\U0001F498") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart); - } else { - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); - } -#else - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); -#endif -} - -/// Draw a series of fields in a column, wrapping to multiple columns if needed -void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) -{ - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const char **f = fields; - int xo = x, yo = y; - while (*f) { - display->drawString(xo, yo, *f); - if ((display->getColor() == BLACK) && config.display.heading_bold) - display->drawString(xo + 1, yo, *f); - - display->setColor(WHITE); - yo += FONT_HEIGHT_SMALL; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { - xo += SCREEN_WIDTH / 2; - yo = 0; - } - f++; - } -} - -// Draw nodes status -static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus) -{ - char usersString[20]; - snprintf(usersString, sizeof(usersString), "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); -#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(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, y + 3, 8, 8, imgUser); -#else - display->drawFastImage(x, y, 8, 8, imgUser); -#endif - display->drawString(x + 10, y - 2, usersString); - if (config.display.heading_bold) - display->drawString(x + 11, y - 2, usersString); -} -#if HAS_GPS -// Draw GPS status summary -static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - if (config.position.fixed_position) { - // GPS coordinates are currently fixed - display->drawString(x - 1, y - 2, "Fixed GPS"); - if (config.display.heading_bold) - display->drawString(x, y - 2, "Fixed GPS"); - return; - } - if (!gps->getIsConnected()) { - display->drawString(x, y - 2, "No GPS"); - if (config.display.heading_bold) - display->drawString(x + 1, y - 2, "No GPS"); - return; - } - display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); - if (!gps->getHasLock()) { - display->drawString(x + 8, y - 2, "No sats"); - if (config.display.heading_bold) - display->drawString(x + 9, y - 2, "No sats"); - return; - } else { - char satsString[3]; - uint8_t bar[2] = {0}; - - // Draw DOP signal bars - for (int i = 0; i < 5; i++) { - if (gps->getDOP() <= dopThresholds[i]) - bar[0] = ~((1 << (5 - i)) - 1); - else - bar[0] = 0b10000000; - // bar[1] = bar[0]; - display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); - } - - // Draw satellite image - display->drawFastImage(x + 24, y, 8, 8, imgSatellite); - - // Draw the number of satellites - snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - display->drawString(x + 34, y - 2, satsString); - if (config.display.heading_bold) - display->drawString(x + 35, y - 2, satsString); - } -} - -// Draw status when GPS is disabled or not present -static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine; - int pos; - if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - pos = SCREEN_WIDTH - display->getStringWidth(displayLine); - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" - : "GPS is disabled"; - pos = (SCREEN_WIDTH - display->getStringWidth(displayLine)) / 2; - } - display->drawString(x + pos, y, displayLine); -} - -static void drawGPSAltitude(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine = ""; - if (!gps->getIsConnected() && !config.position.fixed_position) { - // displayLine = "No GPS Module"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - // displayLine = "No GPS Lock"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } -} - -// Draw GPS status coordinates -static void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - auto gpsFormat = config.display.gps_format; - String displayLine = ""; - - if (!gps->getIsConnected() && !config.position.fixed_position) { - displayLine = "No GPS present"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - displayLine = "No GPS Lock"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - - if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { - char coordinateLine[22]; - if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, - geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), - geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), - geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region - snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); - else - snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); - } - - // If fixed position, display text "Fixed GPS" alternating with the coordinates. - if (config.position.fixed_position) { - if ((millis() / 10000) % 2) { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); - } - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } - } else { - char latLine[22]; - char lonLine[22]; - snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, latLine); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(lonLine))) / 2, y, lonLine); - } - } -} -#endif /** * Given a recent lat/lon return a guess of the heading the user is walking on. * @@ -1290,239 +218,10 @@ float Screen::estimatedHeading(double lat, double lon) /// We will skip one node - the one for us, so we just blindly loop over all /// nodes -static size_t nodeIndex; static int8_t prevFrame = -1; -// Draw the arrow pointing to a node's location -void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) -{ - Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; - Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); - - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } - /* Old arrow - display->drawLine(tip.x, tip.y, tail.x, tail.y); - display->drawLine(leftArrow.x, leftArrow.y, tip.x, tip.y); - display->drawLine(rightArrow.x, rightArrow.y, tip.x, tip.y); - display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); - display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); - */ -#ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); -} - -// Get a string representation of the time passed since something happened -void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) -{ - // Use an absolute timestamp in some cases. - // Particularly useful with E-Ink displays. Static UI, fewer refreshes. - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); - - if (agoSecs < 120) // last 2 mins? - snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); - // -- if suitable for timestamp -- - else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); - else if (useTimestamp && daysAgo == 0) // Today - snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); - else if (useTimestamp && daysAgo == 1) // Yesterday - snprintf(timeStr, maxLength, "Seen yesterday"); - else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) - snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); - // -- if using time delta instead -- - else if (agoSecs < 120 * 60) // last 2 hrs - snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) - snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); - else - snprintf(timeStr, maxLength, "unknown age"); -} - -void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) -{ - // If north is supposed to be at the top of the compass we want rotation to be +0 - if (config.display.compass_north_top) - myHeading = -0; - /* N sign points currently not deleted*/ - Point N1(-0.04f, 0.65f), N2(0.04f, 0.65f); // N sign points (N1-N4) - Point N3(-0.04f, 0.55f), N4(0.04f, 0.55f); - Point NC1(0.00f, 0.50f); // north circle center point - Point *rosePoints[] = {&N1, &N2, &N3, &N4, &NC1}; - - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - - for (int i = 0; i < 5; i++) { - // North on compass will be negative of heading - rosePoints[i]->rotate(-myHeading); - rosePoints[i]->scale(compassDiam); - rosePoints[i]->translate(compassX, compassY); - } - - /* changed the N sign to a small circle on the compass circle. - display->drawLine(N1.x, N1.y, N3.x, N3.y); - display->drawLine(N2.x, N2.y, N4.x, N4.y); - display->drawLine(N1.x, N1.y, N4.x, N4.y); - */ - display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. -} - -uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) -{ - uint16_t diam = 0; - uint16_t offset = 0; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - offset = FONT_HEIGHT_SMALL; - - // get the smaller of the 2 dimensions and subtract 20 - if (displayWidth > (displayHeight - offset)) { - diam = displayHeight - offset; - // if 2/3 of the other size would be smaller, use that - if (diam > (displayWidth * 2 / 3)) { - diam = displayWidth * 2 / 3; - } - } else { - diam = displayWidth; - if (diam > ((displayHeight - offset) * 2 / 3)) { - diam = (displayHeight - offset) * 2 / 3; - } - } - - return diam - 20; -}; - -static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // We only advance our nodeIndex if the frame # has changed - because - // drawNodeInfo will be called repeatedly while the frame is shown - if (state->currentFrame != prevFrame) { - prevFrame = state->currentFrame; - - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); - if (n->num == nodeDB->getNodeNum()) { - // Don't show our node, just skip to next - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - n = nodeDB->getMeshNodeByIndex(nodeIndex); - } - } - - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); - - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - } - - const char *username = node->has_user ? node->user.long_name : "Unknown Name"; - - static char signalStr[20]; - - // section here to choose whether to display hops away rather than signal strength if more than 0 hops away. - if (node->hops_away > 0) { - snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); - } else { - snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); - } - - static char lastStr[20]; - screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); - - static char distStr[20]; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - strncpy(distStr, "? mi ?°", sizeof(distStr)); // might not have location data - } else { - strncpy(distStr, "? km ?°", sizeof(distStr)); - } - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - - // coordinates for the center of the compass/circle - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + SCREEN_HEIGHT / 2; - } else { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - FONT_HEIGHT_SMALL) / 2; - } - bool hasNodeHeading = false; - - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - if (nodeDB->hasValidPosition(node)) { - // display direction toward node - hasNodeHeading = true; - const meshtastic_PositionLite &p = node->position; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (!config.display.compass_north_top) - bearingToOther -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; - - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - if (d < (2 * MILES_TO_FEET)) - snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, - bearingToOtherDegrees); - } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees); - } - } - } - if (!hasNodeHeading) { - // direction to node is unknown so display question mark - // Debug info for gps lock errors - // LOG_DEBUG("ourNode %d, ourPos %d, theirPos %d", !!ourNode, ourNode && hasValidPosition(ourNode), - // hasValidPosition(node)); - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); - } - display->drawCircle(compassX, compassY, compassDiam / 2); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); -} +// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes +// Uses a single frame and changes data every few seconds (E-Ink variant is separate) #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); @@ -1541,6 +240,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O ST7789_MISO, ST7789_SCK); #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + static_cast(dispdev)->setRGB(COLOR565(255, 255, 128)); #endif #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, @@ -1558,15 +258,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[displayPanel] != no_screen) { - LOG_DEBUG("Make TFTDisplay!"); - dispdev = new TFTDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - } else { - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; +#elif ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[displayPanel] != no_screen) { + LOG_DEBUG("Make TFTDisplay!"); + dispdev = new TFTDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + } else { + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; + } } #else dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, @@ -1590,7 +292,7 @@ Screen::~Screen() void Screen::doDeepSleep() { #ifdef USE_EINK - setOn(false, drawDeepSleepScreen); + setOn(false, graphics::UIRenderer::drawDeepSleepFrame); #ifdef PIN_EINK_EN digitalWrite(PIN_EINK_EN, LOW); // power off backlight #endif @@ -1608,7 +310,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) if (on != screenOn) { if (on) { LOG_INFO("Turn on screen"); - buttonThread->setScreenFlag(true); powerMon->setState(meshtastic_PowerMon_State_Screen_On); #ifdef T_WATCH_S3 PMU->enablePowerOutput(XPOWERS_ALDO2); @@ -1652,8 +353,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) // eInkScreensaver parameter is usually NULL (default argument), default frame used instead setScreensaverFrames(einkScreensaver); #endif - LOG_INFO("Turn off screen"); - buttonThread->setScreenFlag(false); #ifdef ELECROW_ThinkNode_M1 if (digitalRead(PIN_EINK_EN) == HIGH) { digitalWrite(PIN_EINK_EN, LOW); @@ -1688,10 +387,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) void Screen::setup() { - // We don't set useDisplay until setup() is called, because some boards have a declaration of this object but the device - // is never found when probing i2c and therefore we don't call setup and never want to do (invalid) accesses to this device. + // === Enable display rendering === useDisplay = true; + // === Detect OLED subtype (if supported by board variant) === #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -1702,66 +401,62 @@ void Screen::setup() #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Heltec T114 and T190: honor a custom text color, if defined in variant.h + // Apply custom RGB color (e.g. Heltec T114/T190) static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialising the UI will init the display too. + // === Initialize display and UI system === ui->init(); - displayWidth = dispdev->width(); displayHeight = dispdev->height(); - ui->setTimePerTransition(0); + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - ui->setIndicatorPosition(BOTTOM); - // Defines where the first frame is located in the bar. - ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT); - // Don't show the page swipe dots while in boot screen. - ui->disableAllIndicators(); - // Store a pointer to Screen so we can get to it from static functions. - ui->getUiState()->userData = this; + // === Set custom overlay callbacks === + static OverlayCallback overlays[] = { + graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc. + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // Set the utf8 conversion function + // === Enable UTF-8 to display mapping === dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Double the time if we have a custom logo + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Add frames. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + // === Configure alert frames (e.g., "Resuming..." or region name) === + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { - drawFrameText(display, state, x, y, "Resuming..."); - } else + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) + graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); + else #endif { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawIconScreen(region, display, state, x, y); + const char *region = myRegion ? myRegion->name : nullptr; + graphics::UIRenderer::drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); - // No overlays. - ui->setOverlays(nullptr, 0); + ui->disableAutoTransition(); // Require manual navigation between frames - // Require presses to switch between frames. - ui->disableAutoTransition(); - - // Set up a log buffer with 3 lines, 32 chars each. + // === Log buffer for on-screen logs (3 lines max) === dispdev->setLogBuffer(3, 32); + // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) === #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else - // Standard behaviour is to FLIP the screen (needed on T-Beam). If this config item is set, unflip it, and thereby logically - // flip it. If you have a headache now, you're welcome. if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \ - defined(ST7789_CS) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_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) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); @@ -1771,30 +466,30 @@ void Screen::setup() } #endif - // Get our hardware ID + // === Generate device ID from MAC address === uint8_t dmac[6]; getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + #if ARCH_PORTDUINO - handleSetOn(false); // force clean init + handleSetOn(false); // Ensure proper init for Arduino targets #endif - // Turn on the display. + // === Turn on display and trigger first draw === handleSetOn(true); - - // On some ssd1306 clones, the first draw command is discarded, so draw it - // twice initially. Skip this for EINK Displays to save a few seconds during boot ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); -#if ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[touchscreenModule]) { + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); + } } #elif HAS_TOUCHSCREEN touchScreenImpl1 = @@ -1802,10 +497,11 @@ void Screen::setup() touchScreenImpl1->init(); #endif - // Subscribe to status updates + // === Subscribe to device status updates === powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); + #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif @@ -1814,7 +510,7 @@ void Screen::setup() if (inputBroker) inputObserver.observe(inputBroker); - // Modules can notify screen about refresh + // === Notify modules that support UI events === MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -1882,7 +578,7 @@ int32_t Screen::runOnce() if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { LOG_INFO("Switch to OEM screen..."); // Change frames. - static FrameCallback bootOEMFrames[] = {drawOEMBootScreen}; + static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); ui->setFrames(bootOEMFrames, bootOEMFrameCount); ui->update(); @@ -1894,10 +590,13 @@ int32_t Screen::runOnce() #endif #ifndef DISABLE_WELCOME_UNSET - if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - setWelcomeFrames(); + if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + LoraRegionPicker(0); } #endif + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + showOverlayBanner("Rebooting...", 0); + } // Process incoming commands. for (;;) { @@ -1924,6 +623,7 @@ int32_t Screen::runOnce() case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away showingNormalScreen = false; + NotificationRenderer::pauseBanner = true; alertFrames[0] = alertFrame; #ifdef USE_EINK EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please @@ -1937,14 +637,11 @@ int32_t Screen::runOnce() handleStartFirmwareUpdateScreen(); break; case Cmd::STOP_ALERT_FRAME: + NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame setFrames(); break; - case Cmd::PRINT: - handlePrint(cmd.print_text); - free(cmd.print_text); - break; default: LOG_ERROR("Invalid screen cmd"); } @@ -1963,6 +660,7 @@ int32_t Screen::runOnce() // Switch to a low framerate (to save CPU) when we are not in transition // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; targetFramerate = IDLE_FRAMERATE; @@ -1978,8 +676,8 @@ int32_t Screen::runOnce() if (config.display.auto_screen_carousel_secs > 0 && !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 -// Carousel is potentially a major source of E-Ink display wear + // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead + // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif @@ -1997,47 +695,18 @@ int32_t Screen::runOnce() return (1000 / targetFramerate); } -void Screen::drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrame(display, state, x, y); -} - -void Screen::drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameSettings(display, state, x, y); -} - -void Screen::drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameWiFi(display, state, x, y); -} - /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ void Screen::setSSLFrames() { if (address_found.address) { // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {drawSSLScreen}; + static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); ui->update(); } } -/* show a message that the SSL cert is being built - * it is expected that this will be used during the boot phase */ -void Screen::setWelcomeFrames() -{ - if (address_found.address) { - // LOG_DEBUG("Show Welcome frames"); - static FrameCallback frames[] = {drawWelcomeScreen}; - setFrameImmediateDraw(frames); - } -} - #ifdef USE_EINK /// Determine which screensaver frame to use, then set the FrameCallback void Screen::setScreensaverFrames(FrameCallback einkScreensaver) @@ -2060,7 +729,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Else, display the usual "overlay" screensaver else { - screensaverOverlay = drawScreensaverOverlay; + screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; ui->setOverlays(&screensaverOverlay, 1); } @@ -2096,33 +765,17 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) void Screen::setFrames(FrameFocus focus) { uint8_t originalPosition = ui->getUiState()->currentFrame; + uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter LOG_DEBUG("Show standard frames"); showingNormalScreen = true; -#ifdef USE_EINK - // If user has disabled the screensaver, warn them after boot - static bool warnedScreensaverDisabled = false; - if (config.display.screen_on_secs == 0 && !warnedScreensaverDisabled) { - screen->print("Screensaver disabled\n"); - warnedScreensaverDisabled = true; - } -#endif - - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); - LOG_DEBUG("Show %d module frames", moduleFrames.size()); -#ifdef DEBUG_PORT - int totalFrameCount = MAX_NUM_NODES + NUM_EXTRA_FRAMES + moduleFrames.size(); - LOG_DEBUG("Total frame count: %d", totalFrameCount); -#endif - - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; + indicatorIcons.clear(); size_t numframes = 0; + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); + LOG_DEBUG("Show %d module frames", moduleFrames.size()); // put all of the module frames first. // this is a little bit of a dirty hack; since we're going to call @@ -2137,14 +790,12 @@ void Screen::setFrames(FrameFocus focus) // Check if the module being drawn has requested focus // We will honor this request later, if setFrames was triggered by a UIFrameEvent MeshModule *m = *i; - if (m->isRequestingFocus()) { + if (m->isRequestingFocus()) fsi.positions.focusedModule = numframes; - } - - // Identify the position of specific modules, if we need to know this later if (m == waypointModule) fsi.positions.waypoint = numframes; + indicatorIcons.push_back(icon_module); numframes++; } @@ -2153,55 +804,103 @@ void Screen::setFrames(FrameFocus focus) // If we have a critical fault, show it first fsi.positions.fault = numframes; if (error_code) { - normalFrames[numframes++] = drawCriticalFaultFrame; + normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } #if defined(DISPLAY_CLOCK_FRAME) - normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; + fsi.positions.clock = numframes; + normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame + : &graphics::ClockRenderer::drawAnalogClockFrame; + indicatorIcons.push_back(icon_clock); #endif - // If we have a text message - show it next, unless it's a phone message and we aren't using any special modules - if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = drawTextMessageFrame; + // Declare this early so it’s available in FOCUS_PRESERVE block + bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); + + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); + +#ifndef USE_EINK + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); +#endif + +// Show detailed node views only on E-Ink builds +#ifdef USE_EINK + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); +#endif +#if HAS_GPS + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); +#endif + if (RadioLibInterface::instance) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!dismissedFrames.memory) { + fsi.positions.memory = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; + indicatorIcons.push_back(icon_memory); + } +#if !defined(DISPLAY_CLOCK_FRAME) + fsi.positions.clock = numframes; + normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(icon_clock); +#endif + + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + if (fsi.positions.firstFavorite == 255) + fsi.positions.firstFavorite = numframes; + fsi.positions.lastFavorite = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo; + indicatorIcons.push_back(icon_node); + } } - // then all the nodes - // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens - size_t numToShow = min(numMeshNodes, 4U); - for (size_t i = 0; i < numToShow; i++) - normalFrames[numframes++] = drawNodeInfo; - - // then the debug info - // - // Since frames are basic function pointers, we have to use a helper to - // call a method on debugInfo object. - fsi.positions.log = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; - - // call a method on debugInfoScreen object (for more details) - fsi.positions.settings = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; - - fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (isWifiAvailable()) { - // call a method on debugInfoScreen object (for more details) - normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + if (!dismissedFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); } #endif - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // ✅ Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); - ui->enableAllIndicators(); + ui->disableAllIndicators(); - // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; - static const int functionOverlayCount = sizeof(functionOverlay) / sizeof(functionOverlay[0]); - ui->setOverlays(functionOverlay, functionOverlayCount); + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list // just changed) @@ -2209,12 +908,13 @@ void Screen::setFrames(FrameFocus focus) // Focus on a specific frame, in the frame set we just created switch (focus) { case FOCUS_DEFAULT: - ui->switchToFrame(0); // First frame + ui->switchToFrame(fsi.positions.deviceFocused); break; case FOCUS_FAULT: ui->switchToFrame(fsi.positions.fault); break; case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // ✅ Clear when message is *viewed* ui->switchToFrame(fsi.positions.textMessage); break; case FOCUS_MODULE: @@ -2224,31 +924,14 @@ void Screen::setFrames(FrameFocus focus) break; case FOCUS_PRESERVE: - // If we can identify which type of frame "originalPosition" was, can move directly to it in the new frameset - const FramesetInfo &oldFsi = this->framesetInfo; - if (originalPosition == oldFsi.positions.log) - ui->switchToFrame(fsi.positions.log); - else if (originalPosition == oldFsi.positions.settings) - ui->switchToFrame(fsi.positions.settings); - else if (originalPosition == oldFsi.positions.wifi) - ui->switchToFrame(fsi.positions.wifi); - - // If frame count has decreased - else if (fsi.frameCount < oldFsi.frameCount) { - uint8_t numDropped = oldFsi.frameCount - fsi.frameCount; - // Move n frames backwards - if (numDropped <= originalPosition) - ui->switchToFrame(originalPosition - numDropped); - // Unless that would put us "out of bounds" (< 0) - else - ui->switchToFrame(0); - } - - // If we're not sure exactly which frame we were on, at least return to the same frame number - // (node frames; module frames) - else + // No more adjustment — force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { ui->switchToFrame(originalPosition); - + } break; } @@ -2276,18 +959,25 @@ void Screen::dismissCurrentFrame() if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { LOG_INFO("Dismiss Text Message"); devicestate.has_rx_text_message = false; - dismissed = true; - } - - else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; + dismissedFrames.waypoint = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.wifi) { + LOG_DEBUG("Dismiss WiFi Screen"); + dismissedFrames.wifi = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.memory) { + LOG_INFO("Dismiss Memory"); + dismissedFrames.memory = true; dismissed = true; } - // If we did make changes to dismiss, we now need to regenerate the frameset - if (dismissed) - setFrames(); + if (dismissed) { + setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE + } } void Screen::handleStartFirmwareUpdateScreen() @@ -2296,7 +986,7 @@ void Screen::handleStartFirmwareUpdateScreen() showingNormalScreen = false; EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {drawFrameFirmware}; + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; setFrameImmediateDraw(frames); } @@ -2363,41 +1053,8 @@ void Screen::removeFunctionSymbol(std::string sym) setFastFramerate(); } -std::string Screen::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) -{ - std::string uptime; - - if (days > (hours_in_month * 6)) - uptime = "?"; - else if (days >= 2) - uptime = std::to_string(days) + "d"; - else if (hours >= 2) - uptime = std::to_string(hours) + "h"; - else if (minutes >= 1) - uptime = std::to_string(minutes) + "m"; - else - uptime = std::to_string(seconds) + "s"; - return uptime; -} - -void Screen::handlePrint(const char *text) -{ - // the string passed into us probably has a newline, but that would confuse the logging system - // so strip it - LOG_DEBUG("Screen: %.*s", strlen(text) - 1, text); - if (!useDisplay || !showingNormalScreen) - return; - - dispdev->print(text); -} - void Screen::handleOnPress() { - // If Canned Messages is using the "Scan and Select" input, dismiss the canned message frame when user button is pressed - // Minimize impact as a courtesy, as "scan and select" may be used as default config for some boards - if (scanAndSelectInput != nullptr && scanAndSelectInput->dismissCannedMessageFrame()) - return; - // If screen was off, just wake it, otherwise advance to next frame // If we are in a transition, the press must have bounced, drop it. if (ui->getUiState()->frameState == FIXED) { @@ -2443,6 +1100,7 @@ void Screen::setFastFramerate() runASAP = true; } +<<<<<<< store-and-forward void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setFont(FONT_SMALL); @@ -2761,6 +1419,8 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat #endif } +======= +>>>>>>> master int Screen::handleStatusUpdate(const meshtastic::Status *arg) { // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); @@ -2776,16 +1436,58 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) return 0; } +// Handles when message is received; will jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { - // Outgoing message - if (packet->from == 0) - setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + dismissedFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies - // Incoming message - else - setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + } else { + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + forceDisplay(); // Forces screen redraw + + // === Prepare banner content === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + + char banner[256]; + + // Check for bell character in message to determine alert type + bool isAlert = false; + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } + } + + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); + } else { + strcpy(banner, "Alert Received"); + } + } else { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "New Message from\n%s", longName); + } else { + strcpy(banner, "New Message"); + } + } + + screen->showOverlayBanner(banner, 3000); + } } return 0; @@ -2813,20 +1515,39 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { + if (!screenOn) + return 0; -#if defined(DISPLAY_CLOCK_FRAME) - // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button - uint8_t watchFaceFrame = error_code ? 1 : 0; - - if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && - event->touchY >= 204 && event->touchY <= 240) { - screen->digitalWatchFace = !screen->digitalWatchFace; - - setFrames(); - +#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, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP +#endif + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = event->inputEvent; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + NotificationRenderer::drawAlertBannerOverlay}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); return 0; } -#endif + /* + #if defined(DISPLAY_CLOCK_FRAME) + // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button + uint8_t watchFaceFrame = error_code ? 1 : 0; + + if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && + event->touchY >= 204 && event->touchY <= 240) { + screen->digitalWatchFace = !screen->digitalWatchFace; + + setFrames(); + + return 0; + } + #endif + */ // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose @@ -2841,22 +1562,154 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { showPrevFrame(); - else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { showNextFrame(); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + const char *banner_message; + int options; + if (kb_found) { + banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg"; + options = 4; + } else { + banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg"; + options = 3; + } + showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { + if (selected == 1) { + screen->setOn(false); + } else if (selected == 2) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == 3) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }); +#if HAS_TFT + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void { + if (selected == 0) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }); +#else + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + showOverlayBanner( + "Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4, + [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }, + config.device.buzzer_mode); +#endif +#if HAS_GPS + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + showOverlayBanner( + "Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3, + [](int selected) -> void { + if (selected == 1) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + playGPSEnableBeep(); + gps->enable(); + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + playGPSDisableBeep(); + gps->disable(); + service->reloadConfig(SEGMENT_CONFIG); + } + }, + config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 + : 2); // set inital selection +#endif + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + TZPicker(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { + LoraRegionPicker(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && + devicestate.rx_text_message.from) { + const char *banner_message; + int options; + if (kb_found) { + banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext"; + options = 4; + } else { + banner_message = "Message Action?\nBack\nDismiss\nReply via Preset"; + options = 3; + } +#ifdef HAS_I2S + banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud"; + options = 5; +#endif + showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { + if (selected == 1) { + screen->dismissCurrentFrame(); + } else if (selected == 2) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, + devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + } + } else if (selected == 3) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, + devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } +#ifdef HAS_I2S + else if (selected == 4) { + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + audioThread->readAloud(msg); + } +#endif + }); + } else if (framesetInfo.positions.firstFavorite != 255 && + this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + const char *banner_message; + int options; + if (kb_found) { + banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg"; + options = 3; + } else { + banner_message = "Message Node?\nCancel\nConfirm"; + options = 2; + } + showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { + if (selected == 1) { + cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if (selected == 2) { + cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } + }); + } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); + } } } return 0; } -int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { - switch (arg->which_payload_variant) { + switch (arg->request->which_payload_variant) { // Node removed manually (i.e. via app) case meshtastic_AdminMessage_remove_by_nodenum_tag: setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; break; // Default no-op, in case the admin message observable gets used by other classes in future @@ -2866,7 +1719,103 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) return 0; } +bool Screen::isOverlayBannerShowing() +{ + return NotificationRenderer::isOverlayBannerShowing(); +} + +void Screen::LoraRegionPicker(uint32_t duration) +{ + showOverlayBanner( + "Set the LoRa " + "region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_" + "919\nSG_" + "923\nPH_433\nPH_868\nPH_915\nANZ_433", + duration, 23, + [](int selected) -> void { + if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { + config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + // This is needed as we wait til picking the LoRa region to generate keys for the first time. + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }, + 0); +} + +void Screen::TZPicker() +{ + showOverlayBanner( + "Pick " + "Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/" + "Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ", + 30000, 17, [](int selected) -> void { + if (selected == 1) { // Hawaii + strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); + } else if (selected == 2) { // Alaska + strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 3) { // Pacific + strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 4) { // Mountain + strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 5) { // Central + strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 6) { // Eastern + strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 7) { // UTC + strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); + } else if (selected == 8) { // EU/Western + strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); + } else if (selected == 9) { // EU/Central + strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); + } else if (selected == 10) { // EU/Eastern + strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); + } else if (selected == 11) { // Asia/Kolkata + strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); + } else if (selected == 12) { // China + strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); + } else if (selected == 13) { // AU/AWST + strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); + } else if (selected == 14) { // AU/ACST + strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); + } else if (selected == 15) { // AU/AEST + strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); + } else if (selected == 16) { // NZ + strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); + } + if (selected != 0) { + setenv("TZ", config.device.tzdef, 1); + service->reloadConfig(SEGMENT_CONFIG); + } + }); +} + } // namespace graphics + #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ce416156f..8a836edfc 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -5,6 +5,10 @@ #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/config.pb.h" #include +#include +#include + +#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) #if !HAS_SCREEN #include "power.h" @@ -14,11 +18,18 @@ namespace graphics class Screen { public: + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + }; + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); void onPress() {} void setup() {} void setOn(bool) {} - void print(const char *) {} void doDeepSleep() {} void forceDisplay(bool forceUiUpdate = false) {} void startFirmwareUpdateScreen() {} @@ -27,6 +38,11 @@ class Screen void setFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} + void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, + std::function bannerCallback = NULL, int8_t InitialSelected = 0) + { + } + void setFrames(FrameFocus focus) {} void endAlert() {} }; } // namespace graphics @@ -62,8 +78,10 @@ class Screen #include "concurrency/OSThread.h" #include "input/InputBroker.h" #include "mesh/MeshModule.h" +#include "modules/AdminModule.h" #include "power.h" #include +#include // 0 to 255, though particular variants might define different defaults #ifndef BRIGHTNESS_DEFAULT @@ -90,7 +108,7 @@ class Screen /// Convert an integer GPS coords to a floating point #define DegD(i) (i * 1e-7) - +extern bool hasUnreadMessage; namespace { /// A basic 2D point class for drawing @@ -176,14 +194,28 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Screen::handleAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); public: explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - + size_t frameCount = 0; // Total number of active frames ~Screen(); + // Which frame we want to be displayed, after we regen the frameset by calling setFrames + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + }; + + // Regenerate the normal set of frames, focusing a specific frame if requested + // Call when a frame should be added / removed, or custom frames should be cleared + void setFrames(FrameFocus focus = FOCUS_DEFAULT); + + std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; @@ -191,6 +223,12 @@ class Screen : public concurrency::OSThread meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; + bool isOverlayBannerShowing(); + + // Stores the last 4 of our hardware ID, to make finding the device for pairing easier + // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class + char ourId[5]; + /// Initializes the UI, turns on the display, starts showing boot screen. // // Not thread safe - must be called before any other methods are called. @@ -214,21 +252,9 @@ class Screen : public concurrency::OSThread void blink(); - void drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *); - - void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); - // Draw north - void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); - - static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); - float estimatedHeading(double lat, double lon); - void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); - - void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); - /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } @@ -260,6 +286,9 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } + void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, + std::function bannerCallback = NULL, int8_t InitialSelected = 0); + void startFirmwareUpdateScreen() { ScreenCmd cmd; @@ -292,23 +321,6 @@ class Screen : public concurrency::OSThread /// Stops showing the boot screen. void stopBootScreen() { enqueueCmd(ScreenCmd{.cmd = Cmd::STOP_BOOT_SCREEN}); } - /// Writes a string to the screen. - void print(const char *text) - { - ScreenCmd cmd; - cmd.cmd = Cmd::PRINT; - // TODO(girts): strdup() here is scary, but we can't use std::string as - // FreeRTOS queue is just dumbly copying memory contents. It would be - // nice if we had a queue that could copy objects by value. - cmd.print_text = strdup(text); - if (!enqueueCmd(cmd)) { - free(cmd.print_text); - } - } - - /// generates a very brief time delta display - std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); - /// Overrides the default utf8 character conversion, to replace empty space with question marks static char customFontTableLookup(const uint8_t ch) { @@ -533,7 +545,7 @@ class Screen : public concurrency::OSThread int handleTextMessage(const meshtastic_MeshPacket *arg); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); - int handleAdminMessage(const meshtastic_AdminMessage *arg); + int handleAdminMessage(AdminModule_ObserverData *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(bool forceUiUpdate = false); @@ -541,8 +553,6 @@ class Screen : public concurrency::OSThread /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); - void setWelcomeFrames(); - // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) void dismissCurrentFrame(); @@ -591,8 +601,9 @@ class Screen : public concurrency::OSThread void handleOnPress(); void handleShowNextFrame(); void handleShowPrevFrame(); - void handlePrint(const char *text); void handleStartFirmwareUpdateScreen(); + void TZPicker(); + void LoraRegionPicker(uint32_t duration = 30000); // Info collected by setFrames method. // Index location of specific frames. @@ -600,30 +611,32 @@ class Screen : public concurrency::OSThread // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo struct FramesetInfo { struct FramePositions { - uint8_t fault = 0; - uint8_t textMessage = 0; - uint8_t waypoint = 0; - uint8_t focusedModule = 0; - uint8_t log = 0; - uint8_t settings = 0; - uint8_t wifi = 0; + uint8_t fault = 255; + uint8_t textMessage = 255; + uint8_t waypoint = 255; + uint8_t focusedModule = 255; + uint8_t log = 255; + uint8_t settings = 255; + uint8_t wifi = 255; + uint8_t deviceFocused = 255; + uint8_t memory = 255; + uint8_t gps = 255; + uint8_t home = 255; + uint8_t clock = 255; + uint8_t firstFavorite = 255; + uint8_t lastFavorite = 255; + uint8_t lora = 255; } positions; uint8_t frameCount = 0; } framesetInfo; - // Which frame we want to be displayed, after we regen the frameset by calling setFrames - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_TEXTMESSAGE, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - }; - - // Regenerate the normal set of frames, focusing a specific frame if requested - // Call when a frame should be added / removed, or custom frames should be cleared - void setFrames(FrameFocus focus = FOCUS_DEFAULT); + struct DismissedFrames { + bool textMessage = false; + bool waypoint = false; + bool wifi = false; + bool memory = false; + } dismissedFrames; /// Try to start drawing ASAP void setFastFramerate(); @@ -631,34 +644,6 @@ class Screen : public concurrency::OSThread // Sets frame up for immediate drawing void setFrameImmediateDraw(FrameCallback *drawFrames); - /// Called when debug screen is to be drawn, calls through to debugInfo.drawFrame. - static void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - -#if defined(DISPLAY_CLOCK_FRAME) - static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); - - static void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); - - static void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); - - static void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); - - // Whether we are showing the digital watch face or the analog one - bool digitalWatchFace = true; -#endif - /// callback for current alert frame FrameCallback alertFrame; @@ -691,4 +676,8 @@ class Screen : public concurrency::OSThread } // namespace graphics +// Extern declarations for function symbols used in UIRenderer +extern std::vector functionSymbol; +extern std::string functionSymbolString; + #endif \ No newline at end of file diff --git a/src/graphics/ScreenGlobals.cpp b/src/graphics/ScreenGlobals.cpp new file mode 100644 index 000000000..bc139faaf --- /dev/null +++ b/src/graphics/ScreenGlobals.cpp @@ -0,0 +1,6 @@ +#include +#include + +// Global variables for screen function overlay +std::vector functionSymbol; +std::string functionSymbolString; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp new file mode 100644 index 000000000..af427cae4 --- /dev/null +++ b/src/graphics/SharedUIDisplay.cpp @@ -0,0 +1,323 @@ +#include "graphics/SharedUIDisplay.h" +#include "RTC.h" +#include "graphics/ScreenFonts.h" +#include "main.h" +#include "meshtastic/config.pb.h" +#include "power.h" +#include +#include + +namespace graphics +{ + +// === Shared External State === +bool hasUnreadMessage = false; +bool isMuted = false; + +// === Internal State === +bool isBoltVisibleShared = true; +uint32_t lastBlinkShared = 0; +bool isMailIconVisible = true; +uint32_t lastMailBlink = 0; + +// ********************************* +// * Rounded Header when inverted * +// ********************************* +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) +{ + // Draw the center and side rectangles + display->fillRect(x + r, y, w - 2 * r, h); // center bar + display->fillRect(x, y + r, r, h - 2 * r); // left edge + display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge + + // Draw the rounded corners using filled circles + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right +} + +// ************************* +// * Common Header Drawing * +// ************************* +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr) +{ + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; + + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + const bool useBigIcons = (screenW > 128); + + // === Inverted Header Background === + if (isInverted) { + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 3); + display->setColor(WHITE); + if (screenW > 128) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } + } + + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Battery State === + int chargePercent = powerStatus->getBatteryChargePercent(); + bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; + uint32_t now = millis(); + +#ifndef USE_EINK + if (isCharging && now - lastBlinkShared > 500) { + isBoltVisibleShared = !isBoltVisibleShared; + lastBlinkShared = now; + } +#endif + + bool useHorizontalBattery = (screenW > 128 && screenW >= screenH); + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + // === Battery Icons === + if (useHorizontalBattery) { + int batteryX = 2; + int batteryY = HEADER_OFFSET_Y + 2; + display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); + else { + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); + int fillWidth = 24 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); + } + } else { + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; +#ifdef USE_EINK + batteryY += 2; +#endif + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + } + + // === Battery % Display === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + const int batteryOffset = useHorizontalBattery ? 28 : 6; +#ifdef USE_EINK + const int percentX = x + xOffset + batteryOffset - 2; +#else + const int percentX = x + xOffset + batteryOffset; +#endif + display->drawString(percentX, textY, chargeStr); + display->drawString(percentX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(percentX + 1, textY, chargeStr); + display->drawString(percentX + chargeNumWidth, textY, "%"); + } + + // === Time and Right-aligned Icons === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char timeStr[10] = "--:--"; // Fallback display + int timeStrWidth = display->getStringWidth("12:34"); // Default alignment + int timeX = screenW - xOffset - timeStrWidth + 4; + + if (rtc_sec > 0) { + // === Build Time String === + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour %= 12; + if (hour == 0) + hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } + + timeStrWidth = display->getStringWidth(timeStr); + timeX = screenW - xOffset - timeStrWidth + 4; + + // === Show Mail or Mute Icon to the Left of Time === + int iconRightEdge = timeX - 1; + + bool showMail = false; + +#ifndef USE_EINK + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); + } + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - (mail_width - 2); + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + if (useBigIcons) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + + } else { + // === No Time Available: Mail/Mute Icon Moves to Far Right === + int iconRightEdge = screenW - xOffset; + + bool showMail = false; + + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + if (useBigIcons) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } + } + } + + display->setColor(WHITE); // Reset for other UI +} + +const int *getTextPositions(OLEDDisplay *display) +{ + static int textPositions[7]; // Static array that persists beyond function scope + + if (display->getHeight() > 64) { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine_medium; + textPositions[2] = textSecondLine_medium; + textPositions[3] = textThirdLine_medium; + textPositions[4] = textFourthLine_medium; + textPositions[5] = textFifthLine_medium; + textPositions[6] = textSixthLine_medium; + } else { + textPositions[0] = textZeroLine; + textPositions[1] = textFirstLine; + textPositions[2] = textSecondLine; + textPositions[3] = textThirdLine; + textPositions[4] = textFourthLine; + textPositions[5] = textFifthLine; + textPositions[6] = textSixthLine; + } + return textPositions; +} + +} // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h new file mode 100644 index 000000000..41411ba7f --- /dev/null +++ b/src/graphics/SharedUIDisplay.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +namespace graphics +{ + +// ======================= +// Shared UI Helpers +// ======================= + +#define textZeroLine 0 +// Consistent Line Spacing - this is standard for all display and the fall-back spacing +#define textFirstLine (FONT_HEIGHT_SMALL - 1) +#define textSecondLine (textFirstLine + (FONT_HEIGHT_SMALL - 5)) +#define textThirdLine (textSecondLine + (FONT_HEIGHT_SMALL - 5)) +#define textFourthLine (textThirdLine + (FONT_HEIGHT_SMALL - 5)) +#define textFifthLine (textFourthLine + (FONT_HEIGHT_SMALL - 5)) +#define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5)) + +// Consistent Line Spacing for devices like T114 and TEcho/ThinkNode M1 of devices +#define textFirstLine_medium (FONT_HEIGHT_SMALL + 1) +#define textSecondLine_medium (textFirstLine_medium + FONT_HEIGHT_SMALL) +#define textThirdLine_medium (textSecondLine_medium + FONT_HEIGHT_SMALL) +#define textFourthLine_medium (textThirdLine_medium + FONT_HEIGHT_SMALL) +#define textFifthLine_medium (textFourthLine_medium + FONT_HEIGHT_SMALL) +#define textSixthLine_medium (textFifthLine_medium + FONT_HEIGHT_SMALL) + +// Consistent Line Spacing for devices like VisionMaster T190 +#define textFirstLine_large (FONT_HEIGHT_SMALL + 1) +#define textSecondLine_large (textFirstLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textThirdLine_large (textSecondLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textFourthLine_large (textThirdLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5)) +#define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5)) + +// Quick screen access +#define SCREEN_WIDTH display->getWidth() +#define SCREEN_HEIGHT display->getHeight() + +// Shared state (declare inside namespace) +extern bool hasUnreadMessage; +extern bool isMuted; + +// Rounded highlight (used for inverted headers) +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); + +// Shared battery/time/mail header +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = ""); + +const int *getTextPositions(OLEDDisplay *display); + +} // namespace graphics diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 14787baff..92b2c3d02 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -467,18 +467,27 @@ class LGFX : public lgfx::LGFX_Device // The following setting values ​​are general initial values ​​for each panel, so please comment out any // unknown items and try them. - - 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) +#if defined(T_WATCH_S3) + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned + cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout +#else + 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) +#endif #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 = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_pixel = 9; // 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 @@ -653,7 +662,7 @@ static LGFX *tft = nullptr; #include // Graphics and font library for ILI9342 driver chip static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h -#elif ARCH_PORTDUINO && HAS_SCREEN != 0 && !HAS_TFT +#elif ARCH_PORTDUINO #include // Graphics and font library for ST7735 driver chip class LGFX : public lgfx::LGFX_Device @@ -697,11 +706,16 @@ class LGFX : public lgfx::LGFX_Device _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); + LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]); cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = settingsMap[displayReset]; - cfg.panel_width = settingsMap[displayWidth]; // actual displayable width - cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + if (settingsMap[displayRotate]) { + cfg.panel_width = settingsMap[displayHeight]; // actual displayable width + cfg.panel_height = settingsMap[displayWidth]; // actual displayable height + } else { + cfg.panel_width = settingsMap[displayWidth]; // actual displayable width + cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + } cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) @@ -978,9 +992,9 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g #if ARCH_PORTDUINO if (settingsMap[displayRotate]) { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); - } else { setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + } else { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); } #elif defined(SCREEN_ROTATE) @@ -1169,6 +1183,8 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) tft->setRotation(2); // T-Watch S3 left-handed orientation +#elif ARCH_PORTDUINO + tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif diff --git a/src/graphics/TimeFormatters.cpp b/src/graphics/TimeFormatters.cpp new file mode 100644 index 000000000..47036078b --- /dev/null +++ b/src/graphics/TimeFormatters.cpp @@ -0,0 +1,103 @@ +#include "TimeFormatters.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "mesh/NodeDB.h" +#include + +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) +{ + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; + + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; + return validCached; + } + + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } + + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } + + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; + } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; + return validCached; +} + +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) +{ + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + + if (agoSecs < 120) // last 2 mins? + snprintf(timeStr, maxLength, "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(timeStr, maxLength, "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo); + // -- if using time delta instead -- + else if (agoSecs < 120 * 60) // last 2 hrs + snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (730 * 6)) + snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); + else + snprintf(timeStr, maxLength, "unknown age"); +} diff --git a/src/graphics/TimeFormatters.h b/src/graphics/TimeFormatters.h new file mode 100644 index 000000000..b3d8413a2 --- /dev/null +++ b/src/graphics/TimeFormatters.h @@ -0,0 +1,26 @@ +#pragma once + +#include "configuration.h" +#include "gps/RTC.h" +#include +#include + +/** + * Convert a delta in seconds ago to timestamp information (hours, minutes, days ago). + * + * @param secondsAgo Number of seconds ago to convert + * @param hours Pointer to store the hours (0-23) + * @param minutes Pointer to store the minutes (0-59) + * @param daysAgo Pointer to store the number of days ago + * @return true if conversion was successful, false if invalid input or time not available + */ +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo); + +/** + * Get a human-readable string representing the time ago in a format like "2 days, 3 hours, 15 minutes". + * + * @param agoSecs Number of seconds ago to convert + * @param timeStr Pointer to store the resulting string + * @param maxLength Maximum length of the resulting string buffer + */ +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp new file mode 100644 index 000000000..2e301b4e1 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.cpp @@ -0,0 +1,473 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "ClockRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "graphics/images.h" +#include "main.h" + +#if !MESHTASTIC_EXCLUDE_BLUETOOTH +#include "nimble/NimbleBluetooth.h" +#endif + +namespace graphics +{ + +namespace ClockRenderer +{ + +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; + + uint16_t topAndBottomX = x + (4 * scale); + + uint16_t quarterCellHeight = cellHeight / 4; + + uint16_t topY = y + quarterCellHeight; + uint16_t bottomY = y + (quarterCellHeight * 3); + + display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); + display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); +} + +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) +{ + // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of + // segment {innerIndex + 1} + // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. + uint8_t numbers[10][7] = { + {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key + {0, 1, 1, 0, 0, 0, 0}, // 1 1 + {1, 1, 0, 1, 1, 0, 1}, // 2 ___ + {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 + {0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_| + {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 + {1, 0, 1, 1, 1, 1, 1}, // 6 |___| + {1, 1, 1, 0, 0, 1, 0}, // 7 + {1, 1, 1, 1, 1, 1, 1}, // 8 4 + {1, 1, 1, 1, 0, 1, 1}, // 9 + }; + + // the width and height of each segment's central rectangle: + // _____________________ + // ⋰| (only this part, |⋱ + // ⋰ | not including | ⋱ + // ⋱ | the triangles | ⋰ + // ⋱| on the ends) |⋰ + // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // segment x and y coordinates + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentTwoX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; + + uint16_t segmentFourX = segmentOneX; + uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; + + uint16_t segmentFiveX = x; + uint16_t segmentFiveY = segmentThreeY; + + uint16_t segmentSixX = x; + uint16_t segmentSixY = segmentTwoY; + + uint16_t segmentSevenX = segmentOneX; + uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; + + if (numbers[number][0]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + } + + if (numbers[number][1]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + } + + if (numbers[number][2]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + } + + if (numbers[number][3]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } + + if (numbers[number][4]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); + } + + if (numbers[number][5]) { + graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); + } + + if (numbers[number][6]) { + graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); + } +} + +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, width, height); + + // draw end triangles + display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); + + display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); +} + +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) +{ + int halfHeight = height / 2; + + // draw central rectangle + display->fillRect(x, y, height, width); + + // draw end triangles + display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); + + display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); +} + +void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) +{ + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + if (digitalMode) { + uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; + uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); + uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); + + display->drawCircle(centerX, centerY, radius); + display->drawCircle(centerX, centerY, radius + 1); + display->drawLine(centerX, centerY, centerX, centerY - radius + 3); + display->drawLine(centerX, centerY, centerX + radius - 3, centerY); + } else { + uint16_t segmentOneX = x + segmentHeight + 2; + uint16_t segmentOneY = y; + + uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; + uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; + + uint16_t segmentThreeX = segmentOneX; + uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; + + uint16_t segmentFourX = x; + uint16_t segmentFourY = y + segmentHeight + 2; + + drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); + drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); + drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); + } +} + +// Draw a digital clock +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; + +#ifdef T_WATCH_S3 + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + } + + drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, + graphics::ClockRenderer::digitalWatchFace, 1); +#endif + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + char timeString[16]; + int hour = 0; + int minute = 0; + int second = 0; + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + hour = hms / SEC_PER_HOUR; + minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + } + + bool isPM = hour >= 12; + // hour = hour > 12 ? hour - 12 : hour; + if (config.display.use_12h_clock) { + hour %= 12; + if (hour == 0) + hour = 12; + bool isPM = hour >= 12; + snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); + } else { + snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute); + } + + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); + +#ifdef T_WATCH_S3 + float scale = 1.5; +#else + float scale = 0.75; + if (SCREEN_WIDTH > 128) { + scale = 1.5; + } +#endif + + uint16_t segmentWidth = SEGMENT_WIDTH * scale; + uint16_t segmentHeight = SEGMENT_HEIGHT * scale; + + // calculate hours:minutes string width + uint16_t timeStringWidth = strlen(timeString) * 5; + + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; + + if (character == ':') { + timeStringWidth += segmentHeight; + } else { + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2); + + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); + + // iterate over characters in hours:minutes string and draw segmented characters + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; + + if (character == ':') { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + } + + hourMinuteTextX += 5; + } + + // draw seconds string + display->setFont(FONT_SMALL); + int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1; + if (hour >= 10) { + xOffset += (SCREEN_WIDTH > 128) ? 32 : 18; + } + int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + if (config.display.use_12h_clock) { + display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, + isPM ? "pm" : "am"); + } +#ifndef USE_EINK + xOffset = (SCREEN_WIDTH > 128) ? 18 : 10; + display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, + secondString); +#endif +} + +void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) +{ + display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); +} + +// Draw an analog clock +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + + graphics::UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); + + if (powerStatus->getHasBattery()) { + char batteryPercent[8]; + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); + + display->setFont(FONT_SMALL); + + display->drawString(x + 20, y + 2, batteryPercent); + } +#ifdef T_WATCH_S3 + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + } +#endif + drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, + graphics::ClockRenderer::digitalWatchFace, 1); + + // clock face center coordinates + int16_t centerX = display->getWidth() / 2; + int16_t centerY = display->getHeight() / 2; + + // clock face radius + int16_t radius = (display->getWidth() / 2) * 0.8; + + // noon (0 deg) coordinates (outermost circle) + int16_t noonX = centerX; + int16_t noonY = centerY - radius; + + // second hand radius and y coordinate (outermost circle) + int16_t secondHandNoonY = noonY + 1; + + // tick mark outer y coordinate; (first nested circle) + int16_t tickMarkOuterNoonY = secondHandNoonY; + + // seconds tick mark inner y coordinate; (second nested circle) + double secondsTickMarkInnerNoonY = (double)noonY + 8; + + // hours tick mark inner y coordinate; (third nested circle) + double hoursTickMarkInnerNoonY = (double)noonY + 16; + + // minute hand y coordinate + int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; + + // hour string y coordinate + int16_t hourStringNoonY = minuteHandNoonY + 18; + + // hour hand radius and y coordinate + int16_t hourHandRadius = radius * 0.55; + int16_t hourHandNoonY = centerY - hourHandRadius; + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + display->drawCircle(centerX, centerY, radius); + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + hour = hour > 12 ? hour - 12 : hour; + + int16_t degreesPerHour = 30; + int16_t degreesPerMinuteOrSecond = 6; + + double hourBaseAngle = hour * degreesPerHour; + double hourAngleOffset = ((double)minute / 60) * degreesPerHour; + double hourAngle = radians(hourBaseAngle + hourAngleOffset); + + double minuteBaseAngle = minute * degreesPerMinuteOrSecond; + double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; + double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); + + double secondAngle = radians(second * degreesPerMinuteOrSecond); + + double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; + double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; + + double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; + double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; + + double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; + double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; + + display->setFont(FONT_MEDIUM); + + // draw minute and hour tick marks and hour numbers + for (uint16_t angle = 0; angle < 360; angle += 6) { + double angleInRadians = radians(angle); + + double sineAngleInRadians = sin(-angleInRadians); + double cosineAngleInRadians = cos(-angleInRadians); + + double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; + double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; + + if (angle % degreesPerHour == 0) { + double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; + + // draw hour tick mark + display->drawLine(startX, startY, endX, endY); + + static char buffer[2]; + + uint8_t hourInt = (angle / 30); + + if (hourInt == 0) { + hourInt = 12; + } + + // hour number x offset needs to be adjusted for some cases + int8_t hourStringXOffset; + int8_t hourStringYOffset = 13; + + switch (hourInt) { + case 3: + hourStringXOffset = 5; + break; + case 9: + hourStringXOffset = 7; + break; + case 10: + case 11: + hourStringXOffset = 8; + break; + case 12: + hourStringXOffset = 13; + break; + default: + hourStringXOffset = 6; + break; + } + + double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; + double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; + + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } + + if (angle % degreesPerMinuteOrSecond == 0) { + double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; + double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; + + // draw minute tick mark + display->drawLine(startX, startY, endX, endY); + } + } + + // draw hour hand + display->drawLine(centerX, centerY, hourX, hourY); + + // draw minute hand + display->drawLine(centerX, centerY, minuteX, minuteY); + + // draw second hand + display->drawLine(centerX, centerY, secondX, secondY); + } +} + +} // namespace ClockRenderer + +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h new file mode 100644 index 000000000..4660dcc35 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +namespace ClockRenderer +{ +// Whether we are showing the digital watch face or the analog one +static bool digitalWatchFace = true; + +// Clock frame functions +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Segmented display functions +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); + +// UI elements for clock displays +void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); +void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); + +} // namespace ClockRenderer + +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp new file mode 100644 index 000000000..fef993e2d --- /dev/null +++ b/src/graphics/draw/CompassRenderer.cpp @@ -0,0 +1,140 @@ +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/ScreenFonts.h" +#include + +namespace graphics +{ +namespace CompassRenderer +{ + +// Point helper class for compass calculations +struct Point { + float x, y; + Point(float x, float y) : x(x), y(y) {} + + void rotate(float angle) + { + float cos_a = cos(angle); + float sin_a = sin(angle); + float new_x = x * cos_a - y * sin_a; + float new_y = x * sin_a + y * cos_a; + x = new_x; + y = new_y; + } + + void scale(float factor) + { + x *= factor; + y *= factor; + } + + void translate(float dx, float dy) + { + x += dx; + y += dy; + } +}; + +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) +{ + // Show the compass heading (not implemented in original) + // This could draw a "N" indicator or north arrow + // For now, we'll draw a simple north indicator + // const float radius = 17.0f; + if (display->width() > 128) { + radius += 4; + } + Point north(0, -radius); + north.rotate(-myHeading); + north.translate(compassX, compassY); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setColor(BLACK); + if (display->width() > 128) { + display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + } else { + display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + } + display->setColor(WHITE); + display->drawString(north.x, north.y - 3, "N"); +} + +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) +{ + Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; + Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); + + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; + + for (int i = 0; i < 4; i++) { + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(compassDiam * 0.6); + arrowPoints[i]->translate(compassX, compassY); + } + +#ifdef USE_EINK + display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#else + display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#endif + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); +} + +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) +{ + float radians = bearing * DEG_TO_RAD; + + Point tip(0, -size / 2); + Point left(-size / 4, size / 4); + Point right(size / 4, size / 4); + + tip.rotate(radians); + left.rotate(radians); + right.rotate(radians); + + tip.translate(x, y); + left.translate(x, y); + right.translate(x, y); + + display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y); +} + +float estimatedHeading(double lat, double lon) +{ + // Simple magnetic declination estimation + // This is a very basic implementation - the original might be more sophisticated + return 0.0f; // Return 0 for now, indicating no heading available +} + +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + // Calculate appropriate compass diameter based on display size + uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; + uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension + + // Ensure minimum and maximum bounds + if (maxDiam < 16) + maxDiam = 16; + if (maxDiam > 64) + maxDiam = 64; + + return maxDiam; +} + +float calculateBearing(double lat1, double lon1, double lat2, double lon2) +{ + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); + double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); + double bearing = atan2(y, x) * RAD_TO_DEG; + return fmod(bearing + 360.0, 360.0); +} + +} // namespace CompassRenderer +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h new file mode 100644 index 000000000..4b26e6463 --- /dev/null +++ b/src/graphics/draw/CompassRenderer.h @@ -0,0 +1,36 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Compass and navigation drawing functions + * + * Contains all functions related to drawing compass elements, headings, + * navigation arrows, and location-based UI components. + */ +namespace CompassRenderer +{ +// Compass drawing functions +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); + +// Navigation and location functions +float estimatedHeading(double lat, double lon); +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); + +// Utility functions for bearing calculations +float calculateBearing(double lat1, double lon1, double lat2, double lon2); + +} // namespace CompassRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp new file mode 100644 index 000000000..2c3a3a3a8 --- /dev/null +++ b/src/graphics/draw/DebugRenderer.cpp @@ -0,0 +1,634 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "../Screen.h" +#include "DebugRenderer.h" +#include "FSCommon.h" +#include "NodeDB.h" +#include "Throttle.h" +#include "UIRenderer.h" +#include "airtime.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "mesh/Channels.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "sleep.h" + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#include "mesh/wifi/WiFiAPClient.h" +#include +#ifdef ARCH_ESP32 +#include "mesh/wifi/WiFiAPClient.h" +#endif +#endif + +#ifdef ARCH_ESP32 +#include "modules/StoreForwardModule.h" +#endif +#include +#include +#include + +using namespace meshtastic; + +// External variables +extern graphics::Screen *screen; +extern PowerStatus *powerStatus; +extern NodeStatus *nodeStatus; +extern GPSStatus *gpsStatus; +extern Channels channels; +extern AirTime *airTime; + +// External functions from Screen.cpp +extern bool heartbeat; + +#ifdef ARCH_ESP32 +extern StoreForwardModule *storeForwardModule; +#endif + +namespace graphics +{ +namespace DebugRenderer +{ + +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char channelStr[20]; + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + + // Display power status + if (powerStatus->getHasBattery()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus); + } else { + UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); + } + } else if (powerStatus->knowsUSB()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } else { + display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } + } + // Display nodes status + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + } else { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); + } +#if HAS_GPS + // Display GPS status + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + } else { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); + } + } +#endif + display->setColor(WHITE); + // Draw the channel name + display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); + // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo + if (moduleConfig.store_forward.enabled) { +#ifdef ARCH_ESP32 + if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, + (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit +#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) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, + 8, imgQuestion); +#endif + } else { +#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(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, + 8, imgSF); +#endif + } +#endif + } else { + // 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) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, + imgInfo); +#endif + } + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); + + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// **************************** +// * WiFi Screen * +// **************************** +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "WiFi"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + const char *wifiName = config.network.wifi_ssid; + + if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected"); + } else { + display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected"); + + char rssiStr[32]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); + display->drawString(x, getTextPositions(display)[line++], rssiStr); + } + + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; + + */ + if (WiFi.status() == WL_CONNECTED) { + char ipStr[64]; + snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); + display->drawString(x, getTextPositions(display)[line++], ipStr); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, getTextPositions(display)[line++], "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, getTextPositions(display)[line++], "Connection Lost"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, getTextPositions(display)[line++], "Connection Failed"); + } +#ifdef ARCH_ESP32 + else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + display->drawString(x, getTextPositions(display)[line++], + WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } +#else + else { + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); + display->drawString(x, getTextPositions(display)[line++], statusStr); + } +#endif + + char ssidStr[64]; + snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); + display->drawString(x, getTextPositions(display)[line++], ssidStr); + + display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local"); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +#endif +} + +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); + + // Line 1 + display->drawString(x, y, batStr); + if (config.display.heading_bold) + display->drawString(x + 1, y, batStr); + } else { + // Line 1 + display->drawString(x, y, "USB"); + if (config.display.heading_bold) + 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 seconds = currentMillis / 1000; + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + // currentMillis %= 1000; + // seconds %= 60; + // minutes %= 60; + // hours %= 24; + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + display->setColor(WHITE); + + // Setup string to assemble analogClock string + std::string analogClock = ""; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + char timebuf[12]; + + if (config.display.use_12h_clock) { + std::string meridiem = "am"; + if (hour >= 12) { + if (hour > 12) + hour -= 12; + meridiem = "pm"; + } + if (hour == 00) { + hour = 12; + } + snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); + } else { + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); + } + analogClock += timebuf; + } + + // Line 2 + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); + +#if HAS_GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // Line 3 + if (config.display.gps_format != + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + + // Line 4 + UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + } +#endif +/* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// Trampoline functions for DebugInfo class access +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrame(display, state, x, y); +} + +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameSettings(display, state, x, y); +} + +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameWiFi(display, state, x, y); +} + +// **************************** +// * LoRa Focused Screen * +// **************************** +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === First Row: Region / BLE Name === + graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, ""); + + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + + // === Second Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + if (region != nullptr) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); + + // === Third Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if (config.lora.channel_num == 0) { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); + } + size_t len = strlen(frequencyslot); + if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); + + // === Fourth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; + int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; + int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + + display->drawString(starting_position, getTextPositions(display)[line++], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], + chUtilPercentage); +} + +// **************************** +// * Memory Screen * +// **************************** +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Set Title + const char *titleStr = "System"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Layout === + int line = 1; + const int barHeight = 6; + const int labelX = x; + const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; + const int barX = x + 40 + barsOffset; + + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) + return; + + int percent = (used * 100) / total; + + char combinedStr[24]; + if (SCREEN_WIDTH > 128) { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, + total / 1024); + } else { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); + } + + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(labelX, getTextPositions(display)[line], label); + + // Bar + int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); + + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); + + // Value string + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr); + }; + + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); + + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); + + uint32_t flashUsed = 0, flashTotal = 0; +#ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); +#endif + + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = false; + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal, true); +#ifdef ESP32 + if (psramUsed > 0) { + line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + } + if (flashTotal > 0) { + line += 1; + drawUsageRow("Flash:", flashUsed, flashTotal); + } +#endif + if (hasSD && sdTotal > 0) { + line += 1; + drawUsageRow("SD:", sdUsed, sdTotal); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + // System Uptime + if (line < 2) { + line += 1; + } + line += 1; + char appversionstr[35]; + snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION)); + int textWidth = display->getStringWidth(appversionstr); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line], appversionstr); + + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it + line += 1; + char uptimeStr[32] = ""; + uint32_t uptime = millis() / 1000; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + textWidth = display->getStringWidth(uptimeStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line], uptimeStr); + } +} +} // namespace DebugRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h new file mode 100644 index 000000000..f4d484f58 --- /dev/null +++ b/src/graphics/draw/DebugRenderer.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; +class DebugInfo; + +/** + * @brief Debug and diagnostic drawing functions + * + * Contains all functions related to drawing debug information, + * WiFi status, settings screens, and diagnostic data. + */ +namespace DebugRenderer +{ +// Debug frame functions +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Trampoline functions for framework callback compatibility +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// LoRa information display +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Memory screen display +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +} // namespace DebugRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h new file mode 100644 index 000000000..6f1929ebd --- /dev/null +++ b/src/graphics/draw/DrawRenderers.h @@ -0,0 +1,38 @@ +#pragma once + +/** + * @brief Master include file for all Screen draw renderers + * + * This file includes all the individual renderer headers to provide + * a convenient single include for accessing all draw functions. + */ + +#include "graphics/draw/ClockRenderer.h" +#include "graphics/draw/CompassRenderer.h" +#include "graphics/draw/DebugRenderer.h" +#include "graphics/draw/NodeListRenderer.h" +#include "graphics/draw/ScreenRenderer.h" +#include "graphics/draw/UIRenderer.h" + +namespace graphics +{ + +/** + * @brief Collection of all draw renderers + * + * This namespace provides access to all the specialized rendering + * functions organized by category. + */ +namespace DrawRenderers +{ +// Re-export all renderer namespaces for convenience +using namespace ClockRenderer; +using namespace CompassRenderer; +using namespace DebugRenderer; +using namespace NodeListRenderer; +using namespace ScreenRenderer; +using namespace UIRenderer; + +} // namespace DrawRenderers + +} // namespace graphics diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp new file mode 100644 index 000000000..707517d82 --- /dev/null +++ b/src/graphics/draw/MessageRenderer.cpp @@ -0,0 +1,392 @@ +/* +BaseUI + +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) – Lead development and implementation. +- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) – Project management, structural planning, and testing + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "configuration.h" +#if HAS_SCREEN +#include "MessageRenderer.h" + +// Core includes +#include "NodeDB.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "main.h" +#include "meshUtils.h" + +// Additional includes for UI rendering +#include "UIRenderer.h" +#include "graphics/TimeFormatters.h" + +// Additional includes for dependencies +#include +#include + +// External declarations +extern bool hasUnreadMessage; +extern meshtastic_DeviceState devicestate; + +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + +namespace graphics +{ +namespace MessageRenderer +{ + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) +{ + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; + + // === Step 1: Find tallest emote in the line === + int maxIconHeight = fontHeight; + for (size_t i = 0; i < line.length();) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + uint8_t c = static_cast(line[i]); + if ((c & 0xE0) == 0xC0) + i += 2; + else if ((c & 0xF0) == 0xE0) + i += 3; + else if ((c & 0xF8) == 0xF0) + i += 4; + else + i += 1; + } + } + + // === Step 2: Baseline alignment === + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; + int fontMidline = fontY + fontHeight / 2; + + // === Step 3: Render line in segments === + size_t i = 0; + bool inBold = false; + + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; + } + + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote *matchedEmote = nullptr; + size_t emojiLen = 0; + + for (int e = 0; e < emoteCount; ++e) { + size_t pos = line.find(emotes[e].label, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].label); + } + } + + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) + nextControl = line.length(); + + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } + display->drawString(cursorX, fontY, textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); + i = nextControl; + continue; + } + + // Render the emote (if found) + if (matchedEmote && i == nextEmotePos) { + int iconY = fontMidline - matchedEmote->height / 2 - 1; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + } else { + // No more emotes — render the rest of the line + std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } + display->drawString(cursorX, fontY, remaining.c_str()); + cursorX += display->getStringWidth(remaining.c_str()); + break; + } + } +} + +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; + + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + const int navHeight = FONT_HEIGHT_SMALL; + const int scrollBottom = SCREEN_HEIGHT - navHeight; + const int usableHeight = scrollBottom; + const int textWidth = SCREEN_WIDTH; + + bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + // === Set Title + const char *titleStr = "Messages"; + + // Check if we have more than an empty message to show + char messageBuf[237]; + snprintf(messageBuf, sizeof(messageBuf), "%s", msg); + if (strlen(messageBuf) == 0) { + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + const char *messageString = "No messages"; + int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2); + display->drawString(center_text, getTextPositions(display)[2], messageString); + return; + } + + // === Header Construction === + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + char headerStr[80]; + const char *sender = "???"; + if (node && node->has_user) { + if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { + sender = node->user.long_name; + } else { + sender = node->user.short_name; + } + } + uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; + if (config.display.use_12h_clock) { + bool isPM = timestampHours >= 12; + timestampHours = timestampHours % 12; + if (timestampHours == 0) + timestampHours = 12; + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, + isPM ? "p" : "a", sender); + } else { + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, + sender); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); + } + +#ifndef EXCLUDE_EMOJI + // === Bounce animation setup === + static uint32_t lastBounceTime = 0; + static int bounceY = 0; + const int bounceRange = 2; // Max pixels to bounce up/down + const int bounceInterval = 10; // How quickly to change bounce direction (ms) + + uint32_t now = millis(); + if (now - lastBounceTime >= bounceInterval) { + lastBounceTime = now; + bounceY = (bounceY + 1) % (bounceRange * 2); + } + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (strcmp(msg, e.label) == 0) { + int headerY = getTextPositions(display)[1]; // same as scrolling header line + display->drawString(x + 3, headerY, headerStr); + if (isInverted && isBold) + display->drawString(x + 4, headerY, headerStr); + + // Draw separator (same as scroll version) + for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { + display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13)); + } + + // Center the emote below the header line + separator + nav + int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; + int emoteY = headerY + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; + display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + return; + } + } +#endif + + // === Word-wrap and build line list === + std::vector lines; + lines.push_back(std::string(headerStr)); // Header line is always first + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if (ch == '\n') { + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); + } else { + word += ch; + std::string test = line + word; + if (display->getStringWidth(test.c_str()) > textWidth) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } + } + } + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + + // === Scrolling logic === + std::vector rowHeights; + + for (const auto &_line : lines) { + int lineHeight = FONT_HEIGHT_SMALL; + bool hasEmote = false; + + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (_line.find(e.label) != std::string::npos) { + lineHeight = std::max(lineHeight, e.height); + hasEmote = true; + } + } + + // Apply tighter spacing if no emotes on this line + if (!hasEmote) { + lineHeight -= 2; // reduce by 2px for tighter spacing + if (lineHeight < 8) + lineHeight = 8; // minimum safety + } + + rowHeights.push_back(lineHeight); + } + int totalHeight = 0; + for (size_t i = 1; i < rowHeights.size(); ++i) { + totalHeight += rowHeights[i]; + } + int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height + int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back()); + + static float scrollY = 0.0f; + static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; + static bool waitingToReset = false, scrollStarted = false; + + // === Smooth scrolling adjustment === + // You can tweak this divisor to change how smooth it scrolls. + // Lower = smoother, but can feel slow. + float delta = (now - lastTime) / 400.0f; + lastTime = now; + + const float scrollSpeed = 2.0f; // pixels per second + + // Delay scrolling start by 2 seconds + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; + + if (totalHeight > usableScrollHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = lastTime; + } + } else if (lastTime - pauseStart > 3000) { + scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = lastTime; + } + } + } else { + scrollY = 0; + } + + int scrollOffset = static_cast(scrollY); + int yOffset = -scrollOffset + getTextPositions(display)[1]; + for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { + display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13)); + } + + // === Render visible lines === + for (size_t i = 0; i < lines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += rowHeights[j]; + if (lineY > -rowHeights[i] && lineY < scrollBottom) { + if (i == 0 && isInverted) { + display->drawString(x + 3, lineY, lines[i].c_str()); + if (isBold) + display->drawString(x + 4, lineY, lines[i].c_str()); + } else { + drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); + } + } + } + + // Draw header at the end to sort out overlapping elements + graphics::drawCommonHeader(display, x, y, titleStr); +} + +} // namespace MessageRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h new file mode 100644 index 000000000..d92b96014 --- /dev/null +++ b/src/graphics/draw/MessageRenderer.h @@ -0,0 +1,18 @@ +#pragma once +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" +#include "graphics/emotes.h" + +namespace graphics +{ +namespace MessageRenderer +{ + +// Text and emote rendering +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); + +/// Draws the text message frame for displaying received messages +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +} // namespace MessageRenderer +} // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp new file mode 100644 index 000000000..13b71546e --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -0,0 +1,595 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "NodeListRenderer.h" +#include "UIRenderer.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" // for getTime() function +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "meshUtils.h" +#include + +// Forward declarations for functions defined in Screen.cpp +namespace graphics +{ +extern bool haveGlyphs(const char *str); +} // namespace graphics + +// Global screen instance +extern graphics::Screen *screen; + +namespace graphics +{ +namespace NodeListRenderer +{ + +// Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) +{ + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } + } + } +} + +// Static variables for dynamic cycling +static NodeListMode currentMode = MODE_LAST_HEARD; +static int scrollIndex = 0; + +// ============================= +// Utility Functions +// ============================= + +const char *getSafeNodeName(meshtastic_NodeInfoLite *node) +{ + static char nodeName[16] = "?"; + if (node->has_user && strlen(node->user.short_name) > 0) { + bool valid = true; + const char *name = node->user.short_name; + for (size_t i = 0; i < strlen(name); i++) { + uint8_t c = (uint8_t)name[i]; + if (c < 32 || c > 126) { + valid = false; + break; + } + } + if (valid) { + strncpy(nodeName, name, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; + } else { + snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF)); + } + } else { + strcpy(nodeName, "?"); + } + return nodeName; +} + +const char *getCurrentModeTitle(int screenWidth) +{ + switch (currentMode) { + case MODE_LAST_HEARD: + return "Last Heard"; + case MODE_HOP_SIGNAL: + return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig"; + case MODE_DISTANCE: + return "Distance"; + default: + return "Nodes"; + } +} + +// Use dynamic timing based on mode +unsigned long getModeCycleIntervalMs() +{ + return 3000; +} + +// Calculate bearing between two lat/lon points +float calculateBearing(double lat1, double lon1, double lat2, double lon2) +{ + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); + double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); + double bearing = atan2(y, x) * RAD_TO_DEG; + return fmod(bearing + 360.0, 360.0); +} + +int calculateMaxScroll(int totalEntries, int visibleRows) +{ + return std::max(0, (totalEntries - 1) / (visibleRows * 2)); +} + +void retrieveAndSortNodes(std::vector &nodeList) +{ + size_t numNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numNodes; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == nodeDB->getNodeNum()) + continue; + + NodeEntry entry; + entry.node = node; + entry.sortValue = sinceLastSeen(node); + + nodeList.push_back(entry); + } + + // Sort nodes: favorites first, then by last heard (most recent first) + std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { + bool aFav = a.node->is_favorite; + bool bFav = b.node->is_favorite; + if (aFav != bFav) + return aFav; + if (a.sortValue == 0 || a.sortValue == UINT32_MAX) + return false; + if (b.sortValue == 0 || b.sortValue == UINT32_MAX) + return true; + return a.sortValue < b.sortValue; + }); +} + +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) +{ + int columnWidth = display->getWidth() / 2; + int separatorX = x + columnWidth - 2; + for (int y = yStart; y <= yEnd; y += 2) { + display->setPixel(separatorX, y); + } +} + +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) +{ + if (totalEntries <= visibleNodeRows * columns) + return; + + int scrollbarX = display->getWidth() - 2; + int scrollbarHeight = display->getHeight() - scrollStartY - 10; + int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries); + int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); + int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } +} + +// ============================= +// Entry Renderers +// ============================= + +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + + const char *nodeName = getSafeNodeName(node); + + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); +} + +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + int nameMaxWidth = columnWidth - 25; + int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + + int barsXOffset = columnWidth - barsOffset; + + const char *nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + // Draw signal strength bars + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + } + } + + // Draw hop count + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + + if (hopStr[0] != '\0') { + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); + } +} + +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + const char *nodeName = getSafeNodeName(node); + char distStr[10] = ""; + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } + } + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + if (strlen(distStr) > 0) { + int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distStr); + display->drawString(rightEdge - textWidth, y, distStr); + } +} + +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + switch (currentMode) { + case MODE_LAST_HEARD: + drawEntryLastHeard(display, node, x, y, columnWidth); + break; + case MODE_HOP_SIGNAL: + drawEntryHopSignal(display, node, x, y, columnWidth); + break; + case MODE_DISTANCE: + drawNodeDistance(display, node, x, y, columnWidth); + break; + default: + break; + } +} + +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + const char *nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } +} + +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; + + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float angle = relativeBearing * DEG_TO_RAD; + + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + float halfSize = size / 2.0; + + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); + + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; + + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); + + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); +} + +// ============================= +// Main Screen Functions +// ============================= + +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) +{ + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + + int columnWidth = display->getWidth() / 2; + + display->clear(); + + // Draw the battery/time header + graphics::drawCommonHeader(display, x, y, title); + + // Space below header + y += COMMON_HEADER_HEIGHT; + + // Fetch and display sorted node list + std::vector nodeList; + retrieveAndSortNodes(nodeList); + + int totalEntries = nodeList.size(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; +#ifdef USE_EINK + totalRowsAvailable -= 1; +#endif + int visibleNodeRows = totalRowsAvailable; + int totalColumns = 2; + + int startIndex = scrollIndex * visibleNodeRows * totalColumns; + int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); + + int yOffset = 0; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + int rowCount = 0; + + for (int i = startIndex; i < endIndex; ++i) { + int xPos = x + (col * columnWidth); + int yPos = y + yOffset; + renderer(display, nodeList[i].node, xPos, yPos, columnWidth); + + if (extras) { + extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); + } + + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; + shownCount++; + rowCount++; + + if (rowCount >= totalRowsAvailable) { + yOffset = 0; + rowCount = 0; + col++; + if (col > (totalColumns - 1)) + break; + } + } + + // Draw column separator + if (shownCount > 0) { + const int firstNodeY = y + 3; + drawColumnSeparator(display, x, firstNodeY, lastNodeY); + } + + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); +} + +// ============================= +// Screen Frame Functions +// ============================= + +#ifndef USE_EINK +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static NodeListMode lastRenderedMode = MODE_COUNT; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT) { + currentMode = MODE_LAST_HEARD; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode = static_cast((currentMode + 1) % MODE_COUNT); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode; +} +#endif + +#ifdef USE_EINK +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Last Heard"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Hops/Signal"; + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); +} + +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Distance"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +} +#endif + +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + float heading = 0; + bool validHeading = false; + double lat = 0; + double lon = 0; + +#if HAS_GPS + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } +#endif + + if (!validHeading) + return; + + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); +} + +/// Draw a series of fields in a column, wrapping to multiple columns if needed +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +{ + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char **f = fields; + int xo = x, yo = y; + while (*f) { + display->drawString(xo, yo, *f); + if ((display->getColor() == BLACK) && config.display.heading_bold) + display->drawString(xo + 1, yo, *f); + + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; + } + f++; + } +} + +} // namespace NodeListRenderer +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h new file mode 100644 index 000000000..63f0d1c69 --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.h @@ -0,0 +1,69 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Node list and entry rendering functions + * + * Contains all functions related to drawing node lists and individual node entries + * including last heard, hop signal, distance, and compass views. + */ +namespace NodeListRenderer +{ +// Entry renderer function types +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); + +// Node entry structure +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t sortValue; +}; + +// Node list mode enumeration +enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; + +// Main node list screen function +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + double lon = 0); + +// Entry renderers +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); + +// Extras renderers +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon); + +// Screen frame functions +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Utility functions +const char *getCurrentModeTitle(int screenWidth); +void retrieveAndSortNodes(std::vector &nodeList); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node); +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); + +// Bitmap drawing function +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); + +} // namespace NodeListRenderer + +} // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp new file mode 100644 index 000000000..ed5257012 --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -0,0 +1,265 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "DisplayFormatters.h" +#include "NodeDB.h" +#include "NotificationRenderer.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include +#include +#include + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#endif + +using namespace meshtastic; + +// External references to global variables from Screen.cpp +extern std::vector functionSymbol; +extern std::string functionSymbolString; +extern bool hasUnreadMessage; + +namespace graphics +{ + +char NotificationRenderer::inEvent = INPUT_BROKER_NONE; +int8_t NotificationRenderer::curSelected = 0; +char NotificationRenderer::alertBannerMessage[256] = {0}; +uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever +uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +std::function NotificationRenderer::alertBannerCallback = NULL; +bool NotificationRenderer::pauseBanner = false; + +// Used on boot when a certificate is being created +void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(64 + x, y, "Creating SSL certificate"); + +#ifdef ARCH_ESP32 + yield(); + esp_task_wdt_reset(); +#endif + + display->setFont(FONT_SMALL); + if ((millis() / 1000) % 2) { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); + } else { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); + } +} + +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // Exit if no message is active or duration has passed + if (!isOverlayBannerShowing()) + return; + + if (pauseBanner) + return; + + // === Layout Configuration === + constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint16_t vPadding = 2; // Padding around text inside the box + constexpr uint8_t lineSpacing = 1; // Extra space between lines + + // Search the message to determine if we need the bell added + bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); + + uint8_t firstOption = 0; + uint8_t firstOptionToShow = 0; + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line + const int MAX_LINES = 24; + + uint16_t maxWidth = 0; + uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); + uint16_t lineWidths[MAX_LINES] = {0}; + uint16_t lineLengths[MAX_LINES] = {0}; + char *lineStarts[MAX_LINES + 1]; + uint16_t lineCount = 0; + char lineBuffer[40] = {0}; + // pointer to the terminating null + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; + + // loop through lines finding \n characters + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); + lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + if (lineStarts[lineCount + 1][0] == '\n') { + lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n + } + lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); + if (lineWidths[lineCount] > maxWidth) { + maxWidth = lineWidths[lineCount]; + } + if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) { + maxWidth = lineWidths[lineCount] + arrowsWidth; + } + lineCount++; + // if we are doing a selection, add extra width for arrows + } + + if (alertBannerOptions > 0) { + // respond to input + if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { + curSelected--; + } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { + curSelected++; + } else if (inEvent == INPUT_BROKER_SELECT) { + alertBannerCallback(curSelected); + alertBannerMessage[0] = '\0'; + } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { + alertBannerMessage[0] = '\0'; + } + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + // compare number of options to number of lines + if (lineCount < alertBannerOptions) + return; + firstOption = lineCount - alertBannerOptions; + if (curSelected > 1 && alertBannerOptions > 3) { + firstOptionToShow = curSelected + firstOption - 1; + // put the selected option in the middle + } else { + firstOptionToShow = firstOption; + } + } else { // not in an alert with a callback + // TODO: check that at least a second has passed since the alert started + if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) { + alertBannerMessage[0] = '\0'; // end the alert early + } + } + inEvent = INPUT_BROKER_NONE; + if (alertBannerMessage[0] == '\0') + return; + + // set width from longest line + uint16_t boxWidth = padding * 2 + maxWidth; + if (needs_bell) { + if (SCREEN_WIDTH > 128 && boxWidth <= 150) { + boxWidth += 26; + } + if (SCREEN_WIDTH <= 128 && boxWidth <= 100) { + boxWidth += 20; + } + } + // calculate max lines on screen? for now it's 4 + // set height from line count + uint16_t boxHeight; + if (lineCount <= 4) { + boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; + } else { + boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing; + } + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + // === Draw background box === + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); // Top Left + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right + display->setColor(WHITE); + + // === Draw each line centered in the box === + int16_t lineY = boxTop + vPadding; + + for (int i = 0; i < lineCount; i++) { + // is this line selected? + // if so, start the buffer with -> and strncpy to the 4th location + if (i < lineCount - alertBannerOptions || alertBannerOptions == 0) { + strncpy(lineBuffer, lineStarts[i], 40); + if (lineLengths[i] > 39) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } else if (i >= firstOptionToShow && i < firstOptionToShow + 3) { + if (i == curSelected + firstOption) { + if (lineLengths[i] > 35) + lineLengths[i] = 35; + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, lineStarts[i], 36); + strncpy(lineBuffer + lineLengths[i] + 2, " <", 3); + lineLengths[i] += 4; + lineWidths[i] += display->getStringWidth("> <", 4, true); + if (lineLengths[i] > 35) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } else { + strncpy(lineBuffer, lineStarts[i], 40); + if (lineLengths[i] > 39) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } + } else { // add break for the additional lines + continue; + } + + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); + } + + display->drawString(textX, lineY, lineBuffer); + lineY += FONT_HEIGHT_SMALL + lineSpacing; + } +} + +/// Draw the last text message we received +void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + + char tempBuf[24]; + snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); + display->drawString(0 + x, 0 + y, tempBuf); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); +} + +void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(64 + x, y, "Updating"); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), + "Please be patient and do not power off."); +} + +bool NotificationRenderer::isOverlayBannerShowing() +{ + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); +} + +} // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h new file mode 100644 index 000000000..3ed931dc6 --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.h @@ -0,0 +1,28 @@ +#pragma once + +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" + +namespace graphics +{ + +class NotificationRenderer +{ + public: + static char inEvent; + static int8_t curSelected; + static char alertBannerMessage[256]; + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static uint8_t alertBannerOptions; // last x lines are seelctable options + static std::function alertBannerCallback; + + static bool pauseBanner; + + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static bool isOverlayBannerShowing(); +}; + +} // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp new file mode 100644 index 000000000..a77d5b44b --- /dev/null +++ b/src/graphics/draw/UIRenderer.cpp @@ -0,0 +1,1240 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "CompassRenderer.h" +#include "GPSStatus.h" +#include "NodeDB.h" +#include "NodeListRenderer.h" +#include "UIRenderer.h" +#include "airtime.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "target_specific.h" +#include +#include +#include + +#if !MESHTASTIC_EXCLUDE_GPS + +// External variables +extern graphics::Screen *screen; + +namespace graphics +{ + +// GeoCoord object for coordinate conversions +extern GeoCoord geoCoord; + +// Threshold values for the GPS lock accuracy bar display +extern uint32_t dopThresholds[5]; + +NodeNum UIRenderer::currentFavoriteNodeNum = 0; + +// Draw GPS status summary +void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + // Draw satellite image + if (SCREEN_WIDTH > 128) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); + } + char textString[10]; + + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + snprintf(textString, sizeof(textString), "Fixed"); + } + if (!gps->getIsConnected()) { + snprintf(textString, sizeof(textString), "No Lock"); + } + if (!gps->getHasLock()) { + // Draw "No sats" to the right of the icon with slightly more gap + snprintf(textString, sizeof(textString), "No Sats"); + } else { + snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); + } + if (SCREEN_WIDTH > 128) { + display->drawString(x + 18, y, textString); + } else { + display->drawString(x + 11, y, textString); + } +} + +// Draw status when GPS is disabled or not present +void UIRenderer::drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + const char *displayLine; + int pos; + if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + pos = display->getWidth() - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" + : "GPS is disabled"; + pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); +} + +void UIRenderer::drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + char displayLine[32]; + if (!gps->getIsConnected() && !config.position.fixed_position) { + // displayLine = "No GPS Module"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + // displayLine = "No GPS Lock"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + else + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } +} + +// Draw GPS status coordinates +void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + auto gpsFormat = config.display.gps_format; + char displayLine[32]; + + if (!gps->getIsConnected() && !config.position.fixed_position) { + strcpy(displayLine, "No GPS present"); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + strcpy(displayLine, "No GPS Lock"); + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + + if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { + char coordinateLine[22]; + if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, + geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), + geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), + geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region + snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); + else + snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + } + + // If fixed position, display text "Fixed GPS" alternating with the coordinates. + if (config.position.fixed_position) { + if ((millis() / 10000) % 2) { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, + coordinateLine); + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + } + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); + } + } else { + char latLine[22]; + char lonLine[22]; + snprintf(latLine, sizeof(latLine), "%2i° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), + geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(lonLine, sizeof(lonLine), "%3i° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), + geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, + latLine); + display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine); + } + } +} + +void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, + const meshtastic::PowerStatus *powerStatus) +{ + static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; + static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; + + // Clear the bar area inside the battery image + for (int i = 1; i < 14; i++) { + imgBuffer[i] = 0x81; + } + + // Fill with lightning or power bars + if (powerStatus->getIsCharging()) { + memcpy(imgBuffer + 3, lightning, 8); + } else { + for (int i = 0; i < 4; i++) { + if (powerStatus->getBatteryChargePercent() >= 25 * i) + memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); + } + } + + // Slightly more conservative scaling based on screen width + int scale = 1; + + if (SCREEN_WIDTH >= 200) + scale = 2; + if (SCREEN_WIDTH >= 300) + scale = 2; // Do NOT go higher than 2 + + // Draw scaled battery image (16 columns × 8 rows) + for (int col = 0; col < 16; col++) { + uint8_t colBits = imgBuffer[col]; + for (int row = 0; row < 8; row++) { + if (colBits & (1 << row)) { + display->fillRect(x + col * scale, y + row * scale, scale, scale); + } + } + } +} + +// Draw nodes status +void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, + bool show_total, String additional_words) +{ + char usersString[20]; + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; + + snprintf(usersString, sizeof(usersString), "%d %s", nodes_online, additional_words.c_str()); + + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d %s", nodes_online, nodes_total, additional_words.c_str()); + } + +#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(DISPLAY_FORCE_SMALL_FONTS) + + if (SCREEN_WIDTH > 128) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 3, 8, 8, imgUser); + } +#else + if (SCREEN_WIDTH > 128) { + NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); + } else { + display->drawFastImage(x, y + 1, 8, 8, imgUser); + } +#endif + int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0; + display->drawString(x + 10 + string_offset, y - 2, usersString); +} + +// ********************** +// * Favorite Node Info * +// ********************** +void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // --- Cache favorite nodes for the current frame only, to save computation --- + static std::vector favoritedNodes; + static int prevFrame = -1; + + // --- Only rebuild favorites list if we're on a new frame --- + if (state->currentFrame != prevFrame) { + prevFrame = state->currentFrame; + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + // Skip nulls and ourself + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); + } + // Keep a stable, consistent display order + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + } + if (favoritedNodes.empty()) + return; + + // --- Only display if index is valid --- + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; + + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + + display->clear(); + currentFavoriteNodeNum = node->num; + // === Create the shortName and title string === + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + char titlestr[32] = {0}; + snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); + + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y, titlestr); + + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. + + // List of available macro Y positions in order, from top to bottom. + int line = 1; // which slot to use next + + // === 1. Long Name (always try to show first) === + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + if (username && line < 5) { + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, getTextPositions(display)[line++], username); + } + + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Sig: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + + // Always use "Sig" for the label + const char *signalLabel = " Sig"; + + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], signalHopsStr); + } + + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], seenStr); + } + + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + uint32_t uptime = node->device_metrics.uptime_seconds; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], uptimeStr); + } + + // === 5. Distance (only if both nodes have GPS position) === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ¼mi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: 1km"); + haveDistance = true; + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, getTextPositions(display)[line++], distStr); + } + + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + + display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2) + : getTextPositions(display)[1]; + const int margin = 4; +// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- +#if defined(USE_EINK) + const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + + const auto &p = node->position; + /* unused + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } +} + +// **************************** +// * Device Focused Screen * +// **************************** +void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Header === + graphics::drawCommonHeader(display, x, y, ""); + + // === Content below header === + + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } + + // === First Row: Region / Channel Utilization and Uptime === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + // Display Region and Channel Utilization + drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online"); + + char uptimeStr[32] = ""; + uint32_t uptime = millis() / 1000; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), getTextPositions(display)[line++], uptimeStr); + + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; + +#if HAS_GPS + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + const char *displayLine; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + if (SCREEN_WIDTH > 128) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); + } else { + UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); + } +#endif + + if (powerStatus->getHasBattery()) { + char batStr[20]; + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), getTextPositions(display)[line++], batStr); + } else { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), getTextPositions(display)[line++], "USB"); + } + + config.display.heading_bold = origBold; + + // === Third Row: Channel Utilization Bluetooth Off (Only If Actually Off) === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = getTextPositions(display)[line] + 3; + + int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; + if (!config.bluetooth.enabled) { + chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40; + } + int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; + int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + if (!config.bluetooth.enabled) { + extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1; + } + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + if (!config.bluetooth.enabled) { + starting_position = 0; + } + + display->drawString(starting_position, getTextPositions(display)[line], chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line], + chUtilPercentage); + + if (!config.bluetooth.enabled) { + display->drawString(SCREEN_WIDTH - display->getStringWidth("BT off"), getTextPositions(display)[line], "BT off"); + } + + line += 1; + + // === Fourth & Fifth Rows: Node Identity === + int textWidth = 0; + int nameX = 0; + int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5; + const char *longName = nullptr; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + longName = ourNode->user.long_name; + } + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + char combinedName[50]; + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); + if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) { + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString( + nameX, ((rows == 4) ? getTextPositions(display)[line++] : getTextPositions(display)[line++]) + yOffset, combinedName); + } else { + // === LongName Centered === + textWidth = display->getStringWidth(longName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], longName); + + // === ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], shortnameble); + } +} + +// Start Functions to write date/time to the screen +// Helper function to check if a year is a leap year +bool isLeapYear(int year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +// Array of days in each month (non-leap year) +const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +// Fills the buffer with a formatted date/time string and returns pixel width +int UIRenderer::formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) +{ + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; + + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= (uint32_t)daysInYear) { + rtc_sec -= daysInYear; + year++; + } else { + break; + } + } + + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) + dim++; + if (rtc_sec >= (uint32_t)dim) { + rtc_sec -= dim; + month++; + } else { + break; + } + } + + int day = rtc_sec + 1; + + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } + + return display->getStringWidth(buf); +} + +// Check if the display can render a string (detect special chars; emoji) +bool UIRenderer::haveGlyphs(const char *str) +{ +#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) + // Don't want to make any assumptions about custom language support + return true; +#endif + + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ¿ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; + } + } + + // LOG_DEBUG("haveGlyphs=%d", have); + return have; +} + +#ifdef USE_EINK +/// Used on eink displays while in deep sleep +void UIRenderer::drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + + // Next frame should use full-refresh, and block while running, else device will sleep before async callback + EINK_ADD_FRAMEFLAG(display, COSMETIC); + EINK_ADD_FRAMEFLAG(display, BLOCKING); + + LOG_DEBUG("Draw deep sleep screen"); + + // Display displayStr on the screen + graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); +} + +/// Used on eink displays when screen updates are paused +void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + LOG_DEBUG("Draw screensaver overlay"); + + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh + + // Config + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *pauseText = "Screen Paused"; + const char *idText = owner.short_name; + const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name + constexpr uint16_t padding = 5; + constexpr uint8_t dividerGap = 1; + constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. + + // Dimensions + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; + + // Position + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); + // const int16_t boxRight = boxLeft + boxWidth - 1; + const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); + const int16_t boxBottom = boxTop + boxHeight - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + 1 + dividerGap; + const int16_t dividerBottom = boxBottom - 1 - dividerGap; + + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box + display->setColor(EINK_BLACK); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + + // Draw: Text + if (useId) + display->drawString(idTextLeft, idTextTop, idText); + display->drawString(pauseTextLeft, pauseTextTop, pauseText); + display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold + + // Draw: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); +} +#endif + +/** + * Draw the icon with extra info printed around the corners + */ +void UIRenderer::drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // draw an xbm image. + // Please note that everything that should be transitioned + // needs to be drawn relative to x and y + + // draw centered icon left to right and centered above the one line of app text + display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, + icon_width, icon_height, icon_bits); + + display->setFont(FONT_MEDIUM); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = "meshtastic.org"; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and short name in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +// **************************** +// * My Position Screen * +// **************************** +void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "Position"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === First Row: My Location === +#if HAS_GPS + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + const char *displayLine = ""; // Initialize to empty string by default + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + if (SCREEN_WIDTH > 128) { + NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, + imgSatellite_height, imgSatellite, display); + } else { + display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, + imgSatellite); + } + int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; + display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); + } else { + UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); + } + + config.display.heading_bold = origBold; + + // === Update GeoCoord === + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); + + // === Determine Compass Heading === + float heading; + bool validHeading = false; + + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } + + // If GPS is off, no need to display these parts + if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { + + // === Second Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); + display->drawString(0, getTextPositions(display)[line++], fullLine); + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++], latStr); + + // === Fourth Row: Longitude === + char lonStr[32]; + snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, getTextPositions(display)[line++], lonStr); + + // === Fifth Row: Altitude === + char DisplayLineTwo[32] = {0}; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + } else { + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); + } + display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); + } + + // === Draw Compass if heading is valid === + if (validHeading) { + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = getTextPositions(display)[1]; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; + + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = +#if defined(USE_EINK) + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink +#else + SCREEN_HEIGHT - yBelowContent - margin; +#endif + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } + } +#endif +} + +#ifdef USERPREFS_OEM_TEXT + +void UIRenderer::drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: + display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); +} + +#endif + +// Function overlay for showing mute/buzzer modifiers etc. +void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // LOG_DEBUG("Draw function overlay"); + if (functionSymbol.begin() != functionSymbol.end()) { + char buf[64]; + display->setFont(FONT_SMALL); + snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); + display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); + } +} + +// Navigation bar overlay implementation +static int8_t lastFrameIndex = -1; +static uint32_t lastFrameChangeTime = 0; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; + +void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; + + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } + + const bool useBigIcons = (SCREEN_WIDTH > 128); + const int iconSize = useBigIcons ? 16 : 8; + const int spacing = useBigIcons ? 8 : 4; + const int bigOffset = useBigIcons ? 1 : 0; + + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; + + const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); + + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + + // Only show bar briefly after switching frames (unless on E-Ink) +#if defined(USE_EINK) + int y = SCREEN_HEIGHT - iconSize - 1; +#else + int y = SCREEN_HEIGHT - iconSize - 1; + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { + y = SCREEN_HEIGHT; + } +#endif + + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; + + // Clear background and draw border + display->setColor(BLACK); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); + display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); + + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + (i - pageStart) * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); + + if (isActive) { + display->setColor(WHITE); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); + display->setColor(BLACK); + } + + if (useBigIcons) { + NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + + if (isActive) { + display->setColor(WHITE); + } + } + + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); +} + +void UIRenderer::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) +{ + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, message); +} + +std::string UIRenderer::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) +{ + std::string uptime; + + if (days > (HOURS_IN_MONTH * 6)) + uptime = "?"; + else if (days >= 2) + uptime = std::to_string(days) + "d"; + else if (hours >= 2) + uptime = std::to_string(hours) + "h"; + else if (minutes >= 1) + uptime = std::to_string(minutes) + "m"; + else + uptime = std::to_string(seconds) + "s"; + return uptime; +} + +} // namespace graphics + +#endif // !MESHTASTIC_EXCLUDE_GPS +#endif // HAS_SCREEN \ No newline at end of file diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h new file mode 100644 index 000000000..21e4aef61 --- /dev/null +++ b/src/graphics/draw/UIRenderer.h @@ -0,0 +1,93 @@ +#pragma once + +#include "graphics/Screen.h" +#include "graphics/emotes.h" +#include +#include +#include + +#define HOURS_IN_MONTH 730 + +// Forward declarations for status types +namespace meshtastic +{ +class PowerStatus; +class NodeStatus; +class GPSStatus; +} // namespace meshtastic + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief UI utility drawing functions + * + * Contains utility functions for drawing common UI elements, overlays, + * battery indicators, and other shared graphical components. + */ +class UIRenderer +{ + public: + // Common UI elements + static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, + const meshtastic::PowerStatus *powerStatus); + static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, + int node_offset = 0, bool show_total = true, String additional_words = ""); + + // GPS status functions + static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + + // Layout and utility functions + static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY); + + // Overlay and special screens + static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); + + // Function overlay for showing mute/buzzer modifiers etc. + static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + + // Navigation bar overlay + static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); + + static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); + + static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // Icon and screen drawing functions + static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // Compass and location screen + static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + static NodeNum currentFavoriteNodeNum; + +// OEM screens +#ifdef USERPREFS_OEM_TEXT + static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#endif + +#ifdef USE_EINK + /// Used on eink displays while in deep sleep + static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + /// Used on eink displays when screen updates are paused + static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); +#endif + + static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); + static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + + // Message filtering + static bool shouldDrawMessage(const meshtastic_MeshPacket *packet); + // Check if the display can render a string (detect special chars; emoji) + static bool haveGlyphs(const char *str); +}; // namespace UIRenderer + +} // namespace graphics diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp new file mode 100644 index 000000000..205d5c660 --- /dev/null +++ b/src/graphics/emotes.cpp @@ -0,0 +1,225 @@ +#include "emotes.h" + +namespace graphics +{ + +// Always define Emote list and count +const Emote emotes[] = { +#ifndef EXCLUDE_EMOJI + // --- Thumbs --- + {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // 👍 Thumbs Up + {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down + + // --- Smileys (Multiple Unicode Aliases) --- + {"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes + {"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face + {"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face + {"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face + {"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes + + // --- Question/Alert --- + {"\u2753", question, question_width, question_height}, // ❓ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark + + // --- Laughing Faces --- + {"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy + {"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing + {"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat + {"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes + + // --- Gestures and People --- + {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand + {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face + {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones + + // --- Weather --- + {"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog + + // --- Misc Faces --- + {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns + + // --- Hearts (Multiple Unicode Aliases) --- + {"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart + {"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart + {"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation + {"\U00002764", heart, heart_width, heart_height}, // ❤ Red Heart (legacy) + {"\U0001F495", heart, heart_width, heart_height}, // 💕 Two Hearts + {"\U0001F496", heart, heart_width, heart_height}, // 💖 Sparkling Heart + {"\U0001F497", heart, heart_width, heart_height}, // 💗 Growing Heart + {"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow + + // --- Objects --- + {"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height} // 🔔 Bell +#endif +}; + +const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); + +#ifndef EXCLUDE_EMOJI +const unsigned char thumbup[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, + 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, + 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, + 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, + 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +}; + +const unsigned char thumbdown[] PROGMEM = { + 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, + 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, + 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, + 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, + 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +}; + +const unsigned char smiley[] PROGMEM = { + 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, + 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, + 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, + 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, + 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; + +const unsigned char question[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, + 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char bang[] PROGMEM = { + 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, + 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, + 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +}; + +const unsigned char haha[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, + 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, + 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, + 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, + 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char wave_icon[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, + 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, + 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, + 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, + 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char cowboy[] PROGMEM = { + 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, + 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, + 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, + 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, + 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, + 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, +}; + +const unsigned char deadmau5[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, + 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, + 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, + 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, + 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char sun[] PROGMEM = { + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, + 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, + 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, + 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, + 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +}; + +const unsigned char rain[] PROGMEM = { + 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, + 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, + 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, + 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +}; + +const unsigned char cloud[] PROGMEM = { + 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, + 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, + 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +}; + +const unsigned char fog[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, + 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char devil[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, + 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, + 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, + 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, + 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, + 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char heart[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, + 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, + 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, + 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +}; + +const unsigned char poo[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, + 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, + 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, + 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, + 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, + 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, +}; + +const unsigned char bell_icon[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, + 0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000, + 0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000, + 0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011, + 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, + 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, + 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; +#endif + +} // namespace graphics diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h new file mode 100644 index 000000000..5640ac04a --- /dev/null +++ b/src/graphics/emotes.h @@ -0,0 +1,86 @@ +#pragma once +#include + +namespace graphics +{ + +// === Emote List === +struct Emote { + const char *label; + const unsigned char *bitmap; + int width; + int height; +}; + +extern const Emote emotes[/* numEmotes */]; +extern const int numEmotes; + +#ifndef EXCLUDE_EMOJI +// === Emote Bitmaps === +#define thumbs_height 25 +#define thumbs_width 25 +extern const unsigned char thumbup[] PROGMEM; +extern const unsigned char thumbdown[] PROGMEM; + +#define smiley_height 30 +#define smiley_width 30 +extern const unsigned char smiley[] PROGMEM; + +#define question_height 25 +#define question_width 25 +extern const unsigned char question[] PROGMEM; + +#define bang_height 30 +#define bang_width 30 +extern const unsigned char bang[] PROGMEM; + +#define haha_height 30 +#define haha_width 30 +extern const unsigned char haha[] PROGMEM; + +#define wave_icon_height 30 +#define wave_icon_width 30 +extern const unsigned char wave_icon[] PROGMEM; + +#define cowboy_height 30 +#define cowboy_width 30 +extern const unsigned char cowboy[] PROGMEM; + +#define deadmau5_height 30 +#define deadmau5_width 60 +extern const unsigned char deadmau5[] PROGMEM; + +#define sun_height 30 +#define sun_width 30 +extern const unsigned char sun[] PROGMEM; + +#define rain_height 30 +#define rain_width 30 +extern const unsigned char rain[] PROGMEM; + +#define cloud_height 30 +#define cloud_width 30 +extern const unsigned char cloud[] PROGMEM; + +#define fog_height 25 +#define fog_width 25 +extern const unsigned char fog[] PROGMEM; + +#define devil_height 30 +#define devil_width 30 +extern const unsigned char devil[] PROGMEM; + +#define heart_height 30 +#define heart_width 30 +extern const unsigned char heart[] PROGMEM; + +#define poo_height 30 +#define poo_width 30 +extern const unsigned char poo[] PROGMEM; + +#define bell_icon_width 30 +#define bell_icon_height 30 +extern const unsigned char bell_icon[] PROGMEM; +#endif // EXCLUDE_EMOJI + +} // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index 069839a16..e9c2f00ea 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -6,7 +6,12 @@ const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; -const uint8_t imgSatellite[] PROGMEM = {0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70}; +#define imgSatellite_width 8 +#define imgSatellite_height 8 +const uint8_t imgSatellite[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000, +}; + const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C}; const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; @@ -14,11 +19,12 @@ const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3 const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; -#if defined(DISPLAY_CLOCK_FRAME) const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f, 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -#endif + +// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function +static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; #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) || ARCH_PORTDUINO) && \ @@ -37,181 +43,248 @@ const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5}; #endif -#ifndef EXCLUDE_EMOJI -#define thumbs_height 25 -#define thumbs_width 25 -static unsigned char thumbup[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, - 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, - 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, - 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, - 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +// === Horizontal battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_h[] PROGMEM = { + 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, + 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, + 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, + 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111}; + +// This is the left and right bars for the fill in +const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = { + 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111}; + +// Lightning Bolt +const unsigned char lightning_bolt_h[] PROGMEM = { + 0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, + 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000, + 0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000}; + +// === Vertical battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_v[] PROGMEM = {0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000, + 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110}; +// This is the left and right bars for the fill in +const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010, 0b10000010}; +// Lightning Bolt +const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100}; + +#define mail_width 10 +#define mail_height 7 +static const unsigned char mail[] PROGMEM = { + 0b11111111, 0b00, // Top line + 0b10000001, 0b00, // Edges + 0b11000011, 0b00, // Diagonals start + 0b10100101, 0b00, // Inner M part + 0b10011001, 0b00, // Inner M part + 0b10000001, 0b00, // Edges + 0b11111111, 0b00 // Bottom line }; -static unsigned char thumbdown[] PROGMEM = { - 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, - 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, - 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, - 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, - 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +// 📬 Mail / Message +const uint8_t icon_mail[] PROGMEM = { + 0b11111111, // ████████ top border + 0b10000001, // █ █ sides + 0b11000011, // ██ ██ diagonal + 0b10100101, // █ █ █ █ inner M + 0b10011001, // █ ██ █ inner M + 0b10000001, // █ █ sides + 0b10000001, // █ █ sides + 0b11111111 // ████████ bottom }; -#define smiley_height 30 -#define smiley_width 30 -static unsigned char smiley[] PROGMEM = { - 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, - 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, - 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, - 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, - 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; - -#define question_height 25 -#define question_width 25 -static unsigned char question[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, - 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 📍 GPS Screen / Location Pin +const unsigned char icon_compass[] PROGMEM = { + 0x3C, // Row 0: ..####.. + 0x52, // Row 1: .#..#.#. + 0x91, // Row 2: #...#..# + 0x91, // Row 3: #...#..# + 0x91, // Row 4: #...#..# + 0x81, // Row 5: #......# + 0x42, // Row 6: .#....#. + 0x3C // Row 7: ..####.. }; -#define bang_height 30 -#define bang_width 30 -static unsigned char bang[] PROGMEM = { - 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, - 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, - 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +const uint8_t icon_radio[] PROGMEM = { + 0x0F, // Row 0: ####.... + 0x10, // Row 1: ....#... + 0x27, // Row 2: ###..#.. + 0x48, // Row 3: ...#..#. + 0x93, // Row 4: ##..#..# + 0xA4, // Row 5: ..#..#.# + 0xA8, // Row 6: ...#.#.# + 0xA9 // Row 7: #..#.#.# }; -#define haha_height 30 -#define haha_width 30 -static unsigned char haha[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, - 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, - 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, - 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, - 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 🪙 Memory Icon +const uint8_t icon_memory[] PROGMEM = { + 0x24, // Row 0: ..#..#.. + 0x3C, // Row 1: ..####.. + 0xC3, // Row 2: ##....## + 0x5A, // Row 3: .#.##.#. + 0x5A, // Row 4: .#.##.#. + 0xC3, // Row 5: ##....## + 0x3C, // Row 6: ..####.. + 0x24 // Row 7: ..#..#.. }; -#define wave_icon_height 30 -#define wave_icon_width 30 -static unsigned char wave_icon[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, - 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, - 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, - 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, - 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 🌐 Wi-Fi +const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, + 0b11011011, 0b00011000, 0b00011000, 0b00000000}; + +const uint8_t icon_nodes[] PROGMEM = { + 0xF9, // Row 0 #..####### + 0x00, // Row 1 + 0xF9, // Row 2 #..####### + 0x00, // Row 3 + 0xF9, // Row 4 #..####### + 0x00, // Row 5 + 0xF9, // Row 6 #..####### + 0x00 // Row 7 }; -#define cowboy_height 30 -#define cowboy_width 30 -static unsigned char cowboy[] PROGMEM = { - 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, - 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, - 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, - 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, - 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, - 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, +// ➤ Chevron Triangle Arrow Icon (8x8) +const uint8_t icon_list[] PROGMEM = { + 0x10, // Row 0: ...#.... + 0x10, // Row 1: ...#.... + 0x38, // Row 2: ..###... + 0x38, // Row 3: ..###... + 0x7C, // Row 4: .#####.. + 0x6C, // Row 5: .##.##.. + 0xC6, // Row 6: ##...##. + 0x82 // Row 7: #.....#. }; -#define deadmau5_height 30 -#define deadmau5_width 60 -static unsigned char deadmau5[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, - 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, - 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, - 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, - 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 📶 Signal Bars Icon (left to right, small to large with spacing) +const uint8_t icon_signal[] PROGMEM = { + 0b00000000, // ░░░░░░░ + 0b10000000, // ░░░░░░░ + 0b10100000, // ░░░░█░█ + 0b10100000, // ░░░░█░█ + 0b10101000, // ░░█░█░█ + 0b10101000, // ░░█░█░█ + 0b10101010, // █░█░█░█ + 0b11111111 // ███████ }; -#define sun_width 30 -#define sun_height 30 -static unsigned char sun[] PROGMEM = { - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, - 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, - 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, - 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, - 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +// ↔️ Distance / Measurement Icon (double-ended arrow) +const uint8_t icon_distance[] PROGMEM = { + 0b00000000, // ░░░░░░░░ + 0b10000001, // █░░░░░█ arrowheads + 0b01000010, // ░█░░░█░ + 0b00100100, // ░░█░█░░ + 0b00011000, // ░░░██░░ center + 0b00100100, // ░░█░█░░ + 0b01000010, // ░█░░░█░ + 0b10000001 // █░░░░░█ }; -#define rain_width 30 -#define rain_height 30 -static unsigned char rain[] PROGMEM = { - 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, - 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, - 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, - 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, - 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +// ⚠️ Error / Fault +const uint8_t icon_error[] PROGMEM = { + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00011000, // ░░░██░░░ + 0b00000000, // ░░░░░░░░ + 0b00011000, // ░░░██░░░ + 0b00000000, // ░░░░░░░░ + 0b00000000 // ░░░░░░░░ }; -#define cloud_height 30 -#define cloud_width 30 -static unsigned char cloud[] PROGMEM = { - 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, - 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, - 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +// 🏠 Optimized Home Icon (8x8) +const uint8_t icon_home[] PROGMEM = { + 0b00011000, // ██ + 0b00111100, // ████ + 0b01111110, // ██████ + 0b11111111, // ███████ + 0b11000011, // ██ ██ + 0b11011011, // ██ ██ ██ + 0b11011011, // ██ ██ ██ + 0b11111111 // ███████ }; -#define fog_height 25 -#define fog_width 25 -static unsigned char fog[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, - 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, - 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +// 🔧 Generic module (gear-like shape) +const uint8_t icon_module[] PROGMEM = { + 0b00011000, // ░░░██░░░ + 0b00111100, // ░░████░░ + 0b01111110, // ░██████░ + 0b11011011, // ██░██░██ + 0b11011011, // ██░██░██ + 0b01111110, // ░██████░ + 0b00111100, // ░░████░░ + 0b00011000 // ░░░██░░░ }; -#define devil_height 30 -#define devil_width 30 -static unsigned char devil[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, - 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, - 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, - 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, - 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, - 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, +#define mute_symbol_width 8 +#define mute_symbol_height 8 +const uint8_t mute_symbol[] PROGMEM = { + 0b00011001, // █ + 0b00100110, // █ + 0b00100100, // ████ + 0b01001010, // █ █ █ + 0b01010010, // █ █ █ + 0b01100010, // ████████ + 0b11111111, // █ █ + 0b10011000, // █ }; -#define heart_height 30 -#define heart_width 30 -static unsigned char heart[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, - 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, - 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, - 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, - 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +#define mute_symbol_big_width 16 +#define mute_symbol_big_height 16 +const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, + 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, + 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, + 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, + 0b10000000, 0b01000001, 0b00000000, 0b10000000}; + +// Bell icon for Alert Message +#define bell_alert_width 8 +#define bell_alert_height 8 +const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, + 0b01000010, 0b01000010, 0b11111111, 0b00011000}; + +#define key_symbol_width 8 +#define key_symbol_height 8 +const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, + 0b10101001, 0b10000110, 0b00000000, 0b00000000}; + +#define placeholder_width 8 +#define placeholder_height 8 +const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, 0b11111111}; + +#define icon_node_width 8 +#define icon_node_height 8 +static const uint8_t icon_node[] PROGMEM = { + 0x10, // # + 0x10, // # ← antenna + 0x10, // # + 0xFE, // ####### ← device top + 0x82, // # # + 0xAA, // # # # # ← body with pattern + 0x92, // # # # + 0xFE // ####### ← device base }; -#define poo_width 30 -#define poo_height 30 -static unsigned char poo[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, - 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, - 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, - 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, - 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, - 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, -}; -#endif +#define bluetoothdisabled_width 8 +#define bluetoothdisabled_height 8 +const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, + 0b01001100, 0b00000000, 0b00000000, 0b00000000}; + +#define smallbulletpoint_width 8 +#define smallbulletpoint_height 8 +const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000}; + +// Clock +#define icon_clock_width 8 +#define icon_clock_height 8 +const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, + 0b10010001, 0b10000001, 0b01000010, 0b00111100}; #include "img/icon.xbm" +static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h index e1bb96450..3ce16e473 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0213BNS800.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: DKE - Size: 2.13 inch - Resolution: 122px x 250px - - Flex connector marking: FPC-7528B + - Flex connector marking (not a unique identifier): FPC-7528B Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs. DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead. diff --git a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h index 72062e0d6..257fed1a6 100644 --- a/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h +++ b/src/graphics/niche/Drivers/EInk/DEPG0290BNS800.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: DKE - Size: 2.9 inch - Resolution: 128px x 296px - - Flex connector marking: FPC-7519 rev.b + - Flex connector marking (not a unique identifier): FPC-7519 rev.b */ diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp index 2cab179b9..9a06fa841 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.cpp @@ -9,12 +9,9 @@ void GDEY0154D67::configScanning() { // "Driver output control" sendCommand(0x01); - sendData(0xC7); + sendData(0xC7); // Scan until gate 199 (200px vertical res.) sendData(0x00); sendData(0x00); - - // To-do: delete this method? - // Values set here might be redundant: C7, 00, 00 seems to be default } // Specify which information is used to control the sequence of voltages applied to move the pixels @@ -52,10 +49,10 @@ void GDEY0154D67::detachFromUpdate() { switch (updateType) { case FAST: - return beginPolling(50, 500); // At least 500ms for fast refresh + return beginPolling(50, 300); // At least 300ms for fast refresh case FULL: default: - return beginPolling(100, 2000); // At least 2 seconds for full refresh + return beginPolling(100, 1500); // At least 1.5 seconds for full refresh } } #endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h index fc4d93d12..e391eea50 100644 --- a/src/graphics/niche/Drivers/EInk/GDEY0154D67.h +++ b/src/graphics/niche/Drivers/EInk/GDEY0154D67.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Goodisplay - Size: 1.54 inch - Resolution: 200px x 200px - - Flex connector marking: FPC-B001 + - Flex connector marking (not a unique identifier): FPC-B001 */ @@ -31,9 +31,9 @@ class GDEY0154D67 : public SSD16XX GDEY0154D67() : SSD16XX(width, height, supported) {} protected: - virtual void configScanning() override; - virtual void configWaveform() override; - virtual void configUpdateSequence() override; + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; void detachFromUpdate() override; }; diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp new file mode 100644 index 000000000..b3a585eb7 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.cpp @@ -0,0 +1,58 @@ +#include "./GDEY0213B74.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void GDEY0213B74::configScanning() +{ + // "Driver output control" + 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 GDEY0213B74::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void GDEY0213B74::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 GDEY0213B74::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 \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/GDEY0213B74.h b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h new file mode 100644 index 000000000..1c36f295d --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/GDEY0213B74.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - GDEY0213B74 + - Manufacturer: Goodisplay + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): + - FPC-A002 + - FPC-A005 20.06.15 TRX + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class GDEY0213B74 : 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: + GDEY0213B74() : 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 \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp new file mode 100644 index 000000000..0509b0502 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.cpp @@ -0,0 +1,61 @@ +#include "./HINK_E0213A289.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void HINK_E0213A289::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); // Maximum gate # (249, bits 0-7) + sendData(0x00); // Maximum gate # (bit 8) + sendData(0x00); // (Do not invert scanning order) +} + +// 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 HINK_E0213A289::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +// Describes the sequence of events performed by the displays controller IC during a refresh +// Includes "power up", "load settings from memory", "update the pixels", etc +void HINK_E0213A289::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 HINK_E0213A289::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once) + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h new file mode 100644 index 000000000..eab0bf59d --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/HINK_E0213A289.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - HINK_E0213A289 + - Manufacturer: Holitech + - Size: 2.13 inch + - Resolution: 122px x 250px + - Flex connector label (not a unique identifier): FPC-7528B + + Note: as of Feb. 2025, these panels are used for "WeActStudio 2.13in B&W" display modules + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class HINK_E0213A289 : 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: + HINK_E0213A289() : SSD16XX(width, height, supported, 1) {} + + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h index ac03b65ef..612072b50 100644 --- a/src/graphics/niche/Drivers/EInk/HINK_E042A87.h +++ b/src/graphics/niche/Drivers/EInk/HINK_E042A87.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Holitech - Size: 4.2 inch - Resolution: 400px x 300px - - Flex connector marking: HINK-E042A07-FPC-A1 + - Flex connector marking (not a unique identifier): HINK-E042A07-FPC-A1 - Silver sticker with QR code, marked: HE042A87 Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h index b78e3bcca..9fa6eaac9 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13ECC1.h @@ -5,7 +5,6 @@ E-Ink display driver - Manufacturer: WISEVAST - Size: 2.13 inch - Resolution: 122px x 255px - - Flex connector marking: Soldering connector, no connector is needed */ diff --git a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h index f9da202aa..499daef05 100644 --- a/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h +++ b/src/graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h @@ -5,7 +5,7 @@ E-Ink display driver - Manufacturer: Wisevast - Size: 2.13 inch - Resolution: 122px x 250px - - Flex connector marking: HINK-E0213A162-FPC-A0 (Hidden, printed on back-side) + - Flex connector marking (not a unique identifier): HINK-E0213A162-FPC-A0 (Hidden, printed on back-side) Note: this display uses an uncommon controller IC, Fitipower JD79656. It is implemented as a "one-off", directly inheriting the EInk base class, unlike SSD16XX displays. diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp new file mode 100644 index 000000000..a8f43420f --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.cpp @@ -0,0 +1,59 @@ +#include "./ZJY128296_029EAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY128296_029EAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 295 (vertical resolution 296px) + sendCommand(0x01); + sendData(0x27); // Number of gates (295, bits 0-7) + sendData(0x01); // Number of gates (295, bit 8) + sendData(0x00); // (Do not invert scanning order) +} + +// 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 ZJY128296_029EAAMFGN::configWaveform() +{ + sendCommand(0x3C); // Border waveform: + sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white) + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY128296_029EAAMFGN::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 ZJY128296_029EAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 300); // At least 300ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h new file mode 100644 index 000000000..27644e709 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h @@ -0,0 +1,44 @@ +/* + +E-Ink display driver + - ZJY128296-029EAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.9 inch + - Resolution: 128px x 296px + - Flex connector label (not a unique identifier): FPC-A005 20.06.15 TRX + + Note: as of Feb. 2025, these panels are used for "WeActStudio 2.9in B&W" display modules + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY128296_029EAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 128; + static constexpr uint32_t height = 296; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + void configScanning() override; + void configWaveform() override; + void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h new file mode 100644 index 000000000..fb16bcf2f --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h @@ -0,0 +1,32 @@ +/* + +E-Ink display driver + - ZJY200200-0154DAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 1.54 inch + - Resolution: 200px x 200px + - Flex connector marking: FPC-B001 + + Note: as of Feb. 2025, these panels are used for "WeActStudio 1.54in B&W" display modules + + This *is* a distinct panel, however the driver is currently identical to GDEY0154D67 + We recognize it as separate now, to avoid breaking any custom builds if the drivers do need to diverge in future. + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./GDEY0154D67.h" + +namespace NicheGraphics::Drivers +{ + +typedef GDEY0154D67 ZJY200200_0154DAAMFGN; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp index 89bdb0bc7..fa85deab3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Logo/LogoApplet.cpp @@ -111,9 +111,10 @@ void InkHUD::LogoApplet::onShutdown() // Prepare for the powered-off screen now // We can change these values because the initial "shutting down" screen has already rendered at this point + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); textLeft = ""; textRight = ""; - textTitle = owner.short_name; + textTitle = parseShortName(ourNode); fontTitle = fontLarge; // This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index f162aa385..f42b9dc2c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, SHUTDOWN, NEXT_TILE, TOGGLE_BACKLIGHT, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 9fdfad8ee..69965972f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -5,6 +5,7 @@ #include "RTC.h" #include "MeshService.h" +#include "Router.h" #include "airtime.h" #include "main.h" #include "power.h" @@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") if (settings->optionalMenuItems.backlight) { backlight = Drivers::LatchingBacklight::getInstance(); } + + // Initialize the Canned Message store + // This is a shared nicheGraphics component + // - handles loading & parsing the canned messages + // - handles setting / getting of canned messages via apps (Client API Admin Messages) + cm.store = CannedMessageStore::getInstance(); } void InkHUD::MenuApplet::onForeground() @@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground() void InkHUD::MenuApplet::onBackground() { + // Discard any data we generated while selecting a canned message + // Frees heap mem + freeCannedMessageResources(); + // If device has a backlight which isn't controlled by aux button: // Item in options submenu allows keeping backlight on after menu is closed // If this item is deselected we will turn backlight off again, now that menu is closing @@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; + case STORE_CANNEDMESSAGE_SELECTION: + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + break; + + case SEND_CANNEDMESSAGE: + cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here + break; + case ROTATE: inkhud->rotate(); break; @@ -260,9 +281,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case SEND: - items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // Todo: canned messages - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + populateSendPage(); + break; + + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); break; case OPTIONS: @@ -497,6 +520,8 @@ void InkHUD::MenuApplet::populateAutoshowPage() } } +// Create MenuItem entries to select our definition of "Recent" +// Controls how long data will remain in any "Recents" flavored applets void InkHUD::MenuApplet::populateRecentsPage() { // How many values are shown for use to choose from @@ -510,6 +535,112 @@ void InkHUD::MenuApplet::populateRecentsPage() } } +// MenuItem entries for the "send" page +// Dynamically creates menu items based on available canned messages +void InkHUD::MenuApplet::populateSendPage() +{ + // Position / NodeInfo packet + items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); + + // One menu item for each canned message + uint8_t count = cm.store->size(); + for (uint8_t i = 0; i < count; i++) { + // Gather the information for this item + CannedMessages::MessageItem messageItem; + messageItem.rawText = cm.store->at(i); + messageItem.label = parse(messageItem.rawText); + + // Store the item (until the menu closes) + cm.messageItems.push_back(messageItem); + + // Create a menu item + const char *itemText = cm.messageItems.back().label.c_str(); + items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + +// Dynamically create MenuItem entries for possible canned message destinations +// All available channels are shown +// Favorite nodes are shown, provided we don't have an *excessive* amount +void InkHUD::MenuApplet::populateRecipientPage() +{ + // Create recipient data (and menu items) for any channels + // -------------------------------------------------------- + + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + // Get the channel, and check if it's enabled + meshtastic_Channel &channel = channels.getByIndex(i); + if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) + continue; + + CannedMessages::RecipientItem r; + + // Set index + r.channelIndex = channel.index; + + // Set a label for the menu item + r.label = "Ch " + to_string(i) + ": "; + if (channel.role == meshtastic_Channel_Role_PRIMARY) + r.label += "Primary"; + else + r.label += parse(channel.settings.name); + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + + // Create recipient data (and menu items) for favorite nodes + // --------------------------------------------------------- + + uint32_t nodeCount = nodeDB->getNumMeshNodes(); + uint32_t favoriteCount = 0; + + // Count favorites + for (uint32_t i = 0; i < nodeCount; i++) { + if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + favoriteCount++; + } + + // Only add favorites if the number is reasonable + // Don't want some monstrous list that takes 100 clicks to reach exit + if (favoriteCount < 20) { + for (uint32_t i = 0; i < nodeCount; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip node if not a favorite + if (!node->is_favorite) + continue; + + CannedMessages::RecipientItem r; + + r.dest = node->num; + r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) + + // Set a label for the menu item + r.label = "DM: "; + if (node->has_user) + r.label += parse(node->user.long_name); + else + r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. @@ -619,4 +750,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() return height; } +// Send a text message to the mesh +// Used to send our canned messages +void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) +{ + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + // Tack on a bell character if requested + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator + p->decoded.payload.size++; + } + + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + + service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone +} + +// Free up any heap mmemory we'd used while selecting / sending canned messages +void InkHUD::MenuApplet::freeCannedMessageResources() +{ + cm.selectedMessageItem = nullptr; + cm.selectedRecipientItem = nullptr; + cm.messageItems.clear(); + cm.recipientItems.clear(); +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index d9297c8ed..4c974672a 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -6,10 +6,12 @@ #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/Persistence.h" #include "graphics/niche/InkHUD/SystemApplet.h" +#include "graphics/niche/Utils/CannedMessageStore.h" #include "./MenuItem.h" #include "./MenuPage.h" +#include "Channels.h" #include "concurrency/OSThread.h" namespace NicheGraphics::InkHUD @@ -36,12 +38,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any void showPage(MenuPage page); // Load and display a MenuPage + + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, - uint16_t *height = nullptr); // Info panel at top of root menu + uint16_t *height = nullptr); // Info panel at top of root menu + void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh + void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage currentPage = MenuPage::ROOT; uint8_t cursor = 0; // Which menu item is currently highlighted @@ -51,6 +59,37 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread std::vector items; // MenuItems for the current page. Filled by ShowPage + // Data for selecting and sending canned messages via the menu + // Placed into a sub-class for organization only + class CannedMessages + { + public: + // Share NicheGraphics component + // Handles loading, getting, setting + CannedMessageStore *store; + + // One canned message + // Links the menu item to the true message text + struct MessageItem { + std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed + std::string rawText; // The message which will be sent, if this item is selected + } *selectedMessageItem; + + // One possible destination for a canned message + // Links the menu item to the intended recipient + // May represent either broadcast or DM + struct RecipientItem { + std::string label; // Shown in menu + NodeNum dest = NODENUM_BROADCAST; + uint8_t channelIndex = 0; + } *selectedRecipientItem; + + // These lists are generated when the menu page is populated + // Cleared onBackground (when MenuApplet closes) + std::vector messageItems; + std::vector recipientItems; + } cm; + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu }; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index d2314e83b..389e411c3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD enum MenuPage : uint8_t { ROOT, // Initial menu page SEND, + CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, APPLETS, AUTOSHOW, diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index f9439fab8..ae0836d19 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -213,7 +213,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila // Sender id if (node && node->has_user) - text += node->user.short_name; + text += parseShortName(node); else text += hexifyNodeNum(message->sender); @@ -227,7 +227,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila // Sender id if (node && node->has_user) - text += node->user.short_name; + text += parseShortName(node); else text += hexifyNodeNum(message->sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index 968775302..17d724aee 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -67,13 +67,13 @@ void InkHUD::AllMessageApplet::onRender() } // Sender's id - // - shortname, if available, or + // - short name and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender); if (sender && sender->has_user) { - header += sender->user.short_name; + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += sender->user.long_name; + header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(message->sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index 3c69495ed..dbf5c08fb 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -63,13 +63,13 @@ void InkHUD::DMApplet::onRender() } // Sender's id - // - shortname, if available, or + // - shortname and long name, if available, or // - node id meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender); if (sender && sender->has_user) { - header += sender->user.short_name; + header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc) header += " ("; - header += sender->user.long_name; + header += parse(sender->user.long_name); header += ")"; } else header += hexifyNodeNum(latestMessage->dm.sender); diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index d5d7f77f8..fdb5a168d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -13,7 +13,8 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex) +InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) + : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { // Create the message store // Will shortly attempt to load messages from RAM, if applet is active @@ -69,9 +70,8 @@ void InkHUD::ThreadedMessageApplet::onRender() // Grab data for message MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0); - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message + std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message // Cache bottom Y of message text // - Used when drawing vertical line alongside @@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender() void InkHUD::ThreadedMessageApplet::onActivate() { loadMessagesFromFlash(); - textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage + loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running -// This might be happen at shutdown, or if user disables the applet at run-time +// This might be at shutdown, or if the user disables the applet at run-time, via the menu void InkHUD::ThreadedMessageApplet::onDeactivate() { - textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage + loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel -// Method should return 0, to indicate general success to TextMessageModule -int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets if (!isActive()) - return 0; + return ProcessMessage::CONTINUE; // Abort if wrong channel - if (p->channel != this->channelIndex) - return 0; + if (mp.channel != this->channelIndex) + return ProcessMessage::CONTINUE; // Abort if message was a DM - if (p->to != NODENUM_BROADCAST) - return 0; + if (mp.to != NODENUM_BROADCAST) + return ProcessMessage::CONTINUE; // Extract info into our slimmed-down "StoredMessage" type MessageStore::Message newMessage; newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = p->from; - newMessage.channelIndex = p->channel; - newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]); + newMessage.sender = mp.from; + newMessage.channelIndex = mp.channel; + newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); // Store newest message at front // These records are used when rendering, and also stored in flash at shutdown store->messages.push_front(newMessage); // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(p) != nodeDB->getNodeNum()) + if (getFrom(&mp) != nodeDB->getNodeNum()) requestAutoshow(); // Redraw the applet, perhaps. requestUpdate(); // Want to update display, if applet is foreground - return 0; + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages + return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 3e11a25f2..c986539b3 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD class Applet; -class ThreadedMessageApplet : public Applet +class ThreadedMessageApplet : public Applet, public SinglePortModule { public: explicit ThreadedMessageApplet(uint8_t channelIndex); @@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet void onActivate() override; void onDeactivate() override; void onShutdown() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; bool approveNotification(Notification &n) override; // Which notifications to suppress protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, - &ThreadedMessageApplet::onReceiveTextMessage); - void saveMessagesToFlash(); void loadMessagesFromFlash(); diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index ee6c04938..2abe30793 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -3,13 +3,14 @@ #include "./Events.h" #include "RTC.h" -#include "modules/AdminModule.h" +#include "buzz.h" +#include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" -#include "graphics/niche/FlashData.h" +#include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; @@ -28,7 +29,7 @@ void InkHUD::Events::begin() rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); @@ -37,6 +38,13 @@ void InkHUD::Events::begin() void InkHUD::Events::onButtonShort() { + // Audio feedback (via buzzer) + // Short low tone + playBoop(); + // Cancel any beeping, buzzing, blinking + // Some button handling suppressed if we are dismissing an external notification (see below) + bool dismissedExt = dismissExternalNotification(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -49,12 +57,16 @@ void InkHUD::Events::onButtonShort() // If no system applet is handling input, default behavior instead is to cycle applets if (consumer) consumer->onButtonShortPress(); - else + else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module inkhud->nextApplet(); } void InkHUD::Events::onButtonLong() { + // Audio feedback (via buzzer) + // Low tone, longer than playBoop + playBeep(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -102,6 +114,10 @@ int InkHUD::Events::beforeDeepSleep(void *unused) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); delay(1000); // Cooldown, before potentially yanking display power + // InkHUD shutdown complete + // Firmware shutdown continues for several seconds more; flash write still pending + playShutdownMelody(); + return 0; // We agree: deep sleep now } @@ -176,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } -int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message) +int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { - switch (message->which_payload_variant) { + switch (data->request->which_payload_variant) { // Factory reset // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. case meshtastic_AdminMessage_factory_reset_device_tag: case meshtastic_AdminMessage_factory_reset_config_tag: eraseOnReboot = true; + *data->result = AdminMessageHandleResult::HANDLED; break; default: @@ -204,4 +221,24 @@ int InkHUD::Events::beforeLightSleep(void *unused) } #endif +// Silence all ongoing beeping, blinking, buzzing, coming from the external notification module +// Returns true if an external notification was active, and we dismissed it +// Button handling changes depending on our result +bool InkHUD::Events::dismissExternalNotification() +{ + // Abort if not using external notifications + if (!moduleConfig.external_notification.enabled) + return false; + + // Abort if nothing to dismiss + if (!externalNotificationModule->nagging()) + return false; + + // Stop the beep buzz blink + externalNotificationModule->stopNow(); + + // Inform that we did indeed dismiss an external notification + return true; +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 489135ea3..df68f368c 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "configuration.h" -#include "Observer.h" +#include "modules/AdminModule.h" #include "./InkHUD.h" #include "./Persistence.h" @@ -33,7 +33,7 @@ class Events int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message - int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); // Prepare for light sleep #endif @@ -54,14 +54,17 @@ class Events CallbackObserver(this, &Events::onReceiveTextMessage); // Get notified of incoming admin messages, and handle any which are relevant to InkHUD - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Events::onAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep CallbackObserver lightSleepObserver = CallbackObserver(this, &Events::beforeLightSleep); #endif + // End any externalNotification beeping, buzzing, blinking etc + bool dismissExternalNotification(); + // If set, InkHUD's data will be erased during onReboot bool eraseOnReboot = false; }; diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 40f1dd521..b85274c87 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -15,8 +15,8 @@ The save / load mechanism is a shared NicheGraphics feature. #include "configuration.h" #include "./InkHUD.h" -#include "graphics/niche/FlashData.h" #include "graphics/niche/InkHUD/MessageStore.h" +#include "graphics/niche/Utils/FlashData.h" namespace NicheGraphics::InkHUD { diff --git a/src/graphics/niche/InkHUD/PlatformioConfig.ini b/src/graphics/niche/InkHUD/PlatformioConfig.ini index cab0ea7bc..e5a0e67df 100644 --- a/src/graphics/niche/InkHUD/PlatformioConfig.ini +++ b/src/graphics/niche/InkHUD/PlatformioConfig.ini @@ -6,6 +6,7 @@ build_flags = -D MESHTASTIC_INCLUDE_NICHE_GRAPHICS ; Use NicheGraphics -D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD (a NicheGraphics UI) -D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class + -D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling -D HAS_BUTTON=0 ; Suppress default ButtonThread lib_deps = https://github.com/ZinggJM/GFX_Root#2.0.0 ; Used by InkHUD as a "slimmer" version of AdafruitGFX \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp new file mode 100644 index 000000000..50998930d --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -0,0 +1,163 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./CannedMessageStore.h" + +#include "FSCommon.h" +#include "NodeDB.h" +#include "SPILock.h" +#include "generated/meshtastic/cannedmessages.pb.h" + +using namespace NicheGraphics; + +// Location of the file which stores the canned messages on flash +static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; + +CannedMessageStore::CannedMessageStore() +{ +#if !MESHTASTIC_EXCLUDE_ADMIN + adminMessageObserver.observe(adminModule); +#endif + + // Load & parse messages from flash + load(); +} + +// Get access to (or create) the singleton instance of this class +CannedMessageStore *CannedMessageStore::getInstance() +{ + // Instantiate the class the first time this method is called + static CannedMessageStore *const singletonInstance = new CannedMessageStore; + + return singletonInstance; +} + +// Access canned messages by index +// Consumer should check CannedMessageStore::size to avoid accessing out of bounds +const std::string &CannedMessageStore::at(uint8_t i) +{ + assert(i < messages.size()); + return messages.at(i); +} + +// Number of canned message strings available +uint8_t CannedMessageStore::size() +{ + return messages.size(); +} + +// Load canned message data from flash, and parse into the individual strings +void CannedMessageStore::load() +{ + // In case we're reloading + messages.clear(); + + // Attempt to load the bulk canned message data from flash + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, + sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Abort if nothing to load + if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) + return; + + // Split into individual canned messages + // These are concatenated when stored in flash, using '|' as a delimiter + std::string s; + for (char c : cannedMessageModuleConfig.messages) { // Character by character + + // If found end of a string + if (c == '|' || c == '\0') { + // Copy into the vector (if non-empty) + if (!s.empty()) + messages.push_back(s); + + // Reset the string builder + s.clear(); + + // End of data, all strings processed + if (c == 0) + break; + } + + // Otherwise, append char (continue building string) + else + s.push_back(c); + } +} + +// Handle incoming admin messages +// We get these as an observer of AdminModule +// It's our responsibility to handle setting and getting of canned messages via the client API +// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics +int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) +{ + switch (data->request->which_payload_variant) { + + // Client API changing the canned messages + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + handleSet(data->request); + *data->result = AdminMessageHandleResult::HANDLED; + break; + + // Client API wants to know the current canned messages + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + handleGet(data->response); + *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; + + default: + break; + } + + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) +} + +// Client API changing the canned messages +void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) +{ + // Copy into the correct struct (for writing to flash as protobuf) + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, + sizeof(cannedMessageModuleConfig.messages)); + + // Ensure the directory exists +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + + // Write to flash + nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Reload from flash, to update the canned messages in RAM + // (This is a lazy way to handle it) + load(); +} + +// Client API wants to know the current canned messages +// We're reconstructing the monolithic canned message string from our copy of the messages in RAM +// Lazy, but more convenient that reloading the monolithic string from flash just for this +void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) +{ + // Merge the canned messages back into the delimited format expected + std::string merged; + if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 + merged.reserve(201); + for (std::string &s : messages) { + merged += s; + merged += '|'; + } + merged.pop_back(); // Drop the final delimiter (loop added one too many) + } + + // Place the data into the response + // This response is scoped to AdminModule::handleReceivedProtobuf + // We were passed reference to it via the observable + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.h b/src/graphics/niche/Utils/CannedMessageStore.h new file mode 100644 index 000000000..c00e1cf5c --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.h @@ -0,0 +1,54 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics tool + +Makes canned message data accessible to any NicheGraphics UI. + - handles loading & parsing from flash + - handles the admin messages for setting & getting canned messages via client API (phone apps, etc) + +The original CannedMessageModule class is bound to Screen.cpp, +making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp + +This implementation aims to be self-contained. +The necessary interaction with the AdminModule is done as an observer. + +*/ + +#pragma once + +#include "configuration.h" + +#include "modules/AdminModule.h" + +namespace NicheGraphics +{ + +class CannedMessageStore +{ + public: + static CannedMessageStore *getInstance(); // Create or get the singleton instance + const std::string &at(uint8_t i); // Get canned message at index + uint8_t size(); // Get total number of canned messages + + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + + private: + CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() + + void load(); // Load from flash, and parse + + void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages + void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages + + std::vector messages; + + // Get notified of incoming admin messages, to get / set canned messages + CallbackObserver adminMessageObserver = + CallbackObserver(this, &CannedMessageStore::onAdminMessage); +}; + +}; // namespace NicheGraphics + +#endif \ No newline at end of file diff --git a/src/graphics/niche/FlashData.h b/src/graphics/niche/Utils/FlashData.h similarity index 100% rename from src/graphics/niche/FlashData.h rename to src/graphics/niche/Utils/FlashData.h diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp new file mode 100644 index 000000000..bc75e0a54 --- /dev/null +++ b/src/input/ButtonThread.cpp @@ -0,0 +1,318 @@ +#include "ButtonThread.h" +#include "meshUtils.h" + +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_GPS +#include "GPS.h" +#endif +#include "MeshService.h" +#include "RadioLibInterface.h" +#include "buzz.h" +#include "input/InputBroker.h" +#include "main.h" +#include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" +#include "power.h" +#include "sleep.h" +#ifdef ARCH_PORTDUINO +#include "platform/portduino/PortduinoGlue.h" +#endif + +using namespace concurrency; + +#if HAS_BUTTON +#endif +ButtonThread::ButtonThread(const char *name) : OSThread(name) +{ + _originName = name; +} + +bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine, + input_broker_event singlePress, input_broker_event longPress, uint16_t longPressTime, + input_broker_event doublePress, input_broker_event longLongPress, uint16_t longLongPressTime, + input_broker_event triplePress, input_broker_event shortLong, bool touchQuirk) +{ + if (inputBroker) + inputBroker->registerSource(this); + _longPressTime = longPressTime; + _longLongPressTime = longLongPressTime; + _pinNum = pinNumber; + _activeLow = activeLow; + _touchQuirk = touchQuirk; + _intRoutine = intRoutine; + _longLongPress = longLongPress; + + userButton = OneButton(pinNumber, activeLow, activePullup); + + if (pullupSense != 0) { + pinMode(pinNumber, pullupSense); + } + + _singlePress = singlePress; + userButton.attachClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_PRESSED; + }, + this); + + if (longPress != INPUT_BROKER_NONE) { + _longPress = longPress; + userButton.attachLongPressStart( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; + }, + this); + userButton.attachLongPressStop( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; + }, + this); + } + + if (doublePress != INPUT_BROKER_NONE) { + _doublePress = doublePress; + userButton.attachDoubleClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; + }, + this); + } + + if (triplePress != INPUT_BROKER_NONE) { + _triplePress = triplePress; + userButton.attachMultiClick( + [](void *callerThread) -> void { + ButtonThread *thread = (ButtonThread *)callerThread; + thread->storeClickCount(); + thread->btnEvent = BUTTON_EVENT_MULTI_PRESSED; + }, + this); + } + if (shortLong != INPUT_BROKER_NONE) { + _shortLong = shortLong; + } + + userButton.setDebounceMs(1); + userButton.setPressMs(_longPressTime); + + if (screen) { + userButton.setClickMs(20); + } else { + userButton.setClickMs(BUTTON_CLICK_MS); + } + attachButtonInterrupts(); +#ifdef ARCH_ESP32 + // Register callbacks for before and after lightsleep + // Used to detach and reattach interrupts + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif + return true; +} + +int32_t ButtonThread::runOnce() +{ + // If the button is pressed we suppress CPU sleep until release + canSleep = true; // Assume we should not keep the board awake + + // Check for combination timeout + if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { + waitingForLongPress = false; + } + + userButton.tick(); + canSleep &= userButton.isIdle(); + + // Check if we should play lead-up sound during long press + // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers + bool buttonCurrentlyPressed = isButtonPressed(_pinNum); + + // Detect start of button press + if (buttonCurrentlyPressed && !buttonWasPressed) { + buttonPressStartTime = millis(); + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + // Progressive lead-up sound system + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && + (millis() - buttonPressStartTime) < _longLongPressTime) { + + // Start the progressive sequence if not already active + if (!leadUpSequenceActive) { + leadUpSequenceActive = true; + lastLeadUpNoteTime = millis(); + playNextLeadUpNote(); // Play the first note immediately + } + // Continue playing notes at intervals + else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes + if (playNextLeadUpNote()) { + lastLeadUpNoteTime = millis(); + } + } + } + + // Reset when button is released + if (!buttonCurrentlyPressed && buttonWasPressed) { + leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); + } + + buttonWasPressed = buttonCurrentlyPressed; + + // new behavior + if (btnEvent != BUTTON_EVENT_NONE) { + InputEvent evt; + evt.source = _originName; + evt.kbchar = 0; + evt.touchX = 0; + evt.touchY = 0; + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + evt.inputEvent = _singlePress; + // evt.kbchar = _singlePress; // todo: fix this. Some events are kb characters rather than event types + this->notifyObservers(&evt); + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + // Ignore if: TX in progress + // Uncommon T-Echo hardware bug, LoRa TX triggers touch button + if (_touchQuirk && RadioLibInterface::instance && RadioLibInterface::instance->isSending()) + break; + + // Check if this is part of a short-press + long-press combination + if (_shortLong != INPUT_BROKER_NONE && waitingForLongPress && + (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { + evt.inputEvent = _shortLong; + // evt.kbchar = _shortLong; + this->notifyObservers(&evt); + // Play the combination tune + playComboTune(); + + break; + } + + // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) + evt.inputEvent = _longPress; + this->notifyObservers(&evt); + + // Reset combination tracking + waitingForLongPress = false; + + break; + } + + case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected + LOG_INFO("Double press!"); + + // Reset combination tracking + waitingForLongPress = false; + + evt.inputEvent = _doublePress; + // evt.kbchar = _doublePress; + this->notifyObservers(&evt); + playComboTune(); + + break; + } + + case BUTTON_EVENT_MULTI_PRESSED: { // not wired in when screen is present + LOG_INFO("Mulitipress! %hux", multipressClickCount); + + // Reset combination tracking + waitingForLongPress = false; + + switch (multipressClickCount) { + case 3: + evt.inputEvent = _triplePress; + // evt.kbchar = _triplePress; + this->notifyObservers(&evt); + playComboTune(); + break; + + // No valid multipress action + default: + break; + } // end switch: click count + + break; + } // end multipress event + + // Do actual shutdown when button released, otherwise the button release + // may wake the board immediatedly. + case BUTTON_EVENT_LONG_RELEASED: { + + LOG_INFO("LONG PRESS RELEASE"); + if (_longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime) { + evt.inputEvent = _longLongPress; + this->notifyObservers(&evt); + } + // Reset combination tracking + waitingForLongPress = false; + + break; + } + } + } + btnEvent = BUTTON_EVENT_NONE; + return 50; +} + +/* + * Attach (or re-attach) hardware interrupts for buttons + * Public method. Used outside class when waking from MCU sleep + */ +void ButtonThread::attachButtonInterrupts() +{ + // Interrupt for user button, during normal use. Improves responsiveness. + attachInterrupt(_pinNum, _intRoutine, CHANGE); +} + +/* + * Detach the "normal" button interrupts. + * Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep + */ +void ButtonThread::detachButtonInterrupts() +{ + detachInterrupt(_pinNum); +} + +#ifdef ARCH_ESP32 + +// Detach our class' interrupts before lightsleep +// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press +int ButtonThread::beforeLightSleep(void *unused) +{ + detachButtonInterrupts(); + return 0; // Indicates success +} + +// Reconfigure our interrupts +// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep +int ButtonThread::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachButtonInterrupts(); + return 0; // Indicates success +} + +#endif + +// Non-static method, runs during callback. Grabs info while still valid +void ButtonThread::storeClickCount() +{ + multipressClickCount = userButton.getNumberClicks(); +} \ No newline at end of file diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h new file mode 100644 index 000000000..033f92b8b --- /dev/null +++ b/src/input/ButtonThread.h @@ -0,0 +1,113 @@ +#pragma once + +#include "InputBroker.h" +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +typedef void (*voidFuncPtr)(void); + +#ifndef BUTTON_CLICK_MS +#define BUTTON_CLICK_MS 250 +#endif + +#ifndef BUTTON_TOUCH_MS +#define BUTTON_TOUCH_MS 400 +#endif + +#ifndef BUTTON_COMBO_TIMEOUT_MS +#define BUTTON_COMBO_TIMEOUT_MS 1000 // 1 second to complete the combination -- tap faster +#endif + +#ifndef BUTTON_LEADUP_MS +#define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding +#endif + +class ButtonThread : public Observable, public concurrency::OSThread +{ + public: + const char *_originName; + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + bool initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine, + input_broker_event singlePress, input_broker_event longPress = INPUT_BROKER_NONE, + uint16_t longPressTime = 500, input_broker_event doublePress = INPUT_BROKER_NONE, + input_broker_event longLongPress = INPUT_BROKER_NONE, uint16_t longLongPressTime = 5000, + input_broker_event triplePress = INPUT_BROKER_NONE, input_broker_event shortLong = INPUT_BROKER_NONE, + bool touchQuirk = false); + + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_PRESSED_SCREEN, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, + }; + + ButtonThread(const char *name); + int32_t runOnce() override; + OneButton userButton; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + bool isButtonPressed(int buttonPin) + { + if (_activeLow) + return !digitalRead(buttonPin); // Active low: pressed = LOW + else + return digitalRead(buttonPin); // Most buttons are active low by default + } + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + private: + input_broker_event _singlePress = INPUT_BROKER_NONE; + input_broker_event _longPress = INPUT_BROKER_NONE; + input_broker_event _longLongPress = INPUT_BROKER_NONE; + + input_broker_event _doublePress = INPUT_BROKER_NONE; + input_broker_event _triplePress = INPUT_BROKER_NONE; + input_broker_event _shortLong = INPUT_BROKER_NONE; + + voidFuncPtr _intRoutine = nullptr; + uint16_t _longPressTime = 500; + uint16_t _longLongPressTime = 5000; + int _pinNum = 0; + bool _activeLow = true; + bool _touchQuirk = false; + + uint32_t buttonPressStartTime = 0; + bool buttonWasPressed = false; + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &ButtonThread::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &ButtonThread::afterLightSleep); +#endif + + volatile ButtonEventType btnEvent = BUTTON_EVENT_NONE; + + // Store click count during callback, for later use + volatile int multipressClickCount = 0; + + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; + + // Long press lead-up tracking + bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; + + static void wakeOnIrq(int irq, int mode); +}; + +extern ButtonThread *buttonThread; diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 56413bd55..1981a45d4 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -146,31 +146,31 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) { switch (key) { case LEFT: - if (inCannedMessageMenu()) // If in canned message menu - sendKey(CANCEL); // exit the menu (press imaginary cancel key) + if (inCannedMessageMenu()) // If in canned message menu + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else - sendKey(LEFT); + sendKey(INPUT_BROKER_LEFT); break; case RIGHT: - if (inCannedMessageMenu()) // If in canned message menu: - sendKey(CANCEL); // exit the menu (press imaginary cancel key) + if (inCannedMessageMenu()) // If in canned message menu: + sendKey(INPUT_BROKER_CANCEL); // exit the menu (press imaginary cancel key) else - sendKey(RIGHT); + sendKey(INPUT_BROKER_RIGHT); break; case UP: if (length == LONG) toggleGPS(); else - sendKey(UP); + sendKey(INPUT_BROKER_UP); break; case DOWN: if (length == LONG) sendAdhocPing(); else - sendKey(DOWN); + sendKey(INPUT_BROKER_DOWN); break; case OK: @@ -186,7 +186,7 @@ void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length) } // Feed input to the canned messages module -void ExpressLRSFiveWay::sendKey(KeyType key) +void ExpressLRSFiveWay::sendKey(input_broker_event key) { InputEvent e; e.source = inputSourceName; @@ -243,15 +243,9 @@ void ExpressLRSFiveWay::shutdown() shutdownAtMsec = millis() + 3000; } -// Emulate user button, or canned message SELECT -// This is necessary as canned message module doesn't translate SELECT to user button presses if the module is disabled -// Contained as one method for easier remapping of buttons by user void ExpressLRSFiveWay::click() { - if (!moduleConfig.canned_message.enabled) - powerFSM.trigger(EVENT_PRESS); - else - sendKey(OK); + sendKey(INPUT_BROKER_SELECT); } ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; diff --git a/src/input/ExpressLRSFiveWay.h b/src/input/ExpressLRSFiveWay.h index c53aa9c09..7c7f210f8 100644 --- a/src/input/ExpressLRSFiveWay.h +++ b/src/input/ExpressLRSFiveWay.h @@ -40,13 +40,13 @@ class ExpressLRSFiveWay : public Observable, public concurre // This merged an enum used by the ExpressLRS code, with meshtastic canned message values // Key names are kept simple, to allow user customizaton typedef enum { - UP = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP, - DOWN = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN, - LEFT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT, - RIGHT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT, - OK = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT, - CANCEL = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL, - NO_PRESS = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE + UP = INPUT_BROKER_UP, + DOWN = INPUT_BROKER_DOWN, + LEFT = INPUT_BROKER_LEFT, + RIGHT = INPUT_BROKER_RIGHT, + OK = INPUT_BROKER_SELECT, + CANCEL = INPUT_BROKER_CANCEL, + NO_PRESS = INPUT_BROKER_NONE } KeyType; typedef enum { SHORT, LONG } PressLength; @@ -63,7 +63,7 @@ class ExpressLRSFiveWay : public Observable, public concurre // Meshtastic code void determineAction(KeyType key, PressLength length); - void sendKey(KeyType key); + void sendKey(input_broker_event key); inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); } int32_t runOnce() override; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index cb73e32ba..ef6d8df91 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -12,7 +12,7 @@ void InputBroker::registerSource(Observable *source) int InputBroker::handleInputEvent(const InputEvent *event) { - powerFSM.trigger(EVENT_INPUT); + powerFSM.trigger(EVENT_INPUT); // todo: not every input should wake, like long hold release this->notifyObservers(event); return 0; } \ No newline at end of file diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index db7524bb0..4487fa662 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -1,29 +1,40 @@ #pragma once #include "Observer.h" -#define ANYKEY 0xFF -#define MATRIXKEY 0xFE +enum input_broker_event { + INPUT_BROKER_NONE = 0, + INPUT_BROKER_SELECT = 10, + INPUT_BROKER_UP = 17, + INPUT_BROKER_DOWN = 18, + INPUT_BROKER_LEFT = 19, + INPUT_BROKER_RIGHT = 20, + INPUT_BROKER_CANCEL = 24, + INPUT_BROKER_BACK = 27, + INPUT_BROKER_USER_PRESS, + INPUT_BROKER_ALT_PRESS, + INPUT_BROKER_ALT_LONG, + INPUT_BROKER_SHUTDOWN = 0x9b, + INPUT_BROKER_GPS_TOGGLE = 0x9e, + INPUT_BROKER_SEND_PING = 0xaf, + INPUT_BROKER_MATRIXKEY = 0xFE, + INPUT_BROKER_ANYKEY = 0xff + +}; #define INPUT_BROKER_MSG_BRIGHTNESS_UP 0x11 #define INPUT_BROKER_MSG_BRIGHTNESS_DOWN 0x12 #define INPUT_BROKER_MSG_REBOOT 0x90 -#define INPUT_BROKER_MSG_SHUTDOWN 0x9b -#define INPUT_BROKER_MSG_GPS_TOGGLE 0x9e #define INPUT_BROKER_MSG_MUTE_TOGGLE 0xac -#define INPUT_BROKER_MSG_SEND_PING 0xaf -#define INPUT_BROKER_MSG_DISMISS_FRAME 0x8b -#define INPUT_BROKER_MSG_LEFT 0xb4 -#define INPUT_BROKER_MSG_UP 0xb5 -#define INPUT_BROKER_MSG_DOWN 0xb6 -#define INPUT_BROKER_MSG_RIGHT 0xb7 #define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA +#define INPUT_BROKER_MSG_TAB 0x09 +#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { const char *source; - char inputEvent; - char kbchar; + input_broker_event inputEvent; + unsigned char kbchar; uint16_t touchX; uint16_t touchY; } InputEvent; @@ -35,6 +46,7 @@ class InputBroker : public Observable public: InputBroker(); void registerSource(Observable *source); + void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } protected: int handleInputEvent(const InputEvent *event); diff --git a/src/input/LinuxInput.cpp b/src/input/LinuxInput.cpp index 57a87b0ef..90f06ecc9 100644 --- a/src/input/LinuxInput.cpp +++ b/src/input/LinuxInput.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ int32_t LinuxInput::runOnce() perror("unable to epoll add"); return disable(); } + kb_found = true; // This is the first time the OSThread library has called this function, so do port setup firstTime = 0; } @@ -72,7 +74,7 @@ int32_t LinuxInput::runOnce() assert(rd > ((signed int)sizeof(struct input_event))); for (int j = 0; j < rd / ((signed int)sizeof(struct input_event)); j++) { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; e.kbchar = 0; unsigned int type, code; @@ -131,36 +133,36 @@ int32_t LinuxInput::runOnce() mod = 0x08; break; case KEY_ESC: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case KEY_BACK: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; // e.kbchar = key; break; case KEY_UP: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; break; case KEY_DOWN: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; break; case KEY_LEFT: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; break; - e.kbchar = INPUT_BROKER_MSG_LEFT; + e.kbchar = INPUT_BROKER_LEFT; case KEY_RIGHT: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; break; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.kbchar = 0; case KEY_ENTER: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case KEY_POWER: system("poweroff"); break; default: // all other keys if (keymap[code]) { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = keymap[code]; } break; @@ -173,8 +175,8 @@ int32_t LinuxInput::runOnce() } report[0] = modifiers; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { - if (e.inputEvent == ANYKEY && (modifiers && 0x22)) + if (e.inputEvent != INPUT_BROKER_NONE) { + if (e.inputEvent == INPUT_BROKER_ANYKEY && (modifiers && 0x22)) e.kbchar = uppers[e.kbchar]; // doesn't get punctuation. Meh. this->notifyObservers(&e); } diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 785d98ebe..0557bc180 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -7,7 +7,8 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu } void RotaryEncoderInterruptBase::init( - uint8_t pinA, uint8_t pinB, uint8_t pinPress, char eventCw, char eventCcw, char eventPressed, + uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, // std::function onIntA, std::function onIntB, std::function onIntPress) : void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { @@ -34,7 +35,7 @@ void RotaryEncoderInterruptBase::init( int32_t RotaryEncoderInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; if (this->action == ROTARY_ACTION_PRESSED) { @@ -48,7 +49,7 @@ int32_t RotaryEncoderInterruptBase::runOnce() e.inputEvent = this->_eventCcw; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index 9bcf25a69..9bdab4730 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -12,7 +12,8 @@ class RotaryEncoderInterruptBase : public Observable, public { public: explicit RotaryEncoderInterruptBase(const char *name); - void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, char eventCw, char eventCcw, char eventPressed, + void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, + input_broker_event eventPressed, // std::function onIntA, std::function onIntB, std::function onIntPress); void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); void intPressHandler(); @@ -34,8 +35,8 @@ class RotaryEncoderInterruptBase : public Observable, public private: uint8_t _pinA = 0; uint8_t _pinB = 0; - char _eventCw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventCcw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + input_broker_event _eventCw = INPUT_BROKER_NONE; + input_broker_event _eventCcw = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; }; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 7e79289e5..4f19c8b0b 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -16,9 +16,9 @@ bool RotaryEncoderInterruptImpl1::init() uint8_t pinA = moduleConfig.canned_message.inputbroker_pin_a; uint8_t pinB = moduleConfig.canned_message.inputbroker_pin_b; uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - char eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); - char eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); - char eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); // moduleConfig.canned_message.ext_notification_module_output RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, diff --git a/src/input/ScanAndSelect.cpp b/src/input/ScanAndSelect.cpp deleted file mode 100644 index 1262f99b4..000000000 --- a/src/input/ScanAndSelect.cpp +++ /dev/null @@ -1,230 +0,0 @@ -#include "configuration.h" - -// Normally these input methods are protected by guarding in setupModules -// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class -#if HAS_SCREEN - -#include "ScanAndSelect.h" -#include "modules/CannedMessageModule.h" -#include -#ifdef ARCH_PORTDUINO // Only to check for pin conflict with user button -#include "platform/portduino/PortduinoGlue.h" -#endif - -// Config -static const char name[] = "scanAndSelect"; // should match "allow input source" string -static constexpr uint32_t durationShortMs = 50; -static constexpr uint32_t durationLongMs = 1500; -static constexpr uint32_t durationAlertMs = 2000; - -// Constructor: init base class -ScanAndSelectInput::ScanAndSelectInput() : concurrency::OSThread(name) {} - -// Attempt to setup class; true if success. -// Called by setupModules method. Instance deleted if setup fails. -bool ScanAndSelectInput::init() -{ - // Short circuit: Canned messages enabled? - if (!moduleConfig.canned_message.enabled) - return false; - - // Short circuit: Using correct "input source"? - // Todo: protobuf enum instead of string? - if (strcasecmp(moduleConfig.canned_message.allow_input_source, name) != 0) - return false; - - // Determine which pin to use for the single scan-and-select button - // User can specify this by setting any of the inputbroker pins - // If all values are zero, we'll assume the user *does* want GPIO0 - if (moduleConfig.canned_message.inputbroker_pin_press) - pin = moduleConfig.canned_message.inputbroker_pin_press; - else if (moduleConfig.canned_message.inputbroker_pin_a) - pin = moduleConfig.canned_message.inputbroker_pin_a; - else if (moduleConfig.canned_message.inputbroker_pin_b) - pin = moduleConfig.canned_message.inputbroker_pin_b; - else - pin = 0; // GPIO 0 then - - // Short circuit: if selected pin conficts with the user button -#if defined(ARCH_PORTDUINO) - int pinUserButton = 0; - if (settingsMap.count(user) != 0) { - pinUserButton = settingsMap[user]; - } -#elif defined(USERPREFS_BUTTON_PIN) - int pinUserButton = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; -#elif defined(BUTTON_PIN) - int pinUserButton = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; -#else - int pinUserButton = config.device.button_gpio; -#endif - if (pin == pinUserButton) { - LOG_ERROR("ScanAndSelect conflict with user button"); - return false; - } - - // Set-up the button - pinMode(pin, INPUT_PULLUP); - attachInterrupt(pin, handleChangeInterrupt, CHANGE); - - // Connect our class to the canned message module - inputBroker->registerSource(this); - - LOG_INFO("Initialized 'Scan and Select' input for Canned Messages, using pin %d", pin); - return true; // Init succeded -} - -// Runs periodically, unless sleeping between presses -int32_t ScanAndSelectInput::runOnce() -{ - uint32_t now = millis(); - - // If: "no messages added" alert screen currently shown - if (alertingNoMessage) { - // Dismiss the alert screen several seconds after it appears - if (!Throttle::isWithinTimespanMs(alertingSinceMs, durationAlertMs)) { - alertingNoMessage = false; - screen->endAlert(); - } - } - - // If: Button is pressed - if (digitalRead(pin) == LOW) { - // New press - if (!held) { - downSinceMs = now; - } - - // Existing press - else { - // Longer than shortpress window - // Long press not yet fired (prevent repeat firing while held) - if (!longPressFired && !Throttle::isWithinTimespanMs(downSinceMs, durationLongMs)) { - longPressFired = true; - longPress(); - } - } - - // Record the change of state: button is down - held = true; - } - - // If: Button is not pressed - else { - // Button newly released - // Long press event didn't already fire - if (held && !longPressFired) { - // Duration within shortpress window - // - longer than durationShortPress (debounce) - // - shorter than durationLongPress - if (!Throttle::isWithinTimespanMs(downSinceMs, durationShortMs)) { - shortPress(); - } - } - - // Record the change of state: button is up - held = false; - longPressFired = false; // Re-Arm: allow another long press - } - - // If thread's job is done, let it sleep - if (!held && !alertingNoMessage) { - Thread::canSleep = true; - return OSThread::disable(); - } - - // Run this method again is a few ms - return durationShortMs; -} - -void ScanAndSelectInput::longPress() -{ - // (If canned messages set) - if (cannedMessageModule->hasMessages()) { - // If module frame displayed already, send the current message - if (cannedMessageModule->shouldDraw()) - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); - - // Otherwise, initial long press opens the module frame - else - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - } - - // (If canned messages not set) tell the user - else - alertNoMessage(); -} - -void ScanAndSelectInput::shortPress() -{ - // (If canned messages set) scroll to next message - if (cannedMessageModule->hasMessages()) - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - - // (If canned messages not yet set) tell the user - else - alertNoMessage(); -} - -// Begin running runOnce at regular intervals -// Called from pin change interrupt -void ScanAndSelectInput::enableThread() -{ - Thread::canSleep = false; - OSThread::enabled = true; - OSThread::setIntervalFromNow(0); -} - -// Inform user (screen) that no canned messages have been added -// Automatically dismissed after several seconds -void ScanAndSelectInput::alertNoMessage() -{ - alertingNoMessage = true; - alertingSinceMs = millis(); - - // Graphics code: the alert frame to show on screen - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH); - display->setFont(FONT_SMALL); - int16_t textX = display->getWidth() / 2; - int16_t textY = display->getHeight() / 2; - display->drawString(textX + x, textY + y, "No Canned Messages"); - }); -} - -// Remove the canned message frame from screen -// Used to dismiss the module frame when user button pressed -// Returns true if the frame was previously displayed, and has now been closed -// Return value consumed by Screen class when determining how to handle user button -bool ScanAndSelectInput::dismissCannedMessageFrame() -{ - if (cannedMessageModule->shouldDraw()) { - raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); - return true; - } - - return false; -} - -// Feed input to the canned messages module -void ScanAndSelectInput::raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key) -{ - InputEvent e; - e.source = name; - e.inputEvent = key; - notifyObservers(&e); -} - -// Pin change interrupt -void ScanAndSelectInput::handleChangeInterrupt() -{ - // Because we need to detect both press and release (rising and falling edge), the interrupt itself can't determine the - // action. Instead, we start up the thread and get it to read the button for us - - // The instance we're referring to here is created in setupModules() - scanAndSelectInput->enableThread(); -} - -ScanAndSelectInput *scanAndSelectInput = nullptr; // Instantiated in setupModules method. Deleted if unused, or init() fails - -#endif \ No newline at end of file diff --git a/src/input/ScanAndSelect.h b/src/input/ScanAndSelect.h deleted file mode 100644 index 0b3e2716e..000000000 --- a/src/input/ScanAndSelect.h +++ /dev/null @@ -1,50 +0,0 @@ -/* - A "single button" input method for Canned Messages - - - Short press to cycle through messages - - Long Press to send - - To use: - - set "allow input source" to "scanAndSelect" - - set the single button's GPIO as either pin A, pin B, or pin Press - - Originally designed to make use of "extra" built-in button on some boards. - Non-intrusive; suitable for use as a default module config. -*/ - -#pragma once -#include "concurrency/OSThread.h" -#include "main.h" - -// Normally these input methods are protected by guarding in setupModules -// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class -#if HAS_SCREEN - -class ScanAndSelectInput : public Observable, public concurrency::OSThread -{ - public: - ScanAndSelectInput(); // No-op constructor, only initializes OSThread base class - bool init(); // Attempt to setup class; true if success. Instance deleted if setup fails - bool dismissCannedMessageFrame(); // Remove the canned message frame from screen. True if frame was open, and now closed. - void alertNoMessage(); // Inform user (screen) that no canned messages have been added - - protected: - int32_t runOnce() override; // Runs at regular intervals, when enabled - void enableThread(); // Begin running runOnce at regular intervals - static void handleChangeInterrupt(); // Calls enableThread from pin change interrupt - void shortPress(); // Code to run when short press fires - void longPress(); // Code to run when long press fires - void raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key); // Feed input to canned message module - - bool held = false; // Have we handled a change in button state? - bool longPressFired = false; // Long press fires while button still held. This bool ensures the release is no-op - uint32_t downSinceMs = 0; // Debouncing for short press, timing for long press - uint8_t pin = -1; // Read from cannned message config during init - - bool alertingNoMessage = false; // Is the "no canned messages" alert shown on screen? - uint32_t alertingSinceMs = 0; // Used to dismiss the "no canned message" alert several seconds -}; - -extern ScanAndSelectInput *scanAndSelectInput; // Instantiated in setupModules method. Deleted if unused, or init() fails - -#endif \ No newline at end of file diff --git a/src/input/SerialKeyboard.cpp b/src/input/SerialKeyboard.cpp index 8d0730418..63501bda5 100644 --- a/src/input/SerialKeyboard.cpp +++ b/src/input/SerialKeyboard.cpp @@ -30,7 +30,7 @@ SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name) void SerialKeyboard::erase() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; e.source = this->_originName; this->notifyObservers(&e); @@ -81,18 +81,18 @@ int32_t SerialKeyboard::runOnce() if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but // shouldn't be a limitation InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; // SELECT OR SEND OR CANCEL EVENT if (!(shiftRegister2 & (1 << 3))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; } else if (!(shiftRegister2 & (1 << 2))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; } else if (!(shiftRegister2 & (1 << 1))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; } else if (!(shiftRegister2 & (1 << 0))) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; } // TEXT INPUT EVENT @@ -120,10 +120,10 @@ int32_t SerialKeyboard::runOnce() // BACKSPACE or TAB else if (!(shiftRegister1 & (1 << 7))) { if (shift == 0 || shift == 2) { // BACKSPACE - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; } else { // shift = 1 => TAB - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; } } @@ -146,7 +146,7 @@ int32_t SerialKeyboard::runOnce() if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) { erase(); } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = char(KeyMap[shift][quickPress][keyPressed]); } else { // then it's shift shift += 1; @@ -159,7 +159,7 @@ int32_t SerialKeyboard::runOnce() keyPressed = 13; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index 21cd7b2d5..d99379b23 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -147,7 +147,6 @@ TCA8418Keyboard::TCA8418Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nu { state = Init; last_key = -1; - next_key = -1; should_backspace = false; last_tap = 0L; char_idx = 0; diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index c7f3c1f28..5c53452a4 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -21,7 +21,6 @@ class TCA8418Keyboard KeyState state; int8_t last_key; - int8_t next_key; bool should_backspace; uint32_t last_tap; uint8_t char_idx; diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp index d2f7b54f8..c2755980e 100644 --- a/src/input/TouchScreenBase.cpp +++ b/src/input/TouchScreenBase.cpp @@ -43,6 +43,8 @@ int32_t TouchScreenBase::runOnce() // process touch events int16_t x, y; bool touched = getTouch(x, y); + if (x < 0 || y < 0) // T-deck can emit phantom touch events with a negative value when turing off the screen + touched = false; if (touched) { this->setInterval(20); _last_x = x; @@ -93,8 +95,6 @@ int32_t TouchScreenBase::runOnce() if (duration > 0 && duration < TIME_LONG_PRESS) { if (_tapped) { _tapped = false; - e.touchEvent = static_cast(TOUCH_ACTION_DOUBLE_TAP); - LOG_DEBUG("action DOUBLE TAP(%d/%d)", x, y); } else { _tapped = true; } @@ -124,7 +124,7 @@ int32_t TouchScreenBase::runOnce() } #else // fire TAP event when no 2nd tap occured within time - if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { + if (_tapped) { _tapped = false; e.touchEvent = static_cast(TOUCH_ACTION_TAP); LOG_DEBUG("action TAP(%d/%d)", _last_x, _last_y); diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h index 0b2002551..90314cf02 100644 --- a/src/input/TouchScreenBase.h +++ b/src/input/TouchScreenBase.h @@ -28,7 +28,6 @@ class TouchScreenBase : public Observable, public concurrenc TOUCH_ACTION_LEFT, TOUCH_ACTION_RIGHT, TOUCH_ACTION_TAP, - TOUCH_ACTION_DOUBLE_TAP, TOUCH_ACTION_LONG_PRESS }; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index 20196278d..cea47faeb 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -49,41 +49,33 @@ void TouchScreenImpl1::onEvent(const TouchEvent &event) { InputEvent e; e.source = event.source; - + e.kbchar = 0; e.touchX = event.x; e.touchY = event.y; switch (event.touchEvent) { case TOUCH_ACTION_LEFT: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + e.inputEvent = INPUT_BROKER_LEFT; break; } case TOUCH_ACTION_RIGHT: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + e.inputEvent = INPUT_BROKER_RIGHT; break; } case TOUCH_ACTION_UP: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + e.inputEvent = INPUT_BROKER_UP; break; } case TOUCH_ACTION_DOWN: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - break; - } - case TOUCH_ACTION_DOUBLE_TAP: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + e.inputEvent = INPUT_BROKER_DOWN; break; } case TOUCH_ACTION_LONG_PRESS: { - e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); + e.inputEvent = INPUT_BROKER_SELECT; break; } case TOUCH_ACTION_TAP: { - if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { - externalNotificationModule->stopNow(); - } else { - powerFSM.trigger(EVENT_INPUT); - } + e.inputEvent = INPUT_BROKER_USER_PRESS; break; } default: diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index e35da3622..d41ad2fd6 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -4,31 +4,41 @@ 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, - char eventDown, char eventUp, char eventLeft, char eventRight, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()) + input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, + input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; this->_pinLeft = pinLeft; this->_pinRight = pinRight; + this->_pinPress = pinPress; this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - pinMode(this->_pinLeft, INPUT_PULLUP); - pinMode(this->_pinRight, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); - attachInterrupt(this->_pinLeft, onIntLeft, RISING); - attachInterrupt(this->_pinRight, onIntRight, RISING); + if (pinPress != 255) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, TB_DIRECTION); + } + if (this->_pinDown != 255) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION); + } + if (this->_pinUp != 255) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION); + } + if (this->_pinLeft != 255) { + pinMode(this->_pinLeft, INPUT_PULLUP); + attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION); + } + if (this->_pinRight != 255) { + pinMode(this->_pinRight, INPUT_PULLUP); + attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); + } LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, pinPress); @@ -39,31 +49,49 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef int32_t TrackballInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_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) { // LOG_DEBUG("Trackball event Press"); e.inputEvent = this->_eventPressed; - } else if (this->action == TB_ACTION_UP) { + } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; - } else if (this->action == TB_ACTION_DOWN) { + } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { // LOG_DEBUG("Trackball event DOWN"); e.inputEvent = this->_eventDown; - } else if (this->action == TB_ACTION_LEFT) { + } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { // LOG_DEBUG("Trackball event LEFT"); e.inputEvent = this->_eventLeft; - } else if (this->action == TB_ACTION_RIGHT) { + } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { // LOG_DEBUG("Trackball event RIGHT"); e.inputEvent = this->_eventRight; } +#else + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; + } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } +#endif - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); } - + lastEvent = action; this->action = TB_ACTION_NONE; return 100; diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index e7fc99f54..2397839b9 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -3,18 +3,24 @@ #include "InputBroker.h" #include "mesh/NodeDB.h" +#ifndef TB_DIRECTION +#define TB_DIRECTION RISING +#endif + class TrackballInterruptBase : public Observable, public concurrency::OSThread { public: explicit TrackballInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, char eventDown, char eventUp, - char eventLeft, char eventRight, char eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), - void (*onIntRight)(), void (*onIntPress)()); + 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 eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); void intLeftHandler(); void intRightHandler(); + uint32_t lastTime = 0; virtual int32_t runOnce() override; @@ -27,18 +33,20 @@ class TrackballInterruptBase : public Observable, public con TB_ACTION_LEFT, TB_ACTION_RIGHT }; - - volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - - private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; uint8_t _pinLeft = 0; uint8_t _pinRight = 0; - char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventLeft = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventRight = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + uint8_t _pinPress = 0; + + volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + + private: + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventLeft = INPUT_BROKER_NONE; + input_broker_event _eventRight = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; + TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 0a73b83b6..896238f38 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -6,49 +6,58 @@ TrackballInterruptImpl1 *trackballInterruptImpl1; TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} -void TrackballInterruptImpl1::init() +void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress) { -#if !HAS_TRACKBALL - // Input device is disabled. - return; -#else - uint8_t pinUp = TB_UP; - uint8_t pinDown = TB_DOWN; - uint8_t pinLeft = TB_LEFT; - uint8_t pinRight = TB_RIGHT; - uint8_t pinPress = TB_PRESS; - - char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); - char eventLeft = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); - char eventRight = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); - char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventLeft = INPUT_BROKER_LEFT; + input_broker_event eventRight = INPUT_BROKER_RIGHT; + input_broker_event eventPressed = INPUT_BROKER_SELECT; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); -#endif } void TrackballInterruptImpl1::handleIntDown() { - trackballInterruptImpl1->intDownHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intDownHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntUp() { - trackballInterruptImpl1->intUpHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intUpHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntLeft() { - trackballInterruptImpl1->intLeftHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intLeftHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntRight() { - trackballInterruptImpl1->intRightHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intRightHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } void TrackballInterruptImpl1::handleIntPressed() { - trackballInterruptImpl1->intPressHandler(); + if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) { + trackballInterruptImpl1->lastTime = millis(); + trackballInterruptImpl1->intPressHandler(); + trackballInterruptImpl1->setIntervalFromNow(20); + } } diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h index 36efac6a6..4683efa41 100644 --- a/src/input/TrackballInterruptImpl1.h +++ b/src/input/TrackballInterruptImpl1.h @@ -5,7 +5,7 @@ class TrackballInterruptImpl1 : public TrackballInterruptBase { public: TrackballInterruptImpl1(); - void init(); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress); static void handleIntDown(); static void handleIntUp(); static void handleIntLeft(); diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 979489c57..c66eb13d0 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -6,8 +6,9 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre this->_originName = name; } -void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()) +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)(), + void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -31,27 +32,35 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, int32_t UpDownInterruptBase::runOnce() { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - + e.inputEvent = INPUT_BROKER_NONE; + unsigned long now = millis(); if (this->action == UPDOWN_ACTION_PRESSED) { - LOG_DEBUG("GPIO event Press"); - e.inputEvent = this->_eventPressed; + if (now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + LOG_DEBUG("GPIO event Press"); + e.inputEvent = this->_eventPressed; + } } else if (this->action == UPDOWN_ACTION_UP) { - LOG_DEBUG("GPIO event Up"); - e.inputEvent = this->_eventUp; + if (now - lastUpKeyTime >= updownDebounceMs) { + lastUpKeyTime = now; + LOG_DEBUG("GPIO event Up"); + e.inputEvent = this->_eventUp; + } } else if (this->action == UPDOWN_ACTION_DOWN) { - LOG_DEBUG("GPIO event Down"); - e.inputEvent = this->_eventDown; + if (now - lastDownKeyTime >= updownDebounceMs) { + lastDownKeyTime = now; + LOG_DEBUG("GPIO event Down"); + e.inputEvent = this->_eventDown; + } } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { e.source = this->_originName; - e.kbchar = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.kbchar = INPUT_BROKER_NONE; this->notifyObservers(&e); } this->action = UPDOWN_ACTION_NONE; - return 100; } diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index 7060a0d80..a83a298f2 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -7,8 +7,9 @@ class UpDownInterruptBase : public Observable, public concur { public: explicit UpDownInterruptBase(const char *name); - void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, char eventDown, char eventUp, char eventPressed, - void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)()); + 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)(), + unsigned long updownDebounceMs = 50); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -23,8 +24,14 @@ class UpDownInterruptBase : public Observable, public concur private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; - char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + input_broker_event _eventDown = INPUT_BROKER_NONE; + input_broker_event _eventUp = INPUT_BROKER_NONE; + input_broker_event _eventPressed = INPUT_BROKER_NONE; const char *_originName; + + unsigned long lastUpKeyTime = 0; + unsigned long lastDownKeyTime = 0; + unsigned long lastPressKeyTime = 0; + unsigned long updownDebounceMs = 50; + const unsigned long pressDebounceMs = 200; }; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 7dd1f76b2..761b92348 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -17,9 +17,9 @@ bool UpDownInterruptImpl1::init() uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); - char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); - char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + input_broker_event eventDown = INPUT_BROKER_DOWN; + input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventPressed = INPUT_BROKER_SELECT; UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 70e9e4365..5cc069816 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -75,94 +75,94 @@ int32_t KbI2cBase::runOnce() const BBQ10Keyboard::KeyEvent key = Q10keyboard.keyEvent(); if ((key.key != 0x00) && (key.state == BBQ10Keyboard::StateRelease)) { InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key.key) { case 'p': // TAB case 't': // TAB as well if (is_sym) { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'q': // ESC if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = key.key; break; case 'e': // sym e if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; - e.kbchar = INPUT_BROKER_MSG_UP; + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = INPUT_BROKER_UP; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'x': // sym x if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; - e.kbchar = INPUT_BROKER_MSG_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 's': // sym s if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 'f': // sym f if (is_sym) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; // tweak for destSelect is_sym = false; // reset sym state after second keypress } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; } break; case 0x13: // Code scanner says the SYM key is 0x13 is_sym = !is_sym; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // the modifier key is active break; case 0x0a: // apparently Enter on Q10 is a line feed instead of carriage return - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key.key; is_sym = false; // reset sym state after second keypress break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } @@ -175,57 +175,57 @@ int32_t KbI2cBase::runOnce() while (MPRkeyboard.hasEvent()) { char nextEvent = MPRkeyboard.dequeueEvent(); - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case 0x00: // MPR121_NONE - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case 0x90: // MPR121_REBOOT - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case 0xb4: // MPR121_LEFT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case 0xb5: // MPR121_UP - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case 0xb6: // MPR121_DOWN - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case 0xb7: // MPR121_RIGHT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case 0x1b: // MPR121_ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; break; case 0x08: // MPR121_BSP - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case 0x0d: // MPR121_SELECT - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; - e.kbchar = 0x0d; + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; break; default: if (nextEvent > 127) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { LOG_DEBUG("MP121 Notifying: %i Char: %i", e.inputEvent, e.kbchar); this->notifyObservers(&e); } @@ -237,57 +237,57 @@ int32_t KbI2cBase::runOnce() InputEvent e; while (TCAKeyboard.hasEvent()) { char nextEvent = TCAKeyboard.dequeueEvent(); - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x00; e.source = this->_originName; switch (nextEvent) { case _TCA8418_NONE: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; case _TCA8418_REBOOT: - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_REBOOT; break; case _TCA8418_LEFT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; e.kbchar = 0x00; break; case _TCA8418_UP: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; e.kbchar = 0x00; break; case _TCA8418_DOWN: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; e.kbchar = 0x00; break; case _TCA8418_RIGHT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; e.kbchar = 0x00; break; case _TCA8418_BSP: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; + e.inputEvent = INPUT_BROKER_BACK; e.kbchar = 0x08; break; case _TCA8418_SELECT: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; - e.kbchar = 0x0d; + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0x00; break; case _TCA8418_ESC: - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; - e.kbchar = 0x1b; + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; break; default: if (nextEvent > 127) { - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.kbchar = 0x00; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = nextEvent; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { LOG_DEBUG("TCA8418 Notifying: %i Char: %c", e.inputEvent, e.kbchar); this->notifyObservers(&e); } @@ -310,7 +310,7 @@ int32_t KbI2cBase::runOnce() if (PrintDataBuf != 0) { LOG_DEBUG("RAK14004 key 0x%x pressed", PrintDataBuf); InputEvent e; - e.inputEvent = MATRIXKEY; + e.inputEvent = INPUT_BROKER_MATRIXKEY; e.source = this->_originName; e.kbchar = PrintDataBuf; this->notifyObservers(&e); @@ -325,138 +325,150 @@ int32_t KbI2cBase::runOnce() if (i2cBus->available()) { char c = i2cBus->read(); InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (c) { case 0x71: // This is the button q. If modifier and q pressed, it cancels the input if (is_sym) { is_sym = false; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x74: // letter t. if modifier and t pressed call 'tab' if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = 0x09; // TAB Scancode } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6d: // letter m. Modifier makes it mute notifications if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; // mute notifications } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x6f: // letter o(+). Modifier makes screen increase in brightness if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_UP; // Increase Brightness code } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x69: // letter i(-). Modifier makes screen decrease in brightness if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = INPUT_BROKER_MSG_BRIGHTNESS_DOWN; // Decrease Brightness code } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x20: // Space. Send network ping like double press does if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; - e.kbchar = INPUT_BROKER_MSG_SEND_PING; // (fn + space) + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_SEND_PING; // (fn + space) } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x67: // letter g. toggle gps if (is_sym) { is_sym = false; - e.inputEvent = ANYKEY; - e.kbchar = INPUT_BROKER_MSG_GPS_TOGGLE; + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = INPUT_BROKER_GPS_TOGGLE; } else { - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; } break; case 0x1b: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; - e.kbchar = c; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; break; case 0xb5: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; - e.kbchar = INPUT_BROKER_MSG_UP; + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0; break; case 0xb6: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; - e.kbchar = INPUT_BROKER_MSG_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; break; case 0xb4: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; - e.kbchar = INPUT_BROKER_MSG_LEFT; + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; break; case 0xb7: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = INPUT_BROKER_MSG_RIGHT; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; break; case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker)) // toggle moddifiers button. is_sym = !is_sym; - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the : INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active break; + case 0x9e: // fn+g INPUT_BROKER_GPS_TOGGLE + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + e.kbchar = c; + break; + case 0xaf: // fn+space INPUT_BROKER_SEND_PING + e.inputEvent = INPUT_BROKER_SEND_PING; + e.kbchar = c; + break; + case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN + e.inputEvent = INPUT_BROKER_SHUTDOWN; + e.kbchar = c; + break; + case 0x90: // fn+r INPUT_BROKER_MSG_REBOOT case 0x91: // fn+t - case 0x9b: // fn+s INPUT_BROKER_MSG_SHUTDOWN case 0xac: // fn+m INPUT_BROKER_MSG_MUTE_TOGGLE - case 0x9e: // fn+g INPUT_BROKER_MSG_GPS_TOGGLE - case 0xaf: // fn+space INPUT_BROKER_MSG_SEND_PING + case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST // just pass those unmodified - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; break; case 0x0d: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; default: // all other keys if (c > 127) { // bogus key value - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; } - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = c; is_sym = false; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/input/kbMatrixBase.cpp b/src/input/kbMatrixBase.cpp index 51815b525..05f4d8177 100644 --- a/src/input/kbMatrixBase.cpp +++ b/src/input/kbMatrixBase.cpp @@ -73,35 +73,35 @@ int32_t KbMatrixBase::runOnce() LOG_DEBUG("Key 0x%x pressed", key); // reset shift now that we have a keypress InputEvent e; - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; switch (key) { case 0x1b: // ESC - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL; + e.inputEvent = INPUT_BROKER_CANCEL; break; case 0x08: // Back - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; break; case 0xb5: // Up - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP; + e.inputEvent = INPUT_BROKER_UP; break; case 0xb6: // Down - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN; + e.inputEvent = INPUT_BROKER_DOWN; break; case 0xb4: // Left - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; break; case 0xb7: // Right - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT; - e.kbchar = key; + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; break; case 0x0d: // Enter - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; + e.inputEvent = INPUT_BROKER_SELECT; break; case 0x00: // nopress - e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + e.inputEvent = INPUT_BROKER_NONE; break; case 0x1a: // Shift shift++; @@ -110,11 +110,11 @@ int32_t KbMatrixBase::runOnce() } break; default: // all other keys - e.inputEvent = ANYKEY; + e.inputEvent = INPUT_BROKER_ANYKEY; e.kbchar = key; break; } - if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { + if (e.inputEvent != INPUT_BROKER_NONE) { this->notifyObservers(&e); } } diff --git a/src/main.cpp b/src/main.cpp index 6aaa948e5..ad593f519 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,7 +99,24 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #endif #if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "ButtonThread.h" +#include "input/ButtonThread.h" + +#if defined(BUTTON_PIN_TOUCH) +ButtonThread *TouchButtonThread = nullptr; +#endif + +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) +ButtonThread *UserButtonThread = nullptr; +#endif + +#if defined(ALT_BUTTON_PIN) +ButtonThread *BackButtonThread = nullptr; +#endif + +#if defined(CANCEL_BUTTON_PIN) +ButtonThread *CancelButtonThread = nullptr; +#endif + #endif #include "AmbientLightingThread.h" @@ -169,6 +186,8 @@ ScanI2C::DeviceAddress screen_found = ScanI2C::ADDRESS_NONE; ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; // 0x02 for RAK14004, 0x00 for cardkb, 0x10 for T-Deck uint8_t kb_model; +// global bool to record that a kb is present +bool kb_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -220,64 +239,6 @@ const char *getDeviceName() return name; } -#if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) -static int32_t ledBlinkCount = 0; - -static int32_t elecrowLedBlinker() -{ - // are we in alert buzzer mode? -#if HAS_BUTTON - if (buttonThread->isBuzzing()) { - // blink LED three times for 3 seconds, then 3 times for a second, with one second pause - if (ledBlinkCount % 2) { // odd means LED OFF - ledBlink.set(false); - ledBlinkCount++; - if (ledBlinkCount >= 12) - ledBlinkCount = 0; - noTone(PIN_BUZZER); - return 1000; - } else { - if (ledBlinkCount < 6) { - ledBlink.set(true); - tone(PIN_BUZZER, 4000, 3000); - ledBlinkCount++; - return 3000; - } else { - ledBlink.set(true); - tone(PIN_BUZZER, 4000, 1000); - ledBlinkCount++; - return 1000; - } - } - } else { -#endif - ledBlinkCount = 0; - if (config.device.led_heartbeat_disabled) - return 1000; - - static bool ledOn; - // remain on when fully charged or discharging above 10% - if ((powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() >= 100) || - (!powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() >= 10)) { - ledOn = true; - } else { - ledOn ^= 1; - } - ledBlink.set(ledOn); - // when charging, blink 0.5Hz square wave rate to indicate that - if (powerStatus->getIsCharging()) { - return 500; - } - // Blink rapidly when almost empty or if battery is not connected - if ((!powerStatus->getIsCharging() && powerStatus->getBatteryChargePercent() < 10) || !powerStatus->getHasBattery()) { - return 250; - } -#if HAS_BUTTON - } -#endif - return 1000; -} -#else static int32_t ledBlinker() { // Still set up the blinking (heartbeat) interval but skip code path below, so LED will blink if @@ -293,7 +254,6 @@ static int32_t ledBlinker() // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that return powerStatus->getIsCharging() ? 1000 : (ledOn ? 1 : 1000); } -#endif uint32_t timeLastPowered = 0; @@ -337,12 +297,22 @@ void setup() #ifdef LED_POWER pinMode(LED_POWER, OUTPUT); - digitalWrite(LED_POWER, HIGH); + digitalWrite(LED_POWER, LED_STATE_ON); #endif #ifdef USER_LED pinMode(USER_LED, OUTPUT); - digitalWrite(USER_LED, LOW); + digitalWrite(USER_LED, HIGH ^ LED_STATE_ON); +#endif + +#ifdef WIFI_LED + pinMode(WIFI_LED, OUTPUT); + digitalWrite(WIFI_LED, LOW); +#endif + +#ifdef BLE_LED + pinMode(BLE_LED, OUTPUT); + digitalWrite(BLE_LED, LOW); #endif #if defined(T_DECK) @@ -372,11 +342,9 @@ void setup() SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif -#if !HAS_TFT meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; -#endif #ifdef USE_SEGGER auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; @@ -465,6 +433,10 @@ void setup() gpio_pullup_en((gpio_num_t)(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN)); delay(10); #endif +#ifdef BUTTON_NEED_PULLUP2 + gpio_pullup_en((gpio_num_t)BUTTON_NEED_PULLUP2); + delay(10); +#endif #endif #endif #endif @@ -475,26 +447,13 @@ void setup() #if defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) // The ThinkNodes have their own blink logic - ledPeriodic = new Periodic("Blink", elecrowLedBlinker); + // ledPeriodic = new Periodic("Blink", elecrowLedBlinker); #else ledPeriodic = new Periodic("Blink", ledBlinker); #endif fsInit(); -#if defined(_SEEED_XIAO_NRF52840_SENSE_H_) - - pinMode(CHARGE_LED, INPUT); // sets to detect if charge LED is on or off to see if USB is plugged in - - pinMode(HICHG, OUTPUT); - digitalWrite(HICHG, LOW); // 100 mA charging current if set to LOW and 50mA (actually about 20mA) if set to HIGH - - pinMode(BAT_READ, OUTPUT); - digitalWrite(BAT_READ, LOW); // This is pin P0_14 = 14 and by pullling low to GND it provices path to read on pin 32 (P0,31) - // PIN_VBAT the voltage from divider on XIAO board - -#endif - #if !MESHTASTIC_EXCLUDE_I2C #if defined(I2C_SDA1) && defined(ARCH_RP2040) Wire1.setSDA(I2C_SDA1); @@ -539,10 +498,6 @@ void setup() digitalWrite(AQ_SET_PIN, HIGH); #endif -#if HAS_TFT - tftSetup(); -#endif - // Currently only the tbeam has a PMU // PMU initialization needs to be placed before i2c scanning power = new Power(); @@ -605,7 +560,6 @@ void setup() } #endif -#if !HAS_TFT auto screenInfo = i2cScanner->firstScreen(); screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; @@ -623,16 +577,18 @@ void setup() screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } } -#endif #define UPDATE_FROM_SCANNER(FIND_FN) - +#if defined(USE_VIRTUAL_KEYBOARD) + kb_found = true; +#endif auto rtc_info = i2cScanner->firstRTC(); rtc_found = rtc_info.type != ScanI2C::DeviceType::NONE ? rtc_info.address : rtc_found; auto kb_info = i2cScanner->firstKeyboard(); if (kb_info.type != ScanI2C::DeviceType::NONE) { + kb_found = true; cardkb_found = kb_info.address; switch (kb_info.type) { case ScanI2C::DeviceType::RAK14004: @@ -732,6 +688,8 @@ void setup() scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::DFROBOT_RAIN, meshtastic_TelemetrySensorType_DFROBOT_RAIN); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::LTR390UV, meshtastic_TelemetrySensorType_LTR390UV); scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::DPS310, meshtastic_TelemetrySensorType_DPS310); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::RAK12035, meshtastic_TelemetrySensorType_RAK12035); + scannerToSensorsMap(i2cScanner, ScanI2C::DeviceType::PCT2075, meshtastic_TelemetrySensorType_PCT2075); i2cScanner.reset(); #endif @@ -771,6 +729,12 @@ void setup() // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; +#if HAS_TFT + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } +#endif + // If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { router = new NextHopRouter(); @@ -780,11 +744,6 @@ void setup() } else router = new ReliableRouter(); -#if HAS_BUTTON || defined(ARCH_PORTDUINO) - // Buttons. Moved here cause we need NodeDB to be initialized - buttonThread = new ButtonThread(); -#endif - // only play start melody when role is not tracker or sensor if (config.power.is_power_saving == true && IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_TRACKER, @@ -793,11 +752,9 @@ void setup() else playStartMelody(); -#if !HAS_TFT // fixed screen override? if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) screen_model = config.display.oled; -#endif #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 @@ -866,8 +823,23 @@ void setup() // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + 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) || \ + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); +#elif defined(ARCH_PORTDUINO) + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + } +#else + if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #endif + } +#endif // HAS_SCREEN + // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ LOG_DEBUG("Use compiled/slipstreamed %s", slipstreamTZString); // important, removing this clobbers our magic string @@ -931,9 +903,125 @@ void setup() service = new MeshService(); service->init(); + if (nodeDB->keyIsLowEntropy) { + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + // Now that the mesh service is created, create any modules setupModules(); +// buttons are now inputBroker, so have to come after setupModules +#if HAS_BUTTON + int pullup_sense = 0; +#ifdef INPUT_PULLUP_SENSE + // Some platforms (nrf52) have a SENSE variant which allows wake from sleep - override what OneButton did +#ifdef BUTTON_SENSE_TYPE + pullup_sense = BUTTON_SENSE_TYPE; +#else + pullup_sense = INPUT_PULLUP_SENSE; +#endif +#endif +#if defined(ARCH_PORTDUINO) + + if (settingsMap.count(userButtonPin) != 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + + LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]); + UserButtonThread = new ButtonThread("UserButton"); + if (screen) + UserButtonThread->initButton( + settingsMap[userButtonPin], true, true, INPUT_PULLUP, // pull up bias + []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT); + } +#endif + +#ifdef BUTTON_PIN_TOUCH + TouchButtonThread = new ButtonThread("BackButton"); + TouchButtonThread->initButton( + BUTTON_PIN_TOUCH, true, true, pullup_sense, + []() { + TouchButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_NONE, INPUT_BROKER_BACK); +#endif + +#if defined(CANCEL_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + CancelButtonThread = new ButtonThread("CancelButton"); + CancelButtonThread->initButton( + CANCEL_BUTTON_PIN, CANCEL_BUTTON_ACTIVE_LOW, CANCEL_BUTTON_ACTIVE_PULLUP, pullup_sense, + []() { + CancelButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_CANCEL, INPUT_BROKER_SHUTDOWN, 4000); +#endif + +#if defined(ALT_BUTTON_PIN) + // Buttons. Moved here cause we need NodeDB to be initialized + BackButtonThread = new ButtonThread("BackButton"); + BackButtonThread->initButton( + ALT_BUTTON_PIN, ALT_BUTTON_ACTIVE_LOW, ALT_BUTTON_ACTIVE_PULLUP, pullup_sense, + []() { + BackButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_ALT_PRESS, INPUT_BROKER_ALT_LONG, 500); +#endif + +#if defined(BUTTON_PIN) +#if defined(USERPREFS_BUTTON_PIN) + int _pinNum = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; +#else + int _pinNum = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; +#endif +#ifndef BUTTON_ACTIVE_LOW +#define BUTTON_ACTIVE_LOW true +#endif +#ifndef BUTTON_ACTIVE_PULLUP +#define BUTTON_ACTIVE_PULLUP true +#endif + + // Buttons. Moved here cause we need NodeDB to be initialized + // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP + UserButtonThread = new ButtonThread("UserButton"); + if (screen) + UserButtonThread->initButton( + _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense, + []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT, 500, INPUT_BROKER_NONE, INPUT_BROKER_SHUTDOWN); + else + UserButtonThread->initButton( + _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense, + []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }, + INPUT_BROKER_USER_PRESS, INPUT_BROKER_SHUTDOWN, 5000, INPUT_BROKER_SEND_PING, INPUT_BROKER_GPS_TOGGLE); +#endif + +#endif + #ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS // After modules are setup, so we can observe modules setupNicheGraphics(); @@ -956,19 +1044,19 @@ void setup() // the current region name) #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) - screen->setup(); + if (screen) + screen->setup(); #elif defined(ARCH_PORTDUINO) - if (screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) { + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) screen->setup(); #endif #endif - screen->print("Started...\n"); - #ifdef PIN_PWR_DELAY_MS // This may be required to give the peripherals time to power up. delay(PIN_PWR_DELAY_MS); @@ -1227,9 +1315,12 @@ void setup() LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); + if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) { + screen->showOverlayBanner("Rebooting..."); + } rebootAtMsec = millis() + 5000; } } @@ -1332,7 +1423,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics -#if (!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES +#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO diff --git a/src/main.h b/src/main.h index beeb1f940..79094e2d3 100644 --- a/src/main.h +++ b/src/main.h @@ -31,13 +31,13 @@ extern HardwareSPI *LoraSPI; extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; +extern bool kb_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; extern bool eink_found; extern bool pmu_found; -extern bool isCharging; extern bool isUSBPowered; #ifdef T_WATCH_S3 diff --git a/src/mesh/CryptoEngine.cpp b/src/mesh/CryptoEngine.cpp index d32b73855..82d0a9f57 100644 --- a/src/mesh/CryptoEngine.cpp +++ b/src/mesh/CryptoEngine.cpp @@ -3,12 +3,17 @@ #include "architecture.h" #if !(MESHTASTIC_EXCLUDE_PKI) +#include "NodeDB.h" #include "aes-ccm.h" #include "meshUtils.h" #include #include +#include #include #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) +#if !defined(ARCH_STM32WL) +#define CryptRNG RNG +#endif /** * Create a public/private key pair with Curve25519. @@ -18,6 +23,14 @@ */ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey) { + // Mix in any randomness we can, to make key generation stronger. + CryptRNG.begin(optstr(APP_VERSION)); + if (myNodeInfo.device_id.size == 16) { + CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size); + } + auto noise = random(); + CryptRNG.stir((uint8_t *)&noise, sizeof(noise)); + LOG_DEBUG("Generate Curve25519 keypair"); Curve25519::dh1(public_key, private_key); memcpy(pubKey, public_key, sizeof(public_key)); diff --git a/src/mesh/Default.h b/src/mesh/Default.h index 208f992c8..fd3f10668 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -61,12 +61,17 @@ class Default throttlingFactor = 0.04; else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST) throttlingFactor = 0.02; - else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW) - throttlingFactor = 0.01; else if (config.lora.use_preset && IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST, - meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO)) - return 1.0; // Don't bother throttling for highest bandwidth presets + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO, + meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)) + throttlingFactor = 0.01; + +#if USERPREFS_EVENT_MODE + // If we are in event mode, scale down the throttling factor + throttlingFactor = 0.04; +#endif + // Scaling up traffic based on number of nodes over 40 int nodesOverForty = (numOnlineNodes - 40); return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default) diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index aecc8f722..8cc05994c 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -71,6 +71,8 @@ template bool LR11x0Interface::init() RadioLibInterface::init(); + limitPower(); + if (power > LR1110_MAX_POWER) // Clamp power to maximum defined level power = LR1110_MAX_POWER; @@ -80,8 +82,6 @@ template bool LR11x0Interface::init() preambleLength = 12; // 12 is the default for operation above 2GHz } - limitPower(); - #ifdef LR11X0_RF_SWITCH_SUBGHZ pinMode(LR11X0_RF_SWITCH_SUBGHZ, OUTPUT); digitalWrite(LR11X0_RF_SWITCH_SUBGHZ, getFreq() < 1e9 ? HIGH : LOW); diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index 0c312fd1e..f8af81321 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -118,10 +118,10 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t } /* Attempt to find a packet from this queue. Return true if it was found. */ -bool MeshPacketQueue::find(NodeNum from, PacketId id) +bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); + const auto p = (*it); if (getFrom(p) == from && p->id == id) { return true; } diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index 6b2c3998a..1b338f9ed 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -39,5 +39,5 @@ class MeshPacketQueue meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true); /* Attempt to find a packet from this queue. Return true if it was found. */ - bool find(NodeNum from, PacketId id); + bool find(const NodeNum from, const PacketId id); }; \ No newline at end of file diff --git a/src/mesh/NextHopRouter.cpp b/src/mesh/NextHopRouter.cpp index f21974a2e..860250f75 100644 --- a/src/mesh/NextHopRouter.cpp +++ b/src/mesh/NextHopRouter.cpp @@ -165,10 +165,15 @@ bool NextHopRouter::stopRetransmission(GlobalPacketId key) /* Only when we already transmitted a packet via LoRa, we will cancel the packet in the Tx queue to avoid canceling a transmission if it was ACKed super fast via MQTT */ if (old->numRetransmissions < NUM_RELIABLE_RETX - 1) { - // remove the 'original' (identified by originator and packet->id) from the txqueue and free it - cancelSending(getFrom(p), p->id); - // now free the pooled copy for retransmission too - packetPool.release(p); + // We only cancel it if we are the original sender or if we're not a router(_late)/repeater + if (isFromUs(p) || (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) { + // remove the 'original' (identified by originator and packet->id) from the txqueue and free it + cancelSending(getFrom(p), p->id); + // now free the pooled copy for retransmission too + packetPool.release(p); + } } auto numErased = pending.erase(key); assert(numErased == 1); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 629204622..ed4a7b5c9 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -8,6 +8,7 @@ #include "Default.h" #include "FSCommon.h" #include "MeshRadio.h" +#include "MeshService.h" #include "NodeDB.h" #include "PacketHistory.h" #include "PowerFSM.h" @@ -264,7 +265,7 @@ NodeDB::NodeDB() #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI) - if (!owner.is_licensed) { + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { bool keygenSuccess = false; if (config.security.private_key.size == 32) { if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { @@ -290,6 +291,16 @@ NodeDB::NodeDB() crypto->setDHPrivateKey(config.security.private_key.bytes); } #endif + keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key); + if (keyIsLowEntropy) { + LOG_WARN("Erasing low entropy keys"); + config.security.private_key.size = 0; + memfll(config.security.private_key.bytes, '\0', sizeof(config.security.private_key.bytes)); + config.security.public_key.size = 0; + memfll(config.security.public_key.bytes, '\0', sizeof(config.security.public_key.bytes)); + owner.public_key.size = 0; + memfll(owner.public_key.bytes, '\0', sizeof(owner.public_key.bytes)); + } // Include our owner in the node db under our nodenum meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum()); info->user = TypeConversions::ConvertToUserLite(owner); @@ -502,6 +513,25 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; + +#if HAS_TFT // For the devices that support MUI, default to that + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; +#endif + +#ifdef USERPREFS_CONFIG_DEVICE_ROLE + // Restrict ROUTER*, LOST AND FOUND, and REPEATER roles for security reasons + if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, + meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_REPEATER, + meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) { + LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role"); + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else { + config.device.role = USERPREFS_CONFIG_DEVICE_ROLE; + } +#else + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client. +#endif + #ifdef USERPREFS_CONFIG_LORA_REGION config.lora.region = USERPREFS_CONFIG_LORA_REGION; #else @@ -674,6 +704,11 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) } #endif +#ifdef USERPREFS_CONFIG_DEVICE_ROLE + // Apply role-specific defaults when role is set via user preferences + installRoleDefaults(config.device.role); +#endif + initConfigIntervals(); } @@ -760,15 +795,7 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; #endif -#ifdef BUTTON_SECONDARY_CANNEDMESSAGES - // Use a board's second built-in button as input source for canned messages - moduleConfig.canned_message.enabled = true; - moduleConfig.canned_message.inputbroker_pin_press = BUTTON_PIN_SECONDARY; - strcpy(moduleConfig.canned_message.allow_input_source, "scanAndSelect"); -#endif - moduleConfig.has_canned_message = true; - #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif @@ -1517,15 +1544,25 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) return; } info->num = contact.node_num; - info->last_heard = getValidTime(RTCQualityNTP); info->has_user = true; info->user = TypeConversions::ConvertToUserLite(contact.user); - info->is_favorite = true; - // Mark the node's key as manually verified to indicate trustworthiness. - info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; - updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); - notifyObservers(true); // Force an update whether or not our node counts have changed + if (contact.should_ignore) { + // If should_ignore is set, + // we need to clear the public key and other cruft, in addition to setting the node as ignored + info->is_ignored = true; + info->has_device_metrics = false; + info->has_position = false; + info->user.public_key.size = 0; + info->user.public_key.bytes[0] = 0; + } else { + info->last_heard = getValidTime(RTCQualityNTP); + info->is_favorite = true; + info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + // Mark the node's key as manually verified to indicate trustworthiness. + updateGUIforNode = info; + // powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired + notifyObservers(true); // Force an update whether or not our node counts have changed + } saveNodeDatabaseToDisk(); } @@ -1539,8 +1576,22 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde } #if !(MESHTASTIC_EXCLUDE_PKI) - if (p.public_key.size > 0) { + if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) { printBytes("Incoming Pubkey: ", p.public_key.bytes, 32); + + // Alert the user if a remote node is advertising public key that matches our own + if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0 && !duplicateWarned) { + duplicateWarned = true; + char warning[] = "Remote device %s has advertised your public key. This may indicate a compromised key. You may need " + "to regenerate your public keys."; + LOG_WARN(warning, p.long_name); + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->which_payload_variant = meshtastic_ClientNotification_duplicated_public_key_tag; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, warning, p.long_name); + service->sendClientNotification(cn); + } } if (info->user.public_key.size > 0) { // if we have a key for this user already, don't overwrite with a new one LOG_INFO("Public Key set for node, not updating!"); @@ -1568,7 +1619,6 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde if (changed) { updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); notifyObservers(true); // Force an update whether or not our node counts have changed // We just changed something about a User, @@ -1715,6 +1765,39 @@ UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum) return info->user.is_licensed ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed; } +bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t keyToTest) +{ + if (keyToTest.size == 32) { + uint8_t keyHash[32] = {0}; + memcpy(keyHash, keyToTest.bytes, keyToTest.size); + crypto->hash(keyHash, 32); + if (memcmp(keyHash, LOW_ENTROPY_HASH1, sizeof(LOW_ENTROPY_HASH1)) == + 0 || // should become an array that gets looped through rather than this abomination + memcmp(keyHash, LOW_ENTROPY_HASH2, sizeof(LOW_ENTROPY_HASH2)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH3, sizeof(LOW_ENTROPY_HASH3)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH4, sizeof(LOW_ENTROPY_HASH4)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH5, sizeof(LOW_ENTROPY_HASH5)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH6, sizeof(LOW_ENTROPY_HASH6)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH7, sizeof(LOW_ENTROPY_HASH7)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH8, sizeof(LOW_ENTROPY_HASH8)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH9, sizeof(LOW_ENTROPY_HASH9)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH10, sizeof(LOW_ENTROPY_HASH10)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH11, sizeof(LOW_ENTROPY_HASH11)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH12, sizeof(LOW_ENTROPY_HASH12)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH13, sizeof(LOW_ENTROPY_HASH13)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH14, sizeof(LOW_ENTROPY_HASH14)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH15, sizeof(LOW_ENTROPY_HASH15)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH16, sizeof(LOW_ENTROPY_HASH16)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH17, sizeof(LOW_ENTROPY_HASH17)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH18, sizeof(LOW_ENTROPY_HASH18)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH19, sizeof(LOW_ENTROPY_HASH19)) == 0 || + memcmp(keyHash, LOW_ENTROPY_HASH20, sizeof(LOW_ENTROPY_HASH20)) == 0) { + return true; + } + } + return false; +} + bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location) { bool success = false; @@ -1806,10 +1889,6 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, /// Record an error that should be reported via analytics void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename) { - // Print error to screen and serial port - String lcd = String("Critical error ") + code + "!\n"; - if (screen) - screen->print(lcd.c_str()); if (filename) { LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address); } else { @@ -1825,4 +1904,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co LOG_ERROR("A critical failure occurred, portduino is exiting"); exit(2); #endif -} \ No newline at end of file +} diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 16159d380..90ca5aefd 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -17,6 +17,70 @@ #include "PortduinoGlue.h" #endif +#if !defined(MESHTASTIC_EXCLUDE_PKI) + +static const uint8_t LOW_ENTROPY_HASH1[] = {0xf4, 0x7e, 0xcc, 0x17, 0xe6, 0xb4, 0xa3, 0x22, 0xec, 0xee, 0xd9, + 0x08, 0x4f, 0x39, 0x63, 0xea, 0x80, 0x75, 0xe1, 0x24, 0xce, 0x05, + 0x36, 0x69, 0x63, 0xb2, 0xcb, 0xc0, 0x28, 0xd3, 0x34, 0x8b}; +static const uint8_t LOW_ENTROPY_HASH2[] = {0x5a, 0x9e, 0xa2, 0xa6, 0x8a, 0xa6, 0x66, 0xc1, 0x5f, 0x55, 0x00, + 0x64, 0xa3, 0xa6, 0xfe, 0x71, 0xc0, 0xbb, 0x82, 0xc3, 0x32, 0x3d, + 0x7a, 0x7a, 0xe3, 0x6e, 0xfd, 0xdd, 0xad, 0x3a, 0x66, 0xb9}; +static const uint8_t LOW_ENTROPY_HASH3[] = {0xb3, 0xdf, 0x3b, 0x2e, 0x67, 0xb6, 0xd5, 0xf8, 0xdf, 0x76, 0x2c, + 0x45, 0x5e, 0x2e, 0xbd, 0x16, 0xc5, 0xf8, 0x67, 0xaa, 0x15, 0xf8, + 0x92, 0x0b, 0xdf, 0x5a, 0x66, 0x50, 0xac, 0x0d, 0xbb, 0x2f}; +static const uint8_t LOW_ENTROPY_HASH4[] = {0x3b, 0x8f, 0x86, 0x3a, 0x38, 0x1f, 0x77, 0x39, 0xa9, 0x4e, 0xef, + 0x91, 0x18, 0x5a, 0x62, 0xe1, 0xaa, 0x9d, 0x36, 0xea, 0xce, 0x60, + 0x35, 0x8d, 0x9d, 0x1f, 0xf4, 0xb8, 0xc9, 0x13, 0x6a, 0x5d}; +static const uint8_t LOW_ENTROPY_HASH5[] = {0x36, 0x7e, 0x2d, 0xe1, 0x84, 0x5f, 0x42, 0x52, 0x29, 0x11, 0x0a, + 0x25, 0x64, 0x54, 0x6a, 0x6b, 0xfd, 0xb6, 0x65, 0xff, 0x15, 0x1a, + 0x51, 0x71, 0x22, 0x40, 0x57, 0xf6, 0x91, 0x9b, 0x64, 0x58}; +static const uint8_t LOW_ENTROPY_HASH6[] = {0x16, 0x77, 0xeb, 0xa4, 0x52, 0x91, 0xfb, 0x26, 0xcf, 0x8f, 0xd7, + 0xd9, 0xd1, 0x5d, 0xc4, 0x68, 0x73, 0x75, 0xed, 0xc5, 0x95, 0x58, + 0xee, 0x90, 0x56, 0xd4, 0x2f, 0x31, 0x29, 0xf7, 0x8c, 0x1f}; +static const uint8_t LOW_ENTROPY_HASH7[] = {0x31, 0x8c, 0xa9, 0x5e, 0xed, 0x3c, 0x12, 0xbf, 0x97, 0x9c, 0x47, + 0x8e, 0x98, 0x9d, 0xc2, 0x3e, 0x86, 0x23, 0x90, 0x29, 0xc8, 0xb0, + 0x20, 0xf8, 0xb1, 0xb0, 0xaa, 0x19, 0x2a, 0xcf, 0x0a, 0x54}; +static const uint8_t LOW_ENTROPY_HASH8[] = {0xa4, 0x8a, 0x99, 0x0e, 0x51, 0xdc, 0x12, 0x20, 0xf3, 0x13, 0xf5, + 0x2b, 0x3a, 0xe2, 0x43, 0x42, 0xc6, 0x52, 0x98, 0xcd, 0xbb, 0xca, + 0xb1, 0x31, 0xa0, 0xd4, 0xd6, 0x30, 0xf3, 0x27, 0xfb, 0x49}; +static const uint8_t LOW_ENTROPY_HASH9[] = {0xd2, 0x3f, 0x13, 0x8d, 0x22, 0x04, 0x8d, 0x07, 0x59, 0x58, 0xa0, + 0xf9, 0x55, 0xcf, 0x30, 0xa0, 0x2e, 0x2f, 0xca, 0x80, 0x20, 0xe4, + 0xde, 0xa1, 0xad, 0xd9, 0x58, 0xb3, 0x43, 0x2b, 0x22, 0x70}; +static const uint8_t LOW_ENTROPY_HASH10[] = {0x40, 0x41, 0xec, 0x6a, 0xd2, 0xd6, 0x03, 0xe4, 0x9a, 0x9e, 0xbd, + 0x6c, 0x0a, 0x9b, 0x75, 0xa4, 0xbc, 0xab, 0x6f, 0xa7, 0x95, 0xff, + 0x2d, 0xf6, 0xe9, 0xb9, 0xab, 0x4c, 0x0c, 0x1c, 0xd0, 0x3b}; +static const uint8_t LOW_ENTROPY_HASH11[] = {0x22, 0x49, 0x32, 0x2b, 0x00, 0xf9, 0x22, 0xfa, 0x17, 0x02, 0xe9, + 0x64, 0x82, 0xf0, 0x4d, 0x1b, 0xc7, 0x04, 0xfc, 0xdc, 0x8c, 0x5e, + 0xb6, 0xd9, 0x16, 0xd6, 0x37, 0xce, 0x59, 0xaa, 0x09, 0x49}; +static const uint8_t LOW_ENTROPY_HASH12[] = {0x48, 0x6f, 0x1e, 0x48, 0x97, 0x88, 0x64, 0xac, 0xe8, 0xeb, 0x30, + 0xa3, 0xc3, 0xe1, 0xcf, 0x97, 0x39, 0xa6, 0x55, 0x5b, 0x5f, 0xbf, + 0x18, 0xb7, 0x3a, 0xdf, 0xa8, 0x75, 0xe7, 0x9d, 0xe0, 0x1e}; +static const uint8_t LOW_ENTROPY_HASH13[] = {0x09, 0xb4, 0xe2, 0x6d, 0x28, 0x98, 0xc9, 0x47, 0x66, 0x46, 0xbf, + 0xff, 0x58, 0x17, 0x91, 0xaa, 0xc3, 0xbf, 0x4a, 0x9d, 0x0b, 0x88, + 0xb1, 0xf1, 0x03, 0xdd, 0x61, 0xd7, 0xba, 0x9e, 0x64, 0x98}; +static const uint8_t LOW_ENTROPY_HASH14[] = {0x39, 0x39, 0x84, 0xe0, 0x22, 0x2f, 0x7d, 0x78, 0x45, 0x18, 0x72, + 0xb4, 0x13, 0xd2, 0x01, 0x2f, 0x3c, 0xa1, 0xb0, 0xfe, 0x39, 0xd0, + 0xf1, 0x3c, 0x72, 0xd6, 0xef, 0x54, 0xd5, 0x77, 0x22, 0xa0}; +static const uint8_t LOW_ENTROPY_HASH15[] = {0x0a, 0xda, 0x5f, 0xec, 0xff, 0x5c, 0xc0, 0x2e, 0x5f, 0xc4, 0x8d, + 0x03, 0xe5, 0x80, 0x59, 0xd3, 0x5d, 0x49, 0x86, 0xe9, 0x8d, 0xf6, + 0xf6, 0x16, 0x35, 0x3d, 0xf9, 0x9b, 0x29, 0x55, 0x9e, 0x64}; +static const uint8_t LOW_ENTROPY_HASH16[] = {0x08, 0x56, 0xF0, 0xD7, 0xEF, 0x77, 0xD6, 0x11, 0x1C, 0x8F, 0x95, + 0x2D, 0x3C, 0xDF, 0xB1, 0x22, 0xBF, 0x60, 0x9B, 0xE5, 0xA9, 0xC0, + 0x6E, 0x4B, 0x01, 0xDC, 0xD1, 0x57, 0x44, 0xB2, 0xA5, 0xCF}; +static const uint8_t LOW_ENTROPY_HASH17[] = {0x2C, 0xB2, 0x77, 0x85, 0xD6, 0xB7, 0x48, 0x9C, 0xFE, 0xBC, 0x80, + 0x26, 0x60, 0xF4, 0x6D, 0xCE, 0x11, 0x31, 0xA2, 0x1E, 0x33, 0x0A, + 0x6D, 0x2B, 0x00, 0xFA, 0x0C, 0x90, 0x95, 0x8F, 0x5C, 0x6B}; +static const uint8_t LOW_ENTROPY_HASH18[] = {0xFA, 0x59, 0xC8, 0x6E, 0x94, 0xEE, 0x75, 0xC9, 0x9A, 0xB0, 0xFE, + 0x89, 0x36, 0x40, 0xC9, 0x99, 0x4A, 0x3B, 0xF4, 0xAA, 0x12, 0x24, + 0xA2, 0x0F, 0xF9, 0xD1, 0x08, 0xCB, 0x78, 0x19, 0xAA, 0xE5}; +static const uint8_t LOW_ENTROPY_HASH19[] = {0x6E, 0x42, 0x7A, 0x4A, 0x8C, 0x61, 0x62, 0x22, 0xA1, 0x89, 0xD3, + 0xA4, 0xC2, 0x19, 0xA3, 0x83, 0x53, 0xA7, 0x7A, 0x0A, 0x89, 0xE2, + 0x54, 0x52, 0x62, 0x3D, 0xE7, 0xCA, 0x8C, 0xF6, 0x6A, 0x60}; +static const uint8_t LOW_ENTROPY_HASH20[] = {0x20, 0x27, 0x2F, 0xBA, 0x0C, 0x99, 0xD7, 0x29, 0xF3, 0x11, 0x35, + 0x89, 0x9D, 0x0E, 0x24, 0xA1, 0xC3, 0xCB, 0xDF, 0x8A, 0xF1, 0xC6, + 0xFE, 0xD0, 0xD7, 0x9F, 0x92, 0xD6, 0x8F, 0x59, 0xBF, 0xE4}; +static const char LOW_ENTROPY_WARNING[] = "Compromised keys detected, please regenerate."; +#endif /* DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a #define here. @@ -87,6 +151,9 @@ class NodeDB Observable newStatus; pb_size_t numMeshNodes; + bool keyIsLowEntropy = false; + bool hasWarned = false; + /// don't do mesh based algorithm for node id assignment (initially) /// instead just store in flash - possibly even in the initial alpha release do this hack NodeDB(); @@ -205,11 +272,14 @@ class NodeDB bool hasValidPosition(const meshtastic_NodeInfoLite *n); + bool checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t keyToTest); + bool backupPreferences(meshtastic_AdminMessage_BackupLocation location); bool restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat = SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS); private: + bool duplicateWarned = false; uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually /// Find a node in our DB, create an empty NodeInfoLite if missing diff --git a/src/mesh/PacketHistory.cpp b/src/mesh/PacketHistory.cpp index 15fa9cdcd..f42b151c8 100644 --- a/src/mesh/PacketHistory.cpp +++ b/src/mesh/PacketHistory.cpp @@ -7,152 +7,378 @@ #endif #include "Throttle.h" -PacketHistory::PacketHistory() +#define PACKETHISTORY_MAX \ + max((u_int32_t)(MAX_NUM_NODES * 2.0), \ + (u_int32_t)100) // x2..3 Should suffice. Empirical setup. 16B per record malloc'ed, but no less than 100 + +#define RECENT_WARN_AGE (10 * 60 * 1000L) // Warn if the packet that gets removed was more recent than 10 min + +#define VERBOSE_PACKET_HISTORY 0 // Set to 1 for verbose logging, 2 for heavy debugging +#define PACKET_HISTORY_TRACE_AGING 1 // Set to 1 to enable logging of the age of re/used history slots + +PacketHistory::PacketHistory(uint32_t size) : recentPacketsCapacity(0), recentPackets(NULL) // Initialize members { - recentPackets.reserve(MAX_NUM_NODES); // Prealloc the worst case # of records - to prevent heap fragmentation - // setup our periodic task + if (size < 4 || size > PACKETHISTORY_MAX) { // Copilot suggested - makes sense + LOG_WARN("Packet History - Invalid size %d, using default %d", size, PACKETHISTORY_MAX); + size = PACKETHISTORY_MAX; // Use default size if invalid + } + + // Allocate memory for the recent packets array + recentPacketsCapacity = size; + recentPackets = new PacketRecord[recentPacketsCapacity]; + if (!recentPackets) { // No logging here, console/log probably uninitialized yet. + LOG_ERROR("Packet History - Memory allocation failed for size=%d entries / %d Bytes", size, + sizeof(PacketRecord) * recentPacketsCapacity); + recentPacketsCapacity = 0; // mark allocation fail + return; // return early + } + + // Initialize the recent packets array to zero + memset(recentPackets, 0, sizeof(PacketRecord) * recentPacketsCapacity); } -/** - * Update recentBroadcasts and return true if we have already seen this packet - */ +PacketHistory::~PacketHistory() +{ + recentPacketsCapacity = 0; + delete[] recentPackets; + recentPackets = NULL; +} + +/** Update recentPackets and return true if we have already seen this packet */ bool PacketHistory::wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate, bool *wasFallback, bool *weWereNextHop) { + if (!initOk()) { + LOG_ERROR("Packet History - Was Seen Recently: NOT INITIALIZED!"); + return false; + } + if (p->id == 0) { - LOG_DEBUG("Ignore message with zero id"); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: ID is 0, not a floodable message"); +#endif return false; // Not a floodable message ID, so we don't care } PacketRecord r; + memset(&r, 0, sizeof(PacketRecord)); // Initialize the record to zero + + // Save basic info from checked packet r.id = p->id; - r.sender = getFrom(p); - r.rxTimeMsec = millis(); + r.sender = getFrom(p); // If 0 then use our ID r.next_hop = p->next_hop; r.relayed_by[0] = p->relay_node; - // LOG_INFO("Add relayed_by 0x%x for id=0x%x", p->relay_node, r.id); - auto found = recentPackets.find(r); - bool seenRecently = (found != recentPackets.end()); // found not equal to .end() means packet was seen recently + r.rxTimeMsec = millis(); // + if (r.rxTimeMsec == 0) // =0 every 49.7 days? 0 is special + r.rxTimeMsec = 1; - if (seenRecently && - !Throttle::isWithinTimespanMs(found->rxTimeMsec, FLOOD_EXPIRE_TIME)) { // Check whether found packet has already expired - recentPackets.erase(found); // Erase and pretend packet has not been seen recently - found = recentPackets.end(); - seenRecently = false; - } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: @start s=%08x id=%08x / to=%08x nh=%02x rn=%02x / wUpd=%s / wasFb?%d wWNH?%d", + r.sender, r.id, p->to, p->next_hop, p->relay_node, withUpdate ? "YES" : "NO", wasFallback ? *wasFallback : -1, + weWereNextHop ? *weWereNextHop : -1); +#endif + + PacketRecord *found = find(r.sender, r.id); // Find the packet record in the recentPackets array + bool seenRecently = (found != NULL); // If found -> the packet was seen recently if (seenRecently) { - LOG_DEBUG("Found existing packet record for fr=0x%x,to=0x%x,id=0x%x", p->from, p->to, p->id); - uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); + uint8_t ourRelayID = nodeDB->getLastByteOfNodeNum(nodeDB->getNodeNum()); // Get our relay ID from our node number + if (wasFallback) { // If it was seen with a next-hop not set to us and now it's NO_NEXT_HOP_PREFERENCE, and the relayer relayed already // before, it's a fallback to flooding. If we didn't already relay and the next-hop neither, we might need to handle // it now. if (found->sender != nodeDB->getNodeNum() && found->next_hop != NO_NEXT_HOP_PREFERENCE && - found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, found) && - !wasRelayer(ourRelayID, found) && !wasRelayer(found->next_hop, found)) { + found->next_hop != ourRelayID && p->next_hop == NO_NEXT_HOP_PREFERENCE && wasRelayer(p->relay_node, *found) && + !wasRelayer(ourRelayID, *found) && + !wasRelayer( + found->next_hop, + *found)) { // If we were not the next hop and the next hop is not us, and we are not relaying this packet +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-set TRUE", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif *wasFallback = true; + } else { + // debug log only +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x oID=%02x, wasFbk=%d-no change", + p->from, p->id, p->next_hop, p->relay_node, ourRelayID, wasFallback ? *wasFallback : -1); +#endif } } // Check if we were the next hop for this packet if (weWereNextHop) { - *weWereNextHop = found->next_hop == ourRelayID; + *weWereNextHop = (found->next_hop == ourRelayID); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: f=%08x id=%08x nh=%02x rn=%02x foundnh=%02x oID=%02x -> wWNH=%s", + p->from, p->id, p->next_hop, p->relay_node, found->next_hop, ourRelayID, (*weWereNextHop) ? "YES" : "NO"); +#endif } } if (withUpdate) { - if (found != recentPackets.end()) { // delete existing to updated timestamp and relayed_by (re-insert) + if (found != NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd BEFORE", + found->sender, found->id, found->next_hop, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], + millis() - found->rxTimeMsec); +#endif + // Add the existing relayed_by to the new record - for (uint8_t i = 0; i < NUM_RELAYERS - 1; i++) { - if (found->relayed_by[i]) + for (uint8_t i = 0; i < (NUM_RELAYERS - 1); i++) { + if (found->relayed_by[i] != 0) r.relayed_by[i + 1] = found->relayed_by[i]; } r.next_hop = found->next_hop; // keep the original next_hop (such that we check whether we were originally asked) - recentPackets.erase(found); // as unsorted_set::iterator is const (can't update - so re-insert..) +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: s=%08x id=%08x nh=%02x rby=%02x %02x %02x age=%d wUpd AFTER", r.sender, + r.id, r.next_hop, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], millis() - r.rxTimeMsec); +#endif + // TODO: have direct *found entry - can modify directly without local copy _vs_ not convolute the code by this } - recentPackets.insert(r); - LOG_DEBUG("Add packet record fr=0x%x, id=0x%x", p->from, p->id); - } - - // Capacity is reerved, so only purge expired packets if recentPackets fills past 90% capacity - // Expiry is normally dealt with after having searched/found a packet (above) - if (recentPackets.size() > (MAX_NUM_NODES * 0.9)) { - clearExpiredRecentPackets(); + insert(r); // Insert or update the packet record in the history } +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - Was Seen Recently: @exit s=%08x id=%08x (to=%08x) relby=%02x %02x %02x nxthop=%02x rxT=%d " + "found?%s seenRecently?%s wUpd?%s", + r.sender, r.id, p->to, r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], r.next_hop, r.rxTimeMsec, + found ? "YES" : "NO ", seenRecently ? "YES" : "NO ", withUpdate ? "YES" : "NO "); +#endif return seenRecently; } -/** - * Iterate through all recent packets, and remove all older than FLOOD_EXPIRE_TIME - */ -void PacketHistory::clearExpiredRecentPackets() +/** Find a packet record in history. + * @return pointer to PacketRecord if found, NULL if not found */ +PacketHistory::PacketRecord *PacketHistory::find(NodeNum sender, PacketId id) { - LOG_DEBUG("recentPackets size=%ld", recentPackets.size()); + if (sender == 0 || id == 0) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x sender/id=0->NOT FOUND", sender, id); +#endif + return NULL; + } - for (auto it = recentPackets.begin(); it != recentPackets.end();) { - if (!Throttle::isWithinTimespanMs(it->rxTimeMsec, FLOOD_EXPIRE_TIME)) { - it = recentPackets.erase(it); // erase returns iterator pointing to element immediately following the one erased - } else { - ++it; + PacketRecord *it = NULL; + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == id && it->sender == sender) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x FOUND nh=%02x rby=%02x %02x %02x age=%d slot=%d/%d", it->sender, + it->id, it->next_hop, it->relayed_by[0], it->relayed_by[1], it->relayed_by[2], millis() - (it->rxTimeMsec), + it - recentPackets, recentPacketsCapacity); +#endif + // only the first match is returned, so be careful not to create duplicate entries + return it; // Return pointer to the found record } } - LOG_DEBUG("recentPackets size=%ld (after clearing expired packets)", recentPackets.size()); +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - find: s=%08x id=%08x NOT FOUND", sender, id); +#endif + return NULL; // Not found +} + +/** Insert/Replace oldest PacketRecord in recentPackets. */ +void PacketHistory::insert(PacketRecord &r) +{ + uint32_t now_millis = millis(); // Should not jump with time changes + uint32_t OldtrxTimeMsec = 0; + PacketRecord *tu = NULL; // Will insert here. + PacketRecord *it = NULL; + + // Find a free, matching or oldest used slot in the recentPackets array + for (it = recentPackets; it < (recentPackets + recentPacketsCapacity); ++it) { + if (it->id == 0 && it->sender == 0 /*&& rxTimeMsec == 0*/) { // Record is empty + tu = it; // Remember the free slot +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Free slot@ %d/%d", tu - recentPackets, recentPacketsCapacity); +#endif + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else if (it->id == r.id && it->sender == r.sender) { // Record matches the packet we want to insert + tu = it; // Remember the matching slot + OldtrxTimeMsec = now_millis - it->rxTimeMsec; // ..and save current entry's age +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Matched slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); +#endif + // We have that, Exit the loop + it = (recentPackets + recentPacketsCapacity); + } else { + if (it->rxTimeMsec == 0) { + LOG_WARN( + "Packet History - insert: Found packet s=%08x id=%08x with rxTimeMsec = 0, slot %d/%d. Should never happen!", + it->sender, it->id, it - recentPackets, recentPacketsCapacity); + } + if ((now_millis - it->rxTimeMsec) > OldtrxTimeMsec) { // 49.7 days rollover friendly + OldtrxTimeMsec = now_millis - it->rxTimeMsec; + tu = it; // remember the oldest packet +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - insert: Older slot@ %d/%d age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); +#endif + } + // keep looking for oldest till entire array is checked + } + } + + if (tu == NULL) { + LOG_ERROR("Packet History - insert: No free slot, no matched packet, no oldest to reuse. Something leaked."); // mx + // assert(false); // This should never happen, we should always have at least one packet to clear + return; // Return early if we can't update the history + } + +#if VERBOSE_PACKET_HISTORY + if (tu->id == 0 && tu->sender == 0) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d is NEW", tu - recentPackets, recentPacketsCapacity); + } else if (tu->id == r.id && tu->sender == r.sender) { + LOG_DEBUG("Packet History - insert: slot@ %d/%d MATCHED, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } else { + LOG_DEBUG("Packet History - insert: slot@ %d/%d REUSE OLDEST, age=%d", tu - recentPackets, recentPacketsCapacity, + OldtrxTimeMsec); + } +#endif + + // If we are reusing a slot, we should warn if the packet is too recent +#if RECENT_WARN_AGE > 0 + if (tu->rxTimeMsec && (OldtrxTimeMsec < RECENT_WARN_AGE)) { + if (!(tu->id == r.id && tu->sender == r.sender)) { + LOG_WARN("Packet History - insert: Reusing slot aged %ds < %ds RECENT_WARN_AGE", OldtrxTimeMsec / 1000, + RECENT_WARN_AGE / 1000); + } else { + // debug only +#if VERBOSE_PACKET_HISTORY + LOG_WARN("Packet History - insert: Reusing slot aged %.3fs < %ds with MATCHED PACKET - this is normal", + OldtrxTimeMsec / 1000., RECENT_WARN_AGE / 1000); +#endif + } + } + +#if PACKET_HISTORY_TRACE_AGING + if (tu->rxTimeMsec != 0) { + LOG_INFO("Packet History - insert: Reusing slot aged %.3fs TRACE %s", OldtrxTimeMsec / 1000., + (tu->id == r.id && tu->sender == r.sender) ? "MATCHED PACKET" : "OLDEST SLOT"); + } else { + LOG_INFO("Packet History - insert: Using new slot @uptime %.3fs TRACE NEW", millis() / 1000.); + } +#endif + +#endif + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d BEFORE", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); +#endif + + if (r.rxTimeMsec == 0) { + LOG_WARN("Packet History - insert: I will not store packet with rxTimeMsec = 0."); + return; // Return early if we can't update the history + } + + *tu = r; // store the packet + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - insert: Store slot@ %d/%d s=%08x id=%08x nh=%02x rby=%02x %02x %02x rxT=%d AFTER", + tu - recentPackets, recentPacketsCapacity, tu->sender, tu->id, tu->next_hop, tu->relayed_by[0], tu->relayed_by[1], + tu->relayed_by[2], tu->rxTimeMsec); +#endif } /* 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 */ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { - if (relayer == 0) - return false; - - PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; - auto found = recentPackets.find(r); - - if (found == recentPackets.end()) { + if (!initOk()) { + LOG_ERROR("PacketHistory - wasRelayer: NOT INITIALIZED!"); return false; } - return wasRelayer(relayer, found); + if (relayer == 0) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x=zero. NO", sender, id, relayer); +#endif + return false; + } + + PacketRecord *found = find(sender, id); + + if (found == NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x / rl=%02x / PR not found. NO", sender, id, relayer); +#endif + return false; + } + +#if VERBOSE_PACKET_HISTORY >= 2 + LOG_DEBUG("Packet History - was relayer: s=%08x id=%08x nh=%02x age=%d rls=%02x %02x %02x InHistory,check:%02x", + found->sender, found->id, found->next_hop, millis() - found->rxTimeMsec, found->relayed_by[0], found->relayed_by[1], + found->relayed_by[2], relayer); +#endif + return wasRelayer(relayer, *found); } /* 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 */ -bool PacketHistory::wasRelayer(const uint8_t relayer, std::unordered_set::iterator r) +bool PacketHistory::wasRelayer(const uint8_t relayer, PacketRecord &r) { for (uint8_t i = 0; i < NUM_RELAYERS; i++) { - if (r->relayed_by[i] == relayer) { + if (r.relayed_by[i] == relayer) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - was rel.PR.: s=%08x id=%08x rls=%02x %02x %02x / rl=%02x? YES", r.sender, r.id, + r.relayed_by[0], r.relayed_by[1], r.relayed_by[2], relayer); +#endif return true; } } +#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], + r.relayed_by[1], r.relayed_by[2], relayer); +#endif return false; } // Remove a relayer from the list of relayers of a packet in the history given an ID and sender void PacketHistory::removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender) { - PacketRecord r = {.sender = sender, .id = id, .rxTimeMsec = 0, .next_hop = 0}; - auto found = recentPackets.find(r); - - if (found == recentPackets.end()) { + if (!initOk()) { + LOG_ERROR("Packet History - remove Relayer: NOT INITIALIZED!"); return; } - // Make a copy of the found record - r.next_hop = found->next_hop; - r.rxTimeMsec = found->rxTimeMsec; - // Only add the relayers that are not the one we want to remove - uint8_t j = 0; - for (uint8_t i = 0; i < NUM_RELAYERS; i++) { - if (found->relayed_by[i] != relayer) { - r.relayed_by[j] = found->relayed_by[i]; - j++; - } + PacketRecord *found = find(sender, id); + if (found == NULL) { +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x (rl=%02x) NOT FOUND", sender, id, relayer); +#endif + return; // Nothing to remove } - recentPackets.erase(found); - recentPackets.insert(r); -} \ No newline at end of file +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x, rl:%02x BEFORE", found->sender, found->id, + found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer); +#endif + + // nexthop and rxTimeMsec too stay in found entry + + uint8_t j = 0; + uint8_t i = 0; + for (; i < NUM_RELAYERS; i++) { + if (found->relayed_by[i] != relayer) { + found->relayed_by[j] = found->relayed_by[i]; + j++; + } else + found->relayed_by[i] = 0; + } + for (; j < NUM_RELAYERS; j++) { // Clear the rest of the relayed_by array + found->relayed_by[j] = 0; + } + +#if VERBOSE_PACKET_HISTORY + LOG_DEBUG("Packet History - remove Relayer s=%08x id=%08x rby=%02x %02x %02x rl:%02x AFTER - removed?%d", found->sender, + found->id, found->relayed_by[0], found->relayed_by[1], found->relayed_by[2], relayer, i != j); +#endif +} diff --git a/src/mesh/PacketHistory.h b/src/mesh/PacketHistory.h index db7698f5b..d06c9bd2f 100644 --- a/src/mesh/PacketHistory.h +++ b/src/mesh/PacketHistory.h @@ -1,49 +1,47 @@ #pragma once #include "NodeDB.h" -#include - -/// We clear our old flood record 10 minutes after we see the last of it -#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -#define FLOOD_EXPIRE_TIME (5 * 1000L) // Don't allow too many packets to accumulate when fuzzing. -#else -#define FLOOD_EXPIRE_TIME (10 * 60 * 1000L) -#endif #define NUM_RELAYERS \ 3 // Number of relayer we keep track of. Use 3 to be efficient with memory alignment of PacketRecord to 16 bytes -/** - * A record of a recent message broadcast - */ -struct PacketRecord { - NodeNum sender; - PacketId id; - uint32_t rxTimeMsec; // Unix time in msecs - the time we received it - uint8_t next_hop; // The next hop asked for this packet - uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet - - bool operator==(const PacketRecord &p) const { return sender == p.sender && id == p.id; } -}; - -class PacketRecordHashFunction -{ - public: - size_t operator()(const PacketRecord &p) const { return (std::hash()(p.sender)) ^ (std::hash()(p.id)); } -}; - /** * This is a mixin that adds a record of past packets we have seen */ class PacketHistory { private: - std::unordered_set recentPackets; + struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class. + NodeNum sender; + PacketId id; + uint32_t rxTimeMsec; // Unix time in msecs - the time we received it, 0 means empty + uint8_t next_hop; // The next hop asked for this packet + uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet + }; // 4B + 4B + 4B + 1B + 3B = 16B - void clearExpiredRecentPackets(); // clear all recentPackets older than FLOOD_EXPIRE_TIME + uint32_t recentPacketsCapacity = + 0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets. + PacketRecord *recentPackets = NULL; // Simple and fixed in size. Debloat. + /** Find a packet record in history. + * @param sender NodeNum + * @param id PacketId + * @return pointer to PacketRecord if found, NULL if not found */ + PacketRecord *find(NodeNum sender, PacketId id); + + /** Insert/Replace oldest PacketRecord in mx_recentPackets. + * @param r PacketRecord to insert or replace */ + void insert(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 + * @return true if node was indeed a relayer, false if not */ + bool wasRelayer(const uint8_t relayer, PacketRecord &r); + + PacketHistory(const PacketHistory &); // non construction-copyable + PacketHistory &operator=(const PacketHistory &); // non copyable public: - PacketHistory(); + explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX + ~PacketHistory(); /** * Update recentBroadcasts and return true if we have already seen this packet @@ -59,10 +57,9 @@ class PacketHistory * @return true if node was indeed a relayer, false if not */ bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender); - /* 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 */ - bool wasRelayer(const uint8_t relayer, std::unordered_set::iterator r); - // 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); -}; \ No newline at end of file + + // To check if the PacketHistory was initialized correctly by constructor + bool initOk(void) { return recentPackets != NULL && recentPacketsCapacity != 0; } +}; diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index fcc76013c..d72befbf1 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -122,11 +122,11 @@ bool RF95Interface::init() power = dacDbValues.db; #endif + limitPower(); + if (power > RF95_MAX_POWER) // This chip has lower power limits than some power = RF95_MAX_POWER; - limitPower(); - iface = lora = new RadioLibRF95(&module); #ifdef RF95_TCXO diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 86903153b..f7cd6f4c1 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -64,6 +64,13 @@ const RegionInfo regions[] = { */ RDEF(ANZ, 915.0f, 928.0f, 100, 0, 30, true, false, false), + /* + 433.05 - 434.79 MHz, 25mW EIRP max, No duty cycle restrictions + AU Low Interference Potential https://www.acma.gov.au/licences/low-interference-potential-devices-lipd-class-licence + NZ General User Radio Licence for Short Range Devices https://gazette.govt.nz/notice/id/2022-go3100 + */ + RDEF(ANZ_433, 433.05f, 434.79f, 100, 0, 14, true, false, false), + /* https://digital.gov.ru/uploaded/files/prilozhenie-12-k-reshenyu-gkrch-18-46-03-1.pdf @@ -528,8 +535,8 @@ void RadioInterface::applyModemConfig() power = loraConfig.tx_power; - if ((power == 0) || ((power + REGULATORY_GAIN_LORA > myRegion->powerLimit) && !devicestate.owner.is_licensed)) - power = myRegion->powerLimit - REGULATORY_GAIN_LORA; + if ((power == 0) || ((power > myRegion->powerLimit) && !devicestate.owner.is_licensed)) + power = myRegion->powerLimit; if (power == 0) power = 17; // Default to this power level if we don't have a valid regional power limit (powerLimit of myRegion defaults @@ -616,7 +623,12 @@ void RadioInterface::limitPower() power = maxPower; } - LOG_INFO("Set radio: final power level=%d", power); + if (TX_GAIN_LORA > 0) { + LOG_INFO("Requested Tx power: %d dBm; Device LoRa Tx gain: %d dB", power, TX_GAIN_LORA); + power -= TX_GAIN_LORA; + } + + LOG_INFO("Final Tx power: %d dBm", power); } void RadioInterface::deliverToReceiver(meshtastic_MeshPacket *p) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index fef29388e..02968513c 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -548,6 +548,20 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) numbytes += MESHTASTIC_PKC_OVERHEAD; p->channel = 0; p->pki_encrypted = true; + + // warn the user about a low entropy key + if (nodeDB->keyIsLowEntropy) { + LOG_WARN(LOW_ENTROPY_WARNING); + if (!nodeDB->hasWarned) { + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->which_payload_variant = meshtastic_ClientNotification_low_entropy_key_tag; + cn->level = meshtastic_LogRecord_Level_WARNING; + cn->time = getValidTime(RTCQualityFromNet); + sprintf(cn->message, LOW_ENTROPY_WARNING); + service->sendClientNotification(cn); + nodeDB->hasWarned = true; + } + } } else { if (p->pki_encrypted == true) { // Client specifically requested PKI encryption diff --git a/src/mesh/STM32WLE5JCInterface.cpp b/src/mesh/STM32WLE5JCInterface.cpp index 6a340dd28..3c8bf89c3 100644 --- a/src/mesh/STM32WLE5JCInterface.cpp +++ b/src/mesh/STM32WLE5JCInterface.cpp @@ -25,11 +25,11 @@ bool STM32WLE5JCInterface::init() lora.setRfSwitchTable(rfswitch_pins, rfswitch_table); + limitPower(); + if (power > STM32WLx_MAX_POWER) // This chip has lower power limits than some power = STM32WLx_MAX_POWER; - limitPower(); - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage); LOG_INFO("STM32WLx init result %d", res); diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index c867466b7..e5ecd9302 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -69,11 +69,11 @@ template bool SX126xInterface::init() RadioLibInterface::init(); + limitPower(); + if (power > SX126X_MAX_POWER) // Clamp power to maximum defined level power = SX126X_MAX_POWER; - limitPower(); - int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength, tcxoVoltage, useRegulatorLDO); // \todo Display actual typename of the adapter, not just `SX126x` LOG_INFO("SX126x init result %d", res); diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 23a023d3f..2b17543fc 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -62,11 +62,11 @@ template bool SX128xInterface::init() RadioLibInterface::init(); + limitPower(); + if (power > SX128X_MAX_POWER) // This chip has lower power limits than some power = SX128X_MAX_POWER; - limitPower(); - preambleLength = 12; // 12 is the default for this chip, 32 does not RX at all int res = lora.begin(getFreq(), bw, sf, cr, syncWord, power, preambleLength); diff --git a/src/mesh/TypeConversions.cpp b/src/mesh/TypeConversions.cpp index c47a67e68..17cd92851 100644 --- a/src/mesh/TypeConversions.cpp +++ b/src/mesh/TypeConversions.cpp @@ -87,6 +87,8 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user) memcpy(lite.macaddr, user.macaddr, sizeof(lite.macaddr)); memcpy(lite.public_key.bytes, user.public_key.bytes, sizeof(lite.public_key.bytes)); lite.public_key.size = user.public_key.size; + lite.has_is_unmessagable = user.has_is_unmessagable; + lite.is_unmessagable = user.is_unmessagable; return lite; } @@ -103,6 +105,8 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User memcpy(user.macaddr, lite.macaddr, sizeof(user.macaddr)); memcpy(user.public_key.bytes, lite.public_key.bytes, sizeof(user.public_key.bytes)); user.public_key.size = lite.public_key.size; + user.has_is_unmessagable = lite.has_is_unmessagable; + user.is_unmessagable = lite.is_unmessagable; return user; } \ No newline at end of file diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 70c6e3fe4..9c92a6c27 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -64,7 +64,9 @@ static int32_t reconnectETH() } #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif ethStartupComplete = true; diff --git a/src/mesh/generated/meshtastic/admin.pb.cpp b/src/mesh/generated/meshtastic/admin.pb.cpp index 9bf40870f..4c4d0e3d1 100644 --- a/src/mesh/generated/meshtastic/admin.pb.cpp +++ b/src/mesh/generated/meshtastic/admin.pb.cpp @@ -9,6 +9,9 @@ PB_BIND(meshtastic_AdminMessage, meshtastic_AdminMessage, 2) +PB_BIND(meshtastic_AdminMessage_InputEvent, meshtastic_AdminMessage_InputEvent, AUTO) + + PB_BIND(meshtastic_HamParameters, meshtastic_HamParameters, AUTO) @@ -18,6 +21,11 @@ PB_BIND(meshtastic_NodeRemoteHardwarePinsResponse, meshtastic_NodeRemoteHardware PB_BIND(meshtastic_SharedContact, meshtastic_SharedContact, AUTO) +PB_BIND(meshtastic_KeyVerificationAdmin, meshtastic_KeyVerificationAdmin, AUTO) + + + + diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index 0a46e6275..071640b0d 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -77,7 +77,32 @@ typedef enum _meshtastic_AdminMessage_BackupLocation { meshtastic_AdminMessage_BackupLocation_SD = 1 } meshtastic_AdminMessage_BackupLocation; +/* Three stages of this request. */ +typedef enum _meshtastic_KeyVerificationAdmin_MessageType { + /* This is the first stage, where a client initiates */ + meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION = 0, + /* After the nonce has been returned over the mesh, the client prompts for the security number + And uses this message to provide it to the node. */ + meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER = 1, + /* Once the user has compared the verification message, this message notifies the node. */ + meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY = 2, + /* This is the cancel path, can be taken at any point */ + meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY = 3 +} meshtastic_KeyVerificationAdmin_MessageType; + /* Struct definitions */ +/* Input event message to be sent to the node. */ +typedef struct _meshtastic_AdminMessage_InputEvent { + /* The input event code */ + uint8_t event_code; + /* Keyboard character code */ + uint8_t kb_char; + /* The touch X coordinate */ + uint16_t touch_x; + /* The touch Y coordinate */ + uint16_t touch_y; +} meshtastic_AdminMessage_InputEvent; + /* Parameters for setting up Meshtastic for ameteur radio usage */ typedef struct _meshtastic_HamParameters { /* Amateur radio call sign, eg. KD2ABC */ @@ -105,8 +130,22 @@ typedef struct _meshtastic_SharedContact { /* The User of the contact */ bool has_user; meshtastic_User user; + /* Add this contact to the blocked / ignored list */ + bool should_ignore; } meshtastic_SharedContact; +/* This message is used by a client to initiate or complete a key verification */ +typedef struct _meshtastic_KeyVerificationAdmin { + meshtastic_KeyVerificationAdmin_MessageType message_type; + /* The nodenum we're requesting */ + uint32_t remote_nodenum; + /* The nonce is used to track the connection */ + uint64_t nonce; + /* The 4 digit code generated by the remote node, and communicated outside the mesh */ + bool has_security_number; + uint32_t security_number; +} meshtastic_KeyVerificationAdmin; + typedef PB_BYTES_ARRAY_T(8) meshtastic_AdminMessage_session_passkey_t; /* This message is handled by the Admin module and is responsible for all settings/channel read/write operations. This message is used to do settings operations to both remote AND local nodes. @@ -166,6 +205,9 @@ typedef struct _meshtastic_AdminMessage { meshtastic_AdminMessage_BackupLocation restore_preferences; /* Remove backups of the node's preferences */ meshtastic_AdminMessage_BackupLocation remove_backup_preferences; + /* Send an input event to the node. + This is used to trigger physical input events like button presses, touch events, etc. */ + meshtastic_AdminMessage_InputEvent send_input_event; /* Set the owner for this node */ meshtastic_User set_owner; /* Set channels (using the new API). @@ -212,6 +254,8 @@ typedef struct _meshtastic_AdminMessage { bool commit_edit_settings; /* Add a contact (User) to the nodedb */ meshtastic_SharedContact add_contact; + /* Initiate or respond to a key verification request */ + meshtastic_KeyVerificationAdmin key_verification; /* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. */ int32_t factory_reset_device; /* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) @@ -253,6 +297,10 @@ extern "C" { #define _meshtastic_AdminMessage_BackupLocation_MAX meshtastic_AdminMessage_BackupLocation_SD #define _meshtastic_AdminMessage_BackupLocation_ARRAYSIZE ((meshtastic_AdminMessage_BackupLocation)(meshtastic_AdminMessage_BackupLocation_SD+1)) +#define _meshtastic_KeyVerificationAdmin_MessageType_MIN meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION +#define _meshtastic_KeyVerificationAdmin_MessageType_MAX meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY +#define _meshtastic_KeyVerificationAdmin_MessageType_ARRAYSIZE ((meshtastic_KeyVerificationAdmin_MessageType)(meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY+1)) + #define meshtastic_AdminMessage_payload_variant_get_config_request_ENUMTYPE meshtastic_AdminMessage_ConfigType #define meshtastic_AdminMessage_payload_variant_get_module_config_request_ENUMTYPE meshtastic_AdminMessage_ModuleConfigType #define meshtastic_AdminMessage_payload_variant_backup_preferences_ENUMTYPE meshtastic_AdminMessage_BackupLocation @@ -263,17 +311,28 @@ extern "C" { +#define meshtastic_KeyVerificationAdmin_message_type_ENUMTYPE meshtastic_KeyVerificationAdmin_MessageType + + /* Initializer values for message structs */ #define meshtastic_AdminMessage_init_default {0, {0}, {0, {0}}} +#define meshtastic_AdminMessage_InputEvent_init_default {0, 0, 0, 0} #define meshtastic_HamParameters_init_default {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_default {0, {meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default, meshtastic_NodeRemoteHardwarePin_init_default}} -#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default} +#define meshtastic_SharedContact_init_default {0, false, meshtastic_User_init_default, 0} +#define meshtastic_KeyVerificationAdmin_init_default {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} #define meshtastic_AdminMessage_init_zero {0, {0}, {0, {0}}} +#define meshtastic_AdminMessage_InputEvent_init_zero {0, 0, 0, 0} #define meshtastic_HamParameters_init_zero {"", 0, 0, ""} #define meshtastic_NodeRemoteHardwarePinsResponse_init_zero {0, {meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero, meshtastic_NodeRemoteHardwarePin_init_zero}} -#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero} +#define meshtastic_SharedContact_init_zero {0, false, meshtastic_User_init_zero, 0} +#define meshtastic_KeyVerificationAdmin_init_zero {_meshtastic_KeyVerificationAdmin_MessageType_MIN, 0, 0, false, 0} /* Field tags (for use in manual encoding/decoding) */ +#define meshtastic_AdminMessage_InputEvent_event_code_tag 1 +#define meshtastic_AdminMessage_InputEvent_kb_char_tag 2 +#define meshtastic_AdminMessage_InputEvent_touch_x_tag 3 +#define meshtastic_AdminMessage_InputEvent_touch_y_tag 4 #define meshtastic_HamParameters_call_sign_tag 1 #define meshtastic_HamParameters_tx_power_tag 2 #define meshtastic_HamParameters_frequency_tag 3 @@ -281,6 +340,11 @@ extern "C" { #define meshtastic_NodeRemoteHardwarePinsResponse_node_remote_hardware_pins_tag 1 #define meshtastic_SharedContact_node_num_tag 1 #define meshtastic_SharedContact_user_tag 2 +#define meshtastic_SharedContact_should_ignore_tag 3 +#define meshtastic_KeyVerificationAdmin_message_type_tag 1 +#define meshtastic_KeyVerificationAdmin_remote_nodenum_tag 2 +#define meshtastic_KeyVerificationAdmin_nonce_tag 3 +#define meshtastic_KeyVerificationAdmin_security_number_tag 4 #define meshtastic_AdminMessage_get_channel_request_tag 1 #define meshtastic_AdminMessage_get_channel_response_tag 2 #define meshtastic_AdminMessage_get_owner_request_tag 3 @@ -306,6 +370,7 @@ extern "C" { #define meshtastic_AdminMessage_backup_preferences_tag 24 #define meshtastic_AdminMessage_restore_preferences_tag 25 #define meshtastic_AdminMessage_remove_backup_preferences_tag 26 +#define meshtastic_AdminMessage_send_input_event_tag 27 #define meshtastic_AdminMessage_set_owner_tag 32 #define meshtastic_AdminMessage_set_channel_tag 33 #define meshtastic_AdminMessage_set_config_tag 34 @@ -326,6 +391,7 @@ extern "C" { #define meshtastic_AdminMessage_begin_edit_settings_tag 64 #define meshtastic_AdminMessage_commit_edit_settings_tag 65 #define meshtastic_AdminMessage_add_contact_tag 66 +#define meshtastic_AdminMessage_key_verification_tag 67 #define meshtastic_AdminMessage_factory_reset_device_tag 94 #define meshtastic_AdminMessage_reboot_ota_seconds_tag 95 #define meshtastic_AdminMessage_exit_simulator_tag 96 @@ -362,6 +428,7 @@ X(a, STATIC, ONEOF, UINT32, (payload_variant,set_scale,set_scale), 23) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,backup_preferences,backup_preferences), 24) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,restore_preferences,restore_preferences), 25) \ X(a, STATIC, ONEOF, UENUM, (payload_variant,remove_backup_preferences,remove_backup_preferences), 26) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,send_input_event,send_input_event), 27) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_owner,set_owner), 32) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_channel,set_channel), 33) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,set_config,set_config), 34) \ @@ -382,6 +449,7 @@ X(a, STATIC, ONEOF, UINT32, (payload_variant,remove_ignored_node,remove_i X(a, STATIC, ONEOF, BOOL, (payload_variant,begin_edit_settings,begin_edit_settings), 64) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,commit_edit_settings,commit_edit_settings), 65) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,add_contact,add_contact), 66) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification,key_verification), 67) \ X(a, STATIC, ONEOF, INT32, (payload_variant,factory_reset_device,factory_reset_device), 94) \ X(a, STATIC, ONEOF, INT32, (payload_variant,reboot_ota_seconds,reboot_ota_seconds), 95) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,exit_simulator,exit_simulator), 96) \ @@ -400,6 +468,7 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_payload_variant_get_device_connection_status_response_MSGTYPE meshtastic_DeviceConnectionStatus #define meshtastic_AdminMessage_payload_variant_set_ham_mode_MSGTYPE meshtastic_HamParameters #define meshtastic_AdminMessage_payload_variant_get_node_remote_hardware_pins_response_MSGTYPE meshtastic_NodeRemoteHardwarePinsResponse +#define meshtastic_AdminMessage_payload_variant_send_input_event_MSGTYPE meshtastic_AdminMessage_InputEvent #define meshtastic_AdminMessage_payload_variant_set_owner_MSGTYPE meshtastic_User #define meshtastic_AdminMessage_payload_variant_set_channel_MSGTYPE meshtastic_Channel #define meshtastic_AdminMessage_payload_variant_set_config_MSGTYPE meshtastic_Config @@ -408,6 +477,15 @@ X(a, STATIC, SINGULAR, BYTES, session_passkey, 101) #define meshtastic_AdminMessage_payload_variant_get_ui_config_response_MSGTYPE meshtastic_DeviceUIConfig #define meshtastic_AdminMessage_payload_variant_store_ui_config_MSGTYPE meshtastic_DeviceUIConfig #define meshtastic_AdminMessage_payload_variant_add_contact_MSGTYPE meshtastic_SharedContact +#define meshtastic_AdminMessage_payload_variant_key_verification_MSGTYPE meshtastic_KeyVerificationAdmin + +#define meshtastic_AdminMessage_InputEvent_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT32, event_code, 1) \ +X(a, STATIC, SINGULAR, UINT32, kb_char, 2) \ +X(a, STATIC, SINGULAR, UINT32, touch_x, 3) \ +X(a, STATIC, SINGULAR, UINT32, touch_y, 4) +#define meshtastic_AdminMessage_InputEvent_CALLBACK NULL +#define meshtastic_AdminMessage_InputEvent_DEFAULT NULL #define meshtastic_HamParameters_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, call_sign, 1) \ @@ -425,28 +503,43 @@ X(a, STATIC, REPEATED, MESSAGE, node_remote_hardware_pins, 1) #define meshtastic_SharedContact_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, node_num, 1) \ -X(a, STATIC, OPTIONAL, MESSAGE, user, 2) +X(a, STATIC, OPTIONAL, MESSAGE, user, 2) \ +X(a, STATIC, SINGULAR, BOOL, should_ignore, 3) #define meshtastic_SharedContact_CALLBACK NULL #define meshtastic_SharedContact_DEFAULT NULL #define meshtastic_SharedContact_user_MSGTYPE meshtastic_User +#define meshtastic_KeyVerificationAdmin_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, message_type, 1) \ +X(a, STATIC, SINGULAR, UINT32, remote_nodenum, 2) \ +X(a, STATIC, SINGULAR, UINT64, nonce, 3) \ +X(a, STATIC, OPTIONAL, UINT32, security_number, 4) +#define meshtastic_KeyVerificationAdmin_CALLBACK NULL +#define meshtastic_KeyVerificationAdmin_DEFAULT NULL + extern const pb_msgdesc_t meshtastic_AdminMessage_msg; +extern const pb_msgdesc_t meshtastic_AdminMessage_InputEvent_msg; extern const pb_msgdesc_t meshtastic_HamParameters_msg; extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; extern const pb_msgdesc_t meshtastic_SharedContact_msg; +extern const pb_msgdesc_t meshtastic_KeyVerificationAdmin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define meshtastic_AdminMessage_fields &meshtastic_AdminMessage_msg +#define meshtastic_AdminMessage_InputEvent_fields &meshtastic_AdminMessage_InputEvent_msg #define meshtastic_HamParameters_fields &meshtastic_HamParameters_msg #define meshtastic_NodeRemoteHardwarePinsResponse_fields &meshtastic_NodeRemoteHardwarePinsResponse_msg #define meshtastic_SharedContact_fields &meshtastic_SharedContact_msg +#define meshtastic_KeyVerificationAdmin_fields &meshtastic_KeyVerificationAdmin_msg /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size +#define meshtastic_AdminMessage_InputEvent_size 14 #define meshtastic_AdminMessage_size 511 #define meshtastic_HamParameters_size 31 +#define meshtastic_KeyVerificationAdmin_size 25 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 -#define meshtastic_SharedContact_size 123 +#define meshtastic_SharedContact_size 125 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/config.pb.cpp b/src/mesh/generated/meshtastic/config.pb.cpp index 5512584a7..52a591f33 100644 --- a/src/mesh/generated/meshtastic/config.pb.cpp +++ b/src/mesh/generated/meshtastic/config.pb.cpp @@ -65,6 +65,8 @@ PB_BIND(meshtastic_Config_SessionkeyConfig, meshtastic_Config_SessionkeyConfig, + + diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index edcd7b41c..ed1849be8 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -88,6 +88,23 @@ typedef enum _meshtastic_Config_DeviceConfig_RebroadcastMode { meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY = 5 } meshtastic_Config_DeviceConfig_RebroadcastMode; +/* Defines buzzer behavior for audio feedback */ +typedef enum _meshtastic_Config_DeviceConfig_BuzzerMode { + /* Default behavior. + Buzzer is enabled for all audio feedback including button presses and alerts. */ + meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED = 0, + /* Disabled. + All buzzer audio feedback is disabled. */ + meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED = 1, + /* Notifications Only. + Buzzer is enabled only for notifications and alerts, but not for button presses. + External notification config determines the specifics of the notification behavior. */ + meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY = 2, + /* Non-notification system buzzer tones only. + Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. */ + meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY = 3 +} meshtastic_Config_DeviceConfig_BuzzerMode; + /* Bit field of boolean configuration options, indicating which optional fields to include when assembling POSITION messages. Longitude, latitude, altitude, speed, heading, and DOP @@ -266,7 +283,9 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode { /* Philippines 868mhz */ meshtastic_Config_LoRaConfig_RegionCode_PH_868 = 20, /* Philippines 915mhz */ - meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21 + meshtastic_Config_LoRaConfig_RegionCode_PH_915 = 21, + /* Australia / New Zealand 433MHz */ + meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 = 22 } meshtastic_Config_LoRaConfig_RegionCode; /* Standard predefined channel settings @@ -335,6 +354,9 @@ typedef struct _meshtastic_Config_DeviceConfig { char tzdef[65]; /* If true, disable the default blinking LED (LED_PIN) behavior on the device */ bool led_heartbeat_disabled; + /* Controls buzzer behavior for audio feedback + Defaults to ENABLED */ + meshtastic_Config_DeviceConfig_BuzzerMode buzzer_mode; } meshtastic_Config_DeviceConfig; /* Position Config */ @@ -441,6 +463,8 @@ typedef struct _meshtastic_Config_NetworkConfig { char rsyslog_server[33]; /* Flags for enabling/disabling network protocols */ uint32_t enabled_protocols; + /* Enable/Disable ipv6 support */ + bool ipv6_enabled; } meshtastic_Config_NetworkConfig; /* Display Config */ @@ -616,6 +640,10 @@ extern "C" { #define _meshtastic_Config_DeviceConfig_RebroadcastMode_MAX meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY #define _meshtastic_Config_DeviceConfig_RebroadcastMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_RebroadcastMode)(meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY+1)) +#define _meshtastic_Config_DeviceConfig_BuzzerMode_MIN meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED +#define _meshtastic_Config_DeviceConfig_BuzzerMode_MAX meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY +#define _meshtastic_Config_DeviceConfig_BuzzerMode_ARRAYSIZE ((meshtastic_Config_DeviceConfig_BuzzerMode)(meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY+1)) + #define _meshtastic_Config_PositionConfig_PositionFlags_MIN meshtastic_Config_PositionConfig_PositionFlags_UNSET #define _meshtastic_Config_PositionConfig_PositionFlags_MAX meshtastic_Config_PositionConfig_PositionFlags_SPEED #define _meshtastic_Config_PositionConfig_PositionFlags_ARRAYSIZE ((meshtastic_Config_PositionConfig_PositionFlags)(meshtastic_Config_PositionConfig_PositionFlags_SPEED+1)) @@ -653,8 +681,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1)) #define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET -#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_PH_915 -#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_PH_915+1)) +#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ANZ_433 +#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ANZ_433+1)) #define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST #define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO @@ -667,6 +695,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_role_ENUMTYPE meshtastic_Config_DeviceConfig_Role #define meshtastic_Config_DeviceConfig_rebroadcast_mode_ENUMTYPE meshtastic_Config_DeviceConfig_RebroadcastMode +#define meshtastic_Config_DeviceConfig_buzzer_mode_ENUMTYPE meshtastic_Config_DeviceConfig_BuzzerMode #define meshtastic_Config_PositionConfig_gps_mode_ENUMTYPE meshtastic_Config_PositionConfig_GpsMode @@ -690,10 +719,10 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_Config_init_default {0, {meshtastic_Config_DeviceConfig_init_default}} -#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_DeviceConfig_init_default {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0} -#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0} +#define meshtastic_Config_NetworkConfig_init_default {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_default, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_default {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_default {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} #define meshtastic_Config_LoRaConfig_init_default {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} @@ -701,10 +730,10 @@ extern "C" { #define meshtastic_Config_SecurityConfig_init_default {{0, {0}}, {0, {0}}, 0, {{0, {0}}, {0, {0}}, {0, {0}}}, 0, 0, 0, 0} #define meshtastic_Config_SessionkeyConfig_init_default {0} #define meshtastic_Config_init_zero {0, {meshtastic_Config_DeviceConfig_init_zero}} -#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0} +#define meshtastic_Config_DeviceConfig_init_zero {_meshtastic_Config_DeviceConfig_Role_MIN, 0, 0, 0, _meshtastic_Config_DeviceConfig_RebroadcastMode_MIN, 0, 0, 0, 0, "", 0, _meshtastic_Config_DeviceConfig_BuzzerMode_MIN} #define meshtastic_Config_PositionConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, _meshtastic_Config_PositionConfig_GpsMode_MIN} #define meshtastic_Config_PowerConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0} -#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0} +#define meshtastic_Config_NetworkConfig_init_zero {0, "", "", "", 0, _meshtastic_Config_NetworkConfig_AddressMode_MIN, false, meshtastic_Config_NetworkConfig_IpV4Config_init_zero, "", 0, 0} #define meshtastic_Config_NetworkConfig_IpV4Config_init_zero {0, 0, 0, 0} #define meshtastic_Config_DisplayConfig_init_zero {0, _meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MIN, 0, 0, 0, _meshtastic_Config_DisplayConfig_DisplayUnits_MIN, _meshtastic_Config_DisplayConfig_OledType_MIN, _meshtastic_Config_DisplayConfig_DisplayMode_MIN, 0, 0, _meshtastic_Config_DisplayConfig_CompassOrientation_MIN, 0} #define meshtastic_Config_LoRaConfig_init_zero {0, _meshtastic_Config_LoRaConfig_ModemPreset_MIN, 0, 0, 0, 0, _meshtastic_Config_LoRaConfig_RegionCode_MIN, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0}, 0, 0} @@ -724,6 +753,7 @@ extern "C" { #define meshtastic_Config_DeviceConfig_disable_triple_click_tag 10 #define meshtastic_Config_DeviceConfig_tzdef_tag 11 #define meshtastic_Config_DeviceConfig_led_heartbeat_disabled_tag 12 +#define meshtastic_Config_DeviceConfig_buzzer_mode_tag 13 #define meshtastic_Config_PositionConfig_position_broadcast_secs_tag 1 #define meshtastic_Config_PositionConfig_position_broadcast_smart_enabled_tag 2 #define meshtastic_Config_PositionConfig_fixed_position_tag 3 @@ -759,6 +789,7 @@ extern "C" { #define meshtastic_Config_NetworkConfig_ipv4_config_tag 8 #define meshtastic_Config_NetworkConfig_rsyslog_server_tag 9 #define meshtastic_Config_NetworkConfig_enabled_protocols_tag 10 +#define meshtastic_Config_NetworkConfig_ipv6_enabled_tag 11 #define meshtastic_Config_DisplayConfig_screen_on_secs_tag 1 #define meshtastic_Config_DisplayConfig_gps_format_tag 2 #define meshtastic_Config_DisplayConfig_auto_screen_carousel_secs_tag 3 @@ -846,7 +877,8 @@ X(a, STATIC, SINGULAR, BOOL, double_tap_as_button_press, 8) \ X(a, STATIC, SINGULAR, BOOL, is_managed, 9) \ X(a, STATIC, SINGULAR, BOOL, disable_triple_click, 10) \ X(a, STATIC, SINGULAR, STRING, tzdef, 11) \ -X(a, STATIC, SINGULAR, BOOL, led_heartbeat_disabled, 12) +X(a, STATIC, SINGULAR, BOOL, led_heartbeat_disabled, 12) \ +X(a, STATIC, SINGULAR, UENUM, buzzer_mode, 13) #define meshtastic_Config_DeviceConfig_CALLBACK NULL #define meshtastic_Config_DeviceConfig_DEFAULT NULL @@ -889,7 +921,8 @@ X(a, STATIC, SINGULAR, BOOL, eth_enabled, 6) \ X(a, STATIC, SINGULAR, UENUM, address_mode, 7) \ X(a, STATIC, OPTIONAL, MESSAGE, ipv4_config, 8) \ X(a, STATIC, SINGULAR, STRING, rsyslog_server, 9) \ -X(a, STATIC, SINGULAR, UINT32, enabled_protocols, 10) +X(a, STATIC, SINGULAR, UINT32, enabled_protocols, 10) \ +X(a, STATIC, SINGULAR, BOOL, ipv6_enabled, 11) #define meshtastic_Config_NetworkConfig_CALLBACK NULL #define meshtastic_Config_NetworkConfig_DEFAULT NULL #define meshtastic_Config_NetworkConfig_ipv4_config_MSGTYPE meshtastic_Config_NetworkConfig_IpV4Config @@ -991,16 +1024,16 @@ extern const pb_msgdesc_t meshtastic_Config_SessionkeyConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_CONFIG_PB_H_MAX_SIZE meshtastic_Config_size #define meshtastic_Config_BluetoothConfig_size 10 -#define meshtastic_Config_DeviceConfig_size 98 +#define meshtastic_Config_DeviceConfig_size 100 #define meshtastic_Config_DisplayConfig_size 32 #define meshtastic_Config_LoRaConfig_size 85 #define meshtastic_Config_NetworkConfig_IpV4Config_size 20 -#define meshtastic_Config_NetworkConfig_size 202 +#define meshtastic_Config_NetworkConfig_size 204 #define meshtastic_Config_PositionConfig_size 62 #define meshtastic_Config_PowerConfig_size 52 #define meshtastic_Config_SecurityConfig_size 178 #define meshtastic_Config_SessionkeyConfig_size 0 -#define meshtastic_Config_size 205 +#define meshtastic_Config_size 207 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/device_ui.pb.h b/src/mesh/generated/meshtastic/device_ui.pb.h index 5692a2749..3a8ddd3a4 100644 --- a/src/mesh/generated/meshtastic/device_ui.pb.h +++ b/src/mesh/generated/meshtastic/device_ui.pb.h @@ -55,6 +55,8 @@ typedef enum _meshtastic_Language { meshtastic_Language_SLOVENIAN = 15, /* Ukrainian */ meshtastic_Language_UKRAINIAN = 16, + /* Bulgarian */ + meshtastic_Language_BULGARIAN = 17, /* Simplified Chinese (experimental) */ meshtastic_Language_SIMPLIFIED_CHINESE = 30, /* Traditional Chinese (experimental) */ diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index 2436098da..f78689cb2 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -360,7 +360,7 @@ extern const pb_msgdesc_t meshtastic_BackupPreferences_msg; /* Maximum encoded size of messages (where known) */ /* meshtastic_NodeDatabase_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_DEVICEONLY_PB_H_MAX_SIZE meshtastic_BackupPreferences_size -#define meshtastic_BackupPreferences_size 2267 +#define meshtastic_BackupPreferences_size 2271 #define meshtastic_ChannelFile_size 718 #define meshtastic_DeviceState_size 1722 #define meshtastic_NodeInfoLite_size 196 diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index 53d8d7d80..ca8dcd5fb 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -187,7 +187,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_LOCALONLY_PB_H_MAX_SIZE meshtastic_LocalConfig_size -#define meshtastic_LocalConfig_size 743 +#define meshtastic_LocalConfig_size 747 #define meshtastic_LocalModuleConfig_size 669 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/mesh.pb.cpp b/src/mesh/generated/meshtastic/mesh.pb.cpp index 6c5c7a4be..361d01b9a 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.cpp +++ b/src/mesh/generated/meshtastic/mesh.pb.cpp @@ -21,6 +21,9 @@ PB_BIND(meshtastic_Routing, meshtastic_Routing, AUTO) PB_BIND(meshtastic_Data, meshtastic_Data, 2) +PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO) + + PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) @@ -48,6 +51,21 @@ PB_BIND(meshtastic_FromRadio, meshtastic_FromRadio, 2) PB_BIND(meshtastic_ClientNotification, meshtastic_ClientNotification, 2) +PB_BIND(meshtastic_KeyVerificationNumberInform, meshtastic_KeyVerificationNumberInform, AUTO) + + +PB_BIND(meshtastic_KeyVerificationNumberRequest, meshtastic_KeyVerificationNumberRequest, AUTO) + + +PB_BIND(meshtastic_KeyVerificationFinal, meshtastic_KeyVerificationFinal, AUTO) + + +PB_BIND(meshtastic_DuplicatedPublicKey, meshtastic_DuplicatedPublicKey, AUTO) + + +PB_BIND(meshtastic_LowEntropyKey, meshtastic_LowEntropyKey, AUTO) + + PB_BIND(meshtastic_FileInfo, meshtastic_FileInfo, AUTO) diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index d6816eeef..b07c59625 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -247,6 +247,26 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO = 96, /* Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin */ meshtastic_HardwareModel_CROWPANEL = 97, + /* * + Lilygo LINK32 board with sensors */ + meshtastic_HardwareModel_LINK_32 = 98, + /* * + Seeed Tracker L1 */ + meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 = 99, + /* * + Seeed Tracker L1 EINK driver */ + meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK = 100, + /* Reserved ID for future and past use */ + meshtastic_HardwareModel_QWANTZ_TINY_ARMS = 101, + /* * + Lilygo T-Deck Pro */ + meshtastic_HardwareModel_T_DECK_PRO = 102, + /* * + Lilygo TLora Pager */ + meshtastic_HardwareModel_T_LORA_PAGER = 103, + /* * + GAT562 Mesh Trial Tracker */ + meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER = 104, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ @@ -680,6 +700,19 @@ typedef struct _meshtastic_Data { uint8_t bitfield; } meshtastic_Data; +typedef PB_BYTES_ARRAY_T(32) meshtastic_KeyVerification_hash1_t; +typedef PB_BYTES_ARRAY_T(32) meshtastic_KeyVerification_hash2_t; +/* The actual over-the-mesh message doing KeyVerification */ +typedef struct _meshtastic_KeyVerification { + /* random value Selected by the requesting node */ + uint64_t nonce; + /* The final authoritative hash, only to be sent by NodeA at the end of the handshake */ + meshtastic_KeyVerification_hash1_t hash1; + /* The intermediary hash (actually derived from hash1), + sent from NodeB to NodeA in response to the initial message. */ + meshtastic_KeyVerification_hash2_t hash2; +} meshtastic_KeyVerification; + /* Waypoint message, used to share arbitrary locations across the mesh */ typedef struct _meshtastic_Waypoint { /* Id of the waypoint */ @@ -907,6 +940,32 @@ typedef struct _meshtastic_QueueStatus { uint32_t mesh_packet_id; } meshtastic_QueueStatus; +typedef struct _meshtastic_KeyVerificationNumberInform { + uint64_t nonce; + char remote_longname[40]; + uint32_t security_number; +} meshtastic_KeyVerificationNumberInform; + +typedef struct _meshtastic_KeyVerificationNumberRequest { + uint64_t nonce; + char remote_longname[40]; +} meshtastic_KeyVerificationNumberRequest; + +typedef struct _meshtastic_KeyVerificationFinal { + uint64_t nonce; + char remote_longname[40]; + bool isSender; + char verification_characters[10]; +} meshtastic_KeyVerificationFinal; + +typedef struct _meshtastic_DuplicatedPublicKey { + char dummy_field; +} meshtastic_DuplicatedPublicKey; + +typedef struct _meshtastic_LowEntropyKey { + char dummy_field; +} meshtastic_LowEntropyKey; + /* A notification message from the device to the client To be used for important messages that should to be displayed to the user in the form of push notifications or validation messages when saving @@ -921,6 +980,14 @@ typedef struct _meshtastic_ClientNotification { meshtastic_LogRecord_Level level; /* The message body of the notification */ char message[400]; + pb_size_t which_payload_variant; + union { + meshtastic_KeyVerificationNumberInform key_verification_number_inform; + meshtastic_KeyVerificationNumberRequest key_verification_number_request; + meshtastic_KeyVerificationFinal key_verification_final; + meshtastic_DuplicatedPublicKey duplicated_public_key; + meshtastic_LowEntropyKey low_entropy_key; + } payload_variant; } meshtastic_ClientNotification; /* Individual File info for the device */ @@ -1183,6 +1250,7 @@ extern "C" { + #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority #define meshtastic_MeshPacket_delayed_ENUMTYPE meshtastic_MeshPacket_Delayed @@ -1196,6 +1264,11 @@ extern "C" { + + + + + #define meshtastic_Compressed_portnum_ENUMTYPE meshtastic_PortNum @@ -1215,6 +1288,7 @@ extern "C" { #define meshtastic_RouteDiscovery_init_default {0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}} #define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} #define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} +#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}} #define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} @@ -1223,7 +1297,12 @@ extern "C" { #define meshtastic_LogRecord_init_default {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_default {0, 0, 0, 0} #define meshtastic_FromRadio_init_default {0, 0, {meshtastic_MeshPacket_init_default}} -#define meshtastic_ClientNotification_init_default {false, 0, 0, _meshtastic_LogRecord_Level_MIN, ""} +#define meshtastic_ClientNotification_init_default {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_default}} +#define meshtastic_KeyVerificationNumberInform_init_default {0, "", 0} +#define meshtastic_KeyVerificationNumberRequest_init_default {0, ""} +#define meshtastic_KeyVerificationFinal_init_default {0, "", 0, ""} +#define meshtastic_DuplicatedPublicKey_init_default {0} +#define meshtastic_LowEntropyKey_init_default {0} #define meshtastic_FileInfo_init_default {"", 0} #define meshtastic_ToRadio_init_default {0, {meshtastic_MeshPacket_init_default}} #define meshtastic_Compressed_init_default {_meshtastic_PortNum_MIN, {0, {0}}} @@ -1240,6 +1319,7 @@ extern "C" { #define meshtastic_RouteDiscovery_init_zero {0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}} #define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} #define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0} +#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}} #define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0} #define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN, 0, 0, {0, {0}}, 0, 0, 0, 0} @@ -1248,7 +1328,12 @@ extern "C" { #define meshtastic_LogRecord_init_zero {"", 0, "", _meshtastic_LogRecord_Level_MIN} #define meshtastic_QueueStatus_init_zero {0, 0, 0, 0} #define meshtastic_FromRadio_init_zero {0, 0, {meshtastic_MeshPacket_init_zero}} -#define meshtastic_ClientNotification_init_zero {false, 0, 0, _meshtastic_LogRecord_Level_MIN, ""} +#define meshtastic_ClientNotification_init_zero {false, 0, 0, _meshtastic_LogRecord_Level_MIN, "", 0, {meshtastic_KeyVerificationNumberInform_init_zero}} +#define meshtastic_KeyVerificationNumberInform_init_zero {0, "", 0} +#define meshtastic_KeyVerificationNumberRequest_init_zero {0, ""} +#define meshtastic_KeyVerificationFinal_init_zero {0, "", 0, ""} +#define meshtastic_DuplicatedPublicKey_init_zero {0} +#define meshtastic_LowEntropyKey_init_zero {0} #define meshtastic_FileInfo_init_zero {"", 0} #define meshtastic_ToRadio_init_zero {0, {meshtastic_MeshPacket_init_zero}} #define meshtastic_Compressed_init_zero {_meshtastic_PortNum_MIN, {0, {0}}} @@ -1310,6 +1395,9 @@ extern "C" { #define meshtastic_Data_reply_id_tag 7 #define meshtastic_Data_emoji_tag 8 #define meshtastic_Data_bitfield_tag 9 +#define meshtastic_KeyVerification_nonce_tag 1 +#define meshtastic_KeyVerification_hash1_tag 2 +#define meshtastic_KeyVerification_hash2_tag 3 #define meshtastic_Waypoint_id_tag 1 #define meshtastic_Waypoint_latitude_i_tag 2 #define meshtastic_Waypoint_longitude_i_tag 3 @@ -1367,10 +1455,24 @@ extern "C" { #define meshtastic_QueueStatus_free_tag 2 #define meshtastic_QueueStatus_maxlen_tag 3 #define meshtastic_QueueStatus_mesh_packet_id_tag 4 +#define meshtastic_KeyVerificationNumberInform_nonce_tag 1 +#define meshtastic_KeyVerificationNumberInform_remote_longname_tag 2 +#define meshtastic_KeyVerificationNumberInform_security_number_tag 3 +#define meshtastic_KeyVerificationNumberRequest_nonce_tag 1 +#define meshtastic_KeyVerificationNumberRequest_remote_longname_tag 2 +#define meshtastic_KeyVerificationFinal_nonce_tag 1 +#define meshtastic_KeyVerificationFinal_remote_longname_tag 2 +#define meshtastic_KeyVerificationFinal_isSender_tag 3 +#define meshtastic_KeyVerificationFinal_verification_characters_tag 4 #define meshtastic_ClientNotification_reply_id_tag 1 #define meshtastic_ClientNotification_time_tag 2 #define meshtastic_ClientNotification_level_tag 3 #define meshtastic_ClientNotification_message_tag 4 +#define meshtastic_ClientNotification_key_verification_number_inform_tag 11 +#define meshtastic_ClientNotification_key_verification_number_request_tag 12 +#define meshtastic_ClientNotification_key_verification_final_tag 13 +#define meshtastic_ClientNotification_duplicated_public_key_tag 14 +#define meshtastic_ClientNotification_low_entropy_key_tag 15 #define meshtastic_FileInfo_file_name_tag 1 #define meshtastic_FileInfo_size_bytes_tag 2 #define meshtastic_Compressed_portnum_tag 1 @@ -1501,6 +1603,13 @@ X(a, STATIC, OPTIONAL, UINT32, bitfield, 9) #define meshtastic_Data_CALLBACK NULL #define meshtastic_Data_DEFAULT NULL +#define meshtastic_KeyVerification_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ +X(a, STATIC, SINGULAR, BYTES, hash1, 2) \ +X(a, STATIC, SINGULAR, BYTES, hash2, 3) +#define meshtastic_KeyVerification_CALLBACK NULL +#define meshtastic_KeyVerification_DEFAULT NULL + #define meshtastic_Waypoint_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, id, 1) \ X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \ @@ -1629,9 +1738,50 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,deviceuiConfig,deviceuiConfi X(a, STATIC, OPTIONAL, UINT32, reply_id, 1) \ X(a, STATIC, SINGULAR, FIXED32, time, 2) \ X(a, STATIC, SINGULAR, UENUM, level, 3) \ -X(a, STATIC, SINGULAR, STRING, message, 4) +X(a, STATIC, SINGULAR, STRING, message, 4) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_inform,payload_variant.key_verification_number_inform), 11) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_number_request,payload_variant.key_verification_number_request), 12) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,key_verification_final,payload_variant.key_verification_final), 13) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,duplicated_public_key,payload_variant.duplicated_public_key), 14) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,low_entropy_key,payload_variant.low_entropy_key), 15) #define meshtastic_ClientNotification_CALLBACK NULL #define meshtastic_ClientNotification_DEFAULT NULL +#define meshtastic_ClientNotification_payload_variant_key_verification_number_inform_MSGTYPE meshtastic_KeyVerificationNumberInform +#define meshtastic_ClientNotification_payload_variant_key_verification_number_request_MSGTYPE meshtastic_KeyVerificationNumberRequest +#define meshtastic_ClientNotification_payload_variant_key_verification_final_MSGTYPE meshtastic_KeyVerificationFinal +#define meshtastic_ClientNotification_payload_variant_duplicated_public_key_MSGTYPE meshtastic_DuplicatedPublicKey +#define meshtastic_ClientNotification_payload_variant_low_entropy_key_MSGTYPE meshtastic_LowEntropyKey + +#define meshtastic_KeyVerificationNumberInform_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ +X(a, STATIC, SINGULAR, STRING, remote_longname, 2) \ +X(a, STATIC, SINGULAR, UINT32, security_number, 3) +#define meshtastic_KeyVerificationNumberInform_CALLBACK NULL +#define meshtastic_KeyVerificationNumberInform_DEFAULT NULL + +#define meshtastic_KeyVerificationNumberRequest_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ +X(a, STATIC, SINGULAR, STRING, remote_longname, 2) +#define meshtastic_KeyVerificationNumberRequest_CALLBACK NULL +#define meshtastic_KeyVerificationNumberRequest_DEFAULT NULL + +#define meshtastic_KeyVerificationFinal_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UINT64, nonce, 1) \ +X(a, STATIC, SINGULAR, STRING, remote_longname, 2) \ +X(a, STATIC, SINGULAR, BOOL, isSender, 3) \ +X(a, STATIC, SINGULAR, STRING, verification_characters, 4) +#define meshtastic_KeyVerificationFinal_CALLBACK NULL +#define meshtastic_KeyVerificationFinal_DEFAULT NULL + +#define meshtastic_DuplicatedPublicKey_FIELDLIST(X, a) \ + +#define meshtastic_DuplicatedPublicKey_CALLBACK NULL +#define meshtastic_DuplicatedPublicKey_DEFAULT NULL + +#define meshtastic_LowEntropyKey_FIELDLIST(X, a) \ + +#define meshtastic_LowEntropyKey_CALLBACK NULL +#define meshtastic_LowEntropyKey_DEFAULT NULL #define meshtastic_FileInfo_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, STRING, file_name, 1) \ @@ -1731,6 +1881,7 @@ extern const pb_msgdesc_t meshtastic_User_msg; extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg; extern const pb_msgdesc_t meshtastic_Routing_msg; extern const pb_msgdesc_t meshtastic_Data_msg; +extern const pb_msgdesc_t meshtastic_KeyVerification_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg; @@ -1740,6 +1891,11 @@ extern const pb_msgdesc_t meshtastic_LogRecord_msg; extern const pb_msgdesc_t meshtastic_QueueStatus_msg; extern const pb_msgdesc_t meshtastic_FromRadio_msg; extern const pb_msgdesc_t meshtastic_ClientNotification_msg; +extern const pb_msgdesc_t meshtastic_KeyVerificationNumberInform_msg; +extern const pb_msgdesc_t meshtastic_KeyVerificationNumberRequest_msg; +extern const pb_msgdesc_t meshtastic_KeyVerificationFinal_msg; +extern const pb_msgdesc_t meshtastic_DuplicatedPublicKey_msg; +extern const pb_msgdesc_t meshtastic_LowEntropyKey_msg; extern const pb_msgdesc_t meshtastic_FileInfo_msg; extern const pb_msgdesc_t meshtastic_ToRadio_msg; extern const pb_msgdesc_t meshtastic_Compressed_msg; @@ -1758,6 +1914,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_RouteDiscovery_fields &meshtastic_RouteDiscovery_msg #define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Data_fields &meshtastic_Data_msg +#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg #define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg @@ -1767,6 +1924,11 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; #define meshtastic_QueueStatus_fields &meshtastic_QueueStatus_msg #define meshtastic_FromRadio_fields &meshtastic_FromRadio_msg #define meshtastic_ClientNotification_fields &meshtastic_ClientNotification_msg +#define meshtastic_KeyVerificationNumberInform_fields &meshtastic_KeyVerificationNumberInform_msg +#define meshtastic_KeyVerificationNumberRequest_fields &meshtastic_KeyVerificationNumberRequest_msg +#define meshtastic_KeyVerificationFinal_fields &meshtastic_KeyVerificationFinal_msg +#define meshtastic_DuplicatedPublicKey_fields &meshtastic_DuplicatedPublicKey_msg +#define meshtastic_LowEntropyKey_fields &meshtastic_LowEntropyKey_msg #define meshtastic_FileInfo_fields &meshtastic_FileInfo_msg #define meshtastic_ToRadio_fields &meshtastic_ToRadio_msg #define meshtastic_Compressed_fields &meshtastic_Compressed_msg @@ -1784,14 +1946,20 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg; /* meshtastic_ChunkedPayloadResponse_size depends on runtime parameters */ #define MESHTASTIC_MESHTASTIC_MESH_PB_H_MAX_SIZE meshtastic_FromRadio_size #define meshtastic_ChunkedPayload_size 245 -#define meshtastic_ClientNotification_size 415 +#define meshtastic_ClientNotification_size 482 #define meshtastic_Compressed_size 239 #define meshtastic_Data_size 269 #define meshtastic_DeviceMetadata_size 54 +#define meshtastic_DuplicatedPublicKey_size 0 #define meshtastic_FileInfo_size 236 #define meshtastic_FromRadio_size 510 #define meshtastic_Heartbeat_size 0 +#define meshtastic_KeyVerificationFinal_size 65 +#define meshtastic_KeyVerificationNumberInform_size 58 +#define meshtastic_KeyVerificationNumberRequest_size 52 +#define meshtastic_KeyVerification_size 79 #define meshtastic_LogRecord_size 426 +#define meshtastic_LowEntropyKey_size 0 #define meshtastic_MeshPacket_size 378 #define meshtastic_MqttClientProxyMessage_size 501 #define meshtastic_MyNodeInfo_size 77 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 4e7c43e58..5bd27ef7d 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -74,6 +74,8 @@ typedef enum _meshtastic_PortNum { meshtastic_PortNum_DETECTION_SENSOR_APP = 10, /* Same as Text Message but used for critical alerts. */ meshtastic_PortNum_ALERT_APP = 11, + /* Module/port for handling key verification requests. */ + meshtastic_PortNum_KEY_VERIFICATION_APP = 12, /* Provides a 'ping' service that replies to any packet it receives. Also serves as a small example module. ENCODING: ASCII Plaintext */ diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 4071c611e..4fa673df8 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -277,6 +277,10 @@ typedef struct _meshtastic_LocalStats { /* Number of times we canceled a packet to be relayed, because someone else did it before us. This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. */ uint32_t num_tx_relay_canceled; + /* Number of bytes used in the heap */ + uint32_t heap_total_bytes; + /* Number of bytes free in the heap */ + uint32_t heap_free_bytes; } meshtastic_LocalStats; /* Health telemetry metrics */ @@ -374,7 +378,7 @@ extern "C" { #define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} -#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_default {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_default {0, 0, {meshtastic_DeviceMetrics_init_default}} @@ -383,7 +387,7 @@ extern "C" { #define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} -#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} #define meshtastic_HostMetrics_init_zero {0, 0, 0, false, 0, false, 0, 0, 0, 0, false, ""} #define meshtastic_Telemetry_init_zero {0, 0, {meshtastic_DeviceMetrics_init_zero}} @@ -447,6 +451,8 @@ extern "C" { #define meshtastic_LocalStats_num_rx_dupe_tag 9 #define meshtastic_LocalStats_num_tx_relay_tag 10 #define meshtastic_LocalStats_num_tx_relay_canceled_tag 11 +#define meshtastic_LocalStats_heap_total_bytes_tag 12 +#define meshtastic_LocalStats_heap_free_bytes_tag 13 #define meshtastic_HealthMetrics_heart_bpm_tag 1 #define meshtastic_HealthMetrics_spO2_tag 2 #define meshtastic_HealthMetrics_temperature_tag 3 @@ -544,7 +550,9 @@ X(a, STATIC, SINGULAR, UINT32, num_online_nodes, 7) \ X(a, STATIC, SINGULAR, UINT32, num_total_nodes, 8) \ X(a, STATIC, SINGULAR, UINT32, num_rx_dupe, 9) \ X(a, STATIC, SINGULAR, UINT32, num_tx_relay, 10) \ -X(a, STATIC, SINGULAR, UINT32, num_tx_relay_canceled, 11) +X(a, STATIC, SINGULAR, UINT32, num_tx_relay_canceled, 11) \ +X(a, STATIC, SINGULAR, UINT32, heap_total_bytes, 12) \ +X(a, STATIC, SINGULAR, UINT32, heap_free_bytes, 13) #define meshtastic_LocalStats_CALLBACK NULL #define meshtastic_LocalStats_DEFAULT NULL @@ -621,7 +629,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; #define meshtastic_EnvironmentMetrics_size 113 #define meshtastic_HealthMetrics_size 11 #define meshtastic_HostMetrics_size 264 -#define meshtastic_LocalStats_size 60 +#define meshtastic_LocalStats_size 72 #define meshtastic_Nau7802Config_size 16 #define meshtastic_PowerMetrics_size 30 #define meshtastic_Telemetry_size 272 diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 5841fe478..42ebb8417 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -903,7 +903,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) } } else { #if HAS_SCREEN - screen->blink(); + if (screen) + screen->blink(); #endif } diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 5f6ad9eb3..bf170de59 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -154,7 +154,8 @@ void createSSLCert() esp_task_wdt_reset(); #if HAS_SCREEN if (millis() / 1000 >= 3) { - screen->setSSLFrames(); + if (screen) + screen->setSSLFrames(); } #endif } diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 789f8ac44..24be97ad7 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -46,6 +46,10 @@ uint8_t wifiDisconnectReason = 0; // Stores our hostname char ourHost[16]; +// To replace blocking wifi connect delay with a non-blocking sleep +static unsigned long wifiReconnectStartMillis = 0; +static bool wifiReconnectPending = false; + bool APStartupComplete = 0; unsigned long lastrun_ntp = 0; @@ -124,10 +128,14 @@ static void onNetworkConnected() } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - initWebServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initWebServer(); + } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif APStartupComplete = true; } @@ -160,17 +168,30 @@ static int32_t reconnectWiFi() #endif LOG_INFO("Reconnecting to WiFi access point %s", wifiName); - delay(5000); + // Start the non-blocking wait for 5 seconds + wifiReconnectStartMillis = millis(); + wifiReconnectPending = true; + // Do not attempt to connect yet, wait for the next invocation + return 5000; // Schedule next check soon + } - if (!WiFi.isConnected()) { + // Check if we are ready to proceed with the WiFi connection after the 5s wait + if (wifiReconnectPending) { + if (millis() - wifiReconnectStartMillis >= 5000) { + if (!WiFi.isConnected()) { #ifdef CONFIG_IDF_TARGET_ESP32C3 - WiFi.mode(WIFI_MODE_NULL); - WiFi.useStaticBuffers(true); - WiFi.mode(WIFI_STA); + WiFi.mode(WIFI_MODE_NULL); + WiFi.useStaticBuffers(true); + WiFi.mode(WIFI_STA); #endif - WiFi.begin(wifiName, wifiPsw); + WiFi.begin(wifiName, wifiPsw); + } + isReconnecting = false; + wifiReconnectPending = false; + } else { + // Still waiting for 5s to elapse + return 100; // Check again soon } - isReconnecting = false; } #ifndef DISABLE_NTP @@ -193,8 +214,6 @@ static int32_t reconnectWiFi() if (config.network.wifi_enabled && !WiFi.isConnected()) { #ifdef ARCH_RP2040 // (ESP32 handles this in WiFiEvent) - /* If APStartupComplete, but we're not connected, try again. - Shouldn't try again before APStartupComplete. */ needReconnect = APStartupComplete; #endif return 1000; // check once per second @@ -327,9 +346,15 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: LOG_INFO("Connected to access point"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: LOG_INFO("Disconnected from WiFi access point"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif if (!isReconnecting) { WiFi.disconnect(false, true); syslog.disable(); @@ -378,9 +403,15 @@ static void WiFiEvent(WiFiEvent_t event) break; case ARDUINO_EVENT_WIFI_AP_START: LOG_INFO("WiFi access point started"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, HIGH); +#endif break; case ARDUINO_EVENT_WIFI_AP_STOP: LOG_INFO("WiFi access point stopped"); +#ifdef WIFI_LED + digitalWrite(WIFI_LED, LOW); +#endif break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: LOG_INFO("Client connected"); @@ -474,4 +505,4 @@ uint8_t getWifiDisconnectReason() { return wifiDisconnectReason; } -#endif \ No newline at end of file +#endif // HAS_WIFI \ No newline at end of file diff --git a/src/meshUtils.h b/src/meshUtils.h index 47d42b41b..35b88e8b2 100644 --- a/src/meshUtils.h +++ b/src/meshUtils.h @@ -11,6 +11,14 @@ template constexpr const T &clamp(const T &v, const T &lo, const T &hi return (v < lo) ? lo : (hi < v) ? hi : v; } +#if HAS_SCREEN +#define IF_SCREEN(X) \ + if (screen) \ + X; +#else +#define IF_SCREEN(...) +#endif + #if (defined(ARCH_PORTDUINO) && !defined(STRNSTR)) #define STRNSTR #include diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 3ff4fa74d..aad7f5f06 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -5,8 +5,10 @@ #include "PowerFSM.h" #include "RTC.h" #include "SPILock.h" +#include "input/InputBroker.h" #include "meshUtils.h" #include +#include // for better whitespace handling #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_BLUETOOTH #include "BleOta.h" #endif @@ -155,6 +157,28 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta */ case meshtastic_AdminMessage_set_owner_tag: LOG_DEBUG("Client set owner"); + // Validate names + if (*r->set_owner.long_name) { + const char *start = r->set_owner.long_name; + // Skip all whitespace (space, tab, newline, etc) + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } + if (*r->set_owner.short_name) { + const char *start = r->set_owner.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character"); + myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp); + break; + } + } handleSetOwner(r->set_owner); break; @@ -200,14 +224,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH if (!BleOta::getOtaAppVersion().isEmpty()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); BleOta::switchToOtaApp(); LOG_INFO("Rebooting to BLE OTA"); } #endif #if !MESHTASTIC_EXCLUDE_WIFI if (WiFiOTA::trySwitchToOTA()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); WiFiOTA::saveConfig(&config.network); LOG_INFO("Rebooting to WiFi OTA"); } @@ -297,6 +323,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = true; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -306,6 +334,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = false; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -420,6 +450,11 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #endif break; } + case meshtastic_AdminMessage_send_input_event_tag: { + LOG_INFO("Client requesting to send input event"); + handleSendInputEvent(r->send_input_event); + break; + } #ifdef ARCH_PORTDUINO case meshtastic_AdminMessage_exit_simulator_tag: LOG_INFO("Exiting simulator"); @@ -435,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta setPassKey(&res); myReply = allocDataProtobuf(res); } else if (mp.decoded.want_response) { - LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant); + LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); } else if (handleResult != AdminMessageHandleResult::HANDLED) { // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant); + LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); } break; } + // Allow any observers (e.g. the UI) to handle/respond + AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; + meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; + AdminModule_ObserverData observerData = { + .request = r, + .response = &observerResponse, + .result = &observerResult, + }; + + notifyObservers(&observerData); + + if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&observerResponse); + myReply = allocDataProtobuf(observerResponse); + LOG_DEBUG("Observer responded to admin message"); + } else if (observerResult == AdminMessageHandleResult::HANDLED) { + LOG_DEBUG("Observer handled admin message"); + } + // If asked for a response and it is not yet set, generate an 'ACK' response if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - // Allow any observers (e.g. the UI) to respond to this event - notifyObservers(r); - return handled; } @@ -507,7 +558,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { changed = 1; - owner.has_is_unmessagable = o.has_is_unmessagable || o.has_is_unmessagable; + owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; owner.is_unmessagable = o.is_unmessagable; } @@ -620,8 +671,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) config.has_display = true; if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && config.display.flip_screen == c.payload_variant.display.flip_screen && - config.display.oled == c.payload_variant.display.oled) { + config.display.oled == c.payload_variant.display.oled && + config.display.displaymode == c.payload_variant.display.displaymode) { requiresReboot = false; + } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && + c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + config.bluetooth.enabled = false; } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && @@ -661,6 +716,24 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) config.lora = c.payload_variant.lora; // If we're setting region for the first time, init the region if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } config.lora.tx_enabled = true; initRegion(); if (myRegion->dutyCycle < 100) { @@ -681,11 +754,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) LOG_INFO("Set config: Security"); config.security = c.payload_variant.security; #if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI) - // We check for a potentially valid private key, and a blank public key, and regen the public key if needed. - if (config.security.private_key.size == 32 && !memfll(config.security.private_key.bytes, 0, 32) && - (config.security.public_key.size == 0 || memfll(config.security.public_key.bytes, 0, 32))) { - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - config.security.public_key.size = 32; + // If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode + if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + if (config.security.private_key.size != 32) { + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + + } else if (config.security.public_key.size != 32) { + // We check for a potentially valid private key, and a blank public key, and regen the public key if needed. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + config.security.public_key.size = 32; + } } } #endif @@ -1075,7 +1153,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r #endif #endif conn.has_serial = true; // No serial-less devices -#if !EXCLUDE_POWER_FSM +#if !MESHTASTIC_EXCLUDE_POWER_FSM conn.serial.is_connected = powerFSM.getState() == &stateSERIAL; #else conn.serial.is_connected = powerFSM.getState(); @@ -1111,7 +1189,8 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); - screen->startAlert("Rebooting..."); + if (screen) + screen->showOverlayBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } @@ -1135,6 +1214,27 @@ void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uic void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) { + // Validate ham parameters before setting since this would bypass validation in the owner struct + if (*p.call_sign) { + const char *start = p.call_sign; + // Skip all whitespace + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character"); + return; + } + } + if (*p.short_name) { + const char *start = p.short_name; + while (*start && isspace((unsigned char)*start)) + start++; + if (*start == '\0') { + LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character"); + return; + } + } + // Set call sign and override lora limitations for licensed use strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name)); strncpy(owner.short_name, p.short_name, sizeof(owner.short_name)); @@ -1221,6 +1321,39 @@ bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r) return false; } +void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent) +{ + LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code, + inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y); + + // Validate input parameters + if (inputEvent.event_code > INPUT_BROKER_ANYKEY) { + LOG_WARN("Invalid input event code: %u", inputEvent.event_code); + return; + } + + // Create InputEvent for injection + InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code, + .kbchar = (unsigned char)inputEvent.kb_char, + .touchX = inputEvent.touch_x, + .touchY = inputEvent.touch_y}; + + // Log the event being injected + LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent, + (event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY); + + // Wake the device if asleep + powerFSM.trigger(EVENT_INPUT); +#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) + // Inject the event through InputBroker + if (inputBroker) { + inputBroker->injectInputEvent(&event); + } else { + LOG_ERROR("InputBroker not available for event injection"); + } +#endif +} + void AdminModule::sendWarning(const char *message) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 246d39e37..867751f49 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -6,10 +6,19 @@ #include "mesh/wifi/WiFiAPClient.h" #endif +/** + * Datatype passed to Observers by AdminModule, to allow external handling of admin messages + */ +struct AdminModule_ObserverData { + const meshtastic_AdminMessage *request; + meshtastic_AdminMessage *response; + AdminMessageHandleResult *result; +}; + /** * Admin module for admin messages */ -class AdminModule : public ProtobufModule, public Observable +class AdminModule : public ProtobufModule, public Observable { public: /** Constructor @@ -54,6 +63,7 @@ class AdminModule : public ProtobufModule, public Obser void handleSetChannel(); void handleSetHamMode(const meshtastic_HamParameters &req); void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg); + void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent); void reboot(int32_t seconds); void setPassKey(meshtastic_AdminMessage *res); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c16c0e4b3..b24f3ca00 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -8,14 +8,16 @@ #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" -#include "PowerFSM.h" // needed for button bypass #include "SPILock.h" +#include "buzz.h" #include "detect/ScanI2C.h" -#include "input/ScanAndSelect.h" +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "graphics/images.h" +#include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" - -#include "main.h" // for cardkb_found #include "modules/ExternalNotificationModule.h" // for buzzer control #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" @@ -35,6 +37,7 @@ #define INACTIVATE_AFTER_MS 20000 extern ScanI2C::DeviceAddress cardkb_found; +extern bool graphics::isMuted; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; @@ -45,358 +48,763 @@ CannedMessageModule *cannedMessageModule; CannedMessageModule::CannedMessageModule() : SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage") { - if (moduleConfig.canned_message.enabled || CANNED_MESSAGE_MODULE_ENABLE) { - this->loadProtoForModule(); - if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && - !CANNED_MESSAGE_MODULE_ENABLE) { - LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); - this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; - disable(); - } else { - LOG_INFO("CannedMessageModule is enabled"); - - // T-Watch interface currently has no way to select destination type, so default to 'node' -#if defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; -#endif - - this->inputObserver.observe(inputBroker); - } - } else { + this->loadProtoForModule(); + if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE && + !CANNED_MESSAGE_MODULE_ENABLE) { + LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled"); this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED; disable(); + } else { + LOG_INFO("CannedMessageModule is enabled"); + this->inputObserver.observe(inputBroker); } } +void CannedMessageModule::LaunchWithDestination(NodeNum newDest, uint8_t newChannel) +{ + dest = newDest; + channel = newChannel; + // Always select the first real canned message on activation + int firstRealMsgIdx = 0; + for (int i = 0; i < messagesCount; ++i) { + if (strcmp(messages[i], "[Select Destination]") != 0 && strcmp(messages[i], "[Exit]") != 0 && + strcmp(messages[i], "[---- Free Text ----]") != 0) { + firstRealMsgIdx = i; + break; + } + } + currentMessageIndex = firstRealMsgIdx; + + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); +} + +void CannedMessageModule::LaunchFreetextWithDestination(NodeNum newDest, uint8_t newChannel) +{ + dest = newDest; + channel = newChannel; + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); +} + +static bool returnToCannedList = false; +bool hasKeyForNode(const meshtastic_NodeInfoLite *node) +{ + return node && node->has_user && node->user.public_key.size > 0; +} /** * @brief Items in array this->messages will be set to be pointing on the right * starting points of the string this->messageStore * * @return int Returns the number of messages found. */ -// FIXME: This is just one set of messages now + int CannedMessageModule::splitConfiguredMessages() { - int messageIndex = 0; int i = 0; String canned_messages = cannedMessageModuleConfig.messages; -#if defined(USE_VIRTUAL_KEYBOARD) - String separator = canned_messages.length() ? "|" : ""; - - canned_messages = "[---- Free Text ----]" + separator + canned_messages; -#endif - - // collect all the message parts + // Copy all message parts into the buffer strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); - // The first message points to the beginning of the store. - this->messages[messageIndex++] = this->messageStore; + // Temporary array to allow for insertion + const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + int tempCount = 0; + // Insert at position 0 (top) + tempMessages[tempCount++] = "[Select Destination]"; + +#if defined(USE_VIRTUAL_KEYBOARD) + // Add a "Free Text" entry at the top if using a keyboard + tempMessages[tempCount++] = "[-- Free Text --]"; +#endif + + // First message always starts at buffer start + tempMessages[tempCount++] = this->messageStore; int upTo = strlen(this->messageStore) - 1; + // Walk buffer, splitting on '|' while (i < upTo) { if (this->messageStore[i] == '|') { - // Message ending found, replace it with string-end character. - this->messageStore[i] = '\0'; - - // hit our max messages, bail - if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { - this->messagesCount = messageIndex; - return this->messagesCount; - } - - // Next message starts after pipe (|) just found. - this->messages[messageIndex++] = (this->messageStore + i + 1); + this->messageStore[i] = '\0'; // End previous message + if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) + break; + tempMessages[tempCount++] = (this->messageStore + i + 1); } i += 1; } - if (strlen(this->messages[messageIndex - 1]) > 0) { - // We have a last message. - LOG_DEBUG("CannedMessage %d is: '%s'", messageIndex - 1, this->messages[messageIndex - 1]); - this->messagesCount = messageIndex; - } else { - this->messagesCount = messageIndex - 1; + + // Add [Exit] as the last entry + tempMessages[tempCount++] = "[Exit]"; + + // Copy to the member array + for (int k = 0; k < tempCount; ++k) { + this->messages[k] = (char *)tempMessages[k]; } + this->messagesCount = tempCount; return this->messagesCount; } +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) +{ + if (display->getWidth() > 128) { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + } + } else { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadc@%.5s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %s", getNodeName(this->dest)); + } + } +} +void CannedMessageModule::resetSearch() +{ + LOG_INFO("Resetting search, restoring full destination list"); + + int previousDestIndex = destIndex; + + searchQuery = ""; + updateDestinationSelectionList(); + + // Adjust scrollIndex so previousDestIndex is still visible + int totalEntries = activeChannelIndices.size() + filteredNodes.size(); + this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) + this->visibleRows = 1; + int maxScrollIndex = std::max(0, totalEntries - visibleRows); + scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); + + lastUpdateMillis = millis(); + requestFocus(); +} +void CannedMessageModule::updateDestinationSelectionList() +{ + static size_t lastNumMeshNodes = 0; + static String lastSearchQuery = ""; + + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + bool nodesChanged = (numMeshNodes != lastNumMeshNodes); + lastNumMeshNodes = numMeshNodes; + + // Early exit if nothing changed + if (searchQuery == lastSearchQuery && !nodesChanged) + return; + lastSearchQuery = searchQuery; + needsUpdate = false; + + this->filteredNodes.clear(); + this->activeChannelIndices.clear(); + + NodeNum myNodeNum = nodeDB->getNodeNum(); + String lowerSearchQuery = searchQuery; + lowerSearchQuery.toLowerCase(); + + // Preallocate space to reduce reallocation + this->filteredNodes.reserve(numMeshNodes); + + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum) + continue; + + const String &nodeName = node->user.long_name; + + if (searchQuery.length() == 0) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } else { + // Avoid unnecessary lowercase conversion if already matched + String lowerNodeName = nodeName; + lowerNodeName.toLowerCase(); + + if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } + } + } + + // Populate active channels + std::vector seenChannels; + seenChannels.reserve(channels.getNumChannels()); + for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { + String name = channels.getName(i); + if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { + this->activeChannelIndices.push_back(i); + seenChannels.push_back(name); + } + } + + // Sort by favorite, then last heard + std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { + if (a.node->is_favorite != b.node->is_favorite) + return a.node->is_favorite > b.node->is_favorite; + return a.lastHeard < b.lastHeard; + }); + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry + if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + LOG_INFO("Nodes changed, forcing UI refresh."); + screen->forceDisplay(); + } +} + +// Returns true if character input is currently allowed (used for search/freetext states) +bool CannedMessageModule::isCharInputAllowed() const +{ + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +} +/** + * Main input event dispatcher for CannedMessageModule. + * Routes keyboard/button/touch input to the correct handler based on the current runState. + * Only one handler (per state) processes each event, eliminating redundancy. + */ int CannedMessageModule::handleInputEvent(const InputEvent *event) { - if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { - // Event source is not accepted. - // Event only accepted if source matches the configured one, or - // the configured one is "_any" (or if there is no configured - // source at all) + // Block ALL input if an alert banner is active + if (screen && screen->isOverlayBannerShowing()) { return 0; } - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - return 0; // Ignore input while sending - } - bool validEvent = false; - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - validEvent = true; - } - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - validEvent = true; - } - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { -#if defined(USE_VIRTUAL_KEYBOARD) - if (this->currentMessageIndex == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + // Tab key: Always allow switching between canned/destination screens + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) + return 1; - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + // Matrix keypad: If matrix key, trigger action select for canned message + if (event->inputEvent == INPUT_BROKER_MATRIXKEY) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = INPUT_BROKER_MATRIXKEY; + currentMessageIndex = event->kbchar - 1; + lastTouchMillis = millis(); + requestFocus(); + return 1; + } + + // Always normalize navigation/select buttons for further handlers + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // Route event to handler for current UI state (no double-handling) + switch (runState) { + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) + return 1; + return 0; // prevent fall-through to selector input + + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: + return handleEmotePickerInput(event); + + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (isSelect) { + return 0; // Main button press no longer runs through powerFSM + } + // Let LEFT/RIGHT pass through so frame navigation works + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) { + break; + } + // Handle UP/DOWN: activate canned message list! + if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN || + event->inputEvent == INPUT_BROKER_ALT_LONG) { + LaunchWithDestination(NODENUM_BROADCAST); + return 1; + } + // Printable char (ASCII) opens free text compose + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - - return 0; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); } -#endif + break; - // when inactive, call the onebutton shortpress instead. Activate Module only on up/down - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { - powerFSM.trigger(EVENT_PRESS); - } else { - this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - validEvent = true; - } + // (Other states can be added here as needed) + default: + break; } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->currentMessageIndex = -1; -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->freetext = ""; // clear freetext - this->cursor = 0; - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif + // If no state handler above processed the event, let the message selector try to handle it + // (Handles up/down/select on canned message list, exit/return) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) + return 1; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->notifyObservers(&e); - } - if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { + // Default: event not handled by canned message system, allow others to process + return 0; +} -#if defined(USE_VIRTUAL_KEYBOARD) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } -#else - // tweak for left/right events generated via trackball/touch with empty kbchar - if (!event->kbchar) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } - } else { - // pass the pressed key - this->payload = event->kbchar; - } -#endif +bool CannedMessageModule::isUpEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_UP || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + event->inputEvent == INPUT_BROKER_ALT_PRESS); +} +bool CannedMessageModule::isDownEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_DOWN || + ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && + event->inputEvent == INPUT_BROKER_USER_PRESS); +} +bool CannedMessageModule::isSelectEvent(const InputEvent *event) +{ + return event->inputEvent == INPUT_BROKER_SELECT; +} - this->lastTouchMillis = millis(); - validEvent = true; - } - if (event->inputEvent == static_cast(ANYKEY)) { - // when inactive, this will switch to the freetext mode - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - } +bool CannedMessageModule::handleTabSwitch(const InputEvent *event) +{ + if (event->kbchar != 0x09) + return false; - validEvent = false; // If key is normal than it will be set to true. + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - // Run modifier key code below, (doesnt inturrupt typing or reset to start screen page) - switch (event->kbchar) { - case INPUT_BROKER_MSG_BRIGHTNESS_UP: // make screen brighter - if (screen) - screen->increaseBrightness(); - LOG_DEBUG("Increase Screen Brightness"); - break; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: // make screen dimmer - if (screen) - screen->decreaseBrightness(); - LOG_DEBUG("Decrease Screen Brightness"); - break; - case INPUT_BROKER_MSG_FN_SYMBOL_ON: // draw modifier (function) symbol - if (screen) - screen->setFunctionSymbol("Fn"); - break; - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: // remove modifier (function) symbol - if (screen) - screen->removeFunctionSymbol("Fn"); - break; - // mute (switch off/toggle) external notifications on fn+m - case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - showTemporaryMessage("Notifications \nEnabled"); - if (screen) - screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner - } else { - externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop - externalNotificationModule->setMute(true); - showTemporaryMessage("Notifications \nDisabled"); - if (screen) - screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner - } - } - break; - case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does -#if !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) { - gps->toggleGpsMode(); - } - if (screen) - screen->forceDisplay(); - showTemporaryMessage("GPS Toggled"); -#endif - break; - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off - if (config.bluetooth.enabled == true) { - config.bluetooth.enabled = false; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - disableBluetooth(); - showTemporaryMessage("Bluetooth OFF"); - } else if (config.bluetooth.enabled == false) { - config.bluetooth.enabled = true; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - rebootAtMsec = millis() + 2000; - showTemporaryMessage("Bluetooth ON\nReboot"); - } - break; - case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - showTemporaryMessage("Position \nUpdate Sent"); - } else { - showTemporaryMessage("Node Info \nUpdate Sent"); - } - break; - case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint - // Avoid opening the canned message screen frame - // We're only handling the keypress here by convention, this has nothing to do with canned messages - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - // Attempt to close whatever frame is currently shown on display - screen->dismissCurrentFrame(); - return 0; - default: - // pass the pressed key - // LOG_DEBUG("Canned message ANYKEY (%x)", event->kbchar); - this->payload = event->kbchar; - this->lastTouchMillis = millis(); - validEvent = true; - break; - } - if (screen && (event->kbchar != INPUT_BROKER_MSG_FN_SYMBOL_ON)) { - screen->removeFunctionSymbol("Fn"); // remove modifier (function) symbol + destIndex = 0; + scrollIndex = 0; + // RESTORE THIS! + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + updateDestinationSelectionList(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; +} + +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for destination selector behavior + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; } } -#if defined(USE_VIRTUAL_KEYBOARD) - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - - if (keyTapped == "⇧") { - this->highlight = -1; - - this->payload = 0x00; - - validEvent = true; - - this->shift = !this->shift; - } else if (keyTapped == "⌫") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = 0x08; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "123" || keyTapped == "ABC") { - this->highlight = -1; - - this->payload = 0x00; - - this->charSet = this->charSet == 0 ? 1 : 0; - - validEvent = true; - } else if (keyTapped == " ") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = keyTapped[0]; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "↵") { - this->highlight = 0x00; - - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - - this->currentMessageIndex = event->kbchar - 1; - - validEvent = true; - - this->shift = false; - } else if (keyTapped != "") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]); - - validEvent = true; - - this->shift = false; + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != INPUT_BROKER_LEFT && + event->inputEvent != INPUT_BROKER_RIGHT && event->inputEvent != INPUT_BROKER_SELECT) { + this->searchQuery += (char)event->kbchar; + needsUpdate = true; + if ((millis() - lastFilterUpdate) > filterDebounceMs) { + runOnce(); // update filter immediately + lastFilterUpdate = millis(); } - } -#endif - - if (event->inputEvent == static_cast(MATRIXKEY)) { - // this will send the text immediately on matrix press - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - this->payload = MATRIXKEY; - this->currentMessageIndex = event->kbchar - 1; - this->lastTouchMillis = millis(); - validEvent = true; + return 1; } - if (validEvent) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + size_t numMeshNodes = filteredNodes.size(); + int totalEntries = numMeshNodes + activeChannelIndices.size(); + int columns = 1; + int totalRows = totalEntries; + int maxScrollIndex = std::max(0, totalRows - visibleRows); + scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); - // Let runOnce to be called immediately. - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - setIntervalFromNow(0); // on fast keypresses, this isn't fast enough. - } else { + // Handle backspace + if (event->inputEvent == INPUT_BROKER_BACK) { + if (searchQuery.length() > 0) { + searchQuery.remove(searchQuery.length() - 1); + needsUpdate = true; runOnce(); } + if (searchQuery.length() == 0) { + resetSearch(); + needsUpdate = false; + } + return 1; + } + + // UP + if (isUp && destIndex > 0) { + destIndex--; + if ((destIndex / columns) < scrollIndex) + scrollIndex = destIndex / columns; + else if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(); + return 1; + } + + // DOWN + if (isDown && destIndex + 1 < totalEntries) { + destIndex++; + if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + screen->forceDisplay(); + return 1; + } + + // SELECT + if (isSelect) { + if (destIndex < static_cast(activeChannelIndices.size())) { + dest = NODENUM_BROADCAST; + channel = activeChannelIndices[destIndex]; + } else { + int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { + const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; + if (selectedNode) { + dest = selectedNode->num; + channel = selectedNode->channel; + } + } + } + + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + screen->forceDisplay(); + return 1; + } + + // CANCEL + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + searchQuery = ""; + + // UIFrameEvent e; + // e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + // notifyObservers(&e); + screen->forceDisplay(); + return 1; + } + + return 0; +} + +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + // Override isDown and isSelect ONLY for canned message list behavior + if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + return false; + + // === Handle Cancel key: go inactive, clear UI state === + if (runState != CANNED_MESSAGE_RUN_STATE_INACTIVE && + (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + bool handled = false; + + // Handle up/down navigation + if (isUp && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + handled = true; + } else if (isDown && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + handled = true; + } else if (isSelect) { + const char *current = messages[currentMessageIndex]; + + // === [Select Destination] triggers destination selection UI === + if (strcmp(current, "[Select Destination]") == 0) { + returnToCannedList = true; + runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destIndex = 0; + scrollIndex = 0; + updateDestinationSelectionList(); // Make sure list is fresh + screen->forceDisplay(); + return true; + } + + // === [Exit] returns to the main/inactive screen === + if (strcmp(current, "[Exit]") == 0) { + // Set runState to inactive so we return to main UI + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + currentMessageIndex = -1; + + // Notify UI to regenerate frame set and redraw + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + // === [Free Text] triggers the free text input (virtual keyboard) === +#if defined(USE_VIRTUAL_KEYBOARD) + if (strcmp(current, "[-- Free Text --]") == 0) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return true; + } +#endif + + // Normal canned message selection + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + } else { + payload = runState; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + handled = true; + } + } + + if (handled) { + requestFocus(); + if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) + setIntervalFromNow(0); + else + runOnce(); + } + + return handled; +} +bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) +{ + // Always process only if in FREETEXT mode + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + return false; + +#if defined(USE_VIRTUAL_KEYBOARD) + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_LEFT) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + // Touch input (virtual keyboard) handling + // Only handle if touch coordinates present (CardKB won't set these) + if (event->touchX != 0 || event->touchY != 0) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; + + if (keyTapped == "⇧") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "⌫") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = keyTapped[0]; + shift = false; + valid = true; + } + // Touch enter/submit + else if (keyTapped == "↵") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!(keyTapped == "")) { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; + } + + if (valid) { + lastTouchMillis = millis(); + runOnce(); + payload = 0; + return true; // STOP: We handled a VKB touch + } + } +#endif // USE_VIRTUAL_KEYBOARD + + // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + + if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { + runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; + requestFocus(); + screen->forceDisplay(); + return true; + } + // Confirm select (Enter) + bool isSelect = isSelectEvent(event); + if (isSelect) { + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, + freetext.c_str()); + if (dest == 0) + dest = NODENUM_BROADCAST; + // Defensive: If channel isn't valid, pick the first available channel + if (channel >= channels.getNumChannels()) + channel = 0; + + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Backspace + if (event->inputEvent == INPUT_BROKER_BACK) { + payload = 0x08; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Move cursor left + if (event->inputEvent == INPUT_BROKER_LEFT) { + payload = INPUT_BROKER_LEFT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + // Move cursor right + if (event->inputEvent == INPUT_BROKER_RIGHT) { + payload = INPUT_BROKER_RIGHT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + // Cancel (dismiss freetext screen) + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } + + // Tab (switch destination) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { + return handleTabSwitch(event); // Reuse tab logic + } + + // Printable ASCII (add char to draft) + if (event->kbchar >= 32 && event->kbchar <= 126) { + payload = event->kbchar; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + return false; +} + +int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) +{ + int numEmotes = graphics::numEmotes; + + // Override isDown and isSelect ONLY for emote picker behavior + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + if (event->inputEvent == INPUT_BROKER_USER_PRESS) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + isSelect = true; + } + } + + // Scroll emote list + if (isUp && emotePickerIndex > 0) { + emotePickerIndex--; + screen->forceDisplay(); + return 1; + } + if (isDown && emotePickerIndex < numEmotes - 1) { + emotePickerIndex++; + screen->forceDisplay(); + return 1; + } + + // Select emote: insert into freetext at cursor and return to freetext + if (isSelect) { + String label = graphics::emotes[emotePickerIndex].label; + String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" + if (cursor == freetext.length()) { + freetext += emoteInsert; + } else { + freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); + } + cursor += emoteInsert.length(); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + + // Cancel returns to freetext + if (event->inputEvent == INPUT_BROKER_CANCEL || event->inputEvent == INPUT_BROKER_ALT_LONG) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; } return 0; @@ -404,278 +812,196 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { + // === Prepare packet === meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; + + // Save destination for ACK/NACK UI fallback + this->lastSentNode = dest; + this->incoming = dest; + + // Copy message payload p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + // Optionally add bell character if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character - p->decoded.payload.size++; + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate } - // Only receive routing messages when expecting ACK for a canned message - // Prevents the canned message module from regenerating the screen's frameset at unexpected times, - // or raising a UIFrameEvent before another module has the chance + // Mark as waiting for ACK to trigger ACK/NACK screen this->waitingForAck = true; + // Log outgoing message LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh( - p, RX_SRC_LOCAL, - true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs -} + // Send to mesh and phone (even if no phone connected, to track ACKs) + service->sendToMesh(p, RX_SRC_LOCAL, true); + // === Simulate local message to clear unread UI === + if (screen) { + meshtastic_MeshPacket simulatedPacket = {}; + simulatedPacket.from = 0; // Local device + screen->handleTextMessage(&simulatedPacket); + } + playComboTune(); +} int32_t CannedMessageModule::runOnce() { - if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { + updateDestinationSelectionList(); + needsUpdate = false; + } + + // If we're in node selection, do nothing except keep alive + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + return INACTIVATE_AFTER_MS; + } + + // Normal module disable/idle handling + if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; return INT32_MAX; } - // LOG_DEBUG("Check status"); + UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE)) { - // TODO: might have some feedback of sending state + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || + (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { - // Reset module - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + // Reset module on inactivity + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { - sendText(this->dest, indexChannels[this->channel], this->freetext.c_str(), true); + sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } else { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + return INT32_MAX; + } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { - powerFSM.trigger(EVENT_PRESS); return INT32_MAX; } else { -#if defined(USE_VIRTUAL_KEYBOARD) - sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); -#else - sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); -#endif + sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { - // LOG_DEBUG("Reset message is empty"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->notifyObservers(&e); return 2000; - } else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - this->currentMessageIndex = 0; - LOG_DEBUG("First touch (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + } + // Always highlight the first real canned message when entering the message list + else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + int firstRealMsgIdx = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 && + strcmp(this->messages[i], "[---- Free Text ----]") != 0) { + firstRealMsgIdx = i; + break; + } + } + this->currentMessageIndex = firstRealMsgIdx; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { - case INPUT_BROKER_MSG_LEFT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i > 0) ? nodeDB->getMeshNodeByIndex(i - 1)->num : nodeDB->getMeshNodeByIndex(numMeshNodes - 1)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == 0) { - this->channel = numChannels - 1; - } else { - this->channel--; - } - } else { - if (this->cursor > 0) { - this->cursor--; - } + case INPUT_BROKER_LEFT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { + this->cursor--; } break; - case INPUT_BROKER_MSG_RIGHT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i < numMeshNodes - 1) ? nodeDB->getMeshNodeByIndex(i + 1)->num : nodeDB->getMeshNodeByIndex(0)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == numChannels - 1) { - this->channel = 0; - } else { - this->channel++; - } - } else { - if (this->cursor < this->freetext.length()) { - this->cursor++; - } + case INPUT_BROKER_RIGHT: + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { + this->cursor++; } break; default: break; } if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the - // display back to the default window - case 0x08: // backspace - if (this->freetext.length() > 0 && this->highlight == 0x00) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + switch (this->payload) { + case 0x08: // backspace + if (this->freetext.length() > 0) { + if (this->cursor > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; } - this->cursor--; } break; - case 0x09: // tab - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL; - } else { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; - } - break; - case INPUT_BROKER_MSG_LEFT: - case INPUT_BROKER_MSG_RIGHT: - // already handled above - break; - // handle fn+s for shutdown - case INPUT_BROKER_MSG_SHUTDOWN: - if (screen) - screen->startAlert("Shutting down..."); - shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - break; - // and fn+r for reboot - case INPUT_BROKER_MSG_REBOOT: - if (screen) - screen->startAlert("Rebooting..."); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler + return 0; + case INPUT_BROKER_LEFT: + case INPUT_BROKER_RIGHT: break; default: - if (this->highlight != 0x00) { - break; - } - - if (this->cursor == this->freetext.length()) { - this->freetext += this->payload; - } else { - this->freetext = - this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); - } - - this->cursor += 1; - - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); - if (this->freetext.length() > maxChars) { - this->cursor = maxChars; - this->freetext = this->freetext.substring(0, maxChars); + // Only insert ASCII printable characters (32–126) + if (this->payload >= 32 && this->payload <= 126) { + if (this->cursor == this->freetext.length()) { + this->freetext += (char)this->payload; + } else { + this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + + this->freetext.substring(this->cursor); + } + this->cursor++; + uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } } break; } - if (screen) - screen->removeFunctionSymbol("Fn"); } - this->lastTouchMillis = millis(); this->notifyObservers(&e); return INACTIVATE_AFTER_MS; @@ -686,7 +1012,6 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } - return INT32_MAX; } @@ -709,29 +1034,21 @@ const char *CannedMessageModule::getMessageByIndex(int index) const char *CannedMessageModule::getNodeName(NodeNum node) { - if (node == NODENUM_BROADCAST) { + if (node == NODENUM_BROADCAST) return "Broadcast"; - } else { - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info != NULL) { - return info->user.long_name; - } else { - return "Unknown"; - } + + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user && strlen(info->user.long_name) > 0) { + return info->user.long_name; } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; } bool CannedMessageModule::shouldDraw() { - if (!moduleConfig.canned_message.enabled && !CANNED_MESSAGE_MODULE_ENABLE) { - return false; - } - - // If using "scan and select" input, don't draw the module frame just to say "disabled" - // The scanAndSelectInput class will draw its own temporary alert for user, when the input button is pressed - else if (scanAndSelectInput != nullptr && !hasMessages()) - return false; - return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE); } @@ -765,7 +1082,7 @@ void CannedMessageModule::showTemporaryMessage(const String &message) UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen notifyObservers(&e); - runState = CANNED_MESSAGE_RUN_STATE_MESSAGE; + runState = CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION; // run this loop again in 2 seconds, next iteration will clear the display setIntervalFromNow(2000); } @@ -983,188 +1300,656 @@ bool CannedMessageModule::interceptingKeyboardInput() } } -#if !HAS_TFT +// Draw the node/channel selection screen +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + requestFocus(); + display->setColor(WHITE); // Always draw cleanly + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === List Items === + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); + if (this->visibleRows < 1) + this->visibleRows = 1; + + // === Clamp scrolling === + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; + + for (int row = 0; row < visibleRows; row++) { + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) + break; + + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64] = ""; + + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + if (node->is_favorite) { + snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); + } else { + snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); + } + } + } + } + + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); + + // === Highlight background (if selected) === + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + } + + // === Draw entry text === + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + + // === Draw key icon (after highlight) === + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } + } + } + } + + // Scrollbar + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); + } +} + +void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height + const int headerMargin = 2; // Extra pixels below header + const int labelGap = 6; + const int bitmapGapX = 4; + + // Find max emote height (assume all same, or precalculated) + int maxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) + if (graphics::emotes[i].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[i].height; + + const int rowHeight = maxEmoteHeight + 2; + + // Place header at top, then compute start of emote list + int headerY = y; + int listTop = headerY + headerFontHeight + headerMargin; + + int visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int numEmotes = graphics::numEmotes; + + // Clamp highlight index + if (emotePickerIndex < 0) + emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) + emotePickerIndex = numEmotes - 1; + + // Determine which emote is at the top + int topIndex = emotePickerIndex - visibleRows / 2; + if (topIndex < 0) + topIndex = 0; + if (topIndex > numEmotes - visibleRows) + topIndex = std::max(0, numEmotes - visibleRows); + + // Draw header/title + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + + // Draw emote rows + display->setTextAlignment(TEXT_ALIGN_LEFT); + + for (int vis = 0; vis < visibleRows; ++vis) { + int emoteIdx = topIndex + vis; + if (emoteIdx >= numEmotes) + break; + const graphics::Emote &emote = graphics::emotes[emoteIdx]; + int rowY = listTop + vis * rowHeight; + + // Draw highlight box 2px taller than emote (1px margin above and below) + if (emoteIdx == emotePickerIndex) { + display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->setColor(BLACK); + } + + // Emote bitmap (left), 1px margin from highlight bar top + int emoteY = rowY + 1; + display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + + // Emote label (right of bitmap) + display->setFont(FONT_MEDIUM); + int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); + display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + + if (emoteIdx == emotePickerIndex) + display->setColor(WHITE); + } + + // Draw scrollbar if needed + if (numEmotes > visibleRows) { + int scrollbarHeight = visibleRows * rowHeight; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); + int scrollBarLen = std::max(6, (scrollbarHeight * visibleRows) / numEmotes); + int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; + display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); + } +} + void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + this->displayHeight = display->getHeight(); // Store display height for later use char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // === Draw temporary message if available === if (temporaryMessage.length() != 0) { requestFocus(); // Tell Screen::setFrames to move to our module's frame LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str()); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious + return; + } -#ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text -#else - display->setFont(FONT_MEDIUM); // Chunky text -#endif + // === Emote Picker Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + return; + } - String displayString; - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (this->ack) { - displayString = "Delivered to\n%s"; - } else { - displayString = "Delivery failed\nto %s"; - } - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, - cannedMessageModule->getNodeName(this->incoming)); + // === Destination Selection === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + drawDestinationSelectionScreen(display, state, x, y); + return; + } - display->setFont(FONT_SMALL); - - String snrString = "Last Rx SNR: %f"; - String rssiString = "Last Rx RSSI: %d"; - - // Don't bother drawing snr and rssi for tiny displays - if (display->getHeight() > 100) { - - // Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small - int16_t snrY = 100; - int16_t rssiY = 130; - - // If dislay is *slighly* too small for the original consants, squish up a bit - if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { - snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); - rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); - } - - if (this->ack) { - display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr); - display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi); - } - } - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - // E-Ink: clean the screen *after* this pop-up + // === ACK/NACK Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { + requestFocus(); EINK_ADD_FRAMEFLAG(display, COSMETIC); - - requestFocus(); // Tell Screen::setFrames to move to our module's frame + display->setTextAlignment(TEXT_ALIGN_CENTER); #ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text + display->setFont(FONT_SMALL); + int yOffset = y + 10; #else - display->setFont(FONT_MEDIUM); // Chunky text + display->setFont(FONT_MEDIUM); + int yOffset = y + 10; #endif + // --- Delivery Status Message --- + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel)); + } else if (this->lastAckHopLimit > this->lastAckHopStart) { + snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", this->lastAckHopLimit - this->lastAckHopStart, + getNodeName(this->incoming)); + } else { + snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming)); + } + } else { + snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); + } + + // Draw delivery message and compute y-offset after text height + int lineCount = 1; + for (const char *ptr = buffer; *ptr; ptr++) { + if (*ptr == '\n') + lineCount++; + } + + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding + +#ifndef USE_EINK + // --- SNR + RSSI Compact Line --- + if (this->ack) { + display->setFont(FONT_SMALL); + snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + } +#endif + + return; + } + + // === Sending Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { + EINK_ADD_FRAMEFLAG(display, COSMETIC); + requestFocus(); +#ifdef USE_EINK + display->setFont(FONT_SMALL); +#else + display->setFont(FONT_MEDIUM); +#endif display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + return; + } + + // === Disabled Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame + return; + } + + // === Free Text Input Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) EInkDynamicDisplay *einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing + einkDisplay->enableUnlimitedFastMode(); #endif - #if defined(USE_VIRTUAL_KEYBOARD) drawKeyboard(display, state, 0, 0); #else - display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - switch (this->destSelect) { - case CANNED_MESSAGE_DESTINATION_TYPE_NODE: - display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - break; - case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: - display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - break; - default: - if (display->getWidth() > 128) { - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - } else { - display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); - } - break; - } - // used chars right aligned, only when not editing the destination - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + + // --- Draw node/channel header at the top --- + drawHeader(display, x, y, buffer); + + // --- Char count right-aligned --- + if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } + + // --- Draw Free Text input with multi-emote support and proper line wrapping --- display->setColor(WHITE); - display->drawStringMaxWidth( - 0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), - cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); -#endif - } else { - if (this->messagesCount > 0) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest)); - int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; - if (lines == 3) { - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); - if (this->messagesCount > 1) { - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); + { + int inputY = 0 + y + FONT_HEIGHT_SMALL; + String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); + + // Tokenize input into (isEmote, token) pairs + std::vector> tokens; + const char *msg = msgWithCursor.c_str(); + int msgLen = strlen(msg); + int pos = 0; + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } } - } else { - int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; - for (int i = 0; i < std::min(messagesCount, lines); i++) { - if (i == currentMessageIndex - topMsg) { -#ifdef USE_EINK - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); - display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getCurrentMessage()); -#else - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), - y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); -#endif - } else if (messagesCount > 1) { // Only draw others if there are multiple messages - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getMessageByIndex(topMsg + i)); + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; } } } + + // ===== Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) ===== + std::vector>> lines; + std::vector> currentLine; + int lineWidth = 0; + int maxWidth = display->getWidth(); + for (auto &token : tokens) { + if (token.first) { + // Emote + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; + } + } + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside word if needed + String text = token.second; + uint16_t pos = 0; + while (pos < text.length()) { + // Find next space (or end) + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + // If word itself too big, split by character + if (wordWidth > maxWidth) { + uint16_t charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } + } + } + if (!currentLine.empty()) + lines.push_back(currentLine); + + // Draw lines with emotes + int rowHeight = FONT_HEIGHT_SMALL; + int yLine = inputY; + for (auto &line : lines) { + int nextX = x; + for (auto &token : line) { + if (token.first) { + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + display->drawString(nextX, yLine, token.second); + nextX += display->getStringWidth(token.second); + } + } + yLine += rowHeight; + } + } +#endif + return; + } + + // === Canned Messages List === + if (this->messagesCount > 0) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // ====== Precompute per-row heights based on emotes (centered if present) ====== + const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; + + int topMsg; + std::vector rowHeights; + int visibleRows; + + // Draw header (To: ...) + drawHeader(display, x, y, buffer); + + // Shift message list upward by 3 pixels to reduce spacing between header and first message + const int listYOffset = y + FONT_HEIGHT_SMALL - 3; + visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; + + // Figure out which messages are visible and their needed heights + topMsg = + (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0; + int countRows = std::min(messagesCount, visibleRows); + + // --- Build per-row max height based on all emotes in line --- + for (int i = 0; i < countRows; i++) { + const char *msg = getMessageByIndex(topMsg + i); + int maxEmoteHeight = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *search = msg; + while ((search = strstr(search, label))) { + if (graphics::emotes[j].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[j].height; + search += strlen(label); // Advance past this emote + } + } + rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); + } + + // --- Draw all message rows with multi-emote support --- + int yCursor = listYOffset; + for (int vis = 0; vis < countRows; vis++) { + int msgIdx = topMsg + vis; + int lineY = yCursor; + const char *msg = getMessageByIndex(msgIdx); + int rowHeight = rowHeights[vis]; + bool highlight = (msgIdx == currentMessageIndex); + + // --- Multi-emote tokenization --- + std::vector> tokens; // (isEmote, token) + int pos = 0; + int msgLen = strlen(msg); + while (pos < msgLen) { + const graphics::Emote *foundEmote = nullptr; + int foundLen = 0; + + // Look for any emote label at this pos (prefer longest match) + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) + continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } + } + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char *label = graphics::emotes[j].label; + if (label[0] == 0) + continue; + const char *found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } + } + } + // --- End multi-emote tokenization --- + + // Vertically center based on rowHeight + int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; + +#ifdef USE_EINK + int nextX = x + (highlight ? 12 : 0); + if (highlight) + display->drawString(x + 0, lineY + textYOffset, ">"); +#else + int scrollPadding = 8; + if (highlight) { + display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); + display->setColor(BLACK); + } + int nextX = x + (highlight ? 2 : 0); +#endif + + // Draw all tokens left to right + for (auto &token : tokens) { + if (token.first) { + // Emote + const graphics::Emote *emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + // Text + display->drawString(nextX, lineY + textYOffset, token.second); + nextX += display->getStringWidth(token.second); + } + } +#ifndef USE_EINK + if (highlight) + display->setColor(WHITE); +#endif + + yCursor += rowHeight; + } + + // Scrollbar + if (messagesCount > visibleRows) { + int scrollHeight = display->getHeight() - listYOffset; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); + int barHeight = (scrollHeight * visibleRows) / messagesCount; + int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; + display->fillRect(scrollTrackX, scrollPos, 4, barHeight); } } } -#endif //! HAS_TFT ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { - // look for a request_id if (mp.decoded.request_id != 0) { + // Trigger screen refresh for ACK/NACK feedback UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + requestFocus(); this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); + + // Decode the routing response meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; - waitingForAck = false; // No longer want routing packets + + // Track hop metadata + this->lastAckWasRelayed = (mp.hop_limit != mp.hop_start); + this->lastAckHopStart = mp.hop_start; + this->lastAckHopLimit = mp.hop_limit; + + // Determine ACK status + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + + // Identify the responding node + if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; // Relayed by another node + } else { + this->incoming = this->lastSentNode; // Direct reply + } + + // Final ACK confirmation logic + this->ack = isAck && (wasBroadcast || isFromDest); + + waitingForAck = false; this->notifyObservers(&e); - // run the next time 2 seconds later - setIntervalFromNow(2000); + setIntervalFromNow(3000); // Time to show ACK/NACK screen } } @@ -1206,7 +1991,7 @@ bool CannedMessageModule::saveProtoForModule() */ void CannedMessageModule::installDefaultCannedMessageModuleConfig() { - memset(cannedMessageModuleConfig.messages, 0, sizeof(cannedMessageModuleConfig.messages)); + strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); } /** @@ -1276,4 +2061,4 @@ String CannedMessageModule::drawWithCursor(String text, int cursor) return result; } -#endif +#endif \ No newline at end of file diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index a91933a0f..55a0a1185 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -3,27 +3,38 @@ #include "ProtobufModule.h" #include "input/InputBroker.h" +// ============================ +// Enums & Defines +// ============================ + enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_DISABLED, CANNED_MESSAGE_RUN_STATE_INACTIVE, CANNED_MESSAGE_RUN_STATE_ACTIVE, - CANNED_MESSAGE_RUN_STATE_FREETEXT, CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, - CANNED_MESSAGE_RUN_STATE_MESSAGE, CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, -}; - -enum cannedMessageDestinationType { - CANNED_MESSAGE_DESTINATION_TYPE_NONE, - CANNED_MESSAGE_DESTINATION_TYPE_NODE, - CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, + CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; +#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 +#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 + +#ifndef CANNED_MESSAGE_MODULE_ENABLE +#define CANNED_MESSAGE_MODULE_ENABLE 0 +#endif + +// ============================ +// Data Structures +// ============================ + struct Letter { String character; float width; @@ -33,71 +44,72 @@ struct Letter { int rectHeight; }; -#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 -/** - * Sum of CannedMessageModuleConfig part sizes. - */ -#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; +}; -#ifndef CANNED_MESSAGE_MODULE_ENABLE -#define CANNED_MESSAGE_MODULE_ENABLE 0 -#endif +// ============================ +// Main Class +// ============================ class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { - CallbackObserver inputObserver = - CallbackObserver(this, &CannedMessageModule::handleInputEvent); - public: CannedMessageModule(); + + void LaunchWithDestination(NodeNum, uint8_t newChannel = 0); + void LaunchFreetextWithDestination(NodeNum, uint8_t newChannel = 0); + + // === Emote Picker navigation === + int emotePickerIndex = 0; // Tracks currently selected emote in the picker + + // === Message navigation === const char *getCurrentMessage(); const char *getPrevMessage(); const char *getNextMessage(); const char *getMessageByIndex(int index); const char *getNodeName(NodeNum node); + + // === State/UI === bool shouldDraw(); bool hasMessages(); - // void eventUp(); - // void eventDown(); - // void eventSelect(); + void showTemporaryMessage(const String &message); + void resetSearch(); + void updateDestinationSelectionList(); + void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + bool isCharInputAllowed() const; + String drawWithCursor(String text, int cursor); + // === Emote Picker === + int handleEmotePickerInput(const InputEvent *event); + void drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + // === Admin Handlers === void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); void handleSetCannedMessageModuleMessages(const char *from_msg); - void showTemporaryMessage(const String &message); - - String drawWithCursor(String text, int cursor); - #ifdef RAK14014 cannedMessageModuleRunState getRunState() const { return runState; } #endif - /* - -Override the wantPacket method. We need the Routing Messages to look for ACKs. - */ + // === Packet Interest Filter === virtual bool wantPacket(const meshtastic_MeshPacket *p) override { - if (p->rx_rssi != 0) { - this->lastRxRssi = p->rx_rssi; - } - - if (p->rx_snr > 0) { - this->lastRxSnr = p->rx_snr; - } - - switch (p->decoded.portnum) { - case meshtastic_PortNum_ROUTING_APP: - return waitingForAck; - default: - return false; - } + if (p->rx_rssi != 0) + lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) + lastRxSnr = p->rx_snr; + return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; } protected: + // === Thread Entry Point === virtual int32_t runOnce() override; + // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); @@ -105,58 +117,87 @@ class CannedMessageModule : public SinglePortModule, public ObservableshouldDraw(); } + virtual bool wantUIFrame() override { return shouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } virtual bool interceptingKeyboardInput() override; -#if !HAS_TFT virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; -#endif virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; - /** Called to handle a particular incoming message - * @return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered - * for it - */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; void loadProtoForModule(); bool saveProtoForModule(); - void installDefaultCannedMessageModuleConfig(); - int currentMessageIndex = -1; - cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - char payload = 0x00; - unsigned int cursor = 0; - String freetext = ""; // Text Buffer for Freetext Editor - NodeNum dest = NODENUM_BROADCAST; - ChannelIndex channel = 0; - cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - uint8_t numChannels = 0; - ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; - NodeNum incoming = NODENUM_BROADCAST; - bool ack = false; // True means ACK, false means NAK (error_reason != NONE) - bool waitingForAck = false; // Are currently interested in routing packets? - float lastRxSnr = 0; - int32_t lastRxRssi = 0; + private: + // === Input Observers === + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); + // === Display and UI === + int displayHeight = 64; + int destIndex = 0; + int scrollIndex = 0; + int visibleRows = 0; + bool needsUpdate = true; + unsigned long lastUpdateMillis = 0; + String searchQuery; + String freetext; + String temporaryMessage; + + // === Message Storage === char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; int messagesCount = 0; + int currentMessageIndex = -1; + + // === Routing & Acknowledgment === + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display) + ChannelIndex channel = 0; // Channel index used when sending a message + + bool ack = false; // True = ACK received, False = NACK or failed + bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets + bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes + uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet + uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet + + float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI) + int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI) + + // === State Tracking === + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + char highlight = 0x00; + char payload = 0x00; + unsigned int cursor = 0; unsigned long lastTouchMillis = 0; - String temporaryMessage; + uint32_t lastFilterUpdate = 0; + static constexpr uint32_t filterDebounceMs = 30; + std::vector activeChannelIndices; + std::vector filteredNodes; + +#if defined(USE_VIRTUAL_KEYBOARD) + bool shift = false; + int charSet = 0; // 0=ABC, 1=123 +#endif + + bool isUpEvent(const InputEvent *event); + bool isDownEvent(const InputEvent *event); + bool isSelectEvent(const InputEvent *event); + bool handleTabSwitch(const InputEvent *event); + int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleFreeTextInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index dc17460f6..956508ce5 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -188,7 +188,7 @@ int32_t ExternalNotificationModule::runOnce() // Play RTTTL over i2s audio interface if enabled as buzzer #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { + if (moduleConfig.external_notification.use_i2s_as_buzzer && canBuzz()) { if (audioThread->isPlaying()) { // Continue playing } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -197,7 +197,7 @@ int32_t ExternalNotificationModule::runOnce() } #endif // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio) { + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { if (rtttl::isPlaying()) { rtttl::play(); } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -210,6 +210,18 @@ int32_t ExternalNotificationModule::runOnce() } } +/** + * Based on buzzer mode, return true if we can buzz. + */ +bool ExternalNotificationModule::canBuzz() +{ + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + return true; + } + return false; +} + bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); @@ -293,6 +305,12 @@ bool ExternalNotificationModule::getExternal(uint8_t index) return externalCurrentState[index]; } +// Allow other firmware components to determine whether a notification is ongoing +bool ExternalNotificationModule::nagging() +{ + return isNagging; +} + void ExternalNotificationModule::stopNow() { rtttl::stop(); @@ -338,6 +356,9 @@ ExternalNotificationModule::ExternalNotificationModule() // moduleConfig.external_notification.alert_message_buzzer = true; if (moduleConfig.external_notification.enabled) { + if (inputBroker) // put our callback in the inputObserver list + inputObserver.observe(inputBroker); + if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); @@ -364,7 +385,7 @@ ExternalNotificationModule::ExternalNotificationModule() setExternalState(1, false); externalTurnedOn[1] = 0; } - if (moduleConfig.external_notification.output_buzzer) { + if (moduleConfig.external_notification.output_buzzer && canBuzz()) { if (!moduleConfig.external_notification.use_pwm) { LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); @@ -454,7 +475,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_bell_buzzer) { + if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { if (containsBell) { LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); isNagging = true; @@ -583,4 +604,13 @@ void ExternalNotificationModule::handleSetRingtone(const char *from_msg) if (changed) { nodeDB->saveProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, &meshtastic_RTTTLConfig_msg, &rtttlConfig); } +} + +int ExternalNotificationModule::handleInputEvent(const InputEvent *event) +{ + if (nagCycleCutoff != UINT32_MAX) { + stopNow(); + return 1; + } + return 0; } \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 841ca6de9..19cf9eb7b 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -3,6 +3,8 @@ #include "SinglePortModule.h" #include "concurrency/OSThread.h" #include "configuration.h" +#include "input/InputBroker.h" + #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !defined(CONFIG_IDF_TARGET_ESP32C6) #include #else @@ -27,11 +29,15 @@ class rtttl */ class ExternalNotificationModule : public SinglePortModule, private concurrency::OSThread { + CallbackObserver inputObserver = + CallbackObserver(this, &ExternalNotificationModule::handleInputEvent); uint32_t output = 0; public: ExternalNotificationModule(); + int handleInputEvent(const InputEvent *arg); + uint32_t nagCycleCutoff = 1; void setExternalState(uint8_t index = 0, bool on = false); @@ -40,6 +46,9 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: void setMute(bool mute) { isMuted = mute; } bool getMute() { return isMuted; } + bool canBuzz(); + bool nagging(); + void stopNow(); void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp new file mode 100644 index 000000000..f5a9f2359 --- /dev/null +++ b/src/modules/KeyVerificationModule.cpp @@ -0,0 +1,310 @@ +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#include "MeshService.h" +#include "RTC.h" +#include "main.h" +#include "modules/AdminModule.h" +#include + +KeyVerificationModule *keyVerificationModule; + +KeyVerificationModule::KeyVerificationModule() + : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) +{ + ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; +} + +AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + updateState(); + if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { + LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); + + if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && + currentState == KEY_VERIFICATION_IDLE) { + sendInitialRequest(request->key_verification.remote_nodenum); + + } else if (request->key_verification.message_type == + meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && + request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && + request->key_verification.nonce == currentNonce) { + processSecurityNumber(request->key_verification.security_number); + + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && + request->key_verification.nonce == currentNonce) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { + resetToIdle(); + } + return AdminMessageHandleResult::HANDLED; + } + return AdminMessageHandleResult::NOT_HANDLED; +} + +bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) +{ + updateState(); + if (mp.pki_encrypted == false) + return false; + if (mp.from != currentRemoteNode) // because the inital connection request is handled in allocReply() + return false; + if (currentState == KEY_VERIFICATION_IDLE) { + return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() + + } else if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && + r->hash1.size == 0) { + memcpy(hash2, r->hash2.bytes, 32); + if (screen) + screen->showOverlayBanner("Enter Security Number", 30000); + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Enter Security Number for Key Verification"); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; + cn->payload_variant.key_verification_number_request.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + service->sendClientNotification(cn); + LOG_INFO("Received hash2"); + currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; + return true; + + } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { + if (memcmp(hash1, r->hash1.bytes, 32) == 0) { + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); + sprintf(message + 24, "\nACCEPT\nREJECT"); + LOG_INFO("Hash1 matches!"); + if (screen) { + screen->showOverlayBanner(message, 30000, 2, [=](int selected) { + if (selected == 0) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + } + }); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = false; + service->sendClientNotification(cn); + + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; + return true; + } + } + return false; +} + +bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) +{ + LOG_DEBUG("keyVerification start"); + // generate nonce + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { + return false; + } + currentNonce = random(); + currentNonceTimestamp = getTime(); + currentRemoteNode = remoteNode; + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 0; + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = remoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + + currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + return true; +} + +meshtastic_MeshPacket *KeyVerificationModule::allocReply() +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period + LOG_WARN("Key Verification requested, but already in a request"); + return nullptr; + } else if (!currentRequest->pki_encrypted) { + LOG_WARN("Key Verification requested, but not in a PKI packet"); + return nullptr; + } + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; + + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_KeyVerification scratch; + meshtastic_KeyVerification response; + meshtastic_MeshPacket *responsePacket = nullptr; + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); + + currentNonce = scratch.nonce; + response.nonce = scratch.nonce; + currentRemoteNode = req.from; + currentNonceTimestamp = getTime(); + currentSecurityNumber = random(1, 999999); + + // generate hash1 + hash.reset(); + hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); + hash.update(owner.public_key.bytes, owner.public_key.size); + hash.finalize(hash1, 32); + + // generate hash2 + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(hash2, 32); + response.hash1.size = 0; + response.hash2.size = 32; + memcpy(response.hash2.bytes, hash2, 32); + + responsePacket = allocDataProtobuf(response); + + responsePacket->pki_encrypted = true; + if (screen) { + snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + screen->showOverlayBanner(message, 30000); + LOG_WARN("%s", message); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, + currentSecurityNumber % 1000); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; + cn->payload_variant.key_verification_number_inform.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; + service->sendClientNotification(cn); + LOG_WARN("Security Number %04u, nonce %llu", currentSecurityNumber, currentNonce); + return responsePacket; +} + +void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + uint8_t scratch_hash[32] = {0}; + LOG_WARN("received security number: %u", incomingNumber); + meshtastic_NodeInfoLite *remoteNodePtr = nullptr; + remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { + currentState = KEY_VERIFICATION_IDLE; + return; // should we throw an error here? + } + LOG_WARN("hashing "); + // calculate hash1 + hash.reset(); + hash.update(&incomingNumber, sizeof(incomingNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(owner.public_key.bytes, owner.public_key.size); + + hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.finalize(hash1, 32); + + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(scratch_hash, 32); + + if (memcmp(scratch_hash, hash2, 32) != 0) { + LOG_WARN("Hash2 did not match"); + return; // should probably throw an error of some sort + } + currentSecurityNumber = incomingNumber; + + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 32; + memcpy(KeyVerification.hash1.bytes, hash1, 32); + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = currentRemoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); // send the toPhone packet + if (screen) { + screen->showOverlayBanner(message, 30000); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = true; + service->sendClientNotification(cn); + LOG_INFO(message); + + return; +} + +void KeyVerificationModule::updateState() +{ + if (currentState != KEY_VERIFICATION_IDLE) { + // check for the 30 second timeout + if (currentNonceTimestamp < getTime() - 60) { + resetToIdle(); + } else { + currentNonceTimestamp = getTime(); + } + } +} + +void KeyVerificationModule::resetToIdle() +{ + memset(hash1, 0, 32); + memset(hash2, 0, 32); + currentNonce = 0; + currentNonceTimestamp = 0; + currentSecurityNumber = 0; + currentRemoteNode = 0; + currentState = KEY_VERIFICATION_IDLE; +} + +void KeyVerificationModule::generateVerificationCode(char *readableCode) +{ + for (int i = 0; i < 4; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } + readableCode[4] = ' '; + for (int i = 5; i < 9; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } +} +#endif \ No newline at end of file diff --git a/src/modules/KeyVerificationModule.h b/src/modules/KeyVerificationModule.h new file mode 100644 index 000000000..f659e961a --- /dev/null +++ b/src/modules/KeyVerificationModule.h @@ -0,0 +1,64 @@ +#pragma once + +#include "ProtobufModule.h" +#include "SinglePortModule.h" + +enum KeyVerificationState { + KEY_VERIFICATION_IDLE, + KEY_VERIFICATION_SENDER_HAS_INITIATED, + KEY_VERIFICATION_SENDER_AWAITING_NUMBER, + KEY_VERIFICATION_SENDER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, +}; + +class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // +{ + // CallbackObserver nodeStatusObserver = + // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); + + public: + KeyVerificationModule(); + /* : concurrency::OSThread("KeyVerification"), + ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) + { + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + }*/ + virtual bool wantUIFrame() { return false; }; + bool sendInitialRequest(NodeNum remoteNode); + + protected: + /* Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); + // virtual meshtastic_MeshPacket *allocReply() override; + + // rather than add to the craziness that is the admin module, just handle those requests here. + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + /* + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + virtual meshtastic_MeshPacket *allocReply() override; + + private: + uint64_t currentNonce = 0; + uint32_t currentNonceTimestamp = 0; + NodeNum currentRemoteNode = 0; + uint32_t currentSecurityNumber = 0; + KeyVerificationState currentState = KEY_VERIFICATION_IDLE; + uint8_t hash1[32] = {0}; // + uint8_t hash2[32] = {0}; // + char message[40] = {0}; + + void processSecurityNumber(uint32_t); + void updateState(); // check the timeouts and maybe reset the state to idle + void resetToIdle(); // Zero out module state + void generateVerificationCode(char *); // fills char with the user readable verification code +}; + +extern KeyVerificationModule *keyVerificationModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 442b286c9..9f74a0119 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -1,17 +1,21 @@ #include "configuration.h" #if !MESHTASTIC_EXCLUDE_INPUTBROKER +#include "buzz/BuzzerFeedbackThread.h" #include "input/ExpressLRSFiveWay.h" #include "input/InputBroker.h" #include "input/RotaryEncoderInterruptImpl1.h" -#include "input/ScanAndSelect.h" #include "input/SerialKeyboardImpl.h" #include "input/TrackballInterruptImpl1.h" #include "input/UpDownInterruptImpl1.h" +#include "modules/SystemCommandsModule.h" #if !MESHTASTIC_EXCLUDE_I2C #include "input/cardKbI2cImpl.h" #endif #include "input/kbMatrixImpl.h" #endif +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#endif #if !MESHTASTIC_EXCLUDE_ADMIN #include "modules/AdminModule.h" #endif @@ -62,6 +66,7 @@ #include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" +#include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" @@ -100,7 +105,11 @@ void setupModules() { if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - inputBroker = new InputBroker(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + inputBroker = new InputBroker(); + systemCommandsModule = new SystemCommandsModule(); + buzzerFeedbackThread = new BuzzerFeedbackThread(); + } #endif #if !MESHTASTIC_EXCLUDE_ADMIN adminModule = new AdminModule(); @@ -129,7 +138,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_ATAK atakPluginModule = new AtakPluginModule(); #endif - +#if !MESHTASTIC_EXCLUDE_PKI + keyVerificationModule = new KeyVerificationModule(); +#endif #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif @@ -148,50 +159,49 @@ void setupModules() // Example: Put your module here // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } -#if HAS_SCREEN - // In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class - scanAndSelectInput = new ScanAndSelectInput(); - if (!scanAndSelectInput->init()) { - delete scanAndSelectInput; - scanAndSelectInput = nullptr; - } -#endif - - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); #ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE #ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE + } #endif // HAS_BUTTON -#if ARCH_PORTDUINO && !HAS_TFT - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } #endif -#if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(); +#if !MESHTASTIC_EXCLUDE_INPUTBROKER + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(TB_DOWN, TB_UP, TB_LEFT, TB_RIGHT, TB_PRESS); + } #endif #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES - cannedMessageModule = new CannedMessageModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + cannedMessageModule = new CannedMessageModule(); + } #endif #if ARCH_PORTDUINO new HostMetricsModule(); @@ -217,7 +227,9 @@ void setupModules() #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL - new SerialModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + new SerialModule(); + } #endif #endif #ifdef ARCH_ESP32 diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index 5142f2db0..cf9940e25 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -14,17 +14,13 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes { auto p = *pptr; + // Coerce user.id to be derived from the node number + snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp)); + bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel); bool wasBroadcast = isBroadcast(mp.to); - // Show new nodes on LCD screen - if (wasBroadcast) { - String lcd = String("Joined: ") + p.long_name + "\n"; - if (screen) - screen->print(lcd.c_str()); - } - // if user has changed while packet was not for us, inform phone if (hasChanged && !wasBroadcast && !isToUs(&mp)) service->sendToPhone(packetPool.allocCopy(mp)); diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 0b1bdcc46..93c65ecc1 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -265,7 +265,6 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() } LOG_INFO("Position packet: time=%i lat=%i lon=%i", p.time, p.latitude_i, p.longitude_i); - lastSentToMesh = millis(); // TAK Tracker devices should send their position in a TAK packet over the ATAK port if (config.device.role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) @@ -276,13 +275,18 @@ meshtastic_MeshPacket *PositionModule::allocPositionPacket() meshtastic_MeshPacket *PositionModule::allocReply() { - if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentToMesh && - Throttle::isWithinTimespanMs(lastSentToMesh, 3 * 60 * 1000)) { - LOG_DEBUG("Skip Position reply since we sent it <3min ago"); + if (config.device.role != meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND && lastSentReply && + Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) { + LOG_DEBUG("Skip Position reply since we sent a reply <3min ago"); ignoreRequest = true; // Mark it as ignored for MeshModule return nullptr; } - return allocPositionPacket(); + + meshtastic_MeshPacket *reply = allocPositionPacket(); + if (reply) { + lastSentReply = millis(); // Track when we sent this reply + } + return reply; } meshtastic_MeshPacket *PositionModule::allocAtakPli() @@ -328,7 +332,13 @@ void PositionModule::sendOurPosition() // If we changed channels, ask everyone else for their latest info LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); - sendOurPosition(NODENUM_BROADCAST, requestReplies); + for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { + if (channels.getByIndex(channelNum).settings.has_module_settings && + channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { + sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); + return; + } + } } void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) @@ -340,11 +350,6 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha // Set's the class precision value for this particular packet if (channels.getByIndex(channel).settings.has_module_settings) { precision = channels.getByIndex(channel).settings.module_settings.position_precision; - } else if (channels.getByIndex(channel).role == meshtastic_Channel_Role_PRIMARY) { - // backwards compatibility for Primary channels created before position_precision was set by default - precision = 13; - } else { - precision = 0; } meshtastic_MeshPacket *p = allocPositionPacket(); diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index dc732a3db..b9fd527c9 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -63,7 +63,7 @@ class PositionModule : public ProtobufModule, private concu void sendLostAndFoundText(); bool hasQualityTimesource(); bool hasGPS(); - uint32_t lastSentToMesh = 0; // Last time we sent our position to the mesh + uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only) const uint32_t minimumTimeThreshold = Default::getConfiguredOrDefaultMs(config.position.broadcast_smart_minimum_interval_secs, 30); diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp index 9bc8512b6..04cfeb651 100644 --- a/src/modules/RemoteHardwareModule.cpp +++ b/src/modules/RemoteHardwareModule.cpp @@ -83,9 +83,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r switch (p.type) { case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { - // Print notification to LCD screen - screen->print("Write GPIOs\n"); - pinModes(p.gpio_mask, OUTPUT, availablePins); for (uint8_t i = 0; i < NUM_GPIOS; i++) { uint64_t mask = 1ULL << i; @@ -98,10 +95,6 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r } case meshtastic_HardwareMessage_Type_READ_GPIOS: { - // Print notification to LCD screen - if (screen) - screen->print("Read GPIOs\n"); - uint64_t res = digitalReads(p.gpio_mask, availablePins); // Send the reply diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index c4f63c6b1..8892aaa97 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -15,8 +15,6 @@ meshtastic_MeshPacket *ReplyModule::allocReply() LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif - screen->print("Send reply\n"); - const char *replyStr = "Message Received"; auto reply = allocDataPacket(); // Allocate a packet for sending reply->decoded.payload.size = strlen(replyStr); // You must specify how many bytes are in the reply diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 8d280581c..f3921ef19 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -341,7 +341,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp serialPrint->write(p.payload.bytes, p.payload.size); } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - String sender = (node && node->has_user) ? node->user.short_name : "???"; + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; serialPrint->println(); serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->println(); @@ -410,8 +410,8 @@ uint32_t SerialModule::getBaudRate() // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { - String name; - String value; + char name[64]; + char value[128]; }; /** @@ -438,16 +438,30 @@ ParsedLine parseLine(const char *line) strncpy(nameBuf, line, nameLen); nameBuf[nameLen] = '\0'; - // Create trimmed name string - String name = String(nameBuf); - name.trim(); + // Trim whitespace from name + char *nameStart = nameBuf; + while (*nameStart && isspace(*nameStart)) + nameStart++; + char *nameEnd = nameStart + strlen(nameStart) - 1; + while (nameEnd > nameStart && isspace(*nameEnd)) + *nameEnd-- = '\0'; - // Extract value after equals sign - String value = String(equals + 1); - value.trim(); + // Copy trimmed name + strncpy(result.name, nameStart, sizeof(result.name) - 1); + result.name[sizeof(result.name) - 1] = '\0'; + + // Extract value part (after equals) + const char *valueStart = equals + 1; + while (*valueStart && isspace(*valueStart)) + valueStart++; + strncpy(result.value, valueStart, sizeof(result.value) - 1); + result.value[sizeof(result.value) - 1] = '\0'; + + // Trim trailing whitespace from value + char *valueEnd = result.value + strlen(result.value) - 1; + while (valueEnd > result.value && isspace(*valueEnd)) + *valueEnd-- = '\0'; - result.name = name; - result.value = value; return result; } @@ -517,16 +531,16 @@ void SerialModule::processWXSerial() memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); ParsedLine parsed = parseLine(line); - if (parsed.name.length() > 0) { - if (parsed.name == "WindDir") { - strlcpy(windDir, parsed.value.c_str(), sizeof(windDir)); + if (strlen(parsed.name) > 0) { + if (strcmp(parsed.name, "WindDir") == 0) { + strlcpy(windDir, parsed.value, sizeof(windDir)); double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); dir_sum_sin += sin(radians); dir_sum_cos += cos(radians); dirCount++; gotwind = true; - } else if (parsed.name == "WindSpeed") { - strlcpy(windVel, parsed.value.c_str(), sizeof(windVel)); + } else if (strcmp(parsed.name, "WindSpeed") == 0) { + strlcpy(windVel, parsed.value, sizeof(windVel)); float newv = strtof(windVel, nullptr); velSum += newv; velCount++; @@ -534,28 +548,28 @@ void SerialModule::processWXSerial() lull = newv; } gotwind = true; - } else if (parsed.name == "WindGust") { - strlcpy(windGust, parsed.value.c_str(), sizeof(windGust)); + } else if (strcmp(parsed.name, "WindGust") == 0) { + strlcpy(windGust, parsed.value, sizeof(windGust)); float newg = strtof(windGust, nullptr); if (newg > gust) { gust = newg; } gotwind = true; - } else if (parsed.name == "BatVoltage") { - strlcpy(batVoltage, parsed.value.c_str(), sizeof(batVoltage)); + } else if (strcmp(parsed.name, "BatVoltage") == 0) { + strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); batVoltageF = strtof(batVoltage, nullptr); break; // last possible data we want so break - } else if (parsed.name == "CapVoltage") { - strlcpy(capVoltage, parsed.value.c_str(), sizeof(capVoltage)); + } else if (strcmp(parsed.name, "CapVoltage") == 0) { + strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); capVoltageF = strtof(capVoltage, nullptr); - } else if (parsed.name == "GXTS04Temp" || parsed.name == "Temperature") { - strlcpy(temperature, parsed.value.c_str(), sizeof(temperature)); + } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { + strlcpy(temperature, parsed.value, sizeof(temperature)); temperatureF = strtof(temperature, nullptr); - } else if (parsed.name == "RainIntSum") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "RainIntSum") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rainSum = int(strtof(rainStr, nullptr)); - } else if (parsed.name == "Rain") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "Rain") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rain = strtof(rainStr, nullptr); } } diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp new file mode 100644 index 000000000..a6b01d68a --- /dev/null +++ b/src/modules/SystemCommandsModule.cpp @@ -0,0 +1,118 @@ +#include "SystemCommandsModule.h" +#include "meshUtils.h" +#if HAS_SCREEN +#include "graphics/Screen.h" +#include "graphics/SharedUIDisplay.h" +#endif +#include "GPS.h" +#include "MeshService.h" +#include "Module.h" +#include "NodeDB.h" +#include "main.h" +#include "modules/AdminModule.h" +#include "modules/ExternalNotificationModule.h" + +SystemCommandsModule *systemCommandsModule; + +SystemCommandsModule::SystemCommandsModule() +{ + if (inputBroker) + inputObserver.observe(inputBroker); +} + +int SystemCommandsModule::handleInputEvent(const InputEvent *event) +{ + LOG_INFO("Input event %u! kb %u", event->inputEvent, event->kbchar); + // System commands (all others fall through) + switch (event->kbchar) { + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + IF_SCREEN(screen->setFunctionSymbol("Fn")); + return 0; + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + IF_SCREEN(screen->removeFunctionSymbol("Fn")); + return 0; + // Brightness + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + IF_SCREEN(screen->increaseBrightness()); + LOG_DEBUG("Increase Screen Brightness"); + return 0; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + IF_SCREEN(screen->decreaseBrightness()); + LOG_DEBUG("Decrease Screen Brightness"); + return 0; + // Mute + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + bool isMuted = externalNotificationModule->getMute(); + externalNotificationModule->setMute(!isMuted); + IF_SCREEN(graphics::isMuted = !isMuted; if (!isMuted) externalNotificationModule->stopNow(); + screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);) + } + return 0; + // Bluetooth + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); +#if defined(ARDUINO_ARCH_NRF52) + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#else + if (!config.bluetooth.enabled) { + disableBluetooth(); + IF_SCREEN(screen->showOverlayBanner("Bluetooth OFF", 3000)); + } else { + IF_SCREEN(screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000)); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#endif + return 0; + case INPUT_BROKER_MSG_REBOOT: + IF_SCREEN(screen->showOverlayBanner("Rebooting...", 0)); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + } + + switch (event->inputEvent) { + // GPS + case INPUT_BROKER_GPS_TOGGLE: + LOG_WARN("GPS Toggle"); +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) { + LOG_WARN("GPS Toggle2"); + gps->toggleGpsMode(); + const char *msg = + (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; + IF_SCREEN(screen->forceDisplay(); screen->showOverlayBanner(msg, 3000);) + } +#endif + return true; + // Mesh ping + case INPUT_BROKER_SEND_PING: + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000)); + } else { + IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 3000)); + } + return true; + // Power control + case INPUT_BROKER_SHUTDOWN: + LOG_ERROR("Shutting down"); + IF_SCREEN(screen->showOverlayBanner("Shutting down...")); + nodeDB->saveToDisk(); + shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; + // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + } + return false; +} \ No newline at end of file diff --git a/src/modules/SystemCommandsModule.h b/src/modules/SystemCommandsModule.h new file mode 100644 index 000000000..44910f443 --- /dev/null +++ b/src/modules/SystemCommandsModule.h @@ -0,0 +1,19 @@ +#pragma once + +#include "MeshModule.h" +#include "configuration.h" +#include "input/InputBroker.h" +#include +#include + +class SystemCommandsModule +{ + CallbackObserver inputObserver = + CallbackObserver(this, &SystemCommandsModule::handleInputEvent); + + public: + SystemCommandsModule(); + int handleInputEvent(const InputEvent *event); +}; + +extern SystemCommandsModule *systemCommandsModule; \ No newline at end of file diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index fafb28699..2472b95b1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "AirQualityTelemetry.h" diff --git a/src/modules/Telemetry/DeviceTelemetry.cpp b/src/modules/Telemetry/DeviceTelemetry.cpp index 251608641..43c2dd84c 100644 --- a/src/modules/Telemetry/DeviceTelemetry.cpp +++ b/src/modules/Telemetry/DeviceTelemetry.cpp @@ -9,6 +9,7 @@ #include "Router.h" #include "configuration.h" #include "main.h" +#include "memGet.h" #include #include #include @@ -133,6 +134,9 @@ meshtastic_Telemetry DeviceTelemetryModule::getLocalStatsTelemetry() telemetry.variant.local_stats.num_packets_rx_bad = SimRadio::instance->rxBad; telemetry.variant.local_stats.num_tx_relay = SimRadio::instance->txRelay; } +#else + telemetry.variant.local_stats.heap_total_bytes = memGet.getHeapSize(); + telemetry.variant.local_stats.heap_free_bytes = memGet.getFreeHeap(); #endif if (router) { telemetry.variant.local_stats.num_rx_dupe = router->rxDupe; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 56f9d7433..375d1e596 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -11,20 +11,27 @@ #include "RTC.h" #include "Router.h" #include "UnitConversions.h" +#include "buzz.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #include "target_specific.h" #include -#include #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL -// Sensors +// Sensors #include "Sensor/CGRadSensSensor.h" #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); +} #if __has_include() #include "Sensor/AHT10.h" AHT10Sensor aht10Sensor; @@ -52,6 +59,13 @@ BMP280Sensor bmp280Sensor; NullSensor bme280Sensor; #endif +#if __has_include() +#include "Sensor/LTR390UVSensor.h" +LTR390UVSensor ltr390uvSensor; +#else +NullSensor ltr390uvSensor; +#endif + #if __has_include() #include "Sensor/BME680Sensor.h" BME680Sensor bme680Sensor; @@ -94,6 +108,13 @@ SHTC3Sensor shtc3Sensor; NullSensor shtc3Sensor; #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 +#include "Sensor/RAK12035Sensor.h" +RAK12035Sensor rak12035Sensor; +#else +NullSensor rak12035Sensor; +#endif + #if __has_include() #include "Sensor/VEML7700Sensor.h" VEML7700Sensor veml7700Sensor; @@ -157,8 +178,16 @@ BMP3XXSensor bmp3xxSensor; NullSensor bmp3xxSensor; #endif +#if __has_include() +#include "Sensor/PCT2075Sensor.h" +PCT2075Sensor pct2075Sensor; +#else +NullSensor pct2075Sensor; +#endif + RCWL9620Sensor rcwl9620Sensor; CGRadSensSensor cgRadSens; + #endif #ifdef T1000X_SENSOR_EN #include "Sensor/T1000xSensor.h" @@ -168,6 +197,7 @@ T1000xSensor t1000xSensor; #include "Sensor/IndicatorSensor.h" IndicatorSensor indicatorSensor; #endif + #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -224,6 +254,8 @@ int32_t EnvironmentTelemetryModule::runOnce() #endif if (bme280Sensor.hasSensor()) result = bme280Sensor.runOnce(); + if (ltr390uvSensor.hasSensor()) + result = ltr390uvSensor.runOnce(); if (bmp3xxSensor.hasSensor()) result = bmp3xxSensor.runOnce(); if (bme680Sensor.hasSensor()) @@ -264,12 +296,19 @@ int32_t EnvironmentTelemetryModule::runOnce() result = max17048Sensor.runOnce(); if (cgRadSens.hasSensor()) result = cgRadSens.runOnce(); + if (pct2075Sensor.hasSensor()) + result = pct2075Sensor.runOnce(); // this only works on the wismesh hub with the solar option. This is not an I2C sensor, so we don't need the // sensormap here. #ifdef HAS_RAKPROT result = rak9154Sensor.runOnce(); #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 + if (rak12035Sensor.hasSensor()) { + result = rak12035Sensor.runOnce(); + } +#endif #endif } // it's possible to have this module enabled, only for displaying values on the screen. @@ -312,120 +351,152 @@ bool EnvironmentTelemetryModule::wantUIFrame() void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Setup display === + display->clear(); display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; - if (lastMeasurementPacket == nullptr) { - // If there's no valid packet, display "Environment" - display->drawString(x, y, "Environment"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // === Set Title + const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = graphics::getTextPositions(display)[line++]; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Decode the last measurement packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); - + // Decode the telemetry message from the latest received packet const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Display "Env. From: ..." on its own - display->drawString(x, y, "Env. From: " + String(lastSender) + " (" + String(agoSecs) + "s)"); + const auto &m = telemetry.variant.environment_metrics; - // Prepare sensor data strings - String sensorData[10]; - int sensorCount = 0; + // Check if any telemetry field has valid data + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || + m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; - if (lastMeasurement.variant.environment_metrics.has_temperature || - lastMeasurement.variant.environment_metrics.has_relative_humidity) { - String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "°C"; - if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = - String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "°F"; + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_temperature) { + String tempStr = moduleConfig.telemetry.environment_display_fahrenheit + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F" + : "Tmp: " + String(m.temperature, 1) + "°C"; + entries.push_back(tempStr); + } + if (m.has_relative_humidity) + entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); + if (m.barometric_pressure != 0) + entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner + + if (m.iaq <= 25) + aqi += " (Excellent)"; + else if (m.iaq <= 50) + aqi += " (Good)"; + else if (m.iaq <= 100) + aqi += " (Moderate)"; + else if (m.iaq <= 150) + aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; } - sensorData[sensorCount++] = - "Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%"; - } + entries.push_back(aqi); - if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) { - sensorData[sensorCount++] = - "Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA"; - } + // === IAQ alert logic === + static uint32_t lastAlertTime = 0; + uint32_t now = millis(); - if (lastMeasurement.variant.environment_metrics.voltage != 0) { - sensorData[sensorCount++] = "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " + - String(lastMeasurement.variant.environment_metrics.current, 0) + "mA"; - } + bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isCooldownOver = (now - lastAlertTime > 60000); - if (lastMeasurement.variant.environment_metrics.iaq != 0) { - sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq); - } + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg); + screen->showOverlayBanner(bannerMsg, 3000); - if (lastMeasurement.variant.environment_metrics.distance != 0) { - sensorData[sensorCount++] = "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm"; - } - - if (lastMeasurement.variant.environment_metrics.weight != 0) { - sensorData[sensorCount++] = "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"; - } - - if (lastMeasurement.variant.environment_metrics.radiation != 0) { - sensorData[sensorCount++] = "Rad: " + String(lastMeasurement.variant.environment_metrics.radiation, 2) + "µR/h"; - } - - if (lastMeasurement.variant.environment_metrics.lux != 0) { - sensorData[sensorCount++] = "Illuminance: " + String(lastMeasurement.variant.environment_metrics.lux, 2) + "lx"; - } - - if (lastMeasurement.variant.environment_metrics.white_lux != 0) { - sensorData[sensorCount++] = "W_Lux: " + String(lastMeasurement.variant.environment_metrics.white_lux, 2) + "lx"; - } - - static int scrollOffset = 0; - static bool scrollingDown = true; - static uint32_t lastScrollTime = millis(); - - // Determine how many lines we can fit on display - // Calculated once only: display dimensions don't change during runtime. - static int maxLines = 0; - if (!maxLines) { - const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text - const int16_t paddingBottom = 8; // Indicator dots - maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL); - assert(maxLines > 0); - } - - // Draw as many lines of data as we can fit - int linesToShow = min(maxLines, sensorCount); - for (int i = 0; i < linesToShow; i++) { - int index = (scrollOffset + i) % sensorCount; - display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]); - } - - // Only scroll if there are more than 3 sensor data lines - if (sensorCount > 3) { - // Update scroll offset every 5 seconds - if (millis() - lastScrollTime > 5000) { - if (scrollingDown) { - scrollOffset++; - if (scrollOffset + linesToShow >= sensorCount) { - scrollingDown = false; - } - } else { - scrollOffset--; - if (scrollOffset <= 0) { - scrollingDown = true; - } + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); } - lastScrollTime = millis(); + + lastAlertTime = now; } } + if (m.voltage != 0 || m.current != 0) + entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); + if (m.lux != 0) + entries.push_back("Light: " + String(m.lux, 0) + "lx"); + if (m.white_lux != 0) + entries.push_back("White: " + String(m.white_lux, 0) + "lx"); + if (m.weight != 0) + entries.push_back("Weight: " + String(m.weight, 0) + "kg"); + if (m.distance != 0) + entries.push_back("Level: " + String(m.distance, 0) + "mm"); + if (m.radiation != 0) + entries.push_back("Rad: " + String(m.radiation, 2) + " µR/h"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); + } + + currentY += rowHeight; + } } bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) @@ -515,6 +586,10 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && bme280Sensor.getMetrics(m); hasSensor = true; } + if (ltr390uvSensor.hasSensor()) { + valid = valid && ltr390uvSensor.getMetrics(m); + hasSensor = true; + } if (bmp3xxSensor.hasSensor()) { valid = valid && bmp3xxSensor.getMetrics(m); hasSensor = true; @@ -595,10 +670,22 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m valid = valid && cgRadSens.getMetrics(m); hasSensor = true; } + if (pct2075Sensor.hasSensor()) { + valid = valid && pct2075Sensor.getMetrics(m); + hasSensor = true; + } #ifdef HAS_RAKPROT valid = valid && rak9154Sensor.getMetrics(m); hasSensor = true; #endif +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \ + RAK_4631 == \ + 1 // Not really needed, but may as well just skip at a lower level it if no library or not a RAK_4631 + if (rak12035Sensor.hasSensor()) { + valid = valid && rak12035Sensor.getMetrics(m); + hasSensor = true; + } +#endif #endif return valid && hasSensor; } @@ -653,6 +740,9 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) LOG_INFO("Send: radiation=%fµR/h", m.variant.environment_metrics.radiation); + LOG_INFO("Send: soil_temperature=%f, soil_moisture=%u", m.variant.environment_metrics.soil_temperature, + m.variant.environment_metrics.soil_moisture); + sensor_read_error_count = 0; meshtastic_MeshPacket *p = allocDataProtobuf(m); @@ -739,6 +829,11 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } + if (ltr390uvSensor.hasSensor()) { + result = ltr390uvSensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } if (bmp3xxSensor.hasSensor()) { result = bmp3xxSensor.handleAdminMessage(mp, request, response); if (result != AdminMessageHandleResult::NOT_HANDLED) @@ -819,8 +914,17 @@ AdminMessageHandleResult EnvironmentTelemetryModule::handleAdminMessageForModule if (result != AdminMessageHandleResult::NOT_HANDLED) return result; } +#if __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && \ + RAK_4631 == \ + 1 // Not really needed, but may as well just skip it at a lower level if no library or not a RAK_4631 + if (rak12035Sensor.hasSensor()) { + result = rak12035Sensor.handleAdminMessage(mp, request, response); + if (result != AdminMessageHandleResult::NOT_HANDLED) + return result; + } +#endif #endif return result; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index a2a18ba03..3a735b1fa 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -118,22 +118,31 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * } // Display "Health From: ..." on its own - display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + char headerStr[64]; + snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); + display->drawString(x, y, headerStr); - String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "°C"; + char last_temp[16]; if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "°F"; + snprintf(last_temp, sizeof(last_temp), "%.0f°F", + UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); + } else { + snprintf(last_temp, sizeof(last_temp), "%.0f°C", lastMeasurement.variant.health_metrics.temperature); } // Continue with the remaining details - display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp); + char tempStr[32]; + snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); + display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm"); + char heartStr[32]; + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %"); + char spo2Str[32]; + snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } diff --git a/src/modules/Telemetry/HostMetrics.cpp b/src/modules/Telemetry/HostMetrics.cpp index dc4315efa..6a92b15f8 100644 --- a/src/modules/Telemetry/HostMetrics.cpp +++ b/src/modules/Telemetry/HostMetrics.cpp @@ -29,12 +29,15 @@ bool HostMetricsModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, if (t->which_variant == meshtastic_Telemetry_host_metrics_tag) { #ifdef DEBUG_PORT const char *sender = getSenderShortName(mp); + if (t->variant.host_metrics.has_user_string) + t->variant.host_metrics.user_string[sizeof(t->variant.host_metrics.user_string) - 1] = '\0'; - LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f, %s", - sender, t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, + LOG_INFO("(Received Host Metrics from %s): uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", sender, + t->variant.host_metrics.uptime_seconds, t->variant.host_metrics.diskfree1_bytes, t->variant.host_metrics.freemem_bytes, static_cast(t->variant.host_metrics.load1) / 100, static_cast(t->variant.host_metrics.load5) / 100, - static_cast(t->variant.host_metrics.load15) / 100, t->variant.host_metrics.user_string); + static_cast(t->variant.host_metrics.load15) / 100); + // t->variant.host_metrics.has_user_string ? t->variant.host_metrics.user_string : ""); #endif } return false; // Let others look at this message also if they want @@ -110,7 +113,8 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics() if (settingsStrings[hostMetrics_user_command] != "") { std::string userCommandResult = exec(settingsStrings[hostMetrics_user_command].c_str()); if (userCommandResult.length() > 1) { - strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), 200); + strncpy(t.variant.host_metrics.user_string, userCommandResult.c_str(), sizeof(t.variant.host_metrics.user_string)); + t.variant.host_metrics.user_string[sizeof(t.variant.host_metrics.user_string) - 1] = '\0'; t.variant.host_metrics.has_user_string = true; } } @@ -120,11 +124,12 @@ meshtastic_Telemetry HostMetricsModule::getHostMetrics() bool HostMetricsModule::sendMetrics() { meshtastic_Telemetry telemetry = getHostMetrics(); - LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f %s", + LOG_INFO("Send: uptime=%u, diskfree=%lu, memory free=%lu, load=%04.2f, %04.2f, %04.2f", telemetry.variant.host_metrics.uptime_seconds, telemetry.variant.host_metrics.diskfree1_bytes, telemetry.variant.host_metrics.freemem_bytes, static_cast(telemetry.variant.host_metrics.load1) / 100, static_cast(telemetry.variant.host_metrics.load5) / 100, - static_cast(telemetry.variant.host_metrics.load15) / 100, telemetry.variant.host_metrics.user_string); + static_cast(telemetry.variant.host_metrics.load15) / 100); + // telemetry.variant.host_metrics.has_user_string ? telemetry.variant.host_metrics.user_string : ""); meshtastic_MeshPacket *p = allocDataProtobuf(telemetry); p->to = NODENUM_BROADCAST; @@ -135,4 +140,4 @@ bool HostMetricsModule::sendMetrics() service->sendToMesh(p, RX_SRC_LOCAL, true); return true; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 54ec90dae..df1505226 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -10,6 +10,7 @@ #include "PowerTelemetry.h" #include "RTC.h" #include "Router.h" +#include "graphics/SharedUIDisplay.h" #include "main.h" #include "power.h" #include "sleep.h" @@ -21,6 +22,11 @@ #include "graphics/ScreenFonts.h" #include +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); +} + int32_t PowerTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -103,13 +109,20 @@ bool PowerTelemetryModule::wantUIFrame() void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); if (lastMeasurementPacket == nullptr) { - // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, y, "Power Telemetry"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // In case of no valid packet, display "Power Telemetry", "No measurement" + display->drawString(x, graphics::getTextPositions(display)[line++], "No measurement"); return; } @@ -120,29 +133,35 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); + display->drawString(x, graphics::getTextPositions(display)[line++], "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } // Display "Pow. From: ..." - display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + char fromStr[64]; + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); + display->drawString(x, graphics::getTextPositions(display)[line++], fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags - if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); + const auto &m = lastMeasurement.variant.power_metrics; + int lineY = textSecondLine; + + auto drawLine = [&](const char *label, float voltage, float current) { + char lineStr[64]; + snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); + display->drawString(x, lineY, lineStr); + lineY += _fontHeight(FONT_SMALL); + }; + + if (m.has_ch1_voltage || m.has_ch1_current) { + drawLine("Ch1", m.ch1_voltage, m.ch1_current); } - if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); + if (m.has_ch2_voltage || m.has_ch2_current) { + drawLine("Ch2", m.ch2_voltage, m.ch2_current); } - if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); + if (m.has_ch3_voltage || m.has_ch3_current) { + drawLine("Ch3", m.ch3_voltage, m.ch3_current); } } diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 0e0212bc5..fce029deb 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -137,17 +137,17 @@ void BME680Sensor::updateState() #endif } -void BME680Sensor::checkStatus(String functionName) +void BME680Sensor::checkStatus(const char *functionName) { if (bme680.status < BSEC_OK) - LOG_ERROR("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); else if (bme680.status > BSEC_OK) - LOG_WARN("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); if (bme680.sensor.status < BME68X_OK) - LOG_ERROR("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); else if (bme680.sensor.status > BME68X_OK) - LOG_WARN("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index 249c4b3e7..ce1fa4f3b 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -34,7 +34,7 @@ class BME680Sensor : public TelemetrySensor BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; void loadState(); void updateState(); - void checkStatus(String functionName); + void checkStatus(const char *functionName); public: BME680Sensor(); diff --git a/src/modules/Telemetry/Sensor/INA3221Sensor.h b/src/modules/Telemetry/Sensor/INA3221Sensor.h index 69edf8c50..0581f92f6 100644 --- a/src/modules/Telemetry/Sensor/INA3221Sensor.h +++ b/src/modules/Telemetry/Sensor/INA3221Sensor.h @@ -8,16 +8,24 @@ #include "VoltageSensor.h" #include +#ifndef INA3221_ENV_CH +#define INA3221_ENV_CH INA3221_CH1 +#endif + +#ifndef INA3221_BAT_CH +#define INA3221_BAT_CH INA3221_CH1 +#endif + class INA3221Sensor : public TelemetrySensor, VoltageSensor, CurrentSensor { private: INA3221 ina3221 = INA3221(INA3221_ADDR42_SDA); // channel to report voltage/current for environment metrics - ina3221_ch_t ENV_CH = INA3221_CH1; + static const ina3221_ch_t ENV_CH = INA3221_ENV_CH; // channel to report battery voltage for device_battery_ina_address - ina3221_ch_t BAT_CH = INA3221_CH1; + static const ina3221_ch_t BAT_CH = INA3221_BAT_CH; // get a single measurement for a channel struct _INA3221Measurement getMeasurement(ina3221_ch_t ch); diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp new file mode 100644 index 000000000..fb84700c4 --- /dev/null +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.cpp @@ -0,0 +1,73 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "LTR390UVSensor.h" +#include "TelemetrySensor.h" +#include + +LTR390UVSensor::LTR390UVSensor() : TelemetrySensor(meshtastic_TelemetrySensorType_LTR390UV, "LTR390UV") {} + +int32_t LTR390UVSensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + status = ltr390uv.begin(nodeTelemetrySensorsMap[sensorType].second); + ltr390uv.setMode(LTR390_MODE_UVS); + ltr390uv.setGain(LTR390_GAIN_18); // Datasheet default + ltr390uv.setResolution(LTR390_RESOLUTION_20BIT); // Datasheet default + + return initI2CSensor(); +} + +void LTR390UVSensor::setup() {} + +bool LTR390UVSensor::getMetrics(meshtastic_Telemetry *measurement) +{ + LOG_DEBUG("LTR390UV getMetrics"); + + // Because the sensor does not measure Lux and UV at the same time, we need to read them in two passes. + if (ltr390uv.newDataAvailable()) { + measurement->variant.environment_metrics.has_lux = true; + measurement->variant.environment_metrics.has_uv_lux = true; + + if (ltr390uv.getMode() == LTR390_MODE_ALS) { + lastLuxReading = 0.6 * ltr390uv.readALS() / (1 * 4); // Datasheet page 23 for gain x1 and 20bit resolution + LOG_DEBUG("LTR390UV Lux reading: %f", lastLuxReading); + + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; + + ltr390uv.setGain( + LTR390_GAIN_18); // Recommended for UVI - x18. Do not change, 2300 UV Sensitivity only specified for x18 gain + ltr390uv.setMode(LTR390_MODE_UVS); + + return true; + + } else if (ltr390uv.getMode() == LTR390_MODE_UVS) { + lastUVReading = ltr390uv.readUVS() / + 2300.f; // Datasheet page 23 and page 6, only characterisation for gain x18 and 20bit resolution + LOG_DEBUG("LTR390UV UV reading: %f", lastUVReading); + + measurement->variant.environment_metrics.lux = lastLuxReading; + measurement->variant.environment_metrics.uv_lux = lastUVReading; + + ltr390uv.setGain( + LTR390_GAIN_1); // x1 gain will already max out the sensor at direct sunlight, so no need to increase it + ltr390uv.setMode(LTR390_MODE_ALS); + + return true; + } + } + + // In case we fail to read the sensor mode, set the has_lux and has_uv_lux back to false + measurement->variant.environment_metrics.has_lux = false; + measurement->variant.environment_metrics.has_uv_lux = false; + + return false; +} +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/LTR390UVSensor.h b/src/modules/Telemetry/Sensor/LTR390UVSensor.h new file mode 100644 index 000000000..40206bce8 --- /dev/null +++ b/src/modules/Telemetry/Sensor/LTR390UVSensor.h @@ -0,0 +1,25 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class LTR390UVSensor : public TelemetrySensor +{ + private: + Adafruit_LTR390 ltr390uv = Adafruit_LTR390(); + float lastLuxReading = 0; + float lastUVReading = 0; + + protected: + virtual void setup() override; + + public: + LTR390UVSensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp new file mode 100644 index 000000000..d2b50d983 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.cpp @@ -0,0 +1,35 @@ +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "PCT2075Sensor.h" +#include "TelemetrySensor.h" +#include + +PCT2075Sensor::PCT2075Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_PCT2075, "PCT2075") {} + +int32_t PCT2075Sensor::runOnce() +{ + LOG_INFO("Init sensor: %s", sensorName); + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + status = pct2075.begin(nodeTelemetrySensorsMap[sensorType].first, nodeTelemetrySensorsMap[sensorType].second); + + return initI2CSensor(); +} + +void PCT2075Sensor::setup() {} + +bool PCT2075Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + measurement->variant.environment_metrics.has_temperature = true; + + measurement->variant.environment_metrics.temperature = pct2075.getTemperature(); + + return true; +} + +#endif diff --git a/src/modules/Telemetry/Sensor/PCT2075Sensor.h b/src/modules/Telemetry/Sensor/PCT2075Sensor.h new file mode 100644 index 000000000..842c973d0 --- /dev/null +++ b/src/modules/Telemetry/Sensor/PCT2075Sensor.h @@ -0,0 +1,24 @@ +#pragma once +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "TelemetrySensor.h" +#include + +class PCT2075Sensor : public TelemetrySensor +{ + private: + Adafruit_PCT2075 pct2075; + + protected: + virtual void setup() override; + + public: + PCT2075Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; + +#endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp new file mode 100644 index 000000000..7a1bb01ce --- /dev/null +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.cpp @@ -0,0 +1,109 @@ +#include "configuration.h" +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("RAK12035_SoilMoisture.h") && defined(RAK_4631) && RAK_4631 == 1 + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RAK12035Sensor.h" + +RAK12035Sensor::RAK12035Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_RAK12035, "RAK12035") {} + +int32_t RAK12035Sensor::runOnce() +{ + if (!hasSensor()) { + return DEFAULT_SENSOR_MINIMUM_WAIT_TIME_BETWEEN_READS; + } + + // TODO:: check for up to 2 additional sensors and start them if present. + sensor.set_sensor_addr(RAK120351_ADDR); + delay(100); + sensor.begin(nodeTelemetrySensorsMap[sensorType].first); + + // Get sensor firmware version + uint8_t data = 0; + sensor.get_sensor_version(&data); + if (data != 0) { + LOG_INFO("Init sensor: %s", sensorName); + LOG_INFO("RAK12035Sensor Init Succeed \nSensor1 Firmware version: %i, Sensor Name: %s", data, sensorName); + status = true; + sensor.sensor_sleep(); + } else { + // If we reach here, it means the sensor did not initialize correctly. + LOG_INFO("Init sensor: %s", sensorName); + LOG_ERROR("RAK12035Sensor Init Failed"); + status = false; + } + + return initI2CSensor(); +} + +void RAK12035Sensor::setup() +{ + // Set the calibration values + // Reading the saved calibration values from the sensor. + // TODO:: Check for and run calibration check for up to 2 additional sensors if present. + uint16_t zero_val = 0; + uint16_t hundred_val = 0; + uint16_t default_zero_val = 550; + uint16_t default_hundred_val = 420; + sensor.sensor_on(); + delay(200); + sensor.get_dry_cal(&zero_val); + sensor.get_wet_cal(&hundred_val); + delay(200); + if (zero_val == 0 || zero_val <= hundred_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Dry Calibration: %d", default_zero_val); + sensor.set_dry_cal(default_zero_val); + sensor.get_dry_cal(&zero_val); + LOG_INFO("Dry calibration reset complete. New value is %d", zero_val); + } + if (hundred_val == 0 || hundred_val >= zero_val) { + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); + LOG_INFO("This does not make sense. You can recalibrate this sensor using the calibration sketch included here: " + "https://github.com/RAKWireless/RAK12035_SoilMoisture."); + LOG_INFO("For now, setting default calibration value for Wet Calibration: %d", default_hundred_val); + sensor.set_wet_cal(default_hundred_val); + sensor.get_wet_cal(&hundred_val); + LOG_INFO("Wet calibration reset complete. New value is %d", hundred_val); + } + sensor.sensor_sleep(); + delay(200); + LOG_INFO("Dry calibration value is %d", zero_val); + LOG_INFO("Wet calibration value is %d", hundred_val); +} + +bool RAK12035Sensor::getMetrics(meshtastic_Telemetry *measurement) +{ + // TODO:: read and send metrics for up to 2 additional soil monitors if present. + // -- how to do this.. this could get a little complex.. + // ie - 1> we combine them into an average and send that, 2> we send them as separate metrics + // ^-- these scenarios would require different handling of the metrics in the receiving end and maybe a setting in the + // device ui and an additional proto for that? + measurement->variant.environment_metrics.has_soil_temperature = true; + measurement->variant.environment_metrics.has_soil_moisture = true; + + uint8_t moisture = 0; + uint16_t temp = 0; + bool success = false; + + sensor.sensor_on(); + delay(200); + success = sensor.get_sensor_moisture(&moisture); + delay(200); + success &= sensor.get_sensor_temperature(&temp); + delay(200); + sensor.sensor_sleep(); + + if (success == false) { + LOG_ERROR("Failed to read sensor data"); + return false; + } + measurement->variant.environment_metrics.soil_temperature = ((float)temp / 10.0f); + measurement->variant.environment_metrics.soil_moisture = moisture; + + return true; +} +#endif diff --git a/src/modules/Telemetry/Sensor/RAK12035Sensor.h b/src/modules/Telemetry/Sensor/RAK12035Sensor.h new file mode 100644 index 000000000..2c32a840d --- /dev/null +++ b/src/modules/Telemetry/Sensor/RAK12035Sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "configuration.h" + +#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include() && defined(RAK_4631) +#ifndef _MT_RAK12035VBSENSOR_H +#define _MT_RAK12035VBSENSOR_H +#endif + +#include "../mesh/generated/meshtastic/telemetry.pb.h" +#include "RAK12035_SoilMoisture.h" +#include "TelemetrySensor.h" +#include + +class RAK12035Sensor : public TelemetrySensor +{ + private: + RAK12035 sensor; + + protected: + virtual void setup() override; + + public: + RAK12035Sensor(); + virtual int32_t runOnce() override; + virtual bool getMetrics(meshtastic_Telemetry *measurement) override; +}; +#endif diff --git a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp index e352dda8d..9f7a55cc5 100644 --- a/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp +++ b/src/modules/Telemetry/Sensor/RCWL9620Sensor.cpp @@ -41,21 +41,36 @@ void RCWL9620Sensor::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl float RCWL9620Sensor::getDistance() { - uint32_t data; - _wire->beginTransmission(_addr); // Transfer data to addr. - _wire->write(0x01); - _wire->endTransmission(); // Stop data transmission with the Ultrasonic - // Unit. + uint32_t data = 0; + uint8_t b1 = 0, b2 = 0, b3 = 0; - _wire->requestFrom(_addr, - (uint8_t)3); // Request 3 bytes from Ultrasonic Unit. + LOG_DEBUG("[RCWL9620] Start measure command"); + + _wire->beginTransmission(_addr); + _wire->write(0x01); // À tester aussi sans cette ligne si besoin + uint8_t result = _wire->endTransmission(); + LOG_DEBUG("[RCWL9620] endTransmission result = %d", result); + delay(100); // délai pour laisser le capteur répondre + + LOG_DEBUG("[RCWL9620] Read i2c data:"); + _wire->requestFrom(_addr, (uint8_t)3); + + if (_wire->available() < 3) { + LOG_DEBUG("[RCWL9620] less than 3 octets !"); + return 0.0; + } + + b1 = _wire->read(); + b2 = _wire->read(); + b3 = _wire->read(); + + data = ((uint32_t)b1 << 16) | ((uint32_t)b2 << 8) | b3; + + float Distance = float(data) / 1000.0; + + LOG_DEBUG("[RCWL9620] Bytes readed = %02X %02X %02X", b1, b2, b3); + LOG_DEBUG("[RCWL9620] data=%.2f, level=%.2f", (double)data, (double)Distance); - data = _wire->read(); - data <<= 8; - data |= _wire->read(); - data <<= 8; - data |= _wire->read(); - float Distance = float(data) / 1000; if (Distance > 4500.00) { return 4500.00; } else { @@ -63,4 +78,4 @@ float RCWL9620Sensor::getDistance() } } -#endif \ No newline at end of file +#endif diff --git a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp index beec3c70b..04443ebec 100644 --- a/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp +++ b/src/modules/Telemetry/Sensor/TSL2591Sensor.cpp @@ -23,8 +23,8 @@ int32_t TSL2591Sensor::runOnce() void TSL2591Sensor::setup() { - tsl.setGain(TSL2591_GAIN_MED); // 25x gain - tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS); + tsl.setGain(TSL2591_GAIN_LOW); // 1x gain + tsl.setTiming(TSL2591_INTEGRATIONTIME_100MS); } bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) @@ -41,4 +41,4 @@ bool TSL2591Sensor::getMetrics(meshtastic_Telemetry *measurement) return true; } -#endif \ No newline at end of file +#endif diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 479a973c2..578e7183a 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,9 +2,13 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#include "graphics/draw/CompassRenderer.h" + #if HAS_SCREEN #include "gps/RTC.h" #include "graphics/Screen.h" +#include "graphics/TimeFormatters.h" +#include "graphics/draw/NodeListRenderer.h" #include "main.h" #endif @@ -48,6 +52,8 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp) bool WaypointModule::shouldDraw() { #if !MESHTASTIC_EXCLUDE_WAYPOINT + if (screen == nullptr) + return false; // If no waypoint to show if (!devicestate.has_rx_waypoint) return false; @@ -79,13 +85,15 @@ bool WaypointModule::shouldDraw() /// Draw the last waypoint we received void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; // Prepare to draw display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // Handle inverted display // Unsure of expected behavior: for now, copy drawNodeInfo - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); // Decode the waypoint @@ -101,7 +109,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Get timestamp info. Will pass as a field to drawColumns static char lastStr[20]; - screen->getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); + getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr)); // Will contain distance information, passed as a field to drawColumns static char distStr[20]; @@ -115,7 +123,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Dimensions / co-ordinates for the compass/circle int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { compassX = x + display->getWidth() - compassDiam / 2 - 5; @@ -133,7 +141,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); // Compass bearing to waypoint float bearingToOther = @@ -142,7 +150,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // If the top of the compass is not a static north we need adjust bearingToOther based on heading if (!config.display.compass_north_top) bearingToOther -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; @@ -180,11 +188,11 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Undo color-inversion, if set prior to drawing header // Unsure of expected behavior? For now: copy drawNodeInfo - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); + graphics::NodeListRenderer::drawColumns(display, x, y, fields); } #endif diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index b27586771..8b1fc5302 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -3,6 +3,9 @@ #include "Default.h" #include "MeshService.h" #include "PaxcounterModule.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include PaxcounterModule *paxcounterModule; @@ -112,20 +115,32 @@ int32_t PaxcounterModule::runOnce() #if HAS_SCREEN #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + + // === Set Title + const char *titleStr = "Pax"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + 0, y + 0, "PAX"); libpax_counter_count(&count_from_libpax); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, "WiFi: %d\nBLE: %d\nuptime: %ds", - count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); + display->drawStringf(display->getWidth() / 2 + x, graphics::getTextPositions(display)[line++], buffer, + "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, + millis() / 1000); } #endif // HAS_SCREEN diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 02e5b0bd4..f08ee00f9 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -10,6 +10,7 @@ #ifdef HAS_BMA423 #include "BMA423Sensor.h" #endif +#include "BMM150Sensor.h" #include "BMX160Sensor.h" #include "ICM20948Sensor.h" #include "LIS3DHSensor.h" @@ -107,6 +108,9 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::ICM20948: sensor = new ICM20948Sensor(device); break; + case ScanI2C::DeviceType::BMM150: + sensor = new BMM150Sensor(device); + break; #ifdef HAS_QMA6100P case ScanI2C::DeviceType::QMA6100P: sensor = new QMA6100PSensor(device); diff --git a/src/motion/BMM150Sensor.cpp b/src/motion/BMM150Sensor.cpp new file mode 100644 index 000000000..4b3a1215c --- /dev/null +++ b/src/motion/BMM150Sensor.cpp @@ -0,0 +1,93 @@ +#include "BMM150Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + +// screen is defined in main.cpp +extern graphics::Screen *screen; +#endif + +// Flag when an interrupt has been detected +volatile static bool BMM150_IRQ = false; + +BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool BMM150Sensor::init() +{ + // Initialise the sensor + sensor = BMM150Singleton::GetInstance(device); + return sensor->init(device); +} + +int32_t BMM150Sensor::runOnce() +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + float heading = sensor->getCompassDegree(); + + switch (config.display.compass_orientation) { + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0: + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED: + heading += 90; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED: + heading += 180; + break; + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270: + case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED: + heading += 270; + break; + } + if (screen) + screen->setHeading(heading); +#endif + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +// ---------------------------------------------------------------------- +// BMM150Singleton +// ---------------------------------------------------------------------- + +// Get a singleton wrapper for an Sparkfun BMM_150_I2C +BMM150Singleton *BMM150Singleton::GetInstance(ScanI2C::FoundDevice device) +{ +#if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); +#else + TwoWire &bus = Wire; // fallback if only one I2C interface +#endif + if (pinstance == nullptr) { + pinstance = new BMM150Singleton(&bus, device.address.address); + } + return pinstance; +} + +BMM150Singleton::~BMM150Singleton() {} + +BMM150Singleton *BMM150Singleton::pinstance{nullptr}; + +// Initialise the BMM150 Sensor +// https://github.com/DFRobot/DFRobot_BMM150/blob/master/examples/getGeomagneticData/getGeomagneticData.ino +bool BMM150Singleton::init(ScanI2C::FoundDevice device) +{ + + // startup + LOG_DEBUG("BMM150 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + uint8_t status = begin(); + if (status != 0) { + LOG_DEBUG("BMM150 init error %u", status); + return false; + } + + // SW reset to make sure the device starts in a known state + setOperationMode(BMM150_POWERMODE_NORMAL); + setPresetMode(BMM150_PRESETMODE_LOWPOWER); + setRate(BMM150_DATA_RATE_02HZ); + setMeasurementXYZ(); + return true; +} + +#endif \ No newline at end of file diff --git a/src/motion/BMM150Sensor.h b/src/motion/BMM150Sensor.h new file mode 100644 index 000000000..879045400 --- /dev/null +++ b/src/motion/BMM150Sensor.h @@ -0,0 +1,57 @@ +#pragma once +#ifndef _BMM_150_SENSOR_H_ +#define _BMM_150_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "Fusion/Fusion.h" +#include + +// The I2C address of the Accelerometer (if found) from main.cpp +extern ScanI2C::DeviceAddress accelerometer_found; + +// Singleton wrapper +class BMM150Singleton : public DFRobot_BMM150_I2C +{ + private: + static BMM150Singleton *pinstance; + + protected: + BMM150Singleton(TwoWire *tw, uint8_t addr) : DFRobot_BMM150_I2C(tw, addr) {} + ~BMM150Singleton(); + + public: + // Create a singleton instance (not thread safe) + static BMM150Singleton *GetInstance(ScanI2C::FoundDevice device); + + // Singletons should not be cloneable. + BMM150Singleton(BMM150Singleton &other) = delete; + + // Singletons should not be assignable. + void operator=(const BMM150Singleton &) = delete; + + // Initialise the motion sensor singleton for normal operation + bool init(ScanI2C::FoundDevice device); +}; + +class BMM150Sensor : public MotionSensor +{ + private: + BMM150Singleton *sensor = nullptr; + bool showingScreen = false; + + public: + explicit BMM150Sensor(ScanI2C::FoundDevice foundDevice); + + // Initialise the motion sensor + virtual bool init() override; + + // Called each time our sensor gets a chance to run + virtual int32_t runOnce() override; +}; + +#endif + +#endif \ No newline at end of file diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index a3909ea3a..003ee850c 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -37,7 +37,8 @@ int32_t BMX160Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magAccel.x > highestX) @@ -58,7 +59,8 @@ int32_t BMX160Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -103,8 +105,8 @@ int32_t BMX160Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; @@ -118,7 +120,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index ecc48d39b..76ba8e8cf 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -60,7 +60,8 @@ int32_t ICM20948Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magX > highestX) @@ -81,7 +82,8 @@ int32_t ICM20948Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -124,8 +126,8 @@ int32_t ICM20948Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) @@ -159,7 +161,8 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } // ---------------------------------------------------------------------- diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 56738d355..b00460aff 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -1,4 +1,5 @@ #include "MotionSensor.h" +#include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C @@ -34,6 +35,8 @@ ScanI2C::I2CPort MotionSensor::devicePort() #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; // int x_offset = display->width() / 2; // int y_offset = display->height() <= 80 ? 0 : 32; display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -46,7 +49,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState display->drawString(x, y + 40, timeRemainingBuffer); int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { @@ -57,7 +60,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; } display->drawCircle(compassX, compassY, compassDiam / 2); - screen->drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index dca8a3b44..137c92056 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -355,7 +355,7 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length) // if another "/" was added, parse string up to that character channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; // We allow downlink JSON packets only on a channel named "mqtt" - meshtastic_Channel &sendChannel = channels.getByName(channelName); + const meshtastic_Channel &sendChannel = channels.getByName(channelName); if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && sendChannel.settings.downlink_enabled)) { LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); @@ -491,7 +491,7 @@ void MQTT::reconnect() return; // Don't try to connect directly to the server } #if HAS_NETWORKING - const PubSubConfig config(moduleConfig.mqtt); + const PubSubConfig ps_config(moduleConfig.mqtt); MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS if (moduleConfig.mqtt.tls_enabled) { @@ -502,7 +502,7 @@ void MQTT::reconnect() LOG_INFO("Use non-TLS-encrypted session"); } #endif - if (connectPubSub(config, pubSub, *clientConnection)) { + if (connectPubSub(ps_config, pubSub, *clientConnection)) { enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; @@ -763,7 +763,10 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp_encrypted, const meshtastic_Me } entry->topic = std::move(topic); entry->envBytes.assign(bytes, numBytes); - assert(mqttQueue.enqueue(entry, 0)); + if (mqttQueue.enqueue(entry, 0) == false) { + LOG_CRIT("Failed to add a message to mqttQueue!"); + abort(); + } } } diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 009439f25..3ab06695b 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -94,31 +94,33 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", passkey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + char pin[8]; + snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); + display->setFont(FONT_SMALL); + char deviceName[64]; + snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif passkeyShowing = true; @@ -134,7 +136,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { passkeyShowing = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } } @@ -171,6 +174,11 @@ void NimbleBluetooth::deinit() { #ifdef ARCH_ESP32 LOG_INFO("Disable bluetooth until reboot"); + +#ifdef BLE_LED + digitalWrite(BLE_LED, LOW); +#endif + NimBLEDevice::deinit(); #endif } diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/WiFiOTA.cpp index eac124dda..4cf157b4c 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/WiFiOTA.cpp @@ -80,13 +80,13 @@ bool trySwitchToOTA() return true; } -String getVersion() +const char *getVersion() { const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; + static esp_app_desc_t app_desc; if (!getAppDesc(part, &app_desc)) - return String(); - return String(app_desc.version); + return ""; + return app_desc.version; } } // namespace WiFiOTA diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h index 61860ed5e..5a7ee348a 100644 --- a/src/platform/esp32/WiFiOTA.h +++ b/src/platform/esp32/WiFiOTA.h @@ -12,7 +12,7 @@ bool isUpdated(); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network); bool trySwitchToOTA(); -String getVersion(); +const char *getVersion(); } // namespace WiFiOTA #endif // WIFIOTA_H diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 68d06c6d7..3763bce1e 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -184,6 +184,8 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_SENSOR_HUB #elif defined(ELECROW_PANEL) #define HW_VENDOR meshtastic_HardwareModel_CROWPANEL +#elif defined(LINK_32) +#define HW_VENDOR meshtastic_HardwareModel_LINK_32 #endif // ----------------------------------------------------------------------------- diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 3c4faac3e..cdea53c9a 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -56,9 +56,11 @@ void updateBatteryLevel(uint8_t level) {} void getMacAddr(uint8_t *dmac) { #if defined(CONFIG_IDF_TARGET_ESP32C6) && defined(CONFIG_SOC_IEEE802154_SUPPORTED) - assert(esp_base_mac_addr_get(dmac) == ESP_OK); + auto res = esp_base_mac_addr_get(dmac); + assert(res == ESP_OK); #else - assert(esp_efuse_mac_get_default(dmac) == ESP_OK); + auto res = esp_efuse_mac_get_default(dmac); + assert(res == ESP_OK); #endif } diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 4f6fe7c6b..89e92afc6 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -331,7 +331,7 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke char btPIN[16] = "888888"; snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; + int y_offset = display->height() <= 80 ? 0 : 12; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, y_offset + y, "Bluetooth"); diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 9d1d48f1c..684d20e84 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -49,6 +49,8 @@ #define HW_VENDOR meshtastic_HardwareModel_RAK2560 #elif defined(WISMESH_TAP) #define HW_VENDOR meshtastic_HardwareModel_WISMESH_TAP +#elif defined(GAT562_MESH_TRIAL_TRACKER) +#define HW_VENDOR meshtastic_HardwareModel_GAT562_MESH_TRIAL_TRACKER #elif defined(RAK4630) #define HW_VENDOR meshtastic_HardwareModel_RAK4631 #elif defined(TTGO_T_ECHO) @@ -85,6 +87,10 @@ #define HW_VENDOR meshtastic_HardwareModel_SEEED_SOLAR_NODE #elif defined(HELTEC_MESH_POCKET) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_POCKET +#elif defined(NOMADSTAR_METEOR_PRO) +#define HW_VENDOR meshtastic_HardwareModel_NOMADSTAR_METEOR_PRO +#elif defined(SEEED_WIO_TRACKER_L1) +#define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 #else #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN #endif @@ -115,10 +121,6 @@ #define BUTTON_PIN PIN_BUTTON1 #endif -#ifdef PIN_BUTTON2 -#define BUTTON_PIN_ALT PIN_BUTTON2 -#endif - #ifdef PIN_BUTTON_TOUCH #define BUTTON_PIN_TOUCH PIN_BUTTON_TOUCH #endif @@ -137,4 +139,4 @@ #if !defined(PIN_SERIAL_RX) && !defined(NRF52840_XXAA) // No serial ports on this board - ONLY use segger in memory console #define USE_SEGGER -#endif \ No newline at end of file +#endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 9accd2a02..1bf9a39fd 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -308,6 +308,9 @@ void cpuDeepSleep(uint32_t msecToWake) nrf_gpio_cfg_default(SCREEN_TOUCH_INT); nrf_gpio_cfg_default(WB_I2C1_SCL); nrf_gpio_cfg_default(WB_I2C1_SDA); + + // nrf_gpio_cfg_default(WB_I2C2_SCL); + // nrf_gpio_cfg_default(WB_I2C2_SDA); #endif #endif #ifdef MESHLINK diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index cc0c417d3..f582a116d 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -143,10 +143,26 @@ void portduinoSetup() { printf("Set up Meshtastic on Portduino...\n"); int max_GPIO = 0; - const configNames GPIO_lines[] = { - cs_pin, irq_pin, busy_pin, reset_pin, sx126x_ant_sw_pin, txen_pin, - rxen_pin, displayDC, displayCS, displayBacklight, displayBacklightPWMChannel, displayReset, - touchscreenCS, touchscreenIRQ, user}; + const configNames GPIO_lines[] = {cs_pin, + irq_pin, + busy_pin, + reset_pin, + sx126x_ant_sw_pin, + txen_pin, + rxen_pin, + displayDC, + displayCS, + displayBacklight, + displayBacklightPWMChannel, + displayReset, + touchscreenCS, + touchscreenIRQ, + userButtonPin, + tbUpPin, + tbDownPin, + tbLeftPin, + tbRightPin, + tbPressPin}; std::string gpioChipName = "gpiochip"; settingsStrings[i2cdev] = ""; @@ -159,6 +175,11 @@ void portduinoSetup() settingsMap[ascii_logs] = !isatty(1); settingsMap[displayPanel] = no_screen; settingsMap[touchscreenModule] = no_touchscreen; + settingsMap[tbUpPin] = RADIOLIB_NC; + settingsMap[tbDownPin] = RADIOLIB_NC; + settingsMap[tbLeftPin] = RADIOLIB_NC; + settingsMap[tbRightPin] = RADIOLIB_NC; + settingsMap[tbPressPin] = RADIOLIB_NC; YAML::Node yamlConfig; @@ -313,9 +334,34 @@ void portduinoSetup() // Need to bind all the configured GPIO pins so they're not simulated // TODO: If one of these fails, we should log and terminate - if (settingsMap.count(user) > 0 && settingsMap[user] != RADIOLIB_NC) { - if (initGPIOPin(settingsMap[user], defaultGpioChipName, settingsMap[user]) != ERRNO_OK) { - settingsMap[user] = RADIOLIB_NC; + if (settingsMap.count(userButtonPin) > 0 && settingsMap[userButtonPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[userButtonPin], defaultGpioChipName, settingsMap[userButtonPin]) != ERRNO_OK) { + settingsMap[userButtonPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbUpPin) > 0 && settingsMap[tbUpPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbUpPin], defaultGpioChipName, settingsMap[tbUpPin]) != ERRNO_OK) { + settingsMap[tbUpPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbDownPin) > 0 && settingsMap[tbDownPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbDownPin], defaultGpioChipName, settingsMap[tbDownPin]) != ERRNO_OK) { + settingsMap[tbDownPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbLeftPin) > 0 && settingsMap[tbLeftPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbLeftPin], defaultGpioChipName, settingsMap[tbLeftPin]) != ERRNO_OK) { + settingsMap[tbLeftPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbRightPin) > 0 && settingsMap[tbRightPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbRightPin], defaultGpioChipName, settingsMap[tbRightPin]) != ERRNO_OK) { + settingsMap[tbRightPin] = RADIOLIB_NC; + } + } + if (settingsMap.count(tbPressPin) > 0 && settingsMap[tbPressPin] != RADIOLIB_NC) { + if (initGPIOPin(settingsMap[tbPressPin], defaultGpioChipName, settingsMap[tbPressPin]) != ERRNO_OK) { + settingsMap[tbPressPin] = RADIOLIB_NC; } } if (settingsMap[displayPanel] != no_screen) { @@ -377,6 +423,8 @@ int initGPIOPin(int pinNum, const std::string gpioChipName, int line) { #ifdef PORTDUINO_LINUX_HARDWARE std::string gpio_name = "GPIO" + std::to_string(pinNum); + std::cout << gpio_name; + printf("\n"); try { GPIOPin *csPin; csPin = new LinuxGPIOPin(pinNum, gpioChipName.c_str(), line, gpio_name.c_str()); @@ -498,7 +546,7 @@ bool loadConfig(const char *configPath) } } if (yamlConfig["GPIO"]) { - settingsMap[user] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); + settingsMap[userButtonPin] = yamlConfig["GPIO"]["User"].as(RADIOLIB_NC); } if (yamlConfig["GPS"]) { std::string serialPath = yamlConfig["GPS"]["SerialPath"].as(""); @@ -588,6 +636,12 @@ bool loadConfig(const char *configPath) if (yamlConfig["Input"]) { settingsStrings[keyboardDevice] = (yamlConfig["Input"]["KeyboardDevice"]).as(""); settingsStrings[pointerDevice] = (yamlConfig["Input"]["PointerDevice"]).as(""); + settingsMap[userButtonPin] = yamlConfig["Input"]["User"].as(RADIOLIB_NC); + settingsMap[tbUpPin] = yamlConfig["Input"]["TrackballUp"].as(RADIOLIB_NC); + settingsMap[tbDownPin] = yamlConfig["Input"]["TrackballDown"].as(RADIOLIB_NC); + settingsMap[tbLeftPin] = yamlConfig["Input"]["TrackballLeft"].as(RADIOLIB_NC); + settingsMap[tbRightPin] = yamlConfig["Input"]["TrackballRight"].as(RADIOLIB_NC); + settingsMap[tbPressPin] = yamlConfig["Input"]["TrackballPress"].as(RADIOLIB_NC); } if (yamlConfig["Webserver"]) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index d324aaf47..43aea4218 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -57,7 +57,12 @@ enum configNames { lora_usb_serial_num, lora_usb_pid, lora_usb_vid, - user, + userButtonPin, + tbUpPin, + tbDownPin, + tbLeftPin, + tbRightPin, + tbPressPin, spidev, spiSpeed, i2cdev, diff --git a/src/platform/portduino/architecture.h b/src/platform/portduino/architecture.h index a5e263d5a..07d0aeee0 100644 --- a/src/platform/portduino/architecture.h +++ b/src/platform/portduino/architecture.h @@ -8,6 +8,9 @@ #define HW_VENDOR meshtastic_HardwareModel_PORTDUINO +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif #ifndef HAS_WIFI #define HAS_WIFI 1 #endif @@ -22,4 +25,12 @@ #endif #ifndef HAS_SENSOR #define HAS_SENSOR 1 +#endif +#ifndef HAS_TRACKBALL +#define HAS_TRACKBALL 1 +#define TB_DOWN (uint8_t) settingsMap[tbDownPin] +#define TB_UP (uint8_t) settingsMap[tbUpPin] +#define TB_LEFT (uint8_t) settingsMap[tbLeftPin] +#define TB_RIGHT (uint8_t) settingsMap[tbRightPin] +#define TB_PRESS (uint8_t) settingsMap[tbPressPin] #endif \ No newline at end of file diff --git a/src/power.h b/src/power.h index d7fa7f8a9..33a356d92 100644 --- a/src/power.h +++ b/src/power.h @@ -78,8 +78,8 @@ extern NullSensor ina3221Sensor; #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) -#include "modules/Telemetry/Sensor/MAX17048Sensor.h" #if __has_include() +#include "modules/Telemetry/Sensor/MAX17048Sensor.h" extern MAX17048Sensor max17048Sensor; #else extern NullSensor max17048Sensor; diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index 2c1dc0ca7..fc8531298 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -61,40 +61,97 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - msgPayload["battery_level"] = new JSONValue((unsigned int)decoded->variant.device_metrics.battery_level); + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + msgPayload["battery_level"] = new JSONValue((int)decoded->variant.device_metrics.battery_level); + } msgPayload["voltage"] = new JSONValue(decoded->variant.device_metrics.voltage); msgPayload["channel_utilization"] = new JSONValue(decoded->variant.device_metrics.channel_utilization); msgPayload["air_util_tx"] = new JSONValue(decoded->variant.device_metrics.air_util_tx); msgPayload["uptime_seconds"] = new JSONValue((unsigned int)decoded->variant.device_metrics.uptime_seconds); } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); - msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); - msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); - msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); - msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); - msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); - msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); - msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); - msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); - msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); - msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); - msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); - msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); - msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + // Avoid sending 0s for sensors that could be 0 + if (decoded->variant.environment_metrics.has_temperature) { + msgPayload["temperature"] = new JSONValue(decoded->variant.environment_metrics.temperature); + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); + } + if (decoded->variant.environment_metrics.has_voltage) { + msgPayload["voltage"] = new JSONValue(decoded->variant.environment_metrics.voltage); + } + if (decoded->variant.environment_metrics.has_current) { + msgPayload["current"] = new JSONValue(decoded->variant.environment_metrics.current); + } + if (decoded->variant.environment_metrics.has_lux) { + msgPayload["lux"] = new JSONValue(decoded->variant.environment_metrics.lux); + } + if (decoded->variant.environment_metrics.has_white_lux) { + msgPayload["white_lux"] = new JSONValue(decoded->variant.environment_metrics.white_lux); + } + if (decoded->variant.environment_metrics.has_iaq) { + msgPayload["iaq"] = new JSONValue((uint)decoded->variant.environment_metrics.iaq); + } + if (decoded->variant.environment_metrics.has_wind_speed) { + msgPayload["wind_speed"] = new JSONValue(decoded->variant.environment_metrics.wind_speed); + } + if (decoded->variant.environment_metrics.has_wind_direction) { + msgPayload["wind_direction"] = new JSONValue((uint)decoded->variant.environment_metrics.wind_direction); + } + if (decoded->variant.environment_metrics.has_wind_gust) { + msgPayload["wind_gust"] = new JSONValue(decoded->variant.environment_metrics.wind_gust); + } + if (decoded->variant.environment_metrics.has_wind_lull) { + msgPayload["wind_lull"] = new JSONValue(decoded->variant.environment_metrics.wind_lull); + } + if (decoded->variant.environment_metrics.has_radiation) { + msgPayload["radiation"] = new JSONValue(decoded->variant.environment_metrics.radiation); + } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); - msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); - msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); - msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); - msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); - msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + msgPayload["pm10"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_standard); + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + msgPayload["pm25"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_standard); + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); - msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); - msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); - msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); - msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); - msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + if (decoded->variant.power_metrics.has_ch1_voltage) { + msgPayload["voltage_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_voltage); + } + if (decoded->variant.power_metrics.has_ch1_current) { + msgPayload["current_ch1"] = new JSONValue(decoded->variant.power_metrics.ch1_current); + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + msgPayload["voltage_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_voltage); + } + if (decoded->variant.power_metrics.has_ch2_current) { + msgPayload["current_ch2"] = new JSONValue(decoded->variant.power_metrics.ch2_current); + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + msgPayload["voltage_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_voltage); + } + if (decoded->variant.power_metrics.has_ch3_current) { + msgPayload["current_ch3"] = new JSONValue(decoded->variant.power_metrics.ch3_current); + } } jsonObj["payload"] = new JSONValue(msgPayload); } else if (shouldLog) { diff --git a/src/serialization/MeshPacketSerializer_nRF52.cpp b/src/serialization/MeshPacketSerializer_nRF52.cpp index 89ecddfad..e0daa1a88 100644 --- a/src/serialization/MeshPacketSerializer_nRF52.cpp +++ b/src/serialization/MeshPacketSerializer_nRF52.cpp @@ -58,40 +58,96 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, if (pb_decode_from_bytes(mp->decoded.payload.bytes, mp->decoded.payload.size, &meshtastic_Telemetry_msg, &scratch)) { decoded = &scratch; if (decoded->which_variant == meshtastic_Telemetry_device_metrics_tag) { - jsonObj["payload"]["battery_level"] = (unsigned int)decoded->variant.device_metrics.battery_level; + // If battery is present, encode the battery level value + // TODO - Add a condition to send a code for a non-present value + if (decoded->variant.device_metrics.has_battery_level) { + jsonObj["payload"]["battery_level"] = (int)decoded->variant.device_metrics.battery_level; + } jsonObj["payload"]["voltage"] = decoded->variant.device_metrics.voltage; jsonObj["payload"]["channel_utilization"] = decoded->variant.device_metrics.channel_utilization; jsonObj["payload"]["air_util_tx"] = decoded->variant.device_metrics.air_util_tx; jsonObj["payload"]["uptime_seconds"] = (unsigned int)decoded->variant.device_metrics.uptime_seconds; } else if (decoded->which_variant == meshtastic_Telemetry_environment_metrics_tag) { - jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; - jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; - jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; - jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; - jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; - jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; - jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; - jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; - jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; - jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; - jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; - jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; - jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; - jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + if (decoded->variant.environment_metrics.has_temperature) { + jsonObj["payload"]["temperature"] = decoded->variant.environment_metrics.temperature; + } + if (decoded->variant.environment_metrics.has_relative_humidity) { + jsonObj["payload"]["relative_humidity"] = decoded->variant.environment_metrics.relative_humidity; + } + if (decoded->variant.environment_metrics.has_barometric_pressure) { + jsonObj["payload"]["barometric_pressure"] = decoded->variant.environment_metrics.barometric_pressure; + } + if (decoded->variant.environment_metrics.has_gas_resistance) { + jsonObj["payload"]["gas_resistance"] = decoded->variant.environment_metrics.gas_resistance; + } + if (decoded->variant.environment_metrics.has_voltage) { + jsonObj["payload"]["voltage"] = decoded->variant.environment_metrics.voltage; + } + if (decoded->variant.environment_metrics.has_current) { + jsonObj["payload"]["current"] = decoded->variant.environment_metrics.current; + } + if (decoded->variant.environment_metrics.has_lux) { + jsonObj["payload"]["lux"] = decoded->variant.environment_metrics.lux; + } + if (decoded->variant.environment_metrics.has_white_lux) { + jsonObj["payload"]["white_lux"] = decoded->variant.environment_metrics.white_lux; + } + if (decoded->variant.environment_metrics.has_iaq) { + jsonObj["payload"]["iaq"] = (uint)decoded->variant.environment_metrics.iaq; + } + if (decoded->variant.environment_metrics.has_wind_speed) { + jsonObj["payload"]["wind_speed"] = decoded->variant.environment_metrics.wind_speed; + } + if (decoded->variant.environment_metrics.has_wind_direction) { + jsonObj["payload"]["wind_direction"] = (uint)decoded->variant.environment_metrics.wind_direction; + } + if (decoded->variant.environment_metrics.has_wind_gust) { + jsonObj["payload"]["wind_gust"] = decoded->variant.environment_metrics.wind_gust; + } + if (decoded->variant.environment_metrics.has_wind_lull) { + jsonObj["payload"]["wind_lull"] = decoded->variant.environment_metrics.wind_lull; + } + if (decoded->variant.environment_metrics.has_radiation) { + jsonObj["payload"]["radiation"] = decoded->variant.environment_metrics.radiation; + } } else if (decoded->which_variant == meshtastic_Telemetry_air_quality_metrics_tag) { - jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; - jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; - jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; - jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; - jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; - jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + if (decoded->variant.air_quality_metrics.has_pm10_standard) { + jsonObj["payload"]["pm10"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_standard; + } + if (decoded->variant.air_quality_metrics.has_pm25_standard) { + jsonObj["payload"]["pm25"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_standard; + } + if (decoded->variant.air_quality_metrics.has_pm100_standard) { + jsonObj["payload"]["pm100"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_standard; + } + if (decoded->variant.air_quality_metrics.has_pm10_environmental) { + jsonObj["payload"]["pm10_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm10_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm25_environmental) { + jsonObj["payload"]["pm25_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm25_environmental; + } + if (decoded->variant.air_quality_metrics.has_pm100_environmental) { + jsonObj["payload"]["pm100_e"] = (unsigned int)decoded->variant.air_quality_metrics.pm100_environmental; + } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { - jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; - jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; - jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; - jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; - jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; - jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + if (decoded->variant.power_metrics.has_ch1_voltage) { + jsonObj["payload"]["voltage_ch1"] = decoded->variant.power_metrics.ch1_voltage; + } + if (decoded->variant.power_metrics.has_ch1_current) { + jsonObj["payload"]["current_ch1"] = decoded->variant.power_metrics.ch1_current; + } + if (decoded->variant.power_metrics.has_ch2_voltage) { + jsonObj["payload"]["voltage_ch2"] = decoded->variant.power_metrics.ch2_voltage; + } + if (decoded->variant.power_metrics.has_ch2_current) { + jsonObj["payload"]["current_ch2"] = decoded->variant.power_metrics.ch2_current; + } + if (decoded->variant.power_metrics.has_ch3_voltage) { + jsonObj["payload"]["voltage_ch3"] = decoded->variant.power_metrics.ch3_voltage; + } + if (decoded->variant.power_metrics.has_ch3_current) { + jsonObj["payload"]["current_ch3"] = decoded->variant.power_metrics.ch3_current; + } } } else if (shouldLog) { LOG_ERROR("Error decoding proto for telemetry message!"); diff --git a/src/shutdown.h b/src/shutdown.h index f02cb7964..998944677 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -41,8 +41,8 @@ void powerCommandsCheck() } #if defined(ARCH_ESP32) || defined(ARCH_NRF52) - if (shutdownAtMsec) { - screen->startAlert("Shutting down..."); + if (shutdownAtMsec && screen) { + screen->showOverlayBanner("Shutting Down...", 0); // stays on screen } #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 8ffb08b04..09484f46e 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -221,8 +221,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); - - screen->doDeepSleep(); // datasheet says this will draw only 10ua + if (screen) + screen->doDeepSleep(); // datasheet says this will draw only 10ua if (!skipSaveNodeDb) { nodeDB->saveToDisk(); @@ -332,7 +332,7 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN } #endif -#if defined(ARCH_ESP32) && defined(I2C_SDA) +#if !MESHTASTIC_EXCLUDE_I2C && defined(ARCH_ESP32) && defined(I2C_SDA) // Added by https://github.com/meshtastic/firmware/pull/4418 // Possibly to support Heltec Capsule Sensor? Wire.end(); @@ -542,4 +542,4 @@ void enableLoraInterrupt() } #endif } -#endif \ No newline at end of file +#endif diff --git a/suppressions.txt b/suppressions.txt index 04937523d..ab57c9298 100644 --- a/suppressions.txt +++ b/suppressions.txt @@ -53,4 +53,8 @@ internalAstError:*/CrossPlatformCryptoEngine.cpp uninitMemberVar:*/AudioThread.h // False positive constVariableReference:*/Channels.cpp -constParameterPointer:*/unishox2.c \ No newline at end of file +constParameterPointer:*/unishox2.c + +useStlAlgorithm + +variableScope \ No newline at end of file diff --git a/userPrefs.jsonc b/userPrefs.jsonc index a349a5700..497327478 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -21,6 +21,7 @@ // "USERPREFS_CONFIG_LORA_REGION": "meshtastic_Config_LoRaConfig_RegionCode_US", // "USERPREFS_CONFIG_OWNER_LONG_NAME": "My Long Name", // "USERPREFS_CONFIG_OWNER_SHORT_NAME": "MLN", + // "USERPREFS_CONFIG_DEVICE_ROLE": "meshtastic_Config_DeviceConfig_Role_CLIENT", // Defaults to CLIENT. ROUTER*, LOST AND FOUND, and REPEATER roles are restricted. // "USERPREFS_EVENT_MODE": "1", // "USERPREFS_FIXED_BLUETOOTH": "121212", // "USERPREFS_FIXED_GPS": "", diff --git a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h index c2c351925..f3b709261 100644 --- a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -22,6 +22,9 @@ #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -98,8 +101,14 @@ void setupNicheGraphics() buttons->setWiring(1, PIN_BUTTON1); buttons->setTiming(1, 50, 500); // 500ms before latch buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); - buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); - buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + buttons->setHandlerLongPress(1, [backlight]() { + backlight->latch(); + playBeep(); + }); + buttons->setHandlerShortPress(1, [backlight]() { + backlight->off(); + playBoop(); + }); // Begin handling button events buttons->start(); diff --git a/variants/ELECROW-ThinkNode-M1/variant.h b/variants/ELECROW-ThinkNode-M1/variant.h index 2e91e378d..79e31c54a 100644 --- a/variants/ELECROW-ThinkNode-M1/variant.h +++ b/variants/ELECROW-ThinkNode-M1/variant.h @@ -63,6 +63,9 @@ extern "C" { * Buttons */ #define PIN_BUTTON2 (32 + 10) +#define ALT_BUTTON_PIN PIN_BUTTON2 +#define ALT_BUTTON_ACTIVE_LOW true +#define ALT_BUTTON_ACTIVE_PULLUP true #define PIN_BUTTON1 (32 + 7) // #define PIN_BUTTON1 (0 + 11) diff --git a/variants/ELECROW-ThinkNode-M2/variant.h b/variants/ELECROW-ThinkNode-M2/variant.h index a6bb40f1a..cd8d43555 100644 --- a/variants/ELECROW-ThinkNode-M2/variant.h +++ b/variants/ELECROW-ThinkNode-M2/variant.h @@ -3,6 +3,9 @@ #define PIN_BUTTON1 47 // 功能键 #define PIN_BUTTON2 4 // 电源键 +#define ALT_BUTTON_PIN PIN_BUTTON2 +#define ALT_BUTTON_ACTIVE_LOW false +#define ALT_BUTTON_ACTIVE_PULLUP false #define LED_POWER 6 #define ADC_V 42 @@ -60,4 +63,3 @@ #define HAS_GPS 0 #define BUTTON_PIN PIN_BUTTON1 -#define BUTTON_PIN_ALT PIN_BUTTON2 diff --git a/variants/diy/hydra/variant.h b/variants/diy/hydra/variant.h index 60bb60beb..4c809502e 100644 --- a/variants/diy/hydra/variant.h +++ b/variants/diy/hydra/variant.h @@ -8,7 +8,9 @@ #define PIN_GPS_EN 4 #define GPS_POWER_TOGGLE // Moved definition from platformio.ini to here -#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +// Note: On the ESP32 base version, gpio34-39 are input-only, and do not have internal pull-ups. +// If 39 is not being used for a button, it is suggested to remove the #define. #define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO35_CHANNEL #define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) diff --git a/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py b/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py new file mode 100644 index 000000000..00896e21f --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py @@ -0,0 +1,62 @@ +# Simplifies DIY InkHUD builds, with presets for several common E-Ink displays +# - build using custom task in Platformio's "Project Tasks" panel +# - build with `pio run -e -t build_weact_154` (or similar) + +# Silence trunk's objections to the import statements +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821) + +from SCons.Script import COMMAND_LINE_TARGETS + +Import("env") +Import("projenv") + +# Custom targets +# These wrappers just run the normal build task under a different target name +# We intercept the build later on, based on the target name +env.AddTarget( + name="build_weact_154", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 1.54")', +) +env.AddTarget( + name="build_weact_213", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.13")', +) +env.AddTarget( + name="build_weact_290", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.9")', +) +env.AddTarget( + name="build_weact_420", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 4.2")', +) + +# Check whether a build was started via one of our custom targets above + +if "build_weact_154" in COMMAND_LINE_TARGETS: + print('Building for WeAct 1.54" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_213" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.13" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) + +elif "build_weact_290" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.9" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_420" in COMMAND_LINE_TARGETS: + print('Building for WeAct 4.2" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) diff --git a/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h b/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h new file mode 100644 index 000000000..bbd530595 --- /dev/null +++ b/variants/diy/nrf52_promicro_diy_tcxo/nicheGraphics.h @@ -0,0 +1,94 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" +#include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" +#include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" +#include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" + +#include "graphics/niche/Inputs/TwoButton.h" + +#if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) +#error If not using a DIY preset, display model and resilience must be set manually +#endif + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + SPI.begin(); + + // Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // Setup the main user button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true); // Internal pull up + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/diy/nrf52_promicro_diy_tcxo/variant.h b/variants/diy/nrf52_promicro_diy_tcxo/variant.h index de49018f4..e93442c7e 100644 --- a/variants/diy/nrf52_promicro_diy_tcxo/variant.h +++ b/variants/diy/nrf52_promicro_diy_tcxo/variant.h @@ -22,26 +22,26 @@ extern "C" { /* NRF52 PRO MICRO PIN ASSIGNMENT -| Pin | Function | | Pin | Function | RF95 | +| Pin   | Function   |   | Pin     | Function     | RF95 | | ----- | ----------- | --- | -------- | ------------ | ----- | -| Gnd | | | vbat | | | -| P0.06 | Serial2 RX | | vbat | | | -| P0.08 | Serial2 TX | | Gnd | | | -| Gnd | | | reset | | | -| Gnd | | | ext_vcc | *see 0.13 | | -| P0.17 | RXEN | | P0.31 | BATTERY_PIN | | -| P0.20 | GPS_RX | | P0.29 | BUSY | DIO0 | -| P0.22 | GPS_TX | | P0.02 | MISO | MISO | -| P0.24 | GPS_EN | | P1.15 | MOSI | MOSI | -| P1.00 | BUTTON_PIN | | P1.13 | CS | CS | -| P0.11 | SCL | | P1.11 | SCK | SCK | -| P1.04 | SDA | | P0.10 | DIO1/IRQ | DIO1 | -| P1.06 | Free pin | | P0.09 | RESET | RST | -| | | | | | | -| | Mid board | | | Internal | | -| P1.01 | Free pin | | 0.15 | LED | | -| P1.02 | Free pin | | 0.13 | 3V3_EN | | -| P1.07 | Free pin | | | | | +| Gnd   |             |   | vbat     |             | | +| P0.06 | Serial2 RX |   | vbat     |             | | +| P0.08 | Serial2 TX |   | Gnd     |             | | +| Gnd   |             |   | reset   |             | | +| Gnd   |             |   | ext_vcc | *see 0.13   | | +| P0.17 | RXEN       |   | P0.31   | BATTERY_PIN | | +| P0.20 | GPS_RX     |   | P0.29   | BUSY         | DIO0 | +| P0.22 | GPS_TX     |   | P0.02   | MISO | MISO | +| P0.24 | GPS_EN     |   | P1.15   | MOSI         | MOSI | +| P1.00 | BUTTON_PIN |   | P1.13   | CS           | CS   | +| P0.11 | SCL         |   | P1.11   | SCK         | SCK | +| P1.04 | SDA         |   | P0.10   | DIO1/IRQ     | DIO1 | +| P1.06 | Free pin   |   | P0.09   | RESET       | RST | +|       |             |   |         |             | | +|       | Mid board   |   |         | Internal     | | +| P1.01 | Free pin   |   | 0.15     | LED         | | +| P1.02 | Free pin   |   | 0.13     | 3V3_EN       | | +| P1.07 | Free pin   |   |         |             | | */ // Number of pins defined in PinDescription array @@ -185,6 +185,12 @@ settings. #define SX126X_DIO3_TCXO_VOLTAGE 1.8 #define TCXO_OPTIONAL // make it so that the firmware can try both TCXO and XTAL +// E-Ink DIY +#define PIN_EINK_CS (32 + 7) +#define PIN_EINK_DC (32 + 2) +#define PIN_EINK_RES (32 + 1) +#define PIN_EINK_BUSY (32 + 6) + #ifdef __cplusplus } #endif diff --git a/variants/diy/platformio.ini b/variants/diy/platformio.ini index d8ceee9cc..153796daf 100644 --- a/variants/diy/platformio.ini +++ b/variants/diy/platformio.ini @@ -68,6 +68,34 @@ lib_deps = ${nrf52840_base.lib_deps} debug_tool = jlink +; NRF52 ProMicro w/ E-Ink display +[env:nrf52_promicro_diy-inkhud] +board_level = extra +extends = nrf52840_base, inkhud +board = promicro-nrf52840 +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/diy/nrf52_promicro_diy_tcxo + -D NRF52_PROMICRO_DIY +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} + +<../variants/diy/nrf52_promicro_diy_tcxo> +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${nrf52840_base.lib_deps} +extra_scripts = + ${env.extra_scripts} + variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays + +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} -D PRIVATE_HW -DXIAO_BLE_LEGACY_PINOUT -DEBYTE_E22 -DEBYTE_E22_900M30S +build_unflags = -DGPS_L76K + ; Seeed XIAO nRF52840 + XIAO Wio SX1262 DIY [env:seeed-xiao-nrf52840-wio-sx1262] board = xiao_ble_sense diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp index 2c6c3e539..300f69d0b 100644 --- a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.cpp @@ -52,4 +52,11 @@ const uint32_t g_ADigitalPinMap[] = { // VBAT 31, // D32 is P0.10 (VBAT) -}; \ No newline at end of file +}; + +void initVariant() +{ + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); +} diff --git a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h index 7a76727f2..277377d71 100644 --- a/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h +++ b/variants/diy/seeed-xiao-nrf52840-wio-sx1262/variant.h @@ -164,7 +164,8 @@ static const uint8_t SCK = PIN_SPI_SCK; // ------- // P0_14 = 14 Reads battery voltage from divider on signal board. // PIN_VBAT is reading voltage divider on XIAO and is program pin 32 / or P0.31 -#define BAT_READ 14 +#define ADC_CTRL VBAT_ENABLE +#define ADC_CTRL_ENABLED LOW #define BATTERY_SENSE_RESOLUTION_BITS 10 #define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED #define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge diff --git a/variants/xiao_ble/README.md b/variants/diy/xiao_ble/README.md similarity index 100% rename from variants/xiao_ble/README.md rename to variants/diy/xiao_ble/README.md diff --git a/variants/elecrow_panel/platformio.ini b/variants/elecrow_panel/platformio.ini index 963174560..5bce58208 100644 --- a/variants/elecrow_panel/platformio.ini +++ b/variants/elecrow_panel/platformio.ini @@ -48,7 +48,7 @@ lib_deps = ${esp32s3_base.lib_deps} lovyan03/LovyanGFX@1.2.0 ; note: v1.2.7 breaks the elecrow 7" display functionality hideakitai/TCA9534@0.1.1 -[crowpanel_small] ; 2.4, 2.8, 3.5 inch +[crowpanel_small_esp32s3_base] ; 2.4, 2.8, 3.5 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} @@ -62,7 +62,7 @@ build_flags = -D VIEW_320x240 -D MAP_FULL_REDRAW -[crowpanel_large] ; 4.3, 5.0, 7.0 inch +[crowpanel_large_esp32s3_base] ; 4.3, 5.0, 7.0 inch extends = crowpanel_base build_flags = ${crowpanel_base.build_flags} @@ -73,9 +73,9 @@ build_flags = -D DISPLAY_SET_RESOLUTION [env:elecrow-adv-24-28-tft] -extends = crowpanel_small +extends = crowpanel_small_esp32s3_base build_flags = - ${crowpanel_small.build_flags} + ${crowpanel_small_esp32s3_base.build_flags} -D SPI_FREQUENCY=80000000 -D LGFX_SCREEN_WIDTH=240 -D LGFX_SCREEN_HEIGHT=320 @@ -96,9 +96,9 @@ build_flags = -D LGFX_TOUCH_ROTATION=0 [env:elecrow-adv-35-tft] -extends = crowpanel_small +extends = crowpanel_small_esp32s3_base build_flags = - ${crowpanel_small.build_flags} + ${crowpanel_small_esp32s3_base.build_flags} -D LV_CACHE_DEF_SIZE=2097152 -D SPI_FREQUENCY=60000000 -D LGFX_SCREEN_WIDTH=320 @@ -122,7 +122,7 @@ build_flags = ; 4.3, 5.0, 7.0 inch 800x480 IPS (V1) [env:elecrow-adv1-43-50-70-tft] -extends = crowpanel_large +extends = crowpanel_large_esp32s3_base build_flags = - ${crowpanel_large.build_flags} + ${crowpanel_large_esp32s3_base.build_flags} -D VIEW_320x240 diff --git a/variants/gat562_mesh_trial_tracker/platformio.ini b/variants/gat562_mesh_trial_tracker/platformio.ini new file mode 100644 index 000000000..e67f3ec8d --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/platformio.ini @@ -0,0 +1,13 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:gat562_mesh_trial_tracker] +extends = nrf52840_base +board = gat562_mesh_trial_tracker +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/gat562_mesh_trial_tracker -D GAT562_MESH_TRIAL_TRACKER + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/gat562_mesh_trial_tracker> +lib_deps = + ${nrf52840_base.lib_deps} diff --git a/variants/gat562_mesh_trial_tracker/variant.cpp b/variants/gat562_mesh_trial_tracker/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/gat562_mesh_trial_tracker/variant.h b/variants/gat562_mesh_trial_tracker/variant.h new file mode 100644 index 000000000..2af0bc76d --- /dev/null +++ b/variants/gat562_mesh_trial_tracker/variant.h @@ -0,0 +1,288 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_GAT562_MESH_TRIAL_TRACKER_ +#define _VARIANT_GAT562_MESH_TRIAL_TRACKER_ + +#define GAT562_MESH_TRIAL_TRACKER + +// led pin 2 (blue), see https://github.com/meshtastic/firmware/blob/master/src/mesh/NodeDB.cpp#L723 +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +// #define PIN_EINK_CS (0 + 26) +// #define PIN_EINK_BUSY (0 + 4) +// #define PIN_EINK_DC (0 + 17) +// #define PIN_EINK_RES (-1) +// #define PIN_EINK_SCLK (0 + 3) +// #define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// Display - OLED connected via I2C +#define HAS_SCREEN 1 +#define USE_SSD1306 + +// RAKRGB +// #define HAS_NCP5623 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +// configure the SET pin on the RAK12039 sensor board to disable the sensor while not reading +// air quality telemetry. PIN_NFC2 doesn't seem to be used anywhere else in the codebase, but if +// you're having problems with your node behaving weirdly when a RAK12039 board isn't connected, +// try disabling this. +// #define PMSA003I_ENABLE_PIN PIN_NFC2 + +// #define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_BAUDRATE 9600 + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +// #define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +// #define HAS_RTC 1 + +// #define HAS_ETHERNET 1 + +// #define RAK_4631 1 + +// #define PIN_ETHERNET_RESET 21 +// #define PIN_ETHERNET_SS PIN_EINK_CS +// #define ETH_SPI_PORT SPI1 +// #define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/heltec_capsule_sensor_v3/variant.h b/variants/heltec_capsule_sensor_v3/variant.h index 415de0559..b30b7fc3e 100644 --- a/variants/heltec_capsule_sensor_v3/variant.h +++ b/variants/heltec_capsule_sensor_v3/variant.h @@ -3,6 +3,8 @@ #define EXT_PWR_DETECT 35 #define BUTTON_PIN 18 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false #define BATTERY_PIN 7 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO7_CHANNEL diff --git a/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py b/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py new file mode 100644 index 000000000..00896e21f --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/custom_build_tasks.py @@ -0,0 +1,62 @@ +# Simplifies DIY InkHUD builds, with presets for several common E-Ink displays +# - build using custom task in Platformio's "Project Tasks" panel +# - build with `pio run -e -t build_weact_154` (or similar) + +# Silence trunk's objections to the import statements +# trunk-ignore-all(ruff/F821) +# trunk-ignore-all(flake8/F821) + +from SCons.Script import COMMAND_LINE_TARGETS + +Import("env") +Import("projenv") + +# Custom targets +# These wrappers just run the normal build task under a different target name +# We intercept the build later on, based on the target name +env.AddTarget( + name="build_weact_154", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 1.54")', +) +env.AddTarget( + name="build_weact_213", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.13")', +) +env.AddTarget( + name="build_weact_290", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 2.9")', +) +env.AddTarget( + name="build_weact_420", + dependencies=["buildprog"], + actions=None, + title='Build (WeAct 4.2")', +) + +# Check whether a build was started via one of our custom targets above + +if "build_weact_154" in COMMAND_LINE_TARGETS: + print('Building for WeAct 1.54" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY200200_0154DAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_213" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.13" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E0213A289")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "10")) + +elif "build_weact_290" in COMMAND_LINE_TARGETS: + print('Building for WeAct 2.9" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "ZJY128296_029EAAMFGN")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) + +elif "build_weact_420" in COMMAND_LINE_TARGETS: + print('Building for WeAct 4.2" Display') + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DRIVER", "HINK_E042A87")) + projenv["CPPDEFINES"].append(("INKHUD_BUILDCONF_DISPLAYRESILIENCE", "15")) diff --git a/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h b/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h new file mode 100644 index 000000000..fe1c281bf --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/nicheGraphics.h @@ -0,0 +1,95 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/EInk/HINK_E0213A289.h" // WeAct 2.13" +#include "graphics/niche/Drivers/EInk/HINK_E042A87.h" // WeAct 4.2" +#include "graphics/niche/Drivers/EInk/ZJY128296_029EAAMFGN.h" // WeAct 2.9" +#include "graphics/niche/Drivers/EInk/ZJY200200_0154DAAMFGN.h" // WeACt 1.54" + +#include "graphics/niche/Inputs/TwoButton.h" + +#if !defined(INKHUD_BUILDCONF_DRIVER) || !defined(INKHUD_BUILDCONF_DISPLAYRESILIENCE) +// cppcheck-suppress preprocessorErrorDirective +#error If not using a DIY preset, display model and resilience must be set manually +#endif + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + SPI1.begin(); + + // Driver + // ----------------------------- + + // Use E-Ink driver + Drivers::EInk *driver = new Drivers::INKHUD_BUILDCONF_DRIVER; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update. + inkhud->setDisplayResilience(INKHUD_BUILDCONF_DISPLAYRESILIENCE); // Suggest roughly ten + + // Prepare fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Init settings, and customize defaults + // Values ignored individually if found saved to flash + inkhud->persistence->settings.rotation = (driver->height > driver->width ? 1 : 0); // Rotate 90deg to landscape, if needed + inkhud->persistence->settings.userTiles.maxCount = 4; + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet); + inkhud->addApplet("DMs", new InkHUD::DMApplet, true, false, 3); // Default on tile 3 + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0), true, false, 2); // Default on tile 2 + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true, false, 1); // Default on tile 1 + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet, true, false, 0); // Default on tile 0 + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true); // Background + + // Start running InkHUD + inkhud->begin(); + + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/heltec_mesh_node_t114-inkhud/platformio.ini b/variants/heltec_mesh_node_t114-inkhud/platformio.ini new file mode 100644 index 000000000..9a5673040 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/platformio.ini @@ -0,0 +1,19 @@ +[env:heltec-mesh-node-t114-inkhud] +board_level = extra +extends = nrf52840_base, inkhud +board = heltec_mesh_node_t114 +board_check = true +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/heltec_mesh_node_t114-inkhud +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX + ${nrf52840_base.lib_deps} + lewisxhe/PCF8563_Library@^1.0.1 +extra_scripts = + ${env.extra_scripts} + variants/diy/nrf52_promicro_diy_tcxo/custom_build_tasks.py ; Add to PIO's Project Tasks pane: preset builds for common displays \ No newline at end of file diff --git a/variants/heltec_mesh_node_t114-inkhud/variant.cpp b/variants/heltec_mesh_node_t114-inkhud/variant.cpp new file mode 100644 index 000000000..85c9f4a72 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/variant.cpp @@ -0,0 +1,38 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); +} diff --git a/variants/heltec_mesh_node_t114-inkhud/variant.h b/variants/heltec_mesh_node_t114-inkhud/variant.h new file mode 100644 index 000000000..39cbc8f01 --- /dev/null +++ b/variants/heltec_mesh_node_t114-inkhud/variant.h @@ -0,0 +1,175 @@ +// Unlike many other InkHUD variants, this environment does require its own variant.h file +// This is because the default T114 variant maps SPI1 pins to the optional TFT display, and those pins are not broken out + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define HELTEC_MESH_NODE_T114 + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (32 + 3) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_STATE_ON 0 // State when LED is lit + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 2 // How many neopixels are connected +#define NEOPIXEL_DATA 14 // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) +// #define PIN_SERIAL2_EN (0 + 17) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +// Routed to footprint for PCF8563TS RTC +// Not populated on T114 V1, maybe in future? +#define PIN_WIRE_SDA (0 + 26) // P0.26 +#define PIN_WIRE_SCL (0 + 27) // P0.27 + +// I2C bus 1 +// Available on header pins, for general use +#define PIN_WIRE1_SDA (0 + 16) // P0.16 +#define PIN_WIRE1_SCL (0 + 13) // P0.13 + +/* + * Lora radio + */ + +#define USE_SX1262 +// #define USE_SX1268 +#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +// #define SX1262_DIO3 (0 + 21) +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the +// main +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// Not really an E22 but TTGO seems to be trying to clone that +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * E-Ink DIY + */ +#define PIN_EINK_MOSI (0 + 8) // also called SDA +#define PIN_EINK_SCLK (0 + 7) +#define PIN_EINK_CS (32 + 12) +#define PIN_EINK_DC (32 + 14) +#define PIN_EINK_RES (0 + 5) +#define PIN_EINK_BUSY (32 + 15) + +/* + * GPS pins + */ + +#define GPS_L76K + +// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +#define GPS_RESET_MODE LOW +// #define PIN_GPS_EN (21) +#define VEXT_ENABLE (0 + 21) +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +// #define GPS_EN_ACTIVE HIGH +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (32 + 4) +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +#define PIN_SPI1_MISO -1 +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (4) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution + +#define ADC_CTRL 6 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 4 +#define ADC_RESOLUTION 14 + +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER (4.90F) + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h index 426085a26..798c3538a 100644 --- a/variants/heltec_mesh_node_t114/variant.h +++ b/variants/heltec_mesh_node_t114/variant.h @@ -56,6 +56,7 @@ extern "C" { #define TFT_WIDTH 240 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 + // #define TFT_OFFSET_ROTATION 0 // #define SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/heltec_mesh_pocket/platformio.ini b/variants/heltec_mesh_pocket/platformio.ini index 6632c10fe..2f3886887 100644 --- a/variants/heltec_mesh_pocket/platformio.ini +++ b/variants/heltec_mesh_pocket/platformio.ini @@ -28,7 +28,7 @@ lib_deps = https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d -[env:heltec-mesh-pocket-inkhud-5000] +[env:heltec-mesh-pocket-5000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/heltec_mesh_pocket> ${inkhud.build_src_filter} @@ -73,7 +73,7 @@ lib_deps = https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d -[env:heltec-mesh-pocket-inkhud-10000] +[env:heltec-mesh-pocket-10000-inkhud] extends = nrf52840_base, inkhud board = heltec_mesh_pocket build_src_filter = ${nrf52_base.build_src_filter} +<../variants/heltec_mesh_pocket> ${inkhud.build_src_filter} diff --git a/variants/heltec_sensor_hub/variant.h b/variants/heltec_sensor_hub/variant.h index 771cefee3..8c5d31c9a 100644 --- a/variants/heltec_sensor_hub/variant.h +++ b/variants/heltec_sensor_hub/variant.h @@ -1,6 +1,8 @@ #define EXT_PWR_DETECT 20 #define BUTTON_PIN 17 +#define BUTTON_ACTIVE_LOW false +#define BUTTON_ACTIVE_PULLUP false #define BATTERY_PIN 7 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO7_CHANNEL diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index 7eccb2955..26f393f6c 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -21,6 +21,9 @@ #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -84,8 +87,11 @@ void setupNicheGraphics() buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button - buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playBoop(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e213/platformio.ini b/variants/heltec_vision_master_e213/platformio.ini index 037d10168..34cebb6e3 100644 --- a/variants/heltec_vision_master_e213/platformio.ini +++ b/variants/heltec_vision_master_e213/platformio.ini @@ -32,7 +32,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_vision_master_e213 -D HELTEC_VISION_MASTER_E213 - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/heltec_vision_master_e213/variant.h b/variants/heltec_vision_master_e213/variant.h index ebb2c341f..60f4e00cc 100644 --- a/variants/heltec_vision_master_e213/variant.h +++ b/variants/heltec_vision_master_e213/variant.h @@ -1,7 +1,7 @@ #define LED_PIN 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h index af78df746..f3cf6355e 100644 --- a/variants/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -34,6 +34,9 @@ Different NicheGraphics UIs and different hardware variants will each have their #include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -97,8 +100,11 @@ void setupNicheGraphics() buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); // #1: Aux Button - buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setWiring(1, PIN_BUTTON2); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playBoop(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e290/platformio.ini b/variants/heltec_vision_master_e290/platformio.ini index 6952e9f9e..cda3fde00 100644 --- a/variants/heltec_vision_master_e290/platformio.ini +++ b/variants/heltec_vision_master_e290/platformio.ini @@ -36,7 +36,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_vision_master_e290 -D HELTEC_VISION_MASTER_E290 - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/heltec_vision_master_e290/variant.h b/variants/heltec_vision_master_e290/variant.h index 02986d26b..d7bae7dc2 100644 --- a/variants/heltec_vision_master_e290/variant.h +++ b/variants/heltec_vision_master_e290/variant.h @@ -1,7 +1,7 @@ #define LED_PIN 45 // LED is not populated on earliest board variant #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA diff --git a/variants/heltec_vision_master_t190/variant.h b/variants/heltec_vision_master_t190/variant.h index 788466919..a6a809207 100644 --- a/variants/heltec_vision_master_t190/variant.h +++ b/variants/heltec_vision_master_t190/variant.h @@ -1,6 +1,7 @@ +#ifndef HAS_TFT #define BUTTON_PIN 0 -#define BUTTON_PIN_SECONDARY 21 // Second built-in button -#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input +#define PIN_BUTTON2 21 // Second built-in button +#define ALT_BUTTON_PIN PIN_BUTTON2 // Send the up event // I2C #define I2C_SDA SDA @@ -68,4 +69,5 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif // HAS_TFT \ No newline at end of file diff --git a/variants/heltec_wireless_bridge/platformio.ini b/variants/heltec_wireless_bridge/platformio.ini index 45c3aba74..ab30eb744 100644 --- a/variants/heltec_wireless_bridge/platformio.ini +++ b/variants/heltec_wireless_bridge/platformio.ini @@ -3,4 +3,21 @@ extends = esp32_base board = heltec_wifi_lora_32 build_flags = - ${esp32_base.build_flags} -D HELTEC_WIRELESS_BRIDGE -I variants/heltec_wireless_bridge \ No newline at end of file + ${esp32_base.build_flags} + -I variants/heltec_wireless_bridge + -D HELTEC_WIRELESS_BRIDGE + -D BOARD_HAS_PSRAM + -D RADIOLIB_EXCLUDE_LR11X0=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 + -D MESHTASTIC_EXCLUDE_DETECTIONSENSOR=1 + -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 + -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL=1 + -D MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1 + -D MESHTASTIC_EXCLUDE_GPS=1 + -D MESHTASTIC_EXCLUDE_I2C=1 + -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 + -D MESHTASTIC_EXCLUDE_POWER_FSM=1 + -D MESHTASTIC_EXCLUDE_SERIAL=1 + -D MESHTASTIC_EXCLUDE_SCREEN=1 + -D MESHTASTIC_EXCLUDE_WAYPOINT=1 diff --git a/variants/heltec_wireless_bridge/variant.h b/variants/heltec_wireless_bridge/variant.h index 7c4f41660..5ad16d0e2 100644 --- a/variants/heltec_wireless_bridge/variant.h +++ b/variants/heltec_wireless_bridge/variant.h @@ -1,29 +1,41 @@ -// the default ESP32 Pin of 15 is the Oled SCL, set to 36 and 37 and works fine. -// Tested on Neo6m module. + +// updated variant 20250420 berlincount, tested with HTIT-TB +// +// connections in HTIT-WB +// per https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +// md5: a0e6ae10ff76611aa61433366b2e4f5c esp32_datasheet_en.pdf +// per https://resource.heltec.cn/download/Wireless_Bridge/Schematic_Diagram_HTIT-WB_V0.2.pdf +// md5: d5c1b0219ece347dd8cee866d7d3ab0a Schematic_Diagram_HTIT-WB_V0.2.pdf + +#define NO_EXT_GPIO 1 +#define NO_GPS 1 + +#define HAS_GPS 0 // GPS is not equipped #undef GPS_RX_PIN #undef GPS_TX_PIN -#define GPS_RX_PIN 36 -#define GPS_TX_PIN 33 -#ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag -#define I2C_SDA 4 // I2C pins for this board -#define I2C_SCL 15 -#endif - -#define LED_PIN 25 // If defined we will blink this LED -#define BUTTON_PIN 0 // If defined, this will be used for user button presses +// Green / Lora = PIN 22 / GPIO2, Yellow / Wifi = PIN 23 / GPIO0, Blue / BLE = PIN 25 / GPIO16 +#define LED_PIN 22 +#define WIFI_LED 23 +#define BLE_LED 25 +// ESP32-D0WDQ6 direct pins SX1276 #define USE_RF95 -#define LORA_DIO0 26 // a No connect on the SX1262 module -#ifndef USE_JTAG -#define LORA_RESET 14 -#endif +#define LORA_DIO0 26 #define LORA_DIO1 35 -#define LORA_DIO2 34 // Not really used +#define LORA_DIO2 34 +#define LORA_SCK 05 +#define LORA_MISO 19 +#define LORA_MOSI 27 +#define LORA_CS 18 -// ratio of voltage divider = 3.20 (R1=100k, R2=220k) -#define ADC_MULTIPLIER 3.2 +// several things are not possible with JTAG enabled +#ifndef USE_JTAG +#define LORA_RESET 14 // LoRa Reset shares a pin with MTMS +#define I2C_SDA 4 // SD_DATA1 going to W25Q64, but +#define I2C_SCL 15 // SD_CMD shared a pin with MTD0 +#endif -#define BATTERY_PIN 13 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage -#define ADC_CHANNEL ADC2_GPIO13_CHANNEL -#define BAT_MEASURE_ADC_UNIT 2 \ No newline at end of file +// user button is present on device, but currently untested & unconfigured - couldn't figure out how it's connected + +// battery support is present within device, but currently untested & unconfigured - couldn't find reliable information yet diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 51430ebff..ce5b5e533 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -33,7 +33,6 @@ build_flags = ${inkhud.build_flags} -I variants/heltec_wireless_paper -D HELTEC_WIRELESS_PAPER - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} diff --git a/variants/link32_s3_v1/pins_arduino.h b/variants/link32_s3_v1/pins_arduino.h new file mode 100644 index 000000000..010e5bf2e --- /dev/null +++ b/variants/link32_s3_v1/pins_arduino.h @@ -0,0 +1,19 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// The default Wire will be mapped to PMU and RTC +static const uint8_t SDA = 47; +static const uint8_t SCL = 48; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 21; +static const uint8_t MOSI = 34; +static const uint8_t MISO = 33; +static const uint8_t SCK = 16; + +#endif /* Pins_Arduino_h */ diff --git a/variants/link32_s3_v1/platformio.ini b/variants/link32_s3_v1/platformio.ini new file mode 100644 index 000000000..5a614a7af --- /dev/null +++ b/variants/link32_s3_v1/platformio.ini @@ -0,0 +1,11 @@ +[env:link32-s3-v1] +extends = esp32s3_base +board = esp32-s3-devkitc-1 +build_flags = + ${esp32_base.build_flags} -D LINK_32 -I variants/link32_s3_v1 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DARDUINO_USB_CDC_ON_BOOT + -DARDUINO_USB_MODE=1 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 diff --git a/variants/link32_s3_v1/variant.h b/variants/link32_s3_v1/variant.h new file mode 100644 index 000000000..a16c0ff68 --- /dev/null +++ b/variants/link32_s3_v1/variant.h @@ -0,0 +1,44 @@ +#define BATTERY_PIN 15 +#define ADC_CHANNEL ADC2_GPIO15_CHANNEL // ADC channel for battery voltage measurement +#define BATTERY_SENSE_SAMPLES 30 +#define BAT_MEASURE_ADC_UNIT 2 // Use ADC2 for battery measurement + +#define USE_SSD1306 + +#define BUTTON_PIN 0 // Button pin for this board +#define CANCEL_BUTTON_PIN 36 +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP true + +#define HAS_NEOPIXEL // If defined, we will use the neopixel library +#define NEOPIXEL_DATA 35 // Neopixel pin for this board +#define NEOPIXEL_COUNT 1 // Number of neopixels on this board +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +#define ADC_MULTIPLIER 2 + +#define I2C_SDA 47 // I2C pins for this board +#define I2C_SCL 48 + +#define USE_SX1262 + +#define LORA_SCK 16 +#define LORA_MISO 33 +#define LORA_MOSI 34 +#define LORA_CS 21 +#define LORA_RESET 18 + +#define LORA_DIO0 12 // a No connect on the SX1262 module +#define LORA_DIO1 13 +#define LORA_DIO2 14 // Not really used + +#define LORA_TCXO_GPIO 17 + +#define TCXO_OPTIONAL + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 diff --git a/variants/mesh-tab/platformio.ini b/variants/mesh-tab/platformio.ini index 728fa5100..beeb58a48 100644 --- a/variants/mesh-tab/platformio.ini +++ b/variants/mesh-tab/platformio.ini @@ -28,7 +28,6 @@ build_flags = ${esp32s3_base.build_flags} -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D RADIOLIB_SPI_PARANOID=0 - -D MAX_THREADS=40 -D HAS_SCREEN=0 -D HAS_TFT=1 -D USE_PIN_BUZZER diff --git a/variants/nano-g1-explorer/variant.h b/variants/nano-g1-explorer/variant.h index 3d5d71acc..f3640241a 100644 --- a/variants/nano-g1-explorer/variant.h +++ b/variants/nano-g1-explorer/variant.h @@ -3,9 +3,7 @@ #define I2C_SDA 21 #define I2C_SCL 22 -#define BUTTON_PIN 36 // The user button (information button) GPIO on the Nano G1 explorer -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The user button (information button) GPIO on the Nano G1 explorer #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/nano-g1/variant.h b/variants/nano-g1/variant.h index dd8355492..2521c3ffe 100644 --- a/variants/nano-g1/variant.h +++ b/variants/nano-g1/variant.h @@ -3,9 +3,7 @@ #define I2C_SDA 21 #define I2C_SCL 22 -#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/nugget_s3_lora/platformio.ini b/variants/nugget_s3_lora/platformio.ini index 729a3ef23..1085d633b 100644 --- a/variants/nugget_s3_lora/platformio.ini +++ b/variants/nugget_s3_lora/platformio.ini @@ -2,5 +2,5 @@ extends = esp32s3_base board = lolin_s3_mini board_level = extra -build_flags = - ${esp32s3_base.build_flags} -D PRIVATE_HW -I variants/nugget_s3_lora \ No newline at end of file +build_flags = + ${esp32s3_base.build_flags} -D ARDUINO_USB_CDC_ON_BOOT=1 -D PRIVATE_HW -I variants/nugget_s3_lora diff --git a/variants/nugget_s3_lora/variant.h b/variants/nugget_s3_lora/variant.h index 488fe4e44..8e6057d5b 100644 --- a/variants/nugget_s3_lora/variant.h +++ b/variants/nugget_s3_lora/variant.h @@ -1,5 +1,8 @@ -#define I2C_SDA 34 // I2C pins for this board -#define I2C_SCL 38 +#define I2C_SDA 35 // I2C pins for this board +#define I2C_SCL 36 + +#define USE_SSD1306 +#define DISPLAY_FLIP_SCREEN #define LED_PIN 15 // If defined we will blink this LED @@ -8,7 +11,8 @@ #define NEOPIXEL_DATA 10 // gpio pin used to send data to the neopixels #define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use -#define BUTTON_PIN 0 // If defined, this will be used for user button presses +// Button A (44), B (43), R (12), U (13), L (11), D (18) +#define BUTTON_PIN 44 // If defined, this will be used for user button presses #define BUTTON_NEED_PULLUP #define USE_RF95 diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini index df2d0dfdc..b861b5496 100644 --- a/variants/picomputer-s3/platformio.ini +++ b/variants/picomputer-s3/platformio.ini @@ -26,21 +26,15 @@ extends = env:picomputer-s3 build_flags = ${env:picomputer-s3.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_MATRIX_TYPE=1 -D USE_PIN_BUZZER=PIN_BUZZER -D USE_SX127x - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1024 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D RAM_SIZE=1560 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 -D LV_USE_PERF_MONITOR=0 @@ -51,7 +45,7 @@ build_flags = -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" -D VIEW_320x240 -; -D USE_DOUBLE_BUFFER +; -D USE_DOUBLE_BUFFER -D USE_PACKET_API lib_deps = diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index fe89ad6e6..6da827508 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -26,7 +26,8 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 + -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LIBINPUT=1 @@ -41,7 +42,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-fb] extends = native_base @@ -56,7 +56,7 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -D USE_FRAMEBUFFER=1 -D LV_COLOR_DEPTH=32 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D LV_BUILD_TEST=0 -D LV_USE_LOG=0 -D LV_USE_EVDEV=1 @@ -72,7 +72,6 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-tft-debug] extends = native_base diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index a64cd43bf..bdb04ff71 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -18,6 +18,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 https://github.com/RAKWireless/RAK13800-W5100S/archive/1.0.2.zip rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip https://github.com/Woutvstk/SdFat_wrapper25.git#6f8f48d56c15cbeac753560dfeede4a487f81f4c @@ -53,4 +54,4 @@ lib_deps = upload_protocol = stlink ; eventually use platformio/tool-pyocd@^2.3600.0 instad ;upload_protocol = custom -;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index d73e64655..86b525156 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -88,8 +88,13 @@ static const uint8_t A7 = PIN_A7; #define ADC_RESOLUTION 14 // Other pins + #define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT + #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT + #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -159,8 +164,8 @@ static const uint8_t SCK = PIN_SPI_SCK; */ #define WIRE_INTERFACES_COUNT 1 -#define PIN_WIRE_SDA (13) -#define PIN_WIRE_SCL (14) +#define PIN_WIRE_SDA (WB_I2C1_SDA) +#define PIN_WIRE_SCL (WB_I2C1_SCL) // QSPI Pins #define PIN_QSPI_SCK 3 @@ -235,10 +240,15 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // Testing USB detection #define NRF_APM +// If using a power chip like the INA3221 you can override the default battery voltage channel below +// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging +// #define INA3221_BAT_CH INA3221_CH2 +// #define INA3221_ENV_CH INA3221_CH1 // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -292,4 +302,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_epaper/platformio.ini b/variants/rak4631_epaper/platformio.ini index 7c8a299bb..47e4451c7 100644 --- a/variants/rak4631_epaper/platformio.ini +++ b/variants/rak4631_epaper/platformio.ini @@ -16,6 +16,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak4631_epaper/variant.h b/variants/rak4631_epaper/variant.h index 0bb97498c..c1e11bee5 100644 --- a/variants/rak4631_epaper/variant.h +++ b/variants/rak4631_epaper/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -188,6 +190,7 @@ static const uint8_t SCK = PIN_SPI_SCK; // enables 3.3V periphery like GPS or IO Module #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -231,4 +234,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_epaper_onrxtx/platformio.ini b/variants/rak4631_epaper_onrxtx/platformio.ini index c749fc686..52a13f2a7 100644 --- a/variants/rak4631_epaper_onrxtx/platformio.ini +++ b/variants/rak4631_epaper_onrxtx/platformio.ini @@ -18,6 +18,7 @@ lib_deps = melopero/Melopero RV3028@^1.1.0 rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 beegee-tokyo/RAKwireless RAK12034@^1.0.0 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak4631_epaper_onrxtx/variant.h b/variants/rak4631_epaper_onrxtx/variant.h index 5888cff33..1f8257e8e 100644 --- a/variants/rak4631_epaper_onrxtx/variant.h +++ b/variants/rak4631_epaper_onrxtx/variant.h @@ -69,7 +69,9 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) -// #define PIN_NFC1 (9) +#define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) // #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -160,6 +162,7 @@ static const uint8_t SCK = PIN_SPI_SCK; // enables 3.3V periphery like GPS or IO Module #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // NO GPS #undef GPS_RX_PIN @@ -202,4 +205,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_eth_gw/variant.h b/variants/rak4631_eth_gw/variant.h index bc5541336..c8a2f83ae 100644 --- a/variants/rak4631_eth_gw/variant.h +++ b/variants/rak4631_eth_gw/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -217,6 +219,7 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN // RAK1910 GPS module // If using the wisblock GPS module and pluged into Port A on WisBlock base @@ -270,4 +273,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/rak4631_nomadstar_meteor_pro/platformio.ini b/variants/rak4631_nomadstar_meteor_pro/platformio.ini new file mode 100644 index 000000000..d5fbe6a16 --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/platformio.ini @@ -0,0 +1,51 @@ +; NomadStar Meteor Pro based on RAK4631 with RGBW LED LP5562 support +[env:rak4631_nomadstar_meteor_pro] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} -Ivariants/rak4631_nomadstar_meteor_pro -D NOMADSTAR_METEOR_PRO + -L "${platformio.libdeps_dir}/${this.__env__}/bsec2/src/cortex-m4/fpv4-sp-d16-hard" + ;-DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DEINK_DISPLAY_MODEL=GxEPD2_213_BN + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631_nomadstar_meteor_pro> + + +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/NomadStar-outdoor/IOBoard-RGB-LP5562-Library.git#9c366c8 + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" +[env:rak4631_nomadstar_meteor_pro_dbg] +extends = env:rak4631_nomadstar_meteor_pro +board_level = extra + +; if the builtin version of openocd has a buggy version of semihosting, so use the external version +; platform_packages = platformio/tool-openocd@^3.1200.0 + +build_flags = + ${env:rak4631.build_flags} + -D USE_SEMIHOSTING + +lib_deps = + ${env:rak4631.lib_deps} + https://github.com/geeksville/Armduino-Semihosting/archive/35b538fdf208c3530c1434cd099a08e486672ee4.zip + +; NOTE: the pyocd support for semihosting is buggy. So I switched to using the builtin platformio support for the stlink adapter which worked much better. +; However the built in openocd version in platformio has buggy support for TCP to semihosting. +; +; So I'm now trying the external openocd - but the openocd scripts for nrf52.cfg assume you are using a DAP adapter not an STLINK adapter. +; In theory I could change those scripts. But for now I'm trying going back to a DAP adapter but with the external openocd. + +upload_protocol = stlink +; eventually use platformio/tool-pyocd@^2.3600.0 instad +;upload_protocol = custom +;upload_command = pyocd flash -t nrf52840 $UPLOADERFLAGS $SOURCE \ No newline at end of file diff --git a/variants/rak4631_nomadstar_meteor_pro/variant.cpp b/variants/rak4631_nomadstar_meteor_pro/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/rak4631_nomadstar_meteor_pro/variant.h b/variants/rak4631_nomadstar_meteor_pro/variant.h new file mode 100644 index 000000000..51baf3ada --- /dev/null +++ b/variants/rak4631_nomadstar_meteor_pro/variant.h @@ -0,0 +1,271 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK4630_ +#define _VARIANT_RAK4630_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ + +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +// #define USE_EINK + +// Texas Instrument LP5562 +#define HAS_LP5562 +#define ENABLE_AMBIENTLIGHTING + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2 + A1 <-> P0.31/AIN7 (Arduino Analog A7 + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define DETECTION_SENSOR_EN 4 + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 0 + +#define HAS_ETHERNET 0 + +#define RAK_4631 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS PIN_EINK_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak_wismeshtap/platformio.ini b/variants/rak_wismeshtap/platformio.ini index 6ed97c7ad..bfb3ea927 100644 --- a/variants/rak_wismeshtap/platformio.ini +++ b/variants/rak_wismeshtap/platformio.ini @@ -22,6 +22,7 @@ lib_deps = bodmer/TFT_eSPI beegee-tokyo/RAKwireless RAK12034@^1.0.0 beegee-tokyo/RAK14014-FT6336U @ 1.0.1 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink diff --git a/variants/rak_wismeshtap/variant.h b/variants/rak_wismeshtap/variant.h index 1980dc4a1..f961ddf6e 100644 --- a/variants/rak_wismeshtap/variant.h +++ b/variants/rak_wismeshtap/variant.h @@ -90,6 +90,8 @@ static const uint8_t A7 = PIN_A7; // Other pins #define PIN_AREF (2) #define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) #define PIN_NFC2 (10) static const uint8_t AREF = PIN_AREF; @@ -176,11 +178,11 @@ static const uint8_t SCK = PIN_SPI_SCK; // No reason not to have the RAK Wireless pin defs here too. This allows code from example RAK sketches to run without // modification. -static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B -static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B -static const uint8_t WB_IO3 = 21; // SLOT_C -static const uint8_t WB_IO4 = 4; // SLOT_C -static const uint8_t WB_IO5 = 9; // SLOT_D +static const uint8_t WB_IO1 = 17; // SLOT_A SLOT_B +static const uint8_t WB_IO2 = 34; // SLOT_A SLOT_B +static const uint8_t WB_IO3 = 21; // SLOT_C +// static const uint8_t WB_IO4 = 4; // SLOT_C <- already defined above (ln. 94) +// static const uint8_t WB_IO5 = 9; // SLOT_D <- already defined above (ln. 93) static const uint8_t WB_IO6 = 10; // SLOT_D static const uint8_t WB_SW1 = 33; // IO_SLOT static const uint8_t WB_A0 = 5; // IO_SLOT @@ -314,4 +316,4 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif \ No newline at end of file +#endif diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index b643288a6..2187ebd8a 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -36,17 +36,9 @@ upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_SCREEN=1 - -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=38 - -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=4096 diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index 1010e04c8..8915395f3 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -7,9 +7,10 @@ #define SENSOR_PORT_NUM 2 #define SENSOR_BAUD_RATE 115200 -#if !HAS_TFT #define BUTTON_PIN 38 -#endif +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true + // #define BUTTON_NEED_PULLUP // #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage diff --git a/variants/seeed_wio_tracker_L1/platformio.ini b/variants/seeed_wio_tracker_L1/platformio.ini new file mode 100644 index 000000000..3c4653d7e --- /dev/null +++ b/variants/seeed_wio_tracker_L1/platformio.ini @@ -0,0 +1,13 @@ +[env:seeed_wio_tracker_L1] +board = seeed_wio_tracker_L1 +extends = nrf52840_base +;board_level = extra +build_flags = ${nrf52840_base.build_flags} + -I $PROJECT_DIR/variants/seeed_wio_tracker_L1 + -D SEEED_WIO_TRACKER_L1 + -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/seeed_wio_tracker_L1> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink diff --git a/variants/seeed_wio_tracker_L1/variant.cpp b/variants/seeed_wio_tracker_L1/variant.cpp new file mode 100644 index 000000000..a045b0cf9 --- /dev/null +++ b/variants/seeed_wio_tracker_L1/variant.cpp @@ -0,0 +1,96 @@ +/* + * variant.cpp - Digital pin mapping for TRACKER L1 + * + * This file defines the pin mapping array that maps logical digital pins (D0-D17) + * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. + * + * Board: [Seeed Studio WIO TRACKER L1] + * Hardware Features: + * - LoRa module (CS/SCK/MISO/MOSI control pins) + * - GNSS module (TX/RX/Reset/Wakeup) + * - User LEDs (D11-D12) + * - User button (D13) + * - Grove/NFC interface (D14-D15) + * - Battery voltage monitoring (D16) + * + * Created [20250521] + * By [Dylan] + */ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +/** + * @brief Digital pin to GPIO port/pin mapping table + * + * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) + * + */ + +extern "C" { +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1,07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 40, // D5 P1.08 (A5/SCL) LORA_SW + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.1 User LED + // Buzzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - Grove interface + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVESDA + 44, // D18 P0.01 GROVESCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + 4, // D30 BAT_CTL +}; +} + +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); + + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); + pinMode(PIN_LED2, OUTPUT); +} \ No newline at end of file diff --git a/variants/seeed_wio_tracker_L1/variant.h b/variants/seeed_wio_tracker_L1/variant.h new file mode 100644 index 000000000..0c5964c5a --- /dev/null +++ b/variants/seeed_wio_tracker_L1/variant.h @@ -0,0 +1,187 @@ +#ifndef _SEEED_TRACKER_L1_H_ +#define _SEEED_TRACKER_L1_H_ +#include "WVariant.h" +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Clock Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define VARIANT_MCK (64000000ul) // Master clock frequency +#define USE_LFXO // 32.768kHz crystal for LFCLK + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Pin Capacity Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PINS_COUNT (33u) // Total GPIO pins +#define NUM_DIGITAL_PINS (33u) // Digital I/O pins +#define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) +#define NUM_ANALOG_OUTPUTS (0u) + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LED Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LEDs +// LEDs +#define PIN_LED1 (11) // LED P1.15 +#define PIN_LED2 (12) // + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 +// #define LED_PIN PIN_LED2 +#define LED_STATE_ON 1 // State when LED is litted +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Button Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define CANCEL_BUTTON_PIN D13 // This is the Program Button +// #define BUTTON_NEED_PULLUP 1 +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false + +// #define BUTTON_PIN_TOUCH 13 // Touch button +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Digital Pin Mapping (D0-D10) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define D0 0 // P1.06 GNSS_WAKEUP/IO0 +#define D1 1 // P0.07 LORA_DIO1 +#define D2 2 // P1.07 LORA_RESET +#define D3 3 // P1.10 LORA_BUSY +#define D4 4 // P1.14 LORA_CS +#define D5 5 // P1.08 LORA_SW +#define D6 6 // P0.27 GNSS_TX +#define D7 7 // P0.26 GNSS_RX +#define D8 8 // P0.30 SPI_SCK +#define D9 9 // P0.03 SPI_MISO +#define D10 10 // P0.28 SPI_MOSI +#define D12 12 // P1.00 Buzzer +#define D13 13 // P0.08 User Button +#define D14 14 // P0.05 OLED SCL +#define D15 15 // P0.06 OLED SDA +#define D16 16 // P0.31 VBAT_ADC +#define D17 17 // P0.00 GROVE SDA +#define D18 18 // P0.01 GROVE_SCL +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Analog Pin Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_A0 0 // P0.02 Analog Input 0 +#define PIN_A1 1 // P0.03 Analog Input 1 +#define PIN_A2 2 // P0.28 Analog Input 2 +#define PIN_A3 3 // P0.29 Analog Input 3 +#define PIN_A4 4 // P0.04 Analog Input 4 +#define PIN_A5 5 // P0.05 Analog Input 5 +#define PIN_VBAT D16 // P0.31 Battery voltage sense +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Communication Interfaces +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// I2C Configuration +// #define HAS_WIRE 1 +#define PIN_WIRE_SDA D14 // P0.09 +#define PIN_WIRE_SCL D15 // P0.10 +#define WIRE_INTERFACES_COUNT 2 +#define PIN_WIRE1_SDA D18 +#define PIN_WIRE1_SCL D17 +#define I2C_NO_RESCAN + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +#define HAS_SCREEN 1 +#define USE_SSD1306 1 + +// SPI Configuration (SX1262) + +#define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO 9 // P0.03 (D9) +#define PIN_SPI_MOSI 10 // P0.28 (D10) +#define PIN_SPI_SCK 8 // P0.30 (D8) + +// SX1262 LoRa Module Pins +#define USE_SX1262 +#define SX126X_CS D4 // Chip select +#define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) +#define SX126X_BUSY D3 // Busy status +#define SX126X_RESET D2 // Reset control +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage +#define SX126X_RXEN D5 // RX enable control +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Power Management +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +#define BAT_READ 30 // D30 = P0.04 Reads battery voltage from divider on signal board. +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define ADC_MULTIPLIER 2.0 +#define BATTERY_PIN PIN_VBAT // PIN_A7 +#define AREF_VOLTAGE 3.6 +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// GPS L76KB +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define GPS_L76K +#ifdef GPS_L76K +#define PIN_GPS_RX D6 // P0.26 +#define PIN_GPS_TX D7 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define GPS_RX_PIN PIN_GPS_TX +#define GPS_TX_PIN PIN_GPS_RX +#define PIN_GPS_STANDBY D0 + +// #define GPS_DEBUG +// #define GPS_EN D18 // P1.05 +#endif + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Buzzer +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Buzzer + +#define PIN_BUZZER D12 // P1.00, pwm output + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// joystick +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// trackball +#define HAS_TRACKBALL 1 +#define TB_UP 25 +#define TB_DOWN 26 +#define TB_LEFT 27 +#define TB_RIGHT 28 +#define TB_PRESS 29 +#define TB_DIRECTION FALLING +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Compatibility Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#ifdef __cplusplus +extern "C" { +#endif +// Serial port placeholders + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) +#ifdef __cplusplus +} +#endif + +#endif // _SEEED_SOLAR_NODE_H_ \ No newline at end of file diff --git a/variants/seeed_xiao_nrf52840_kit/platformio.ini b/variants/seeed_xiao_nrf52840_kit/platformio.ini index 0a8bee31c..8c4c5a57b 100644 --- a/variants/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/seeed_xiao_nrf52840_kit/platformio.ini @@ -2,7 +2,7 @@ [env:seeed_xiao_nrf52840_kit] extends = nrf52840_base board = xiao_ble_sense -build_flags = ${nrf52840_base.build_flags} -Ivariants/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT +build_flags = ${nrf52840_base.build_flags} -Ivariants/seeed_xiao_nrf52840_kit -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -DSEEED_XIAO_NRF52840_KIT -DGPS_L76K board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld build_src_filter = ${nrf52_base.build_src_filter} +<../variants/seeed_xiao_nrf52840_kit> lib_deps = diff --git a/variants/seeed_xiao_nrf52840_kit/variant.cpp b/variants/seeed_xiao_nrf52840_kit/variant.cpp index 22072312a..70cadf5db 100644 --- a/variants/seeed_xiao_nrf52840_kit/variant.cpp +++ b/variants/seeed_xiao_nrf52840_kit/variant.cpp @@ -34,9 +34,9 @@ const uint32_t g_ADigitalPinMap[] = { 11, // D18 is P0.11 (6D_INT1) // MIC - 42, // 17,//42, // D19 is P1.10 (MIC_PWR) - 32, // 26,//32, // D20 is P1.00 (PDM_CLK) - 16, // 25,//16, // D21 is P0.16 (PDM_DATA) + 42, // D19 is P1.10 (MIC_PWR) + 32, // D20 is P1.00 (PDM_CLK) + 16, // D21 is P0.16 (PDM_DATA) // BQ25100 13, // D22 is P0.13 (HICHG) @@ -80,13 +80,17 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { - // LED1 & LED2 - pinMode(21, OUTPUT); - digitalWrite(21, LOW); - // LED1 & LED2 - pinMode(22, OUTPUT); - digitalWrite(22, LOW); + // Set BQ25101 ISET to 100mA instead of 50mA + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); - pinMode(PIN_WIRE_SDA, INPUT_PULLUP); - pinMode(PIN_WIRE_SCL, INPUT_PULLUP); -} \ No newline at end of file + // LEDs + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + pinMode(PIN_LED3, OUTPUT); + ledOff(PIN_LED3); +} diff --git a/variants/seeed_xiao_nrf52840_kit/variant.h b/variants/seeed_xiao_nrf52840_kit/variant.h index 869c3d405..d2bbfdda9 100644 --- a/variants/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/seeed_xiao_nrf52840_kit/variant.h @@ -1,5 +1,5 @@ -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ +#ifndef _SEEED_XIAO_NRF52840_KIT_H_ +#define _SEEED_XIAO_NRF52840_KIT_H_ /** Master clock frequency */ #define VARIANT_MCK (64000000ul) @@ -19,31 +19,12 @@ extern "C" { #define PINS_COUNT (33) #define NUM_DIGITAL_PINS (33) -#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference +#define NUM_ANALOG_INPUTS (8) #define NUM_ANALOG_OUTPUTS (0) -// LEDs - -#define LED_RED 11 -#define LED_BLUE 12 -#define LED_GREEN 13 - -#define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE -#define PIN_LED3 LED_RED - -#define PIN_LED PIN_LED1 -#define LED_PWR (PINS_COUNT) - -#define LED_BUILTIN PIN_LED - -#define LED_STATE_ON 1 // State when LED is lit - /* - * Buttons + * Digital Pins */ - -// Digital PINs #define D0 (0ul) #define D1 (1ul) #define D2 (2ul) @@ -56,15 +37,6 @@ extern "C" { #define D9 (9ul) #define D10 (10ul) -/*Due to the lack of pins,and have to make sure gps standby work well we have temporarily removed the button. -There are some technical solutions that can solve this problem, -and we are currently exploring and researching them*/ - -// #define BUTTON_PIN D0 // This is the Program Button -// // #define BUTTON_NEED_PULLUP 1 -// #define BUTTON_ACTIVE_LOW true -// #define BUTTON_ACTIVE_PULLUP false - /* * Analog pins */ @@ -85,79 +57,153 @@ static const uint8_t A4 = PIN_A4; static const uint8_t A5 = PIN_A5; #define ADC_RESOLUTION 12 +/* + * LEDs + */ +#define LED_STATE_ON (0) // RGB LED is common anode +#define LED_RED (11) +#define LED_GREEN (13) +#define LED_BLUE (12) + +#define PIN_LED1 LED_GREEN // PIN_LED1 is used in src/platform/nrf52/architecture.h to define LED_PIN +#define PIN_LED2 LED_BLUE +#define PIN_LED3 LED_RED + +#define LED_BUILTIN LED_RED // LED_BUILTIN is used by framework-arduinoadafruitnrf52 to indicate flash writes + +#define LED_PWR LED_RED +#define USER_LED LED_BLUE + +/* + * Buttons + */ + +/* + * D0 is shared with PIN_GPS_STANDBY on the L76K GNSS Module, so refer to + * GPS_L76K definition preventing this conflict + */ + +// #define BUTTON_PIN D0 + +/* + * Serial Interfaces + */ #define PIN_SERIAL2_RX (-1) #define PIN_SERIAL2_TX (-1) /* - * SPI Interfaces + * Pinout for SX126x */ -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D4; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -// supported modules list #define USE_SX1262 -// common pinouts for SX126X modules +#ifdef XIAO_BLE_LEGACY_PINOUT +// Legacy xiao_ble variant pinout for third-party SX126x modules e.g. EBYTE E22 +#define SX126X_CS D0 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D2 +#define SX126X_RESET D3 +#define SX126X_RXEN D7 +#elif defined(SEEED_XIAO_WIO_BTB) +// Wio-SX1262 for XIAO with 30-pin board-to-board connector +// https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Schematic_Diagram_Wio-SX1262_for_XIAO.pdf +#define SX126X_CS D3 +#define SX126X_DIO1 D0 +#define SX126X_BUSY D1 +#define SX126X_RESET D2 +#define SX126X_RXEN D4 +#else +// Wio-SX1262 for XIAO (standalone SKU 113010003 or nRF52840 kit SKU 102010710) +// https://files.seeedstudio.com/products/SenseCAP/Wio_SX1262/Wio-SX1262%20for%20XIAO%20V1.0_SCH.pdf #define SX126X_CS D4 #define SX126X_DIO1 D1 #define SX126X_BUSY D3 #define SX126X_RESET D2 +#define SX126X_RXEN D5 +#endif +// Common pinouts for all SX126x pinouts above #define SX126X_TXEN RADIOLIB_NC - -#define SX126X_RXEN D5 // This is used to control the RX side of the RF switch #define SX126X_DIO2_AS_RF_SWITCH // DIO2 is used to control the TX side of the RF switch #define SX126X_DIO3_TCXO_VOLTAGE 1.8 /* - * Wire Interfaces + * SPI Interfaces + * Defined after pinout for SX1262x to factor in CS pinout variations */ +#define SPI_INTERFACES_COUNT 1 -#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much -#define WIRE_INTERFACES_COUNT 1 // 2 +#define PIN_SPI_MISO D9 +#define PIN_SPI_MOSI D10 +#define PIN_SPI_SCK D8 -#define PIN_WIRE_SDA (24) // change to use the correct pins if needed -#define PIN_WIRE_SCL (25) // change to use the correct pins if needed +static const uint8_t SS = SX126X_CS; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; - -// GPS L76KB -#define GPS_L76K +/* + * GPS + */ +// GPS L76K #ifdef GPS_L76K #define PIN_GPS_RX D6 #define PIN_GPS_TX D7 #define HAS_GPS 1 -#define GPS_BAUDRATE 9600 #define GPS_THREAD_INTERVAL 50 #define PIN_SERIAL1_RX PIN_GPS_TX #define PIN_SERIAL1_TX PIN_GPS_RX #define PIN_GPS_STANDBY D0 +#else +#define PIN_SERIAL1_RX (-1) +#define PIN_SERIAL1_TX (-1) #endif -// Battery +/* + * Battery + */ +#define BATTERY_PIN PIN_VBAT // P0.31: VBAT voltage divider +#define ADC_MULTIPLIER (3) // ... R17=1M, R18=510k +#define ADC_CTRL VBAT_ENABLE // P0.14: VBAT voltage divider +#define ADC_CTRL_ENABLED LOW // ... sink +#define EXT_CHRG_DETECT (23) // P0.17: Charge LED +#define EXT_CHRG_DETECT_VALUE LOW // ... BQ25101 ~CHG indicates charging +#define HICHG (22) // P0.13: BQ25101 ISET 100mA instead of 50mA -#define BAT_READ \ - 14 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is - // program pin 32 / or P0.31) -#define BATTERY_SENSE_RESOLUTION_BITS 10 -#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED -#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge +#define BATTERY_SENSE_RESOLUTION_BITS (10) -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_VBAT // PIN_A0 +/* + * Wire Interfaces + * Keep this section after potentially conflicting pin definitions + */ +#define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much +#define WIRE_INTERFACES_COUNT 1 -// ratio of voltage divider = 3.0 (R17=1M, R18=510k) -#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic +#if !defined(XIAO_BLE_LEGACY_PINOUT) && !defined(GPS_L76K) +// If D6 and D7 are free, I2C is probably the most versatile assignment +#define PIN_WIRE_SDA D6 +#define PIN_WIRE_SCL D7 +#else +// Internal LSM6DS3TR on XIAO nRF52840 Series +#define PIN_WIRE_SDA (17) +#define PIN_WIRE_SCL (16) +#endif + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +/* + * Buttons + * Keep this section after potentially conflicting pin definitions + * because D0 has multiple possible conflicts with various XIAO modules: + * - PIN_GPS_STANDBY on the L76K GNSS Module + * - DIO1 on the Wio-SX1262 - 30-pin board-to-board connector version + * - SX1262X CS on XIAO BLE legacy pinout + */ + +#if !defined(GPS_L76K) && !defined(SEEED_XIAO_WIO_BTB) && !defined(XIAO_BLE_LEGACY_PINOUT) +#define BUTTON_PIN D0 +#endif #ifdef __cplusplus } diff --git a/variants/station-g1/variant.h b/variants/station-g1/variant.h index 9a3c37b73..6c3a39261 100644 --- a/variants/station-g1/variant.h +++ b/variants/station-g1/variant.h @@ -6,9 +6,7 @@ #define I2C_SDA1 14 // Second i2c channel on external IO connector #define I2C_SCL1 15 // Second i2c channel on external IO connector -#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_PIN 36 // The middle button GPIO on the Nano G1 #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. // common pinout for their SX1262 vs RF95 modules - both can be enabled and we will probe at runtime for RF95 and if diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 14fbee6cf..04e305abb 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -9,7 +9,6 @@ upload_protocol = esptool build_flags = ${esp32s3_base.build_flags} -DT_DECK -DBOARD_HAS_PSRAM - -DMAX_THREADS=40 -DGPS_POWER_TOGGLE -Ivariants/t-deck @@ -25,11 +24,6 @@ extends = env:t-deck build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_I2C_KBD_TYPE=0x55 -D INPUTDRIVER_ENCODER_TYPE=3 -D INPUTDRIVER_ENCODER_LEFT=1 @@ -39,7 +33,7 @@ build_flags = -D INPUTDRIVER_ENCODER_BTN=0 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SDCARD - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 @@ -67,7 +61,9 @@ build_flags = ; -D USE_DOUBLE_BUFFER -D USE_PACKET_API -D MAP_FULL_REDRAW + -D CUSTOM_TOUCH_DRIVER lib_deps = ${env:t-deck.lib_deps} ${device-ui_base.lib_deps} + https://github.com/bitbank2/bb_captouch/archive/refs/tags/1.3.1.zip diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h index a21c786b3..9fa0018ec 100644 --- a/variants/t-deck/variant.h +++ b/variants/t-deck/variant.h @@ -1,7 +1,5 @@ #define TFT_CS 12 -#ifndef HAS_TFT // for TFT-UI the definitions are in device-ui -#define BUTTON_PIN 0 // ST7789 TFT LCD #define ST7789_CS TFT_CS @@ -24,7 +22,6 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness -#endif #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -34,10 +31,10 @@ #define USE_POWERSAVE #define SLEEP_TIME 120 -#ifndef HAS_TFT -#define BUTTON_PIN 0 -// #define BUTTON_NEED_PULLUP -#endif +#define TB_PRESS 0 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true + #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 diff --git a/variants/t-echo/variant.h b/variants/t-echo/variant.h index 38b7f4743..4f3a53ebf 100644 --- a/variants/t-echo/variant.h +++ b/variants/t-echo/variant.h @@ -61,8 +61,12 @@ extern "C" { * Buttons */ #define PIN_BUTTON1 (32 + 10) +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular GPIO #define PIN_BUTTON_TOUCH (0 + 11) // 0.11 is the soft touch button on T-Echo +#define BUTTON_TOUCH_ACTIVE_LOW true +#define BUTTON_TOUCH_ACTIVE_PULLUP true #define BUTTON_CLICK_MS 400 #define BUTTON_TOUCH_MS 200 @@ -139,6 +143,7 @@ External serial flash WP25R1635FZUIL0 // Not really an E22 but TTGO seems to be trying to clone that #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define TCXO_OPTIONAL // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) @@ -214,7 +219,7 @@ External serial flash WP25R1635FZUIL0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 #define ADC_MULTIPLIER (2.0F) -#define NO_EXT_GPIO 1 +// #define NO_EXT_GPIO 1 #define HAS_RTC 1 diff --git a/variants/tbeam-s3-core/variant.h b/variants/tbeam-s3-core/variant.h index fd7649265..70323ab1e 100644 --- a/variants/tbeam-s3-core/variant.h +++ b/variants/tbeam-s3-core/variant.h @@ -7,8 +7,6 @@ #define I2C_SCL 18 // For QMC6310 sensors and screens #define BUTTON_PIN 0 // The middle button GPIO on the T-Beam S3 -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. // #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit diff --git a/variants/tbeam/variant.h b/variants/tbeam/variant.h index 8771c20d2..5b521a2de 100644 --- a/variants/tbeam/variant.h +++ b/variants/tbeam/variant.h @@ -4,8 +4,8 @@ #define I2C_SCL 22 #define BUTTON_PIN 38 // The middle button GPIO on the T-Beam -// #define BUTTON_PIN_ALT 13 // Alternate GPIO for an external button if needed. Does anyone use this? It is not documented -// anywhere. +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP true #define EXT_NOTIFY_OUT 13 // Default pin to use for Ext Notify Module. #define LED_STATE_ON 0 // State when LED is lit diff --git a/variants/tlora_t3s3_epaper/platformio.ini b/variants/tlora_t3s3_epaper/platformio.ini index 957c37b95..0750b5bbb 100644 --- a/variants/tlora_t3s3_epaper/platformio.ini +++ b/variants/tlora_t3s3_epaper/platformio.ini @@ -31,7 +31,6 @@ build_flags = ${inkhud.build_flags} -I variants/tlora_t3s3_epaper -D TLORA_T3S3_EPAPER - -D MAX_THREADS=40 ; Required if used with WiFi lib_deps = ${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX ${esp32s3_base.lib_deps} \ No newline at end of file diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index 399d65b03..ef0f62b60 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -36,21 +36,16 @@ extends = env:unphone build_flags = ${env:unphone.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=21 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D HAS_SDCARD -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=6144 -D LV_CACHE_DEF_SIZE=2097152 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_BUILD_TEST=0 -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 diff --git a/variants/unphone/variant.h b/variants/unphone/variant.h index 9932d3c59..510fd86fd 100644 --- a/variants/unphone/variant.h +++ b/variants/unphone/variant.h @@ -60,9 +60,12 @@ #define LED_PIN 13 // the red part of the RGB LED #define LED_STATE_ON 0 // State when LED is lit -#define BUTTON_PIN 21 // Button 3 - square - top button in landscape mode -#define BUTTON_NEED_PULLUP // we do need a helping hand up -#define BUTTON_PIN_ALT 45 // Button 1 - triangle - bottom button in landscape mode +#define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode +#define BUTTON_PIN 0 // Circle button +#define BUTTON_NEED_PULLUP // we do need a helping hand up +#define CANCEL_BUTTON_PIN 45 // Button 1 - triangle - bottom button in landscape mode +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP true #define I2C_SDA 3 // I2C pins for this board #define I2C_SCL 4 diff --git a/variants/xiao_ble/platformio.ini b/variants/xiao_ble/platformio.ini deleted file mode 100644 index 6fa1dd611..000000000 --- a/variants/xiao_ble/platformio.ini +++ /dev/null @@ -1,13 +0,0 @@ -; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 -[env:xiao_ble] -extends = nrf52840_base -board = xiao_ble_sense -board_level = extra -build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 -D EBYTE_E22 -DEBYTE_E22_900M30S -DPRIVATE_HW -board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/xiao_ble> -lib_deps = - ${nrf52840_base.lib_deps} -debug_tool = jlink -; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) -;upload_protocol = jlink diff --git a/variants/xiao_ble/variant.cpp b/variants/xiao_ble/variant.cpp deleted file mode 100644 index 2c6c3e539..000000000 --- a/variants/xiao_ble/variant.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "variant.h" -#include "nrf.h" -#include "wiring_constants.h" -#include "wiring_digital.h" - -const uint32_t g_ADigitalPinMap[] = { - // D0 .. D13 - 2, // D0 is P0.02 (A0) - 3, // D1 is P0.03 (A1) - 28, // D2 is P0.28 (A2) - 29, // D3 is P0.29 (A3) - 4, // D4 is P0.04 (A4,SDA) - 5, // D5 is P0.05 (A5,SCL) - 43, // D6 is P1.11 (TX) - 44, // D7 is P1.12 (RX) - 45, // D8 is P1.13 (SCK) - 46, // D9 is P1.14 (MISO) - 47, // D10 is P1.15 (MOSI) - - // LEDs - 26, // D11 is P0.26 (LED RED) - 6, // D12 is P0.06 (LED BLUE) - 30, // D13 is P0.30 (LED GREEN) - 14, // D14 is P0.14 (READ_BAT) - - // LSM6DS3TR - 40, // D15 is P1.08 (6D_PWR) - 27, // D16 is P0.27 (6D_I2C_SCL) - 7, // D17 is P0.07 (6D_I2C_SDA) - 11, // D18 is P0.11 (6D_INT1) - - // MIC - 42, // 17,//42, // D19 is P1.10 (MIC_PWR) - 32, // 26,//32, // D20 is P1.00 (PDM_CLK) - 16, // 25,//16, // D21 is P0.16 (PDM_DATA) - - // BQ25100 - 13, // D22 is P0.13 (HICHG) - 17, // D23 is P0.17 (~CHG) - - // - 21, // D24 is P0.21 (QSPI_SCK) - 25, // D25 is P0.25 (QSPI_CSN) - 20, // D26 is P0.20 (QSPI_SIO_0 DI) - 24, // D27 is P0.24 (QSPI_SIO_1 DO) - 22, // D28 is P0.22 (QSPI_SIO_2 WP) - 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) - - // NFC - 9, // D30 is P0.09 (NFC1) - 10, // D31 is P0.10 (NFC2) - - // VBAT - 31, // D32 is P0.10 (VBAT) -}; \ No newline at end of file diff --git a/variants/xiao_ble/variant.h b/variants/xiao_ble/variant.h deleted file mode 100644 index d00f8be89..000000000 --- a/variants/xiao_ble/variant.h +++ /dev/null @@ -1,213 +0,0 @@ -#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ -#define _SEEED_XIAO_NRF52840_SENSE_H_ - -/** Master clock frequency */ -#define VARIANT_MCK (64000000ul) - -#define USE_LFXO // Board uses 32khz crystal for LF -// #define USE_LFRC // Board uses RC for LF - -/*---------------------------------------------------------------------------- - * Headers - *----------------------------------------------------------------------------*/ - -#include "WVariant.h" - -#ifdef __cplusplus -extern "C" { -#endif // __cplusplus - -#define PINS_COUNT (33) -#define NUM_DIGITAL_PINS (33) -#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference -#define NUM_ANALOG_OUTPUTS (0) - -// LEDs - -#define LED_RED 11 -#define LED_BLUE 12 -#define LED_GREEN 13 - -#define PIN_LED1 LED_GREEN -#define PIN_LED2 LED_BLUE -#define PIN_LED3 LED_RED - -#define PIN_LED PIN_LED1 -#define LED_PWR (PINS_COUNT) - -#define LED_BUILTIN PIN_LED - -#define LED_STATE_ON 1 // State when LED is lit - -/* - * Buttons - */ -#define PIN_BUTTON1 (PINS_COUNT) - -// Digital PINs -#define D0 (0ul) -#define D1 (1ul) -#define D2 (2ul) -#define D3 (3ul) -#define D4 (4ul) -#define D5 (5ul) -#define D6 (6ul) -#define D7 (7ul) -#define D8 (8ul) -#define D9 (9ul) -#define D10 (10ul) - -/* - * Analog pins - */ -#define PIN_A0 (0) -#define PIN_A1 (1) -#define PIN_A2 (2) -#define PIN_A3 (3) -#define PIN_A4 (4) -#define PIN_A5 (5) -#define PIN_VBAT (32) -#define VBAT_ENABLE (14) - -static const uint8_t A0 = PIN_A0; -static const uint8_t A1 = PIN_A1; -static const uint8_t A2 = PIN_A2; -static const uint8_t A3 = PIN_A3; -static const uint8_t A4 = PIN_A4; -static const uint8_t A5 = PIN_A5; -#define ADC_RESOLUTION 12 - -// Other pins -#define PIN_NFC1 (30) -#define PIN_NFC2 (31) - -/* - * Serial interfaces - */ -#define PIN_SERIAL1_RX (-1) // (7) -#define PIN_SERIAL1_TX (-1) // (6) - -#define PIN_SERIAL2_RX (-1) -#define PIN_SERIAL2_TX (-1) - -/* - * SPI Interfaces - */ -#define SPI_INTERFACES_COUNT 1 - -#define PIN_SPI_MISO (9) -#define PIN_SPI_MOSI (10) -#define PIN_SPI_SCK (8) - -static const uint8_t SS = D0; -static const uint8_t MOSI = PIN_SPI_MOSI; -static const uint8_t MISO = PIN_SPI_MISO; -static const uint8_t SCK = PIN_SPI_SCK; - -// supported modules list -#define USE_SX1262 - -// common pinouts for SX126X modules -#define SX126X_CS D0 -#define SX126X_DIO1 D1 -#define SX126X_BUSY D2 -#define SX126X_RESET D3 - -// ---------------------------------------------------------------- - -// E22 Tx/Rx control options: - -// 1. Let the E22 control Tx and Rx automagically via DIO2. - -// * The E22's TXEN and DIO2 pins are connected to each other, but not to the MCU. -// * The E22's RXEN pin *is* connected to the MCU. -// * E22_TXEN_CONNECTED_TO_DIO2 is defined so the logic in SX126XInterface.cpp handles this configuration correctly. - -#define SX126X_TXEN RADIOLIB_NC -#define SX126X_RXEN D7 - -// ------------------------------ OR ------------------------------ - -// 2. Control Tx and Rx manually. - -// * The E22's TXEN and RXEN pins are both connected to the MCU. - -// #define SX126X_TXEN D6 -// #define SX126X_RXEN D7 - -// ---------------------------------------------------------------- - -#ifdef EBYTE_E22 -// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch -// (which is the default for the sx1262interface code) -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 -#ifdef EBYTE_E22_900M30S -// 10dB PA gain and 30dB rated output; based on measurements from -// https://github.com/S5NC/EBYTE_ESP32-S3/blob/main/E22-900M30S%20power%20output%20testing.txt -#define REGULATORY_GAIN_LORA 7 -#define SX126X_MAX_POWER 22 -#endif -#ifdef EBYTE_E22_900M33S -// 25dB PA gain and 33dB rated output; based on TX Power Curve from E22-900M33S_UserManual_EN_v1.0.pdf -#define REGULATORY_GAIN_LORA 25 -#define SX126X_MAX_POWER 8 -#endif -#endif - -/* - * Wire Interfaces - */ -#define WIRE_INTERFACES_COUNT 1 // 2 - -#define PIN_WIRE_SDA (4) -#define PIN_WIRE_SCL (5) - -static const uint8_t SDA = PIN_WIRE_SDA; -static const uint8_t SCL = PIN_WIRE_SCL; - -#define PIN_LSM6DS3TR_C_POWER (15) -#define PIN_LSM6DS3TR_C_INT1 (18) - -// PDM Interfaces -// --------------- -#define PIN_PDM_PWR (19) -#define PIN_PDM_CLK (20) -#define PIN_PDM_DIN (21) - -// QSPI Pins -#define PIN_QSPI_SCK (24) -#define PIN_QSPI_CS (25) -#define PIN_QSPI_IO0 (26) -#define PIN_QSPI_IO1 (27) -#define PIN_QSPI_IO2 (28) -#define PIN_QSPI_IO3 (29) - -// On-board QSPI Flash -#define EXTERNAL_FLASH_DEVICES P25Q16H -#define EXTERNAL_FLASH_USE_QSPI - -// Battery - -#define BAT_READ \ - 14 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is - // program pin 32 / or P0.31) -#define BATTERY_SENSE_RESOLUTION_BITS 10 -#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED -#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge - -// The battery sense is hooked to pin A0 (5) -#define BATTERY_PIN PIN_VBAT // PIN_A0 - -// ratio of voltage divider = 3.0 (R17=1M, R18=510k) -#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic - -#ifdef __cplusplus -} -#endif - -/*---------------------------------------------------------------------------- - * Arduino objects - C++ only - *----------------------------------------------------------------------------*/ - -#endif diff --git a/variants/xiao_ble/xiao-ble-internal-format.uf2 b/variants/xiao_ble/xiao-ble-internal-format.uf2 deleted file mode 100644 index 59de2c68a..000000000 Binary files a/variants/xiao_ble/xiao-ble-internal-format.uf2 and /dev/null differ diff --git a/variants/xiao_ble/xiao_ble.sh b/variants/xiao_ble/xiao_ble.sh deleted file mode 100755 index 2f3cc5390..000000000 --- a/variants/xiao_ble/xiao_ble.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -# adapted from the script linked in this very helpful article: https://enzolombardi.net/low-power-bluetooth-advertising-with-xiao-ble-and-platformio-e8e7d0da80d2 - -# source: https://gist.githubusercontent.com/turing-complete-labs/b3105ee653782183c54b4fdbe18f411f/raw/d86779ba7702775d3b79781da63d85442acd9de6/xiao_ble.sh -# download the core for arduino from seeedstudio. Softdevice 7.3.0, linker and variants folder are what we need -curl https://files.seeedstudio.com/arduino/core/nRF52840/Arduino_core_nRF52840.tar.bz2 -o arduino.core.1.0.0.tar.bz2 -tar -xjf arduino.core.1.0.0.tar.bz2 -rm arduino.core.1.0.0.tar.bz2 - -# copy the needed files -cp 1.0.0/cores/nRF5/linker/nrf52840_s140_v7.ld ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/linker -cp -r 1.0.0/cores/nRF5/nordic/softdevice/s140_nrf52_7.3.0_API ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/nordic/softdevice - -rm -rf 1.0.0 -echo done! diff --git a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip b/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip deleted file mode 100644 index 40b966baf..000000000 Binary files a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip and /dev/null differ diff --git a/version.properties b/version.properties index b0e960697..3fe1aa385 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 -minor = 6 -build = 9 +minor = 7 +build = 1